From c9de34be759579d985fb39f76ee7e21f3e8f0ec9 Mon Sep 17 00:00:00 2001 From: ooops_o_O <47597207+mfrolov89@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:32:18 +0300 Subject: [PATCH] =?UTF-8?q?feat(UIKIT-1592,TreeLikeList):=20=D0=A0=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B8=20=D1=81=D0=BA=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D0=BB=20=D0=B4=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE?= =?UTF-8?q?=D1=87=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=20=D0=B2=D1=8B=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=BE=D0=B2=20(#1110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Tree/TreeList/TreeItem/TreeItem.tsx | 3 +- .../TreeList/TreeItem/useLogic/useLogic.ts | 3 + .../src/TreeLikeList/TreeItem/TreeItem.tsx | 45 +++++++----- .../TreeItem/useLogic/useLogic.ts | 22 +++--- .../src/TreeLikeList/TreeLikeList.stories.tsx | 29 +++++++- .../src/TreeLikeList/TreeLikeList.test.tsx | 1 + .../src/TreeLikeList/TreeLikeList.tsx | 26 +++---- .../src/TreeLikeList/useLogic/useLogic.ts | 45 ++++++++++-- .../utils/getChainsId/getChainsId.test.ts | 70 +++++++++++++++++++ .../useLogic/utils/getChainsId/getChainsId.ts | 29 ++++++++ .../useLogic/utils/getChainsId/index.ts | 1 + .../src/TreeLikeList/useLogic/utils/index.ts | 1 + 12 files changed, 224 insertions(+), 51 deletions(-) create mode 100644 packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.test.ts create mode 100644 packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.ts create mode 100644 packages/components/src/TreeLikeList/useLogic/utils/getChainsId/index.ts create mode 100644 packages/components/src/TreeLikeList/useLogic/utils/index.ts diff --git a/packages/components/src/Tree/TreeList/TreeItem/TreeItem.tsx b/packages/components/src/Tree/TreeList/TreeItem/TreeItem.tsx index 25ef37efd..b1bc9041f 100644 --- a/packages/components/src/Tree/TreeList/TreeItem/TreeItem.tsx +++ b/packages/components/src/Tree/TreeList/TreeItem/TreeItem.tsx @@ -63,6 +63,7 @@ export const TreeItem = (props: TreeItemProps) => { isDefaultExpanded, isDisabled, disableReason, + nextLevel, handleChange, } = useLogic(props); @@ -105,7 +106,7 @@ export const TreeItem = (props: TreeItemProps) => { {...child} prefixId={prefixId} renderItem={renderItem} - level={level + 1} + level={nextLevel} isInitialExpanded={isInitialExpanded} expandedLevel={expandedLevel} chainToSelectedItem={chainToSelectedItem} diff --git a/packages/components/src/Tree/TreeList/TreeItem/useLogic/useLogic.ts b/packages/components/src/Tree/TreeList/TreeItem/useLogic/useLogic.ts index 08cd761bc..ab0c499ef 100644 --- a/packages/components/src/Tree/TreeList/TreeItem/useLogic/useLogic.ts +++ b/packages/components/src/Tree/TreeList/TreeItem/useLogic/useLogic.ts @@ -21,6 +21,8 @@ export const useLogic = ({ const isDisabled = Boolean(disabledItem); const disableReason = disabledItem?.disableReason; + const nextLevel = level + 1; + const handleChange = () => { onChange?.(id); }; @@ -30,6 +32,7 @@ export const useLogic = ({ isDefaultExpanded, isDisabled, disableReason, + nextLevel, handleChange, }; }; diff --git a/packages/components/src/TreeLikeList/TreeItem/TreeItem.tsx b/packages/components/src/TreeLikeList/TreeItem/TreeItem.tsx index ea96b3b47..ae6de0ffe 100644 --- a/packages/components/src/TreeLikeList/TreeItem/TreeItem.tsx +++ b/packages/components/src/TreeLikeList/TreeItem/TreeItem.tsx @@ -15,6 +15,11 @@ export type TreeItemProps = TreeListData & { */ value?: MultipleValue; + /** + * Уникальный префикс для идентификаторов в рамках дерева + */ + prefixId?: string; + /** * Render-props, позволяет более гибко настраивать содержимое item */ @@ -36,6 +41,11 @@ export type TreeItemProps = TreeListData & { */ expandedLevel: number; + /** + * Цепочки идентификаторов до выбранных элементов дерева + */ + chainsToSelectedItem?: Array; + /** * Список `value` элементов дерева, которые не доступны для взаимодействия */ @@ -47,19 +57,7 @@ export type TreeItemProps = TreeListData & { onChange: (value: MultipleValue) => void; }; -export const TreeItem = ({ - id, - label, - note, - level, - renderItem, - children = [], - value, - isInitialExpanded, - expandedLevel, - disabledItems, - onChange, -}: TreeItemProps) => { +export const TreeItem = (props: TreeItemProps) => { const { isSelected, isDefaultExpanded, @@ -67,15 +65,23 @@ export const TreeItem = ({ isDisabled, nextLevel, handleChange, - } = useLogic({ + } = useLogic(props); + + const { id, - value, + prefixId, + label, + note, level, + renderItem, + children = [], + value, isInitialExpanded, expandedLevel, + chainsToSelectedItem, disabledItems, onChange, - }); + } = props; /** * Предотвращаем всплытие события, так как клик в области чекбокса или label вызывает обработчик на уровне всего item @@ -85,7 +91,8 @@ export const TreeItem = ({ if (children.length) { return ( ( ; +type UseLogicProps = TreeItemProps; export const useLogic = ({ id, @@ -19,12 +11,20 @@ export const useLogic = ({ level, isInitialExpanded, expandedLevel, + chainsToSelectedItem = [], disabledItems, onChange, }: UseLogicProps) => { const isSelected = checkIsSelected(value, id); - const isDefaultExpanded = isInitialExpanded && level <= expandedLevel - 1; + const flatChainsToSelectedItem: MultipleValue = chainsToSelectedItem?.reduce( + (acc, chain: MultipleValue) => [...(acc || []), ...(chain || [])], + [], + ); + + const isDefaultExpanded = + flatChainsToSelectedItem?.includes(id) || + (isInitialExpanded && level <= expandedLevel - 1); const disabledItem = disabledItems?.find((item) => item.id === id); const isDisabled = Boolean(disabledItem); diff --git a/packages/components/src/TreeLikeList/TreeLikeList.stories.tsx b/packages/components/src/TreeLikeList/TreeLikeList.stories.tsx index eacc92020..f705a4a81 100644 --- a/packages/components/src/TreeLikeList/TreeLikeList.stories.tsx +++ b/packages/components/src/TreeLikeList/TreeLikeList.stories.tsx @@ -108,18 +108,43 @@ const FAKE_NOTE_TREE_LIST_DATA = [ }, ]; +const Wrapper = styled.div` + height: 240px; + overflow-y: auto; +`; + export const Example = () => { - const [value, setValue] = useState | undefined>(); + const [value, setValue] = useState | undefined>(['211', '4']); const fakeData = [ + { + id: 'a', + label: 'Item A', + }, + { + id: 'b', + label: 'Item B', + }, + { + id: 'c', + label: 'Item C', + }, ...FAKE_TREE_LIST_DATA, { id: '3', label: 'Item 3', }, + { + id: '4', + label: 'Item 4', + }, ]; - return ; + return ( + + + + ); }; /** diff --git a/packages/components/src/TreeLikeList/TreeLikeList.test.tsx b/packages/components/src/TreeLikeList/TreeLikeList.test.tsx index 8757c00ba..f548b52bb 100644 --- a/packages/components/src/TreeLikeList/TreeLikeList.test.tsx +++ b/packages/components/src/TreeLikeList/TreeLikeList.test.tsx @@ -218,6 +218,7 @@ describe('TreeLikeList', () => { return ; }; + window.HTMLElement.prototype.scrollIntoView = () => {}; renderWithTheme(); const checked = screen.getAllByRole('checkbox', { checked: true }); diff --git a/packages/components/src/TreeLikeList/TreeLikeList.tsx b/packages/components/src/TreeLikeList/TreeLikeList.tsx index 9a9dd122e..4cb4cc79f 100644 --- a/packages/components/src/TreeLikeList/TreeLikeList.tsx +++ b/packages/components/src/TreeLikeList/TreeLikeList.tsx @@ -7,27 +7,27 @@ export type { TreeLikeListProps }; const INITIAL_LEVEL = 0; -export const TreeLikeList = ({ - data, - value, - className, - expandedLevel = 10, - disabledItems, - ...props -}: TreeLikeListProps) => { - const { formattedDisabledItems } = useLogic({ disabledItems }); +export const TreeLikeList = (props: TreeLikeListProps) => { + const { listProps, itemProps } = useLogic(props); + + const { + data, + className, + expandedLevel = 10, + disabledItems, + ...restProps + } = props; return ( - + {data.map((item) => ( ))} diff --git a/packages/components/src/TreeLikeList/useLogic/useLogic.ts b/packages/components/src/TreeLikeList/useLogic/useLogic.ts index a09bb1cf1..bd5e75588 100644 --- a/packages/components/src/TreeLikeList/useLogic/useLogic.ts +++ b/packages/components/src/TreeLikeList/useLogic/useLogic.ts @@ -1,17 +1,48 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; +import { useId } from '../../hooks/useId'; import { getFormatDisabledItems } from '../../Tree/utils'; -import { type DisabledItemValue } from '../types'; +import { type TreeLikeListProps } from '../TreeLikeList'; -type UseLogicParams = { - disabledItems?: Array; -}; +import { getChainsId } from './utils'; + +type UseLogicParams = TreeLikeListProps; + +export const useLogic = ({ data, value, disabledItems }: UseLogicParams) => { + const prefixId = useId(); + + const listRef = useRef(null); + + useEffect(() => { + if (listRef.current && value?.length) { + // Выбираем первый элемент из списка value + const targetItem = listRef.current.querySelector(` + li[id="${prefixId}${value[0]}"]`); + + if (targetItem) { + targetItem.scrollIntoView({ block: 'center' }); + } + } + }, [listRef, prefixId]); -export const useLogic = ({ disabledItems }: UseLogicParams) => { const formattedDisabledItems = useMemo( () => getFormatDisabledItems(disabledItems), [disabledItems], ); - return { formattedDisabledItems }; + const chainsToSelectedItem = useMemo( + () => getChainsId(data, value), + [data, value], + ); + + return { + listProps: { + ref: listRef, + }, + itemProps: { + chainsToSelectedItem, + disabledItems: formattedDisabledItems, + prefixId, + }, + }; }; diff --git a/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.test.ts b/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.test.ts new file mode 100644 index 000000000..8adf12411 --- /dev/null +++ b/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; + +import type { TreeListData } from '../../../../Tree'; + +import { getChainsId } from './getChainsId'; + +describe('getChainsId', () => { + it('Возвращает цепочки идентификаторов до целевых id', () => { + const fakeTree: Array = [ + { + id: '1', + label: '1', + children: [{ id: '11', label: '11' }], + }, + { + id: '2', + label: '2', + children: [ + { + id: '21', + label: '21', + children: [ + { id: '211', label: '211' }, + { id: '212', label: '212' }, + ], + }, + { + id: '22', + label: '22', + }, + ], + }, + { id: '3', label: '3' }, + ]; + + const sut = getChainsId(fakeTree, ['211', '3']); + + expect(sut).toEqual([['2', '21', '211'], ['3']]); + }); + + it('Возвращает пустой массив, если не указаны целевые id', () => { + const fakeTree: Array = [ + { + id: '1', + label: '1', + children: [{ id: '11', label: '11' }], + }, + { + id: '2', + label: '2', + children: [ + { id: '21', label: '21' }, + { + id: '22', + label: '22', + children: [ + { id: '211', label: '211' }, + { id: '222', label: '222' }, + ], + }, + ], + }, + { id: '3', label: '3' }, + ]; + + const sut = getChainsId(fakeTree, ['211, 3']); + + expect(sut).toEqual([]); + }); +}); diff --git a/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.ts b/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.ts new file mode 100644 index 000000000..901582b21 --- /dev/null +++ b/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/getChainsId.ts @@ -0,0 +1,29 @@ +import type { TreeListData } from '../../../../Tree'; +import type { MultipleValue } from '../../../types'; + +export const getChainsId = ( + tree: Array, + targetId: MultipleValue, +): string[][] => { + const chainIds: string[][] = []; + + const findChain = (node: TreeListData, chainId: string[]): void => { + chainId.push(node.id); + + if (targetId && targetId.includes(node.id)) { + chainIds.push([...chainId]); + + return; + } + + if (node.children) { + for (const child of node.children) { + findChain(child, [...chainId]); + } + } + }; + + tree.forEach((node) => findChain(node, [])); + + return chainIds; +}; diff --git a/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/index.ts b/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/index.ts new file mode 100644 index 000000000..bfb253e75 --- /dev/null +++ b/packages/components/src/TreeLikeList/useLogic/utils/getChainsId/index.ts @@ -0,0 +1 @@ +export * from './getChainsId'; diff --git a/packages/components/src/TreeLikeList/useLogic/utils/index.ts b/packages/components/src/TreeLikeList/useLogic/utils/index.ts new file mode 100644 index 000000000..b18f574d6 --- /dev/null +++ b/packages/components/src/TreeLikeList/useLogic/utils/index.ts @@ -0,0 +1 @@ +export { getChainsId } from './getChainsId';