diff --git a/change/@fluentui-react-tree-aeacc4f5-82c2-4e59-b8bb-a1a28a912247.json b/change/@fluentui-react-tree-aeacc4f5-82c2-4e59-b8bb-a1a28a912247.json new file mode 100644 index 00000000000000..0507897ffb72dd --- /dev/null +++ b/change/@fluentui-react-tree-aeacc4f5-82c2-4e59-b8bb-a1a28a912247.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "chore: openItems property added to TreeOpenChangeData + minor internal improvements", + "packageName": "@fluentui/react-tree", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} 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 30ece2cf07a640..8580af0363e4fb 100644 --- a/packages/react-components/react-tree/etc/react-tree.api.md +++ b/packages/react-components/react-tree/etc/react-tree.api.md @@ -6,22 +6,22 @@ /// -import { ArrowDown } from '@fluentui/keyboard-keys'; -import { ArrowLeft } from '@fluentui/keyboard-keys'; -import { ArrowRight } from '@fluentui/keyboard-keys'; -import { ArrowUp } from '@fluentui/keyboard-keys'; +import type { ArrowDown } from '@fluentui/keyboard-keys'; +import type { ArrowLeft } from '@fluentui/keyboard-keys'; +import type { ArrowRight } from '@fluentui/keyboard-keys'; +import type { ArrowUp } from '@fluentui/keyboard-keys'; import type { AvatarContextValue } from '@fluentui/react-avatar'; import type { AvatarSize } from '@fluentui/react-avatar'; import { ButtonContextValue } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import { ContextSelector } from '@fluentui/react-context-selector'; -import { End } from '@fluentui/keyboard-keys'; -import { Enter } from '@fluentui/keyboard-keys'; +import type { End } from '@fluentui/keyboard-keys'; +import type { Enter } from '@fluentui/keyboard-keys'; import type { ExtractSlotProps } from '@fluentui/react-utilities'; import { FC } from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; -import { Home } from '@fluentui/keyboard-keys'; +import type { Home } from '@fluentui/keyboard-keys'; import { Provider } from 'react'; import { ProviderProps } from 'react'; import * as React_2 from 'react'; @@ -51,7 +51,7 @@ export type FlatTreeItem = }; // @public (undocumented) -export type FlatTreeItemProps = Omit & Partial> & { +export type FlatTreeItemProps = Omit & Partial> & { value: TreeItemValue; parentValue?: TreeItemValue; }; @@ -59,11 +59,12 @@ export type FlatTreeItemProps = Omit & Partial> & { ref: React_2.Ref; - openItems: ImmutableSet; + openItems: ImmutableSet; }; // @public (undocumented) export type NestedTreeItem = Omit & { + value: TreeItemValue; subtree?: NestedTreeItem[]; }; @@ -90,7 +91,7 @@ export type TreeContextValue = { level: number; appearance: 'subtle' | 'subtle-alpha' | 'transparent'; size: 'small' | 'medium'; - openItems: ImmutableSet; + openItems: ImmutableSet; requestTreeResponse(request: TreeItemRequest): void; }; @@ -175,10 +176,13 @@ export type TreeItemState = ComponentState & TreeItemConte itemType: TreeItemType; }; +// @public (undocumented) +export type TreeItemValue = string | number; + // @public (undocumented) export type TreeNavigationData_unstable = { target: HTMLElement; - value: string; + value: TreeItemValue; } & ({ event: React_2.MouseEvent; type: 'Click'; @@ -211,26 +215,23 @@ export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event']; // @public (undocumented) export type TreeOpenChangeData = { open: boolean; - value: string; + value: TreeItemValue; + target: HTMLElement; + openItems: ImmutableSet; } & ({ event: React_2.MouseEvent; - target: HTMLElement; type: 'ExpandIconClick'; } | { event: React_2.MouseEvent; - target: HTMLElement; type: 'Click'; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof Enter; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof ArrowRight; } | { event: React_2.KeyboardEvent; - target: HTMLElement; type: typeof ArrowLeft; }); @@ -241,8 +242,8 @@ export type TreeOpenChangeEvent = TreeOpenChangeData['event']; export type TreeProps = ComponentProps & { appearance?: 'subtle' | 'subtle-alpha' | 'transparent'; size?: 'small' | 'medium'; - openItems?: Iterable; - defaultOpenItems?: Iterable; + openItems?: Iterable; + defaultOpenItems?: Iterable; onOpenChange?(event: TreeOpenChangeEvent, data: TreeOpenChangeData): void; onNavigation_unstable?(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable): void; }; 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 9e666f7159ce84..01a6cc7298d145 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 @@ -1,14 +1,16 @@ -import * as React from 'react'; +import type * 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 type { TreeContextValue } from '../../contexts/treeContext'; +import type { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Enter, Home } from '@fluentui/keyboard-keys'; +import type { TreeItemValue } from '../TreeItem/TreeItem.types'; +import { ImmutableSet } from '../../utils/ImmutableSet'; export type TreeSlots = { root: Slot<'div'>; }; // eslint-disable-next-line @typescript-eslint/naming-convention -export type TreeNavigationData_unstable = { target: HTMLElement; value: string } & ( +export type TreeNavigationData_unstable = { target: HTMLElement; value: TreeItemValue } & ( | { event: React.MouseEvent; type: 'Click' } | { event: React.KeyboardEvent; type: 'TypeAhead' } | { event: React.KeyboardEvent; type: typeof ArrowRight } @@ -22,32 +24,17 @@ export type TreeNavigationData_unstable = { target: HTMLElement; value: string } // eslint-disable-next-line @typescript-eslint/naming-convention export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event']; -export type TreeOpenChangeData = { open: boolean; value: string } & ( - | { - event: React.MouseEvent; - target: HTMLElement; - type: 'ExpandIconClick'; - } - | { - event: React.MouseEvent; - target: HTMLElement; - type: 'Click'; - } - | { - event: React.KeyboardEvent; - target: HTMLElement; - type: typeof Enter; - } - | { - event: React.KeyboardEvent; - target: HTMLElement; - type: typeof ArrowRight; - } - | { - event: React.KeyboardEvent; - target: HTMLElement; - type: typeof ArrowLeft; - } +export type TreeOpenChangeData = { + open: boolean; + value: TreeItemValue; + target: HTMLElement; + openItems: ImmutableSet; +} & ( + | { event: React.MouseEvent; type: 'ExpandIconClick' } + | { event: React.MouseEvent; type: 'Click' } + | { event: React.KeyboardEvent; type: typeof Enter } + | { event: React.KeyboardEvent; type: typeof ArrowRight } + | { event: React.KeyboardEvent; type: typeof ArrowLeft } ); export type TreeOpenChangeEvent = TreeOpenChangeData['event']; @@ -75,13 +62,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. diff --git a/packages/react-components/react-tree/src/components/Tree/useRootTree.ts b/packages/react-components/react-tree/src/components/Tree/useRootTree.ts index 6153dc9da69a81..0a5636516bb563 100644 --- a/packages/react-components/react-tree/src/components/Tree/useRootTree.ts +++ b/packages/react-components/react-tree/src/components/Tree/useRootTree.ts @@ -1,10 +1,10 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { getNativeElementProps, useEventCallback, useMergedRefs } from '@fluentui/react-utilities'; -import { TreeOpenChangeData, TreeProps, TreeState, TreeNavigationData_unstable } from './Tree.types'; -import { useNestedTreeNavigation, useOpenItemsState } from '../../hooks'; +import type { TreeOpenChangeData, TreeProps, TreeState, TreeNavigationData_unstable } from './Tree.types'; +import { createNextOpenItems, useControllableOpenItems, useNestedTreeNavigation } from '../../hooks'; import { treeDataTypes } from '../../utils/tokens'; -import { TreeItemRequest } from '../../contexts/index'; +import { TreeItemRequest } from '../../contexts'; /** * Create the state required to render the root level Tree. @@ -17,15 +17,17 @@ export function useRootTree(props: TreeProps, ref: React.Ref): Tree const { appearance = 'subtle', size = 'medium' } = props; - const [openItems, updateOpenItems] = useOpenItemsState(props); + const [openItems, setOpenItems] = useControllableOpenItems(props); + const [navigate, navigationRef] = useNestedTreeNavigation(); - const requestOpenChange = (data: TreeOpenChangeData) => { - props.onOpenChange?.(data.event, data); + const requestOpenChange = (data: Omit) => { + const nextOpenItems = createNextOpenItems(data, openItems); + props.onOpenChange?.(data.event, { ...data, openItems: nextOpenItems } as TreeOpenChangeData); if (data.event.isDefaultPrevented()) { return; } - return updateOpenItems(data); + return setOpenItems(nextOpenItems); }; const requestNavigation = (data: TreeNavigationData_unstable) => { @@ -39,82 +41,42 @@ export function useRootTree(props: TreeProps, ref: React.Ref): Tree } }; - const handleTreeItemClick = ({ - event, - value, - itemType, - type, - }: Extract) => { - ReactDOM.unstable_batchedUpdates(() => { - requestOpenChange({ - event, - value, - open: itemType === 'branch' && !openItems.has(value), - type, - target: event.currentTarget, - }); - requestNavigation({ event, value, target: event.currentTarget, type: treeDataTypes.Click }); - }); - }; - - const handleTreeItemKeyDown = ({ - event, - type, - value, - itemType, - }: Exclude) => { - const open = openItems.has(value); - switch (type) { - case treeDataTypes.ArrowRight: - if (itemType === 'leaf') { + const requestTreeResponse = useEventCallback((request: TreeItemRequest) => { + switch (request.type) { + case treeDataTypes.Click: + case treeDataTypes.ExpandIconClick: { + return ReactDOM.unstable_batchedUpdates(() => { + requestOpenChange({ ...request, open: request.itemType === 'branch' && !openItems.has(request.value) }); + requestNavigation({ ...request, type: treeDataTypes.Click }); + }); + } + case treeDataTypes.ArrowRight: { + if (request.itemType === 'leaf') { return; } + const open = openItems.has(request.value); if (!open) { - return requestOpenChange({ - event, - value, - open: true, - type: treeDataTypes.ArrowRight, - target: event.currentTarget, - }); + return requestOpenChange({ ...request, open: true }); } - return requestNavigation({ event, value, type, target: event.currentTarget }); - case treeDataTypes.Enter: - return requestOpenChange({ - event, - value, - open: itemType === 'branch' && !open, - type: treeDataTypes.Enter, - target: event.currentTarget, - }); - case treeDataTypes.ArrowLeft: - if (open && itemType === 'branch') { - return requestOpenChange({ - event, - value, - open: false, - type: treeDataTypes.ArrowLeft, - target: event.currentTarget, - }); + return requestNavigation(request); + } + case treeDataTypes.Enter: { + const open = openItems.has(request.value); + return requestOpenChange({ ...request, open: request.itemType === 'branch' && !open }); + } + case treeDataTypes.ArrowLeft: { + const open = openItems.has(request.value); + if (open && request.itemType === 'branch') { + return requestOpenChange({ ...request, open: false, type: treeDataTypes.ArrowLeft }); } - return requestNavigation({ event, value, target: event.currentTarget, type: treeDataTypes.ArrowLeft }); + return requestNavigation({ ...request, type: treeDataTypes.ArrowLeft }); + } case treeDataTypes.End: case treeDataTypes.Home: case treeDataTypes.ArrowUp: case treeDataTypes.ArrowDown: case treeDataTypes.TypeAhead: - return requestNavigation({ event, value, type, target: event.currentTarget }); - } - }; - - const requestTreeResponse = useEventCallback((request: TreeItemRequest) => { - switch (request.event.type) { - case 'click': - // casting is required here as we're narrowing down the event to only click events - return handleTreeItemClick(request as Extract); - case 'keydown': - // casting is required here as we're narrowing down the event to only keyboard events - return handleTreeItemKeyDown(request as Exclude); + return requestNavigation({ ...request, target: request.event.currentTarget }); } }); 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 5605fd81207ae6..1f1389a83194ed 100644 --- a/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx +++ b/packages/react-components/react-tree/src/components/TreeItem/useTreeItem.tsx @@ -90,18 +90,17 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref) => { onKeyDown?.(event); - if (event.isDefaultPrevented()) { - return; - } - if (event.currentTarget !== event.target) { + // Ignore keyboard events that do not originate from the current tree item. + if (event.isDefaultPrevented() || event.currentTarget !== event.target) { return; } switch (event.key) { @@ -112,12 +111,12 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref; + openItems: ImmutableSet; /** * requests root Tree component to respond to some tree item event, */ @@ -15,8 +15,8 @@ export type TreeContextValue = { }; export type TreeItemRequest = { itemType: TreeItemType } & ( - | OmitWithoutExpanding - | OmitWithoutExpanding + | OmitWithoutExpanding + | TreeNavigationData_unstable ); // helper type that avoids the expansion of unions while inferring it, should work exactly the same as Omit @@ -24,7 +24,7 @@ type OmitWithoutExpanding = P extends unk const defaultContextValue: TreeContextValue = { level: 0, - openItems: emptyImmutableSet, + openItems: ImmutableSet.empty, requestTreeResponse: noop, appearance: 'subtle', size: 'medium', diff --git a/packages/react-components/react-tree/src/hooks/index.ts b/packages/react-components/react-tree/src/hooks/index.ts index 7802f3e41eec81..16257766327212 100644 --- a/packages/react-components/react-tree/src/hooks/index.ts +++ b/packages/react-components/react-tree/src/hooks/index.ts @@ -1,3 +1,3 @@ export * from './useFlatTree'; export * from './useNestedTreeNavigation'; -export * from './useOpenItemsState'; +export * from './useControllableOpenItems'; diff --git a/packages/react-components/react-tree/src/hooks/useControllableOpenItems.ts b/packages/react-components/react-tree/src/hooks/useControllableOpenItems.ts new file mode 100644 index 00000000000000..1c8fc65b8df9c5 --- /dev/null +++ b/packages/react-components/react-tree/src/hooks/useControllableOpenItems.ts @@ -0,0 +1,31 @@ +import { useControllableState } from '@fluentui/react-utilities'; +import * as React from 'react'; +import { ImmutableSet } from '../utils/ImmutableSet'; +import type { TreeOpenChangeData, TreeProps } from '../components/Tree/Tree.types'; +import type { TreeItemValue } from '../components/TreeItem/TreeItem.types'; + +/** + * @internal + */ +export function useControllableOpenItems(props: Pick) { + return useControllableState({ + state: React.useMemo(() => props.openItems && ImmutableSet.create(props.openItems), [props.openItems]), + defaultState: props.defaultOpenItems && (() => ImmutableSet.create(props.defaultOpenItems)), + initialState: ImmutableSet.empty, + }); +} + +export function createNextOpenItems( + data: Pick, + previousOpenItems: ImmutableSet, +): ImmutableSet { + if (data.value === null) { + return previousOpenItems; + } + const previousOpenItemsHasId = previousOpenItems.has(data.value); + if (data.open ? previousOpenItemsHasId : !previousOpenItemsHasId) { + return previousOpenItems; + } + const nextOpenItems = ImmutableSet.create(previousOpenItems); + return data.open ? nextOpenItems.add(data.value) : nextOpenItems.delete(data.value); +} diff --git a/packages/react-components/react-tree/src/hooks/useFlatTree.ts b/packages/react-components/react-tree/src/hooks/useFlatTree.ts index 078a1890c1ce17..a032366827355d 100644 --- a/packages/react-components/react-tree/src/hooks/useFlatTree.ts +++ b/packages/react-components/react-tree/src/hooks/useFlatTree.ts @@ -3,7 +3,7 @@ import * as React from 'react'; import { createFlatTreeItems, VisibleFlatTreeItemGenerator } from '../utils/createFlatTreeItems'; import { treeDataTypes } from '../utils/tokens'; import { useFlatTreeNavigation } from './useFlatTreeNavigation'; -import { useOpenItemsState } from './useOpenItemsState'; +import { useControllableOpenItems } from './useControllableOpenItems'; import type { TreeNavigationData_unstable, TreeNavigationEvent_unstable, @@ -12,10 +12,10 @@ import type { TreeProps, } from '../Tree'; import type { TreeItemProps, TreeItemValue } from '../TreeItem'; -import { ImmutableSet } from '../utils/ImmutableSet'; import { dataTreeItemValueAttrName } from '../utils/getTreeItemValueFromElement'; +import { ImmutableSet } from '../utils/ImmutableSet'; -export type FlatTreeItemProps = Omit & +export type FlatTreeItemProps = Omit & Partial> & { value: TreeItemValue; parentValue?: TreeItemValue; @@ -37,7 +37,7 @@ export type FlatTreeItem = export type FlatTreeProps = Required> & { ref: React.Ref; - openItems: ImmutableSet; + openItems: ImmutableSet; }; /** @@ -120,15 +120,15 @@ export function useFlatTree_unstable { - const [openItems, updateOpenItems] = useOpenItemsState(options); const flatTreeItems = React.useMemo(() => createFlatTreeItems(flatTreeItemProps), [flatTreeItemProps]); + const [openItems, setOpenItems] = useControllableOpenItems(options); const [navigate, navigationRef] = useFlatTreeNavigation(flatTreeItems); const treeRef = React.useRef(null); const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { options.onOpenChange?.(event, data); if (!event.isDefaultPrevented()) { - updateOpenItems(data); + setOpenItems(data.openItems); } event.preventDefault(); }); diff --git a/packages/react-components/react-tree/src/hooks/useOpenItemsState.ts b/packages/react-components/react-tree/src/hooks/useOpenItemsState.ts deleted file mode 100644 index 24c60f24a0ed94..00000000000000 --- a/packages/react-components/react-tree/src/hooks/useOpenItemsState.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useControllableState, useEventCallback } from '@fluentui/react-utilities'; -import * as React from 'react'; -import { createImmutableSet, emptyImmutableSet, ImmutableSet } from '../utils/ImmutableSet'; -import type { TreeOpenChangeData, TreeProps } from '../Tree'; - -export function useOpenItemsState(props: Pick) { - const [openItems, setOpenItems] = useControllableState({ - state: React.useMemo(() => props.openItems && createImmutableSet(props.openItems), [props.openItems]), - defaultState: props.defaultOpenItems && (() => createImmutableSet(props.defaultOpenItems)), - initialState: emptyImmutableSet, - }); - const updateOpenItems = useEventCallback((data: TreeOpenChangeData) => - setOpenItems(currentOpenItems => createNextOpenItems(data, currentOpenItems)), - ); - return [openItems, updateOpenItems] as const; -} - -function createNextOpenItems(data: TreeOpenChangeData, previousOpenItems: ImmutableSet): ImmutableSet { - if (data.value === null) { - return previousOpenItems; - } - const previousOpenItemsHasId = previousOpenItems.has(data.value); - if (data.open ? previousOpenItemsHasId : !previousOpenItemsHasId) { - return previousOpenItems; - } - const nextOpenItems = createImmutableSet(previousOpenItems); - 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 0142255f6ab1d3..78183a3a262542 100644 --- a/packages/react-components/react-tree/src/index.ts +++ b/packages/react-components/react-tree/src/index.ts @@ -29,7 +29,7 @@ export { useTreeItemContextValues_unstable, useTreeItem_unstable, } from './TreeItem'; -export type { TreeItemProps, TreeItemState, TreeItemSlots } from './TreeItem'; +export type { TreeItemProps, TreeItemState, TreeItemSlots, TreeItemValue } from './TreeItem'; export { TreeItemLayout, diff --git a/packages/react-components/react-tree/src/testing/flattenTreeFromElement.ts b/packages/react-components/react-tree/src/testing/flattenTreeFromElement.ts index a17a62aa5adf94..00506fa422b082 100644 --- a/packages/react-components/react-tree/src/testing/flattenTreeFromElement.ts +++ b/packages/react-components/react-tree/src/testing/flattenTreeFromElement.ts @@ -1,31 +1,32 @@ -import { TreeItemProps } from '../TreeItem'; +import { TreeItemProps, TreeItemValue } from '../TreeItem'; import * as React from 'react'; import { FlatTreeItemProps } from '../hooks/useFlatTree'; -let count = 1; /** * @internal */ export const flattenTreeFromElement = ( root: React.ReactElement<{ - children?: React.ReactElement | React.ReactElement[]; + children?: + | React.ReactElement + | React.ReactElement[]; }>, parent?: FlatTreeItemProps, level = 1, ): FlatTreeItemProps[] => { - const children = React.Children.toArray(root.props.children) as React.ReactElement[]; + const children = React.Children.toArray(root.props.children) as React.ReactElement< + TreeItemProps & { value: TreeItemValue } + >[]; return children.reduce((acc, curr, index) => { const childrenArray = React.Children.toArray(curr.props.children); const subtree = (childrenArray.length === 3 ? childrenArray[2] : childrenArray[1]) as typeof root | undefined; const [content] = childrenArray; const actions = (childrenArray.length === 3 ? childrenArray[1] : undefined) as React.ReactNode | undefined; - const id = curr.props.id ?? `fui-FlatTreeItem-${count++}`; const flatTreeItem: FlatTreeItemProps = { 'aria-level': level, 'aria-posinset': index + 1, 'aria-setsize': children.length, parentValue: parent?.value, - value: curr.props.value ?? id, ...curr.props, children: actions ? [content, actions] : content, }; diff --git a/packages/react-components/react-tree/src/utils/ImmutableSet.ts b/packages/react-components/react-tree/src/utils/ImmutableSet.ts index 2da43059019f03..3e11c23b6a2d5c 100644 --- a/packages/react-components/react-tree/src/utils/ImmutableSet.ts +++ b/packages/react-components/react-tree/src/utils/ImmutableSet.ts @@ -22,22 +22,22 @@ export interface ImmutableSet { has(value: Value): boolean; /** Iterates over values in the ImmutableSet. */ [Symbol.iterator](): IterableIterator; + /** + * @internal + * Exposes the internal set used to store values. + * This is an internal API and should not be used directly. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + dangerousGetInternalSet_unstable(): Set; } -export const emptyImmutableSet = createImmutableSet(); +const emptyImmutableSet = createImmutableSet(); -/** - * properly creates an ImmutableSet instance from an iterable - */ -export function createImmutableSet(iterable?: Iterable): ImmutableSet { - const internalSet = new Set(iterable); - return dangerouslyCreateImmutableSet(internalSet); -} /** * Avoid using *dangerouslyCreateImmutableSet*, since this method will expose internally used set, use createImmutableSet instead, * @param internalSet - a set that is used internally to store values. */ -export function dangerouslyCreateImmutableSet(internalSet: Set): ImmutableSet { +function dangerouslyCreateImmutableSet(internalSet: Set): ImmutableSet { return { size: internalSet.size, add(value) { @@ -59,5 +59,21 @@ export function dangerouslyCreateImmutableSet(internalSet: Set): I [Symbol.iterator]() { return internalSet[Symbol.iterator](); }, + // eslint-disable-next-line @typescript-eslint/naming-convention + dangerousGetInternalSet_unstable: () => internalSet, }; } + +/** + * properly creates an ImmutableSet instance from an iterable + */ +function createImmutableSet(iterable?: Iterable): ImmutableSet { + const internalSet = new Set(iterable); + return dangerouslyCreateImmutableSet(internalSet); +} + +export const ImmutableSet = { + empty: emptyImmutableSet, + create: createImmutableSet, + dangerouslyCreate: dangerouslyCreateImmutableSet, +}; diff --git a/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts b/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts index ab57390de414f8..ab8fde5691fb99 100644 --- a/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts +++ b/packages/react-components/react-tree/src/utils/createFlatTreeItems.ts @@ -1,6 +1,6 @@ -import type { ImmutableSet } from './ImmutableSet'; import type { FlatTreeItem, FlatTreeItemProps } from '../hooks/useFlatTree'; import { TreeItemValue } from '../TreeItem'; +import { ImmutableSet } from './ImmutableSet'; /** * @internal @@ -105,7 +105,7 @@ function createFlatTreeRootItem(): FlatTreeItem { // eslint-disable-next-line @typescript-eslint/naming-convention export function* VisibleFlatTreeItemGenerator( - openItems: ImmutableSet, + openItems: ImmutableSet, flatTreeItems: FlatTreeItems, ) { for (let index = 0, visibleIndex = 0; index < flatTreeItems.size; index++) { diff --git a/packages/react-components/react-tree/src/utils/flattenTree.ts b/packages/react-components/react-tree/src/utils/flattenTree.ts index 88999c2e953bb3..d840785329b1b7 100644 --- a/packages/react-components/react-tree/src/utils/flattenTree.ts +++ b/packages/react-components/react-tree/src/utils/flattenTree.ts @@ -1,26 +1,24 @@ import { FlatTreeItemProps } from '../hooks/useFlatTree'; -import { TreeItemProps } from '../TreeItem'; +import { TreeItemProps, TreeItemValue } from '../TreeItem'; export type NestedTreeItem = Omit & { + value: TreeItemValue; subtree?: NestedTreeItem[]; }; export type FlattenedTreeItem = FlatTreeItemProps & Props; -let count = 1; function flattenTreeRecursive( items: NestedTreeItem[], parent?: FlatTreeItemProps & Props, level = 1, ): FlattenedTreeItem[] { return items.reduce[]>((acc, { subtree, ...item }, index) => { - const id = item.id ?? `fui-FlatTreeItem-${count++}`; const flatTreeItem = { 'aria-level': level, 'aria-posinset': index + 1, 'aria-setsize': items.length, parentValue: parent?.value, - value: item.value ?? (id as unknown as Props['value']), ...item, } as FlattenedTreeItem; acc.push(flatTreeItem); diff --git a/packages/react-components/react-tree/stories/A_Tree/TreeControllingOpenAndClose.stories.tsx b/packages/react-components/react-tree/stories/A_Tree/TreeControllingOpenAndClose.stories.tsx index 305ab8af92b10e..68b830d41ec9d3 100644 --- a/packages/react-components/react-tree/stories/A_Tree/TreeControllingOpenAndClose.stories.tsx +++ b/packages/react-components/react-tree/stories/A_Tree/TreeControllingOpenAndClose.stories.tsx @@ -1,9 +1,16 @@ import * as React from 'react'; -import { Tree, TreeItem, TreeItemLayout, TreeOpenChangeData, TreeOpenChangeEvent } from '@fluentui/react-tree'; +import { + Tree, + TreeItemValue, + TreeItem, + TreeItemLayout, + TreeOpenChangeData, + TreeOpenChangeEvent, +} from '@fluentui/react-tree'; import story from './TreeControllingOpenAndClose.md'; export const OpenItemsControlled = () => { - const [openItems, setOpenItems] = React.useState([]); + const [openItems, setOpenItems] = React.useState([]); const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { setOpenItems(curr => (data.open ? [...curr, data.value] : curr.filter(value => value !== data.value))); }; diff --git a/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx b/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx index 39ec8db1c1ef52..899676c763323c 100644 --- a/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx +++ b/packages/react-components/react-tree/stories/B_TreeItem/TreeItemExpandIcon.stories.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; -import { Tree, TreeItem, TreeItemLayout } from '@fluentui/react-tree'; +import { Tree, TreeItem, TreeItemLayout, TreeItemValue } from '@fluentui/react-tree'; import { Add12Regular, Subtract12Regular } from '@fluentui/react-icons'; import { TreeOpenChangeData, TreeOpenChangeEvent } from '../../src/Tree'; import story from './TreeItemExpandIcon.md'; export const ExpandIcon = () => { - const [openItems, setOpenItems] = React.useState([]); + const [openItems, setOpenItems] = React.useState([]); const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => { setOpenItems(curr => (data.open ? [...curr, data.value] : curr.filter(value => value !== data.value))); }; diff --git a/packages/react-components/react-tree/stories/D_flatTree/TreeItemAddRemove.stories.tsx b/packages/react-components/react-tree/stories/D_flatTree/TreeItemAddRemove.stories.tsx index a6bd60ad45c184..60a145080325ea 100644 --- a/packages/react-components/react-tree/stories/D_flatTree/TreeItemAddRemove.stories.tsx +++ b/packages/react-components/react-tree/stories/D_flatTree/TreeItemAddRemove.stories.tsx @@ -30,8 +30,10 @@ export const AddRemoveTreeItem = () => { const [trees, setTrees] = React.useState(defaultSubTrees); const handleOpenChange = (_: TreeOpenChangeEvent, data: TreeOpenChangeData) => { - if (data.value.endsWith('-btn')) { - const subtreeIndex = Number(data.value[0]) - 1; + // casting here to string as no number values are used in this example + const value = data.value as string; + if (value.endsWith('-btn')) { + const subtreeIndex = Number(value[0]) - 1; addFlatTreeItem(subtreeIndex); } }; diff --git a/packages/react-components/react-tree/stories/D_flatTree/flattenTree.stories.tsx b/packages/react-components/react-tree/stories/D_flatTree/flattenTree.stories.tsx index 096ed721fce3e4..2ea1815c3f03f4 100644 --- a/packages/react-components/react-tree/stories/D_flatTree/flattenTree.stories.tsx +++ b/packages/react-components/react-tree/stories/D_flatTree/flattenTree.stories.tsx @@ -9,32 +9,42 @@ import { } from '@fluentui/react-tree'; import story from './flattenTree.md'; -type Item = TreeItemProps & { layout: string }; +type Item = TreeItemProps & { layout: React.ReactNode }; const defaultItems = flattenTree_unstable([ { - layout: 'level 1, item 1', + value: '1', + layout: <>level 1, item 1, subtree: [ - { layout: 'level 2, item 1' }, { - layout: 'level 2, item 2', + value: '1-1', + layout: <>level 2, item 1, }, { - layout: 'level 2, item 3', + value: '1-2', + layout: <>level 2, item 2, + }, + { + value: '1-3', + layout: <>level 2, item 3, }, ], }, { - layout: 'level 1, item 2', + value: '2', + layout: <>level 1, item 2, subtree: [ { - layout: 'level 2, item 1', + value: '2-1', + layout: <>level 2, item 1, subtree: [ { - layout: 'level 3, item 1', + value: '2-1-1', + layout: <>level 3, item 1, subtree: [ { - layout: 'level 4, item 1', + value: '2-1-1-1', + layout: <>level 4, item 1, }, ], },