diff --git a/src/renderer/components/SearchableBranchSelect/SearchableBranchSelect.tsx b/src/renderer/components/SearchableBranchSelect/SearchableBranchSelect.tsx index df54d6df..8ce981ae 100644 --- a/src/renderer/components/SearchableBranchSelect/SearchableBranchSelect.tsx +++ b/src/renderer/components/SearchableBranchSelect/SearchableBranchSelect.tsx @@ -32,6 +32,7 @@ export const SearchableBranchSelect: React.FC = ({ label, }) => { const [isOpen, setIsOpen] = useState(false); + const [openDirection, setOpenDirection] = useState<'up' | 'down'>('down'); const [searchTerm, setSearchTerm] = useState(''); const dropdownRef = useRef(null); const inputRef = useRef(null); @@ -50,6 +51,34 @@ export const SearchableBranchSelect: React.FC = ({ } }, [isOpen]); + useEffect(() => { + if (!isOpen || !dropdownRef.current) { + return; + } + + const updateDropdownDirection = () => { + if (!dropdownRef.current) return; + + const rect = dropdownRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const estimatedDropdownHeight = 250; + + setOpenDirection( + spaceBelow < estimatedDropdownHeight && spaceAbove > spaceBelow ? 'up' : 'down' + ); + }; + + updateDropdownDirection(); + window.addEventListener('resize', updateDropdownDirection); + window.addEventListener('scroll', updateDropdownDirection, true); + + return () => { + window.removeEventListener('resize', updateDropdownDirection); + window.removeEventListener('scroll', updateDropdownDirection, true); + }; + }, [isOpen]); + const filteredBranches = branches.filter((branch) => branch.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -99,7 +128,10 @@ export const SearchableBranchSelect: React.FC = ({ {isOpen && (
e.stopPropagation()} > {/* Search input */} diff --git a/tests/components/SearchableBranchSelect.test.tsx b/tests/components/SearchableBranchSelect.test.tsx index 81e8dfa5..8a5b756d 100644 --- a/tests/components/SearchableBranchSelect.test.tsx +++ b/tests/components/SearchableBranchSelect.test.tsx @@ -116,6 +116,40 @@ describe('SearchableBranchSelect Component', () => { expect(mockOnSelect).toHaveBeenCalledWith('develop'); }); + + it('opens upward when there is not enough viewport space below', () => { + const originalInnerHeight = window.innerHeight; + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; + + try { + Object.defineProperty(window, 'innerHeight', { value: 400, configurable: true }); + HTMLElement.prototype.getBoundingClientRect = vi.fn(() => ({ + top: 350, + bottom: 380, + left: 0, + right: 0, + width: 100, + height: 30, + x: 0, + y: 350, + toJSON: () => ({}), + })); + + render( + + ); + + fireEvent.click(screen.getByRole('button')); + + expect(screen.getByTestId('searchable-branch-select-menu')).toHaveClass('bottom-full'); + } finally { + Object.defineProperty(window, 'innerHeight', { + value: originalInnerHeight, + configurable: true, + }); + HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect; + } + }); }); describe('Empty State', () => {