From e32d39838576479dc9ce1cbd351634dbef7a1a7b Mon Sep 17 00:00:00 2001 From: Dawson Booth Date: Wed, 11 Aug 2021 22:53:22 -0500 Subject: [PATCH] #184 Remove virtualization if less than max height --- src/components/Dropdown/Dropdown.stories.tsx | 3 +- src/components/Dropdown/Dropdown.tsx | 287 ++++++++++++------ .../Dropdown/__tests__/Dropdown.test.tsx | 18 +- .../__snapshots__/Dropdown.test.tsx.snap | 120 +++----- 4 files changed, 254 insertions(+), 174 deletions(-) diff --git a/src/components/Dropdown/Dropdown.stories.tsx b/src/components/Dropdown/Dropdown.stories.tsx index 981e1a6e7..5dfbbf842 100644 --- a/src/components/Dropdown/Dropdown.stories.tsx +++ b/src/components/Dropdown/Dropdown.stories.tsx @@ -69,7 +69,8 @@ Basic.args = { variant: variants.fill, optionsVariant: variants.outline, valueVariant: variants.text, - numCities: 20000, + numCities: 200, + virtualizeOptions: true, }; const teaOptions = [ diff --git a/src/components/Dropdown/Dropdown.tsx b/src/components/Dropdown/Dropdown.tsx index 14ea14233..0cde281d6 100644 --- a/src/components/Dropdown/Dropdown.tsx +++ b/src/components/Dropdown/Dropdown.tsx @@ -82,12 +82,12 @@ const ValueItem = styled(Div)` `; const OptionsContainer = styled(Div)` - ${({ color, variant }: UsefulDropdownState) => ` + ${({ color, variant, isVirtual }: UsefulDropdownState & { isVirtual: boolean }) => ` background: white; position: absolute; top: 100%; left: 0px; - height: 10rem; + ${isVirtual ? 'height: 10rem;' : 'max-height: 10rem;'} overflow-y: auto; width: 15rem; ${ @@ -239,7 +239,7 @@ export interface DropdownProps { optionsVariant?: variants; valueVariant?: variants; - initialOptionCount?: number; + virtualizeOptions?: boolean; } const Dropdown = ({ @@ -292,7 +292,7 @@ const Dropdown = ({ valueVariant = variants.text, values = [], - initialOptionCount, + virtualizeOptions = true, }: DropdownProps): JSX.Element | null => { const { colors } = useTheme(); const defaultedColor = color || colors.grayDark; @@ -305,6 +305,12 @@ const Dropdown = ({ const [scrollIndex, setScrollIndex] = useState(0); + const [isVirtual, setIsVirtual] = useState(virtualizeOptions); // TODO: Update if the scroller div is smaller than the max-height + + useEffect(() => { + setIsVirtual(virtualizeOptions); + }, [virtualizeOptions]); + // Merge the default styled container prop and the placeholderProps object to get user styles const placeholderMergedProps = { StyledContainer: PlaceholderContainer, @@ -349,10 +355,34 @@ const Dropdown = ({ } setIsOpen(true); + + window.setTimeout(() => { + const focusedElement = document.activeElement; + + if (focusedElement && focusedElement.id === `${name}-dropdown-button`) { + const button = focusedElement.parentNode as HTMLElement | undefined; + const optionsContainer = button ? button.nextElementSibling : null; + + if (optionsContainer) { + if (isVirtual) { + const lowestContainer = optionsContainer.children[0]?.children[0]?.children[0]; + if (lowestContainer && lowestContainer.clientHeight < optionsContainer.clientHeight) { + setIsVirtual(false); + } + } else if ( + virtualizeOptions && + optionsContainer.scrollHeight > optionsContainer.clientHeight + ) { + setIsVirtual(true); + } + } + } + }, 0); + if (onFocus) { onFocus(); } - }, [onFocus, focusWithin, focusTimeoutId]); + }, [focusTimeoutId, focusWithin, onFocus, name, isVirtual, virtualizeOptions]); const handleSelect = useCallback( (clickedId: string | number) => { @@ -401,54 +431,94 @@ const Dropdown = ({ // to activeElement to after it is updated in the DOM window.setTimeout(() => { const focusedElement = document.activeElement; - switch (key) { - case 'Enter': - const match = focusedElement && focusedElement.id.match(`${name}-option-(.*)`); - if (match) { - handleSelect(match[1]); - } - break; - case 'ArrowUp': - if (focusedElement && focusedElement.id.match(`${name}-option-.*`)) { - const row = focusedElement.parentNode as HTMLElement | undefined; - const rowPrevSibling = row ? row.previousElementSibling : null; - if (rowPrevSibling) { - const toFocus = rowPrevSibling.children[0] as HTMLElement | undefined; - if (toFocus) { - toFocus.focus(); + + if (isVirtual) { + switch (key) { + case 'Enter': + const match = focusedElement && focusedElement.id.match(`${name}-option-(.*)`); + if (match) { + handleSelect(match[1]); + } + break; + case 'ArrowUp': + if (focusedElement && focusedElement.id.match(`${name}-option-.*`)) { + const row = focusedElement.parentNode as HTMLElement | undefined; + const rowPrevSibling = row ? row.previousElementSibling : null; + if (rowPrevSibling) { + const toFocus = rowPrevSibling.children[0] as HTMLElement | undefined; + if (toFocus) { + toFocus.focus(); + } } } - } - break; - case 'ArrowDown': - if (focusedElement && focusedElement.id === `${name}-dropdown-button`) { - const button = focusedElement.parentNode as HTMLElement | undefined; - // get parent before nextElementSibling because buttons are nested inside of skeletons - const optionsContainer = button ? button.nextElementSibling : null; - if (optionsContainer) { - const toFocus = optionsContainer.children[0]?.children[0]?.children[0] - ?.children[0] as HTMLElement | undefined; - if (toFocus) { - toFocus.focus(); + break; + case 'ArrowDown': + if (focusedElement && focusedElement.id === `${name}-dropdown-button`) { + const button = focusedElement.parentNode as HTMLElement | undefined; + // get parent before nextElementSibling because buttons are nested inside of skeletons + const optionsContainer = button ? button.nextElementSibling : null; + if (optionsContainer) { + const toFocus = optionsContainer.children[0]?.children[0]?.children[0] + ?.children[0] as HTMLElement | undefined; + if (toFocus) { + toFocus.focus(); + } + } + } else if (focusedElement && focusedElement.id.match(`${name}-option-.*`)) { + const row = focusedElement.parentNode as HTMLElement | undefined; + const rowNextSibling = row ? row.nextElementSibling : null; + if (rowNextSibling) { + const toFocus = rowNextSibling.children[0] as HTMLElement | undefined; + if (toFocus) { + toFocus.focus(); + } } } - } else if (focusedElement && focusedElement.id.match(`${name}-option-.*`)) { - const row = focusedElement.parentNode as HTMLElement | undefined; - const rowNextSibling = row ? row.nextElementSibling : null; - if (rowNextSibling) { - const toFocus = rowNextSibling.children[0] as HTMLElement | undefined; - if (toFocus) { - toFocus.focus(); + break; + default: + break; + } + } else { + switch (key) { + case 'Enter': + const match = focusedElement && focusedElement.id.match(`${name}-option-(.*)`); + if (match) { + handleSelect(match[1]); + } + break; + case 'ArrowUp': + if (focusedElement && focusedElement.id.match(`${name}-option-.*`)) { + const sibling = focusedElement.previousElementSibling as HTMLElement | null; + if (sibling) { + sibling.focus(); } } - } - break; - default: - break; + break; + case 'ArrowDown': + if (focusedElement && focusedElement.id === `${name}-dropdown-button`) { + const button = focusedElement.parentNode as HTMLElement | undefined; + // get parent before nextElementSibling because buttons are nested inside of skeletons + const optionsContainer = button ? button.nextElementSibling : null; + if (optionsContainer) { + const toFocus = optionsContainer.children[0] as HTMLElement | undefined; + if (toFocus) { + toFocus.focus(); + } + } + } else if (focusedElement && focusedElement.id.match(`${name}-option-.*`)) { + const sibling = focusedElement.nextElementSibling as HTMLElement | null; + if (sibling) { + sibling.focus(); + } + } + break; + default: + break; + } } }, 0); }, - [handleSelect, name], + [handleSelect, isVirtual, name], ); useEffect(() => { @@ -484,6 +554,7 @@ const Dropdown = ({ )), }), - [defaultedColor, optionsContainerProps, optionsContainerRef, optionsVariant], + [defaultedColor, optionsContainerProps, optionsContainerRef, optionsVariant, isVirtual], ); return ( @@ -561,48 +632,90 @@ const Dropdown = ({ {closeIcons} - {isOpen && ( - setScrollIndex(range.startIndex)} - initialTopMostItemIndex={rememberScrollPosition ? scrollIndex : 0} - initialItemCount={ - typeof window !== 'undefined' && window.document && window.document.createElement - ? initialOptionCount - : options.length - } - components={VirtuosoComponents as Components} - itemContent={(_index, option) => ( - handleSelect(option.id)} - tabIndex={-1} - color={defaultedColor} - variant={optionsVariant} - multi={multi} - selected={optionsHash[option.id].isSelected} - ref={optionItemRef} - role="option" - {...optionItemProps} - > - {multi && ( - - {optionsHash[option.id].isSelected && } - - )} - {option.optionValue} - - )} - /> - )} + {isOpen && + (isVirtual ? ( + setScrollIndex(range.startIndex)} + initialTopMostItemIndex={ + rememberScrollPosition && scrollIndex < options.length ? scrollIndex : 0 + } + initialItemCount={ + typeof window !== 'undefined' && window.document && window.document.createElement + ? undefined + : options.length + } + components={VirtuosoComponents as Components} + itemContent={(_index, option) => ( + handleSelect(option.id)} + tabIndex={-1} + color={defaultedColor} + variant={optionsVariant} + multi={multi} + selected={optionsHash[option.id].isSelected} + ref={optionItemRef} + role="option" + {...optionItemProps} + > + {multi && ( + + {optionsHash[option.id].isSelected && } + + )} + {option.optionValue} + + )} + /> + ) : ( + + {options.map(option => ( + handleSelect(option.id)} + tabIndex={-1} + color={defaultedColor} + variant={optionsVariant} + multi={multi} + selected={optionsHash[option.id].isSelected} + ref={optionItemRef} + role="option" + {...optionItemProps} + > + {multi && ( + + {optionsHash[option.id].isSelected && } + + )} + {option.optionValue} + + ))} + + ))} ); }; diff --git a/src/components/Dropdown/__tests__/Dropdown.test.tsx b/src/components/Dropdown/__tests__/Dropdown.test.tsx index 47d640a02..44a8dccf9 100644 --- a/src/components/Dropdown/__tests__/Dropdown.test.tsx +++ b/src/components/Dropdown/__tests__/Dropdown.test.tsx @@ -99,11 +99,7 @@ describe('Dropdown', () => { it('can focus dropdown and select option', async () => { const { container, getByText } = render( - , + , ); // TODO - Don't use id, see if we can use a more semantically meaningful element @@ -122,7 +118,7 @@ describe('Dropdown', () => { onSelect={mockedSelectHandler} multi options={pokeOptions} - initialOptionCount={pokeOptions.length} + virtualizeOptions={false} />, ); @@ -143,7 +139,7 @@ describe('Dropdown', () => { onSelect={mockedSelectHandler} multi options={pokeOptions} - initialOptionCount={pokeOptions.length} + virtualizeOptions={false} />, ); @@ -182,11 +178,7 @@ describe('Dropdown', () => { it('can use arrow keys and enter to navigate options', async () => { const { queryByText } = render( - , + , ); act(() => { screen.getByRole('button').focus(); @@ -263,7 +255,7 @@ describe('Dropdown', () => { options={pokeOptions} onSelect={() => {}} optionItemRef={ref} - initialOptionCount={pokeOptions.length} + virtualizeOptions={false} />, ); act(() => { diff --git a/src/components/Dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap b/src/components/Dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap index 1ccb0d855..4764bec7c 100644 --- a/src/components/Dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap +++ b/src/components/Dropdown/__tests__/__snapshots__/Dropdown.test.tsx.snap @@ -758,7 +758,7 @@ exports[`Dropdown deselects option when clicking on them twice when dropdown is position: absolute; top: 100%; left: 0px; - height: 10rem; + max-height: 10rem; overflow-y: auto; width: 15rem; border: 1px solid #252D34; @@ -880,81 +880,55 @@ exports[`Dropdown deselects option when clicking on them twice when dropdown is role="listbox" >