From ed4d3b80975cd77ce80e74f204216a8bf121e79f Mon Sep 17 00:00:00 2001 From: Brendan Drew Date: Mon, 26 Apr 2021 15:04:52 -0400 Subject: [PATCH] Switch to VariableSizeList --- __stories__/helpers/constants/options-data.ts | 4 ++-- __stories__/helpers/styled/index.ts | 14 ++++++++++++++ __stories__/index.stories.tsx | 19 ++++++++++++++++--- __stories__/types/index.d.ts | 1 + __tests__/MenuList.test.tsx | 4 ++-- src/Select.tsx | 18 ++++++++++-------- src/components/Menu/MenuList.tsx | 14 +++++++------- src/hooks/useMenuPositioner.ts | 5 ++--- 8 files changed, 54 insertions(+), 25 deletions(-) diff --git a/__stories__/helpers/constants/options-data.ts b/__stories__/helpers/constants/options-data.ts index 44a5604f..14f5983c 100644 --- a/__stories__/helpers/constants/options-data.ts +++ b/__stories__/helpers/constants/options-data.ts @@ -1,10 +1,10 @@ import type { CityOption, PackageOption } from '../../types'; export const PACKAGE_OPTIONS: PackageOption[] = [ - { id: 1, name: 'react' }, + { id: 1, name: 'react', description: 'React is a JavaScript library for creating user interfaces'}, { id: 2, name: 'react-dom' }, { id: 3, name: 'reactstrap' }, - { id: 4, name: 'react-scripts' }, + { id: 4, name: 'react-scripts', description: 'Configuration and scripts for Create React App.' }, { id: 5, name: 'react-window' } ]; diff --git a/__stories__/helpers/styled/index.ts b/__stories__/helpers/styled/index.ts index 7949da96..a52c0b11 100644 --- a/__stories__/helpers/styled/index.ts +++ b/__stories__/helpers/styled/index.ts @@ -387,10 +387,24 @@ export const OptionContainer = styled.div` flex-direction: row; `; +export const OptionContent = styled.div` + height: 100%; + display: flex; + flex-direction: column; +`; + export const OptionName = styled.span` color: #515151; font-size: 1em; font-weight: 600; margin-left: 1px; margin-bottom: 1.5px; +`; + +export const OptionDescription = styled.span` + color: #515151; + font-size: 1em; + font-weight: 300; + margin-left: 1px; + margin-bottom: 1.5px; `; \ No newline at end of file diff --git a/__stories__/index.stories.tsx b/__stories__/index.stories.tsx index 3b1b8029..d637feb8 100644 --- a/__stories__/index.stories.tsx +++ b/__stories__/index.stories.tsx @@ -38,7 +38,9 @@ import { CardBody, OtherSpan, OptionContainer, + OptionContent, OptionName, + OptionDescription, ReactSvg, ChevronDownSvg, MenuPortalElement, @@ -296,7 +298,7 @@ export const Styling = () => { const selectWrapperStyle = { marginTop: '1rem' }; const noteStyle = { fontSize: 'inherit', fontWeight: 700 }; - const menuItemSize = selectedOption?.value === ThemeEnum.LARGE_TEXT ? 44 : 35; + const getMenuItemSize = () => selectedOption?.value === ThemeEnum.LARGE_TEXT ? 44 : 35; const memoizedMarkupNode = useMemo(() => ( { isSearchable={false} options={THEME_OPTIONS} themeConfig={themeConfig} - menuItemSize={menuItemSize} + getMenuItemSize={getMenuItemSize} initialValue={THEME_OPTIONS[0]} onOptionChange={setSelectedOption} /> @@ -807,12 +809,22 @@ export const Advanced = () => { - {option.name} + + {option.name} + {option.description && {option.description}} + ), [getIsOptionDisabled] ); + const getMenuItemSize = useCallback( + (index: number): number => ( + PACKAGE_OPTIONS[index].description ? 70 : 35 + ), + [] + ); + const customCaretIcon = useCallback( ({ menuOpen }): ReactNode => ( { caretIcon={customCaretIcon} getOptionValue={getOptionValue} renderOptionLabel={renderOptionLabel} + getMenuItemSize={getMenuItemSize} getIsOptionDisabled={getIsOptionDisabled} /> diff --git a/__stories__/types/index.d.ts b/__stories__/types/index.d.ts index 5da72ee8..890342fd 100644 --- a/__stories__/types/index.d.ts +++ b/__stories__/types/index.d.ts @@ -14,4 +14,5 @@ export type CityOption = Readonly<{ export type PackageOption = Readonly<{ id: number; name: string; + description?: string; }>; \ No newline at end of file diff --git a/__tests__/MenuList.test.tsx b/__tests__/MenuList.test.tsx index e7860d10..f2352d80 100644 --- a/__tests__/MenuList.test.tsx +++ b/__tests__/MenuList.test.tsx @@ -37,11 +37,11 @@ const createMenuListProps = (menuOptions: MenuOption[] = []): MenuListProps => { width: '100%', renderOptionLabel, focusedOptionIndex, - fixedSizeListRef: null, + variableSizeListRef: null, itemKeySelector: undefined, height: MENU_MAX_HEIGHT_DEFAULT, loadingMsg: LOADING_MSG_DEFAULT, - itemSize: MENU_ITEM_SIZE_DEFAULT, + getItemSize: () => MENU_ITEM_SIZE_DEFAULT, noOptionsMsg: NO_OPTIONS_MSG_DEFAULT }; }; diff --git a/src/Select.tsx b/src/Select.tsx index 5b018bc1..22ee4153 100644 --- a/src/Select.tsx +++ b/src/Select.tsx @@ -45,7 +45,7 @@ import { import styled, { css, ThemeProvider } from 'styled-components'; import { Menu, Value, AriaLiveRegion, AutosizeInput, IndicatorIcons } from './components'; -import type { FixedSizeList } from 'react-window'; +import type { VariableSizeList } from 'react-window'; import type { DefaultTheme } from 'styled-components'; import type { Ref, @@ -111,7 +111,7 @@ export type SelectProps = Readonly<{ isDisabled?: boolean; placeholder?: string; menuWidth?: ReactText; - menuItemSize?: number; + getMenuItemSize?: (index: number) => number; isClearable?: boolean; lazyLoadMenu?: boolean; options?: OptionData[]; @@ -281,7 +281,7 @@ const Select = forwardRef(( loadingMsg = LOADING_MSG_DEFAULT, placeholder = PLACEHOLDER_DEFAULT, noOptionsMsg = NO_OPTIONS_MSG_DEFAULT, - menuItemSize = MENU_ITEM_SIZE_DEFAULT, + getMenuItemSize = () => MENU_ITEM_SIZE_DEFAULT, menuMaxHeight = MENU_MAX_HEIGHT_DEFAULT }, ref: Ref @@ -294,7 +294,7 @@ const Select = forwardRef(( const onOptionChangeIsFunc = useRef(isFunction(onOptionChange)); // DOM element refs - const listRef = useRef(null); + const listRef = useRef(null); const menuRef = useRef(null); const inputRef = useRef(null); const controlRef = useRef(null); @@ -343,15 +343,17 @@ const Select = forwardRef(( hideSelectedOptions ); + const menuItemSizes = menuOptions.map((_, i) => getMenuItemSize(i)) + const menuSize = menuItemSizes.reduce((a, b) => a + b, 0) + // Custom hook abstraction that handles calculating menuHeightCalc (defaults to menuMaxHeight) / handles executing callbacks/logic on menuOpen state change. const [menuStyleTop, menuHeightCalc] = useMenuPositioner( menuRef, controlRef, menuOpen, menuPosition, - menuItemSize, menuMaxHeight, - menuOptions.length, + menuSize, !!menuPortalTarget, onMenuOpen, onMenuClose, @@ -801,10 +803,10 @@ const Select = forwardRef(( isLoading={isLoading} menuTop={menuStyleTop} height={menuHeightCalc} - itemSize={menuItemSize} + getItemSize={getMenuItemSize} loadingMsg={loadingMsg} menuOptions={menuOptions} - fixedSizeListRef={listRef} + variableSizeListRef={listRef} noOptionsMsg={noOptionsMsg} selectOption={selectOption} direction={menuItemDirection} diff --git a/src/components/Menu/MenuList.tsx b/src/components/Menu/MenuList.tsx index a43cea03..1f8bd0f8 100644 --- a/src/components/Menu/MenuList.tsx +++ b/src/components/Menu/MenuList.tsx @@ -1,7 +1,7 @@ import React, { useMemo, Fragment } from 'react'; import Option from './Option'; import styled from 'styled-components'; -import { FixedSizeList } from 'react-window'; +import { VariableSizeList } from 'react-window'; import { isArrayWithLength } from '../../utils'; import type { MenuOption } from '../../Select'; @@ -11,7 +11,7 @@ import type { ItemData, RenderLabelCallback, SelectedOption } from '../../types' export type MenuListProps = Readonly<{ height: number; - itemSize: number; + getItemSize: (index: number) => number; loadingMsg: string; isLoading?: boolean; overscanCount?: number; @@ -22,7 +22,7 @@ export type MenuListProps = Readonly<{ noOptionsMsg: string | null; itemKeySelector?: ReactText; renderOptionLabel: RenderLabelCallback; - fixedSizeListRef: MutableRefObject; + variableSizeListRef: MutableRefObject; selectOption: (option: SelectedOption, isSelected?: boolean) => void; }>; @@ -38,7 +38,7 @@ const NoOptionsMsg = styled.div` const MenuList: FunctionComponent = ({ width, height, - itemSize, + getItemSize: itemSize, direction, isLoading, loadingMsg, @@ -47,7 +47,7 @@ const MenuList: FunctionComponent = ({ noOptionsMsg, overscanCount, itemKeySelector, - fixedSizeListRef, + variableSizeListRef: fixedSizeListRef, renderOptionLabel, focusedOptionIndex }) => { @@ -68,7 +68,7 @@ const MenuList: FunctionComponent = ({ return ( - = ({ itemCount={menuOptions.length} > {Option} - + {!isArrayWithLength(menuOptions) && noOptionsMsg && ( {noOptionsMsg} )} diff --git a/src/hooks/useMenuPositioner.ts b/src/hooks/useMenuPositioner.ts index dd58b185..6711dac6 100644 --- a/src/hooks/useMenuPositioner.ts +++ b/src/hooks/useMenuPositioner.ts @@ -21,9 +21,8 @@ const useMenuPositioner = ( controlRef: RefObject, menuOpen: boolean, menuPosition: MenuPositionEnum, - menuItemSize: number, menuHeightDefault: number, - menuOptionsLength: number, + menuSize: number, isMenuPortaled: boolean, onMenuOpen?: CallbackFunction, onMenuClose?: CallbackFunction, @@ -78,7 +77,7 @@ const useMenuPositioner = ( }, [menuRef, menuOpen, menuHeightDefault, scrollMenuIntoView, menuScrollDuration, onMenuCloseRef, onMenuOpenRef]); // Calculated menu height passed react-window; calculate MenuWrapper
'top' style prop if menu is positioned above control - const menuHeightCalc = Math.min(menuHeight, menuOptionsLength * menuItemSize); + const menuHeightCalc = Math.min(menuHeight, menuSize); const menuStyleTop = isMenuTopPosition ? calculateMenuTop(menuHeightCalc, menuRef.current, controlRef.current)