From d0ddc6468633289e5d9efa217d37d2ed457f2195 Mon Sep 17 00:00:00 2001 From: saurabhdaware Date: Mon, 13 Jan 2025 10:20:09 +0530 Subject: [PATCH 1/8] feat: add virtulization library --- packages/blade/package.json | 3 ++- .../src/components/ActionList/ActionList.tsx | 2 -- .../components/ActionList/ActionListBox.web.tsx | 15 +++++++++++++-- .../Dropdown/docs/DropdownWithSelect.stories.tsx | 1 - yarn.lock | 8 ++++++++ 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/blade/package.json b/packages/blade/package.json index a855cc54d9a..143e22576e0 100644 --- a/packages/blade/package.json +++ b/packages/blade/package.json @@ -148,7 +148,8 @@ "@mantine/core": "6.0.21", "@mantine/dates": "6.0.21", "@mantine/hooks": "6.0.21", - "dayjs": "1.11.10" + "dayjs": "1.11.10", + "react-window": "1.8.6" }, "devDependencies": { "http-server": "14.1.1", diff --git a/packages/blade/src/components/ActionList/ActionList.tsx b/packages/blade/src/components/ActionList/ActionList.tsx index 9c1ca3a1bf0..deb9188f0b0 100644 --- a/packages/blade/src/components/ActionList/ActionList.tsx +++ b/packages/blade/src/components/ActionList/ActionList.tsx @@ -38,8 +38,6 @@ const _ActionList = ({ children, testID, ...rest }: ActionListProps): React.Reac [children], ); - console.log({ actionListOptions }); - React.useEffect(() => { setOptions(actionListOptions); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/blade/src/components/ActionList/ActionListBox.web.tsx b/packages/blade/src/components/ActionList/ActionListBox.web.tsx index bb3c46ae3c9..67ce7cacacb 100644 --- a/packages/blade/src/components/ActionList/ActionListBox.web.tsx +++ b/packages/blade/src/components/ActionList/ActionListBox.web.tsx @@ -1,5 +1,6 @@ /* eslint-disable react/display-name */ import React from 'react'; +import { FixedSizeList as VirtualizedList } from 'react-window'; import { StyledListBoxWrapper } from './styles/StyledListBoxWrapper'; import type { SectionData } from './actionListUtils'; import { useBottomSheetContext } from '~components/BottomSheet/BottomSheetContext'; @@ -18,6 +19,7 @@ type ActionListBoxProps = { const _ActionListBox = React.forwardRef( ({ childrenWithId, actionListItemWrapperRole, isMultiSelectable, ...rest }, ref) => { + const items = React.Children.toArray(childrenWithId); // Convert children to an array const { isInBottomSheet } = useBottomSheetContext(); return ( @@ -30,12 +32,21 @@ const _ActionListBox = React.forwardRef( })} {...makeAnalyticsAttribute(rest)} > - {childrenWithId} + {/* {childrenWithId} */} + + {({ index, style }) => ( +
+ {items[index]} +
+ )} +
); }, ); -const ActionListBox = assignWithoutSideEffects(_ActionListBox, { displayName: 'ActionListBox' }); +const ActionListBox = assignWithoutSideEffects(React.memo(_ActionListBox), { + displayName: 'ActionListBox', +}); export { ActionListBox }; diff --git a/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx b/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx index 0e269758ebd..9ab8f84ba8b 100644 --- a/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx +++ b/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx @@ -476,7 +476,6 @@ export const InternalDropdownPerformance = (): React.ReactElement => { ⌘ + S} leading={} - description={fruit.description} key={fruit.name} title={fruit.name} value={fruit.name} diff --git a/yarn.lock b/yarn.lock index ce0d5d458e6..c554a76b210 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24189,6 +24189,14 @@ react-virtualized-auto-sizer@1.0.7: resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz#bfb8414698ad1597912473de3e2e5f82180c1195" integrity sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA== +react-window@1.8.6: + version "1.8.6" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" + integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react-window@1.8.7: version "1.8.7" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.7.tgz#5e9fd0d23f48f432d7022cdb327219353a15f0d4" From de29640ea1005b9e004ca19ceb9b2d8f60a8d9ce Mon Sep 17 00:00:00 2001 From: saurabhdaware Date: Mon, 13 Jan 2025 19:25:20 +0530 Subject: [PATCH 2/8] feat: initial virtulization code --- packages/blade/package.json | 1 + .../src/components/ActionList/ActionList.tsx | 12 ++- .../ActionList/ActionListBox.web.tsx | 74 +++++++++++++++++-- .../components/ActionList/docs/propsTable.tsx | 1 + .../styles/getBaseListBoxWrapperStyles.ts | 12 ++- .../BaseMenu/BaseMenuItem/BaseMenuItem.tsx | 9 +-- .../StyledMenuItemContainer.web.tsx | 5 +- .../BaseMenuItem/getBaseMenuItemStyles.ts | 5 +- .../BaseMenu/BaseMenuItem/tokens.ts | 38 ++++++++++ .../docs/DropdownWithSelect.stories.tsx | 4 +- yarn.lock | 7 ++ 11 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 packages/blade/src/components/BaseMenu/BaseMenuItem/tokens.ts diff --git a/packages/blade/package.json b/packages/blade/package.json index 143e22576e0..0543e1392ea 100644 --- a/packages/blade/package.json +++ b/packages/blade/package.json @@ -223,6 +223,7 @@ "@types/styled-components-react-native": "5.1.3", "@types/tinycolor2": "1.4.3", "@types/react-router-dom": "5.3.3", + "@types/react-window": "1.8.8", "@types/storybook-react-router": "1.0.5", "any-leaf": "1.2.2", "args-parser": "1.3.0", diff --git a/packages/blade/src/components/ActionList/ActionList.tsx b/packages/blade/src/components/ActionList/ActionList.tsx index deb9188f0b0..05ee740e642 100644 --- a/packages/blade/src/components/ActionList/ActionList.tsx +++ b/packages/blade/src/components/ActionList/ActionList.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { getActionListContainerRole, getActionListItemWrapperRole } from './getA11yRoles'; import { getActionListProperties } from './actionListUtils'; -import { ActionListBox } from './ActionListBox'; +import { ActionListBox as ActionListNormalBox, ActionListVirtualizedBox } from './ActionListBox'; import { componentIds } from './componentIds'; import { ActionListNoResults } from './ActionListNoResults'; import { useDropdown } from '~components/Dropdown/useDropdown'; @@ -17,10 +17,16 @@ import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; type ActionListProps = { children: React.ReactNode[]; + isVirtualized?: boolean; } & TestID & DataAnalyticsAttribute; -const _ActionList = ({ children, testID, ...rest }: ActionListProps): React.ReactElement => { +const _ActionList = ({ + children, + testID, + isVirtualized, + ...rest +}: ActionListProps): React.ReactElement => { const { setOptions, actionListItemRef, @@ -31,6 +37,8 @@ const _ActionList = ({ children, testID, ...rest }: ActionListProps): React.Reac filteredValues, } = useDropdown(); + const ActionListBox = isVirtualized ? ActionListVirtualizedBox : ActionListNormalBox; + const { isInBottomSheet } = useBottomSheetContext(); const { sectionData, childrenWithId, actionListOptions } = React.useMemo( diff --git a/packages/blade/src/components/ActionList/ActionListBox.web.tsx b/packages/blade/src/components/ActionList/ActionListBox.web.tsx index 67ce7cacacb..4d075adaa86 100644 --- a/packages/blade/src/components/ActionList/ActionListBox.web.tsx +++ b/packages/blade/src/components/ActionList/ActionListBox.web.tsx @@ -3,11 +3,16 @@ import React from 'react'; import { FixedSizeList as VirtualizedList } from 'react-window'; import { StyledListBoxWrapper } from './styles/StyledListBoxWrapper'; import type { SectionData } from './actionListUtils'; +import { actionListMaxHeight, getActionListPadding } from './styles/getBaseListBoxWrapperStyles'; import { useBottomSheetContext } from '~components/BottomSheet/BottomSheetContext'; import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects'; import { makeAccessible } from '~utils/makeAccessible'; import type { DataAnalyticsAttribute } from '~utils/types'; import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; +import { useIsMobile } from '~utils/useIsMobile'; +import { getItemHeight } from '~components/BaseMenu/BaseMenuItem/tokens'; +import { useTheme } from '~utils'; +import type { Theme } from '~components/BladeProvider'; type ActionListBoxProps = { childrenWithId?: React.ReactNode[] | null; @@ -18,9 +23,64 @@ type ActionListBoxProps = { } & DataAnalyticsAttribute; const _ActionListBox = React.forwardRef( + ({ childrenWithId, actionListItemWrapperRole, isMultiSelectable, ...rest }, ref) => { + const { isInBottomSheet } = useBottomSheetContext(); + + return ( + + {childrenWithId} + + ); + }, +); + +const ActionListBox = assignWithoutSideEffects(React.memo(_ActionListBox), { + displayName: 'ActionListBox', +}); + +const getVirtualItemParams = ({ + theme, + isMobile, +}: { + theme: Theme; + isMobile: boolean; +}): { + itemHeight: number; + actionListBoxHeight: number; +} => { + const itemHeightResponsive = getItemHeight(theme); + const actionListPadding = getActionListPadding(theme); + const actionListBoxHeight = actionListMaxHeight - actionListPadding * 2; + + return { + itemHeight: isMobile + ? itemHeightResponsive.itemHeightMobile + : itemHeightResponsive.itemHeightDesktop, + actionListBoxHeight, + }; +}; + +const _ActionListVirtualizedBox = React.forwardRef( ({ childrenWithId, actionListItemWrapperRole, isMultiSelectable, ...rest }, ref) => { const items = React.Children.toArray(childrenWithId); // Convert children to an array const { isInBottomSheet } = useBottomSheetContext(); + const isMobile = useIsMobile(); + const { theme } = useTheme(); + const { itemHeight, actionListBoxHeight } = React.useMemo( + () => getVirtualItemParams({ theme, isMobile }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [theme.name, isMobile], + ); + + console.log({ itemHeight, actionListBoxHeight }); return ( ( })} {...makeAnalyticsAttribute(rest)} > - {/* {childrenWithId} */} - + {({ index, style }) => (
{items[index]} @@ -45,8 +109,8 @@ const _ActionListBox = React.forwardRef( }, ); -const ActionListBox = assignWithoutSideEffects(React.memo(_ActionListBox), { - displayName: 'ActionListBox', +const ActionListVirtualizedBox = assignWithoutSideEffects(React.memo(_ActionListVirtualizedBox), { + displayName: 'ActionListVirtualizedBox', }); -export { ActionListBox }; +export { ActionListBox, ActionListVirtualizedBox }; diff --git a/packages/blade/src/components/ActionList/docs/propsTable.tsx b/packages/blade/src/components/ActionList/docs/propsTable.tsx index 903810fb716..256ad8e4774 100644 --- a/packages/blade/src/components/ActionList/docs/propsTable.tsx +++ b/packages/blade/src/components/ActionList/docs/propsTable.tsx @@ -19,6 +19,7 @@ const actionListPropsTables: { <ActionListSection[] /> ), + isVirtualized: 'boolean', }, ActionListItem: { title: 'string', diff --git a/packages/blade/src/components/ActionList/styles/getBaseListBoxWrapperStyles.ts b/packages/blade/src/components/ActionList/styles/getBaseListBoxWrapperStyles.ts index da547f32660..0eeec3f01f1 100644 --- a/packages/blade/src/components/ActionList/styles/getBaseListBoxWrapperStyles.ts +++ b/packages/blade/src/components/ActionList/styles/getBaseListBoxWrapperStyles.ts @@ -3,14 +3,20 @@ import type { Theme } from '~components/BladeProvider'; import { makeSize } from '~utils/makeSize'; import { size } from '~tokens/global'; +const actionListMaxHeight = size[300]; + +const getActionListPadding = (theme: Theme): number => { + return theme.spacing[3]; +}; + const getBaseListBoxWrapperStyles = (props: { theme: Theme; isInBottomSheet: boolean; }): CSSObject => { return { - maxHeight: props.isInBottomSheet ? undefined : makeSize(size[300]), - padding: props.isInBottomSheet ? undefined : makeSize(props.theme.spacing[3]), + maxHeight: props.isInBottomSheet ? undefined : makeSize(actionListMaxHeight), + padding: props.isInBottomSheet ? undefined : makeSize(getActionListPadding(props.theme)), }; }; -export { getBaseListBoxWrapperStyles }; +export { getBaseListBoxWrapperStyles, actionListMaxHeight, getActionListPadding }; diff --git a/packages/blade/src/components/BaseMenu/BaseMenuItem/BaseMenuItem.tsx b/packages/blade/src/components/BaseMenu/BaseMenuItem/BaseMenuItem.tsx index 2dc7daee43f..d598c2dfd65 100644 --- a/packages/blade/src/components/BaseMenu/BaseMenuItem/BaseMenuItem.tsx +++ b/packages/blade/src/components/BaseMenu/BaseMenuItem/BaseMenuItem.tsx @@ -2,14 +2,14 @@ import React from 'react'; import type { BaseMenuItemProps } from '../types'; import { BaseMenuItemContext } from '../BaseMenuContext'; import { StyledMenuItemContainer } from './StyledMenuItemContainer'; +import { itemFirstRowHeight } from './tokens'; import { Box } from '~components/Box'; import { getTextProps, Text } from '~components/Typography'; -import { size } from '~tokens/global'; -import { makeSize } from '~utils'; import { makeAccessible } from '~utils/makeAccessible'; import type { BladeElementRef } from '~utils/types'; import { BaseText } from '~components/Typography/BaseText'; import { useTruncationTitle } from '~utils/useTruncationTitle'; +import { makeSize } from '~utils'; const menuItemTitleColor = { negative: { @@ -25,7 +25,6 @@ const menuItemDescriptionColor = { } as const; // This is the height of item excluding the description to make sure description comes at the bottom and other first row items are center aligned -const itemFirstRowHeight = makeSize(size[20]); const _BaseMenuItem: React.ForwardRefRenderFunction = ( { @@ -75,7 +74,7 @@ const _BaseMenuItem: React.ForwardRefRenderFunction {leading} @@ -89,7 +88,7 @@ const _BaseMenuItem: React.ForwardRefRenderFunction ((props) => { return { ...getBaseMenuItemStyles({ theme: props.theme }), - padding: makeSize(props.theme.spacing[2]), + padding: makeSize(getItemPadding(props.theme).itemPaddingMobile), display: props.isVisible ? 'flex' : 'none', [`@media ${getMediaQuery({ min: props.theme.breakpoints.m })}`]: { - padding: makeSize(props.theme.spacing[3]), + padding: makeSize(getItemPadding(props.theme).itemPaddingDesktop), }, '&:hover:not([aria-disabled=true]), &[aria-expanded="true"]': { backgroundColor: diff --git a/packages/blade/src/components/BaseMenu/BaseMenuItem/getBaseMenuItemStyles.ts b/packages/blade/src/components/BaseMenu/BaseMenuItem/getBaseMenuItemStyles.ts index edef57bd3b4..8ce094cbd54 100644 --- a/packages/blade/src/components/BaseMenu/BaseMenuItem/getBaseMenuItemStyles.ts +++ b/packages/blade/src/components/BaseMenu/BaseMenuItem/getBaseMenuItemStyles.ts @@ -1,4 +1,5 @@ import type { CSSObject } from 'styled-components'; +import { getItemMargin } from './tokens'; import type { Theme } from '~components/BladeProvider'; import { isReactNative, makeBorderSize } from '~utils'; import { makeSize } from '~utils/makeSize'; @@ -11,8 +12,8 @@ const getBaseMenuItemStyles = (props: { theme: Theme }): CSSObject => { textAlign: isReactNative() ? undefined : 'left', backgroundColor: 'transparent', borderRadius: makeSize(props.theme.border.radius.medium), - marginTop: makeSize(props.theme.spacing[1]), - marginBottom: makeSize(props.theme.spacing[1]), + marginTop: makeSize(getItemMargin(props.theme)), + marginBottom: makeSize(getItemMargin(props.theme)), textDecoration: 'none', cursor: 'pointer', width: '100%', diff --git a/packages/blade/src/components/BaseMenu/BaseMenuItem/tokens.ts b/packages/blade/src/components/BaseMenu/BaseMenuItem/tokens.ts new file mode 100644 index 00000000000..b5f25d3444e --- /dev/null +++ b/packages/blade/src/components/BaseMenu/BaseMenuItem/tokens.ts @@ -0,0 +1,38 @@ +import type { Theme } from '~components/BladeProvider'; +import { size } from '~tokens/global'; + +const itemFirstRowHeight = size[20]; + +const getItemPadding = ( + theme: Theme, +): { + itemPaddingMobile: 4; + itemPaddingDesktop: 8; +} => { + return { + itemPaddingMobile: theme.spacing[2], + itemPaddingDesktop: theme.spacing[3], + }; +}; + +const getItemMargin = (theme: Theme): number => { + return theme.spacing[1]; +}; + +const getItemHeight = ( + theme: Theme, +): { + itemHeightMobile: number; + itemHeightDesktop: number; +} => { + return { + itemHeightMobile: + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + itemFirstRowHeight + getItemPadding(theme).itemPaddingMobile * 2 + getItemMargin(theme) * 2, + itemHeightDesktop: + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + itemFirstRowHeight + getItemPadding(theme).itemPaddingDesktop * 2 + getItemMargin(theme) * 2, + }; +}; + +export { itemFirstRowHeight, getItemPadding, getItemMargin, getItemHeight }; diff --git a/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx b/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx index 9ab8f84ba8b..1ee8bfbd127 100644 --- a/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx +++ b/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx @@ -381,7 +381,7 @@ export const InternalSectionListPerformance = (): React.ReactElement => { - + @@ -466,7 +466,7 @@ export const InternalDropdownPerformance = (): React.ReactElement => { - + {fruits.map((fruit) => { if (typeof fruit === 'string') { return ; diff --git a/yarn.lock b/yarn.lock index c554a76b210..add86c2ffa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7618,6 +7618,13 @@ dependencies: "@types/react" "*" +"@types/react-window@1.8.8": + version "1.8.8" + resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.8.tgz#c20645414d142364fbe735818e1c1e0a145696e3" + integrity sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@18.2.24", "@types/react@>=16", "@types/react@>=16.0.0", "@types/react@^17": version "18.2.24" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.24.tgz#3c7d68c02e0205a472f04abe4a0c1df35d995c05" From ab9f347b04eb58a4b1ad30abd5f227a384c0f9ba Mon Sep 17 00:00:00 2001 From: saurabhdaware Date: Tue, 14 Jan 2025 09:43:46 +0530 Subject: [PATCH 3/8] fix: get autocomplete working with virtulization --- .../ActionList/ActionListBox.web.tsx | 24 +++++++++++++------ .../components/ActionList/ActionListItem.tsx | 13 ++++++++-- .../docs/DropdownWithSelect.stories.tsx | 4 ++-- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/blade/src/components/ActionList/ActionListBox.web.tsx b/packages/blade/src/components/ActionList/ActionListBox.web.tsx index 4d075adaa86..d6ada925772 100644 --- a/packages/blade/src/components/ActionList/ActionListBox.web.tsx +++ b/packages/blade/src/components/ActionList/ActionListBox.web.tsx @@ -13,6 +13,8 @@ import { useIsMobile } from '~utils/useIsMobile'; import { getItemHeight } from '~components/BaseMenu/BaseMenuItem/tokens'; import { useTheme } from '~utils'; import type { Theme } from '~components/BladeProvider'; +import { useDropdown } from '~components/Dropdown/useDropdown'; +import { dropdownComponentIds } from '~components/Dropdown/dropdownComponentIds'; type ActionListBoxProps = { childrenWithId?: React.ReactNode[] | null; @@ -72,6 +74,11 @@ const _ActionListVirtualizedBox = React.forwardRef { const items = React.Children.toArray(childrenWithId); // Convert children to an array const { isInBottomSheet } = useBottomSheetContext(); + const { filteredValues, hasAutoCompleteInBottomSheetHeader, dropdownTriggerer } = useDropdown(); + const hasAutoComplete = + hasAutoCompleteInBottomSheetHeader || + dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete; + const isMobile = useIsMobile(); const { theme } = useTheme(); const { itemHeight, actionListBoxHeight } = React.useMemo( @@ -80,7 +87,11 @@ const _ActionListVirtualizedBox = React.forwardRef + hasAutoComplete && filteredValues ? filteredValues.includes(itemValue) : true; + + // const filteredItems = filteredValues.map((value) => items.find((item) => item.props.value === value)); + const filteredItems = items.filter((item) => isVisible(item.props.value)); return ( - {({ index, style }) => ( -
- {items[index]} -
- )} + {({ index, style, data }) => + React.cloneElement(data[index], { _style: style, key: `virtual-item-${index}` }) + }
); diff --git a/packages/blade/src/components/ActionList/ActionListItem.tsx b/packages/blade/src/components/ActionList/ActionListItem.tsx index 7e6ccb4f743..1f1606e528a 100644 --- a/packages/blade/src/components/ActionList/ActionListItem.tsx +++ b/packages/blade/src/components/ActionList/ActionListItem.tsx @@ -87,6 +87,13 @@ type ActionListItemProps = { * @private */ _index?: number; + + /** + * Internal prop used in virtulization. It can be removed / changed without notice so do not use + * + * @private + */ + _style?: React.CSSProperties; } & TestID & DataAnalyticsAttribute; @@ -348,10 +355,11 @@ const _ActionListItem = (props: ActionListItemProps): React.ReactElement => { } }, [props.intent, dropdownTriggerer]); + const isVisible = hasAutoComplete && filteredValues ? filteredValues.includes(props.value) : true; + return ( - // We use this context to change the color of subcomponents like ActionListItemIcon, ActionListItemText, etc { selectionType={selectionType} color={props.intent} isKeydownPressed={isKeydownPressed} + style={props._style} /> ); }; diff --git a/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx b/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx index 1ee8bfbd127..eaaf242c3c6 100644 --- a/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx +++ b/packages/blade/src/components/Dropdown/docs/DropdownWithSelect.stories.tsx @@ -16,7 +16,7 @@ import { } from './stories'; import { Sandbox } from '~utils/storybook/Sandbox'; -import { SelectInput } from '~components/Input/DropdownInputTriggers'; +import { AutoComplete, SelectInput } from '~components/Input/DropdownInputTriggers'; import { ActionList, ActionListItem, @@ -464,7 +464,7 @@ export const InternalDropdownPerformance = (): React.ReactElement => { return ( - + {fruits.map((fruit) => { From b9b4a066946063aa6dbe0b2f03fb8e480188eecb Mon Sep 17 00:00:00 2001 From: saurabhdaware Date: Tue, 14 Jan 2025 10:53:36 +0530 Subject: [PATCH 4/8] fix: actionlist rerenders --- .../ActionList/ActionListBox.web.tsx | 79 ++++++++++++++----- .../components/ActionList/ActionListItem.tsx | 8 -- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/packages/blade/src/components/ActionList/ActionListBox.web.tsx b/packages/blade/src/components/ActionList/ActionListBox.web.tsx index d6ada925772..68523cf9eac 100644 --- a/packages/blade/src/components/ActionList/ActionListBox.web.tsx +++ b/packages/blade/src/components/ActionList/ActionListBox.web.tsx @@ -70,14 +70,53 @@ const getVirtualItemParams = ({ }; }; +const useFilteredItems = ( + children: React.ReactNode[], +): { + itemData: React.ReactNode[]; + itemCount: number; +} => { + const childrenArray = React.Children.toArray(children); // Convert children to an array + + const { filteredValues, hasAutoCompleteInBottomSheetHeader, dropdownTriggerer } = useDropdown(); + + const items = React.useMemo(() => { + const hasAutoComplete = + hasAutoCompleteInBottomSheetHeader || + dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete; + + if (!hasAutoComplete) { + return childrenArray; + } + + // @ts-expect-error: props does exist + const filteredItems = childrenArray.filter((item) => filteredValues.includes(item.props.value)); + return filteredItems; + }, [filteredValues, hasAutoCompleteInBottomSheetHeader, dropdownTriggerer, childrenArray]); + + return { + itemData: items, + itemCount: items.length, + }; +}; + +const VirtualListItem = ({ + index, + style, + data, +}: { + index: number; + style: React.CSSProperties; + data: React.ReactNode[]; +}): React.ReactElement => { + return
{data[index]}
; +}; + const _ActionListVirtualizedBox = React.forwardRef( ({ childrenWithId, actionListItemWrapperRole, isMultiSelectable, ...rest }, ref) => { const items = React.Children.toArray(childrenWithId); // Convert children to an array const { isInBottomSheet } = useBottomSheetContext(); - const { filteredValues, hasAutoCompleteInBottomSheetHeader, dropdownTriggerer } = useDropdown(); - const hasAutoComplete = - hasAutoCompleteInBottomSheetHeader || - dropdownTriggerer === dropdownComponentIds.triggers.AutoComplete; + const { itemData, itemCount } = useFilteredItems(items); const isMobile = useIsMobile(); const { theme } = useTheme(); @@ -87,12 +126,6 @@ const _ActionListVirtualizedBox = React.forwardRef - hasAutoComplete && filteredValues ? filteredValues.includes(itemValue) : true; - - // const filteredItems = filteredValues.map((value) => items.find((item) => item.props.value === value)); - const filteredItems = items.filter((item) => isVisible(item.props.value)); - return ( - - {({ index, style, data }) => - React.cloneElement(data[index], { _style: style, key: `virtual-item-${index}` }) - } - + {itemCount < 30 ? ( + childrenWithId + ) : ( + itemData[index]?.props.value} + > + {VirtualListItem} + + )} ); }, diff --git a/packages/blade/src/components/ActionList/ActionListItem.tsx b/packages/blade/src/components/ActionList/ActionListItem.tsx index 1f1606e528a..1c34c504b0a 100644 --- a/packages/blade/src/components/ActionList/ActionListItem.tsx +++ b/packages/blade/src/components/ActionList/ActionListItem.tsx @@ -87,13 +87,6 @@ type ActionListItemProps = { * @private */ _index?: number; - - /** - * Internal prop used in virtulization. It can be removed / changed without notice so do not use - * - * @private - */ - _style?: React.CSSProperties; } & TestID & DataAnalyticsAttribute; @@ -416,7 +409,6 @@ const _ActionListItem = (props: ActionListItemProps): React.ReactElement => { selectionType={selectionType} color={props.intent} isKeydownPressed={isKeydownPressed} - style={props._style} /> ); }; From ae3edc1f664149342af3ab86c680837e212aa4f8 Mon Sep 17 00:00:00 2001 From: saurabhdaware Date: Tue, 14 Jan 2025 11:03:00 +0530 Subject: [PATCH 5/8] fix: ts --- .../blade/src/components/ActionList/ActionListBox.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blade/src/components/ActionList/ActionListBox.native.tsx b/packages/blade/src/components/ActionList/ActionListBox.native.tsx index 8b6782268ab..22660e3037a 100644 --- a/packages/blade/src/components/ActionList/ActionListBox.native.tsx +++ b/packages/blade/src/components/ActionList/ActionListBox.native.tsx @@ -74,4 +74,4 @@ const _ActionListBox = React.forwardRef( const ActionListBox = assignWithoutSideEffects(_ActionListBox, { displayName: 'ActionListBox' }); -export { ActionListBox }; +export { ActionListBox, ActionListBox as ActionListVirtualizedBox }; From 3fe36fa3f6c4a2440ea5165380145699bb2f6785 Mon Sep 17 00:00:00 2001 From: Saurabh Daware Date: Tue, 14 Jan 2025 14:55:41 +0530 Subject: [PATCH 6/8] Create tidy-lies-confess.md --- .changeset/tidy-lies-confess.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/tidy-lies-confess.md diff --git a/.changeset/tidy-lies-confess.md b/.changeset/tidy-lies-confess.md new file mode 100644 index 00000000000..d831b05903c --- /dev/null +++ b/.changeset/tidy-lies-confess.md @@ -0,0 +1,14 @@ +--- +"@razorpay/blade": minor +--- + +feat(ActionList): add Virtualization in ActionList + +```jsx + + +``` + +> **Note** +> +> Current version only supports virtulization of fixed height list where items do not have descriptions. We'll be adding support for dynamic height lists in future versions From a44cadd69cea247e6c9d4e37ff533817960663b6 Mon Sep 17 00:00:00 2001 From: Saurabh Daware Date: Tue, 14 Jan 2025 14:56:21 +0530 Subject: [PATCH 7/8] Update tidy-lies-confess.md --- .changeset/tidy-lies-confess.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tidy-lies-confess.md b/.changeset/tidy-lies-confess.md index d831b05903c..5c392c38001 100644 --- a/.changeset/tidy-lies-confess.md +++ b/.changeset/tidy-lies-confess.md @@ -9,6 +9,6 @@ feat(ActionList): add Virtualization in ActionList
``` -> **Note** +> [!NOTE] > > Current version only supports virtulization of fixed height list where items do not have descriptions. We'll be adding support for dynamic height lists in future versions From db19c891df5bfec948c88ccc2558287d186aca74 Mon Sep 17 00:00:00 2001 From: saurabhdaware Date: Wed, 15 Jan 2025 09:45:13 +0530 Subject: [PATCH 8/8] feat: resolve comments --- packages/blade/package.json | 2 +- .../blade/src/components/ActionList/ActionListBox.web.tsx | 8 +++++++- .../blade/src/components/ActionList/docs/propsTable.tsx | 6 +++++- yarn.lock | 8 ++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/blade/package.json b/packages/blade/package.json index 0543e1392ea..6377a41e10e 100644 --- a/packages/blade/package.json +++ b/packages/blade/package.json @@ -149,7 +149,7 @@ "@mantine/dates": "6.0.21", "@mantine/hooks": "6.0.21", "dayjs": "1.11.10", - "react-window": "1.8.6" + "react-window": "1.8.11" }, "devDependencies": { "http-server": "14.1.1", diff --git a/packages/blade/src/components/ActionList/ActionListBox.web.tsx b/packages/blade/src/components/ActionList/ActionListBox.web.tsx index 68523cf9eac..31bec4c3d2f 100644 --- a/packages/blade/src/components/ActionList/ActionListBox.web.tsx +++ b/packages/blade/src/components/ActionList/ActionListBox.web.tsx @@ -48,6 +48,9 @@ const ActionListBox = assignWithoutSideEffects(React.memo(_ActionListBox), { displayName: 'ActionListBox', }); +/** + * Returns the height of item and height of container based on theme and device + */ const getVirtualItemParams = ({ theme, isMobile, @@ -70,6 +73,9 @@ const getVirtualItemParams = ({ }; }; +/** + * Takes the children (ActionListItem) and returns the filtered items based on `filteredValues` state + */ const useFilteredItems = ( children: React.ReactNode[], ): { @@ -136,7 +142,7 @@ const _ActionListVirtualizedBox = React.forwardRef - {itemCount < 30 ? ( + {itemCount < 10 ? ( childrenWithId ) : ( <ActionListSection[] /> ), - isVirtualized: 'boolean', + isVirtualized: { + note: + 'Currently only works in ActionList with static height items (items without description) and when ActionList has more than 10 items', + type: 'boolean', + }, }, ActionListItem: { title: 'string', diff --git a/yarn.lock b/yarn.lock index add86c2ffa1..a160488d788 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24196,10 +24196,10 @@ react-virtualized-auto-sizer@1.0.7: resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz#bfb8414698ad1597912473de3e2e5f82180c1195" integrity sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA== -react-window@1.8.6: - version "1.8.6" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" - integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== +react-window@1.8.11: + version "1.8.11" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.11.tgz#a857b48fa85bd77042d59cc460964ff2e0648525" + integrity sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6"