+ | 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>,
},
],
},