From 4dde6727c7e7fe7fbf6fedb4e1619deddbd3e16a Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Wed, 19 Apr 2023 10:18:17 +0200 Subject: [PATCH] feat(react-tree): value property over id (#27532) --- ...-7105e198-75d7-4c94-9f29-0708d538d53c.json | 7 ++ ...-96da4453-4b84-41e1-97c5-ae455bf6c037.json | 7 ++ .../etc/react-components.unstable.api.md | 3 - .../react-components/src/unstable/index.ts | 1 - .../react-tree/etc/react-tree.api.md | 103 +++++++++++------- .../src/components/Tree/Tree.cy.tsx | 2 +- .../react-tree/src/components/Tree/Tree.tsx | 4 +- .../src/components/Tree/Tree.types.ts | 32 +++--- .../src/components/TreeItem/TreeItem.tsx | 6 +- .../src/components/TreeItem/TreeItem.types.ts | 4 +- .../src/components/TreeItem/useTreeItem.tsx | 56 +++++++--- .../react-tree/src/contexts/treeContext.ts | 7 +- .../react-tree/src/hooks/useFlatTree.ts | 95 ++++++++-------- .../src/hooks/useFlatTreeNavigation.ts | 19 ++-- .../src/hooks/useNestedTreeNavigation.ts | 2 +- .../react-tree/src/hooks/useOpenItemsState.ts | 18 ++- .../react-components/react-tree/src/index.ts | 2 +- .../src/utils/createFlatTreeItems.ts | 70 ++++++------ .../react-tree/src/utils/flattenTree.ts | 41 ++++--- .../TreeControllingOpenAndClose.stories.tsx | 23 ++-- .../Tree/TreeDefaultOpenTrees.stories.tsx | 6 +- .../stories/Tree/Virtualization.stories.tsx | 12 +- .../stories/Tree/flattenTree.stories.tsx | 6 +- .../stories/Tree/useFlatTree.stories.tsx | 44 ++++---- .../TreeItem/TreeItemAddRemove.stories.tsx | 72 ++++++------ ...TreeItemExpandCollapseIconOnly.stories.tsx | 6 +- .../TreeItem/TreeItemExpandIcon.stories.tsx | 12 +- 27 files changed, 366 insertions(+), 294 deletions(-) create mode 100644 change/@fluentui-react-components-7105e198-75d7-4c94-9f29-0708d538d53c.json create mode 100644 change/@fluentui-react-tree-96da4453-4b84-41e1-97c5-ae455bf6c037.json diff --git a/change/@fluentui-react-components-7105e198-75d7-4c94-9f29-0708d538d53c.json b/change/@fluentui-react-components-7105e198-75d7-4c94-9f29-0708d538d53c.json new file mode 100644 index 00000000000000..6e405d8615b3eb --- /dev/null +++ b/change/@fluentui-react-components-7105e198-75d7-4c94-9f29-0708d538d53c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "removes TreeItemId type from react-tree", + "packageName": "@fluentui/react-components", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tree-96da4453-4b84-41e1-97c5-ae455bf6c037.json b/change/@fluentui-react-tree-96da4453-4b84-41e1-97c5-ae455bf6c037.json new file mode 100644 index 00000000000000..579f523955f5dc --- /dev/null +++ b/change/@fluentui-react-tree-96da4453-4b84-41e1-97c5-ae455bf6c037.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: value property over id", + "packageName": "@fluentui/react-tree", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-components/etc/react-components.unstable.api.md b/packages/react-components/react-components/etc/react-components.unstable.api.md index 4ff2dcdb7324ce..43406b8392284c 100644 --- a/packages/react-components/react-components/etc/react-components.unstable.api.md +++ b/packages/react-components/react-components/etc/react-components.unstable.api.md @@ -53,7 +53,6 @@ import { treeClassNames } from '@fluentui/react-tree'; import { TreeContextValue } from '@fluentui/react-tree'; import { TreeItem } from '@fluentui/react-tree'; import { treeItemClassNames } from '@fluentui/react-tree'; -import { TreeItemId } from '@fluentui/react-tree'; import { TreeItemLayout } from '@fluentui/react-tree'; import { treeItemLayoutClassNames } from '@fluentui/react-tree'; import { TreeItemLayoutProps } from '@fluentui/react-tree'; @@ -215,8 +214,6 @@ export { TreeItem } export { treeItemClassNames } -export { TreeItemId } - export { TreeItemLayout } export { treeItemLayoutClassNames } diff --git a/packages/react-components/react-components/src/unstable/index.ts b/packages/react-components/react-components/src/unstable/index.ts index 2a60e6660eb1e6..757959fabfaf71 100644 --- a/packages/react-components/react-components/src/unstable/index.ts +++ b/packages/react-components/react-components/src/unstable/index.ts @@ -125,7 +125,6 @@ export type { TreeItemLayoutState, TreeItemLayoutSlots, TreeItemLayoutProps, - TreeItemId, TreeContextValue, NestedTreeItem, FlatTree, diff --git a/packages/react-components/react-tree/etc/react-tree.api.md b/packages/react-components/react-tree/etc/react-tree.api.md index 5aa543e4730ae0..b680ce0c98c1f0 100644 --- a/packages/react-components/react-tree/etc/react-tree.api.md +++ b/packages/react-components/react-tree/etc/react-tree.api.md @@ -27,35 +27,36 @@ import { ProviderProps } from 'react'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { SlotRenderFunction } from '@fluentui/react-utilities'; // @public -export const flattenTree_unstable: (items: NestedTreeItem[]) => FlatTreeItemProps[]; +export const flattenTree_unstable: (items: NestedTreeItem[]) => FlatTreeItemProps[]; // @public -export type FlatTree = { - getTreeProps(): FlatTreeProps; - navigate(data: TreeNavigationData_unstable): void; - getNextNavigableItem(visibleItems: FlatTreeItem[], data: TreeNavigationData_unstable): FlatTreeItem | undefined; - items(): IterableIterator; +export type FlatTree = { + getTreeProps(): FlatTreeProps; + navigate(data: TreeNavigationData_unstable): void; + getNextNavigableItem(visibleItems: FlatTreeItem[], data: TreeNavigationData_unstable): FlatTreeItem | undefined; + items(): IterableIterator>; }; // @public (undocumented) -export type FlatTreeItem = Readonly; +export type FlatTreeItem = Readonly>; // @public (undocumented) -export type FlatTreeItemProps = TreeItemProps & { - id: TreeItemId; - parentId?: string; +export type FlatTreeItemProps = Omit & { + value: Value; + parentValue?: Value; }; // @public (undocumented) -export type FlatTreeProps = Required & { +export type FlatTreeProps = Required, 'openItems' | 'onOpenChange' | 'onNavigation_unstable'> & { ref: React_2.Ref; }>; // @public (undocumented) -export type NestedTreeItem = Omit & { - subtree?: NestedTreeItem[]; +export type NestedTreeItem = Omit, 'subtree'> & { + subtree?: NestedTreeItem[]; }; // @public (undocumented) @@ -71,7 +72,22 @@ export const renderTreeItemLayout_unstable: (state: TreeItemLayoutState) => JSX. export const renderTreeItemPersonaLayout_unstable: (state: TreeItemPersonaLayoutState, contextValues: TreeItemPersonaLayoutContextValues) => JSX.Element; // @public -export const Tree: ForwardRefComponent; +export const Tree: React_2.ForwardRefExoticComponent & Omit<{ + as?: "div" | undefined; +} & Pick, HTMLDivElement>, "key" | keyof React_2.HTMLAttributes> & { + ref?: ((instance: HTMLDivElement | null) => void) | React_2.RefObject | null | undefined; +} & { + children?: React_2.ReactNode | SlotRenderFunction, HTMLDivElement>, "key" | keyof React_2.HTMLAttributes> & { + ref?: ((instance: HTMLDivElement | null) => void) | React_2.RefObject | null | undefined; + }>; +}, "ref"> & { + appearance?: "subtle" | "subtle-alpha" | "transparent" | undefined; + size?: "small" | "medium" | undefined; + openItems?: Iterable | undefined; + defaultOpenItems?: Iterable | undefined; + onOpenChange?(event: React_2.KeyboardEvent | React_2.MouseEvent, data: TreeOpenChangeData): void; + onNavigation_unstable?(event: React_2.KeyboardEvent | React_2.MouseEvent, data: TreeNavigationData_unstable): void; +} & React_2.RefAttributes> & ((props: TreeProps) => JSX.Element); // @public (undocumented) export const treeClassNames: SlotClassNames; @@ -81,20 +97,30 @@ export type TreeContextValue = { level: number; appearance: 'subtle' | 'subtle-alpha' | 'transparent'; size: 'small' | 'medium'; - openItems: ImmutableSet; - requestOpenChange(data: TreeOpenChangeData): void; - requestNavigation(data: TreeNavigationData_unstable): void; + openItems: ImmutableSet; + requestOpenChange(data: TreeOpenChangeData): void; + requestNavigation(data: TreeNavigationData_unstable): void; }; // @public -export const TreeItem: ForwardRefComponent; +export const TreeItem: React_2.ForwardRefExoticComponent, "root"> & Omit<{ + as?: "div" | undefined; +} & Pick, HTMLDivElement>, "key" | keyof React_2.HTMLAttributes> & { + ref?: ((instance: HTMLDivElement | null) => void) | React_2.RefObject | null | undefined; +} & { + children?: React_2.ReactNode | SlotRenderFunction, HTMLDivElement>, "key" | keyof React_2.HTMLAttributes> & { + ref?: ((instance: HTMLDivElement | null) => void) | React_2.RefObject | null | undefined; + }>; +} & { + style?: TreeItemCSSProperties | undefined; +}, "ref"> & { + value?: string | undefined; + leaf?: boolean | undefined; +} & React_2.RefAttributes> & ((props: TreeItemProps) => JSX.Element); // @public (undocumented) export const treeItemClassNames: SlotClassNames; -// @public (undocumented) -export type TreeItemId = string; - // @public export const TreeItemLayout: ForwardRefComponent; @@ -143,7 +169,8 @@ export type TreeItemPersonaLayoutState = ComponentState> & { +export type TreeItemProps = ComponentProps> & { + value?: Value; leaf?: boolean; }; @@ -171,46 +198,42 @@ export type TreeItemState = ComponentState & { }; // @public (undocumented) -export type TreeNavigationData_unstable = { - event: React_2.MouseEvent; +export type TreeNavigationData_unstable = { + value: Value; target: HTMLElement; +} & ({ + event: React_2.MouseEvent; type: 'Click'; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: 'TypeAhead'; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof ArrowRight; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof ArrowLeft; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof ArrowUp; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof ArrowDown; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof Home; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof End; -}; +}); // @public (undocumented) export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event']; // @public (undocumented) -export type TreeOpenChangeData = { +export type TreeOpenChangeData = { open: boolean; + value: Value; } & ({ event: React_2.MouseEvent; target: HTMLElement; @@ -237,13 +260,13 @@ export type TreeOpenChangeData = { export type TreeOpenChangeEvent = TreeOpenChangeData['event']; // @public (undocumented) -export type TreeProps = ComponentProps & { +export type TreeProps = ComponentProps & { appearance?: 'subtle' | 'subtle-alpha' | 'transparent'; size?: 'small' | 'medium'; - openItems?: Iterable; - defaultOpenItems?: Iterable; - onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void; - onNavigation_unstable?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void; + openItems?: Iterable; + defaultOpenItems?: Iterable; + onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void; + onNavigation_unstable?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void; }; // @public (undocumented) @@ -258,7 +281,7 @@ export type TreeSlots = { export type TreeState = ComponentState & TreeContextValue; // @public -export function useFlatTree_unstable(flatTreeItemProps: FlatTreeItemProps[], options?: Pick): FlatTree; +export function useFlatTree_unstable(flatTreeItemProps: FlatTreeItemProps[], options?: Pick, 'openItems' | 'defaultOpenItems' | 'onOpenChange' | 'onNavigation_unstable'>): FlatTree; // @public export const useTree_unstable: (props: TreeProps, ref: React_2.Ref) => TreeState; @@ -270,7 +293,7 @@ export const useTreeContext_unstable: (selector: ContextSelector) => TreeItemState; +export function useTreeItem_unstable(props: TreeItemProps, ref: React_2.Ref): TreeItemState; // @public (undocumented) export const useTreeItemContext_unstable: () => TreeItemContextValue; diff --git a/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx b/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx index 17f52be40fd263..5fbe860fe3aa44 100644 --- a/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx +++ b/packages/react-components/react-tree/src/components/Tree/Tree.cy.tsx @@ -99,7 +99,7 @@ const FlatTree: React.FC = (props: TreeProps) => { return ( {Array.from(flatTree.items(), item => ( - + ))} ); diff --git a/packages/react-components/react-tree/src/components/Tree/Tree.tsx b/packages/react-components/react-tree/src/components/Tree/Tree.tsx index 77673c412fcb06..2c5a4c5e5a173a 100644 --- a/packages/react-components/react-tree/src/components/Tree/Tree.tsx +++ b/packages/react-components/react-tree/src/components/Tree/Tree.tsx @@ -14,11 +14,11 @@ import { useTreeContextValues_unstable } from './useTreeContextValues'; * an item representing a folder can be expanded to reveal the contents of the folder, * which may be files, folders, or both. */ -export const Tree: ForwardRefComponent = React.forwardRef((props, ref) => { +export const Tree = React.forwardRef((props, ref) => { const state = useTree_unstable(props, ref); useTreeStyles_unstable(state); const contextValues = useTreeContextValues_unstable(state); return renderTree_unstable(state, contextValues); -}); +}) as ForwardRefComponent & ((props: TreeProps) => JSX.Element); Tree.displayName = 'Tree'; diff --git a/packages/react-components/react-tree/src/components/Tree/Tree.types.ts b/packages/react-components/react-tree/src/components/Tree/Tree.types.ts index c087172c2e7d19..8acb9f46793dbe 100644 --- a/packages/react-components/react-tree/src/components/Tree/Tree.types.ts +++ b/packages/react-components/react-tree/src/components/Tree/Tree.types.ts @@ -2,27 +2,27 @@ import * as React from 'react'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; import { TreeContextValue } from '../../contexts/treeContext'; import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Enter, Home } from '@fluentui/keyboard-keys'; -import { TreeItemId } from '../TreeItem/TreeItem.types'; export type TreeSlots = { root: Slot<'div'>; }; // eslint-disable-next-line @typescript-eslint/naming-convention -export type TreeNavigationData_unstable = - | { event: React.MouseEvent; target: HTMLElement; type: 'Click' } - | { event: React.KeyboardEvent; target: HTMLElement; type: 'TypeAhead' } - | { event: React.KeyboardEvent; target: HTMLElement; type: typeof ArrowRight } - | { event: React.KeyboardEvent; target: HTMLElement; type: typeof ArrowLeft } - | { event: React.KeyboardEvent; target: HTMLElement; type: typeof ArrowUp } - | { event: React.KeyboardEvent; target: HTMLElement; type: typeof ArrowDown } - | { event: React.KeyboardEvent; target: HTMLElement; type: typeof Home } - | { event: React.KeyboardEvent; target: HTMLElement; type: typeof End }; +export type TreeNavigationData_unstable = { value: Value; target: HTMLElement } & ( + | { event: React.MouseEvent; type: 'Click' } + | { event: React.KeyboardEvent; type: 'TypeAhead' } + | { event: React.KeyboardEvent; type: typeof ArrowRight } + | { event: React.KeyboardEvent; type: typeof ArrowLeft } + | { event: React.KeyboardEvent; type: typeof ArrowUp } + | { event: React.KeyboardEvent; type: typeof ArrowDown } + | { event: React.KeyboardEvent; type: typeof Home } + | { event: React.KeyboardEvent; type: typeof End } +); // eslint-disable-next-line @typescript-eslint/naming-convention export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event']; -export type TreeOpenChangeData = { open: boolean } & ( +export type TreeOpenChangeData = { open: boolean; value: Value } & ( | { event: React.MouseEvent; target: HTMLElement; @@ -56,7 +56,7 @@ export type TreeContextValues = { tree: TreeContextValue; }; -export type TreeProps = ComponentProps & { +export type TreeProps = ComponentProps & { /** * A tree item can have various appearances: * - 'subtle' (default): The default tree item styles. @@ -75,13 +75,13 @@ export type TreeProps = ComponentProps & { * Controls the state of the open tree items. * These property is ignored for subtrees. */ - openItems?: Iterable; + openItems?: Iterable; /** * This refers to a list of ids of opened tree items. * Default value for the uncontrolled state of open tree items. * These property is ignored for subtrees. */ - defaultOpenItems?: Iterable; + defaultOpenItems?: Iterable; /** * Callback fired when the component changes value from open state. * These property is ignored for subtrees. @@ -90,7 +90,7 @@ export type TreeProps = ComponentProps & { * @param data - A data object with relevant information, * such as open value and type of interaction that created the event. */ - onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void; + onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void; /** * Callback fired when navigation happens inside the component. @@ -102,7 +102,7 @@ export type TreeProps = ComponentProps & { * @param data - A data object with relevant information, */ // eslint-disable-next-line @typescript-eslint/naming-convention - onNavigation_unstable?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void; + onNavigation_unstable?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void; }; /** diff --git a/packages/react-components/react-tree/src/components/TreeItem/TreeItem.tsx b/packages/react-components/react-tree/src/components/TreeItem/TreeItem.tsx index a20be498e2d237..036727d24ec97f 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/TreeItem.tsx +++ b/packages/react-components/react-tree/src/components/TreeItem/TreeItem.tsx @@ -15,15 +15,15 @@ import { useTreeItemContextValues_unstable } from './useTreeItemContextValues'; * The content and layout of a TreeItem can be defined using the TreeItemLayout or TreeItemPersonaLayout component, * which should be used as a direct child of TreeItem. * - * When a TreeItem has nsted child subtree, an expand/collapse control is displayed, + * When a TreeItem has nested child subtree, an expand/collapse control is displayed, * allowing the user to show or hide the children. */ -export const TreeItem: ForwardRefComponent = React.forwardRef((props, ref) => { +export const TreeItem = React.forwardRef((props, ref) => { const state = useTreeItem_unstable(props, ref); useTreeItemStyles_unstable(state); const contextValues = useTreeItemContextValues_unstable(state); return renderTreeItem_unstable(state, contextValues); -}); +}) as ForwardRefComponent & ((props: TreeItemProps) => JSX.Element); TreeItem.displayName = 'TreeItem'; diff --git a/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts b/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts index 6fdf026fb09560..8c3af8c0bab944 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts +++ b/packages/react-components/react-tree/src/components/TreeItem/TreeItem.types.ts @@ -5,7 +5,6 @@ import { treeItemLevelToken } from '../../utils/tokens'; import * as React from 'react'; export type TreeItemCSSProperties = React.CSSProperties & { [treeItemLevelToken]?: string | number }; -export type TreeItemId = string; export type TreeItemSlots = { root: Slot & { style?: TreeItemCSSProperties }>>; @@ -31,7 +30,8 @@ export type TreeItemContextValues = { /** * TreeItem Props */ -export type TreeItemProps = ComponentProps> & { +export type TreeItemProps = ComponentProps> & { + value?: Value; /** * If a TreeItem is a leaf, it'll not present the `expandIcon` slot by default. * This attribute is used to force the decision if a TreeItem is a leaf or not. By not providing this property diff --git a/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx b/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx index 9333370e17aa25..ea8479cd004546 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx +++ b/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx @@ -20,10 +20,16 @@ import { treeDataTypes } from '../../utils/tokens'; * @param props - props from this instance of TreeItem * @param ref - reference to root HTMLElement of TreeItem */ -export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref): TreeItemState => { +export function useTreeItem_unstable( + props: TreeItemProps, + ref: React.Ref, +): TreeItemState { const [children, subtreeChildren] = React.Children.toArray(props.children); const contextLevel = useTreeContext_unstable(ctx => ctx.level); + + const id = useId('fui-TreeItem-', props.id); + const { content, subtree, @@ -34,17 +40,16 @@ export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref ctx.requestOpenChange); const requestNavigation = useTreeContext_unstable(ctx => ctx.requestNavigation); - const id = useId('fui-TreeItem-', props.id); - const isBranch = !isLeaf; - const open = useTreeContext_unstable(ctx => isBranch && ctx.openItems.has(id)); + const open = useTreeContext_unstable(ctx => isBranch && ctx.openItems.has(value)); const { dir, targetDocument } = useFluent_unstable(); const expandIconRotation = open ? 90 : dir !== 'rtl' ? 0 : 180; @@ -54,22 +59,40 @@ export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref) => { if (!open && isBranch) { - return requestOpenChange({ event, open: true, type: treeDataTypes.arrowRight, target: event.currentTarget }); + return requestOpenChange({ + event, + value, + open: true, + type: treeDataTypes.arrowRight, + target: event.currentTarget, + }); } if (open && isBranch) { - return requestNavigation({ event, type: treeDataTypes.arrowRight, target: event.currentTarget }); + return requestNavigation({ event, value, type: treeDataTypes.arrowRight, target: event.currentTarget }); } }; const handleArrowLeft = (event: React.KeyboardEvent) => { if (open && isBranch) { - return requestOpenChange({ event, open: false, type: treeDataTypes.arrowLeft, target: event.currentTarget }); + return requestOpenChange({ + event, + value, + open: false, + type: treeDataTypes.arrowLeft, + target: event.currentTarget, + }); } if (!open && level > 1) { - return requestNavigation({ event, target: event.currentTarget, type: treeDataTypes.arrowLeft }); + return requestNavigation({ event, value, target: event.currentTarget, type: treeDataTypes.arrowLeft }); } }; const handleEnter = (event: React.KeyboardEvent) => { - requestOpenChange({ event, open: isLeaf ? open : !open, type: treeDataTypes.enter, target: event.currentTarget }); + requestOpenChange({ + event, + value, + open: isLeaf ? open : !open, + type: treeDataTypes.enter, + target: event.currentTarget, + }); }; const handleClick = useEventCallback((event: React.MouseEvent) => { @@ -86,11 +109,12 @@ export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref) => { @@ -109,18 +133,18 @@ export const useTreeItem_unstable = (props: TreeItemProps, ref: React.Ref; + openItems: ImmutableSet; /** * Requests dialog main component to update it's internal open state */ - requestOpenChange(data: TreeOpenChangeData): void; - requestNavigation(data: TreeNavigationData_unstable): void; + requestOpenChange(data: TreeOpenChangeData): void; + requestNavigation(data: TreeNavigationData_unstable): void; }; const defaultContextValue: TreeContextValue = { diff --git a/packages/react-components/react-tree/src/hooks/useFlatTree.ts b/packages/react-components/react-tree/src/hooks/useFlatTree.ts index 0a84a703c3d701..6c125c1fd570e4 100644 --- a/packages/react-components/react-tree/src/hooks/useFlatTree.ts +++ b/packages/react-components/react-tree/src/hooks/useFlatTree.ts @@ -11,32 +11,34 @@ import type { TreeOpenChangeEvent, TreeProps, } from '../Tree'; -import type { TreeItemId, TreeItemProps } from '../TreeItem'; +import type { TreeItemProps } from '../TreeItem'; -export type FlatTreeItemProps = TreeItemProps & { - id: TreeItemId; - parentId?: string; +export type FlatTreeItemProps = Omit & { + value: Value; + parentValue?: Value; }; -export type FlatTreeItem = Readonly; +export type FlatTreeItem = Readonly>; /** * @internal * Used internally on createFlatTreeItems and VisibleFlatTreeItemGenerator * to ensure required properties when building a FlatTreeITem */ -export type MutableFlatTreeItem = { - parentId?: string; +export type MutableFlatTreeItem = { + parentValue?: Value; childrenSize: number; index: number; - id: string; + value: Value; level: number; - getTreeItemProps(): Required> & - TreeItemProps; + getTreeItemProps(): Required< + Pick, 'value' | 'aria-setsize' | 'aria-level' | 'aria-posinset' | 'leaf'> + > & + TreeItemProps; }; -export type FlatTreeProps = Required< - Pick & { ref: React.Ref } +export type FlatTreeProps = Required< + Pick, 'openItems' | 'onOpenChange' | 'onNavigation_unstable'> & { ref: React.Ref } >; /** @@ -49,13 +51,13 @@ export type FlatTreeProps = Required< * * On simple scenarios it is advised to simply use a nested structure instead. */ -export type FlatTree = { +export type FlatTree = { /** * returns the properties required for the Tree component to work properly. * That includes: * `openItems`, `onOpenChange`, `onNavigation_unstable` and `ref` */ - getTreeProps(): FlatTreeProps; + getTreeProps(): FlatTreeProps; /** * internal method used to react to an `onNavigation` event. * This method ensures proper navigation on keyboard and mouse interaction. @@ -79,7 +81,7 @@ export type FlatTree = { * }; *``` */ - navigate(data: TreeNavigationData_unstable): void; + navigate(data: TreeNavigationData_unstable): void; /** * returns next item to be focused on a navigation. * This method is provided to decouple the element that needs to be focused from @@ -87,11 +89,14 @@ export type FlatTree = { * * On the case of TypeAhead navigation this method returns the current item. */ - getNextNavigableItem(visibleItems: FlatTreeItem[], data: TreeNavigationData_unstable): FlatTreeItem | undefined; + getNextNavigableItem( + visibleItems: FlatTreeItem[], + data: TreeNavigationData_unstable, + ): FlatTreeItem | undefined; /** * an iterable containing all visually available flat tree items */ - items(): IterableIterator; + items(): IterableIterator>; }; /** @@ -106,15 +111,15 @@ export type FlatTree = { * @param flatTreeItemProps - a list of tree items * @param options - in case control over the internal openItems is required */ -export function useFlatTree_unstable( - flatTreeItemProps: FlatTreeItemProps[], - options: Pick = {}, -): FlatTree { +export function useFlatTree_unstable( + flatTreeItemProps: FlatTreeItemProps[], + options: Pick, 'openItems' | 'defaultOpenItems' | 'onOpenChange' | 'onNavigation_unstable'> = {}, +): FlatTree { const [openItems, updateOpenItems] = useOpenItemsState(options); const flatTreeItems = React.useMemo(() => createFlatTreeItems(flatTreeItemProps), [flatTreeItemProps]); const [navigate, navigationRef] = useFlatTreeNavigation(flatTreeItems); - const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { + const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { options.onOpenChange?.(event, data); if (!event.isDefaultPrevented()) { updateOpenItems(data); @@ -123,7 +128,7 @@ export function useFlatTree_unstable( }); const handleNavigation = useEventCallback( - (event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => { + (event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => { options.onNavigation_unstable?.(event, data); if (!event.isDefaultPrevented()) { navigate(data); @@ -132,27 +137,29 @@ export function useFlatTree_unstable( }, ); - const getNextNavigableItem = useEventCallback((visibleItems: FlatTreeItem[], data: TreeNavigationData_unstable) => { - const item = flatTreeItems.get(data.target.id); - if (item) { - switch (data.type) { - case treeDataTypes.typeAhead: - return item; - case treeDataTypes.arrowLeft: - return flatTreeItems.get(item.parentId!); - case treeDataTypes.arrowRight: - return visibleItems[item.index + 1]; - case treeDataTypes.end: - return visibleItems[visibleItems.length - 1]; - case treeDataTypes.home: - return visibleItems[0]; - case treeDataTypes.arrowDown: - return visibleItems[item.index + 1]; - case treeDataTypes.arrowUp: - return visibleItems[item.index - 1]; + const getNextNavigableItem = useEventCallback( + (visibleItems: FlatTreeItem[], data: TreeNavigationData_unstable) => { + const item = flatTreeItems.get(data.value); + if (item) { + switch (data.type) { + case treeDataTypes.typeAhead: + return item; + case treeDataTypes.arrowLeft: + return flatTreeItems.get(item.parentValue!); + case treeDataTypes.arrowRight: + return visibleItems[item.index + 1]; + case treeDataTypes.end: + return visibleItems[visibleItems.length - 1]; + case treeDataTypes.home: + return visibleItems[0]; + case treeDataTypes.arrowDown: + return visibleItems[item.index + 1]; + case treeDataTypes.arrowUp: + return visibleItems[item.index - 1]; + } } - } - }); + }, + ); const getTreeProps = React.useCallback( () => ({ @@ -167,7 +174,7 @@ export function useFlatTree_unstable( ); const items = React.useCallback( - () => VisibleFlatTreeItemGenerator(openItems, flatTreeItems), + () => VisibleFlatTreeItemGenerator(openItems, flatTreeItems), [openItems, flatTreeItems], ); diff --git a/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts b/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts index e0a551dce368eb..79eb5773f1d5c8 100644 --- a/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts +++ b/packages/react-components/react-tree/src/hooks/useFlatTreeNavigation.ts @@ -8,12 +8,12 @@ import { treeItemFilter } from '../utils/treeItemFilter'; import { HTMLElementWalker, useHTMLElementWalkerRef } from './useHTMLElementWalker'; import { useRovingTabIndex } from './useRovingTabIndexes'; -export function useFlatTreeNavigation(flatTreeItems: FlatTreeItems) { +export function useFlatTreeNavigation(flatTreeItems: FlatTreeItems) { const { targetDocument } = useFluent_unstable(); const [treeItemWalkerRef, treeItemWalkerRootRef] = useHTMLElementWalkerRef(treeItemFilter); const [{ rove }, rovingRootRef] = useRovingTabIndex(treeItemFilter); - function getNextElement(data: TreeNavigationData_unstable) { + function getNextElement(data: TreeNavigationData_unstable) { if (!targetDocument || !treeItemWalkerRef.current) { return null; } @@ -25,7 +25,7 @@ export function useFlatTreeNavigation(flatTreeItems: FlatTreeItems) { treeItemWalker.currentElement = data.target; return nextTypeAheadElement(treeItemWalker, data.event.key); case treeDataTypes.arrowLeft: - return parentElement(flatTreeItems, data.target, targetDocument); + return parentElement(flatTreeItems, data.value, targetDocument); case treeDataTypes.arrowRight: treeItemWalker.currentElement = data.target; return firstChild(data.target, treeItemWalker); @@ -43,7 +43,7 @@ export function useFlatTreeNavigation(flatTreeItems: FlatTreeItems) { return treeItemWalker.previousElement(); } } - const navigate = useEventCallback((data: TreeNavigationData_unstable) => { + const navigate = useEventCallback((data: TreeNavigationData_unstable) => { const nextElement = getNextElement(data); if (nextElement) { rove(nextElement); @@ -66,10 +66,13 @@ function firstChild(target: HTMLElement, treeWalker: HTMLElementWalker): HTMLEle return null; } -function parentElement(flatTreeItems: FlatTreeItems, target: HTMLElement, document: Document) { - const flatTreeItem = flatTreeItems.get(target.id); - if (flatTreeItem && flatTreeItem.parentId) { - return document.getElementById(flatTreeItem.parentId); +function parentElement(flatTreeItems: FlatTreeItems, value: Value, document: Document) { + const flatTreeItem = flatTreeItems.get(value); + if (flatTreeItem && flatTreeItem.parentValue) { + const parentId = flatTreeItems.get(flatTreeItem.parentValue)?.getTreeItemProps().id; + if (parentId) { + return document.getElementById(parentId); + } } return null; } diff --git a/packages/react-components/react-tree/src/hooks/useNestedTreeNavigation.ts b/packages/react-components/react-tree/src/hooks/useNestedTreeNavigation.ts index ae5dbc81272887..59375d68a8eaac 100644 --- a/packages/react-components/react-tree/src/hooks/useNestedTreeNavigation.ts +++ b/packages/react-components/react-tree/src/hooks/useNestedTreeNavigation.ts @@ -1,10 +1,10 @@ +import { useMergedRefs } from '@fluentui/react-utilities'; import { TreeNavigationData_unstable } from '../Tree'; import { HTMLElementWalker, useHTMLElementWalkerRef } from './useHTMLElementWalker'; import { nextTypeAheadElement } from '../utils/nextTypeAheadElement'; import { treeDataTypes } from '../utils/tokens'; import { treeItemFilter } from '../utils/treeItemFilter'; import { useRovingTabIndex } from './useRovingTabIndexes'; -import { useMergedRefs } from '@fluentui/react-utilities'; export function useNestedTreeNavigation() { const [{ rove }, rovingRootRef] = useRovingTabIndex(treeItemFilter); diff --git a/packages/react-components/react-tree/src/hooks/useOpenItemsState.ts b/packages/react-components/react-tree/src/hooks/useOpenItemsState.ts index 7f06a5342bb764..2dbb3990aac2ee 100644 --- a/packages/react-components/react-tree/src/hooks/useOpenItemsState.ts +++ b/packages/react-components/react-tree/src/hooks/useOpenItemsState.ts @@ -1,10 +1,9 @@ import { useControllableState, useEventCallback } from '@fluentui/react-utilities'; import * as React from 'react'; import { createImmutableSet, emptyImmutableSet, ImmutableSet } from '../utils/ImmutableSet'; -import type { TreeItemId } from '../TreeItem'; import type { TreeOpenChangeData, TreeProps } from '../Tree'; -export function useOpenItemsState(props: Pick) { +export function useOpenItemsState(props: Pick, 'openItems' | 'defaultOpenItems'>) { const [openItems, setOpenItems] = useControllableState({ state: React.useMemo(() => props.openItems && createImmutableSet(props.openItems), [props.openItems]), defaultState: React.useMemo( @@ -13,21 +12,20 @@ export function useOpenItemsState(props: Pick + const updateOpenItems = useEventCallback((data: TreeOpenChangeData) => setOpenItems(currentOpenItems => createNextOpenItems(data, currentOpenItems)), ); return [openItems, updateOpenItems] as const; } -function createNextOpenItems( - data: TreeOpenChangeData, - previousOpenItems: ImmutableSet, -): ImmutableSet { - const id = data.target.id; - const previousOpenItemsHasId = previousOpenItems.has(id); +function createNextOpenItems( + data: TreeOpenChangeData, + previousOpenItems: ImmutableSet, +): ImmutableSet { + const previousOpenItemsHasId = previousOpenItems.has(data.value); if (data.open ? previousOpenItemsHasId : !previousOpenItemsHasId) { return previousOpenItems; } const nextOpenItems = createImmutableSet(previousOpenItems); - return data.open ? nextOpenItems.add(id) : nextOpenItems.delete(id); + return data.open ? nextOpenItems.add(data.value) : nextOpenItems.delete(data.value); } diff --git a/packages/react-components/react-tree/src/index.ts b/packages/react-components/react-tree/src/index.ts index ec8788f87ab35d..c1111982a9c805 100644 --- a/packages/react-components/react-tree/src/index.ts +++ b/packages/react-components/react-tree/src/index.ts @@ -28,7 +28,7 @@ export { useTreeItemStyles_unstable, useTreeItem_unstable, } from './TreeItem'; -export type { TreeItemId, TreeItemProps, TreeItemState, TreeItemSlots } from './TreeItem'; +export type { TreeItemProps, TreeItemState, TreeItemSlots } from './TreeItem'; export { TreeItemLayout, diff --git a/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts b/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts index db8b35daaa1976..b04f9a6b635d08 100644 --- a/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts +++ b/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts @@ -1,32 +1,33 @@ -import type { TreeItemId } from '../TreeItem'; import type { ImmutableSet } from './ImmutableSet'; import type { FlatTreeItem, FlatTreeItemProps, MutableFlatTreeItem } from '../hooks/useFlatTree'; /** * @internal */ -export type FlatTreeItems = { +export type FlatTreeItems = { size: number; - root: FlatTreeItem; - get(id: string): FlatTreeItem | undefined; - set(id: string, value: FlatTreeItem): void; - getByIndex(index: number): FlatTreeItem; + root: FlatTreeItem; + get(key: Value): FlatTreeItem | undefined; + set(key: Value, value: FlatTreeItem): void; + getByIndex(index: number): FlatTreeItem; }; /** * creates a list of flat tree items * and provides a map to access each item by id */ -export function createFlatTreeItems(flatTreeItemProps: FlatTreeItemProps[]): FlatTreeItems { - const root = createFlatTreeRootItem(); - const itemsPerId = new Map([[flatTreeRootId, root]]); - const items: MutableFlatTreeItem[] = []; +export function createFlatTreeItems( + flatTreeItemProps: FlatTreeItemProps[], +): FlatTreeItems { + const root = createFlatTreeRootItem(); + const itemsPerValue = new Map>([[flatTreeRootId as Value, root]]); + const items: MutableFlatTreeItem[] = []; for (let index = 0; index < flatTreeItemProps.length; index++) { - const { parentId = flatTreeRootId, ...treeItemProps } = flatTreeItemProps[index]; + const { parentValue = flatTreeRootId as Value, ...treeItemProps } = flatTreeItemProps[index]; - const nextItemProps: FlatTreeItemProps | undefined = flatTreeItemProps[index + 1]; - const currentParent = itemsPerId.get(parentId); + const nextItemProps: FlatTreeItemProps | undefined = flatTreeItemProps[index + 1]; + const currentParent = itemsPerValue.get(parentValue); if (!currentParent) { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line no-console @@ -36,12 +37,12 @@ export function createFlatTreeItems(flatTreeItemProps: FlatTreeItemProps[]): Fla } break; } - const isLeaf = nextItemProps?.parentId !== treeItemProps.id; + const isLeaf = nextItemProps?.parentValue !== treeItemProps.value; const currentLevel = (currentParent.level ?? 0) + 1; const currentChildrenSize = ++currentParent.childrenSize; - const flatTreeItem: FlatTreeItem = { - id: treeItemProps.id, + const flatTreeItem: FlatTreeItem = { + value: treeItemProps.value, getTreeItemProps: () => ({ ...treeItemProps, 'aria-level': currentLevel, @@ -50,11 +51,11 @@ export function createFlatTreeItems(flatTreeItemProps: FlatTreeItemProps[]): Fla leaf: isLeaf, }), level: currentLevel, - parentId, + parentValue, childrenSize: 0, index: -1, }; - itemsPerId.set(flatTreeItem.id, flatTreeItem); + itemsPerValue.set(flatTreeItem.value, flatTreeItem); items.push(flatTreeItem); } @@ -62,22 +63,22 @@ export function createFlatTreeItems(flatTreeItemProps: FlatTreeItemProps[]): Fla root, size: items.length, getByIndex: index => items[index], - get: id => itemsPerId.get(id), - set: (id, value) => itemsPerId.set(id, value), + get: id => itemsPerValue.get(id), + set: (id, value) => itemsPerValue.set(id, value), }; } -export const flatTreeRootId = '__fuiFlatTreeRoot'; +export const flatTreeRootId = '__fuiFlatTreeRoot' as unknown; -function createFlatTreeRootItem(): FlatTreeItem { +function createFlatTreeRootItem(): FlatTreeItem { return { - id: flatTreeRootId, + value: flatTreeRootId as Value, getTreeItemProps: () => { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.error('useFlatTree: internal error, trying to access treeitem props from invalid root element'); } - return { id: flatTreeRootId, 'aria-setsize': -1, 'aria-level': -1, 'aria-posinset': -1, leaf: true }; + return { value: flatTreeRootId as Value, 'aria-setsize': -1, 'aria-level': -1, 'aria-posinset': -1, leaf: true }; }, childrenSize: 0, get index() { @@ -92,10 +93,13 @@ function createFlatTreeRootItem(): FlatTreeItem { } // eslint-disable-next-line @typescript-eslint/naming-convention -export function* VisibleFlatTreeItemGenerator(openItems: ImmutableSet, flatTreeItems: FlatTreeItems) { +export function* VisibleFlatTreeItemGenerator( + openItems: ImmutableSet, + flatTreeItems: FlatTreeItems, +) { for (let index = 0, visibleIndex = 0; index < flatTreeItems.size; index++) { - const item: MutableFlatTreeItem = flatTreeItems.getByIndex(index); - const parent = item.parentId ? flatTreeItems.get(item.parentId) ?? flatTreeItems.root : flatTreeItems.root; + const item: MutableFlatTreeItem = flatTreeItems.getByIndex(index); + const parent = item.parentValue ? flatTreeItems.get(item.parentValue) ?? flatTreeItems.root : flatTreeItems.root; if (isItemVisible(item, openItems, flatTreeItems)) { item.index = visibleIndex++; yield item; @@ -105,15 +109,19 @@ export function* VisibleFlatTreeItemGenerator(openItems: ImmutableSet, flatTreeItems: FlatTreeItems) { +function isItemVisible( + item: FlatTreeItem, + openItems: ImmutableSet, + flatTreeItems: FlatTreeItems, +) { if (item.level === 1) { return true; } - while (item.parentId && item.parentId !== flatTreeItems.root.id) { - if (!openItems.has(item.parentId)) { + while (item.parentValue && item.parentValue !== flatTreeItems.root.value) { + if (!openItems.has(item.parentValue)) { return false; } - const parent = flatTreeItems.get(item.parentId); + const parent = flatTreeItems.get(item.parentValue); if (!parent) { return false; } diff --git a/packages/react-components/react-tree/src/utils/flattenTree.ts b/packages/react-components/react-tree/src/utils/flattenTree.ts index 3a609b6692f2c1..d41b65ea8ce39c 100644 --- a/packages/react-components/react-tree/src/utils/flattenTree.ts +++ b/packages/react-components/react-tree/src/utils/flattenTree.ts @@ -2,19 +2,24 @@ import * as React from 'react'; import { FlatTreeItemProps } from '../hooks/useFlatTree'; import { TreeItemProps } from '../TreeItem'; -export type NestedTreeItem = Omit & { - subtree?: NestedTreeItem[]; +export type NestedTreeItem = Omit, 'subtree'> & { + subtree?: NestedTreeItem[]; }; let count = 1; -function flattenTreeRecursive(items: NestedTreeItem[], parent?: FlatTreeItemProps, level = 1): FlatTreeItemProps[] { - return items.reduce((acc, { subtree, ...item }, index) => { - const flatTreeItem: FlatTreeItemProps = { +function flattenTreeRecursive( + items: NestedTreeItem[], + parent?: FlatTreeItemProps, + level = 1, +): FlatTreeItemProps[] { + return items.reduce[]>((acc, { subtree, ...item }, index) => { + const id = item.id ?? `fui-FlatTreeItem-${count++}`; + const flatTreeItem: FlatTreeItemProps = { 'aria-level': level, 'aria-posinset': index + 1, 'aria-setsize': items.length, - parentId: parent?.id, - id: item.id ?? `fui-FlatTreeItem-${count++}`, + parentValue: parent?.value, + value: item.value ?? (id as unknown as Value), leaf: subtree === undefined, ...item, }; @@ -67,30 +72,32 @@ function flattenTreeRecursive(items: NestedTreeItem[], parent?: FlatTreeItemProp * ``` */ // eslint-disable-next-line @typescript-eslint/naming-convention -export const flattenTree_unstable = (items: NestedTreeItem[]): FlatTreeItemProps[] => flattenTreeRecursive(items); +export const flattenTree_unstable = (items: NestedTreeItem[]): FlatTreeItemProps[] => + flattenTreeRecursive(items); /** * @internal */ -export const flattenTreeFromElement = ( +export const flattenTreeFromElement = ( root: React.ReactElement<{ - children?: React.ReactElement | React.ReactElement[]; + children?: React.ReactElement> | React.ReactElement>[]; }>, - parent?: FlatTreeItemProps, + parent?: FlatTreeItemProps, level = 1, -): FlatTreeItemProps[] => { - const children = React.Children.toArray(root.props.children) as React.ReactElement[]; - return children.reduce((acc, curr, index) => { +): FlatTreeItemProps[] => { + const children = React.Children.toArray(root.props.children) as React.ReactElement>[]; + return children.reduce[]>((acc, curr, index) => { const [content, subtree] = React.Children.toArray(curr.props.children) as [ React.ReactNode, typeof root | undefined, ]; - const flatTreeItem: FlatTreeItemProps = { + const id = curr.props.id ?? `fui-FlatTreeItem-${count++}`; + const flatTreeItem: FlatTreeItemProps = { 'aria-level': level, 'aria-posinset': index + 1, 'aria-setsize': children.length, - parentId: parent?.id, - id: curr.props.id ?? `fui-FlatTreeItem-${count++}`, + parentValue: parent?.value, + value: curr.props.value ?? (id as unknown as Value), leaf: subtree === undefined, ...curr.props, children: content, diff --git a/packages/react-components/react-tree/stories/Tree/TreeControllingOpenAndClose.stories.tsx b/packages/react-components/react-tree/stories/Tree/TreeControllingOpenAndClose.stories.tsx index b8b92a732088a5..00fa984bb36140 100644 --- a/packages/react-components/react-tree/stories/Tree/TreeControllingOpenAndClose.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/TreeControllingOpenAndClose.stories.tsx @@ -1,24 +1,15 @@ import * as React from 'react'; -import { - Tree, - TreeItem, - TreeItemLayout, - TreeItemId, - TreeOpenChangeData, - TreeOpenChangeEvent, -} from '@fluentui/react-tree'; +import { Tree, TreeItem, TreeItemLayout, TreeOpenChangeData, TreeOpenChangeEvent } from '@fluentui/react-tree'; import story from './TreeControllingOpenAndClose.md'; export const OpenItemsControlled = () => { - const [openItems, setOpenItems] = React.useState([]); - const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { - setOpenItems(curr => - data.open ? [...curr, event.currentTarget.id] : curr.filter(id => id !== event.currentTarget.id), - ); + const [openItems, setOpenItems] = React.useState([]); + const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { + setOpenItems(curr => (data.open ? [...curr, data.value] : curr.filter(value => value !== data.value))); }; return ( - + level 1, item 1 @@ -32,10 +23,10 @@ export const OpenItemsControlled = () => { - + level 1, item 2 - + level 2, item 1 diff --git a/packages/react-components/react-tree/stories/Tree/TreeDefaultOpenTrees.stories.tsx b/packages/react-components/react-tree/stories/Tree/TreeDefaultOpenTrees.stories.tsx index fa3dd1181a7d41..e3955b04a0ab0e 100644 --- a/packages/react-components/react-tree/stories/Tree/TreeDefaultOpenTrees.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/TreeDefaultOpenTrees.stories.tsx @@ -7,7 +7,7 @@ export const DefaultOpenTrees = () => { return ( - + level 1, item 1 @@ -21,10 +21,10 @@ export const DefaultOpenTrees = () => { - + level 1, item 2 - + level 2, item 1 diff --git a/packages/react-components/react-tree/stories/Tree/Virtualization.stories.tsx b/packages/react-components/react-tree/stories/Tree/Virtualization.stories.tsx index b3aa74eb059529..cefd20fde42133 100644 --- a/packages/react-components/react-tree/stories/Tree/Virtualization.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/Virtualization.stories.tsx @@ -18,23 +18,27 @@ import { FixedSizeList, FixedSizeListProps, ListChildComponentProps } from 'reac import { ForwardRefComponent, getSlots } from '@fluentui/react-components'; import story from './Virtualization.md'; -const defaultItems: FlatTreeItemProps[] = [ +const defaultItems: FlatTreeItemProps[] = [ { id: 'flatTreeItem_lvl-1_item-1', + value: 'flatTreeItem_lvl-1_item-1', children: Level 1, item 1, }, ...Array.from({ length: 300 }, (_, i) => ({ id: `flatTreeItem_lvl-1_item-1--child:${i}`, - parentId: 'flatTreeItem_lvl-1_item-1', + value: `flatTreeItem_lvl-1_item-1--child:${i}`, + parentValue: 'flatTreeItem_lvl-1_item-1', children: Item {i + 1}, })), { id: 'flatTreeItem_lvl-1_item-2', + value: 'flatTreeItem_lvl-1_item-2', children: Level 1, item 2, }, ...Array.from({ length: 300 }, (_, index) => ({ id: `flatTreeItem_lvl-1_item-2--child:${index}`, - parentId: 'flatTreeItem_lvl-1_item-2', + value: `flatTreeItem_lvl-1_item-2--child:${index}`, + parentValue: 'flatTreeItem_lvl-1_item-2', children: Item {index + 1}, })), ]; @@ -81,7 +85,7 @@ export const Virtualization = () => { if (!nextItem) { return; } - if (document.getElementById(nextItem.id)) { + if (!document.getElementById(nextItem.value)) { listRef.current?.scrollToItem(nextItem.index); return requestAnimationFrame(() => flatTree.navigate(data)); } diff --git a/packages/react-components/react-tree/stories/Tree/flattenTree.stories.tsx b/packages/react-components/react-tree/stories/Tree/flattenTree.stories.tsx index 60013bf4ba636d..06a9906742cc35 100644 --- a/packages/react-components/react-tree/stories/Tree/flattenTree.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/flattenTree.stories.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Tree, TreeItem, TreeItemLayout, useFlatTree_unstable, flattenTree_unstable } from '@fluentui/react-tree'; import story from './flattenTree.md'; -const defaultItems = flattenTree_unstable([ +const defaultItems = flattenTree_unstable([ { children: level 1, item 1, subtree: [ @@ -38,11 +38,11 @@ const defaultItems = flattenTree_unstable([ ]); export const FlattenTree = () => { - const flatTree = useFlatTree_unstable(defaultItems); + const flatTree = useFlatTree_unstable(defaultItems); return ( {Array.from(flatTree.items(), item => ( - + ))} ); diff --git a/packages/react-components/react-tree/stories/Tree/useFlatTree.stories.tsx b/packages/react-components/react-tree/stories/Tree/useFlatTree.stories.tsx index b851b0934ab0b4..89edcaa09579bc 100644 --- a/packages/react-components/react-tree/stories/Tree/useFlatTree.stories.tsx +++ b/packages/react-components/react-tree/stories/Tree/useFlatTree.stories.tsx @@ -4,60 +4,60 @@ import story from './useFlatTree.md'; const defaultItems: FlatTreeItemProps[] = [ { - id: '1', + value: '1', children: Level 1, item 1, }, { - id: '1-1', - parentId: '1', + value: '1-1', + parentValue: '1', children: Level 2, item 1, }, { - id: '1-2', - parentId: '1', + value: '1-2', + parentValue: '1', children: Level 2, item 2, }, { - id: '1-3', - parentId: '1', + value: '1-3', + parentValue: '1', children: Level 2, item 3, }, { - id: '2', + value: '2', children: Level 1, item 2, }, { - id: '2-1', - parentId: '2', + value: '2-1', + parentValue: '2', children: Level 2, item 1, }, { - id: '2-1-1', - parentId: '2-1', + value: '2-1-1', + parentValue: '2-1', children: Level 3, item 1, }, { - id: '2-2', - parentId: '2', + value: '2-2', + parentValue: '2', children: Level 2, item 2, }, { - id: '2-2-1', - parentId: '2-2', + value: '2-2-1', + parentValue: '2-2', children: Level 3, item 1, }, { - id: '2-2-2', - parentId: '2-2', + value: '2-2-2', + parentValue: '2-2', children: Level 3, item 2, }, { - id: '2-2-3', - parentId: '2-2', + value: '2-2-3', + parentValue: '2-2', children: Level 3, item 3, }, { - id: '3', + value: '3', children: Level 1, item 3, }, ]; @@ -68,7 +68,7 @@ export const UseFlatTree = () => { return ( {Array.from(flatTree.items(), flatTreeItem => ( - + ))} ); diff --git a/packages/react-components/react-tree/stories/TreeItem/TreeItemAddRemove.stories.tsx b/packages/react-components/react-tree/stories/TreeItem/TreeItemAddRemove.stories.tsx index d6ecc40efd063d..ede8cbcd248d49 100644 --- a/packages/react-components/react-tree/stories/TreeItem/TreeItemAddRemove.stories.tsx +++ b/packages/react-components/react-tree/stories/TreeItem/TreeItemAddRemove.stories.tsx @@ -14,13 +14,13 @@ import story from './TreeItemAddRemove.md'; const defaultSubTrees: FlatTreeItemProps[][] = [ [ - { id: '1', children: Level 1, item 1 }, - { id: '1-1', parentId: '1', children: Item 1-1 }, - { id: '1-2', parentId: '1', children: Item 1-2 }, + { value: '1', children: Level 1, item 1 }, + { value: '1-1', parentValue: '1', children: Item 1-1 }, + { value: '1-2', parentValue: '1', children: Item 1-2 }, ], [ - { id: '2', children: Level 1, item 2 }, - { id: '2-1', parentId: '2', children: Item 2-1 }, + { value: '2', children: Level 1, item 2 }, + { value: '2-1', parentValue: '2', children: Item 2-1 }, ], ]; @@ -28,47 +28,47 @@ export const AddRemoveTreeItem = () => { const [trees, setTrees] = React.useState(defaultSubTrees); const handleOpenChange = (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => { - if (data.target.id.endsWith('-btn')) { - setTrees(currentTrees => { - const subtreeIndex = Number(data.target.id[0]) - 1; - const lastItem = currentTrees[subtreeIndex][currentTrees[subtreeIndex].length - 1]; - const newItemId = `${subtreeIndex + 1}-${Number(lastItem.id.slice(2)) + 1}`; - const nextSubTree = [ - ...currentTrees[subtreeIndex], - { - id: newItemId, - parentId: currentTrees[subtreeIndex][0].id, - children: New item {newItemId}, - }, - ]; - return [...currentTrees.slice(0, subtreeIndex), nextSubTree, ...currentTrees.slice(subtreeIndex + 1)]; - }); + if (data.value.endsWith('-btn')) { + const subtreeIndex = Number(data.value[0]) - 1; + addFlatTreeItem(subtreeIndex); } }; - const removeFlatTreeItem = (id: string) => { - const subtreeIndex = Number(id[0]) - 1; - const nextSubTree = trees[subtreeIndex].filter(item => item.id !== id); - setTrees(currentTrees => [ - ...currentTrees.slice(0, subtreeIndex), - nextSubTree, - ...currentTrees.slice(subtreeIndex + 1), - ]); - }; + const addFlatTreeItem = (subtreeIndex: number) => + setTrees(currentTrees => { + const lastItem = currentTrees[subtreeIndex][currentTrees[subtreeIndex].length - 1]; + const newItemValue = `${subtreeIndex + 1}-${Number(lastItem.value.slice(2)) + 1}`; + const nextSubTree: FlatTreeItemProps[] = [ + ...currentTrees[subtreeIndex], + { + value: newItemValue, + parentValue: currentTrees[subtreeIndex][0].value, + children: New item {newItemValue}, + }, + ]; + return [...currentTrees.slice(0, subtreeIndex), nextSubTree, ...currentTrees.slice(subtreeIndex + 1)]; + }); + + const removeFlatTreeItem = (value: string) => + setTrees(currentTrees => { + const subtreeIndex = Number(value[0]) - 1; + const nextSubTree = trees[subtreeIndex].filter(item => item.value !== value); + return [...currentTrees.slice(0, subtreeIndex), nextSubTree, ...currentTrees.slice(subtreeIndex + 1)]; + }); const flatTree = useFlatTree_unstable( React.useMemo( () => [ ...trees[0], { - id: '1-btn', - parentId: '1', + value: '1-btn', + parentValue: '1', children: Add new item, }, ...trees[1], { - id: '2-btn', - parentId: '2', + value: '2-btn', + parentValue: '2', children: Add new item, }, ], @@ -80,17 +80,17 @@ export const AddRemoveTreeItem = () => { return ( {Array.from(flatTree.items(), item => { - const isUndeletable = item.level === 1 || item.id.endsWith('-btn'); + const isUndeletable = item.level === 1 || item.value.endsWith('-btn'); return ( removeFlatTreeItem(item.id)} + onClick={() => removeFlatTreeItem(item.value)} icon={} /> ) diff --git a/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandCollapseIconOnly.stories.tsx b/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandCollapseIconOnly.stories.tsx index e03f1cd330e98f..c4683b50de7aa7 100644 --- a/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandCollapseIconOnly.stories.tsx +++ b/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandCollapseIconOnly.stories.tsx @@ -14,7 +14,7 @@ export const ExpandCollapseIconOnly = () => { return ( - + level 1, item 1 @@ -28,10 +28,10 @@ export const ExpandCollapseIconOnly = () => { - + level 1, item 2 - + level 2, item 1 diff --git a/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandIcon.stories.tsx b/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandIcon.stories.tsx index 49acbe0d497aec..794015aeb12e69 100644 --- a/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandIcon.stories.tsx +++ b/packages/react-components/react-tree/stories/TreeItem/TreeItemExpandIcon.stories.tsx @@ -6,15 +6,13 @@ import story from './TreeItemExpandIcon.md'; export const ExpandIcon = () => { const [openItems, setOpenItems] = React.useState([]); - const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { - setOpenItems(curr => - data.open ? [...curr, event.currentTarget.id] : curr.filter(id => id !== event.currentTarget.id), - ); + const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { + setOpenItems(curr => (data.open ? [...curr, data.value] : curr.filter(value => value !== data.value))); }; return ( : } > level 1, item 1 @@ -31,13 +29,13 @@ export const ExpandIcon = () => { : } > level 1, item 2 : } > level 2, item 1