diff --git a/src/components/TreeSelect/TreeSelect.scss b/src/components/TreeSelect/TreeSelect.scss index ad7cf33af3..35e38f63fc 100644 --- a/src/components/TreeSelect/TreeSelect.scss +++ b/src/components/TreeSelect/TreeSelect.scss @@ -10,6 +10,8 @@ $block: '.#{variables.$ns}tree-select'; } &__popup { + padding: 4px 0; + border-radius: 6px; overflow: hidden; min-width: 300px; } diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index b785583774..802d96c441 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -3,12 +3,10 @@ import React from 'react'; import {useForkRef, useUniqId} from '../../hooks'; import {SelectControl} from '../Select/components'; import {SelectPopup} from '../Select/components/SelectPopup/SelectPopup'; -import {borderRadius} from '../borderRadius'; import {Flex} from '../layout'; import {useMobile} from '../mobile'; import { type ListItemId, - ListItemView, getItemRenderState, isKnownStructureGuard, scrollToListItem, @@ -18,6 +16,7 @@ import { } from '../useList'; import {type CnMods, block} from '../utils/cn'; +import {TreeSelectItem} from './TreeSelectItem'; import {TreeListContainer} from './components/TreeListContainer/TreeListContainer'; import {useTreeSelectSelection, useValue} from './hooks/useTreeSelectSelection'; import type {RenderControlProps, TreeSelectProps} from './types'; @@ -83,17 +82,17 @@ export const TreeSelect = React.forwardRef(function TreeSelect( const listParsedState = useList({ items, - expandedById: listState.expandedById, getId, + ...listState, }); const wrappedOnUpdate = React.useCallback( (ids: ListItemId[]) => onUpdate?.( ids, - ids.map((id) => listParsedState.byId[id]), + ids.map((id) => listParsedState.itemsById[id]), ), - [listParsedState.byId, onUpdate], + [listParsedState.itemsById, onUpdate], ); const {open, toggleOpen, handleClearValue, handleMultipleSelection, handleSingleSelection} = @@ -139,8 +138,8 @@ export const TreeSelect = React.forwardRef(function TreeSelect( id, isGroup: id in listParsedState.groupsState, isLastItem: - listParsedState.flattenIdsOrder[ - listParsedState.flattenIdsOrder.length - 1 + listParsedState.existedFlattenIds[ + listParsedState.existedFlattenIds.length - 1 ] === id, disabled: listState.disabledById[id], }); @@ -152,7 +151,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( onItemClick, listState, listParsedState.groupsState, - listParsedState.flattenIdsOrder, + listParsedState.existedFlattenIds, groupsBehavior, multiple, handleMultipleSelection, @@ -167,7 +166,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( const lastSelectedItemId = value[value.length - 1]; containerRef.current?.focus(); - const firstItemId = listParsedState.flattenIdsOrder[0]; + const firstItemId = listParsedState.existedFlattenIds[0]; listState.setActiveItemId(lastSelectedItemId ?? firstItemId); @@ -207,10 +206,10 @@ export const TreeSelect = React.forwardRef(function TreeSelect( selectedOptionsContent={React.Children.toArray( value.map((id) => { if ('renderControlContent' in props) { - return props.renderControlContent(listParsedState.byId[id]).title; + return props.renderControlContent(listParsedState.itemsById[id]).title; } - const items = listParsedState.byId[id]; + const items = listParsedState.itemsById[id]; if (isKnownStructureGuard(items)) { return items.title; @@ -247,7 +246,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( {togglerNode} ( {...listParsedState} {...listState} renderItem={(id, renderContextProps) => { - const [item, state, context] = getItemRenderState({ + const renderState = getItemRenderState({ id, size, onItemClick: handleItemClick, @@ -273,19 +272,22 @@ export const TreeSelect = React.forwardRef(function TreeSelect( }); // assign components scope logic - state.hasSelectionIcon = context.groupState - ? groupsBehavior === 'selectable' - : undefined; + renderState.props.hasSelectionIcon = Boolean(multiple); if (renderItem) { - return renderItem(item, state, context, renderContextProps); + return renderItem( + renderState.data, + renderState.props, + renderState.context, + renderContextProps, + ); } - const itemData = listParsedState.byId[id]; + const itemData = listParsedState.itemsById[id]; return ( - { + as?: 'div' | 'li'; + itemClassName?: string; +} + +export const TreeSelectItem = React.forwardRef(function TreeSelectItem( + {as = 'div', className, itemClassName, ...props}: TreeSelectItemProps, + ref?: any, +) { + const Tag: React.ElementType = as; + + return ( + + + + ); +}); diff --git a/src/components/TreeSelect/TreeSelectItem/index.ts b/src/components/TreeSelect/TreeSelectItem/index.ts new file mode 100644 index 0000000000..8bde947111 --- /dev/null +++ b/src/components/TreeSelect/TreeSelectItem/index.ts @@ -0,0 +1 @@ +export * from './TreeSelectItem'; diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index ee0de0674e..c705c8248f 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -66,7 +66,6 @@ const WithGroupSelectionControlledStateAndCustomIconTemplate: StoryFn< export const WithGroupSelectionControlledStateAndCustomIcon = WithGroupSelectionControlledStateAndCustomIconTemplate.bind({}); WithGroupSelectionControlledStateAndCustomIcon.args = { - multiple: true, groupsBehavior: 'selectable', }; diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index 0e6a6b3a9e..5cfee9dddd 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -3,10 +3,10 @@ import React from 'react'; import {Label} from '../../../Label'; import {Loader} from '../../../Loader'; import {Flex, spacing} from '../../../layout'; -import {ListItemView} from '../../../useList'; import {IntersectionContainer} from '../../../useList/__stories__/components/IntersectionContainer/IntersectionContainer'; import {useInfinityFetch} from '../../../useList/__stories__/utils/useInfinityFetch'; import {TreeSelect} from '../../TreeSelect'; +import {TreeSelectItem} from '../../TreeSelectItem'; import type {TreeSelectProps} from '../../types'; import {RenderVirtualizedContainer} from './RenderVirtualizedContainer'; @@ -33,10 +33,9 @@ export const InfinityScrollExample = ({itemsCount = 5, ...props}: InfinityScroll {...props} items={data} value={value} - popupClassName={spacing({p: 2})} renderItem={(item, state, {isLastItem, groupState}) => { const node = ( - ({ id, containerRef, - flattenIdsOrder, + existedFlattenIds, renderItem, size, }: RenderContainerProps) => { return ( computeItemSize(size)} > {renderItem} diff --git a/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx b/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx index af57b6da39..71fef24abe 100644 --- a/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx @@ -14,18 +14,19 @@ import { import {Icon} from '../../../Icon'; import {Flex} from '../../../layout'; -import {ListContainerView, ListItemView, ListItemViewProps} from '../../../useList'; +import {ListContainerView} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {reorderArray} from '../../../useList/__stories__/utils/reorderArray'; import {TreeSelect} from '../../TreeSelect'; +import {TreeSelectItem, TreeSelectItemProps} from '../../TreeSelectItem'; import type {TreeSelectProps} from '../../types'; const DraggableListItem = ({ provided, ...props -}: {provided?: DraggableProvided} & ListItemViewProps) => { +}: {provided?: DraggableProvided} & TreeSelectItemProps) => { return ( - { setValue([id]); } }} - renderContainer={({renderItem, flattenIdsOrder, containerRef, id}) => { + renderContainer={({renderItem, existedFlattenIds, containerRef, id}) => { return ( { snapshot: DraggableStateSnapshot, rubric: DraggableRubric, ) => { - return renderItem(flattenIdsOrder[rubric.source.index], { + return renderItem(existedFlattenIds[rubric.source.index], { provided, active: snapshot.isDragging, }); @@ -82,7 +83,7 @@ export const WithDndListExample = (props: WithDndListExampleProps) => { {...droppableProvided.droppableProps} ref={droppableProvided.innerRef} > - {flattenIdsOrder.map((id) => renderItem(id))} + {existedFlattenIds.map((id) => renderItem(id))} {droppableProvided.placeholder} diff --git a/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx b/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx index b4f6abda50..0353d4f837 100644 --- a/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx @@ -56,17 +56,17 @@ export const WithFiltrationAndControlsExample = ({ autoFocus hasClear placeholder="Type for search..." - className={spacing({p: 2})} + className={spacing({px: 2, py: 1})} style={{boxSizing: 'border-box'}} autoComplete="off" value={filterState.filter} - onUpdate={filterState.onChange} + onUpdate={filterState.onFilterUpdate} ref={filterState.filterRef} /> } renderContainer={renderContainer} slotAfterListBody={ - + } placement={['bottom-start', 'bottom-end', 'top-start', 'top-end']} offset={[0, 10]} @@ -108,19 +106,19 @@ export const PopupWithTogglerList = ({size, itemsCount}: PopupWithTogglerListPro expandedById={listState.expandedById} > {(id) => { - const [data, state, listContext] = getItemRenderState({ + const {data, props, context} = getItemRenderState({ id, size, onItemClick, - ...listParsedState, + ...list, ...listState, }); return ( ); }} diff --git a/src/components/useList/__stories__/components/RecursiveList.tsx b/src/components/useList/__stories__/components/RecursiveList.tsx index 5fa8773e77..2d76e88d22 100644 --- a/src/components/useList/__stories__/components/RecursiveList.tsx +++ b/src/components/useList/__stories__/components/RecursiveList.tsx @@ -9,13 +9,13 @@ import {useList} from '../../hooks/useList'; import {useListFilter} from '../../hooks/useListFilter'; import {useListKeydown} from '../../hooks/useListKeydown'; import {useListState} from '../../hooks/useListState'; -import type {ListItemId, ListSizeTypes} from '../../types'; +import type {ListItemId, ListItemSizeType} from '../../types'; import {getItemRenderState} from '../../utils/getItemRenderState'; import {createRandomizedData} from '../utils/makeData'; export interface RecursiveListProps { itemsCount: number; - size: ListSizeTypes; + size: ListItemSizeType; } export const RecursiveList = ({size, itemsCount}: RecursiveListProps) => { @@ -30,14 +30,14 @@ export const RecursiveList = ({size, itemsCount}: RecursiveListProps) => { const listState = useListState(); - const listParsedState = useList({ + const list = useList({ items: filterState.items, - expandedById: listState.expandedById, + ...listState, }); const onItemClick = React.useCallback( (id: ListItemId) => { - if (id in listParsedState.groupsState) { + if (id in list.groupsState) { listState.setExpanded((state) => ({ ...state, [id]: id in state ? !state[id] : false, @@ -52,13 +52,13 @@ export const RecursiveList = ({size, itemsCount}: RecursiveListProps) => { listState.setActiveItemId(id); }, - [listParsedState.groupsState, listState], + [list.groupsState, listState], ); useListKeydown({ containerRef, onItemClick, - ...listParsedState, + ...list, ...listState, }); @@ -67,7 +67,7 @@ export const RecursiveList = ({size, itemsCount}: RecursiveListProps) => { { expandedById={listState.expandedById} > {(id) => { - const [data, state, listContext] = getItemRenderState({ + const {data, props, context} = getItemRenderState({ id, size, onItemClick, - ...listParsedState, + ...list, ...listState, }); return ( ); }} diff --git a/src/components/useList/__stories__/useList.mdx b/src/components/useList/__stories__/useList.mdx index 2f3f031290..09a5b34520 100644 --- a/src/components/useList/__stories__/useList.mdx +++ b/src/components/useList/__stories__/useList.mdx @@ -52,29 +52,33 @@ function List() { const containerRef = React.useRef(null); const listState = useListState(); - const parsedListState = useList({ + const list = useList({ items, - expandedById: listState.expandedById, + ...listState, }); useListKeydown({ onItemClick, containerRef, - ...parsedListState, + ...list, ...listState, }); return ( - {parsedListState.items.map((_, i) => { - const [itemData, computedItemProps, _listContext] = getItemRenderState({ + {list.items.map((_, i) => { + const { + data, + props, + context: _context, + } = getItemRenderState({ id: String(i), onItemClick, - ...parsedListState, + ...list, ...listState, }); - return ; + return ; })} ); @@ -103,7 +107,7 @@ function List() { // same as prev example return ( - {parsedListState.items.map((item, index) => ( + {list.items.map((item, index) => ( {(id) => { - const [itemData, computedItemProps, _listContext] = getItemRenderState({ + const { + data, + props, + context: _context, + } = getItemRenderState({ id: String(i), onItemClick, - ...parsedListState, + ...list, ...listState, }); - return ; + return ; }} ))} @@ -168,7 +176,7 @@ interface ListTreeItemType extends ListItemInitialProps { export type ListItemType = ListTreeItemType | ListFlattenItemType; ``` -- `expandedById` - state for open/closed `List` elements. Affects the formation of the `flattenIdsOrder` - 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; +- `expandedById` - state for open/closed `List` elements. Affects the formation of the `existedFlattenIds` - 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; - `getId` - the property is optional. Allows you to generate an id for a list item depending on the list data: ```tsx @@ -178,7 +186,7 @@ const items = [ ]; /** - * byId: { + * itemsById: { * 'id-1': {id: 'id-1', title: 'some title 1'}, * 'id-2': {id: 'id-2', title: 'some title 2'}, * } @@ -197,12 +205,12 @@ const {byid} = useList({ - `parentId` - Id of the parent element, if there is a parent; - `indentation` - Nesting level; -- `byId` - normalized representation of list items: +- `itemsById` - normalized representation of list items: ```tsx export type ParsedState = { // ... - byId: Record; + itemsById: Record; // ... }; @@ -211,7 +219,7 @@ const {byid} = useList({ {data: {title: 'title-2'}, children: []}, ]; // -> - const byId: { + const itemsById: { 0: {title: 'title-1'}; '0-0': {title: 'title-1-1'}; 1: {title: 'title-2'}; @@ -222,7 +230,7 @@ const {byid} = useList({ - `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; -- `flattenIdsOrder` - sequential representation of list items by id, taking into account invisible elements inside collapsed groups; +- `existedFlattenIds` - sequential representation of list items by id, taking into account invisible elements inside collapsed groups; ### useListKeydown @@ -232,24 +240,24 @@ Keyboard support - `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`; -- `flattenIdsOrder` - a flat list of elements to be navigated through; Collapsed groups must be taken into account in this array; +- `existedFlattenIds` - 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; -- `enactive` - on/off keyboard support. Use it if you need to change the behavior in runtime; +- `enabled` - on/off keyboard support. Use it if you need to change the behavior in runtime; ```tsx const containerRef = React.useRef(null); -const parsedListState = useListState() -const parsedListState = useList(...) +const listState = useListState() +const list = useList(...) const handleItemClick = () => {...}; useListKeydown({ onItemClick: handleItemClick, containerRef, + ...list, ...listState, - ...parsedListState, }) ``` @@ -269,21 +277,25 @@ useListKeydown({ - `filter` - current filter value; - `reset` - method for resetting the filter value; - `items` - list of filtered sheet elements `listItemType[]`; -- `onChange` - callback for changing the filter value; +- `onFilterUpdate` - callback for changing the filter value; ```tsx const List = () => { - const {items, reset: _reset, ...conponentProps} = useListFilter({ + const {items, filter, onFilterUpdate, filterRef} = useListFilter({ items: [...] }) - const parsedListState = useList({ + const list = useList({ items, }) return ( <> - + ) } @@ -411,7 +423,7 @@ For the virtualized version of the list, you need to implement a component with index={index} expandedById={expandedById} > - {(id) => } + {(id) => } ))} @@ -425,13 +437,13 @@ Utility to compute list item height: ```tsx computeItemSize( // list size size, // has subrows - Boolean(get(byId[flattenIdsOrder[index]], 'subtitle')), + Boolean(get(itemsById[existedFlattenIds[index]], 'subtitle')), ) } /> @@ -447,7 +459,7 @@ const containerRef = React.useRef(null); React.useLayoutEffect(() => { if (open) { containerRef.current?.focus(); - listState.setActiveItemId(selectedId ?? listParsedState.flattenIdsOrder[0]); + listState.setActiveItemId(selectedId ?? list.existedFlattenIds[0]); if (selectedId) { scrollToListItem(selectedId, containerRef.current); @@ -493,21 +505,24 @@ item = T - `isLastItem` - if item is last in the list. Useful in cases than you need to do somthing on last item appears. For example, implement custom infinity lists variants ```tsx -const listParsedState = useList(); const listState = useListState(); +const list = useList({ + items, + ...listState, +}); const handleItemClick = () => {}; {(id) => { - const [data, stateProps, _listContext] = getItemRenderState({ + const {data, props} = getItemRenderState({ id, size, // list size onItemClick: handleItemClick, - ...listParsedState, + ...list, ...listState, }); - return ; + return ; }} ; ``` diff --git a/src/components/useList/components/ListContainerView/ListContainerView.scss b/src/components/useList/components/ListContainerView/ListContainerView.scss index 33defb91cb..ba4001b476 100644 --- a/src/components/useList/components/ListContainerView/ListContainerView.scss +++ b/src/components/useList/components/ListContainerView/ListContainerView.scss @@ -8,7 +8,7 @@ $block: '.#{variables.$ns}list-container-view'; outline: none; &_fixed-height { - height: var(--g-list-height, 300px); + height: var(--g-list-container-height, 300px); } &:not(#{$block}_fixed-height) { diff --git a/src/components/useList/components/ListContainerView/ListContainerView.tsx b/src/components/useList/components/ListContainerView/ListContainerView.tsx index f03bc9079d..2e541b719d 100644 --- a/src/components/useList/components/ListContainerView/ListContainerView.tsx +++ b/src/components/useList/components/ListContainerView/ListContainerView.tsx @@ -9,24 +9,30 @@ import './ListContainerView.scss'; const b = block('list-container-view'); -export interface ListContainerViewProps extends QAProps, React.HTMLAttributes<'div'> { +export interface ListContainerViewProps extends QAProps { + /** + * Ability to override default html tag + */ + as?: keyof JSX.IntrinsicElements; id?: string; + role?: React.AriaRole; className?: string; /** * Removes `overflow: auto` from container and set fixed container size (`--g-list-height` = `300px`) */ fixedHeight?: boolean; children: React.ReactNode; + extraProps?: React.HTMLAttributes<'div'>; } export const ListContainerView = React.forwardRef( function ListContainerView( - {role = 'listbox', children, id, className, fixedHeight, ...props}, + {as = 'div', role = 'listbox', children, id, className, fixedHeight, extraProps}, ref, ) { return ( {children} diff --git a/src/components/useList/components/ListItemView/ListItemView.scss b/src/components/useList/components/ListItemView/ListItemView.scss index ff377b774a..5e4aa8b143 100644 --- a/src/components/useList/components/ListItemView/ListItemView.scss +++ b/src/components/useList/components/ListItemView/ListItemView.scss @@ -20,6 +20,19 @@ $block: '.#{variables.$ns}list-item-view'; background: var(--g-color-base-selection); } + &_radius_s { + border-radius: var(--g-list-item-border-radius-s, 3px); + } + &_radius_m { + border-radius: var(--g-list-item-border-radius-m, 6px); + } + &_radius_l { + border-radius: var(--g-list-item-border-radius-l, 8px); + } + &_radius_xl { + border-radius: var(--g-list-item-border-radius-xl, 8px); + } + &__slot { &_indent_1 { width: 16px; diff --git a/src/components/useList/components/ListItemView/ListItemView.tsx b/src/components/useList/components/ListItemView/ListItemView.tsx index c35f95c94a..53acbe1ab9 100644 --- a/src/components/useList/components/ListItemView/ListItemView.tsx +++ b/src/components/useList/components/ListItemView/ListItemView.tsx @@ -4,18 +4,17 @@ import {Check, ChevronDown, ChevronUp} from '@gravity-ui/icons'; import {Icon} from '../../../Icon'; import {Text, colorText} from '../../../Text'; -import {borderRadius} from '../../../borderRadius'; import {Flex, FlexProps, spacing} from '../../../layout'; import type {QAProps} from '../../../types'; import {block} from '../../../utils/cn'; import {LIST_ITEM_DATA_ATR, modToHeight} from '../../constants'; -import type {ListItemId, ListSizeTypes} from '../../types'; +import type {ListItemId, ListItemSizeType} from '../../types'; import './ListItemView.scss'; const b = block('list-item-view'); -export interface ListItemViewProps extends QAProps, Omit, 'title'> { +export interface ListItemViewProps extends QAProps { /** * Ability to override default html tag */ @@ -23,7 +22,7 @@ export interface ListItemViewProps extends QAProps, Omit { +export interface UseListProps extends Partial { items: ListItemType[]; /** * Control expanded items state from external source */ - expandedById?: Record; getId?(item: T): ListItemId; } +export type UseListResult = ListParsedState; + /** * Take array of items as a argument and returns parsed representation of this data structure to work with */ -export const useList = ({items, expandedById, getId}: UseListProps): ListParsedState => { - const {byId, groupsState, itemsState} = useListParsedState({ +export const useList = ({items, expandedById, getId}: UseListProps): UseListResult => { + const {itemsById, groupsState, itemsState, initialState} = useListParsedState({ items, getId, }); - const flattenIdsOrder = useFlattenListItems({ + const existedFlattenIds = useFlattenListItems({ items, - expandedById, + /** + * By default controlled from list items declaration state + */ + expandedById: expandedById || initialState.expandedById, getId, }); - return {items, flattenIdsOrder, byId, groupsState, itemsState}; + return {items, existedFlattenIds, itemsById, groupsState, itemsState}; }; diff --git a/src/components/useList/hooks/useListFilter.ts b/src/components/useList/hooks/useListFilter.ts index a5b55d07b1..4a6d06d8fa 100644 --- a/src/components/useList/hooks/useListFilter.ts +++ b/src/components/useList/hooks/useListFilter.ts @@ -29,7 +29,7 @@ interface UseListFilterProps { * Ready-to-use logic for filtering tree-like data structures * ```tsx * const {item: filteredItems,...listFiltration} = useListFIlter({items}); - * const listParsedState = useList({items: filteredItems}); + * const list = useList({items: filteredItems}); * * * ``` @@ -70,28 +70,29 @@ export function useListFilter({ setPrevItems(externalItems); } - const reset = React.useCallback(() => { - setFilter(initialFilterValue); - setItems(externalItems); - }, [externalItems, initialFilterValue]); - - const onChange = React.useMemo(() => { + const {onFilterUpdate, reset} = React.useMemo(() => { const debouncedFn = debounce( (value) => setItems(filterItemsFn(value, externalItems)), debounceTimeout, ); - return (nextFilterValue: string) => { - setFilter(nextFilterValue); - debouncedFn(nextFilterValue); + return { + reset: () => { + setFilter(initialFilterValue); + debouncedFn(initialFilterValue); + }, + onFilterUpdate: (nextFilterValue: string) => { + setFilter(nextFilterValue); + debouncedFn(nextFilterValue); + }, }; - }, [debounceTimeout, externalItems, filterItemsFn]); + }, [debounceTimeout, externalItems, filterItemsFn, initialFilterValue]); return { filterRef, filter, reset, items, - onChange, + onFilterUpdate, }; } diff --git a/src/components/useList/hooks/useListKeydown.tsx b/src/components/useList/hooks/useListKeydown.tsx index b230cb2809..fd64905401 100644 --- a/src/components/useList/hooks/useListKeydown.tsx +++ b/src/components/useList/hooks/useListKeydown.tsx @@ -1,48 +1,49 @@ import React from 'react'; +import {KeyCode} from '../../../constants'; import type {ListItemId, ListState} from '../types'; import {findNextIndex} from '../utils/findNextIndex'; import {scrollToListItem} from '../utils/scrollToListItem'; interface UseListKeydownProps extends Partial> { - flattenIdsOrder: ListItemId[]; + existedFlattenIds: ListItemId[]; onItemClick?(itemId: ListItemId): void; containerRef?: React.RefObject; setActiveItemId?(id: ListItemId): void; - enactive?: boolean; + enabled?: boolean; } // Use this hook if you need keyboard support for tree structure lists export const useListKeydown = ({ - flattenIdsOrder, + existedFlattenIds, onItemClick, containerRef, disabledById = {}, activeItemId, setActiveItemId, - enactive, + enabled, }: UseListKeydownProps) => { const activateItem = React.useCallback( (index?: number, scrollTo = true) => { - if (typeof index === 'number' && flattenIdsOrder[index]) { + if (typeof index === 'number' && existedFlattenIds[index]) { if (scrollTo) { - scrollToListItem(flattenIdsOrder[index], containerRef?.current); + scrollToListItem(existedFlattenIds[index], containerRef?.current); } - setActiveItemId?.(flattenIdsOrder[index]); + setActiveItemId?.(existedFlattenIds[index]); } }, - [containerRef, flattenIdsOrder, setActiveItemId], + [containerRef, existedFlattenIds, setActiveItemId], ); const handleKeyMove = React.useCallback( (event: KeyboardEvent, step: number, defaultItemIndex = 0) => { event.preventDefault(); - const maybeIndex = flattenIdsOrder.findIndex((i) => i === activeItemId); + const maybeIndex = existedFlattenIds.findIndex((i) => i === activeItemId); const nextIndex = findNextIndex({ - list: flattenIdsOrder, + list: existedFlattenIds, index: (maybeIndex > -1 ? maybeIndex : defaultItemIndex) + step, step: Math.sign(step), disabledItems: disabledById, @@ -50,28 +51,28 @@ export const useListKeydown = ({ activateItem(nextIndex); }, - [activateItem, activeItemId, disabledById, flattenIdsOrder], + [activateItem, activeItemId, disabledById, existedFlattenIds], ); React.useLayoutEffect(() => { const anchor = containerRef?.current; - if (enactive || !anchor) { + if (enabled || !anchor) { return undefined; } const handleKeyDown = (event: KeyboardEvent) => { switch (event.key) { - case 'ArrowDown': { + case KeyCode.ARROW_DOWN: { handleKeyMove(event, 1, -1); break; } - case 'ArrowUp': { + case KeyCode.ARROW_UP: { handleKeyMove(event, -1); break; } - case ' ': - case 'Enter': { + case KeyCode.SPACEBAR: + case KeyCode.ENTER: { if (activeItemId && !disabledById[activeItemId]) { event.preventDefault(); @@ -89,5 +90,5 @@ export const useListKeydown = ({ return () => { anchor.removeEventListener('keydown', handleKeyDown); }; - }, [activeItemId, containerRef, disabledById, enactive, handleKeyMove, onItemClick]); + }, [activeItemId, containerRef, disabledById, enabled, handleKeyMove, onItemClick]); }; diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 77b225dae2..b9a113b989 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -1,6 +1,6 @@ export type ListItemId = string; -export type ListSizeTypes = 's' | 'm' | 'l' | 'xl'; +export type ListItemSizeType = 's' | 'm' | 'l' | 'xl'; interface ListItemInitialProps { /** * If you need to control the state from the outside, @@ -21,7 +21,7 @@ interface ListItemInitialProps { expanded?: boolean; } -export type ListFlattenItemType = T & ListItemInitialProps; +export type ListFlattenItemType = T extends {} ? T & ListItemInitialProps : T; export interface ListTreeItemType extends ListItemInitialProps { data: T; @@ -34,7 +34,7 @@ export type GroupParsedState = { childrenIds: ListItemId[]; }; -export type ItemParsedState = { +export type ItemState = { parentId?: ListItemId; indentation: number; }; @@ -54,7 +54,7 @@ export interface OverrideItemContext { } export type RenderItemContext = { - itemState: ItemParsedState; + itemState: ItemState; /** * Exists if item is group */ @@ -63,7 +63,7 @@ export type RenderItemContext = { }; export type RenderItemState = { - size: ListSizeTypes; + size: ListItemSizeType; id: ListItemId; onClick?(): void; selected: boolean; @@ -79,11 +79,11 @@ export type ParsedState = { * Stored internal meta info about item * Note: Groups are also items */ - itemsState: Record; + itemsState: Record; /** * Normalized original data */ - byId: Record; + itemsById: Record; /** * Stored info about group items: */ @@ -99,5 +99,5 @@ export type ListState = { export type ListParsedState = ParsedState & { items: ListItemType[]; - flattenIdsOrder: ListItemId[]; + existedFlattenIds: ListItemId[]; }; diff --git a/src/components/useList/utils/computeItemSize.ts b/src/components/useList/utils/computeItemSize.ts index 189180b480..23b1544088 100644 --- a/src/components/useList/utils/computeItemSize.ts +++ b/src/components/useList/utils/computeItemSize.ts @@ -1,6 +1,6 @@ import {modToHeight} from '../constants'; -import type {ListSizeTypes} from '../types'; +import type {ListItemSizeType} from '../types'; -export const computeItemSize = (size: ListSizeTypes, hasSubRows = false) => { +export const computeItemSize = (size: ListItemSizeType, hasSubRows = false) => { return modToHeight[size][Number(hasSubRows)]; }; diff --git a/src/components/useList/utils/getItemRenderState.tsx b/src/components/useList/utils/getItemRenderState.tsx index 73e12a6adf..1307031725 100644 --- a/src/components/useList/utils/getItemRenderState.tsx +++ b/src/components/useList/utils/getItemRenderState.tsx @@ -1,8 +1,8 @@ /* eslint-disable valid-jsdoc */ import type { ListItemId, + ListItemSizeType, ListParsedState, - ListSizeTypes, ListState, RenderItemContext, RenderItemState, @@ -10,7 +10,7 @@ import type { type ItemRendererProps = ListState & ListParsedState & { - size?: ListSizeTypes; + size?: ListItemSizeType; id: ListItemId; onItemClick?(id: ListItemId): void; }; @@ -20,12 +20,12 @@ type ItemRendererProps = ListState & */ export const getItemRenderState = ( { - byId, + itemsById, disabledById, expandedById, groupsState, onItemClick, - flattenIdsOrder, + existedFlattenIds, size = 'm', itemsState, selectedById, @@ -34,10 +34,10 @@ export const getItemRenderState = ( }: ItemRendererProps, {defaultExpanded = true}: {defaultExpanded?: boolean} = {}, ) => { - const listContext: RenderItemContext = { + const context: RenderItemContext = { itemState: itemsState[id], groupState: groupsState[id], - isLastItem: id === flattenIdsOrder[flattenIdsOrder.length - 1], + isLastItem: id === existedFlattenIds[existedFlattenIds.length - 1], }; let expanded; @@ -52,11 +52,11 @@ export const getItemRenderState = ( size, expanded, active: id === activeItemId, - indentation: listContext.itemState.indentation, + indentation: context.itemState.indentation, disabled: disabledById[id], selected: selectedById[id], onClick: onItemClick ? () => onItemClick(id) : undefined, }; - return [byId[id], stateProps, listContext] as const; + return {data: itemsById[id], props: stateProps, context}; }; diff --git a/src/components/useList/utils/getListItemId.ts b/src/components/useList/utils/getListItemId.ts index 666978d73c..78d101075e 100644 --- a/src/components/useList/utils/getListItemId.ts +++ b/src/components/useList/utils/getListItemId.ts @@ -13,7 +13,7 @@ export const getListItemId = ({item, groupedId, getId}: GetListItemIdProps if (typeof getId === 'function') { id = getId(isTreeItemGuard(item) ? item.data : item); - } else if (item.id) { + } else if (item && typeof item === 'object' && 'id' in item && item.id) { id = item.id; } diff --git a/src/components/useList/utils/getListParsedState.test.ts b/src/components/useList/utils/getListParsedState.test.ts index fa9bd7a6c4..9122c4cf29 100644 --- a/src/components/useList/utils/getListParsedState.test.ts +++ b/src/components/useList/utils/getListParsedState.test.ts @@ -45,7 +45,7 @@ describe('getListParsedState', () => { '1-1': false, }, }, - byId: { + itemsById: { 0: {title: 'item-0'}, 1: {title: 'item-1'}, '1-0': {title: 'child-1-1'}, @@ -100,7 +100,7 @@ describe('getListParsedState', () => { }, expandedById: {}, }, - byId: { + itemsById: { 0: { a: 'item-1', children: [], @@ -151,7 +151,7 @@ describe('getListParsedState', () => { 'id-4': false, }, }, - byId: { + itemsById: { 'id-1': {title: 'item-0', id: 'id-1'}, 'id-2': {title: 'item-1', id: 'id-2'}, 'id-3': {title: 'child-1-1', id: 'id-3'}, diff --git a/src/components/useList/utils/getListParsedState.ts b/src/components/useList/utils/getListParsedState.ts index 849966e137..86350b3281 100644 --- a/src/components/useList/utils/getListParsedState.ts +++ b/src/components/useList/utils/getListParsedState.ts @@ -44,7 +44,7 @@ export function getListParsedState( } const result: ListParsedStateResult = { - byId: {}, + itemsById: {}, groupsState: {}, itemsState: {}, initialState: { @@ -57,7 +57,7 @@ export function getListParsedState( const traverseItem = ({item, index}: TraverseItemProps) => { const id = getListItemId({groupedId: String(index), item, getId}); - result.byId[id] = item; + result.itemsById[id] = item; if (!result.itemsState[id]) { result.itemsState[id] = { @@ -65,12 +65,14 @@ export function getListParsedState( }; } - if (typeof item.selected !== 'undefined') { - result.initialState.selectedById[id] = item.selected; - } + if (item && typeof item === 'object') { + if ('selected' in item && typeof item.selected === 'boolean') { + result.initialState.selectedById[id] = item.selected; + } - if (typeof item.disabled !== 'undefined') { - result.initialState.disabledById[id] = item.disabled; + if ('disabled' in item && typeof item.disabled === 'boolean') { + result.initialState.disabledById[id] = item.disabled; + } } }; @@ -87,7 +89,7 @@ export function getListParsedState( result.groupsState[parentId].childrenIds.push(id); } - result.byId[id] = item.data; + result.itemsById[id] = item.data; if (!result.itemsState[id]) { result.itemsState[id] = { diff --git a/src/components/useList/utils/scrollToListItem.ts b/src/components/useList/utils/scrollToListItem.ts index a86de0a84f..b2ab2358d1 100644 --- a/src/components/useList/utils/scrollToListItem.ts +++ b/src/components/useList/utils/scrollToListItem.ts @@ -3,10 +3,10 @@ import type {ListItemId} from '../types'; export const scrollToListItem = ( itemId: ListItemId, - containerRef?: HTMLDivElement | HTMLUListElement | null, + containerElement?: HTMLDivElement | HTMLUListElement | null, ) => { if (document) { - const element = (containerRef || document).querySelector( + const element = (containerElement || document).querySelector( `[${LIST_ITEM_DATA_ATR}="${itemId}"]`, ); diff --git a/src/unstable.ts b/src/unstable.ts index c96cb6e2e6..7d0b8d5a49 100644 --- a/src/unstable.ts +++ b/src/unstable.ts @@ -1,2 +1,8 @@ -export {useList, useListFilter, useListKeydown} from './components/useList'; +/* eslint-disable camelcase */ +export { + useList as unstable_useList, + useListState as unstable_useListState, + useListFilter as unstable_useListFilter, + useListKeydown as unstable_useListKeydown, +} from './components/useList'; export * from './components/TreeSelect';