From f9a9c7a07f1931509efd05eafc95f3027b962f1d Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Mon, 17 Jun 2024 22:14:54 +0300 Subject: [PATCH 01/15] feat(useList): redesigned api --- .../TableColumnSetup/TableColumnSetup.tsx | 9 +- src/components/TreeList/TreeList.tsx | 108 +-- src/components/TreeList/__stories__/Docs.mdx | 7 + .../TreeList/__stories__/TreeList.mdx | 425 ---------- .../TreeList/__stories__/TreeList.stories.tsx | 2 +- .../TreeList/__stories__/TreeListDocs.md | 101 +++ .../components/RenderVirtualizedContainer.tsx | 4 +- .../__stories__/stories/DefaultStory.tsx | 32 +- .../stories/InfinityScrollStory.tsx | 53 +- .../stories/WithDisabledElementsStory.tsx | 18 +- .../__stories__/stories/WithDndListStory.tsx | 33 +- .../WithFiltrationAndControlsStory.tsx | 40 +- .../WithGroupSelectionAndCustomIconStory.tsx | 38 +- .../stories/WithItemLinksAndActionsStory.tsx | 42 +- .../TreeListContainer/TreeListContainer.tsx | 16 +- src/components/TreeList/types.ts | 94 +-- src/components/TreeSelect/TreeSelect.scss | 4 + src/components/TreeSelect/TreeSelect.tsx | 177 ++-- .../__stories__/TreeSelect.stories.tsx | 16 +- .../components/InfinityScrollExample.tsx | 20 +- .../components/WithDndListExample.tsx | 19 +- .../WithFiltrationAndControlsExample.tsx | 4 +- ...pSelectionControlledStateAndCustomIcon.tsx | 43 +- .../WithItemLinksAndActionsExample.tsx | 45 +- .../hooks/useTreeSelectSelection.ts | 53 +- src/components/TreeSelect/types.ts | 106 +-- .../__stories__/DndExample.stories.tsx | 2 +- src/components/useList/__stories__/Docs.mdx | 159 ++++ .../ListInfinityScroll.stories.tsx | 2 +- .../__stories__/PopupWithToggler.stories.tsx | 2 +- .../__stories__/RecursiveRenderer.stories.tsx | 2 +- .../__stories__/VirtualizedList.stories.tsx | 2 +- .../__stories__/components/FlattenList.tsx | 47 +- .../components/InfinityScrollList.tsx | 49 +- .../__stories__/components/ListWithDnd.tsx | 34 +- .../components/PopupWithTogglerList.tsx | 54 +- .../__stories__/components/RecursiveList.tsx | 49 +- .../VirtualizedListContainer.async.tsx | 2 +- .../__stories__/docs/compute-item-size.md | 19 + .../__stories__/docs/get-item-render-state.md | 93 +++ .../__stories__/docs/get-list-item-qa.md | 12 + .../__stories__/docs/get-list-parsed-state.md | 14 + .../__stories__/docs/list-container-view.md | 23 + .../__stories__/docs/list-item-view.md | 66 ++ .../docs/list-recursive-renderer.md | 64 ++ .../__stories__/docs/scroll-to-list-item.md | 27 + .../__stories__/docs/use-list-filter.md | 54 ++ .../__stories__/docs/use-list-item-click.md | 34 + .../__stories__/docs/use-list-keydown.md | 32 + .../useList/__stories__/docs/use-list.md | 136 +++ .../useList/__stories__/useList.mdx | 780 ------------------ .../ListContainerView/ListContainerView.tsx | 4 +- .../components/ListItemView/ListItemView.scss | 3 + .../__stories__/ListItemView.stories.tsx | 15 +- .../ListRecursiveRenderer.tsx | 45 +- .../useList/hooks/useFlattenListItems.ts | 2 +- src/components/useList/hooks/useList.ts | 89 +- src/components/useList/hooks/useListFilter.ts | 8 +- .../useList/hooks/useListItemClick.ts | 29 + .../useList/hooks/useListKeydown.tsx | 61 +- .../useList/hooks/useListParsedState.ts | 22 +- src/components/useList/hooks/useListState.ts | 74 +- src/components/useList/index.ts | 2 +- src/components/useList/types.ts | 36 +- .../useList/utils/flattenItems.test.ts | 150 +++- src/components/useList/utils/flattenItems.ts | 44 +- .../useList/utils/getItemRenderState.tsx | 78 +- .../useList/utils/getListParsedState.test.ts | 22 +- .../useList/utils/getListParsedState.ts | 22 +- src/unstable.ts | 3 +- 70 files changed, 1746 insertions(+), 2230 deletions(-) create mode 100644 src/components/TreeList/__stories__/Docs.mdx delete mode 100644 src/components/TreeList/__stories__/TreeList.mdx create mode 100644 src/components/TreeList/__stories__/TreeListDocs.md create mode 100644 src/components/useList/__stories__/Docs.mdx create mode 100644 src/components/useList/__stories__/docs/compute-item-size.md create mode 100644 src/components/useList/__stories__/docs/get-item-render-state.md create mode 100644 src/components/useList/__stories__/docs/get-list-item-qa.md create mode 100644 src/components/useList/__stories__/docs/get-list-parsed-state.md create mode 100644 src/components/useList/__stories__/docs/list-container-view.md create mode 100644 src/components/useList/__stories__/docs/list-item-view.md create mode 100644 src/components/useList/__stories__/docs/list-recursive-renderer.md create mode 100644 src/components/useList/__stories__/docs/scroll-to-list-item.md create mode 100644 src/components/useList/__stories__/docs/use-list-filter.md create mode 100644 src/components/useList/__stories__/docs/use-list-item-click.md create mode 100644 src/components/useList/__stories__/docs/use-list-keydown.md create mode 100644 src/components/useList/__stories__/docs/use-list.md delete mode 100644 src/components/useList/__stories__/useList.mdx create mode 100644 src/components/useList/hooks/useListItemClick.ts diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx index 9398ae549d..6cca789c33 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -113,8 +113,7 @@ const useDndRenderContainer = ({onDragEnd, renderControls}: UseDndRenderContaine const dndRenderContainer: TreeSelectRenderContainer = ({ renderItem, - visibleFlattenIds, - itemsById, + list, containerRef, id, className, @@ -126,15 +125,15 @@ const useDndRenderContainer = ({onDragEnd, renderControls}: UseDndRenderContaine }; return renderItem( - visibleFlattenIds[rubric.source.index], + list.structure.visibleFlattenIds[rubric.source.index], rubric.source.index, renderContainerProps, ); }; const {stickyStartItemIdList, sortableItemIdList, stickyEndItemIdList} = prepareStickyState( - itemsById, - visibleFlattenIds, + list.structure.itemsById, + list.structure.visibleFlattenIds, ); const stickyStartItemList = stickyStartItemIdList.map((visibleFlattenId, idx) => { diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index f3a2248faa..d617638706 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -3,12 +3,16 @@ import React from 'react'; import {useUniqId} from '../../hooks'; -import {ListItemView, getItemRenderState, useList, useListKeydown} from '../useList'; +import {ListItemView, getItemRenderState, useListItemClick, useListKeydown} from '../useList'; import type {ListItemId} from '../useList'; import {block} from '../utils/cn'; import {TreeListContainer} from './components/TreeListContainer/TreeListContainer'; -import type {TreeListProps, TreeListRenderContainerProps} from './types'; +import type { + TreeListOnItemClickPayload, + TreeListProps, + TreeListRenderContainerProps, +} from './types'; const b = block('tree-list'); @@ -16,20 +20,14 @@ export const TreeList = ({ qa, id, size = 'm', - items, className, - expandedById: propsExpandedById, - disabledById: propsDisabledById, - selectedById: propsSelectedById, - activeItemId, - defaultGroupsExpanded = true, - getItemId, + list, renderItem: propsRenderItem, renderContainer = TreeListContainer, - onItemClick, + onItemClick: propsOnItemClick, multiple, - setActiveItemId, containerRef: propsContainerRef, + withItemClick, mapItemDataToProps, }: TreeListProps) => { const uniqId = useUniqId(); @@ -37,70 +35,27 @@ export const TreeList = ({ const containerRefLocal = React.useRef(null); const containerRef = propsContainerRef ?? containerRefLocal; - const listParsedState = useList({ - items, - getItemId, - // used not all of all properties but it may be needed in future - expandedById: propsExpandedById, - disabledById: propsDisabledById, - selectedById: propsSelectedById, - activeItemId, - }); + const defaultOnItemClick = useListItemClick({list, multiple}); - const expandedById = propsExpandedById || listParsedState.initialState.expandedById; - const disabledById = propsDisabledById || listParsedState.initialState.disabledById; - const selectedById = propsSelectedById || listParsedState.initialState.selectedById; + const onItemClick = React.useMemo(() => { + if (propsOnItemClick === null) { + return undefined; + } - const handleItemClick = React.useMemo(() => { - if (onItemClick) { - return (listItemId: ListItemId) => { - onItemClick?.({ - id: listItemId, - index: listParsedState.idToFlattenIndex[listItemId], - data: listParsedState.itemsById[listItemId], - expanded: - // eslint-disable-next-line no-nested-ternary - expandedById && listItemId in expandedById - ? expandedById[listItemId] - : listItemId in listParsedState.initialState.expandedById - ? listParsedState.initialState.expandedById[listItemId] - : defaultGroupsExpanded, - disabled: disabledById - ? Boolean(disabledById[listItemId]) - : Boolean(listParsedState.initialState.disabledById[listItemId]), - selected: selectedById - ? Boolean(selectedById[listItemId]) - : Boolean(listParsedState.initialState.selectedById[listItemId]), + const onClick = propsOnItemClick ?? defaultOnItemClick; - context: { - isLastItem: - listParsedState.visibleFlattenIds[ - listParsedState.visibleFlattenIds.length - 1 - ] === listItemId, - groupState: listParsedState.groupsState[listItemId], - itemState: listParsedState.itemsState[listItemId], - }, - }); - }; - } + return ({id}: {id: ListItemId}) => { + const payload: TreeListOnItemClickPayload = {id, list}; - return undefined; - }, [ - defaultGroupsExpanded, - disabledById, - expandedById, - selectedById, - listParsedState, - onItemClick, - ]); + onClick(payload); + withItemClick?.(payload); + }; + }, [defaultOnItemClick, list, propsOnItemClick, withItemClick]); useListKeydown({ containerRef, - onItemClick: handleItemClick, - ...listParsedState, - activeItemId, - disabledById, - setActiveItemId, + onItemClick, + list, }); const renderItem: TreeListRenderContainerProps['renderItem'] = ( @@ -114,13 +69,8 @@ export const TreeList = ({ size, multiple, mapItemDataToProps, - onItemClick: handleItemClick, - ...listParsedState, - expandedById, - disabledById, - activeItemId, - selectedById, - defaultExpanded: defaultGroupsExpanded, + onItemClick, + list, }); if (propsRenderItem) { @@ -130,6 +80,7 @@ export const TreeList = ({ context: renderState.context, index, renderContainerProps, + list, }); } @@ -143,12 +94,7 @@ export const TreeList = ({ size, containerRef, className: b(null, className), - ...listParsedState, - expandedById, - disabledById, - activeItemId, - selectedById, + list, renderItem, - getItemId, }); }; diff --git a/src/components/TreeList/__stories__/Docs.mdx b/src/components/TreeList/__stories__/Docs.mdx new file mode 100644 index 0000000000..e3c76f1cd5 --- /dev/null +++ b/src/components/TreeList/__stories__/Docs.mdx @@ -0,0 +1,7 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; + +import TreeListDocs from './TreeListDocs.md?raw'; + + + +{TreeListDocs} diff --git a/src/components/TreeList/__stories__/TreeList.mdx b/src/components/TreeList/__stories__/TreeList.mdx deleted file mode 100644 index 7a16ad7570..0000000000 --- a/src/components/TreeList/__stories__/TreeList.mdx +++ /dev/null @@ -1,425 +0,0 @@ -import {Meta} from '@storybook/addon-docs'; - - - -# TreeList - -The basic component for working with lists, including tree-like ones. Under the hood, it uses the [useList](/docs/unstable-uselist--docs). To manage the state, it is recommended to use the [useListState](/docs/unstable-uselist--docs#useliststate) hook. - -`Storybook` provides complex examples how to use this components from this documentation. - -## Props: - -- [items](#items); -- [mapItemDataToProps](#mapitemdatatoprops); -- [qa](#qa); -- [id](#id); -- [containerRef](#containerref); -- [className](#classname); -- [multiple](#multiple); -- [size](#size-available-options); -- [defaultGroupsExpanded](#defaultgroupsexpanded); -- [getItemId](#getItemId); -- [renderItem](#renderitem); -- [renderContainer](#rendercontainer); -- [onItemClick](#onitemclick); -- [...useListState](/docs/unstable-uselist--docs#useliststate) - -## Quick start: - -### Basic example: - -```tsx -import { - type unstable_ListItemType as ListItemType, - unstable_TreeList as TreeList, -} from '@gravity-ui/uikit/unstable'; - -const items: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; - - ({title: item})} />; -``` - -### Example with state: - -```tsx -import { - type unstable_ListItemType as ListItemType, - unstable_TreeList as TreeList, - unstable_useListState as useListState, -} from '@gravity-ui/uikit/unstable'; - -const items: ListItemType[] = [ - {title: 'one'}, - {title: 'two'}, - {title: 'free'}, - {title: 'four'}, - {title: 'five'}, -]; - -const Component = () => { - const listState = useListState(); - - const handleItemClick: TreeListOnItemClick = ({id, disabled, groupState}) => { - if (disabled) return; - - if (groupState) { - listState.setExpanded((prevState) => ({ - ...prevState, - [id]: id in prevState ? !prevState[id] : false, - })); - } else { - listState.setSelected((prevState) => ({ - [id]: !prevState[id], - })); - } - - listState.setActiveItemId(id); - }; - - return ( - ({title})} - /> - ); -}; -``` - -> If you want to display the nodes of the list as regular elements without the possibility of hiding the folded elements of the sheet, then just do not pass the `expandedById` object from the state to the component itself: - -```ts -const {expandedById, setExpandedById, ...listState} = useListState(); - - -``` - -## Component props: - -### items - -Array of list items. More details about data structure and properties you can find [here](/docs/unstable-uselist--docs#items-supported-data-structure); - -### mapItemDataToProps - -Map list item data structire to `ListItemView` [props](/docs/unstable-uselist--docs#listitemview); - -### containerRef - -Pass a ref to pass a link to the DOM element of the container. For example, in order to control the focus of the list to activate keyboard navigation support; - -```tsx -import React from 'react'; -import {Button, Alert} from '@gravity-ui/uikit'; -import { - type unstable_ListItemType as ListItemType, - unstable_TreeList sa TreeList, - unstable_useListState as useListState, -} from '@gravity-ui/uikit/unstable'; - -const items: ListItemType[] = [ - {data: {title: 'one'}}, - {data: {title: 'two'}}, - {data: {title: 'free'}}, - {data: {title: 'four'}}, - {data: {title: 'five'}}, -]; - -const Component = () => { - const containerRef = React.useRef(null); - const listState = useListState(); - - const handleItemClick: TreeListOnItemClick = ({ - id, - disabled, - selected, - expanded, - groupState, - }) => { - // ... - }; - - return ( - <> - - - ({title})} - /> - - ); -}; -``` - -### getItemId - -Generate an id for a list item depending on the list data. If it's necessary to have access to more custom management of the state of the list. The property is optional. - -```tsx -const items = [ - {data: {id: 'id-1', title: 'some title 1'}, children: [...]}, - {data: {id: 'id-2', title: 'some title 2'}, children: [...]}, -]; - - id} /> -``` - -### qa - -Set `qa` attribute for the container and sheet elements. `qa` attribute is also passed to the `ListItemView`. - -> Use the [getListItemQa](/docs/unstable-uselist--docs#getlistitemqa) is used to generate `qa` attributes in list items; also use this function in tests to compute a unique data attribute to access a specific list item - - ```ts - await locator.getByTestId(getListItemQa('some-list-qa', '0')); // select the first item in the list if auto-generated IDs are not used - ``` - -### className - -Pass custom CSS class for the list container. - -### id - -Set a custom id data attribute. By default, a unique identifier will be assigned. - -### multiple - -This prop is necessary for the correct view of the selected elements since the state of the selected elements is controlled from the parent component. - -### setActiveItemId - -Required for correct keyboard interactions. While navigating through the keyboard, you need to set the next active element. - -### defaultGroupsExpanded - -Control the default expanded state of items' groups. Default - `true`. - -### renderItem - -Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/unstable-uselist--docs#listitemview); - -```tsx -import { - unstable_TreeList as TreeList, - unstable_ListItemView as ListItemView, -} from '@gravity-ui/uikit/unstable'; - - { - return ; - }} -/>; -``` - -#### renderItem function argument object: - -```tsx -type ListItemSize = 's' | 'm' | 'l' | 'xl'; - -interface RenderItemProps { - /** - * access to the original object with the data of the list element - */ - data: T; - /** - * ordinal index of the element, taking into account that with a tree-like data structure, the list elements have a flatten representation; - */ - index: number; - /** - * default props generated by the component taking into account the state (whether the element is selected or not, active, disclosed). The set of returned passes corresponds to the result of the function execution [getItemRenderState](/docs/unstable-uselist--docs#item-state-props) - */ - props: { - // item id; - id: string; - // qa attribute for tests - qa?: string; - // item size; - size: ListItemSize; - // expanded state if item group; - expanded?: boolean; - // is item active - active: boolean; - // item nest level; - indentation: number; - // is item disabled; - disabled: boolean; - // is item selected; - selected?: boolean; - // on item click handle if exists; - onClick?(): void; - // affects the view of the selected items; - hasSelectionIcon?: boolean; - // one required field of result `mapItemDataToProps` function work; - title: React.ReactNode; - }; - /** - * during `renderContainer` props you can pass render container context props to items; - */ - renderContainerProps?: Object; - /** - * useful information about the current list item: - */ - context: { - // meta info about item - itemState: { - // integer number, representing nested list level - indentation: number; - // `id` of parent list item if it exists - parentId?: string; - }; - // An optional parameter. If the list item is also the first item of the nested list - groupState: { - // array of `id` of nested list items; - childrenIds: string[]; - }; - // is the current item the last one in the list - isLastItem: boolean; - }; -} -``` - -> Important! Absolutely all the props for [ListItemView](/docs/unstable-uselist--docs#listitemview) can be redefined in the renderItem method. This is the preferred method for changing the view of the list elements. - -### renderContainer - -Render custom list container. - -```tsx -import { - unstable_TreeList as TreeList, - unstable_ListContainerView as ListContainerView, -} from '@gravity-ui/uikit/unstable'; - -) => { - return ( - - computeItemSize(size)} - > - {(id, index) => - renderItem( - id, - index, - _, // here you can optionally pass any props depending of render context */, - ) - } - - - ); - }} -/>; -``` - -### onItemClick - -Item's click callback. It also will be called on keyboard actions. - -```tsx -import {unstable_TreeList as TreeList} from '@gravity-ui/uikit/unstable'; - - { - // just do it! - }} -/>; -``` - -#### onItemClick function argument object: - -```tsx -type OnItemClick = (props: OnItemClickProps) => void; - -interface OnItemClickProps { - /** - * `id` of the current element; - */ - id: string; - /** - * access to the original payload (`T`) list item; - */ - data: T; - /** - * the ordinal index of the element, taking into account that with a tree-like data structure, the list elements have a flatten representation; - */ - index: number; - /** - * whether the item is selected or not; - */ - selected: boolean; - /** - * is the element disabled; - */ - disabled: boolean; - /** - * are nested child elements hidden; - */ - expanded: boolean; - /** - * useful information about the current list item: - */ - context: { - // meta info about item - itemState: { - // integer number, representing nested list level - indentation: number; - // `id` of parent list item if it exists - parentId?: string; - }; - // An optional parameter. If the list item is also the first item of the nested list - groupState: { - // array of `id` of nested list items; - childrenIds: string[]; - }; - // is the current item the last one in the list - isLastItem: boolean; - }; -} -``` diff --git a/src/components/TreeList/__stories__/TreeList.stories.tsx b/src/components/TreeList/__stories__/TreeList.stories.tsx index c3aa652e20..d4b1d213d2 100644 --- a/src/components/TreeList/__stories__/TreeList.stories.tsx +++ b/src/components/TreeList/__stories__/TreeList.stories.tsx @@ -11,7 +11,7 @@ import {WithGroupSelectionAndCustomIconStory} from './stories/WithGroupSelection import {WithItemLinksAndActionsStory} from './stories/WithItemLinksAndActionsStory'; export default { - title: 'Unstable/TreeList', + title: 'Lab/TreeList', component: TreeList, } as Meta; diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md new file mode 100644 index 0000000000..32a6245213 --- /dev/null +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -0,0 +1,101 @@ +# TreeList + +The basic component for working with lists, including tree-like. Under the hood, it uses the [useList](/docs/lab-uselist--docs). + +`Storybook` provides complex examples how to use this components from this documentation. + +## Quick start: + +### Import: + +```tsx +import {unstable_TreeList as TreeList} from '@gravity-ui/uikit/unstable'; +``` + +### Basic example: + +```tsx +import { + type unstable_ListItemType as ListItemType, + unstable_TreeList as TreeList, + unstable_useList as useList, +} from '@gravity-ui/uikit/unstable'; + +const items: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; + +const list = useList({items}); + + ({title: item})} />; +``` + +### Example with state: + +```tsx +import { + type unstable_ListItemType as ListItemType, + unstable_TreeList as TreeList, + unstable_useList as useList, +} from '@gravity-ui/uikit/unstable'; + +const items: ListItemType[] = [ + {title: 'one'}, + {title: 'two'}, + {title: 'free'}, + {title: 'four'}, + {title: 'five'}, +]; + +const Component = () => { + const list = useList({items}); + + const handleItemClick = ({id}) => { + list.state.setSelected({[id]: true}); + }; + + return ( + ({title})} + /> + ); +}; +``` + +## Props: + +| Name | Description | Type | Default | +| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}) => React.JSX.Element \| null` | | +| withItemClick | Don't override default click behavior and add additional logic. Work's if `onItemClick` not `null` | `TreeListOnItemClick \| null` | | + +### TreeListRenderItem props: + +| Name | Description | Type | Default | +| :------------------- | :-------------------------------------------------------------------------- | :------------------------: | :---------: | +| data | List item data | `T` | | +| props | Prepared `ListItemView` [props](/docs/lab-uselist--docs#listitemview) | `ListItemViewProps` | +| context | List item context [props](/docs/lab-uselist--docs#listitemlistcontextprops) | `ListItemListContextProps` | | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | +| index | Index order in flatted visible id's | `number` | | +| renderContainerProps | Data from container rendered context if needed | `P` | `undefined` | + +### TreeListRenderContainer props: + +| Name | Description | Type | Default | +| :----------- | :-------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------: | :-----: | +| id | Id attribute | `string` | | +| list | result of `useList` hook | `UseList` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| className | Class name to mix with | `string` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| renderItem | Render item interface to implement | `(props: {id: ListItemId; index: number;renderContainerProps?: Object,}) => React.JSX.Element` | | diff --git a/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx b/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx index 553ca73b8b..7207b5d106 100644 --- a/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx +++ b/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx @@ -9,7 +9,7 @@ export const RenderVirtualizedContainer = ({ id, qa, containerRef, - visibleFlattenIds, + list, renderItem, size, className, @@ -24,7 +24,7 @@ export const RenderVirtualizedContainer = ({ extraProps={{style: {padding: 0}}} > computeItemSize(size)} > {renderItem} diff --git a/src/components/TreeList/__stories__/stories/DefaultStory.tsx b/src/components/TreeList/__stories__/stories/DefaultStory.tsx index 69a610b0ed..b6fbf5790c 100644 --- a/src/components/TreeList/__stories__/stories/DefaultStory.tsx +++ b/src/components/TreeList/__stories__/stories/DefaultStory.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Text} from '../../../Text'; import {Flex} from '../../../layout'; -import {ListItemView} from '../../../useList'; +import {useList} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; @@ -19,30 +19,32 @@ export interface DefaultStoryProps export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { const items = React.useMemo(() => createRandomizedData({num: itemsCount}), [itemsCount]); + const listWithGroups = useList({items}); + + const listWithNoGroups = useList({ + items, + rootNodesGroups: false, + }); + return ( Default TreeList - + - - To remove default group view, override corresponding (expanded) prop in - renderItem method - + List with `rootNodesGroups` false option in listState { - // if item group - if (groupState) { - props.expanded = undefined; - } - - return ; - }} /> diff --git a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx index cca4c5d75d..01a96c5071 100644 --- a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx +++ b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx @@ -3,11 +3,11 @@ import React from 'react'; import {Label} from '../../../Label'; import {Loader} from '../../../Loader'; import {Flex, spacing} from '../../../layout'; -import {ListItemView, useListState} from '../../../useList'; +import {ListItemView, useList} from '../../../useList'; import {IntersectionContainer} from '../../../useList/__stories__/components/IntersectionContainer/IntersectionContainer'; import {useInfinityFetch} from '../../../useList/__stories__/utils/useInfinityFetch'; import {TreeList} from '../../TreeList'; -import type {TreeListOnItemClick, TreeListProps} from '../../types'; +import type {TreeListProps} from '../../types'; import {RenderVirtualizedContainer} from '../components/RenderVirtualizedContainer'; function identity(value: T): T { @@ -22,33 +22,9 @@ export interface InfinityScrollStoryProps itemsCount?: number; } -export const InfinityScrollStory = ({itemsCount = 5, ...storyProps}: InfinityScrollStoryProps) => { - const listState = useListState(); - - const handleItemClick: TreeListOnItemClick<{title: string}> = ({ - id, - disabled, - expanded, - selected, - context: {groupState}, - }) => { - if (disabled) return; - - listState.setActiveItemId(id); - - if (groupState) { - listState.setExpanded((prevState) => ({ - ...prevState, - [id]: !expanded, - })); - } else { - listState.setSelected((prevState) => ({ - ...prevState, - [id]: !selected, - })); - } - }; +const multiple = true; +export const InfinityScrollStory = ({itemsCount = 3, ...storyProps}: InfinityScrollStoryProps) => { const { data: items = [], onFetchMore, @@ -56,26 +32,21 @@ export const InfinityScrollStory = ({itemsCount = 5, ...storyProps}: InfinityScr isLoading, } = useInfinityFetch<{title: string}>(itemsCount, true); + const list = useList({items}); + return ( - {...storyProps} - {...listState} + size="l" + list={list} mapItemDataToProps={identity} - items={items} - multiple - onItemClick={handleItemClick} - renderItem={({data, props, context: {isLastItem, groupState}}) => { + multiple={multiple} + renderItem={({props, context: {isLastItem, childrenIds}}) => { const node = ( {groupState.childrenIds.length} - ) : undefined - } + endSlot={childrenIds ? : undefined} /> ); diff --git a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx index fd68c78204..9af32e65dc 100644 --- a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Button} from '../../../Button'; import {Flex} from '../../../layout'; -import {useListState} from '../../../useList'; +import {useList} from '../../../useList'; import type {ListItemType} from '../../../useList'; import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; @@ -29,8 +29,8 @@ const items: ListItemType<{text: string}>[] = [ }, ]; -export const WithDisabledElementsStory = ({...props}: WithDisabledElementsStoryProps) => { - const {disabledById: _disabledById, setDisabled: _setDisabled, ...listState} = useListState(); +export const WithDisabledElementsStory = ({...storyProps}: WithDisabledElementsStoryProps) => { + const list = useList({items}); const containerRef = React.useRef(null); return ( @@ -46,14 +46,14 @@ export const WithDisabledElementsStory = ({...props}: WithDisabledElementsStoryP to control from keyboard ({title: text})} - onItemClick={({data, id, selected}) => { - listState.setSelected({[id]: !selected}); - alert(`Clicked by item with id :"${id}" and data: ${JSON.stringify(data)}`); + withItemClick={({id}) => { + alert( + `Clicked by item with id :"${id}" and data: ${JSON.stringify(list.structure.itemsById[id])}`, + ); }} /> diff --git a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx index 1c4f454d0d..ee4d963a23 100644 --- a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx @@ -11,7 +11,7 @@ import type { } from 'react-beautiful-dnd'; import {Icon} from '../../../Icon'; -import {ListContainerView, ListItemView, useListState} from '../../../useList'; +import {ListContainerView, ListItemView, useList} from '../../../useList'; import type {ListItemViewProps} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {reorderArray} from '../../../useList/__stories__/utils/reorderArray'; @@ -46,7 +46,12 @@ export interface WithDndListStoryProps export const WithDndListStory = (storyProps: WithDndListStoryProps) => { const [items, setItems] = React.useState(randomItems); const containerRef = React.useRef(null); - const listState = useListState(); + + const list = useList({ + items, + // you can omit this prop here. If prop `id` passed, TreeSelect would take it by default + getItemId: ({id}) => id, + }); React.useLayoutEffect(() => { containerRef?.current?.focus(); @@ -54,7 +59,7 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { const renderContainer: TreeListRenderContainer = ({ renderItem, - visibleFlattenIds, + list, containerRef, id, }) => { @@ -64,7 +69,7 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { reorderArray(currentItems, source.index, destination.index), ); - listState.setActiveItemId(`${destination.index}`); + list.state.setActiveItemId(`${destination.index}`); } }; @@ -78,7 +83,7 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { rubric: DraggableRubric, ) => { return renderItem( - visibleFlattenIds[rubric.source.index], + list.structure.visibleFlattenIds[rubric.source.index], rubric.source.index, { provided, @@ -93,7 +98,7 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { {...droppableProvided.droppableProps} ref={droppableProvided.innerRef} > - {visibleFlattenIds.map((listItemId, index) => + {list.structure.visibleFlattenIds.map((listItemId, index) => renderItem(listItemId, index), )} {droppableProvided.placeholder} @@ -142,22 +147,10 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { return ( ({title: someRandomKey})} - // you can omit this prop here. If prop `id` passed, TreeSelect would take it by default - getItemId={({id}) => id} - onItemClick={({id, disabled, context: {groupState}}) => { - if (!groupState && !disabled) { - listState.setSelected((prevState) => ({ - [id]: !prevState[id], - })); - - listState.setActiveItemId(id); - } - }} renderContainer={renderContainer} renderItem={renderItem} /> diff --git a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx index 53d1bf4525..715298b1e4 100644 --- a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx @@ -4,7 +4,7 @@ import {Button} from '../../../Button'; import {Text} from '../../../Text'; import {TextInput} from '../../../controls'; import {Flex, spacing} from '../../../layout'; -import {useListFilter, useListState} from '../../../useList'; +import {useList, useListFilter} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; import type {TreeListProps, TreeListRenderContainerProps} from '../../types'; @@ -25,7 +25,7 @@ export const WithFiltrationAndControlsStory = ({ const {items, renderContainer} = React.useMemo(() => { const baseItems = createRandomizedData({num: itemsCount}); const containerRenderer = (props: TreeListRenderContainerProps<{title: string}>) => { - if (props.items.length === 0 && baseItems.length > 0) { + if (props.list.structure.items.length === 0 && baseItems.length > 0) { return ( Nothing found @@ -39,10 +39,10 @@ export const WithFiltrationAndControlsStory = ({ return {items: baseItems, renderContainer: containerRenderer}; }, [itemsCount]); - const listState = useListState(); - const filterState = useListFilter({items}); + const list = useList({items: filterState.items}); + return ( { - if (disabled) return; - - if (groupState) { - listState.setExpanded((prevState) => ({ - ...prevState, - [id]: id in prevState ? !prevState[id] : false, - })); - } else { - listState.setSelected((prevState) => - treeSelectProps.multiple - ? { - ...prevState, - [id]: !prevState[id], - } - : { - [id]: !prevState[id], - }, - ); - } - - listState.setActiveItemId(id); - }} + list={list} mapItemDataToProps={(x) => x} renderContainer={renderContainer} - items={filterState.items} /> - {items.map((item, index) => ( - + {list.structure.itemsSchema.map((itemSchema, index) => ( + {(id) => { const {props, context} = getItemRenderState({ id, size, onItemClick, mapItemDataToProps: (x) => x, - ...list, - ...listState, + list, }); return ( ); }} diff --git a/src/components/useList/__stories__/components/RecursiveList.tsx b/src/components/useList/__stories__/components/RecursiveList.tsx index e05305dcff..05e51c528d 100644 --- a/src/components/useList/__stories__/components/RecursiveList.tsx +++ b/src/components/useList/__stories__/components/RecursiveList.tsx @@ -7,9 +7,9 @@ import {ListItemView} from '../../components/ListItemView/ListItemView'; import {ListItemRecursiveRenderer} from '../../components/ListRecursiveRenderer/ListRecursiveRenderer'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; +import {useListItemClick} from '../../hooks/useListItemClick'; import {useListKeydown} from '../../hooks/useListKeydown'; -import {useListState} from '../../hooks/useListState'; -import type {ListItemId, ListItemSize} from '../../types'; +import type {ListItemSize} from '../../types'; import {getItemRenderState} from '../../utils/getItemRenderState'; import {createRandomizedData} from '../utils/makeData'; @@ -29,38 +29,14 @@ export const RecursiveList = ({size, itemsCount, 'aria-label': ariaLabel}: Recur const filterState = useListFilter({items}); - const listState = useListState(); + const list = useList({items: filterState.items}); - const list = useList({ - items: filterState.items, - ...listState, - }); - - const onItemClick = React.useCallback( - (id: ListItemId) => { - if (id in list.groupsState) { - listState.setExpanded((state) => ({ - ...state, - [id]: id in state ? !state[id] : false, - })); - } else { - // just toggle item by id - listState.setSelected((state) => ({ - ...state, - [id]: !state[id], - })); - } - - listState.setActiveItemId(id); - }, - [list.groupsState, listState], - ); + const onItemClick = useListItemClick({list}); useListKeydown({ containerRef, onItemClick, - ...list, - ...listState, + list, }); return ( @@ -74,14 +50,8 @@ export const RecursiveList = ({size, itemsCount, 'aria-label': ariaLabel}: Recur autoFocus /> - {filterState.items.map((item, index) => ( - + {list.structure.itemsSchema.map((itemSchema, index) => ( + {(id) => { const {props, context} = getItemRenderState({ id, @@ -89,12 +59,11 @@ export const RecursiveList = ({size, itemsCount, 'aria-label': ariaLabel}: Recur onItemClick, multiple: true, mapItemDataToProps: (x) => x, - ...list, - ...listState, + list, }); return ( - + ); }} diff --git a/src/components/useList/__stories__/components/VirtualizedListContainer/VirtualizedListContainer.async.tsx b/src/components/useList/__stories__/components/VirtualizedListContainer/VirtualizedListContainer.async.tsx index 9d18048fdf..6a0b9060a3 100644 --- a/src/components/useList/__stories__/components/VirtualizedListContainer/VirtualizedListContainer.async.tsx +++ b/src/components/useList/__stories__/components/VirtualizedListContainer/VirtualizedListContainer.async.tsx @@ -15,7 +15,7 @@ export const VirtualizedListContainer = (props: ListContainerRenderProps) return ( + } diff --git a/src/components/useList/__stories__/docs/compute-item-size.md b/src/components/useList/__stories__/docs/compute-item-size.md new file mode 100644 index 0000000000..77eb55c9b9 --- /dev/null +++ b/src/components/useList/__stories__/docs/compute-item-size.md @@ -0,0 +1,19 @@ +### computeItemSize; + +Utility to compute list item height: + +#### Usage example: + +```tsx + + computeItemSize( + // list size + size, + // has subrows + Boolean(get(itemsById[visibleFlattenIds[index]], 'subtitle')), + ) + } +/> +``` diff --git a/src/components/useList/__stories__/docs/get-item-render-state.md b/src/components/useList/__stories__/docs/get-item-render-state.md new file mode 100644 index 0000000000..bd6abded64 --- /dev/null +++ b/src/components/useList/__stories__/docs/get-item-render-state.md @@ -0,0 +1,93 @@ +### getItemRenderState; + +Map list state to `ListItemView` render props; + +```tsx +import { + unstable_ListItemView as ListItemView, + unstable_getItemRenderState as getItemRenderState, + unstable_useListState as useListState, + unstable_useList as useList, +} from '@gravity-ui/uikit/unstable'; + +const list = useList({items: [...]}); +const onItemClick = useListItemClick({list}); + +const {data, props, context} = getItemRenderState({ + qa: 'some-qa-id', + id, + multiple: true, + size, // list size + onItemClick, + mapItemDataToProps: (item) => ({title: item.title}), + list, +}); + +return ; +``` + +#### Props: + +| Name | Description | Type | Default | +| :----------------- | :--------------------------------------------------------------------------------- | :--------------------------------: | :-----: | +| id | `id` of list item | `ListItemId` | | +| list | result of `useList` hook | `UseList` | | +| multiple | One or multiple elements selected list | `boolean` | | +| onItemClick | Optional on click handler | `(id: ListItemId) => void` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | + +##### ListItemCommonProps + +| Name | Type | Note | +| :-------- | :---------------: | :------: | +| title | `React.ReactNode` | required | +| subtitle | `React.ReactNode` | optional | +| startSlot | `React.ReactNode` | optional | +| endSlot | `React.ReactNode` | optional | + +#### Returns: + +| Name | Description | Type | +| :------ | :----------------------------------------------: | :------------------------: | +| data | Row list item data | `T` | +| props | Prepared props for `ListItemView` | `ListItemViewProps` | +| context | List item state form `list` picked by current id | `ListItemListContextProps` | + +##### ListItemListContextProps + +| Name | Description | Type | +| :---------- | :----------------------------------------------------------: | :------------: | +| indentation | Item nested level | `number` | +| parentId | Optional. Link to parent group node if current node is child | `ListItemId` | +| childrenIds | Optional. Exists is list item a group node | `ListItemId[]` | + +#### Usage example: + +```tsx +import { + unstable_ListContainerView as ListItemRecursiveRenderer, + unstable_ListItemView as ListItemView, + unstable_getItemRenderState as getItemRenderState, + unstable_useList as useList, +} from '@gravity-ui/uikit/unstable'; + +const list = useList({items}); +const onItemClick = () => {}; + + + {(id) => { + const {props} = getItemRenderState({ + qa: 'some-qa-id', + id, + multiple: false, + size, // list size + onItemClick, + mapItemDataToProps, + list, + }); + + return ; + }} +; +``` diff --git a/src/components/useList/__stories__/docs/get-list-item-qa.md b/src/components/useList/__stories__/docs/get-list-item-qa.md new file mode 100644 index 0000000000..e020b65bb0 --- /dev/null +++ b/src/components/useList/__stories__/docs/get-list-item-qa.md @@ -0,0 +1,12 @@ +### getListItemQa + +Function is used to generate `qa` attributes in list items; +Also use this function in tests to create a unique data attribute for accessing a specific list item. + +#### Usage example: + +```ts +import {unstable_getListItemQa as getListItemQa} from '@gravity-ui/uikit/unstable'; + +await locator.getByTestId(getListItemQa('some-list-qa', '0')); // select the first item in the list if auto-generated `id` are used +``` diff --git a/src/components/useList/__stories__/docs/get-list-parsed-state.md b/src/components/useList/__stories__/docs/get-list-parsed-state.md new file mode 100644 index 0000000000..0e4c79c2d7 --- /dev/null +++ b/src/components/useList/__stories__/docs/get-list-parsed-state.md @@ -0,0 +1,14 @@ +### getListParsedState; + +Used under the hood of `useList().structure` property. Use it if you need to extract initial list state form declaration: + +#### Usage example: + +```tsx +import {unstable_getListParsedState as getListParsedState} from '@gravity-ui/uikit/unstable'; + +// custom controlled state from computed initial state +const [expandedById, setExpanded] = React.useState( + () => getListParsedState(items).initialState.expandedById, +); +``` diff --git a/src/components/useList/__stories__/docs/list-container-view.md b/src/components/useList/__stories__/docs/list-container-view.md new file mode 100644 index 0000000000..46285a926b --- /dev/null +++ b/src/components/useList/__stories__/docs/list-container-view.md @@ -0,0 +1,23 @@ +### ListContainerView + +The default container for all custom lists. Contains all html attributes and styles for quick use in your projects. + +#### Props: + +| Name | Description | Type | Default | +| :---------- | :----------------------------------------------------------------------------------------------------------- | :-------------------: | :-----: | +| id | Optional id attribute | `string` | | +| style | Inline styles if needed | `React.CSSProperties` | | +| className | Custom class name to mix with | `string` | | +| fixedHeight | Removes default `overflow: auto` from container and set fixed container height (`--g-list-height` = `300px`) | `boolean` | | + +#### Usage example: + +```tsx +const containerRef = React.useRef(null); + + + + +; +``` diff --git a/src/components/useList/__stories__/docs/list-item-view.md b/src/components/useList/__stories__/docs/list-item-view.md new file mode 100644 index 0000000000..d0b6bbbe70 --- /dev/null +++ b/src/components/useList/__stories__/docs/list-item-view.md @@ -0,0 +1,66 @@ +### ListItemView + +```tsx +import {unstable_ListItemView as ListItemView} from '@gravity-ui/uikit/unstable'; +``` + +The basic component responsible for the appearance of the list items. +Use it even if the functionality of the `useList` hook seems redundant to you + +#### Usage example: + +```tsx +import { + type unstable_ListItemType as ListItemType, + unstable_ListItemView as ListItemView, +} from '@gravity-ui/uikit/unstable'; + +type Entity = {title: string, subtitle: string, icon: React.ReactNode}; + +const items: ListItemType[] = [ + {title: 'some title 1', subtitle: 'some subtitle 1', icon: }, + {title: 'some title 2', subtitle: 'some subtitle 2', icon: }, +]; + +const List = () => { + return ( + <> + {items.map(item, i) => { + return ( + + ) + }} + + ) +}; +``` + +#### Props: + +| Name | Description | Type | Default | +| :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------: | :-----: | +| id | Required prop. Set `[data-list-item="${id}"]` data attribute. By this it core list engine finds elements to scroll to. | `string` | | +| title | Base required prop to use. If passed string, applies default component styles according design system. Pass you own component if you wont custom behavior; | `React.ReactNode` | | +| subtitle | Slot under `title`. If passed string apply predefined styles. Or you can pass custom `React.ReactNode` to use you own behavior | `React.ReactNode` | | +| as | If needed, override `html` tag. By default - `li` | `HTMLElement` | `li` | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| height | The height of the element in pixels. By default, it is calculated depending on the `size` parameter and the presence of the `subtitle` parameter | `number ` | | +| selected | The selected state of the component | `boolean ` | | +| active | The state when the element is in the user's focus, but not selected. It can also be used when you drag an element | `boolean ` | | +| disabled | The disabled state. It also prevents clicking on an element | `boolean ` | | +| activeOnHover | directly control hover behavior | `boolean ` | | +| indentation | Affects the visual indentation of the element content | `number ` | | +| hasSelectionIcon | Show selected icon if selected and reserve space for this icon | `boolean ` | | +| onClick | On item click callback. If `disabled` option is `true` click don't appears | `() => void` | | +| startSlot | Custom slot before `title` | `React.ReactNode` | | +| endSlot | Custom slot before `title` | `React.ReactNode` | | +| style | Inline styles if needed | `React.CSSProperties` | | +| className | Custom class name to mix with | `string` | | +| expanded | Adds a visual representation of a group element if the value is different from `undefined` | `string \| undefined` | | +| dragging | manage view of dragging element. Required for draggable list implementation | `boolean` | | diff --git a/src/components/useList/__stories__/docs/list-recursive-renderer.md b/src/components/useList/__stories__/docs/list-recursive-renderer.md new file mode 100644 index 0000000000..71558379e0 --- /dev/null +++ b/src/components/useList/__stories__/docs/list-recursive-renderer.md @@ -0,0 +1,64 @@ +### ListRecursiveRenderer + +The basic "renderer" of the `List` elements. When rendering, it retains the nested html structure. + +#### Props: + +| Name | Description | Type | Default | +| :--------- | :------------------------------------ | :-------------------: | :-----: | +| itemSchema | Simplified list representation schema | `ItemSchema` | | +| children | Children React element | `React.ReactNode` | | +| style | Inline styles if needed | `React.CSSProperties` | | +| className | Custom class name to mix with | `string` | | + +##### ItemSchema + +```ts +export type ItemSchema = { + id: ListItemId; + index: number; + children?: ItemSchema[]; +}; +``` + +#### Usage example: + +```tsx +import { + unstable_ListItemRecursiveRenderer as ListItemRecursiveRenderer, + unstable_ListContainerView as ListContainerView, + unstable_ListItemView as ListItemView, + unstable_useList as useList, + unstable_useListItemsClick as useListItemsClick, + unstable_getItemRenderState as getItemRenderState, +} from '@gravity-ui/uikit/unstable'; + +const items: ListItemType[] = [ + {data: 'one'}, + {data: 'two', children: [{data: 'tree', children: [{data: 'four'}, {data: 'five'}]}]}, +]; + +function List() { + const list = useList({items}); + const onItemClick = useListItemsClick({items}); + + return ( + + {list.structure.itemsSchema.map((itemSchema, index) => ( + + {(id) => { + const {props} = getItemRenderState({ + id: String(i), + mapItemDataToProps: (title) => ({title}), + onItemClick, + list, + }); + + return ; + }} + + ))} + + ); +} +``` diff --git a/src/components/useList/__stories__/docs/scroll-to-list-item.md b/src/components/useList/__stories__/docs/scroll-to-list-item.md new file mode 100644 index 0000000000..692ebfddd9 --- /dev/null +++ b/src/components/useList/__stories__/docs/scroll-to-list-item.md @@ -0,0 +1,27 @@ +### scrollToListItem; + +Utility to scroll into list item view by id and ref on container DOM element: + +#### Usage example: + +```tsx +import { + unstable_ListContainerView as ListContainerView, + unstable_scrollToListItem as scrollToListItem, +} from '@gravity-ui/uikit/unstable'; + +const containerRef = React.useRef(null); +// restoring focus when popup opens +React.useLayoutEffect(() => { + if (open) { + containerRef.current?.focus(); + list.state.setActiveItemId(selectedId ?? list.structure.visibleFlattenIds[0]); + + if (selectedId) { + scrollToListItem(selectedId, containerRef.current); + } + } +}, [open]); +// ... +; +``` diff --git a/src/components/useList/__stories__/docs/use-list-filter.md b/src/components/useList/__stories__/docs/use-list-filter.md new file mode 100644 index 0000000000..277837e07f --- /dev/null +++ b/src/components/useList/__stories__/docs/use-list-filter.md @@ -0,0 +1,54 @@ +### useListFilter + +Basic tree like structure list filtration logic and utilities. To avoid implementing custom filtering logic from scratch, first use this hook + +```tsx +import {unstable_useListKeydown as useListFilter} from '@gravity-ui/uikit/unstable'; +``` + +#### Props: + +| Name | Description | Type | Default | +| :----------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------: | :-----: | +| items | Original array of list items, same us used in the `useList` hook | `listItemType[]` | | +| initialFilterValue | The initial value of the filter; | `string` | | +| filterItem | The predicate function determines the principle of leaving elements in the original array. It works recursively, there is no need to implement custom logic to bypass the tree structure; | `(value: string, item: T): boolean;` | | +| filterItems | Completely redefine the filtering logic; | `(value: string, items: ListItemType[]) => ListItemType[]` | | +| debounceTimeout | With what delay to apply the filtering result | `number` | `300` | + +#### Returns: + +| Name | Description | Type | +| :------------- | :------------------------------------------------ | :---------------------------------: | +| filterRef | Ref to the DOM element of the filtering component | `React.RefObject` | +| filter | Current filter value | `string` | +| reset | Method for resetting filter value | `() => void` | +| items | List of filtered list elements | `listItemType[]` | +| onFilterUpdate | Callback on changing filter value | `(filter: string) => void` | + +#### Usage example: + +```tsx +import { + unstable_useList as useList, + unstable_useListKeydown as useListFilter, +} from '@gravity-ui/uikit/unstable'; + +const List = () => { + const {items, filter, onFilterUpdate, filterRef} = useListFilter({ + items: [...] + }) + + const list = useList({items}) + + return ( + <> + + + ) +} +``` diff --git a/src/components/useList/__stories__/docs/use-list-item-click.md b/src/components/useList/__stories__/docs/use-list-item-click.md new file mode 100644 index 0000000000..869996195e --- /dev/null +++ b/src/components/useList/__stories__/docs/use-list-item-click.md @@ -0,0 +1,34 @@ +### useListItemClick + +Basic click logic implemented for you + +```tsx +import {unstable_useListItemClick as useListItemClick} from '@gravity-ui/uikit/unstable'; +``` + +#### props: + +| Name | Description | Type | Default | +| :------- | :------------------------------------- | :-------: | :-----: | +| list | result of `useList` hook | `UseList` | | +| multiple | One or multiple elements selected list | `boolean` | | + +#### Result: + +onClick callback `(payload: {id: listItemId}) => void`; + +#### Usage example: + +```tsx +const filterState = useListFilter({items: [...]}); + +const list = useList({items: filterState.items}); + +const onItemClick = useListItemClick({list}); + +useListKeydown({ + containerRef, + onItemClick, + list, +}); +``` diff --git a/src/components/useList/__stories__/docs/use-list-keydown.md b/src/components/useList/__stories__/docs/use-list-keydown.md new file mode 100644 index 0000000000..6c3781bdb5 --- /dev/null +++ b/src/components/useList/__stories__/docs/use-list-keydown.md @@ -0,0 +1,32 @@ +### useListKeydown + +Keyboard support + +#### Props: + +| Name | Description | Type | Default | +| :----------- | :-------------------------------------------------------------------------------------------- | :------------------------------------------------: | :-----: | +| list | result of `useList` hook | `UseList` | | +| onItemClick | callback will be called when pressing the `Enter`, `Space` keys; | `(payload: {id: ListItemId}) => void` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| enabled | on/off keyboard support. Use it if you need to change the behavior in runtime; | `boolean` | | + +#### Usage example: + +```tsx +import { + unstable_useList as useList, + unstable_useListKeydown as useListKeydown, + unstable_useListItemClick as useListItemClick, +} from '@gravity-ui/uikit/unstable'; + +const containerRef = React.useRef(null); +const list = useList(...) +const handleItemClick = useListItemClick({list}); + +useListKeydown({ + onItemClick, + containerRef, + list, +}) +``` diff --git a/src/components/useList/__stories__/docs/use-list.md b/src/components/useList/__stories__/docs/use-list.md new file mode 100644 index 0000000000..ab2d39a672 --- /dev/null +++ b/src/components/useList/__stories__/docs/use-list.md @@ -0,0 +1,136 @@ +### useList + +The main hook to use what provide you normalized representation of list items (`structure`) and list state (`state`). + +#### Props: + +| Name | Description | Type | Default | +| :----------------- | :---------------------------------------------------------------------- | :-----------------------: | :--------: | +| items | a flat or tree-like data structure, with`List` declaration | `ListItemType[]` | | +| getItemId | Allows you to generate an id for a list item depending on the list data | `(itemData: T) => string` | | +| groupsDefaultState | Default group open state | `expanded`, `closed` | `expanded` | +| rootNodesGroups | Is nodes with children's groups | `boolean` | `true` | +| initialValues | Initial state values | `Partial` | | +| mixState | Way to override state by some controlled values. | `Partial` | | + +#### Result (UseList): + +| Name | Description | Type | +| :-------- | :----------------------------------------------------------------------------------- | :-------------: | +| state | List state to control and store current state values | `ListState` | +| structure | Normalized representation of list and some helpful data structures to work with list | `ListStructure` | + +#### ListState: + +| Name | Description | Type | +| :-------------- | :----------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------: | +| selectedById | Key-value selected elements state | `Record` | +| disabledById | Key-value disabled elements state | `Record` | +| expandedById | Key-value expanded elements state. Available is only `rootNodesGroups` option of `useList` hook is `true` | `Record` | +| activeItemId | Active item id | `ListItemId`, `undefined` | +| setSelected | Method to handle selected state list items state | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | +| setDisabled | Method to handle disable state list items state | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | +| setExpanded | Method to handle expanded state list items state. Available is only `rootNodesGroups` option of `useList` hook is `true` | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | +| setExpanded | Normalized representation of list and some helpful data structures to work with list | `ListStructure` | +| setActiveItemId | Method to handle current active list item state | `(listItemId: ListItemId or undefined) => void` | + +#### ListStructure: + +| Name | Description | Type | +| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------: | +| items | Link for original `items` property | `ListItemType` | +| itemsState | List item state | `Record` | +| groupState | A normalized representation of metadata about a group if the item is both a list item and a group: | `Record` | +| itemsById | The default IDs are formed according to the principle `-`. To make a custom `id`, you need to use it either when forming an array of `items` or through the`getItemId` function. | `Record` | +| visibleFlattenIds | Sequential representation of list items by id, taking into account invisible elements inside collapsed groups. Items visibility determine by `expandedById` state | `ListItemId[]` | +| idToFlattenIndex | Auxiliary data structure for quick get item index by id. Needed, for example, for DnD things with list | `Record` | +| itemsSchema | Item structure to use in custom view implementations if needed. Items visibility determine by `expandedById` state | `ItemSchema[]` | + +##### ItemSchema + +```ts +export type ItemSchema = { + id: ListItemId; + index: number; + children?: ItemSchema[]; +}; +``` + +#### Item variants + +```ts +const simple: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; + +const arbitraryObject: ListItemType<{text: string}>[] = [ + {text: 'one'}, + {text: 'two'}, + {text: 'free'}, + {text: 'four'}, + {text: 'five'}, +]; + +const withNestedChildren: ListItemType[] = [ + {data: 'one'}, + {data: 'two', children: [{data: 'tree', children: [{data: 'four'}, {data: 'five'}]}]}, +]; + +const withNestedChildrenComplexExample: ListItemType[] = [ + {disabled: true, data: {title: 'one', id: '1'}}, + { + expanded: true, + data: {title: 'two', id: '2'}, + children: [ + { + data: {title: 'tree', id: '3'}, + children: [{data: {title: 'four', id: '4'}}, {data: {title: 'five', id: '5'}}], + }, + ], + }, +]; +``` + +#### Object decl reserved properties: + +```tsx +interface ListItemInitialProps { + /** + * If you need to control the state from the outside, + * you can set a unique id for each element + */ + id?: string; + /** + * Initial disabled item state + */ + disabled?: boolean; + /** + * Initial selected item state + */ + selected?: boolean; + /** + * Default expanded state if group + */ + expanded?: boolean; +} + +type ListFlattenItemType = T & ListItemInitialProps; + +interface ListTreeItemType extends ListItemInitialProps { + data: T; + children?: ListTreeItemType[]; +} + +export type ListItemType = ListTreeItemType | ListFlattenItemType; +``` + +#### ControlledValues example: + +```tsx +const [selectedById] = React.useState>({}); + +const list = useList({ + // outer controlled state + mixState: { + selectedById, + }, +}); +``` diff --git a/src/components/useList/__stories__/useList.mdx b/src/components/useList/__stories__/useList.mdx deleted file mode 100644 index 189da940b7..0000000000 --- a/src/components/useList/__stories__/useList.mdx +++ /dev/null @@ -1,780 +0,0 @@ -import {Meta} from '@storybook/addon-docs'; - - - -# useList - -A set of hooks for creating stateless `List` components; - -The basic idea is that hooks take all the complex logic on themselves, and all you have to do is implement the "dumb" components of the view; - -`Storybook` provides complex examples how to use this components from this documentation. - -### Hooks: - -- [useList](#uselist-1); -- [useListKeydown](#uselistkeydown) -- [useListFilter](#uselistfilter); -- [useListState](#useliststate); - -### Components (View only): - -- [ListItemView](#listitemview); -- [ListContainerView](#listcontainerview); -- [ListRecursiveRenderer](#listrecursiverenderer); - -### Utilitys: - -- [computeItemSize](#computeitemsize); -- [scrollToListItem](#scrolltolistitem); -- [getItemRenderState](#getitemrenderstate); -- [getListParsedState](#getlistparsedstate); -- [getListItemQa](#getlistitemqa); - -## Quick code snippets for beginners: - -### flatten items: - -```tsx -import { - type unstable_ListItemId as ListItemId, - type unstable_ListItemType as ListItemType, - unstable_ListContainerView as ListContainerView, - unstable_ListItemView as ListItemView, - unstable_getItemRenderState as getItemRenderState, - unstable_useList as useList, - unstable_useListKeydown as useListKeydown, - unstable_useListState as useListState, -} from '@gravity-ui/uikit/unstable'; - -const items: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; - -function List() { - const containerRef = React.useRef(null); - - const listState = useListState(); - const list = useList({ - items, - ...listState, - }); - - useListKeydown({ - onItemClick, - containerRef, - ...list, - ...listState, - }); - - return ( - - {list.items.map((_, i) => { - const { - data, - props, - context: _context, - } = getItemRenderState({ - id: String(i), - mapItemDataToProps: (title) => ({title}), - onItemClick, - ...list, - ...listState, - }); - - return ; - })} - - ); - - // multiple selection - function onItemClick(id: ListItemId) { - listState.setSelected((prevState) => ({ - ...prevState, - [id]: !prevState[id], - })); - - listState.setActiveItemId(id); - } -} -``` - -### tree item structure: - -```tsx -import { - unstable_ListItemRecursiveRenderer as ListItemRecursiveRenderer, - unstable_ListContainerView as ListContainerView, - unstable_ListItemView as ListItemView, - unstable_getItemRenderState as getItemRenderState, -} from '@gravity-ui/uikit/unstable'; - -const items: ListItemType[] = [ - {data: 'one'}, - {data: 'two', children: [{data: 'tree', children: [{data: 'four'}, {data: 'five'}]}]}, -]; - -function List() { - // same as prev example - return ( - - {list.items.map((item, index) => ( - - {(id) => { - const {props} = getItemRenderState({ - id: String(i), - mapItemDataToProps: (title) => ({title}), - onItemClick, - ...list, - ...listState, - }); - - return ; - }} - - ))} - - ); -} -``` - -## Hooks: - -### useList - -The main hook for creating a stateless version of the sheet. - -#### Props: - -- `items` - `ListItemType[]` - a flat or tree-like data structure, with `List` declaration: - - ##### Item structure variants - - ```ts - const simple: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; - - const arbitraryObject: ListItemType<{text: string}>[] = [ - {text: 'one'}, - {text: 'two'}, - {text: 'free'}, - {text: 'four'}, - {text: 'five'}, - ]; - - const withNestedChildren: ListItemType[] = [ - {data: 'one'}, - {data: 'two', children: [{data: 'tree', children: [{data: 'four'}, {data: 'five'}]}]}, - ]; - - const withNestedChildrenComplexExample: ListItemType[] = [ - {disabled: true, data: {title: 'one', id: '1'}}, - { - expanded: true, - data: {title: 'two', id: '2'}, - children: [ - { - data: {title: 'tree', id: '3'}, - ex - children: [{data: {title: 'four', id: '4'}}, {data: {title: 'five', id: '5'}}], - }, - ], - }, - ]; - ``` - - ##### Object decl reserved propeties: - - ```tsx - interface ListItemInitialProps { - /** - * If you need to control the state from the outside, - * you can set a unique id for each element - */ - id?: string; - /** - * Initial disabled item state - */ - disabled?: boolean; - /** - * Initial selected item state - */ - selected?: boolean; - /** - * Default expanded state if group - */ - expanded?: boolean; - } - - type ListFlattenItemType = T & ListItemInitialProps; - - interface ListTreeItemType extends ListItemInitialProps { - data: T; - children?: ListTreeItemType[]; - } - - export type ListItemType = ListTreeItemType | ListFlattenItemType; - ``` - -- `expandedById` - state for open/closed `List` elements. Affects the formation of the `visibleFlattenIds` - if the element id in this object is set to `false` - all elements of this group and all nested groups will not be present in the final ids order; -- `getItemId` - the property is optional. Allows you to generate an id for a list item depending on the list data: - -```tsx -const items = [ - {data: {id: 'id-1', title: 'some title 1'}, children: [...]}, - {data: {id: 'id-2', title: 'some title 2'}, children: [...]}, -]; - -/** - * itemsById: { - * 'id-1': {id: 'id-1', title: 'some title 1'}, - * 'id-2': {id: 'id-2', title: 'some title 2'}, - * } - */ -const {byid} = useList({ - items, - getItemId: ({id}) => id, -}) -``` - -#### Returned data: - -- `itemsState` - a normalized representation of meta information for each element of the list - features: - - - `parentId` - Id of the parent element, if there is a parent; - - `indentation` - Nesting level; - -- `itemsById` - normalized representation of list items: - - ```tsx - export type ParsedState = { - // ... - itemsById: Record; - // ... - }; - - const items = [ - {data: {title: 'title-1'}, children: [{data: {title: 'title-1-1'}, children: []}]}, - {data: {title: 'title-2'}, children: []}, - ]; - // -> - const itemsById: { - 0: {title: 'title-1'}; - '0-0': {title: 'title-1-1'}; - 1: {title: 'title-2'}; - }; - ``` - - The default IDs are formed according to the principle `-`. To make a custom `id`, you need to use it either when forming an array of `items` or through the`getItemId` function. - -- `groupsState` - a normalized representation of metadata about a group if the item is both a list item and a group: - - `childrenIds` - list of child element IDs; -- `visibleFlattenIds` - sequential representation of list items by id, taking into account invisible elements inside collapsed groups; - -### useListKeydown - -Keyboard support - -#### Props: - -- `disabledById` - key-value representation of disabled elements that do not need to be taken into account when navigating through the `List`; -- `activeItemId` - current active item `id`; -- `visibleFlattenIds` - a flat list of elements to be navigated through; Collapsed groups must be taken into account in this array; -- `onItemClick` - callback will be called when pressing the `Enter`, `Space` keys; -- `containerRef` - a reference to the DOM element of the List container inside which to search for its elements; -- `setActiveItemId` - Callback for setting the current active element; -- `enabled` - on/off keyboard support. Use it if you need to change the behavior in runtime; - -```tsx -import { - unstable_useList as useList, - unstable_useListKeydown as useListKeydown, - unstable_useListState as useListState, -} from '@gravity-ui/uikit/unstable'; - -const containerRef = React.useRef(null); -const listState = useListState() -const list = useList(...) - -const handleItemClick = () => {...}; - -useListKeydown({ - onItemClick: handleItemClick, - containerRef, - ...list, - ...listState, -}) -``` - -### useListFilter - -#### Props: - -- `items` - original array of `listItemType[]`, same us used in the `useList` hook; -- `initialFilterValue` - the initial value of the filter; -- `filterItem` - the predicate function determines the principle of leaving elements in the original array. It works recursively, there is no need to implement custom logic to bypass the tree structure; -- `filterItems` - completely redefine the filtering logic; -- `debounceTimeout` - with what delay to apply the filtering result. By default, `300ms`; - -#### Returns: - -- `filterRef` - ref to the DOM element of the filtering component; -- `filter` - current filter value; -- `reset` - method for resetting the filter value; -- `items` - list of filtered sheet elements `listItemType[]`; -- `onFilterUpdate` - callback for changing the filter value; - -```tsx -import { - unstable_useList as useList, - unstable_useListKeydown as useListFilter, -} from '@gravity-ui/uikit/unstable'; - -const List = () => { - const {items, filter, onFilterUpdate, filterRef} = useListFilter({ - items: [...] - }) - - const list = useList({ - items, - }) - - return ( - <> - - - ) -} -``` - -### useListState - -The basic hook for managing the state of the `List`. You can use your own implementation, the main thing is to understand about the concept of the `state` of the sheet. Which corresponds to the following interface: - -```tsx -import {unstable_useListState as useListState} from '@gravity-ui/uikit/unstable'; - -type ListState = { - disabledById: Record; - selectedById: Record; - expandedById: Record; - activeItemId?: ListItemId; -}; - -const { - disabledById, - setDisabled, - selectedById, - setSelected, - expandedById, - setExpanded, - activeItemId, - setActiveItemId, -} = useListState(); -``` - -#### props: - -```tsx -interface UseListStateProps { - /** - * Initial state values - */ - initialValues?: Partial; - /** - * Ability to pass link to another state value - */ - controlledValues?: Partial; -} -``` - -##### controlledValues example: - -```tsx -const listState = useListState(); - -// inside your component -const innerListState = useListState({ - controlledValues: listState, -}); -``` - -## Components: - -### ListItemView - -The basic component responsible for the appearance of the list items. -Use it even if the functionality of the `useList` hook seems redundant to you - -```tsx -import { - type unstable_ListItemType as ListItemType, - unstable_ListItemView as ListItemView, -} from '@gravity-ui/uikit/unstable'; - -type Entity = {title: stirng, subtitle: string, icon: React.ReactNode}; - -const items: ListItemType[] = [ - {title: 'some title 1', subtitle: 'some subtitle 1', icon: }, - {title: 'some title 2', subtitle: 'some subtitle 2', icon: }, -]; - -const List = () => { - return ( - <> - {items.map(item, i) => { - return ( - - ) - }} - - ) -}; -``` - -#### Props: - -- `id` - required prop. Set `[data-list-item="${id}"]` data attribute. By this it core list engine finds elements to scroll to. -- `title` - base required prop to use. If passed string, applas default component styles according desig system. Pass you own componnet if you wont custom behaviour; -- `as` - if needed, override `html` tag. By default - `li`; -- `size` - the size of the element. This also affects the rounding radius of the list element . By default, `m`. Available sizes are `s`, `m`, `l` and `xl`; -- `height` - the height of the element in pixels. By default, it is calculated depending on the `size` parameter and the presence of the `subtitle` parameter; -- `selected` - the selected state of the component; affects the `activeOnHover` if value if the value is different from `undefined`; -- `active` - the state when the element is in the user's focus, but not selected. It can also be used when you drag an element; -- `disabled` - the disabled state. It also prevents clicking on an element; -- `activeOnHover`- directly control hover behaviour; -- `indentation` - affects the visual indentation of the element content; -- `hasSelectionIcon` - show selected icon if selected and reserve space for this icon; -- `onClick` - on item click callback. !Note: if passed this and `disabled` option is `true` click will not be appear; -- `style` - optional react `React.CSSProperties` object; -- `subtitle` - Slot under `title`. If passed string apply prefefined styles. Or you can pass custom `React.ReactNode` to use you own behaviour; -- `startSlot` - custom slot before `title`; -- `endSlot` - custom slot after `title`; -- `corners` - Prop to remove default border radiuses from element; -- `className` - custom class name to mix with; -- `expanded` - adds a visual representation of a group element if the value is different from `undefined`; -- `dragging` - manage view of dragging element. Required for graggable list implementation - -### ListContainerView - -The default container for all custom lists. Contains all html attributes and styles for quick use in your projects. - -#### Props: - -- `id` - optional id attribute; -- `className` - custom class name to mix with; -- `fixedHeight` - removes default `overflow: auto` from container and set fixed container height (`--g-list-height` = `300px`); - -```tsx -const containerRef = React.useRef(null); - - - - -; -``` - -### ListRecursiveRenderer - -The basic "renderer" of the `List` elements. When rendering, it retains the nested html structure. -You can use it as an example if you need to implement hiding/closing groups of elements with animation. -For the virtualized version of the list, you need to implement a component with a similar interface, see the examples from storybook. - -#### Props: - -- `itemSchema` - base list item (`ListItemType`); -- `children` - render list item function; -- `index` - the ordinal index of the first level of the sheet elements; -- `expandedById` - state for hidden group elements, if the functionality of hiding/opening groups is supported -- `className` - custom class name to mix with; -- `getItemId` - the property is optional. Allows you to generate an id for a list item depending on the list data: -- `style` - optional react `React.CSSProperties` object; - -```tsx -import { - unstable_ListItemView as ListItemView, - unstable_ListContainerView as ListContainerView, - unstable_ListItemRecursiveRenderer as ListItemRecursiveRenderer, - type unstable_ListItemType as ListItemType, - unstable_getItemRenderState as getItemRenderState, -} from '@gravity-ui/uikit/unstable'; - -type Entity = {text: string}: - -const items: ListItemType[] = [{data: {text: 'one'}}, {data: {text: 'two'}}] - - - {items.map((item, index) => ( - - {(id) => { - const {props} = getItemRenderState({ - qa: 'some-qa-id', - id, - multiple: false, - size: 'm', - mapItemDataToProps: (item) => ({title: item.title}), - ...list, - ...listState, - }); - - return ; - }} - - ))} -; -``` - -## Utilitys - -### computeItemSize; - -Utility to compute list item height: - -```tsx - - computeItemSize( - // list size - size, - // has subrows - Boolean(get(itemsById[visibleFlattenIds[index]], 'subtitle')), - ) - } -/> -``` - -### scrollToListItem; - -Utility to sroll into list item view by id and ref on container DOM element: - -```tsx -import { - unstable_ListContainerView as ListContainerView, - unstable_scrollToListItem as scrollToListItem, -} from '@gravity-ui/uikit/unstable'; - -const containerRef = React.useRef(null); -// restoring focus when popup opens -React.useLayoutEffect(() => { - if (open) { - containerRef.current?.focus(); - listState.setActiveItemId(selectedId ?? list.visibleFlattenIds[0]); - - if (selectedId) { - scrollToListItem(selectedId, containerRef.current); - } - } -}, [open]); -// ... -; -``` - -### getItemRenderState; - -Map list state to ListItemView render props; - -```tsx -import { - unstable_ListItemView as ListItemView, - unstable_getItemRenderState as getItemRenderState, - unstable_useListState as useListState, - unstable_useList as useList, -} from '@gravity-ui/uikit/unstable'; - -const list = useList(); -const listState = useListState(); - -const {data, props, context} = getItemRenderState({ - qa: 'some-qa-id', - id, - multiple: true, - size, // list size - onItemClick: (id: ListItemId) => { - // ... - }, - mapItemDataToProps: (item) => ({title: item.title}), - ...list, - ...listState, -}); - -return ; -``` - -#### Props: - -```tsx -type ListItemSize = 's' | 'm' | 'l' | 'xl'; -type ListItemid = string; - -type GetItemRenderStateProps = { - /** - * `id` of list item; - */ - id: ListItemid; - /** - * map item data to view render props with `ListItemCommonProps` interface - */ - mapItemDataToProps(data: T): { - title: React.ReactNode; - endIcon?: React.ReactNode; - startIcon: React.ReactNode; - subtitle?: React.ReactNode; - }; - size: ListItemSize; - /** - * Affects the view of the selected items; - */ - multiple?: boolean; - /** - * Group expanded initial state. Default value `true` - */ - defaultExpanded?: boolean; - onItemClick?(id: ListItemid): void; -} & ReturnType & - ReturnType; -``` - -#### Returns: - -##### item data (`T`); - -```tsx -item = { - data: T, - children: [...] -} -// or, if flatten list declaration variant -item = T -``` - -##### item state props: - -```tsx -type ListItemSize = 's' | 'm' | 'l' | 'xl'; - -interface ItemRenderProps { - // item id; - id: string; - // qa attribute for tests - qa?: string; - // item size; - size: ListItemSize; - // expanded state if item group; - expanded?: boolean; - // is item active - active: boolean; - // item nest level; - indentation: number; - // is item disabled; - disabled: boolean; - // is item selected; - selected?: boolean; - // on item click handle if exists; - onClick?(): void; - // affects the view of the selected items; - hasSelectionIcon?: boolean; - // one required field of result `mapItemDataToProps` function work; - title: React.ReactNode; -} -``` - -##### item list context: - -```tsx -/** - * useful information about the current list item: - */ -interface ItemContext { - // meta info about item - itemState: { - // integer number, representing nested list level - indentation: number; - // `id` of parent list item if it exists - parentId?: string; - }; - // An optional parameter. If the list item is also the first item of the nested list - groupState: { - // array of `id` of nested list items; - childrenIds: string[]; - }; - // is the current item the last one in the list. For example needed to implement custom infinity lists variants - isLastItem: boolean; -} -``` - -```tsx -import { - unstable_ListContainerView as ListItemRecursiveRenderer, - unstable_ListItemView as ListItemView, - unstable_getItemRenderState as getItemRenderState, - unstable_useList as useList, - unstable_useListState as useListState, -} from '@gravity-ui/uikit/unstable'; - -const listState = useListState(); -const list = useList({ - items, - ...listState, -}); -const handleItemClick = () => {}; - - - {(id) => { - const {data, props} = getItemRenderState({ - qa: 'some-qa-id', - id, - multiple: false, - size, // list size - onItemClick: handleItemClick, - mapItemDataToProps, - ...list, - ...listState, - }); - - return ; - }} -; -``` - -### getListParsedState; - -same as `useList` hook functionality in stateless function. Use it if you need to extract initial list state form declaration: - -```tsx -import {unstable_getListParsedState as getListParsedState} from '@gravity-ui/uikit/unstable'; - -// custom controlled state from computed initial state -const [expandedById, setExpanded] = React.useState( - () => getListParsedState(items).initialState.expandedById, -); -``` - -### getListItemQa - -Function is used to generate `qa` attributes in list items; -Also use this function in tests to create a unique data attribute for accessing a specific list item. - -```ts -import {unstable_getListItemQa as getListItemQa} from '@gravity-ui/uikit/unstable'; - -await locator.getByTestId(getListItemQa('some-list-qa', '0')); // select the first item in the list if auto-generated `id` are used -``` diff --git a/src/components/useList/components/ListContainerView/ListContainerView.tsx b/src/components/useList/components/ListContainerView/ListContainerView.tsx index fa05e7ae34..9d06ac9242 100644 --- a/src/components/useList/components/ListContainerView/ListContainerView.tsx +++ b/src/components/useList/components/ListContainerView/ListContainerView.tsx @@ -16,6 +16,7 @@ export interface ListContainerViewProps extends QAProps { id?: string; role?: React.AriaRole; className?: string; + style?: React.CSSProperties; /** * Removes `overflow: auto` from container and set fixed container size (`--g-list-height` = `300px`) */ @@ -26,7 +27,7 @@ export interface ListContainerViewProps extends QAProps { export const ListContainerView = React.forwardRef( function ListContainerView( - {as = 'div', role = 'listbox', children, id, className, fixedHeight, extraProps, qa}, + {as = 'div', role = 'listbox', children, id, className, fixedHeight, extraProps, qa, style}, ref, ) { return ( @@ -39,6 +40,7 @@ export const ListContainerView = React.forwardRef diff --git a/src/components/useList/components/ListItemView/ListItemView.scss b/src/components/useList/components/ListItemView/ListItemView.scss index 5fab17fa17..b589561020 100644 --- a/src/components/useList/components/ListItemView/ListItemView.scss +++ b/src/components/useList/components/ListItemView/ListItemView.scss @@ -30,6 +30,9 @@ $block: '.#{variables.$ns}list-item-view'; &_dragging#{$block}_selected, &_dragging#{$block}_active { background: var(--g-color-base-simple-hover-solid); + // more than `Sheet` z-index (100000) if `ListItemView` used in this component + /* stylelint-disable-next-line declaration-no-important */ + z-index: 100001 !important; } &_radius_s { diff --git a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx index ccac8a7978..2a9d5be1ec 100644 --- a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx +++ b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx @@ -6,12 +6,12 @@ import {Avatar} from '../../../../Avatar'; import {DropdownMenu} from '../../../../DropdownMenu'; import {Text} from '../../../../Text'; import {Flex, sp} from '../../../../layout'; -import {useListState} from '../../../hooks/useListState'; +import type {ListItemId} from '../../../../useList/types'; import {ListItemView as ListItemViewComponent} from '../ListItemView'; import type {ListItemViewProps} from '../ListItemView'; export default { - title: 'Unstable/useList/ListItemView', + title: 'Lab/useList/ListItemView', component: ListItemViewComponent, parameters: { a11y: { @@ -198,7 +198,8 @@ const stories: ListItemViewProps[] = [ ]; const ListItemViewTemplate: StoryFn = () => { - const listState = useListState(); + const [expandedById, setExpandedById] = React.useState>({}); + const [selectedById, setSelectedById] = React.useState>({}); return ( @@ -206,8 +207,8 @@ const ListItemViewTemplate: StoryFn = () => { ))} @@ -219,12 +220,12 @@ const ListItemViewTemplate: StoryFn = () => { return () => { if (isGroup) { - listState.setExpanded((prevState) => ({ + setExpandedById((prevState) => ({ ...prevState, [id]: typeof prevState[id] === 'undefined' ? !expanded : !prevState[id], })); } else { - listState.setSelected((prevState) => ({ + setSelectedById((prevState) => ({ ...prevState, [id]: !prevState[id], })); diff --git a/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx b/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx index 5711469193..791dda44ea 100644 --- a/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx +++ b/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx @@ -1,55 +1,32 @@ import React from 'react'; import {block} from '../../../utils/cn'; -import type {ListItemId, ListItemType, ListState} from '../../types'; -import {getListItemId} from '../../utils/getListItemId'; -import {getGroupItemId} from '../../utils/groupItemId'; -import {isTreeItemGuard} from '../../utils/isTreeItemGuard'; +import type {ItemSchema, ListItemId} from '../../types'; import './ListRecursiveRenderer.scss'; const b = block('list-recursive-renderer'); -export interface ListRecursiveRendererProps extends Partial> { - itemSchema: ListItemType; - children(id: ListItemId, index: number): React.JSX.Element; - index: number; - parentId?: string; +export interface ListRecursiveRendererProps { + itemSchema: ItemSchema; className?: string; - getItemId?(item: T): ListItemId; style?: React.CSSProperties; - idToFlattenIndex: Record; + children(id: ListItemId, index: number): React.JSX.Element; } // Saves the nested html structure for tree data structure -export function ListItemRecursiveRenderer({ - itemSchema, - index, - parentId, - ...props -}: ListRecursiveRendererProps) { - const groupedId = getGroupItemId(index, parentId); - const id = getListItemId({item: itemSchema, groupedId, getItemId: props.getItemId}); - - const node = props.children(id, props.idToFlattenIndex[id]); +export function ListItemRecursiveRenderer({itemSchema, ...props}: ListRecursiveRendererProps) { + const id = itemSchema.id; - if (isTreeItemGuard(itemSchema) && itemSchema.children) { - const isExpanded = - props.expandedById && id in props.expandedById ? props.expandedById[id] : true; + const node = props.children(id, itemSchema.index); + if (itemSchema.children) { return (
    {node} - {isExpanded && - itemSchema.children.map((item, index) => ( - - ))} + {itemSchema.children.map((item, index) => ( + + ))}
); } diff --git a/src/components/useList/hooks/useFlattenListItems.ts b/src/components/useList/hooks/useFlattenListItems.ts index 6043e53ee9..560fecee5e 100644 --- a/src/components/useList/hooks/useFlattenListItems.ts +++ b/src/components/useList/hooks/useFlattenListItems.ts @@ -21,7 +21,7 @@ export function useFlattenListItems({ getItemId, }: UseFlattenListItemsProps) { const order = React.useMemo(() => { - return flattenItems(items, expandedById, getItemId); + return flattenItems({items, expandedById, getItemId}); }, [items, expandedById, getItemId]); return order; diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index b3153afb2a..d5ebf727d7 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -1,50 +1,87 @@ /* eslint-disable valid-jsdoc */ -import type { - InitialListParsedState, - ListItemId, - ListItemType, - ListParsedState, - ListState, -} from '../types'; +import React from 'react'; + +import type {InitialListParsedState, UseList} from '../types'; import {useFlattenListItems} from './useFlattenListItems'; import {useListParsedState} from './useListParsedState'; +import type {UseListParsedStateProps} from './useListParsedState'; +import {useListState} from './useListState'; +import type {UseListStateProps} from './useListState'; -export interface UseListProps extends Partial { - items: ListItemType[]; - /** - * Control expanded items state from external source - */ - getItemId?(item: T): ListItemId; +interface UseListProps extends UseListParsedStateProps, UseListStateProps { + mixState?: Partial; } -export type UseListResult = ListParsedState & {initialState: InitialListParsedState}; - /** - * Take array of items as a argument and returns parsed representation of this data structure to work with + * Take array of items as a argument with params described what type of list initial data represents. */ -export const useList = ({items, expandedById, getItemId}: UseListProps): UseListResult => { +export const useList = ({ + items, + getItemId, + groupsDefaultState = 'expanded', + rootNodesGroups = true, + initialValues, + mixState, +}: UseListProps): UseList => { const {itemsById, groupsState, itemsState, initialState} = useListParsedState({ items, getItemId, + groupsDefaultState, + }); + + const initValues = React.useMemo(() => { + return { + expandedById: {...initialValues?.expandedById, ...initialState.expandedById}, + selectedById: {...initialValues?.selectedById, ...initialState.selectedById}, + disabledById: {...initialValues?.disabledById, ...initialState.disabledById}, + }; + }, [ + initialState.disabledById, + initialState.expandedById, + initialState.selectedById, + initialValues?.disabledById, + initialValues?.expandedById, + initialValues?.selectedById, + ]); + + const innerState = useListState({ + initialValues: initValues, + rootNodesGroups, }); - const {visibleFlattenIds, idToFlattenIndex} = useFlattenListItems({ + const {visibleFlattenIds, idToFlattenIndex, itemsSchema} = useFlattenListItems({ items, /** * By default controlled from list items declaration state */ - expandedById: expandedById || initialState.expandedById, + expandedById: innerState.expandedById, getItemId, }); + const realState = React.useMemo(() => { + if (mixState) { + return { + ...innerState, + expandedById: {...innerState.expandedById, ...mixState?.expandedById}, + selectedById: {...innerState.selectedById, ...mixState?.selectedById}, + disabledById: {...innerState.disabledById, ...mixState?.disabledById}, + }; + } + + return innerState; + }, [mixState, innerState]); + return { - items, - visibleFlattenIds, - idToFlattenIndex, - itemsById, - groupsState, - itemsState, - initialState, + state: realState, + structure: { + itemsSchema, + items, + visibleFlattenIds, + idToFlattenIndex, + itemsById, + groupsState, + itemsState, + }, }; }; diff --git a/src/components/useList/hooks/useListFilter.ts b/src/components/useList/hooks/useListFilter.ts index 3c9e853b6a..979893b61c 100644 --- a/src/components/useList/hooks/useListFilter.ts +++ b/src/components/useList/hooks/useListFilter.ts @@ -21,24 +21,26 @@ interface UseListFilterProps { * Override only logic with item affiliation */ filterItem?(value: string, item: T): boolean; + onFilterChange?(value: string): void; debounceTimeout?: number; initialFilterValue?: string; } /** * Ready-to-use logic for filtering tree-like data structures + * * ```tsx * const {item: filteredItems,...listFiltration} = useListFIlter({items}); * const list = useList({items: filteredItems}); * * * ``` - * @returns - */ export function useListFilter({ items: externalItems, initialFilterValue = '', filterItem, + onFilterChange, filterItems, debounceTimeout = 300, }: UseListFilterProps) { @@ -79,14 +81,16 @@ export function useListFilter({ return { reset: () => { setFilter(initialFilterValue); + onFilterChange?.(initialFilterValue); debouncedFn(initialFilterValue); }, onFilterUpdate: (nextFilterValue: string) => { setFilter(nextFilterValue); + onFilterChange?.(nextFilterValue); debouncedFn(nextFilterValue); }, }; - }, [debouncedFn, initialFilterValue]); + }, [debouncedFn, initialFilterValue, onFilterChange]); return { filterRef, diff --git a/src/components/useList/hooks/useListItemClick.ts b/src/components/useList/hooks/useListItemClick.ts new file mode 100644 index 0000000000..4986b3fb6a --- /dev/null +++ b/src/components/useList/hooks/useListItemClick.ts @@ -0,0 +1,29 @@ +import type {ListItemId, UseList} from '../types'; + +interface UseListItemClickOptions { + multiple?: boolean; + list: UseList; +} + +export const useListItemClick = ({list, multiple}: UseListItemClickOptions) => { + const onItemClick = ({id}: {id: ListItemId}) => { + if (list.state.disabledById[id]) return; + + // always activate selected item + list.state.setActiveItemId(id); + + if (list.state.expandedById && id in list.state.expandedById && list.state.setExpanded) { + list.state.setExpanded((prevState) => ({ + ...prevState, + [id]: !prevState[id], // expanded by id + })); + } else { + list.state.setSelected((prevState) => ({ + ...(multiple ? prevState : {}), + [id]: multiple ? !prevState[id] : true, // always select on click in single select variant + })); + } + }; + + return onItemClick; +}; diff --git a/src/components/useList/hooks/useListKeydown.tsx b/src/components/useList/hooks/useListKeydown.tsx index 802b0f4b42..4f976d5683 100644 --- a/src/components/useList/hooks/useListKeydown.tsx +++ b/src/components/useList/hooks/useListKeydown.tsx @@ -1,57 +1,58 @@ import React from 'react'; import {KeyCode} from '../../../constants'; -import type {ListItemId, ListState} from '../types'; +import type {ListItemId, UseList} from '../types'; import {findNextIndex} from '../utils/findNextIndex'; import {scrollToListItem} from '../utils/scrollToListItem'; -interface UseListKeydownProps extends Partial> { - visibleFlattenIds: ListItemId[]; - onItemClick?(itemId: ListItemId): void; +interface UseListKeydownProps { + onItemClick?(payload: {id: ListItemId}): void; containerRef?: React.RefObject; - setActiveItemId?(id: ListItemId): void; enabled?: boolean; + list: UseList; } // Use this hook if you need keyboard support for tree structure lists -export const useListKeydown = ({ - visibleFlattenIds, - onItemClick, - containerRef, - disabledById = {}, - activeItemId, - setActiveItemId, - enabled, -}: UseListKeydownProps) => { +export const useListKeydown = ({containerRef, onItemClick, enabled, list}: UseListKeydownProps) => { const activateItem = React.useCallback( (index?: number, scrollTo = true) => { - if (typeof index === 'number' && visibleFlattenIds[index]) { + if (typeof index === 'number' && list.structure.visibleFlattenIds[index]) { if (scrollTo) { - scrollToListItem(visibleFlattenIds[index], containerRef?.current); + scrollToListItem( + list.structure.visibleFlattenIds[index], + containerRef?.current, + ); } - setActiveItemId?.(visibleFlattenIds[index]); + list.state.setActiveItemId?.(list.structure.visibleFlattenIds[index]); } }, - [containerRef, visibleFlattenIds, setActiveItemId], + [list.structure.visibleFlattenIds, list.state, containerRef], ); const handleKeyMove = React.useCallback( (event: KeyboardEvent, step: number, defaultItemIndex = 0) => { event.preventDefault(); - const maybeIndex = visibleFlattenIds.findIndex((i) => i === activeItemId); + const maybeIndex = list.structure.visibleFlattenIds.findIndex( + (i) => i === list.state.activeItemId, + ); const nextIndex = findNextIndex({ - list: visibleFlattenIds, + list: list.structure.visibleFlattenIds, index: (maybeIndex > -1 ? maybeIndex : defaultItemIndex) + step, step: Math.sign(step), - disabledItems: disabledById, + disabledItems: list.state.disabledById, }); activateItem(nextIndex); }, - [activateItem, activeItemId, disabledById, visibleFlattenIds], + [ + activateItem, + list.state.activeItemId, + list.state.disabledById, + list.structure.visibleFlattenIds, + ], ); React.useLayoutEffect(() => { @@ -73,10 +74,13 @@ export const useListKeydown = ({ } case KeyCode.SPACEBAR: case KeyCode.ENTER: { - if (activeItemId && !disabledById[activeItemId]) { + if ( + list.state.activeItemId && + !list.state.disabledById[list.state.activeItemId] + ) { event.preventDefault(); - onItemClick?.(activeItemId); + onItemClick?.({id: list.state.activeItemId}); } break; } @@ -90,5 +94,12 @@ export const useListKeydown = ({ return () => { anchor.removeEventListener('keydown', handleKeyDown); }; - }, [activeItemId, containerRef, disabledById, enabled, handleKeyMove, onItemClick]); + }, [ + containerRef, + enabled, + handleKeyMove, + list.state.activeItemId, + list.state.disabledById, + onItemClick, + ]); }; diff --git a/src/components/useList/hooks/useListParsedState.ts b/src/components/useList/hooks/useListParsedState.ts index cfa26652f4..933d9a0c26 100644 --- a/src/components/useList/hooks/useListParsedState.ts +++ b/src/components/useList/hooks/useListParsedState.ts @@ -1,25 +1,25 @@ /* eslint-disable valid-jsdoc */ import React from 'react'; -import type {ListItemId, ListItemType} from '../types'; import {getListParsedState} from '../utils/getListParsedState'; +import type {GetListParsedStateProps} from '../utils/getListParsedState'; -interface UseListParsedStateProps { - items: ListItemType[]; - /** - * List item id dependant of data - */ - getItemId?(item: T): ListItemId; -} +export interface UseListParsedStateProps extends GetListParsedStateProps {} /** * From the tree structure of list items we get meta information and * flatten list in right order without taking elements that hidden in expanded groups */ -export function useListParsedState({items, getItemId}: UseListParsedStateProps) { +export function useListParsedState({ + items, + getItemId: propsGetItemId, + groupsDefaultState, +}: UseListParsedStateProps) { + const getItemId = React.useRef(propsGetItemId).current; + const result = React.useMemo(() => { - return getListParsedState(items, getItemId); - }, [getItemId, items]); + return getListParsedState({items, getItemId, groupsDefaultState}); + }, [getItemId, groupsDefaultState, items]); return result; } diff --git a/src/components/useList/hooks/useListState.ts b/src/components/useList/hooks/useListState.ts index 2e51d0097e..e8cac6a750 100644 --- a/src/components/useList/hooks/useListState.ts +++ b/src/components/useList/hooks/useListState.ts @@ -3,58 +3,50 @@ import React from 'react'; import type {ListState} from '../types'; -interface UseListStateProps { +export interface UseListStateProps { /** * Initial state values */ initialValues?: Partial; - /** - * Ability to pass link to another state value - * - * ```tsx - * const listState = useListState() - * - * // inside your component - * const innerListState = useListState({ - * controlledValues: listState - * }) - * ``` - */ - controlledValues?: Partial; + rootNodesGroups?: boolean; } -function useControlledState(value: T, defaultValue: T) { - const [state, setState] = React.useState(value || defaultValue); +export const useListState = ({initialValues, rootNodesGroups}: UseListStateProps): ListState => { + const initialValuesRef = React.useRef(initialValues); + const needToUpdateInitValues = initialValuesRef.current !== initialValues; + initialValuesRef.current = initialValues; - return [value || state, setState] as const; -} + const [disabledById, setDisabled] = React.useState(() => initialValues?.disabledById ?? {}); + const [selectedById, setSelected] = React.useState(() => initialValues?.selectedById ?? {}); + const [expandedById, setExpanded] = React.useState(() => initialValues?.expandedById ?? {}); + const [activeItemId, setActiveItemId] = React.useState(() => initialValues?.activeItemId); -export const useListState = ({initialValues, controlledValues}: UseListStateProps = {}) => { - const [disabledById, setDisabled] = useControlledState( - controlledValues?.disabledById!, - initialValues?.disabledById || {}, - ); - const [selectedById, setSelected] = useControlledState( - controlledValues?.selectedById!, - initialValues?.selectedById || {}, - ); - const [expandedById, setExpanded] = useControlledState( - controlledValues?.expandedById!, - initialValues?.expandedById || {}, - ); - const [activeItemId, setActiveItemId] = useControlledState( - controlledValues?.activeItemId, - initialValues?.activeItemId, - ); - - return { + if (needToUpdateInitValues) { + if (initialValues?.disabledById) { + setDisabled((prevValues) => ({...initialValues.disabledById, ...prevValues})); + } + if (initialValues?.selectedById) { + setSelected((prevValues) => ({...initialValues.selectedById, ...prevValues})); + } + if (initialValues?.expandedById) { + setExpanded((prevValues) => ({...initialValues.expandedById, ...prevValues})); + } + setActiveItemId((prevValue) => prevValue ?? initialValues?.activeItemId); + } + + const result: ListState = { disabledById, - setDisabled, selectedById, - setSelected, - expandedById, - setExpanded, activeItemId, + setDisabled, + setSelected, setActiveItemId, }; + + if (rootNodesGroups) { + result.expandedById = expandedById; + result.setExpanded = setExpanded; + } + + return result; }; diff --git a/src/components/useList/index.ts b/src/components/useList/index.ts index c2237d6d36..0ca6987c7b 100644 --- a/src/components/useList/index.ts +++ b/src/components/useList/index.ts @@ -1,7 +1,7 @@ export * from './hooks/useListFilter'; export * from './hooks/useList'; export * from './hooks/useListKeydown'; -export * from './hooks/useListState'; +export * from './hooks/useListItemClick'; export * from './types'; export * from './components/ListItemView'; export * from './components/ListRecursiveRenderer/ListRecursiveRenderer'; diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 75be6b1531..011635390f 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -48,14 +48,10 @@ export type ListItemCommonProps = { endSlot?: React.ReactNode; }; -export type RenderItemContext = { - itemState: ItemState; - /** - * Exists if item is group - */ - groupState?: GroupParsedState; - isLastItem: boolean; -}; +export type ListItemListContextProps = ItemState & + Partial & { + isLastItem: boolean; + }; export type RenderItemProps = { size: ListItemSize; @@ -86,10 +82,18 @@ export type ParsedState = { groupsState: Record; }; +type SetStateAction = S | ((prevState: S) => S); + +type ListStateHandler = (arg: SetStateAction) => void; + export type ListState = { disabledById: Record; selectedById: Record; - expandedById: Record; + expandedById?: Record; + setExpanded?: ListStateHandler>; + setSelected: ListStateHandler>; + setDisabled: ListStateHandler>; + setActiveItemId: ListStateHandler; activeItemId?: ListItemId; }; @@ -98,12 +102,24 @@ export type InitialListParsedState = Pick< 'disabledById' | 'expandedById' | 'selectedById' >; +export type ItemSchema = { + id: ListItemId; + index: number; + children?: ItemSchema[]; +}; + export type ParsedFlattenState = { visibleFlattenIds: ListItemId[]; idToFlattenIndex: Record; + itemsSchema: ItemSchema[]; }; -export type ListParsedState = ParsedState & +type ListStructure = ParsedState & ParsedFlattenState & { items: ListItemType[]; }; + +export type UseList = { + state: ListState; + structure: ListStructure; +}; diff --git a/src/components/useList/utils/flattenItems.test.ts b/src/components/useList/utils/flattenItems.test.ts index 2fc9b44956..af32df0eb1 100644 --- a/src/components/useList/utils/flattenItems.test.ts +++ b/src/components/useList/utils/flattenItems.test.ts @@ -1,3 +1,5 @@ +import type {ParsedFlattenState} from '../types'; + import {flattenItems} from './flattenItems'; const data = [ @@ -31,46 +33,152 @@ const data = [ describe('flattenItems', () => { test('should return expected result', () => { - expect(flattenItems(data)).toEqual({ + const result: ParsedFlattenState = { visibleFlattenIds: ['0', '1', '1-0', '1-1', '1-1-0', '1-2', '2'], idToFlattenIndex: {0: 0, 1: 1, '1-0': 2, '1-1': 3, '1-1-0': 4, '1-2': 5, 2: 6}, - }); + itemsSchema: [ + { + id: '0', + index: 0, + }, + { + id: '1', + index: 1, + children: [ + { + id: '1-0', + index: 2, + }, + { + id: '1-1', + index: 3, + children: [{id: '1-1-0', index: 4, children: []}], + }, + { + id: '1-2', + index: 5, + }, + ], + }, + { + id: '2', + index: 6, + children: [], + }, + ], + }; + + expect(flattenItems({items: data})).toEqual(result); }); test('should return expected result with expanded state', () => { + const result: ParsedFlattenState = { + visibleFlattenIds: ['0', '1', '2'], + idToFlattenIndex: {0: 0, 1: 1, 2: 2}, + itemsSchema: [ + { + id: '0', + index: 0, + }, + { + id: '1', + index: 1, + }, + { + id: '2', + index: 2, + children: [], + }, + ], + }; + expect( - flattenItems(data, { - '1': false, + flattenItems({ + items: data, + expandedById: { + '1': false, + }, }), - ).toEqual({visibleFlattenIds: ['0', '1', '2'], idToFlattenIndex: {0: 0, 1: 1, 2: 2}}); + ).toEqual(result); }); test('should return expected result with expanded state 2', () => { - expect( - flattenItems(data, { - '1-1': false, - }), - ).toEqual({ + const result: ParsedFlattenState = { visibleFlattenIds: ['0', '1', '1-0', '1-1', '1-2', '2'], idToFlattenIndex: {0: 0, 1: 1, '1-0': 2, '1-1': 3, '1-2': 4, 2: 5}, - }); - }); + itemsSchema: [ + { + id: '0', + index: 0, + }, + { + id: '1', + index: 1, + children: [ + { + id: '1-0', + index: 2, + }, + { + id: '1-1', + index: 3, + }, + { + id: '1-2', + index: 4, + }, + ], + }, + { + id: '2', + index: 5, + children: [], + }, + ], + }; - test('should return expected result with expanded state and id getter override', () => { expect( - flattenItems( - data, - { - 'item-1': false, + flattenItems({ + items: data, + expandedById: { + '1-1': false, }, - ({title}) => title, - ), - ).toEqual({ + }), + ).toEqual(result); + }); + + test('should return expected result with expanded state and id getter override', () => { + const result: ParsedFlattenState = { visibleFlattenIds: ['item-0', 'item-1', 'item-2'], idToFlattenIndex: { 'item-0': 0, 'item-1': 1, 'item-2': 2, }, - }); + itemsSchema: [ + { + id: 'item-0', + index: 0, + }, + { + id: 'item-1', + index: 1, + }, + { + id: 'item-2', + index: 2, + children: [], + }, + ], + }; + + expect( + flattenItems({ + items: data, + expandedById: { + 'item-1': false, + }, + getItemId: ({title}) => title, + }), + ).toEqual(result); }); }); diff --git a/src/components/useList/utils/flattenItems.ts b/src/components/useList/utils/flattenItems.ts index 8c013ed556..c28b1de31b 100644 --- a/src/components/useList/utils/flattenItems.ts +++ b/src/components/useList/utils/flattenItems.ts @@ -4,11 +4,17 @@ import {getListItemId} from './getListItemId'; import {getGroupItemId} from './groupItemId'; import {isTreeItemGuard} from './isTreeItemGuard'; -export function flattenItems( - items: ListItemType[], - expandedById: Record = {}, - getItemId?: (item: T) => ListItemId, -): ParsedFlattenState { +interface FlattenItemsProps { + items: ListItemType[]; + expandedById?: Record; + getItemId?: (item: T) => ListItemId; +} + +export function flattenItems({ + items, + getItemId, + expandedById = {}, +}: FlattenItemsProps): ParsedFlattenState { const getNestedIds = ( order: string[], item: ListItemType, @@ -46,8 +52,36 @@ export function flattenItems( idToFlattenIndex[index] = item; } + const getItemSchema = ({ + item, + parentId, + index, + }: { + item: ListItemType; + parentId?: string; + index: number; + }) => { + const groupedId = getGroupItemId(index, parentId); + const id = getListItemId({groupedId, item, getItemId}); + + const schema: ParsedFlattenState['itemsSchema'][0] = {id, index: idToFlattenIndex[id]}; + + if (isTreeItemGuard(item) && item.children && !(id in expandedById && !expandedById[id])) { + schema.children = item.children.map((item, index) => + getItemSchema({item, parentId: id, index}), + ); + } + + return schema; + }; + + const itemsSchema: ParsedFlattenState['itemsSchema'] = items.map((item, index) => + getItemSchema({item, index}), + ); + return { visibleFlattenIds, idToFlattenIndex, + itemsSchema, }; } diff --git a/src/components/useList/utils/getItemRenderState.tsx b/src/components/useList/utils/getItemRenderState.tsx index 3d1df14850..eadf7ad1dc 100644 --- a/src/components/useList/utils/getItemRenderState.tsx +++ b/src/components/useList/utils/getItemRenderState.tsx @@ -3,88 +3,68 @@ import type {QAProps} from '../../types'; import type { ListItemCommonProps, ListItemId, + ListItemListContextProps, ListItemSize, - ListParsedState, - ListState, - RenderItemContext, RenderItemProps, + UseList, } from '../types'; import {getListItemQa} from './getListItemQa'; -type ItemRendererProps = Partial & - QAProps & - ListParsedState & { - size?: ListItemSize; - /** - * Affects the view of the selected items - */ - multiple?: boolean; - /** - * @default true - * - * Group expanded initial state - */ - defaultExpanded?: boolean; - id: ListItemId; - mapItemDataToProps(data: T): ListItemCommonProps; - onItemClick?(id: ListItemId): void; - }; +type ItemRendererProps = QAProps & { + size?: ListItemSize; + /** + * Affects the view of the selected items + */ + multiple?: boolean; + id: ListItemId; + mapItemDataToProps(data: T): ListItemCommonProps; + onItemClick?(payload: {id: ListItemId}): void; + list: UseList; +}; /** * Map list state and parsed list state to item render props */ export const getItemRenderState = ({ qa, - itemsById, - disabledById, - expandedById, - groupsState, + list, onItemClick, mapItemDataToProps, - visibleFlattenIds, size = 'm', - itemsState, - selectedById, - activeItemId, multiple = false, - defaultExpanded = true, id, }: ItemRendererProps) => { - const context: RenderItemContext = { - itemState: itemsState[id], - groupState: groupsState[id], - isLastItem: id === visibleFlattenIds[visibleFlattenIds.length - 1], + const context: ListItemListContextProps = { + ...list.structure.itemsState[id], + ...list.structure.groupsState[id], + isLastItem: + id === list.structure.visibleFlattenIds[list.structure.visibleFlattenIds.length - 1], }; let expanded; // `undefined` value means than tree list will look as nested list without groups - let selected; // the absence of the value of the selected element affects its view. For example, an element without a value will not have a visual highlight on the hover // isGroup - if (groupsState[id] && expandedById) { - expanded = expandedById[id] ?? defaultExpanded; - } - - if (selectedById) { - selected = Boolean(selectedById[id]); + if (list.state.expandedById && id in list.state.expandedById) { + expanded = list.state.expandedById[id]; } const props: RenderItemProps = { id, size, expanded, - active: id === activeItemId, - indentation: context.itemState.indentation, - disabled: Boolean(disabledById?.[id]), - selected, - hasSelectionIcon: Boolean(multiple) && !context.groupState, - onClick: onItemClick ? () => onItemClick(id) : undefined, - ...mapItemDataToProps(itemsById[id]), + active: id === list.state.activeItemId, + indentation: context.indentation, + disabled: Boolean(list.state.disabledById?.[id]), + selected: Boolean(list.state.selectedById[id]), + hasSelectionIcon: Boolean(multiple) && !context.childrenIds, // hide multiple selection view at group nodes + onClick: onItemClick ? () => onItemClick({id}) : undefined, + ...mapItemDataToProps(list.structure.itemsById[id]), }; if (qa) { props.qa = getListItemQa(qa, id); } - return {data: itemsById[id], props, context}; + return {data: list.structure.itemsById[id], props, context}; }; diff --git a/src/components/useList/utils/getListParsedState.test.ts b/src/components/useList/utils/getListParsedState.test.ts index e893904422..588d2b4fe3 100644 --- a/src/components/useList/utils/getListParsedState.test.ts +++ b/src/components/useList/utils/getListParsedState.test.ts @@ -7,6 +7,7 @@ describe('getListParsedState', () => { const data: ListItemType[] = [ { data: {title: 'item-0'}, + expanded: true, disabled: true, willNotBeIncluded: '123', }, @@ -33,7 +34,7 @@ describe('getListParsedState', () => { }, ]; - expect(getListParsedState(data)).toEqual({ + expect(getListParsedState({items: data})).toEqual({ initialState: { selectedById: { 2: true, @@ -42,7 +43,10 @@ describe('getListParsedState', () => { 0: true, }, expandedById: { + '1': true, '1-1': false, + '1-1-0': true, + '2': true, }, }, itemsById: { @@ -90,7 +94,7 @@ describe('getListParsedState', () => { }, ]; - expect(getListParsedState(data)).toEqual({ + expect(getListParsedState({items: data})).toEqual({ initialState: { selectedById: { 1: true, @@ -136,19 +140,27 @@ describe('getListParsedState', () => { }, { data: {title: 'child-1-2', id: 'id-4'}, - expanded: false, + expanded: true, children: [{data: {title: 'child-1-2-1', id: 'id-5'}, children: []}], }, ], }, ]; - expect(getListParsedState(data, ({id}) => id)).toEqual({ + expect( + getListParsedState({ + items: data, + groupsDefaultState: 'closed', + getItemId: ({id}) => id, + }), + ).toEqual({ initialState: { selectedById: {}, disabledById: {}, expandedById: { - 'id-4': false, + 'id-2': false, + 'id-4': true, + 'id-5': false, }, }, itemsById: { diff --git a/src/components/useList/utils/getListParsedState.ts b/src/components/useList/utils/getListParsedState.ts index 8b26c0d0d8..c0e38f1074 100644 --- a/src/components/useList/utils/getListParsedState.ts +++ b/src/components/useList/utils/getListParsedState.ts @@ -31,14 +31,20 @@ type ListParsedStateResult = ParsedState & { initialState: InitialListParsedState; }; -export function getListParsedState( - items: ListItemType[], +export interface GetListParsedStateProps { + items: ListItemType[]; + groupsDefaultState?: 'closed' | 'expanded'; /** * For example T is entity type with id what represents db id * So now you can use it id as a list item id in internal state */ - getItemId?: (item: T) => ListItemId, -): ListParsedStateResult { + getItemId?: (item: T) => ListItemId; +} +export function getListParsedState({ + items, + groupsDefaultState = 'expanded', + getItemId, +}: GetListParsedStateProps): ListParsedStateResult { const result: ListParsedStateResult = { itemsById: {}, groupsState: {}, @@ -114,8 +120,12 @@ export function getListParsedState( childrenIds: [], }; - if (typeof item.expanded !== 'undefined') { - result.initialState.expandedById[id] = item.expanded; + if (result.initialState.expandedById) { + if (typeof item.expanded === 'undefined') { + result.initialState.expandedById[id] = groupsDefaultState === 'expanded'; + } else { + result.initialState.expandedById[id] = item.expanded; + } } item.children.forEach((treeItem, index) => { diff --git a/src/unstable.ts b/src/unstable.ts index 1d5149a1de..478f2edfd2 100644 --- a/src/unstable.ts +++ b/src/unstable.ts @@ -1,15 +1,16 @@ /* eslint-disable camelcase */ export { useList as unstable_useList, - useListState as unstable_useListState, useListFilter as unstable_useListFilter, useListKeydown as unstable_useListKeydown, + useListItemClick as unstable_useListItemClick, ListItemView as unstable_ListItemView, type ListItemViewProps as unstable_ListItemViewProps, ListContainerView as unstable_ListContainerView, type ListContainerViewProps as unstable_ListContainerViewProps, type ListItemType as unstable_ListItemType, type ListItemId as unstable_ListItemId, + type UseList as unstable_UseList, getItemRenderState as unstable_getItemRenderState, scrollToListItem as unstable_scrollToListItem, getListItemQa as unstable_getListItemQa, From cd6aac709d5faab2e9af50f8a2063039c27434d3 Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 13:43:02 +0300 Subject: [PATCH 02/15] fix(useList): remove wierd itemsSchema prop --- src/components/TreeList/TreeList.tsx | 28 +++--- .../components/RenderVirtualizedContainer.tsx | 4 +- .../stories/WithDisabledElementsStory.tsx | 5 +- .../WithFiltrationAndControlsStory.tsx | 13 +-- .../TreeListContainer/TreeListContainer.tsx | 24 ----- src/components/TreeList/types.ts | 24 +---- src/components/TreeSelect/types.ts | 4 +- .../__stories__/components/FlattenList.tsx | 4 +- .../components/InfinityScrollList.tsx | 65 +++++++------ .../__stories__/components/ListWithDnd.tsx | 4 +- .../components/PopupWithTogglerList.tsx | 40 ++++---- .../__stories__/components/RecursiveList.tsx | 40 ++++---- .../ListContainer/ListContainer.tsx | 41 ++++++++ .../useList/components/ListContainer/index.ts | 2 + .../components/ListContainerView/index.ts | 1 + .../ListRecursiveRenderer.tsx | 43 ++++++--- .../components/ListRecursiveRenderer/index.ts | 2 + src/components/useList/hooks/useList.ts | 4 +- src/components/useList/index.ts | 5 +- src/components/useList/types.ts | 6 +- .../useList/utils/flattenItems.test.ts | 93 +------------------ src/components/useList/utils/flattenItems.ts | 36 ++----- src/unstable.ts | 2 + 23 files changed, 202 insertions(+), 288 deletions(-) delete mode 100644 src/components/TreeList/components/TreeListContainer/TreeListContainer.tsx create mode 100644 src/components/useList/components/ListContainer/ListContainer.tsx create mode 100644 src/components/useList/components/ListContainer/index.ts create mode 100644 src/components/useList/components/ListContainerView/index.ts create mode 100644 src/components/useList/components/ListRecursiveRenderer/index.ts diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index d617638706..a6dccc3e88 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -3,16 +3,16 @@ import React from 'react'; import {useUniqId} from '../../hooks'; -import {ListItemView, getItemRenderState, useListItemClick, useListKeydown} from '../useList'; -import type {ListItemId} from '../useList'; +import { + ListContainer, + ListItemView, + getItemRenderState, + useListItemClick, + useListKeydown, +} from '../useList'; import {block} from '../utils/cn'; -import {TreeListContainer} from './components/TreeListContainer/TreeListContainer'; -import type { - TreeListOnItemClickPayload, - TreeListProps, - TreeListRenderContainerProps, -} from './types'; +import type {TreeListContainerProps, TreeListOnItemClick, TreeListProps} from './types'; const b = block('tree-list'); @@ -23,7 +23,7 @@ export const TreeList = ({ className, list, renderItem: propsRenderItem, - renderContainer = TreeListContainer, + renderContainer = ListContainer, onItemClick: propsOnItemClick, multiple, containerRef: propsContainerRef, @@ -44,13 +44,13 @@ export const TreeList = ({ const onClick = propsOnItemClick ?? defaultOnItemClick; - return ({id}: {id: ListItemId}) => { - const payload: TreeListOnItemClickPayload = {id, list}; - + const handler: TreeListOnItemClick = (payload) => { onClick(payload); withItemClick?.(payload); }; - }, [defaultOnItemClick, list, propsOnItemClick, withItemClick]); + + return handler; + }, [defaultOnItemClick, propsOnItemClick, withItemClick]); useListKeydown({ containerRef, @@ -58,7 +58,7 @@ export const TreeList = ({ list, }); - const renderItem: TreeListRenderContainerProps['renderItem'] = ( + const renderItem: TreeListContainerProps['renderItem'] = ( itemId, index, renderContainerProps, diff --git a/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx b/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx index 7207b5d106..71a3bff527 100644 --- a/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx +++ b/src/components/TreeList/__stories__/components/RenderVirtualizedContainer.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {ListContainerView, computeItemSize} from '../../../useList'; import {VirtualizedListContainer} from '../../../useList/__stories__/components/VirtualizedListContainer'; -import type {TreeListRenderContainerProps} from '../../types'; +import type {TreeListContainerProps} from '../../types'; // custom container renderer example export const RenderVirtualizedContainer = ({ @@ -13,7 +13,7 @@ export const RenderVirtualizedContainer = ({ renderItem, size, className, -}: TreeListRenderContainerProps) => { +}: TreeListContainerProps) => { return ( [] = [ { - text: 'one', + text: 'default disabled', disabled: true, }, { text: 'two', }, { - text: 'free', + text: 'default selected', + selected: true, }, { text: 'four', diff --git a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx index 715298b1e4..a2463b8684 100644 --- a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx @@ -7,14 +7,15 @@ import {Flex, spacing} from '../../../layout'; import {useList, useListFilter} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; -import type {TreeListProps, TreeListRenderContainerProps} from '../../types'; +import type {TreeListContainerProps, TreeListProps} from '../../types'; import {RenderVirtualizedContainer} from '../components/RenderVirtualizedContainer'; +interface Entity { + title: string; +} + export interface WithFiltrationAndControlsStoryProps - extends Omit< - TreeListProps<{title: string}>, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' - > { + extends Omit, 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps'> { itemsCount?: number; } @@ -24,7 +25,7 @@ export const WithFiltrationAndControlsStory = ({ }: WithFiltrationAndControlsStoryProps) => { const {items, renderContainer} = React.useMemo(() => { const baseItems = createRandomizedData({num: itemsCount}); - const containerRenderer = (props: TreeListRenderContainerProps<{title: string}>) => { + const containerRenderer = (props: TreeListContainerProps) => { if (props.list.structure.items.length === 0 && baseItems.length > 0) { return ( diff --git a/src/components/TreeList/components/TreeListContainer/TreeListContainer.tsx b/src/components/TreeList/components/TreeListContainer/TreeListContainer.tsx deleted file mode 100644 index eddff99775..0000000000 --- a/src/components/TreeList/components/TreeListContainer/TreeListContainer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import {ListContainerView} from '../../../useList'; -import {ListItemRecursiveRenderer} from '../../../useList/components/ListRecursiveRenderer/ListRecursiveRenderer'; -import type {TreeListRenderContainerProps} from '../../types'; - -export const TreeListContainer = ({ - qa, - id, - containerRef, - renderItem, - className, - list, -}: TreeListRenderContainerProps) => { - return ( - - {list.structure.itemsSchema.map((itemSchema, index) => ( - - {renderItem} - - ))} - - ); -}; diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index cc86ffe4f5..89658993f6 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -2,6 +2,7 @@ import type React from 'react'; import type {QAProps} from '../types'; import type { + ListContainerProps, ListItemCommonProps, ListItemId, ListItemListContextProps, @@ -21,34 +22,17 @@ export type TreeListRenderItem = (props: { renderContainerProps?: P; }) => React.JSX.Element; -export type TreeListRenderContainerProps = QAProps & { - id: string; - list: UseList; - /** - * May be needed for items size if it's virtualized container for example - */ +export type TreeListContainerProps = ListContainerProps & { size: ListItemSize; - containerRef?: React.RefObject; - className?: string; - renderItem( - id: ListItemId, - index: number, - /** - * Ability to transfer props from an overridden container render - */ - renderContainerProps?: Object, - ): React.JSX.Element; }; -export type TreeListRenderContainer = ( - props: TreeListRenderContainerProps, -) => React.JSX.Element; +export type TreeListRenderContainer = (props: TreeListContainerProps) => React.JSX.Element; export type TreeListMapItemDataToProps = (item: T) => ListItemCommonProps; export type TreeListOnItemClickPayload = {id: ListItemId; list: UseList}; -type TreeListOnItemClick = (payload: TreeListOnItemClickPayload) => void; +export type TreeListOnItemClick = (payload: TreeListOnItemClickPayload) => void; export interface TreeListProps extends QAProps { /** diff --git a/src/components/TreeSelect/types.ts b/src/components/TreeSelect/types.ts index 5d4d4b7c4b..3fb0b83e89 100644 --- a/src/components/TreeSelect/types.ts +++ b/src/components/TreeSelect/types.ts @@ -4,9 +4,9 @@ import type {PopperPlacement} from '../../hooks/private'; import type {UseOpenProps} from '../../hooks/useSelect/types'; import type {SelectPopupProps} from '../Select/components/SelectPopup/types'; import type { + TreeListContainerProps, TreeListProps, TreeListRenderContainer, - TreeListRenderContainerProps, TreeListRenderItem, } from '../TreeList/types'; import type {ListItemId, ListItemSize, UseList} from '../useList'; @@ -27,7 +27,7 @@ export type TreeSelectRenderControlProps = { }; export type TreeSelectRenderItem = TreeListRenderItem; -export type TreeSelectRenderContainerProps = TreeListRenderContainerProps; +export type TreeSelectRenderContainerProps = TreeListContainerProps; export type TreeSelectRenderContainer = TreeListRenderContainer; interface TreeSelectBehavioralProps extends UseListParsedStateProps { diff --git a/src/components/useList/__stories__/components/FlattenList.tsx b/src/components/useList/__stories__/components/FlattenList.tsx index 543e3282e5..f4a45403cf 100644 --- a/src/components/useList/__stories__/components/FlattenList.tsx +++ b/src/components/useList/__stories__/components/FlattenList.tsx @@ -4,8 +4,8 @@ import get from 'lodash/get'; import {TextInput} from '../../../controls'; import {Flex} from '../../../layout'; -import {ListContainerView} from '../../components/ListContainerView/ListContainerView'; -import {ListItemView} from '../../components/ListItemView/ListItemView'; +import {ListContainerView} from '../../components/ListContainerView'; +import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; import {useListItemClick} from '../../hooks/useListItemClick'; diff --git a/src/components/useList/__stories__/components/InfinityScrollList.tsx b/src/components/useList/__stories__/components/InfinityScrollList.tsx index 81ab16c45f..58ce905c85 100644 --- a/src/components/useList/__stories__/components/InfinityScrollList.tsx +++ b/src/components/useList/__stories__/components/InfinityScrollList.tsx @@ -4,9 +4,8 @@ import {Button} from '../../../Button'; import {Loader} from '../../../Loader'; import {TextInput} from '../../../controls'; import {Flex} from '../../../layout'; -import {ListContainerView} from '../../components/ListContainerView/ListContainerView'; -import {ListItemView} from '../../components/ListItemView/ListItemView'; -import {ListItemRecursiveRenderer} from '../../components/ListRecursiveRenderer/ListRecursiveRenderer'; +import {ListContainer} from '../../components/ListContainer'; +import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; import {useListItemClick} from '../../hooks/useListItemClick'; @@ -64,39 +63,37 @@ export const InfinityScrollList = ({size}: InfinityScrollListProps) => { ref={filterState.filterRef} /> - - {list.structure.itemsSchema.map((itemSchema, index) => ( - - {(id) => { - const {props, context} = getItemRenderState({ - id, - size, - onItemClick, - multiple: true, - mapItemDataToProps: (x) => x, - list, - }); - const node = ; + { + const {props, context} = getItemRenderState({ + id, + size, + onItemClick, + multiple: true, + mapItemDataToProps: (x) => x, + list, + }); + const node = ; - if (context.isLastItem) { - return ( - - {node} - - ); - } + if (context.isLastItem) { + return ( + + {node} + + ); + } - return node; - }} - - ))} - + return node; + }} + /> )} diff --git a/src/components/useList/__stories__/components/ListWithDnd.tsx b/src/components/useList/__stories__/components/ListWithDnd.tsx index ac3ae024a8..e88c58695b 100644 --- a/src/components/useList/__stories__/components/ListWithDnd.tsx +++ b/src/components/useList/__stories__/components/ListWithDnd.tsx @@ -11,8 +11,8 @@ import type { import {Icon} from '../../../Icon'; import {TextInput} from '../../../controls'; import {Flex} from '../../../layout'; -import {ListContainerView} from '../../components/ListContainerView/ListContainerView'; -import {ListItemView} from '../../components/ListItemView/ListItemView'; +import {ListContainerView} from '../../components/ListContainerView'; +import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; import {useListItemClick} from '../../hooks/useListItemClick'; diff --git a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx index f11649b799..834aece277 100644 --- a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx +++ b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx @@ -3,9 +3,8 @@ import React from 'react'; import {Button} from '../../../Button'; import {Popup} from '../../../Popup'; import {Flex} from '../../../layout'; -import {ListContainerView} from '../../components/ListContainerView/ListContainerView'; -import {ListItemView} from '../../components/ListItemView/ListItemView'; -import {ListItemRecursiveRenderer} from '../../components/ListRecursiveRenderer/ListRecursiveRenderer'; +import {ListContainer} from '../../components/ListContainer'; +import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListItemClick} from '../../hooks/useListItemClick'; import {useListKeydown} from '../../hooks/useListKeydown'; @@ -76,28 +75,21 @@ export const PopupWithTogglerList = ({size, itemsCount}: PopupWithTogglerListPro restoreFocus restoreFocusRef={controlRef} > - - {list.structure.itemsSchema.map((itemSchema, index) => ( - - {(id) => { - const {props, context} = getItemRenderState({ - id, - size, - onItemClick, - mapItemDataToProps: (x) => x, - list, - }); + { + const {props, context} = getItemRenderState({ + id, + size, + onItemClick, + mapItemDataToProps: (x) => x, + list, + }); - return ( - - ); - }} - - ))} - + return ; + }} + />
); diff --git a/src/components/useList/__stories__/components/RecursiveList.tsx b/src/components/useList/__stories__/components/RecursiveList.tsx index 05e51c528d..91e4d67287 100644 --- a/src/components/useList/__stories__/components/RecursiveList.tsx +++ b/src/components/useList/__stories__/components/RecursiveList.tsx @@ -2,9 +2,8 @@ import React from 'react'; import {TextInput} from '../../../controls'; import {Flex} from '../../../layout'; -import {ListContainerView} from '../../components/ListContainerView/ListContainerView'; -import {ListItemView} from '../../components/ListItemView/ListItemView'; -import {ListItemRecursiveRenderer} from '../../components/ListRecursiveRenderer/ListRecursiveRenderer'; +import {ListContainer} from '../../components/ListContainer'; +import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; import {useListItemClick} from '../../hooks/useListItemClick'; @@ -49,26 +48,23 @@ export const RecursiveList = ({size, itemsCount, 'aria-label': ariaLabel}: Recur // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus /> - - {list.structure.itemsSchema.map((itemSchema, index) => ( - - {(id) => { - const {props, context} = getItemRenderState({ - id, - size, - onItemClick, - multiple: true, - mapItemDataToProps: (x) => x, - list, - }); + { + const {props, context} = getItemRenderState({ + id, + size, + onItemClick, + multiple: true, + mapItemDataToProps: (x) => x, + list, + }); - return ( - - ); - }} - - ))} - + return ; + }} + />
); }; diff --git a/src/components/useList/components/ListContainer/ListContainer.tsx b/src/components/useList/components/ListContainer/ListContainer.tsx new file mode 100644 index 0000000000..9403d2cf95 --- /dev/null +++ b/src/components/useList/components/ListContainer/ListContainer.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import type {ListItemId, UseList} from '../../types'; +import {ListContainerView} from '../ListContainerView'; +import type {ListContainerViewProps} from '../ListContainerView/ListContainerView'; +import {ListItemRecursiveRenderer} from '../ListRecursiveRenderer/ListRecursiveRenderer'; + +export type ListContainerProps = Omit & { + list: UseList; + containerRef?: React.RefObject; + renderItem( + id: ListItemId, + index: number, + /** + * Ability to transfer props from an overridden container render + */ + renderContainerProps?: Object, + ): React.JSX.Element; +}; + +export function ListContainer({ + containerRef, + renderItem, + list, + ...props +}: ListContainerProps) { + return ( + + {list.structure.items.map((item, index) => ( + + {renderItem} + + ))} + + ); +} diff --git a/src/components/useList/components/ListContainer/index.ts b/src/components/useList/components/ListContainer/index.ts new file mode 100644 index 0000000000..efec57ed9c --- /dev/null +++ b/src/components/useList/components/ListContainer/index.ts @@ -0,0 +1,2 @@ +export {ListContainer} from './ListContainer'; +export type {ListContainerProps} from './ListContainer'; diff --git a/src/components/useList/components/ListContainerView/index.ts b/src/components/useList/components/ListContainerView/index.ts new file mode 100644 index 0000000000..b3dfd217b5 --- /dev/null +++ b/src/components/useList/components/ListContainerView/index.ts @@ -0,0 +1 @@ +export {ListContainerView, type ListContainerViewProps} from './ListContainerView'; diff --git a/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx b/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx index 791dda44ea..b2c92f14f8 100644 --- a/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx +++ b/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx @@ -1,32 +1,51 @@ import React from 'react'; import {block} from '../../../utils/cn'; -import type {ItemSchema, ListItemId} from '../../types'; +import type {ListItemId, ListItemType, UseList} from '../../types'; +import {isTreeItemGuard} from '../../utils/isTreeItemGuard'; import './ListRecursiveRenderer.scss'; const b = block('list-recursive-renderer'); -export interface ListRecursiveRendererProps { - itemSchema: ItemSchema; +export interface ListItemRecursiveRendererProps { + id: ListItemId; + list: UseList; + itemSchema: ListItemType; + children(id: ListItemId, index: number): React.JSX.Element; className?: string; style?: React.CSSProperties; - children(id: ListItemId, index: number): React.JSX.Element; } // Saves the nested html structure for tree data structure -export function ListItemRecursiveRenderer({itemSchema, ...props}: ListRecursiveRendererProps) { - const id = itemSchema.id; - - const node = props.children(id, itemSchema.index); +export function ListItemRecursiveRenderer({ + id, + itemSchema, + list, + ...props +}: ListItemRecursiveRendererProps) { + const node = props.children(id, list.structure.idToFlattenIndex[id]); + + if (isTreeItemGuard(itemSchema) && itemSchema.children) { + const isExpanded = + list.state.expandedById && id in list.state.expandedById + ? list.state.expandedById[id] + : true; - if (itemSchema.children) { return (
    {node} - {itemSchema.children.map((item, index) => ( - - ))} + {isExpanded && + Boolean(list.structure.groupsState[id]?.childrenIds) && + itemSchema.children.map((item, index) => ( + + ))}
); } diff --git a/src/components/useList/components/ListRecursiveRenderer/index.ts b/src/components/useList/components/ListRecursiveRenderer/index.ts new file mode 100644 index 0000000000..78fa5d12da --- /dev/null +++ b/src/components/useList/components/ListRecursiveRenderer/index.ts @@ -0,0 +1,2 @@ +export {ListItemRecursiveRenderer} from './ListRecursiveRenderer'; +export type {ListItemRecursiveRendererProps} from './ListRecursiveRenderer'; diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index d5ebf727d7..42657ffe1a 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -50,7 +50,7 @@ export const useList = ({ rootNodesGroups, }); - const {visibleFlattenIds, idToFlattenIndex, itemsSchema} = useFlattenListItems({ + const {visibleFlattenIds, idToFlattenIndex, rootIds} = useFlattenListItems({ items, /** * By default controlled from list items declaration state @@ -75,7 +75,7 @@ export const useList = ({ return { state: realState, structure: { - itemsSchema, + rootIds, items, visibleFlattenIds, idToFlattenIndex, diff --git a/src/components/useList/index.ts b/src/components/useList/index.ts index 0ca6987c7b..4f4028560c 100644 --- a/src/components/useList/index.ts +++ b/src/components/useList/index.ts @@ -4,8 +4,9 @@ export * from './hooks/useListKeydown'; export * from './hooks/useListItemClick'; export * from './types'; export * from './components/ListItemView'; -export * from './components/ListRecursiveRenderer/ListRecursiveRenderer'; -export * from './components/ListContainerView/ListContainerView'; +export * from './components/ListRecursiveRenderer'; +export * from './components/ListContainerView'; +export * from './components/ListContainer'; export * from './utils/computeItemSize'; export * from './utils/getItemRenderState'; export * from './utils/scrollToListItem'; diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 011635390f..584a44401c 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -109,9 +109,13 @@ export type ItemSchema = { }; export type ParsedFlattenState = { + /** + * Original list ordered ids without flatten elements. + * Use it to get internal item id + */ + rootIds: ListItemId[]; visibleFlattenIds: ListItemId[]; idToFlattenIndex: Record; - itemsSchema: ItemSchema[]; }; type ListStructure = ParsedState & diff --git a/src/components/useList/utils/flattenItems.test.ts b/src/components/useList/utils/flattenItems.test.ts index af32df0eb1..f279b09fe6 100644 --- a/src/components/useList/utils/flattenItems.test.ts +++ b/src/components/useList/utils/flattenItems.test.ts @@ -36,36 +36,7 @@ describe('flattenItems', () => { const result: ParsedFlattenState = { visibleFlattenIds: ['0', '1', '1-0', '1-1', '1-1-0', '1-2', '2'], idToFlattenIndex: {0: 0, 1: 1, '1-0': 2, '1-1': 3, '1-1-0': 4, '1-2': 5, 2: 6}, - itemsSchema: [ - { - id: '0', - index: 0, - }, - { - id: '1', - index: 1, - children: [ - { - id: '1-0', - index: 2, - }, - { - id: '1-1', - index: 3, - children: [{id: '1-1-0', index: 4, children: []}], - }, - { - id: '1-2', - index: 5, - }, - ], - }, - { - id: '2', - index: 6, - children: [], - }, - ], + rootIds: ['0', '1', '2'], }; expect(flattenItems({items: data})).toEqual(result); @@ -75,21 +46,7 @@ describe('flattenItems', () => { const result: ParsedFlattenState = { visibleFlattenIds: ['0', '1', '2'], idToFlattenIndex: {0: 0, 1: 1, 2: 2}, - itemsSchema: [ - { - id: '0', - index: 0, - }, - { - id: '1', - index: 1, - }, - { - id: '2', - index: 2, - children: [], - }, - ], + rootIds: ['0', '1', '2'], }; expect( @@ -105,35 +62,7 @@ describe('flattenItems', () => { const result: ParsedFlattenState = { visibleFlattenIds: ['0', '1', '1-0', '1-1', '1-2', '2'], idToFlattenIndex: {0: 0, 1: 1, '1-0': 2, '1-1': 3, '1-2': 4, 2: 5}, - itemsSchema: [ - { - id: '0', - index: 0, - }, - { - id: '1', - index: 1, - children: [ - { - id: '1-0', - index: 2, - }, - { - id: '1-1', - index: 3, - }, - { - id: '1-2', - index: 4, - }, - ], - }, - { - id: '2', - index: 5, - children: [], - }, - ], + rootIds: ['0', '1', '2'], }; expect( @@ -154,21 +83,7 @@ describe('flattenItems', () => { 'item-1': 1, 'item-2': 2, }, - itemsSchema: [ - { - id: 'item-0', - index: 0, - }, - { - id: 'item-1', - index: 1, - }, - { - id: 'item-2', - index: 2, - children: [], - }, - ], + rootIds: ['item-0', 'item-1', 'item-2'], }; expect( diff --git a/src/components/useList/utils/flattenItems.ts b/src/components/useList/utils/flattenItems.ts index c28b1de31b..285e7ebe46 100644 --- a/src/components/useList/utils/flattenItems.ts +++ b/src/components/useList/utils/flattenItems.ts @@ -15,6 +15,8 @@ export function flattenItems({ getItemId, expandedById = {}, }: FlattenItemsProps): ParsedFlattenState { + const rootIds: ListItemId[] = []; + const getNestedIds = ( order: string[], item: ListItemType, @@ -24,6 +26,11 @@ export function flattenItems({ const groupedId = getGroupItemId(index, parentId); const id = getListItemId({groupedId, item, getItemId}); + // only top level array + if (!parentId) { + rootIds.push(id); + } + order.push(id); if (isTreeItemGuard(item) && item.children) { @@ -52,36 +59,9 @@ export function flattenItems({ idToFlattenIndex[index] = item; } - const getItemSchema = ({ - item, - parentId, - index, - }: { - item: ListItemType; - parentId?: string; - index: number; - }) => { - const groupedId = getGroupItemId(index, parentId); - const id = getListItemId({groupedId, item, getItemId}); - - const schema: ParsedFlattenState['itemsSchema'][0] = {id, index: idToFlattenIndex[id]}; - - if (isTreeItemGuard(item) && item.children && !(id in expandedById && !expandedById[id])) { - schema.children = item.children.map((item, index) => - getItemSchema({item, parentId: id, index}), - ); - } - - return schema; - }; - - const itemsSchema: ParsedFlattenState['itemsSchema'] = items.map((item, index) => - getItemSchema({item, index}), - ); - return { + rootIds, visibleFlattenIds, idToFlattenIndex, - itemsSchema, }; } diff --git a/src/unstable.ts b/src/unstable.ts index 478f2edfd2..149d56386d 100644 --- a/src/unstable.ts +++ b/src/unstable.ts @@ -7,6 +7,8 @@ export { ListItemView as unstable_ListItemView, type ListItemViewProps as unstable_ListItemViewProps, ListContainerView as unstable_ListContainerView, + type ListContainerProps as unstable_ListContainerProps, + ListContainer as unstable_ListContainer, type ListContainerViewProps as unstable_ListContainerViewProps, type ListItemType as unstable_ListItemType, type ListItemId as unstable_ListItemId, From a9b8b1baafc5c47fe421727d54475e65904fb5ea Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 14:20:05 +0300 Subject: [PATCH 03/15] feat(TreeSelect): added example with group toggle behaviour --- src/components/TreeList/TreeList.tsx | 11 +++--- .../stories/InfinityScrollStory.tsx | 8 +++- .../components/InfinityScrollExample.tsx | 39 ++++++++++++++++++- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index a6dccc3e88..7a45c0f896 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -10,9 +10,10 @@ import { useListItemClick, useListKeydown, } from '../useList'; +import type {ListItemId} from '../useList'; import {block} from '../utils/cn'; -import type {TreeListContainerProps, TreeListOnItemClick, TreeListProps} from './types'; +import type {TreeListContainerProps, TreeListProps} from './types'; const b = block('tree-list'); @@ -44,13 +45,13 @@ export const TreeList = ({ const onClick = propsOnItemClick ?? defaultOnItemClick; - const handler: TreeListOnItemClick = (payload) => { + return ({id}: {id: ListItemId}) => { + const payload = {id, list}; + onClick(payload); withItemClick?.(payload); }; - - return handler; - }, [defaultOnItemClick, propsOnItemClick, withItemClick]); + }, [defaultOnItemClick, list, propsOnItemClick, withItemClick]); useListKeydown({ containerRef, diff --git a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx index 01a96c5071..b0dda4d8a7 100644 --- a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx +++ b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx @@ -10,13 +10,17 @@ import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; import {RenderVirtualizedContainer} from '../components/RenderVirtualizedContainer'; +interface Entity { + title: string; +} + function identity(value: T): T { return value; } export interface InfinityScrollStoryProps extends Omit< - TreeListProps<{title: string}>, + TreeListProps, 'value' | 'onUpdate' | 'items' | 'multiple' | 'size' | 'mapItemDataToProps' > { itemsCount?: number; @@ -30,7 +34,7 @@ export const InfinityScrollStory = ({itemsCount = 3, ...storyProps}: InfinityScr onFetchMore, canFetchMore, isLoading, - } = useInfinityFetch<{title: string}>(itemsCount, true); + } = useInfinityFetch(itemsCount, true); const list = useList({items}); diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index 008b6881a7..a7354491d6 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -3,20 +3,26 @@ import React from 'react'; import {Label} from '../../../Label'; import {Loader} from '../../../Loader'; import {RenderVirtualizedContainer} from '../../../TreeList/__stories__/components/RenderVirtualizedContainer'; +import type {TreeListOnItemClick} from '../../../TreeList/types'; import {Flex, sp, spacing} from '../../../layout'; import {ListItemView} from '../../../useList'; +import type {ListItemId} from '../../../useList'; import {IntersectionContainer} from '../../../useList/__stories__/components/IntersectionContainer/IntersectionContainer'; import {useInfinityFetch} from '../../../useList/__stories__/utils/useInfinityFetch'; import {TreeSelect} from '../../TreeSelect'; import type {TreeSelectProps} from '../../types'; +interface Entity { + title: string; +} + function identity(value: T): T { return value; } export interface InfinityScrollExampleProps extends Omit< - TreeSelectProps<{title: string}>, + TreeSelectProps, 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'multiple' | 'defaultValue' > { itemsCount?: number; @@ -32,7 +38,35 @@ export const InfinityScrollExample = ({ onFetchMore, canFetchMore, isLoading, - } = useInfinityFetch<{title: string}>(itemsCount, true); + } = useInfinityFetch(itemsCount, true); + + const handleGroupItemClick: TreeListOnItemClick = ({id, list}) => { + // click on group item + if (list.state.expandedById && list.state.setExpanded && id in list.state.expandedById) { + const treeGroupNextValue = !list.state.expandedById[id]; + const groupItemToToggleIds: ListItemId[] = [id]; + const stack = [...list.structure.groupsState[id].childrenIds]; + + while (stack.length > 0) { + const candidateId = stack.pop(); + + if (candidateId && candidateId in list.structure.groupsState) { + groupItemToToggleIds.push(candidateId); + + stack.push(...list.structure.groupsState[candidateId].childrenIds); + } + } + + list.state.setExpanded((prevValues) => ({ + ...prevValues, + ...groupItemToToggleIds.reduce>((acc, id) => { + acc[id] = treeGroupNextValue; + + return acc; + }, {}), + })); + } + }; return ( @@ -41,6 +75,7 @@ export const InfinityScrollExample = ({ value={value} mapItemDataToProps={identity} items={items} + withItemClick={handleGroupItemClick} renderItem={({data, props, context: {isLastItem, childrenIds}}) => { const node = ( Date: Thu, 20 Jun 2024 14:26:18 +0300 Subject: [PATCH 04/15] fix(useList): groups state -> expanded state --- .../__stories__/stories/DefaultStory.tsx | 6 ++- src/components/TreeSelect/TreeSelect.tsx | 8 ++-- .../__stories__/TreeSelect.stories.tsx | 2 +- src/components/TreeSelect/types.ts | 2 +- .../useList/__stories__/docs/use-list.md | 38 +++++++++---------- src/components/useList/hooks/useList.ts | 8 ++-- .../useList/hooks/useListParsedState.ts | 6 +-- src/components/useList/hooks/useListState.ts | 6 +-- .../useList/utils/getListParsedState.test.ts | 2 +- .../useList/utils/getListParsedState.ts | 6 +-- 10 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/components/TreeList/__stories__/stories/DefaultStory.tsx b/src/components/TreeList/__stories__/stories/DefaultStory.tsx index b6fbf5790c..e3a63f60e9 100644 --- a/src/components/TreeList/__stories__/stories/DefaultStory.tsx +++ b/src/components/TreeList/__stories__/stories/DefaultStory.tsx @@ -23,7 +23,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { const listWithNoGroups = useList({ items, - rootNodesGroups: false, + withExpandedState: false, }); return ( @@ -38,7 +38,9 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { /> - List with `rootNodesGroups` false option in listState + + List with `withExpandedState` false option in list state + = (props) => { export const InfinityScroll = InfinityScrollTemplate.bind({}); InfinityScroll.args = { size: 'm', - groupsDefaultState: 'closed', + defaultExpandedState: 'closed', }; const WithFiltrationAndControlsTemplate: StoryFn = ( diff --git a/src/components/TreeSelect/types.ts b/src/components/TreeSelect/types.ts index 3fb0b83e89..eb9bf221de 100644 --- a/src/components/TreeSelect/types.ts +++ b/src/components/TreeSelect/types.ts @@ -31,7 +31,7 @@ export type TreeSelectRenderContainerProps = TreeListContainerProps; export type TreeSelectRenderContainer = TreeListRenderContainer; interface TreeSelectBehavioralProps extends UseListParsedStateProps { - rootNodesGroups?: boolean; + withExpandedState?: boolean; multiple?: boolean; } diff --git a/src/components/useList/__stories__/docs/use-list.md b/src/components/useList/__stories__/docs/use-list.md index ab2d39a672..4f80c0d23b 100644 --- a/src/components/useList/__stories__/docs/use-list.md +++ b/src/components/useList/__stories__/docs/use-list.md @@ -4,14 +4,14 @@ The main hook to use what provide you normalized representation of list items (` #### Props: -| Name | Description | Type | Default | -| :----------------- | :---------------------------------------------------------------------- | :-----------------------: | :--------: | -| items | a flat or tree-like data structure, with`List` declaration | `ListItemType[]` | | -| getItemId | Allows you to generate an id for a list item depending on the list data | `(itemData: T) => string` | | -| groupsDefaultState | Default group open state | `expanded`, `closed` | `expanded` | -| rootNodesGroups | Is nodes with children's groups | `boolean` | `true` | -| initialValues | Initial state values | `Partial` | | -| mixState | Way to override state by some controlled values. | `Partial` | | +| Name | Description | Type | Default | +| :------------------- | :------------------------------------------------------------------------- | :-----------------------: | :--------: | +| items | a flat or tree-like data structure, with`List` declaration | `ListItemType[]` | | +| getItemId | Allows you to generate an id for a list item depending on the list data | `(itemData: T) => string` | | +| defaultExpandedState | Default state for nodes with children items if `withExpandedState` is true | `expanded`, `closed` | `expanded` | +| withExpandedState | Is nodes with children's needed to be controlled | `boolean` | `true` | +| initialValues | Initial state values | `Partial` | | +| mixState | Way to override state by some controlled values. | `Partial` | | #### Result (UseList): @@ -22,17 +22,17 @@ The main hook to use what provide you normalized representation of list items (` #### ListState: -| Name | Description | Type | -| :-------------- | :----------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------: | -| selectedById | Key-value selected elements state | `Record` | -| disabledById | Key-value disabled elements state | `Record` | -| expandedById | Key-value expanded elements state. Available is only `rootNodesGroups` option of `useList` hook is `true` | `Record` | -| activeItemId | Active item id | `ListItemId`, `undefined` | -| setSelected | Method to handle selected state list items state | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | -| setDisabled | Method to handle disable state list items state | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | -| setExpanded | Method to handle expanded state list items state. Available is only `rootNodesGroups` option of `useList` hook is `true` | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | -| setExpanded | Normalized representation of list and some helpful data structures to work with list | `ListStructure` | -| setActiveItemId | Method to handle current active list item state | `(listItemId: ListItemId or undefined) => void` | +| Name | Description | Type | +| :-------------- | :------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------: | +| selectedById | Key-value selected elements state | `Record` | +| disabledById | Key-value disabled elements state | `Record` | +| expandedById | Key-value expanded elements state. Available is only `withExpandedState` option of `useList` hook is `true` | `Record` | +| activeItemId | Active item id | `ListItemId`, `undefined` | +| setSelected | Method to handle selected state list items state | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | +| setDisabled | Method to handle disable state list items state | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | +| setExpanded | Method to handle expanded state list items state. Available is only `withExpandedState` option of `useList` hook is `true` | `(payload: Record) => void` , `(fn: (payload: Record) => void) => void` | +| setExpanded | Normalized representation of list and some helpful data structures to work with list | `ListStructure` | +| setActiveItemId | Method to handle current active list item state | `(listItemId: ListItemId or undefined) => void` | #### ListStructure: diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index 42657ffe1a..e1941252a8 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -19,15 +19,15 @@ interface UseListProps extends UseListParsedStateProps, UseListStateProps export const useList = ({ items, getItemId, - groupsDefaultState = 'expanded', - rootNodesGroups = true, + defaultExpandedState = 'expanded', + withExpandedState = true, initialValues, mixState, }: UseListProps): UseList => { const {itemsById, groupsState, itemsState, initialState} = useListParsedState({ items, getItemId, - groupsDefaultState, + defaultExpandedState, }); const initValues = React.useMemo(() => { @@ -47,7 +47,7 @@ export const useList = ({ const innerState = useListState({ initialValues: initValues, - rootNodesGroups, + withExpandedState, }); const {visibleFlattenIds, idToFlattenIndex, rootIds} = useFlattenListItems({ diff --git a/src/components/useList/hooks/useListParsedState.ts b/src/components/useList/hooks/useListParsedState.ts index 933d9a0c26..c343e6e7ac 100644 --- a/src/components/useList/hooks/useListParsedState.ts +++ b/src/components/useList/hooks/useListParsedState.ts @@ -13,13 +13,13 @@ export interface UseListParsedStateProps extends GetListParsedStateProps { export function useListParsedState({ items, getItemId: propsGetItemId, - groupsDefaultState, + defaultExpandedState, }: UseListParsedStateProps) { const getItemId = React.useRef(propsGetItemId).current; const result = React.useMemo(() => { - return getListParsedState({items, getItemId, groupsDefaultState}); - }, [getItemId, groupsDefaultState, items]); + return getListParsedState({items, getItemId, defaultExpandedState}); + }, [getItemId, defaultExpandedState, items]); return result; } diff --git a/src/components/useList/hooks/useListState.ts b/src/components/useList/hooks/useListState.ts index e8cac6a750..7ceffb3bf1 100644 --- a/src/components/useList/hooks/useListState.ts +++ b/src/components/useList/hooks/useListState.ts @@ -8,10 +8,10 @@ export interface UseListStateProps { * Initial state values */ initialValues?: Partial; - rootNodesGroups?: boolean; + withExpandedState?: boolean; } -export const useListState = ({initialValues, rootNodesGroups}: UseListStateProps): ListState => { +export const useListState = ({initialValues, withExpandedState}: UseListStateProps): ListState => { const initialValuesRef = React.useRef(initialValues); const needToUpdateInitValues = initialValuesRef.current !== initialValues; initialValuesRef.current = initialValues; @@ -43,7 +43,7 @@ export const useListState = ({initialValues, rootNodesGroups}: UseListStateProps setActiveItemId, }; - if (rootNodesGroups) { + if (withExpandedState) { result.expandedById = expandedById; result.setExpanded = setExpanded; } diff --git a/src/components/useList/utils/getListParsedState.test.ts b/src/components/useList/utils/getListParsedState.test.ts index 588d2b4fe3..0b40db9151 100644 --- a/src/components/useList/utils/getListParsedState.test.ts +++ b/src/components/useList/utils/getListParsedState.test.ts @@ -150,7 +150,7 @@ describe('getListParsedState', () => { expect( getListParsedState({ items: data, - groupsDefaultState: 'closed', + defaultExpandedState: 'closed', getItemId: ({id}) => id, }), ).toEqual({ diff --git a/src/components/useList/utils/getListParsedState.ts b/src/components/useList/utils/getListParsedState.ts index c0e38f1074..39afd6b70e 100644 --- a/src/components/useList/utils/getListParsedState.ts +++ b/src/components/useList/utils/getListParsedState.ts @@ -33,7 +33,7 @@ type ListParsedStateResult = ParsedState & { export interface GetListParsedStateProps { items: ListItemType[]; - groupsDefaultState?: 'closed' | 'expanded'; + defaultExpandedState?: 'closed' | 'expanded'; /** * For example T is entity type with id what represents db id * So now you can use it id as a list item id in internal state @@ -42,7 +42,7 @@ export interface GetListParsedStateProps { } export function getListParsedState({ items, - groupsDefaultState = 'expanded', + defaultExpandedState = 'expanded', getItemId, }: GetListParsedStateProps): ListParsedStateResult { const result: ListParsedStateResult = { @@ -122,7 +122,7 @@ export function getListParsedState({ if (result.initialState.expandedById) { if (typeof item.expanded === 'undefined') { - result.initialState.expandedById[id] = groupsDefaultState === 'expanded'; + result.initialState.expandedById[id] = defaultExpandedState === 'expanded'; } else { result.initialState.expandedById[id] = item.expanded; } From 4464e79585d8399bfd31bf73e13fe7c9cd522d09 Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 14:38:11 +0300 Subject: [PATCH 05/15] fix(useList): default list list item on click handler hook -> util --- src/components/TreeList/TreeList.tsx | 4 ++-- src/components/useList/__stories__/Docs.mdx | 10 +++++----- .../useList/__stories__/components/FlattenList.tsx | 4 ++-- .../__stories__/components/InfinityScrollList.tsx | 4 ++-- .../useList/__stories__/components/ListWithDnd.tsx | 4 ++-- .../__stories__/components/PopupWithTogglerList.tsx | 4 ++-- .../useList/__stories__/components/RecursiveList.tsx | 4 ++-- .../useList/__stories__/docs/get-item-render-state.md | 2 +- ...st-item-click.md => get-list-item-click-handler.md} | 6 +++--- .../useList/__stories__/docs/use-list-keydown.md | 4 ++-- src/components/useList/index.ts | 2 +- .../getListItemClickHandler.ts} | 7 +++++-- src/unstable.ts | 2 +- 13 files changed, 30 insertions(+), 27 deletions(-) rename src/components/useList/__stories__/docs/{use-list-item-click.md => get-list-item-click-handler.md} (77%) rename src/components/useList/{hooks/useListItemClick.ts => utils/getListItemClickHandler.ts} (83%) diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index 7a45c0f896..5b06f97f5d 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -7,7 +7,7 @@ import { ListContainer, ListItemView, getItemRenderState, - useListItemClick, + getListItemClickHandler, useListKeydown, } from '../useList'; import type {ListItemId} from '../useList'; @@ -36,7 +36,7 @@ export const TreeList = ({ const containerRefLocal = React.useRef(null); const containerRef = propsContainerRef ?? containerRefLocal; - const defaultOnItemClick = useListItemClick({list, multiple}); + const defaultOnItemClick = getListItemClickHandler({list, multiple}); const onItemClick = React.useMemo(() => { if (propsOnItemClick === null) { diff --git a/src/components/useList/__stories__/Docs.mdx b/src/components/useList/__stories__/Docs.mdx index ceeb35de5f..a7a6ee20ff 100644 --- a/src/components/useList/__stories__/Docs.mdx +++ b/src/components/useList/__stories__/Docs.mdx @@ -3,7 +3,7 @@ import {Meta, Markdown} from '@storybook/addon-docs'; import UseListHook from './docs/use-list.md?raw'; import UseListKeydownHook from './docs/use-list-keydown.md?raw'; import UseListFilterHook from './docs/use-list-filter.md?raw'; -import UseListItemClickHook from './docs/use-list-item-click.md?raw'; +import GetListItemClickHandler from './docs/get-list-item-click-handler.md?raw'; import ListItemView from './docs/list-item-view.md?raw'; import ListContainerView from './docs/list-container-view.md?raw'; import ListRecursiveRenderer from './docs/list-recursive-renderer.md?raw'; @@ -28,7 +28,6 @@ The basic idea is that hooks take all the complex logic on themselves, and all y - [useList](#uselist); - [useListKeydown](#uselistkeydown) - [useListFilter](#uselistfilter); -- [useListItemClick](#uselistitemclick); ### Components (View only): @@ -38,6 +37,7 @@ The basic idea is that hooks take all the complex logic on themselves, and all y ### Utilities: +- [getListItemClickHandler](#getlistItemclickhandler); - [computeItemSize](#computeitemsize); - [scrollToListItem](#scrolltolistitem); - [getItemRenderState](#getitemrenderstate); @@ -66,7 +66,7 @@ function List() { const containerRef = React.useRef(null); const list = useList({items}); - const onItemClick = useListItemClick({list, multiple: true}); + const onItemClick = getListItemClickHandler({list, multiple: true}); useListKeydown({onItemClick, containerRef, list}); return ( @@ -136,8 +136,6 @@ function List() { {UseListFilterHook} -{UseListItemClickHook} - ## Components: {ListItemView} @@ -148,6 +146,8 @@ function List() { ## Utilities +{GetListItemClickHandler} + {computeItemSize} {scrollToListItem} diff --git a/src/components/useList/__stories__/components/FlattenList.tsx b/src/components/useList/__stories__/components/FlattenList.tsx index f4a45403cf..932e6b4201 100644 --- a/src/components/useList/__stories__/components/FlattenList.tsx +++ b/src/components/useList/__stories__/components/FlattenList.tsx @@ -8,11 +8,11 @@ import {ListContainerView} from '../../components/ListContainerView'; import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; -import {useListItemClick} from '../../hooks/useListItemClick'; import {useListKeydown} from '../../hooks/useListKeydown'; import type {ListItemSize} from '../../types'; import {computeItemSize} from '../../utils/computeItemSize'; import {getItemRenderState} from '../../utils/getItemRenderState'; +import {getListItemClickHandler} from '../../utils/getListItemClickHandler'; import {createRandomizedData} from '../utils/makeData'; import {VirtualizedListContainer} from './VirtualizedListContainer'; @@ -33,7 +33,7 @@ export const FlattenList = ({itemsCount, size}: FlattenListProps) => { const list = useList({items: filterState.items}); - const onItemClick = useListItemClick({list}); + const onItemClick = getListItemClickHandler({list}); useListKeydown({ containerRef, diff --git a/src/components/useList/__stories__/components/InfinityScrollList.tsx b/src/components/useList/__stories__/components/InfinityScrollList.tsx index 58ce905c85..dfbc1adc55 100644 --- a/src/components/useList/__stories__/components/InfinityScrollList.tsx +++ b/src/components/useList/__stories__/components/InfinityScrollList.tsx @@ -8,10 +8,10 @@ import {ListContainer} from '../../components/ListContainer'; import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; -import {useListItemClick} from '../../hooks/useListItemClick'; import {useListKeydown} from '../../hooks/useListKeydown'; import type {ListItemSize} from '../../types'; import {getItemRenderState} from '../../utils/getItemRenderState'; +import {getListItemClickHandler} from '../../utils/getListItemClickHandler'; import {useInfinityFetch} from '../utils/useInfinityFetch'; import {IntersectionContainer} from './IntersectionContainer/IntersectionContainer'; @@ -26,7 +26,7 @@ export const InfinityScrollList = ({size}: InfinityScrollListProps) => { const filterState = useListFilter({items: data}); const list = useList({items: filterState.items}); - const onItemClick = useListItemClick({list, multiple: true}); + const onItemClick = getListItemClickHandler({list, multiple: true}); useListKeydown({ containerRef, diff --git a/src/components/useList/__stories__/components/ListWithDnd.tsx b/src/components/useList/__stories__/components/ListWithDnd.tsx index e88c58695b..ab20c12c61 100644 --- a/src/components/useList/__stories__/components/ListWithDnd.tsx +++ b/src/components/useList/__stories__/components/ListWithDnd.tsx @@ -15,10 +15,10 @@ import {ListContainerView} from '../../components/ListContainerView'; import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; -import {useListItemClick} from '../../hooks/useListItemClick'; import {useListKeydown} from '../../hooks/useListKeydown'; import type {ListItemSize} from '../../types'; import {getItemRenderState} from '../../utils/getItemRenderState'; +import {getListItemClickHandler} from '../../utils/getListItemClickHandler'; import {createRandomizedData} from '../utils/makeData'; import {reorderArray} from '../utils/reorderArray'; @@ -42,7 +42,7 @@ export const ListWithDnd = ({size, itemsCount, 'aria-label': ariaLabel}: ListWit items: filterState.items, }); - const onItemClick = useListItemClick({list}); + const onItemClick = getListItemClickHandler({list}); useListKeydown({ containerRef, diff --git a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx index 834aece277..2a835a7969 100644 --- a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx +++ b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx @@ -6,10 +6,10 @@ import {Flex} from '../../../layout'; import {ListContainer} from '../../components/ListContainer'; import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; -import {useListItemClick} from '../../hooks/useListItemClick'; import {useListKeydown} from '../../hooks/useListKeydown'; import type {ListItemSize} from '../../types'; import {getItemRenderState} from '../../utils/getItemRenderState'; +import {getListItemClickHandler} from '../../utils/getListItemClickHandler'; import {scrollToListItem} from '../../utils/scrollToListItem'; import {createRandomizedData} from '../utils/makeData'; @@ -32,7 +32,7 @@ export const PopupWithTogglerList = ({size, itemsCount}: PopupWithTogglerListPro const list = useList({items}); - const onItemClick = useListItemClick({list}); + const onItemClick = getListItemClickHandler({list}); const [selectedId] = React.useMemo( () => Object.keys(list.state.selectedById), diff --git a/src/components/useList/__stories__/components/RecursiveList.tsx b/src/components/useList/__stories__/components/RecursiveList.tsx index 91e4d67287..7a1e1ef61b 100644 --- a/src/components/useList/__stories__/components/RecursiveList.tsx +++ b/src/components/useList/__stories__/components/RecursiveList.tsx @@ -6,10 +6,10 @@ import {ListContainer} from '../../components/ListContainer'; import {ListItemView} from '../../components/ListItemView'; import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; -import {useListItemClick} from '../../hooks/useListItemClick'; import {useListKeydown} from '../../hooks/useListKeydown'; import type {ListItemSize} from '../../types'; import {getItemRenderState} from '../../utils/getItemRenderState'; +import {getListItemClickHandler} from '../../utils/getListItemClickHandler'; import {createRandomizedData} from '../utils/makeData'; export interface RecursiveListProps { @@ -30,7 +30,7 @@ export const RecursiveList = ({size, itemsCount, 'aria-label': ariaLabel}: Recur const list = useList({items: filterState.items}); - const onItemClick = useListItemClick({list}); + const onItemClick = getListItemClickHandler({list}); useListKeydown({ containerRef, diff --git a/src/components/useList/__stories__/docs/get-item-render-state.md b/src/components/useList/__stories__/docs/get-item-render-state.md index bd6abded64..6057d3b9fe 100644 --- a/src/components/useList/__stories__/docs/get-item-render-state.md +++ b/src/components/useList/__stories__/docs/get-item-render-state.md @@ -11,7 +11,7 @@ import { } from '@gravity-ui/uikit/unstable'; const list = useList({items: [...]}); -const onItemClick = useListItemClick({list}); +const onItemClick = getListItemClickHandler({list}); const {data, props, context} = getItemRenderState({ qa: 'some-qa-id', diff --git a/src/components/useList/__stories__/docs/use-list-item-click.md b/src/components/useList/__stories__/docs/get-list-item-click-handler.md similarity index 77% rename from src/components/useList/__stories__/docs/use-list-item-click.md rename to src/components/useList/__stories__/docs/get-list-item-click-handler.md index 869996195e..4eaa8edea9 100644 --- a/src/components/useList/__stories__/docs/use-list-item-click.md +++ b/src/components/useList/__stories__/docs/get-list-item-click-handler.md @@ -1,9 +1,9 @@ -### useListItemClick +### getListItemClickHandler Basic click logic implemented for you ```tsx -import {unstable_useListItemClick as useListItemClick} from '@gravity-ui/uikit/unstable'; +import {unstable_getListItemClickHandler as getListItemClickHandler} from '@gravity-ui/uikit/unstable'; ``` #### props: @@ -24,7 +24,7 @@ const filterState = useListFilter({items: [...]}); const list = useList({items: filterState.items}); -const onItemClick = useListItemClick({list}); +const onItemClick = getListItemClickHandler({list}); useListKeydown({ containerRef, diff --git a/src/components/useList/__stories__/docs/use-list-keydown.md b/src/components/useList/__stories__/docs/use-list-keydown.md index 6c3781bdb5..a37a1a819a 100644 --- a/src/components/useList/__stories__/docs/use-list-keydown.md +++ b/src/components/useList/__stories__/docs/use-list-keydown.md @@ -17,12 +17,12 @@ Keyboard support import { unstable_useList as useList, unstable_useListKeydown as useListKeydown, - unstable_useListItemClick as useListItemClick, + unstable_getListItemClickHandler as getListItemClickHandler, } from '@gravity-ui/uikit/unstable'; const containerRef = React.useRef(null); const list = useList(...) -const handleItemClick = useListItemClick({list}); +const handleItemClick = getListItemClickHandler({list}); useListKeydown({ onItemClick, diff --git a/src/components/useList/index.ts b/src/components/useList/index.ts index 4f4028560c..0f8ff3ce06 100644 --- a/src/components/useList/index.ts +++ b/src/components/useList/index.ts @@ -1,12 +1,12 @@ export * from './hooks/useListFilter'; export * from './hooks/useList'; export * from './hooks/useListKeydown'; -export * from './hooks/useListItemClick'; export * from './types'; export * from './components/ListItemView'; export * from './components/ListRecursiveRenderer'; export * from './components/ListContainerView'; export * from './components/ListContainer'; +export * from './utils/getListItemClickHandler'; export * from './utils/computeItemSize'; export * from './utils/getItemRenderState'; export * from './utils/scrollToListItem'; diff --git a/src/components/useList/hooks/useListItemClick.ts b/src/components/useList/utils/getListItemClickHandler.ts similarity index 83% rename from src/components/useList/hooks/useListItemClick.ts rename to src/components/useList/utils/getListItemClickHandler.ts index 4986b3fb6a..13f4132e37 100644 --- a/src/components/useList/hooks/useListItemClick.ts +++ b/src/components/useList/utils/getListItemClickHandler.ts @@ -1,11 +1,14 @@ import type {ListItemId, UseList} from '../types'; -interface UseListItemClickOptions { +interface GetListItemClickHandlerProps { multiple?: boolean; list: UseList; } -export const useListItemClick = ({list, multiple}: UseListItemClickOptions) => { +export const getListItemClickHandler = ({ + list, + multiple, +}: GetListItemClickHandlerProps) => { const onItemClick = ({id}: {id: ListItemId}) => { if (list.state.disabledById[id]) return; diff --git a/src/unstable.ts b/src/unstable.ts index 149d56386d..36c1c8fe2b 100644 --- a/src/unstable.ts +++ b/src/unstable.ts @@ -3,7 +3,7 @@ export { useList as unstable_useList, useListFilter as unstable_useListFilter, useListKeydown as unstable_useListKeydown, - useListItemClick as unstable_useListItemClick, + getListItemClickHandler as unstable_getListItemClickHandler, ListItemView as unstable_ListItemView, type ListItemViewProps as unstable_ListItemViewProps, ListContainerView as unstable_ListContainerView, From 9b31f78201f37e97f9c54376a585fc7d15839d07 Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 15:53:39 +0300 Subject: [PATCH 06/15] fix(TreeList): fix onItemClick handler deps --- src/components/TreeList/TreeList.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index 5b06f97f5d..df6cb10e59 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -36,14 +36,12 @@ export const TreeList = ({ const containerRefLocal = React.useRef(null); const containerRef = propsContainerRef ?? containerRefLocal; - const defaultOnItemClick = getListItemClickHandler({list, multiple}); - const onItemClick = React.useMemo(() => { if (propsOnItemClick === null) { return undefined; } - const onClick = propsOnItemClick ?? defaultOnItemClick; + const onClick = propsOnItemClick ?? getListItemClickHandler({list, multiple}); return ({id}: {id: ListItemId}) => { const payload = {id, list}; @@ -51,7 +49,7 @@ export const TreeList = ({ onClick(payload); withItemClick?.(payload); }; - }, [defaultOnItemClick, list, propsOnItemClick, withItemClick]); + }, [list, multiple, propsOnItemClick, withItemClick]); useListKeydown({ containerRef, From c706f20da6ed5e3604d8a2ee1bd9e3173f41d6ce Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 17:16:18 +0300 Subject: [PATCH 07/15] fix(useList): add evend to onItemClick and some minor renames --- src/components/TreeList/TreeList.tsx | 20 ++++++------ .../TreeList/__stories__/TreeListDocs.md | 26 +++++++-------- .../stories/WithDisabledElementsStory.tsx | 2 +- src/components/TreeList/types.ts | 9 ++++-- src/components/TreeSelect/TreeSelect.tsx | 4 +-- .../__stories__/TreeSelect.stories.tsx | 2 +- .../components/InfinityScrollExample.tsx | 2 +- .../__stories__/docs/get-item-render-state.md | 16 +++++----- .../__stories__/docs/use-list-keydown.md | 12 +++---- .../useList/__stories__/docs/use-list.md | 2 +- src/components/useList/hooks/useList.ts | 4 +-- .../useList/hooks/useListKeydown.tsx | 4 +-- src/components/useList/hooks/useListState.ts | 32 +++++++++---------- src/components/useList/types.ts | 7 +++- .../useList/utils/getItemRenderState.tsx | 5 +-- .../useList/utils/getListItemClickHandler.ts | 4 +-- 16 files changed, 82 insertions(+), 69 deletions(-) diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index df6cb10e59..8236d835f2 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -10,7 +10,7 @@ import { getListItemClickHandler, useListKeydown, } from '../useList'; -import type {ListItemId} from '../useList'; +import type {ListOnItemClick} from '../useList'; import {block} from '../utils/cn'; import type {TreeListContainerProps, TreeListProps} from './types'; @@ -23,12 +23,12 @@ export const TreeList = ({ size = 'm', className, list, + multiple, + containerRef: propsContainerRef, renderItem: propsRenderItem, renderContainer = ListContainer, onItemClick: propsOnItemClick, - multiple, - containerRef: propsContainerRef, - withItemClick, + onItemAction, mapItemDataToProps, }: TreeListProps) => { const uniqId = useUniqId(); @@ -43,13 +43,15 @@ export const TreeList = ({ const onClick = propsOnItemClick ?? getListItemClickHandler({list, multiple}); - return ({id}: {id: ListItemId}) => { - const payload = {id, list}; + const handler: ListOnItemClick = (arg, e) => { + const payload = {id: arg.id, list}; - onClick(payload); - withItemClick?.(payload); + onClick(payload, e); + onItemAction?.(payload); }; - }, [list, multiple, propsOnItemClick, withItemClick]); + + return handler; + }, [list, multiple, propsOnItemClick, onItemAction]); useListKeydown({ containerRef, diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md index 32a6245213..46423583ed 100644 --- a/src/components/TreeList/__stories__/TreeListDocs.md +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -64,19 +64,19 @@ const Component = () => { ## Props: -| Name | Description | Type | Default | -| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------: | :-----: | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| qa | Selector for tests | `string` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | -| multiple | One or multiple elements selected list | `boolean` | `false` | -| id | id attribute | `string` | | -| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | -| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | -| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}) => React.JSX.Element \| null` | | -| withItemClick | Don't override default click behavior and add additional logic. Work's if `onItemClick` not `null` | `TreeListOnItemClick \| null` | | +| Name | Description | Type | Default | +| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}, e: React.SyntheticEvent) => void \| null` | | +| onItemAction | Don't override default click behavior and add additional logic. Work's if `onItemClick` not `null` | `TreeListOnItemClick \| null` | | ### TreeListRenderItem props: diff --git a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx index 41528880c9..a92d58a508 100644 --- a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx @@ -51,7 +51,7 @@ export const WithDisabledElementsStory = ({...storyProps}: WithDisabledElementsS list={list} containerRef={containerRef} mapItemDataToProps={({text}) => ({title: text})} - withItemClick={({id}) => { + onItemAction={({id}) => { alert( `Clicked by item with id :"${id}" and data: ${JSON.stringify(list.structure.itemsById[id])}`, ); diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 89658993f6..0d366ceda8 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -32,7 +32,12 @@ export type TreeListMapItemDataToProps = (item: T) => ListItemCommonProps; export type TreeListOnItemClickPayload = {id: ListItemId; list: UseList}; -export type TreeListOnItemClick = (payload: TreeListOnItemClickPayload) => void; +export type TreeListOnItemClick = ( + payload: TreeListOnItemClickPayload, + e?: React.SyntheticEvent, +) => void; + +export type TreeListOnItemAction = (payload: TreeListOnItemClickPayload) => void; export interface TreeListProps extends QAProps { /** @@ -57,6 +62,6 @@ export interface TreeListProps extends QAProps { * Don't override default click behavior and add additional logic. * Work's if `onItemClick` not `null` */ - withItemClick?: TreeListOnItemClick; + onItemAction?: TreeListOnItemAction; mapItemDataToProps: TreeListMapItemDataToProps; } diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 529257cf2d..3cffcde006 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -61,7 +61,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect< onBlur, getItemId, onItemClick, - withItemClick, + onItemAction, }: TreeSelectProps, ref: React.Ref, ) { @@ -223,7 +223,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect< id={`list-${treeSelectId}`} containerRef={containerRef} onItemClick={typeof onItemClick === 'undefined' ? handleItemClick : onItemClick} - withItemClick={withItemClick} + onItemAction={onItemAction} renderContainer={renderContainer} mapItemDataToProps={mapItemDataToProps} renderItem={renderItem ?? defaultItemRenderer} diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index 9992fa9b80..5df6938ef4 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -52,7 +52,7 @@ const DefaultTemplate: StoryFn< {...props} items={items} mapItemDataToProps={(x) => x} - withItemClick={(id) => { + onItemAction={(id) => { console.log('clicked on item with id: ', id); }} /> diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index a7354491d6..2293306272 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -75,7 +75,7 @@ export const InfinityScrollExample = ({ value={value} mapItemDataToProps={identity} items={items} - withItemClick={handleGroupItemClick} + onItemAction={handleGroupItemClick} renderItem={({data, props, context: {isLastItem, childrenIds}}) => { const node = ( ; #### Props: -| Name | Description | Type | Default | -| :----------------- | :--------------------------------------------------------------------------------- | :--------------------------------: | :-----: | -| id | `id` of list item | `ListItemId` | | -| list | result of `useList` hook | `UseList` | | -| multiple | One or multiple elements selected list | `boolean` | | -| onItemClick | Optional on click handler | `(id: ListItemId) => void` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| Name | Description | Type | Default | +| :----------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | +| id | `id` of list item | `ListItemId` | | +| list | result of `useList` hook | `UseList` | | +| multiple | One or multiple elements selected list | `boolean` | | +| onItemClick | Optional on click handler | `(payload :{id: ListItemId}, e: React.SyntheticEvent) => void` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | ##### ListItemCommonProps diff --git a/src/components/useList/__stories__/docs/use-list-keydown.md b/src/components/useList/__stories__/docs/use-list-keydown.md index a37a1a819a..b8fb8331ce 100644 --- a/src/components/useList/__stories__/docs/use-list-keydown.md +++ b/src/components/useList/__stories__/docs/use-list-keydown.md @@ -4,12 +4,12 @@ Keyboard support #### Props: -| Name | Description | Type | Default | -| :----------- | :-------------------------------------------------------------------------------------------- | :------------------------------------------------: | :-----: | -| list | result of `useList` hook | `UseList` | | -| onItemClick | callback will be called when pressing the `Enter`, `Space` keys; | `(payload: {id: ListItemId}) => void` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| enabled | on/off keyboard support. Use it if you need to change the behavior in runtime; | `boolean` | | +| Name | Description | Type | Default | +| :----------- | :-------------------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | +| list | result of `useList` hook | `UseList` | | +| onItemClick | callback will be called when pressing the `Enter`, `Space` keys; | `(payload: {id: ListItemId}, e: React.SyntheticEvent) => void` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| enabled | on/off keyboard support. Use it if you need to change the behavior in runtime; | `boolean` | | #### Usage example: diff --git a/src/components/useList/__stories__/docs/use-list.md b/src/components/useList/__stories__/docs/use-list.md index 4f80c0d23b..b2555ea98a 100644 --- a/src/components/useList/__stories__/docs/use-list.md +++ b/src/components/useList/__stories__/docs/use-list.md @@ -10,7 +10,7 @@ The main hook to use what provide you normalized representation of list items (` | getItemId | Allows you to generate an id for a list item depending on the list data | `(itemData: T) => string` | | | defaultExpandedState | Default state for nodes with children items if `withExpandedState` is true | `expanded`, `closed` | `expanded` | | withExpandedState | Is nodes with children's needed to be controlled | `boolean` | `true` | -| initialValues | Initial state values | `Partial` | | +| initialState | Initial state values | `Partial` | | | mixState | Way to override state by some controlled values. | `Partial` | | #### Result (UseList): diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index e1941252a8..95e0c1cdcb 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -21,7 +21,7 @@ export const useList = ({ getItemId, defaultExpandedState = 'expanded', withExpandedState = true, - initialValues, + initialState: initialValues, mixState, }: UseListProps): UseList => { const {itemsById, groupsState, itemsState, initialState} = useListParsedState({ @@ -46,7 +46,7 @@ export const useList = ({ ]); const innerState = useListState({ - initialValues: initValues, + initialState: initValues, withExpandedState, }); diff --git a/src/components/useList/hooks/useListKeydown.tsx b/src/components/useList/hooks/useListKeydown.tsx index 4f976d5683..780897cae4 100644 --- a/src/components/useList/hooks/useListKeydown.tsx +++ b/src/components/useList/hooks/useListKeydown.tsx @@ -1,12 +1,12 @@ import React from 'react'; import {KeyCode} from '../../../constants'; -import type {ListItemId, UseList} from '../types'; +import type {ListOnItemClick, UseList} from '../types'; import {findNextIndex} from '../utils/findNextIndex'; import {scrollToListItem} from '../utils/scrollToListItem'; interface UseListKeydownProps { - onItemClick?(payload: {id: ListItemId}): void; + onItemClick?: ListOnItemClick; containerRef?: React.RefObject; enabled?: boolean; list: UseList; diff --git a/src/components/useList/hooks/useListState.ts b/src/components/useList/hooks/useListState.ts index 7ceffb3bf1..c5333964a1 100644 --- a/src/components/useList/hooks/useListState.ts +++ b/src/components/useList/hooks/useListState.ts @@ -7,31 +7,31 @@ export interface UseListStateProps { /** * Initial state values */ - initialValues?: Partial; + initialState?: Partial; withExpandedState?: boolean; } -export const useListState = ({initialValues, withExpandedState}: UseListStateProps): ListState => { - const initialValuesRef = React.useRef(initialValues); - const needToUpdateInitValues = initialValuesRef.current !== initialValues; - initialValuesRef.current = initialValues; +export const useListState = ({initialState, withExpandedState}: UseListStateProps): ListState => { + const initialStateRef = React.useRef(initialState); + const needToUpdateInitValues = initialStateRef.current !== initialState; + initialStateRef.current = initialState; - const [disabledById, setDisabled] = React.useState(() => initialValues?.disabledById ?? {}); - const [selectedById, setSelected] = React.useState(() => initialValues?.selectedById ?? {}); - const [expandedById, setExpanded] = React.useState(() => initialValues?.expandedById ?? {}); - const [activeItemId, setActiveItemId] = React.useState(() => initialValues?.activeItemId); + const [disabledById, setDisabled] = React.useState(() => initialState?.disabledById ?? {}); + const [selectedById, setSelected] = React.useState(() => initialState?.selectedById ?? {}); + const [expandedById, setExpanded] = React.useState(() => initialState?.expandedById ?? {}); + const [activeItemId, setActiveItemId] = React.useState(() => initialState?.activeItemId); if (needToUpdateInitValues) { - if (initialValues?.disabledById) { - setDisabled((prevValues) => ({...initialValues.disabledById, ...prevValues})); + if (initialState?.disabledById) { + setDisabled((prevValues) => ({...initialState.disabledById, ...prevValues})); } - if (initialValues?.selectedById) { - setSelected((prevValues) => ({...initialValues.selectedById, ...prevValues})); + if (initialState?.selectedById) { + setSelected((prevValues) => ({...initialState.selectedById, ...prevValues})); } - if (initialValues?.expandedById) { - setExpanded((prevValues) => ({...initialValues.expandedById, ...prevValues})); + if (initialState?.expandedById) { + setExpanded((prevValues) => ({...initialState.expandedById, ...prevValues})); } - setActiveItemId((prevValue) => prevValue ?? initialValues?.activeItemId); + setActiveItemId((prevValue) => prevValue ?? initialState?.activeItemId); } const result: ListState = { diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 584a44401c..b8174d6ea7 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -56,7 +56,7 @@ export type ListItemListContextProps = ItemState & export type RenderItemProps = { size: ListItemSize; id: ListItemId; - onClick: (() => void) | undefined; + onClick: ((e: React.SyntheticEvent) => void) | undefined; selected: boolean | undefined; disabled: boolean; expanded: boolean | undefined; @@ -127,3 +127,8 @@ export type UseList = { state: ListState; structure: ListStructure; }; + +export type ListOnItemClick = ( + payload: {id: ListItemId}, + e?: React.SyntheticEvent, +) => void; diff --git a/src/components/useList/utils/getItemRenderState.tsx b/src/components/useList/utils/getItemRenderState.tsx index eadf7ad1dc..557bf72aca 100644 --- a/src/components/useList/utils/getItemRenderState.tsx +++ b/src/components/useList/utils/getItemRenderState.tsx @@ -5,6 +5,7 @@ import type { ListItemId, ListItemListContextProps, ListItemSize, + ListOnItemClick, RenderItemProps, UseList, } from '../types'; @@ -19,7 +20,7 @@ type ItemRendererProps = QAProps & { multiple?: boolean; id: ListItemId; mapItemDataToProps(data: T): ListItemCommonProps; - onItemClick?(payload: {id: ListItemId}): void; + onItemClick?: ListOnItemClick; list: UseList; }; @@ -58,7 +59,7 @@ export const getItemRenderState = ({ disabled: Boolean(list.state.disabledById?.[id]), selected: Boolean(list.state.selectedById[id]), hasSelectionIcon: Boolean(multiple) && !context.childrenIds, // hide multiple selection view at group nodes - onClick: onItemClick ? () => onItemClick({id}) : undefined, + onClick: onItemClick ? (e: React.SyntheticEvent) => onItemClick({id}, e) : undefined, ...mapItemDataToProps(list.structure.itemsById[id]), }; diff --git a/src/components/useList/utils/getListItemClickHandler.ts b/src/components/useList/utils/getListItemClickHandler.ts index 13f4132e37..db2f359a03 100644 --- a/src/components/useList/utils/getListItemClickHandler.ts +++ b/src/components/useList/utils/getListItemClickHandler.ts @@ -1,4 +1,4 @@ -import type {ListItemId, UseList} from '../types'; +import type {ListOnItemClick, UseList} from '../types'; interface GetListItemClickHandlerProps { multiple?: boolean; @@ -9,7 +9,7 @@ export const getListItemClickHandler = ({ list, multiple, }: GetListItemClickHandlerProps) => { - const onItemClick = ({id}: {id: ListItemId}) => { + const onItemClick: ListOnItemClick = ({id}) => { if (list.state.disabledById[id]) return; // always activate selected item From 1abedc4eb924dc0d41931c478db0751e8c20fa0a Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 19:38:41 +0300 Subject: [PATCH 08/15] fix(TreeSelect): remove deps from multiple from value --- src/components/TreeSelect/TreeSelect.tsx | 15 +++--- .../components/InfinityScrollExample.tsx | 2 +- .../hooks/useTreeSelectSelection.ts | 47 ++++--------------- src/components/TreeSelect/types.ts | 13 ++--- 4 files changed, 21 insertions(+), 56 deletions(-) diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 3cffcde006..35ae7f7ca0 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -13,7 +13,7 @@ import {block} from '../utils/cn'; import type {CnMods} from '../utils/cn'; import {useTreeSelectSelection, useValue} from './hooks/useTreeSelectSelection'; -import type {MultipleValue, TreeSelectProps, TreeSelectRenderControlProps} from './types'; +import type {TreeSelectProps, TreeSelectRenderControlProps} from './types'; import './TreeSelect.scss'; @@ -23,10 +23,7 @@ const defaultItemRenderer: TreeListRenderItem = (renderState) => { return ; }; -export const TreeSelect = React.forwardRef(function TreeSelect< - T, - M extends boolean | undefined = undefined, ->( +export const TreeSelect = React.forwardRef(function TreeSelect( { id, qa, @@ -62,7 +59,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect< getItemId, onItemClick, onItemAction, - }: TreeSelectProps, + }: TreeSelectProps, ref: React.Ref, ) { const mobile = useMobile(); @@ -97,7 +94,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect< value, setInnerValue, onUpdate: (ids) => { - onUpdate?.((multiple ? ids : ids[0]) as MultipleValue, list); + onUpdate?.(ids, list); }, defaultOpen, open: propsOpen, @@ -233,6 +230,6 @@ export const TreeSelect = React.forwardRef(function TreeSelect< ); -}) as ( - props: TreeSelectProps & {ref?: React.Ref}, +}) as ( + props: TreeSelectProps & {ref?: React.Ref}, ) => React.ReactElement; diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index 2293306272..fb6f4f30af 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -32,7 +32,7 @@ export const InfinityScrollExample = ({ itemsCount = 5, ...storyProps }: InfinityScrollExampleProps) => { - const [value, setValue] = React.useState(undefined); + const [value, setValue] = React.useState([]); const { data: items = [], onFetchMore, diff --git a/src/components/TreeSelect/hooks/useTreeSelectSelection.ts b/src/components/TreeSelect/hooks/useTreeSelectSelection.ts index fc014c92f6..cccc08590c 100644 --- a/src/components/TreeSelect/hooks/useTreeSelectSelection.ts +++ b/src/components/TreeSelect/hooks/useTreeSelectSelection.ts @@ -4,51 +4,24 @@ import type {UseOpenProps} from '../../../hooks/useSelect/types'; import {useOpenState} from '../../../hooks/useSelect/useOpenState'; import type {ListItemId} from '../../useList/types'; -type UseValueProps = { - value?: T; - defaultValue?: T; +type UseValueProps = { + value?: string[]; + defaultValue?: string[]; }; -export const useValue = ({ - defaultValue, - value: valueProps, -}: UseValueProps) => { - const [innerValue, setInnerValue] = React.useState( - Array.isArray(defaultValue) ? defaultValue : [], - ); - - const value: string[] = React.useMemo(() => { - if (valueProps) { - if (Array.isArray(valueProps)) { - return valueProps; - } - - if (typeof valueProps === 'string') { - return [valueProps]; - } - - return []; - } +export const useValue = ({defaultValue = [], value: valueProps}: UseValueProps) => { + const [innerValue, setInnerValue] = React.useState(defaultValue); - return innerValue; - }, [valueProps, innerValue]); + const value: string[] = valueProps ?? innerValue; const uncontrolled = !valueProps; const selected: Record = React.useMemo(() => { - if (Array.isArray(value)) { - return value.reduce>((acc, val) => { - acc[val] = true; - - return acc; - }, {}); - } - - if (typeof value === 'string') { - return {[value]: true}; - } + return value.reduce>((acc, val) => { + acc[val] = true; - return {}; + return acc; + }, {}); }, [value]); return { diff --git a/src/components/TreeSelect/types.ts b/src/components/TreeSelect/types.ts index eb9bf221de..5cfa7269cc 100644 --- a/src/components/TreeSelect/types.ts +++ b/src/components/TreeSelect/types.ts @@ -35,21 +35,16 @@ interface TreeSelectBehavioralProps extends UseListParsedStateProps { multiple?: boolean; } -export type MultipleValue = M extends true - ? ListItemId[] - : ListItemId | undefined; - -export interface TreeSelectProps +export interface TreeSelectProps extends Omit, 'list' | 'renderContainer' | 'multiple'>, UseOpenProps, TreeSelectBehavioralProps { - multiple?: M; /** * Control's title attribute value */ title?: string; - value?: MultipleValue; - defaultValue?: MultipleValue | undefined; + value?: ListItemId[]; + defaultValue?: ListItemId[] | undefined; popupClassName?: string; popupWidth?: SelectPopupProps['width']; placement?: PopperPlacement; @@ -66,7 +61,7 @@ export interface TreeSelectProps, list: UseList): void; + onUpdate?(value: ListItemId[], list: UseList): void; /** * Ability to override custom toggler button */ From d5d74ca5878a0fae7bf61919bd40c50f3f31d07d Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 19:52:36 +0300 Subject: [PATCH 09/15] fix(TreeList): remove onItemAction prop and add disableDefaultClickHandler prop instead --- src/components/TreeList/TreeList.tsx | 17 +++--- .../TreeList/__stories__/TreeListDocs.md | 26 ++++----- .../__stories__/stories/DefaultStory.tsx | 2 + .../stories/WithDisabledElementsStory.tsx | 2 +- src/components/TreeList/types.ts | 7 +-- src/components/TreeSelect/TreeSelect.tsx | 58 ++++++++++++------- .../__stories__/TreeSelect.stories.tsx | 2 +- .../components/InfinityScrollExample.tsx | 2 +- 8 files changed, 66 insertions(+), 50 deletions(-) diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index 8236d835f2..80fd5fb7b3 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -28,7 +28,7 @@ export const TreeList = ({ renderItem: propsRenderItem, renderContainer = ListContainer, onItemClick: propsOnItemClick, - onItemAction, + disableDefaultClickHandler, mapItemDataToProps, }: TreeListProps) => { const uniqId = useUniqId(); @@ -37,21 +37,24 @@ export const TreeList = ({ const containerRef = propsContainerRef ?? containerRefLocal; const onItemClick = React.useMemo(() => { - if (propsOnItemClick === null) { + if (disableDefaultClickHandler && !propsOnItemClick) { return undefined; } - const onClick = propsOnItemClick ?? getListItemClickHandler({list, multiple}); - const handler: ListOnItemClick = (arg, e) => { const payload = {id: arg.id, list}; - onClick(payload, e); - onItemAction?.(payload); + if (!disableDefaultClickHandler) { + const baseOnClick = getListItemClickHandler({list, multiple}); + + baseOnClick(payload, e); + } + + propsOnItemClick?.(payload, e); }; return handler; - }, [list, multiple, propsOnItemClick, onItemAction]); + }, [disableDefaultClickHandler, propsOnItemClick, list, multiple]); useListKeydown({ containerRef, diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md index 46423583ed..8cc36e256b 100644 --- a/src/components/TreeList/__stories__/TreeListDocs.md +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -64,19 +64,19 @@ const Component = () => { ## Props: -| Name | Description | Type | Default | -| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------: | :-----: | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| qa | Selector for tests | `string` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | -| multiple | One or multiple elements selected list | `boolean` | `false` | -| id | id attribute | `string` | | -| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | -| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | -| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}, e: React.SyntheticEvent) => void \| null` | | -| onItemAction | Don't override default click behavior and add additional logic. Work's if `onItemClick` not `null` | `TreeListOnItemClick \| null` | | +| Name | Description | Type | Default | +| :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}, e: React.SyntheticEvent) => void \| null` | | +| disableDefaultClickHandler | Disable default `onItemClick` handler logic | `TreeListOnItemClick \| null` | | ### TreeListRenderItem props: diff --git a/src/components/TreeList/__stories__/stories/DefaultStory.tsx b/src/components/TreeList/__stories__/stories/DefaultStory.tsx index e3a63f60e9..5a9dc705a4 100644 --- a/src/components/TreeList/__stories__/stories/DefaultStory.tsx +++ b/src/components/TreeList/__stories__/stories/DefaultStory.tsx @@ -35,6 +35,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { list={listWithGroups} onItemClick={null} mapItemDataToProps={identity} + disableDefaultClickHandler /> @@ -47,6 +48,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { list={listWithNoGroups} onItemClick={null} mapItemDataToProps={identity} + disableDefaultClickHandler />
diff --git a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx index a92d58a508..7c3afb4508 100644 --- a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx @@ -51,7 +51,7 @@ export const WithDisabledElementsStory = ({...storyProps}: WithDisabledElementsS list={list} containerRef={containerRef} mapItemDataToProps={({text}) => ({title: text})} - onItemAction={({id}) => { + onItemClick={({id}) => { alert( `Clicked by item with id :"${id}" and data: ${JSON.stringify(list.structure.itemsById[id])}`, ); diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 0d366ceda8..3e4b7eddf1 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -37,8 +37,6 @@ export type TreeListOnItemClick = ( e?: React.SyntheticEvent, ) => void; -export type TreeListOnItemAction = (payload: TreeListOnItemClickPayload) => void; - export interface TreeListProps extends QAProps { /** * Control outside list container dom element. For example for keyboard @@ -59,9 +57,8 @@ export interface TreeListProps extends QAProps { */ onItemClick?: null | TreeListOnItemClick; /** - * Don't override default click behavior and add additional logic. - * Work's if `onItemClick` not `null` + * Disable default `onItemClick` handler logic */ - onItemAction?: TreeListOnItemAction; + disableDefaultClickHandler?: boolean; mapItemDataToProps: TreeListMapItemDataToProps; } diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 35ae7f7ca0..d0c76afec1 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -6,7 +6,7 @@ import {useFocusWithin, useForkRef, useUniqId} from '../../hooks'; import {SelectControl} from '../Select/components'; import {SelectPopup} from '../Select/components/SelectPopup/SelectPopup'; import {TreeList} from '../TreeList'; -import type {TreeListOnItemClickPayload, TreeListRenderItem} from '../TreeList/types'; +import type {TreeListOnItemClick, TreeListRenderItem} from '../TreeList/types'; import {useMobile} from '../mobile'; import {ListItemView, useList} from '../useList'; import {block} from '../utils/cn'; @@ -47,6 +47,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( defaultValue, withExpandedState = true, defaultExpandedState = 'expanded', + disableDefaultClickHandler, onClose, onOpenChange, onUpdate, @@ -58,7 +59,6 @@ export const TreeSelect = React.forwardRef(function TreeSelect( onBlur, getItemId, onItemClick, - onItemAction, }: TreeSelectProps, ref: React.Ref, ) { @@ -102,30 +102,45 @@ export const TreeSelect = React.forwardRef(function TreeSelect( onOpenChange, }); - const handleItemClick = React.useCallback( - (payload: TreeListOnItemClickPayload) => { + const handleItemClick = React.useMemo(() => { + if (disableDefaultClickHandler && !onItemClick) { + return undefined; + } + + const handler: TreeListOnItemClick = (payload, e) => { const {list, id} = payload; if (list.state.disabledById[id]) return; - // always activate selected item - list.state.setActiveItemId(id); + if (!disableDefaultClickHandler) { + // always activate selected item + list.state.setActiveItemId(id); + + const isGroup = list.state.expandedById && id in list.state.expandedById; + + if (isGroup && list.state.setExpanded) { + list.state.setExpanded((prvState) => ({ + ...prvState, + [id]: !prvState[id], + })); + } else if (multiple) { + handleMultipleSelection(id); + } else { + handleSingleSelection(id); + } + } - const isGroup = list.state.expandedById && id in list.state.expandedById; + onItemClick?.(payload, e); + }; - if (isGroup && list.state.setExpanded) { - list.state.setExpanded((prvState) => ({ - ...prvState, - [id]: !prvState[id], - })); - } else if (multiple) { - handleMultipleSelection(id); - } else { - handleSingleSelection(id); - } - }, - [multiple, handleMultipleSelection, handleSingleSelection], - ); + return handler; + }, [ + multiple, + disableDefaultClickHandler, + onItemClick, + handleMultipleSelection, + handleSingleSelection, + ]); // restoring focus when popup opens React.useLayoutEffect(() => { @@ -219,8 +234,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( multiple={multiple} id={`list-${treeSelectId}`} containerRef={containerRef} - onItemClick={typeof onItemClick === 'undefined' ? handleItemClick : onItemClick} - onItemAction={onItemAction} + onItemClick={handleItemClick} renderContainer={renderContainer} mapItemDataToProps={mapItemDataToProps} renderItem={renderItem ?? defaultItemRenderer} diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index 5df6938ef4..b3d98bc361 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -52,7 +52,7 @@ const DefaultTemplate: StoryFn< {...props} items={items} mapItemDataToProps={(x) => x} - onItemAction={(id) => { + onItemClick={(id) => { console.log('clicked on item with id: ', id); }} /> diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index fb6f4f30af..cf13db57c1 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -75,7 +75,7 @@ export const InfinityScrollExample = ({ value={value} mapItemDataToProps={identity} items={items} - onItemAction={handleGroupItemClick} + onItemClick={handleGroupItemClick} renderItem={({data, props, context: {isLastItem, childrenIds}}) => { const node = ( Date: Thu, 20 Jun 2024 19:54:35 +0300 Subject: [PATCH 10/15] fix(useList): mexState -> controlledState --- src/components/TreeSelect/TreeSelect.tsx | 2 +- src/components/useList/__stories__/docs/use-list.md | 4 ++-- src/components/useList/hooks/useList.ts | 12 +++++------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index d0c76afec1..3433d8ea38 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -80,7 +80,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( }); const list = useList({ - mixState: { + controlledState: { selectedById: selected, }, items, diff --git a/src/components/useList/__stories__/docs/use-list.md b/src/components/useList/__stories__/docs/use-list.md index b2555ea98a..e02ec7d0c7 100644 --- a/src/components/useList/__stories__/docs/use-list.md +++ b/src/components/useList/__stories__/docs/use-list.md @@ -11,7 +11,7 @@ The main hook to use what provide you normalized representation of list items (` | defaultExpandedState | Default state for nodes with children items if `withExpandedState` is true | `expanded`, `closed` | `expanded` | | withExpandedState | Is nodes with children's needed to be controlled | `boolean` | `true` | | initialState | Initial state values | `Partial` | | -| mixState | Way to override state by some controlled values. | `Partial` | | +| controlledState | Way to override state by some controlled values. | `Partial` | | #### Result (UseList): @@ -129,7 +129,7 @@ const [selectedById] = React.useState>({}); const list = useList({ // outer controlled state - mixState: { + controlledState: { selectedById, }, }); diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index 95e0c1cdcb..6438b94cd3 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -10,7 +10,7 @@ import {useListState} from './useListState'; import type {UseListStateProps} from './useListState'; interface UseListProps extends UseListParsedStateProps, UseListStateProps { - mixState?: Partial; + controlledState?: Partial; } /** @@ -22,7 +22,7 @@ export const useList = ({ defaultExpandedState = 'expanded', withExpandedState = true, initialState: initialValues, - mixState, + controlledState, }: UseListProps): UseList => { const {itemsById, groupsState, itemsState, initialState} = useListParsedState({ items, @@ -60,17 +60,15 @@ export const useList = ({ }); const realState = React.useMemo(() => { - if (mixState) { + if (controlledState) { return { ...innerState, - expandedById: {...innerState.expandedById, ...mixState?.expandedById}, - selectedById: {...innerState.selectedById, ...mixState?.selectedById}, - disabledById: {...innerState.disabledById, ...mixState?.disabledById}, + ...controlledState, }; } return innerState; - }, [mixState, innerState]); + }, [controlledState, innerState]); return { state: realState, From 6009d1ed756d8afbe789e78ae3c80719e63fdc3a Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 20:15:38 +0300 Subject: [PATCH 11/15] fix(TreeList): rename prop --- src/components/TreeList/TreeList.tsx | 8 +++--- .../TreeList/__stories__/TreeListDocs.md | 26 +++++++++---------- .../__stories__/stories/DefaultStory.tsx | 4 +-- src/components/TreeList/types.ts | 2 +- src/components/TreeSelect/TreeSelect.tsx | 8 +++--- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index 80fd5fb7b3..569d23ccc2 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -28,7 +28,7 @@ export const TreeList = ({ renderItem: propsRenderItem, renderContainer = ListContainer, onItemClick: propsOnItemClick, - disableDefaultClickHandler, + disableDefaultItemClickBehavior, mapItemDataToProps, }: TreeListProps) => { const uniqId = useUniqId(); @@ -37,14 +37,14 @@ export const TreeList = ({ const containerRef = propsContainerRef ?? containerRefLocal; const onItemClick = React.useMemo(() => { - if (disableDefaultClickHandler && !propsOnItemClick) { + if (disableDefaultItemClickBehavior && !propsOnItemClick) { return undefined; } const handler: ListOnItemClick = (arg, e) => { const payload = {id: arg.id, list}; - if (!disableDefaultClickHandler) { + if (!disableDefaultItemClickBehavior) { const baseOnClick = getListItemClickHandler({list, multiple}); baseOnClick(payload, e); @@ -54,7 +54,7 @@ export const TreeList = ({ }; return handler; - }, [disableDefaultClickHandler, propsOnItemClick, list, multiple]); + }, [disableDefaultItemClickBehavior, propsOnItemClick, list, multiple]); useListKeydown({ containerRef, diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md index 8cc36e256b..668e32624a 100644 --- a/src/components/TreeList/__stories__/TreeListDocs.md +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -64,19 +64,19 @@ const Component = () => { ## Props: -| Name | Description | Type | Default | -| :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------: | :-----: | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| qa | Selector for tests | `string` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | -| multiple | One or multiple elements selected list | `boolean` | `false` | -| id | id attribute | `string` | | -| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | -| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | -| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}, e: React.SyntheticEvent) => void \| null` | | -| disableDefaultClickHandler | Disable default `onItemClick` handler logic | `TreeListOnItemClick \| null` | | +| Name | Description | Type | Default | +| :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}, e: React.SyntheticEvent) => void \| null` | | +| disableDefaultItemClickBehavior | Disable default `onItemClick` handler logic | `TreeListOnItemClick \| null` | | ### TreeListRenderItem props: diff --git a/src/components/TreeList/__stories__/stories/DefaultStory.tsx b/src/components/TreeList/__stories__/stories/DefaultStory.tsx index 5a9dc705a4..eddd6f31a8 100644 --- a/src/components/TreeList/__stories__/stories/DefaultStory.tsx +++ b/src/components/TreeList/__stories__/stories/DefaultStory.tsx @@ -35,7 +35,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { list={listWithGroups} onItemClick={null} mapItemDataToProps={identity} - disableDefaultClickHandler + disableDefaultItemClickBehavior /> @@ -48,7 +48,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { list={listWithNoGroups} onItemClick={null} mapItemDataToProps={identity} - disableDefaultClickHandler + disableDefaultItemClickBehavior /> diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 3e4b7eddf1..575c3adbd5 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -59,6 +59,6 @@ export interface TreeListProps extends QAProps { /** * Disable default `onItemClick` handler logic */ - disableDefaultClickHandler?: boolean; + disableDefaultItemClickBehavior?: boolean; mapItemDataToProps: TreeListMapItemDataToProps; } diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 3433d8ea38..d292410f3a 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -47,7 +47,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( defaultValue, withExpandedState = true, defaultExpandedState = 'expanded', - disableDefaultClickHandler, + disableDefaultItemClickBehavior, onClose, onOpenChange, onUpdate, @@ -103,7 +103,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( }); const handleItemClick = React.useMemo(() => { - if (disableDefaultClickHandler && !onItemClick) { + if (disableDefaultItemClickBehavior && !onItemClick) { return undefined; } @@ -112,7 +112,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( if (list.state.disabledById[id]) return; - if (!disableDefaultClickHandler) { + if (!disableDefaultItemClickBehavior) { // always activate selected item list.state.setActiveItemId(id); @@ -136,7 +136,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( return handler; }, [ multiple, - disableDefaultClickHandler, + disableDefaultItemClickBehavior, onItemClick, handleMultipleSelection, handleSingleSelection, From 392ffd6a18d8972ea198a400ed8d8a9730ee2f0a Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Thu, 20 Jun 2024 20:29:42 +0300 Subject: [PATCH 12/15] fix(useList): rename UserListResult type --- .../TreeList/__stories__/TreeListDocs.md | 30 +++++++++---------- src/components/TreeList/types.ts | 8 ++--- ...pSelectionControlledStateAndCustomIcon.tsx | 4 +-- .../WithItemLinksAndActionsExample.tsx | 4 +-- src/components/TreeSelect/types.ts | 6 ++-- .../__stories__/docs/get-item-render-state.md | 2 +- .../docs/get-list-item-click-handler.md | 8 ++--- .../__stories__/docs/use-list-keydown.md | 2 +- .../useList/__stories__/docs/use-list.md | 2 +- .../ListContainer/ListContainer.tsx | 4 +-- .../ListRecursiveRenderer.tsx | 4 +-- src/components/useList/hooks/useList.ts | 4 +-- .../useList/hooks/useListKeydown.tsx | 4 +-- src/components/useList/types.ts | 2 +- .../useList/utils/getItemRenderState.tsx | 4 +-- .../useList/utils/getListItemClickHandler.ts | 4 +-- src/unstable.ts | 2 +- 17 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md index 668e32624a..dc47dc8d33 100644 --- a/src/components/TreeList/__stories__/TreeListDocs.md +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -64,19 +64,19 @@ const Component = () => { ## Props: -| Name | Description | Type | Default | -| :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------: | :-----: | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| qa | Selector for tests | `string` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | -| multiple | One or multiple elements selected list | `boolean` | `false` | -| id | id attribute | `string` | | -| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | -| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | -| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseList}, e: React.SyntheticEvent) => void \| null` | | -| disableDefaultItemClickBehavior | Disable default `onItemClick` handler logic | `TreeListOnItemClick \| null` | | +| Name | Description | Type | Default | +| :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseListResult}, e: React.SyntheticEvent) => void \| null` | | +| disableDefaultItemClickBehavior | Disable default `onItemClick` handler logic | `TreeListOnItemClick \| null` | | ### TreeListRenderItem props: @@ -85,7 +85,7 @@ const Component = () => { | data | List item data | `T` | | | props | Prepared `ListItemView` [props](/docs/lab-uselist--docs#listitemview) | `ListItemViewProps` | | context | List item context [props](/docs/lab-uselist--docs#listitemlistcontextprops) | `ListItemListContextProps` | | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseList` | | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | | index | Index order in flatted visible id's | `number` | | | renderContainerProps | Data from container rendered context if needed | `P` | `undefined` | @@ -94,7 +94,7 @@ const Component = () => { | Name | Description | Type | Default | | :----------- | :-------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------: | :-----: | | id | Id attribute | `string` | | -| list | result of `useList` hook | `UseList` | | +| list | result of `useList` hook | `UseListResult` | | | size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | | className | Class name to mix with | `string` | | | containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 575c3adbd5..6819d8f604 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -8,7 +8,7 @@ import type { ListItemListContextProps, ListItemSize, RenderItemProps, - UseList, + UseListResult, } from '../useList'; export type TreeListRenderItem = (props: { @@ -17,7 +17,7 @@ export type TreeListRenderItem = (props: { props: RenderItemProps; // internal list context props context: ListItemListContextProps; - list: UseList; + list: UseListResult; index: number; renderContainerProps?: P; }) => React.JSX.Element; @@ -30,7 +30,7 @@ export type TreeListRenderContainer = (props: TreeListContainerProps) => R export type TreeListMapItemDataToProps = (item: T) => ListItemCommonProps; -export type TreeListOnItemClickPayload = {id: ListItemId; list: UseList}; +export type TreeListOnItemClickPayload = {id: ListItemId; list: UseListResult}; export type TreeListOnItemClick = ( payload: TreeListOnItemClickPayload, @@ -42,7 +42,7 @@ export interface TreeListProps extends QAProps { * Control outside list container dom element. For example for keyboard */ containerRef?: React.RefObject; - list: UseList; + list: UseListResult; id?: string | undefined; className?: string; multiple?: boolean; diff --git a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx index 4f9ed0d817..7ceeaa016e 100644 --- a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx +++ b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx @@ -6,7 +6,7 @@ import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex, spacing} from '../../../layout'; import {ListItemView} from '../../../useList'; -import type {ListItemCommonProps, ListItemId, UseList} from '../../../useList'; +import type {ListItemCommonProps, ListItemId, UseListResult} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../../TreeSelect'; import type {TreeSelectProps} from '../../types'; @@ -39,7 +39,7 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ [itemsCount], ); - const onItemClick = (id: ListItemId, list: UseList<{a: string}>) => { + const onItemClick = (id: ListItemId, list: UseListResult<{a: string}>) => { if (list.state.disabledById[id]) return; list.state.setSelected((prevState) => ({ diff --git a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx index 7740e57923..9cc31aa250 100644 --- a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx @@ -7,7 +7,7 @@ import {DropdownMenu} from '../../../DropdownMenu'; import {Icon} from '../../../Icon'; import {Flex} from '../../../layout'; import {ListItemView} from '../../../useList'; -import type {ListItemId, UseList} from '../../../useList'; +import type {ListItemId, UseListResult} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../../TreeSelect'; import type {TreeSelectProps} from '../../types'; @@ -25,7 +25,7 @@ export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExa const [open, setOpen] = React.useState(true); const items = React.useMemo(() => createRandomizedData({num: 10, depth: 1}), []); - const onItemClick = (id: ListItemId, list: UseList<{title: string}>) => { + const onItemClick = (id: ListItemId, list: UseListResult<{title: string}>) => { if (list.state.disabledById[id]) return; list.state.setSelected((prevState) => ({ diff --git a/src/components/TreeSelect/types.ts b/src/components/TreeSelect/types.ts index 5cfa7269cc..ce83d643e5 100644 --- a/src/components/TreeSelect/types.ts +++ b/src/components/TreeSelect/types.ts @@ -9,11 +9,11 @@ import type { TreeListRenderContainer, TreeListRenderItem, } from '../TreeList/types'; -import type {ListItemId, ListItemSize, UseList} from '../useList'; +import type {ListItemId, ListItemSize, UseListResult} from '../useList'; import type {UseListParsedStateProps} from '../useList/hooks/useListParsedState'; export type TreeSelectRenderControlProps = { - list: UseList; + list: UseListResult; open: boolean; toggleOpen(): void; clearValue(): void; @@ -61,7 +61,7 @@ export interface TreeSelectProps * In other situations use `renderContainer` method */ slotAfterListBody?: React.ReactNode; - onUpdate?(value: ListItemId[], list: UseList): void; + onUpdate?(value: ListItemId[], list: UseListResult): void; /** * Ability to override custom toggler button */ diff --git a/src/components/useList/__stories__/docs/get-item-render-state.md b/src/components/useList/__stories__/docs/get-item-render-state.md index a6c6acef5b..da4a87325c 100644 --- a/src/components/useList/__stories__/docs/get-item-render-state.md +++ b/src/components/useList/__stories__/docs/get-item-render-state.md @@ -31,7 +31,7 @@ return ; | Name | Description | Type | Default | | :----------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | | id | `id` of list item | `ListItemId` | | -| list | result of `useList` hook | `UseList` | | +| list | result of `useList` hook | `UseListResult` | | | multiple | One or multiple elements selected list | `boolean` | | | onItemClick | Optional on click handler | `(payload :{id: ListItemId}, e: React.SyntheticEvent) => void` | | | size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | diff --git a/src/components/useList/__stories__/docs/get-list-item-click-handler.md b/src/components/useList/__stories__/docs/get-list-item-click-handler.md index 4eaa8edea9..5283516a4a 100644 --- a/src/components/useList/__stories__/docs/get-list-item-click-handler.md +++ b/src/components/useList/__stories__/docs/get-list-item-click-handler.md @@ -8,10 +8,10 @@ import {unstable_getListItemClickHandler as getListItemClickHandler} from '@grav #### props: -| Name | Description | Type | Default | -| :------- | :------------------------------------- | :-------: | :-----: | -| list | result of `useList` hook | `UseList` | | -| multiple | One or multiple elements selected list | `boolean` | | +| Name | Description | Type | Default | +| :------- | :------------------------------------- | :-------------: | :-----: | +| list | result of `useList` hook | `UseListResult` | | +| multiple | One or multiple elements selected list | `boolean` | | #### Result: diff --git a/src/components/useList/__stories__/docs/use-list-keydown.md b/src/components/useList/__stories__/docs/use-list-keydown.md index b8fb8331ce..6dfe88f7ef 100644 --- a/src/components/useList/__stories__/docs/use-list-keydown.md +++ b/src/components/useList/__stories__/docs/use-list-keydown.md @@ -6,7 +6,7 @@ Keyboard support | Name | Description | Type | Default | | :----------- | :-------------------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | -| list | result of `useList` hook | `UseList` | | +| list | result of `useList` hook | `UseListResult` | | | onItemClick | callback will be called when pressing the `Enter`, `Space` keys; | `(payload: {id: ListItemId}, e: React.SyntheticEvent) => void` | | | containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | | enabled | on/off keyboard support. Use it if you need to change the behavior in runtime; | `boolean` | | diff --git a/src/components/useList/__stories__/docs/use-list.md b/src/components/useList/__stories__/docs/use-list.md index e02ec7d0c7..3b2bcdf5eb 100644 --- a/src/components/useList/__stories__/docs/use-list.md +++ b/src/components/useList/__stories__/docs/use-list.md @@ -13,7 +13,7 @@ The main hook to use what provide you normalized representation of list items (` | initialState | Initial state values | `Partial` | | | controlledState | Way to override state by some controlled values. | `Partial` | | -#### Result (UseList): +#### Result (UseListResult): | Name | Description | Type | | :-------- | :----------------------------------------------------------------------------------- | :-------------: | diff --git a/src/components/useList/components/ListContainer/ListContainer.tsx b/src/components/useList/components/ListContainer/ListContainer.tsx index 9403d2cf95..4f7cde3368 100644 --- a/src/components/useList/components/ListContainer/ListContainer.tsx +++ b/src/components/useList/components/ListContainer/ListContainer.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import type {ListItemId, UseList} from '../../types'; +import type {ListItemId, UseListResult} from '../../types'; import {ListContainerView} from '../ListContainerView'; import type {ListContainerViewProps} from '../ListContainerView/ListContainerView'; import {ListItemRecursiveRenderer} from '../ListRecursiveRenderer/ListRecursiveRenderer'; export type ListContainerProps = Omit & { - list: UseList; + list: UseListResult; containerRef?: React.RefObject; renderItem( id: ListItemId, diff --git a/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx b/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx index b2c92f14f8..a7339fc520 100644 --- a/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx +++ b/src/components/useList/components/ListRecursiveRenderer/ListRecursiveRenderer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {block} from '../../../utils/cn'; -import type {ListItemId, ListItemType, UseList} from '../../types'; +import type {ListItemId, ListItemType, UseListResult} from '../../types'; import {isTreeItemGuard} from '../../utils/isTreeItemGuard'; import './ListRecursiveRenderer.scss'; @@ -10,7 +10,7 @@ const b = block('list-recursive-renderer'); export interface ListItemRecursiveRendererProps { id: ListItemId; - list: UseList; + list: UseListResult; itemSchema: ListItemType; children(id: ListItemId, index: number): React.JSX.Element; className?: string; diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index 6438b94cd3..2e16445e10 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -1,7 +1,7 @@ /* eslint-disable valid-jsdoc */ import React from 'react'; -import type {InitialListParsedState, UseList} from '../types'; +import type {InitialListParsedState, UseListResult} from '../types'; import {useFlattenListItems} from './useFlattenListItems'; import {useListParsedState} from './useListParsedState'; @@ -23,7 +23,7 @@ export const useList = ({ withExpandedState = true, initialState: initialValues, controlledState, -}: UseListProps): UseList => { +}: UseListProps): UseListResult => { const {itemsById, groupsState, itemsState, initialState} = useListParsedState({ items, getItemId, diff --git a/src/components/useList/hooks/useListKeydown.tsx b/src/components/useList/hooks/useListKeydown.tsx index 780897cae4..913b0fbc27 100644 --- a/src/components/useList/hooks/useListKeydown.tsx +++ b/src/components/useList/hooks/useListKeydown.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {KeyCode} from '../../../constants'; -import type {ListOnItemClick, UseList} from '../types'; +import type {ListOnItemClick, UseListResult} from '../types'; import {findNextIndex} from '../utils/findNextIndex'; import {scrollToListItem} from '../utils/scrollToListItem'; @@ -9,7 +9,7 @@ interface UseListKeydownProps { onItemClick?: ListOnItemClick; containerRef?: React.RefObject; enabled?: boolean; - list: UseList; + list: UseListResult; } // Use this hook if you need keyboard support for tree structure lists diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index b8174d6ea7..998ade5588 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -123,7 +123,7 @@ type ListStructure = ParsedState & items: ListItemType[]; }; -export type UseList = { +export type UseListResult = { state: ListState; structure: ListStructure; }; diff --git a/src/components/useList/utils/getItemRenderState.tsx b/src/components/useList/utils/getItemRenderState.tsx index 557bf72aca..c66cc05fb6 100644 --- a/src/components/useList/utils/getItemRenderState.tsx +++ b/src/components/useList/utils/getItemRenderState.tsx @@ -7,7 +7,7 @@ import type { ListItemSize, ListOnItemClick, RenderItemProps, - UseList, + UseListResult, } from '../types'; import {getListItemQa} from './getListItemQa'; @@ -21,7 +21,7 @@ type ItemRendererProps = QAProps & { id: ListItemId; mapItemDataToProps(data: T): ListItemCommonProps; onItemClick?: ListOnItemClick; - list: UseList; + list: UseListResult; }; /** diff --git a/src/components/useList/utils/getListItemClickHandler.ts b/src/components/useList/utils/getListItemClickHandler.ts index db2f359a03..b311495ac4 100644 --- a/src/components/useList/utils/getListItemClickHandler.ts +++ b/src/components/useList/utils/getListItemClickHandler.ts @@ -1,8 +1,8 @@ -import type {ListOnItemClick, UseList} from '../types'; +import type {ListOnItemClick, UseListResult} from '../types'; interface GetListItemClickHandlerProps { multiple?: boolean; - list: UseList; + list: UseListResult; } export const getListItemClickHandler = ({ diff --git a/src/unstable.ts b/src/unstable.ts index 36c1c8fe2b..92a4858717 100644 --- a/src/unstable.ts +++ b/src/unstable.ts @@ -12,7 +12,7 @@ export { type ListContainerViewProps as unstable_ListContainerViewProps, type ListItemType as unstable_ListItemType, type ListItemId as unstable_ListItemId, - type UseList as unstable_UseList, + type UseListResult as unstable_UseListResult, getItemRenderState as unstable_getItemRenderState, scrollToListItem as unstable_scrollToListItem, getListItemQa as unstable_getListItemQa, From b7027c87d0aeac1907203e68f79aa8c79134f559 Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Fri, 21 Jun 2024 12:40:17 +0300 Subject: [PATCH 13/15] fix(TreeSelect): fix root div styles --- src/components/TreeSelect/TreeSelect.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/TreeSelect/TreeSelect.scss b/src/components/TreeSelect/TreeSelect.scss index c95b21bcc9..42f970e0c1 100644 --- a/src/components/TreeSelect/TreeSelect.scss +++ b/src/components/TreeSelect/TreeSelect.scss @@ -3,9 +3,7 @@ $block: '.#{variables.$ns}tree-select'; #{$block} { - display: flex; - flex-direction: column; - gap: var(--g-spacing-5); + display: inline-block; max-width: 100%; From 082a3c8d69e83ebe08499f46324d76b7f78ab5a9 Mon Sep 17 00:00:00 2001 From: Alexandr Isaev Date: Fri, 21 Jun 2024 16:57:12 +0300 Subject: [PATCH 14/15] fix(TreeList): fix examples after adding disableDefaultOnItemClick handler prop --- .../WithGroupSelectionAndCustomIconStory.tsx | 1 + .../stories/WithItemLinksAndActionsStory.tsx | 1 + src/components/TreeList/types.ts | 3 ++- src/components/TreeSelect/TreeSelect.tsx | 1 + ...pSelectionControlledStateAndCustomIcon.tsx | 22 ++++++++++--------- .../WithItemLinksAndActionsExample.tsx | 8 +++---- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx index 5856959a36..a7f4ca8b0a 100644 --- a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx @@ -63,6 +63,7 @@ export const WithGroupSelectionAndCustomIconStory = ({ size="l" mapItemDataToProps={mapCustomDataStructureToKnownProps} onItemClick={onItemClick} + disableDefaultItemClickBehavior renderItem={({ data, props: { diff --git a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx index 261371afa9..37507d1d72 100644 --- a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx @@ -45,6 +45,7 @@ export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStory list={list} mapItemDataToProps={identity} onItemClick={onItemClick} + disableDefaultItemClickBehavior size="l" renderItem={({ data, diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 6819d8f604..605e74c71c 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -53,7 +53,8 @@ export interface TreeListProps extends QAProps { renderItem?: TreeListRenderItem; renderContainer?: TreeListRenderContainer; /** - * `null` value - if for some reason you don't need default click item behavior + * On click will be always appears default onClick logic. + * If you don't need this behavior use `disableDefaultItemClickBehavior` prop */ onItemClick?: null | TreeListOnItemClick; /** diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index d292410f3a..d3b81df8dd 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -235,6 +235,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( id={`list-${treeSelectId}`} containerRef={containerRef} onItemClick={handleItemClick} + disableDefaultItemClickBehavior renderContainer={renderContainer} mapItemDataToProps={mapItemDataToProps} renderItem={renderItem ?? defaultItemRenderer} diff --git a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx index 7ceeaa016e..61da137570 100644 --- a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx +++ b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx @@ -34,18 +34,17 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ itemsCount = 5, ...props }: WithGroupSelectionControlledStateAndCustomIconExampleProps) => { + const [value, setValue] = React.useState([]); + const items = React.useMemo( () => createRandomizedData({num: itemsCount, getData: (a) => ({a})}), [itemsCount], ); - const onItemClick = (id: ListItemId, list: UseListResult<{a: string}>) => { + const onItemClick = ({id, list}: {id: ListItemId; list: UseListResult<{a: string}>}) => { if (list.state.disabledById[id]) return; - list.state.setSelected((prevState) => ({ - ...(props.multiple ? prevState : {}), - [id]: !prevState[id], - })); + setValue([id]); list.state.setActiveItemId(id); }; @@ -55,25 +54,27 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ { // groups items are selectable too - state.hasSelectionIcon = Boolean(props.multiple); + renderProps.hasSelectionIcon = Boolean(props.multiple); return ( onItemClick(state.id, list)} startSlot={ } @@ -86,7 +87,8 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ e.stopPropagation(); list.state.setExpanded?.((prevExpandedState) => ({ ...prevExpandedState, - [state.id]: !prevExpandedState[state.id], + [renderProps.id]: + !prevExpandedState[renderProps.id], })); }} > diff --git a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx index 9cc31aa250..8a88abab74 100644 --- a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx @@ -22,16 +22,14 @@ export interface WithItemLinksAndActionsExampleProps > {} export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExampleProps) => { + const [value, setValue] = React.useState([]); const [open, setOpen] = React.useState(true); const items = React.useMemo(() => createRandomizedData({num: 10, depth: 1}), []); const onItemClick = (id: ListItemId, list: UseListResult<{title: string}>) => { if (list.state.disabledById[id]) return; - list.state.setSelected((prevState) => ({ - ...(props.multiple ? prevState : {}), - [id]: !prevState[id], - })); + setValue([id]); list.state.setActiveItemId(id); @@ -42,9 +40,11 @@ export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExa Date: Fri, 21 Jun 2024 18:44:07 +0300 Subject: [PATCH 15/15] fix(TreeList): remove redundant onClick props --- src/components/TreeList/TreeList.tsx | 11 +-- .../TreeList/__stories__/TreeListDocs.md | 25 +++-- .../__stories__/stories/DefaultStory.tsx | 2 - .../stories/WithDisabledElementsStory.tsx | 3 +- .../WithGroupSelectionAndCustomIconStory.tsx | 11 ++- .../stories/WithItemLinksAndActionsStory.tsx | 1 - src/components/TreeList/types.ts | 7 +- src/components/TreeSelect/TreeSelect.tsx | 76 ++++++-------- .../__stories__/TreeSelect.stories.tsx | 4 +- .../components/InfinityScrollExample.tsx | 4 +- ...pSelectionControlledStateAndCustomIcon.tsx | 12 ++- .../WithItemLinksAndActionsExample.tsx | 1 - .../TreeSelect/hooks/useControlledValue.ts | 60 +++++++++++ .../hooks/useTreeSelectSelection.ts | 99 ------------------- src/components/TreeSelect/types.ts | 2 +- src/components/useList/hooks/useList.ts | 4 +- src/components/useList/types.ts | 2 +- 17 files changed, 134 insertions(+), 190 deletions(-) create mode 100644 src/components/TreeSelect/hooks/useControlledValue.ts delete mode 100644 src/components/TreeSelect/hooks/useTreeSelectSelection.ts diff --git a/src/components/TreeList/TreeList.tsx b/src/components/TreeList/TreeList.tsx index 569d23ccc2..a31acea0e9 100644 --- a/src/components/TreeList/TreeList.tsx +++ b/src/components/TreeList/TreeList.tsx @@ -28,7 +28,6 @@ export const TreeList = ({ renderItem: propsRenderItem, renderContainer = ListContainer, onItemClick: propsOnItemClick, - disableDefaultItemClickBehavior, mapItemDataToProps, }: TreeListProps) => { const uniqId = useUniqId(); @@ -37,24 +36,24 @@ export const TreeList = ({ const containerRef = propsContainerRef ?? containerRefLocal; const onItemClick = React.useMemo(() => { - if (disableDefaultItemClickBehavior && !propsOnItemClick) { + if (propsOnItemClick === null) { return undefined; } const handler: ListOnItemClick = (arg, e) => { const payload = {id: arg.id, list}; - if (!disableDefaultItemClickBehavior) { + if (propsOnItemClick) { + propsOnItemClick?.(payload, e); + } else { const baseOnClick = getListItemClickHandler({list, multiple}); baseOnClick(payload, e); } - - propsOnItemClick?.(payload, e); }; return handler; - }, [disableDefaultItemClickBehavior, propsOnItemClick, list, multiple]); + }, [propsOnItemClick, list, multiple]); useListKeydown({ containerRef, diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md index dc47dc8d33..1655bfbe67 100644 --- a/src/components/TreeList/__stories__/TreeListDocs.md +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -64,19 +64,18 @@ const Component = () => { ## Props: -| Name | Description | Type | Default | -| :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------: | :-----: | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| qa | Selector for tests | `string` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | -| multiple | One or multiple elements selected list | `boolean` | `false` | -| id | id attribute | `string` | | -| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | -| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | -| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseListResult}, e: React.SyntheticEvent) => void \| null` | | -| disableDefaultItemClickBehavior | Disable default `onItemClick` handler logic | `TreeListOnItemClick \| null` | | +| Name | Description | Type | Default | +| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseListResult}, e: React.SyntheticEvent) => void \| null` | | ### TreeListRenderItem props: diff --git a/src/components/TreeList/__stories__/stories/DefaultStory.tsx b/src/components/TreeList/__stories__/stories/DefaultStory.tsx index eddd6f31a8..e3a63f60e9 100644 --- a/src/components/TreeList/__stories__/stories/DefaultStory.tsx +++ b/src/components/TreeList/__stories__/stories/DefaultStory.tsx @@ -35,7 +35,6 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { list={listWithGroups} onItemClick={null} mapItemDataToProps={identity} - disableDefaultItemClickBehavior /> @@ -48,7 +47,6 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { list={listWithNoGroups} onItemClick={null} mapItemDataToProps={identity} - disableDefaultItemClickBehavior /> diff --git a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx index 7c3afb4508..d79539aaa9 100644 --- a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Button} from '../../../Button'; import {Flex} from '../../../layout'; -import {useList} from '../../../useList'; +import {getListItemClickHandler, useList} from '../../../useList'; import type {ListItemType} from '../../../useList'; import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; @@ -52,6 +52,7 @@ export const WithDisabledElementsStory = ({...storyProps}: WithDisabledElementsS containerRef={containerRef} mapItemDataToProps={({text}) => ({title: text})} onItemClick={({id}) => { + getListItemClickHandler({list})({id}); alert( `Clicked by item with id :"${id}" and data: ${JSON.stringify(list.structure.itemsById[id])}`, ); diff --git a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx index a7f4ca8b0a..0483db50b0 100644 --- a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx @@ -63,18 +63,20 @@ export const WithGroupSelectionAndCustomIconStory = ({ size="l" mapItemDataToProps={mapCustomDataStructureToKnownProps} onItemClick={onItemClick} - disableDefaultItemClickBehavior renderItem={({ data, props: { expanded, // don't use default ListItemView expand icon - ...state + ...preparedProps }, context: {childrenIds}, }) => { + // has no group + preparedProps.hasSelectionIcon = Boolean(props.multiple); + return ( @@ -88,7 +90,8 @@ export const WithGroupSelectionAndCustomIconStory = ({ e.stopPropagation(); list.state.setExpanded?.((prevExpandedState) => ({ ...prevExpandedState, - [state.id]: !prevExpandedState[state.id], + [preparedProps.id]: + !prevExpandedState[preparedProps.id], })); }} extraProps={{ diff --git a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx index 37507d1d72..261371afa9 100644 --- a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx @@ -45,7 +45,6 @@ export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStory list={list} mapItemDataToProps={identity} onItemClick={onItemClick} - disableDefaultItemClickBehavior size="l" renderItem={({ data, diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 605e74c71c..466bcdec5f 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -53,13 +53,8 @@ export interface TreeListProps extends QAProps { renderItem?: TreeListRenderItem; renderContainer?: TreeListRenderContainer; /** - * On click will be always appears default onClick logic. - * If you don't need this behavior use `disableDefaultItemClickBehavior` prop + * `null` - disable default click handler */ onItemClick?: null | TreeListOnItemClick; - /** - * Disable default `onItemClick` handler logic - */ - disableDefaultItemClickBehavior?: boolean; mapItemDataToProps: TreeListMapItemDataToProps; } diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index d3b81df8dd..db8f814f1b 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -3,16 +3,18 @@ import React from 'react'; import {useFocusWithin, useForkRef, useUniqId} from '../../hooks'; +import {useOpenState} from '../../hooks/useSelect/useOpenState'; import {SelectControl} from '../Select/components'; import {SelectPopup} from '../Select/components/SelectPopup/SelectPopup'; import {TreeList} from '../TreeList'; -import type {TreeListOnItemClick, TreeListRenderItem} from '../TreeList/types'; +import type {TreeListRenderItem} from '../TreeList/types'; import {useMobile} from '../mobile'; -import {ListItemView, useList} from '../useList'; +import {ListItemView, getListItemClickHandler, useList} from '../useList'; +import type {ListOnItemClick} from '../useList'; import {block} from '../utils/cn'; import type {CnMods} from '../utils/cn'; -import {useTreeSelectSelection, useValue} from './hooks/useTreeSelectSelection'; +import {useControlledValue} from './hooks/useControlledValue'; import type {TreeSelectProps, TreeSelectRenderControlProps} from './types'; import './TreeSelect.scss'; @@ -47,7 +49,6 @@ export const TreeSelect = React.forwardRef(function TreeSelect( defaultValue, withExpandedState = true, defaultExpandedState = 'expanded', - disableDefaultItemClickBehavior, onClose, onOpenChange, onUpdate, @@ -74,14 +75,23 @@ export const TreeSelect = React.forwardRef(function TreeSelect( const handleControlRef = useForkRef(ref, controlRef); - const {value, setInnerValue, selected} = useValue({ + const {toggleOpen, open} = useOpenState({ + defaultOpen, + onClose, + onOpenChange, + open: propsOpen, + }); + + const {value, selectedById, setSelected} = useControlledValue({ value: propsValue, defaultValue, + onUpdate, }); const list = useList({ controlledState: { - selectedById: selected, + selectedById, + setSelected, }, items, getItemId, @@ -89,58 +99,31 @@ export const TreeSelect = React.forwardRef(function TreeSelect( withExpandedState, }); - const {open, toggleOpen, handleClearValue, handleMultipleSelection, handleSingleSelection} = - useTreeSelectSelection({ - value, - setInnerValue, - onUpdate: (ids) => { - onUpdate?.(ids, list); - }, - defaultOpen, - open: propsOpen, - onClose, - onOpenChange, - }); - const handleItemClick = React.useMemo(() => { - if (disableDefaultItemClickBehavior && !onItemClick) { + if (onItemClick === null) { return undefined; } - const handler: TreeListOnItemClick = (payload, e) => { - const {list, id} = payload; + const handler: ListOnItemClick = (arg, e) => { + const payload = {id: arg.id, list}; - if (list.state.disabledById[id]) return; + if (onItemClick) { + onItemClick?.(payload, e); + } else { + const baseOnClick = getListItemClickHandler({list, multiple}); - if (!disableDefaultItemClickBehavior) { - // always activate selected item - list.state.setActiveItemId(id); + baseOnClick(payload, e); - const isGroup = list.state.expandedById && id in list.state.expandedById; + const isGroup = list.state.expandedById && arg.id in list.state.expandedById; - if (isGroup && list.state.setExpanded) { - list.state.setExpanded((prvState) => ({ - ...prvState, - [id]: !prvState[id], - })); - } else if (multiple) { - handleMultipleSelection(id); - } else { - handleSingleSelection(id); + if (!multiple && !isGroup) { + toggleOpen(false); } } - - onItemClick?.(payload, e); }; return handler; - }, [ - multiple, - disableDefaultItemClickBehavior, - onItemClick, - handleMultipleSelection, - handleSingleSelection, - ]); + }, [onItemClick, list, multiple, toggleOpen]); // restoring focus when popup opens React.useLayoutEffect(() => { @@ -170,7 +153,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( list, open, toggleOpen, - clearValue: handleClearValue, + clearValue: () => list.state.setSelected({}), ref: handleControlRef, size, value, @@ -235,7 +218,6 @@ export const TreeSelect = React.forwardRef(function TreeSelect( id={`list-${treeSelectId}`} containerRef={containerRef} onItemClick={handleItemClick} - disableDefaultItemClickBehavior renderContainer={renderContainer} mapItemDataToProps={mapItemDataToProps} renderItem={renderItem ?? defaultItemRenderer} diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index b3d98bc361..7437235538 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type {Meta, StoryFn} from '@storybook/react'; import {Flex} from '../../layout'; +import {getListItemClickHandler} from '../../useList'; import {createRandomizedData} from '../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../TreeSelect'; import type {TreeSelectProps} from '../types'; @@ -52,7 +53,8 @@ const DefaultTemplate: StoryFn< {...props} items={items} mapItemDataToProps={(x) => x} - onItemClick={(id) => { + onItemClick={({id, list}) => { + getListItemClickHandler({list})({id}); console.log('clicked on item with id: ', id); }} /> diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index cf13db57c1..0e868891ff 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -5,7 +5,7 @@ import {Loader} from '../../../Loader'; import {RenderVirtualizedContainer} from '../../../TreeList/__stories__/components/RenderVirtualizedContainer'; import type {TreeListOnItemClick} from '../../../TreeList/types'; import {Flex, sp, spacing} from '../../../layout'; -import {ListItemView} from '../../../useList'; +import {ListItemView, getListItemClickHandler} from '../../../useList'; import type {ListItemId} from '../../../useList'; import {IntersectionContainer} from '../../../useList/__stories__/components/IntersectionContainer/IntersectionContainer'; import {useInfinityFetch} from '../../../useList/__stories__/utils/useInfinityFetch'; @@ -41,6 +41,8 @@ export const InfinityScrollExample = ({ } = useInfinityFetch(itemsCount, true); const handleGroupItemClick: TreeListOnItemClick = ({id, list}) => { + getListItemClickHandler({list})({id}); + // click on group item if (list.state.expandedById && list.state.setExpanded && id in list.state.expandedById) { const treeGroupNextValue = !list.state.expandedById[id]; diff --git a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx index 61da137570..b7229d7b98 100644 --- a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx +++ b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx @@ -34,7 +34,8 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ itemsCount = 5, ...props }: WithGroupSelectionControlledStateAndCustomIconExampleProps) => { - const [value, setValue] = React.useState([]); + // const [value, setValue] = React.useState([]); + const [open, setOpen] = React.useState(true); const items = React.useMemo( () => createRandomizedData({num: itemsCount, getData: (a) => ({a})}), @@ -44,9 +45,11 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ const onItemClick = ({id, list}: {id: ListItemId; list: UseListResult<{a: string}>}) => { if (list.state.disabledById[id]) return; - setValue([id]); + list.state.setSelected({[id]: true}); list.state.setActiveItemId(id); + + setOpen(false); }; return ( @@ -54,11 +57,12 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ + Object.entries(selectedById).reduce((acc, [id, value]) => { + if (value) { + acc.push(id); + } + return acc; + }, []); + +export const useControlledValue = ({ + defaultValue = [], + value: valueProps, + onUpdate, +}: UseControlledValueProps) => { + const [innerValue, setInnerValue] = React.useState(defaultValue); + + const value: string[] = valueProps ?? innerValue; + + const uncontrolled = !valueProps; + + const result = React.useMemo(() => { + const selectedById = value.reduce((acc, val) => { + acc[val] = true; + + return acc; + }, {}); + + const setSelected: ListStateHandler> = (payload) => { + const nextValue = typeof payload === 'function' ? payload(selectedById) : payload; + const preparedValue = prepareParams(nextValue); + + if (uncontrolled) { + setInnerValue(preparedValue); + } else { + onUpdate?.(preparedValue); + } + }; + + return { + value, + selectedById, + setSelected, + /** + * Available only if `uncontrolled` component valiant + */ + setInnerValue: uncontrolled ? setInnerValue : undefined, + }; + }, [onUpdate, uncontrolled, value]); + + return result; +}; diff --git a/src/components/TreeSelect/hooks/useTreeSelectSelection.ts b/src/components/TreeSelect/hooks/useTreeSelectSelection.ts deleted file mode 100644 index cccc08590c..0000000000 --- a/src/components/TreeSelect/hooks/useTreeSelectSelection.ts +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; - -import type {UseOpenProps} from '../../../hooks/useSelect/types'; -import {useOpenState} from '../../../hooks/useSelect/useOpenState'; -import type {ListItemId} from '../../useList/types'; - -type UseValueProps = { - value?: string[]; - defaultValue?: string[]; -}; - -export const useValue = ({defaultValue = [], value: valueProps}: UseValueProps) => { - const [innerValue, setInnerValue] = React.useState(defaultValue); - - const value: string[] = valueProps ?? innerValue; - - const uncontrolled = !valueProps; - - const selected: Record = React.useMemo(() => { - return value.reduce>((acc, val) => { - acc[val] = true; - - return acc; - }, {}); - }, [value]); - - return { - selected, - value, - /** - * Available only if `uncontrolled` component valiant - */ - setInnerValue: uncontrolled ? setInnerValue : undefined, - }; -}; - -type UseTreeSelectSelectionProps = { - value: ListItemId[]; - setInnerValue?(ids: ListItemId[]): void; - onUpdate?: (value: ListItemId[]) => void; -} & UseOpenProps; - -export const useTreeSelectSelection = ({ - value, - setInnerValue, - defaultOpen, - onClose, - onOpenChange, - open: openProps, - onUpdate, -}: UseTreeSelectSelectionProps) => { - const {toggleOpen, open} = useOpenState({ - defaultOpen, - onClose, - onOpenChange, - open: openProps, - }); - - const handleSingleSelection = React.useCallback( - (id: ListItemId) => { - if (!value.includes(id)) { - const nextValue = [id]; - onUpdate?.(nextValue); - - setInnerValue?.(nextValue); - } - - toggleOpen(false); - }, - [value, toggleOpen, onUpdate, setInnerValue], - ); - - const handleMultipleSelection = React.useCallback( - (id: ListItemId) => { - const alreadySelected = value.includes(id); - const nextValue = alreadySelected - ? value.filter((iteratedVal) => iteratedVal !== id) - : [...value, id]; - - onUpdate?.(nextValue); - - setInnerValue?.(nextValue); - }, - [value, onUpdate, setInnerValue], - ); - - const handleClearValue = React.useCallback(() => { - onUpdate?.([]); - setInnerValue?.([]); - }, [onUpdate, setInnerValue]); - - return { - open, - toggleOpen, - handleSingleSelection, - handleMultipleSelection, - handleClearValue, - }; -}; diff --git a/src/components/TreeSelect/types.ts b/src/components/TreeSelect/types.ts index ce83d643e5..23e1833c23 100644 --- a/src/components/TreeSelect/types.ts +++ b/src/components/TreeSelect/types.ts @@ -61,7 +61,7 @@ export interface TreeSelectProps * In other situations use `renderContainer` method */ slotAfterListBody?: React.ReactNode; - onUpdate?(value: ListItemId[], list: UseListResult): void; + onUpdate?(value: ListItemId[]): void; /** * Ability to override custom toggler button */ diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index 2e16445e10..69000f2a74 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -1,7 +1,7 @@ /* eslint-disable valid-jsdoc */ import React from 'react'; -import type {InitialListParsedState, UseListResult} from '../types'; +import type {ListState, UseListResult} from '../types'; import {useFlattenListItems} from './useFlattenListItems'; import {useListParsedState} from './useListParsedState'; @@ -10,7 +10,7 @@ import {useListState} from './useListState'; import type {UseListStateProps} from './useListState'; interface UseListProps extends UseListParsedStateProps, UseListStateProps { - controlledState?: Partial; + controlledState?: Partial; } /** diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 998ade5588..5afa809406 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -84,7 +84,7 @@ export type ParsedState = { type SetStateAction = S | ((prevState: S) => S); -type ListStateHandler = (arg: SetStateAction) => void; +export type ListStateHandler = (arg: SetStateAction) => void; export type ListState = { disabledById: Record;