Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: makes useFlatTree generic",
"packageName": "@fluentui/react-tree",
"email": "[email protected]",
"dependentChangeType": "patch"
}
36 changes: 22 additions & 14 deletions packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,41 @@ import type { SlotClassNames } from '@fluentui/react-utilities';
import { SlotRenderFunction } from '@fluentui/react-utilities';

// @public
export const flattenTree_unstable: <Value = string>(items: NestedTreeItem<Value>[]) => FlatTreeItemProps<Value>[];
export const flattenTree_unstable: <Props extends TreeItemProps<unknown>>(items: NestedTreeItem<Props>[]) => FlattenedTreeItem<Props>[];

// @public
export type FlatTree<Value = string> = {
getTreeProps(): FlatTreeProps<Value>;
navigate(data: TreeNavigationData_unstable<Value>): void;
getNextNavigableItem(visibleItems: FlatTreeItem<Value>[], data: TreeNavigationData_unstable<Value>): FlatTreeItem<Value> | undefined;
items(): IterableIterator<FlatTreeItem<Value>>;
export type FlatTree<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps> = {
getTreeProps(): FlatTreeProps<Props['value']>;
navigate(data: TreeNavigationData_unstable<Props['value']>): void;
getNextNavigableItem(visibleItems: FlatTreeItem<Props>[], data: TreeNavigationData_unstable<Props['value']>): FlatTreeItem<Props> | undefined;
items(): IterableIterator<FlatTreeItem<Props>>;
};

// @public (undocumented)
export type FlatTreeItem<Value = string> = Readonly<MutableFlatTreeItem<Value>>;
// @public
export type FlatTreeItem<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps> = {
index: number;
level: number;
childrenSize: number;
value: Props['value'];
parentValue: Props['parentValue'];
ref: React_2.RefObject<HTMLDivElement>;
getTreeItemProps(): Required<Pick<Props, 'value' | 'aria-setsize' | 'aria-level' | 'aria-posinset' | 'leaf'>> & Omit<Props, 'parentValue'>;
};

// @public (undocumented)
export type FlatTreeItemProps<Value = string> = Omit<TreeItemProps, 'value'> & {
export type FlatTreeItemProps<Value = string> = TreeItemProps<Value> & {
value: Value;
parentValue?: Value;
};

// @public (undocumented)
export type FlatTreeProps<Value = string> = Required<Pick<TreeProps<Value>, 'openItems' | 'onOpenChange' | 'onNavigation_unstable'> & {
export type FlatTreeProps<Value = string> = Required<Pick<TreeProps<Value>, 'openItems' | 'onOpenChange' | 'onNavigation_unstable'>> & {
ref: React_2.Ref<HTMLDivElement>;
}>;
};

// @public (undocumented)
export type NestedTreeItem<Value = string> = Omit<TreeItemProps<Value>, 'subtree'> & {
subtree?: NestedTreeItem<Value>[];
export type NestedTreeItem<Props extends TreeItemProps<unknown>> = Omit<Props, 'subtree'> & {
subtree?: NestedTreeItem<Props>[];
};

// @public (undocumented)
Expand Down Expand Up @@ -281,7 +289,7 @@ export type TreeSlots = {
export type TreeState = ComponentState<TreeSlots> & TreeContextValue;

// @public
export function useFlatTree_unstable<Value = string>(flatTreeItemProps: FlatTreeItemProps<Value>[], options?: Pick<TreeProps<Value>, 'openItems' | 'defaultOpenItems' | 'onOpenChange' | 'onNavigation_unstable'>): FlatTree<Value>;
export function useFlatTree_unstable<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps>(flatTreeItemProps: Props[], options?: FlatTreeOptions<Props>): FlatTree<Props>;

// @public
export const useTree_unstable: (props: TreeProps, ref: React_2.Ref<HTMLElement>) => TreeState;
Expand Down
69 changes: 37 additions & 32 deletions packages/react-components/react-tree/src/hooks/useFlatTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,34 @@ import type {
} from '../Tree';
import type { TreeItemProps } from '../TreeItem';

export type FlatTreeItemProps<Value = string> = Omit<TreeItemProps, 'value'> & {
export type FlatTreeItemProps<Value = string> = TreeItemProps<Value> & {
value: Value;
parentValue?: Value;
};

export type FlatTreeItem<Value = string> = Readonly<MutableFlatTreeItem<Value>>;

/**
* @internal
* Used internally on createFlatTreeItems and VisibleFlatTreeItemGenerator
* to ensure required properties when building a FlatTreeITem
* The item that is returned by `useFlatTree`, it represents a wrapper around the properties provided to
* `useFlatTree` but with extra information that might be useful on flat tree scenarios
*/
export type MutableFlatTreeItem<Value = string> = {
parentValue?: Value;
childrenSize: number;
export type FlatTreeItem<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps> = {
index: number;
value: Value;
level: number;
childrenSize: number;
value: Props['value'];
parentValue: Props['parentValue'];
/**
* A reference to the element that will render the `TreeItem`,
* this is necessary for nodes with parents (to ensure child to parent navigation),
* if a node has no parent then this reference will be null.
*/
ref: React.RefObject<HTMLDivElement>;
getTreeItemProps(): Required<
Pick<TreeItemProps<Value>, 'value' | 'aria-setsize' | 'aria-level' | 'aria-posinset' | 'leaf'>
> &
TreeItemProps<Value>;
getTreeItemProps(): Required<Pick<Props, 'value' | 'aria-setsize' | 'aria-level' | 'aria-posinset' | 'leaf'>> &
Omit<Props, 'parentValue'>;
};

export type FlatTreeProps<Value = string> = Required<
Pick<TreeProps<Value>, 'openItems' | 'onOpenChange' | 'onNavigation_unstable'> & { ref: React.Ref<HTMLDivElement> }
>;
Pick<TreeProps<Value>, 'openItems' | 'onOpenChange' | 'onNavigation_unstable'>
> & { ref: React.Ref<HTMLDivElement> };

/**
* FlatTree API to manage all required mechanisms to convert a list of items into renderable TreeItems
Expand All @@ -52,13 +52,13 @@ export type FlatTreeProps<Value = string> = Required<
*
* On simple scenarios it is advised to simply use a nested structure instead.
*/
export type FlatTree<Value = string> = {
export type FlatTree<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps> = {
/**
* returns the properties required for the Tree component to work properly.
* That includes:
* `openItems`, `onOpenChange`, `onNavigation_unstable` and `ref`
*/
getTreeProps(): FlatTreeProps<Value>;
getTreeProps(): FlatTreeProps<Props['value']>;
/**
* internal method used to react to an `onNavigation` event.
* This method ensures proper navigation on keyboard and mouse interaction.
Expand All @@ -82,7 +82,7 @@ export type FlatTree<Value = string> = {
* };
*```
*/
navigate(data: TreeNavigationData_unstable<Value>): void;
navigate(data: TreeNavigationData_unstable<Props['value']>): void;
/**
* returns next item to be focused on a navigation.
* This method is provided to decouple the element that needs to be focused from
Expand All @@ -91,15 +91,20 @@ export type FlatTree<Value = string> = {
* On the case of TypeAhead navigation this method returns the current item.
*/
getNextNavigableItem(
visibleItems: FlatTreeItem<Value>[],
data: TreeNavigationData_unstable<Value>,
): FlatTreeItem<Value> | undefined;
visibleItems: FlatTreeItem<Props>[],
data: TreeNavigationData_unstable<Props['value']>,
): FlatTreeItem<Props> | undefined;
/**
* an iterable containing all visually available flat tree items
*/
items(): IterableIterator<FlatTreeItem<Value>>;
items(): IterableIterator<FlatTreeItem<Props>>;
};

type FlatTreeOptions<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps> = Pick<
TreeProps<Props['value']>,
'openItems' | 'defaultOpenItems' | 'onOpenChange' | 'onNavigation_unstable'
>;

/**
* this hook provides FlatTree API to manage all required mechanisms to convert a list of items into renderable TreeItems
* in multiple scenarios including virtualization.
Expand All @@ -112,15 +117,15 @@ export type FlatTree<Value = string> = {
* @param flatTreeItemProps - a list of tree items
* @param options - in case control over the internal openItems is required
*/
export function useFlatTree_unstable<Value = string>(
flatTreeItemProps: FlatTreeItemProps<Value>[],
options: Pick<TreeProps<Value>, 'openItems' | 'defaultOpenItems' | 'onOpenChange' | 'onNavigation_unstable'> = {},
): FlatTree<Value> {
const [openItems, updateOpenItems] = useOpenItemsState(options);
export function useFlatTree_unstable<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps>(
flatTreeItemProps: Props[],
options: FlatTreeOptions<Props> = {},
): FlatTree<Props> {
const [openItems, updateOpenItems] = useOpenItemsState<Props['value']>(options);
const flatTreeItems = React.useMemo(() => createFlatTreeItems(flatTreeItemProps), [flatTreeItemProps]);
const [navigate, navigationRef] = useFlatTreeNavigation(flatTreeItems);

const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData<Value>) => {
const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData<Props['value']>) => {
options.onOpenChange?.(event, data);
if (!event.isDefaultPrevented()) {
updateOpenItems(data);
Expand All @@ -129,7 +134,7 @@ export function useFlatTree_unstable<Value = string>(
});

const handleNavigation = useEventCallback(
(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable<Value>) => {
(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable<Props['value']>) => {
options.onNavigation_unstable?.(event, data);
if (!event.isDefaultPrevented()) {
navigate(data);
Expand All @@ -139,7 +144,7 @@ export function useFlatTree_unstable<Value = string>(
);

const getNextNavigableItem = useEventCallback(
(visibleItems: FlatTreeItem<Value>[], data: TreeNavigationData_unstable<Value>) => {
(visibleItems: FlatTreeItem<Props>[], data: TreeNavigationData_unstable<Props['value']>) => {
const item = flatTreeItems.get(data.value);
if (item) {
switch (data.type) {
Expand Down Expand Up @@ -175,7 +180,7 @@ export function useFlatTree_unstable<Value = string>(
);

const items = React.useCallback(
() => VisibleFlatTreeItemGenerator<Value>(openItems, flatTreeItems),
() => VisibleFlatTreeItemGenerator(openItems, flatTreeItems),
[openItems, flatTreeItems],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import { treeDataTypes } from '../utils/tokens';
import { treeItemFilter } from '../utils/treeItemFilter';
import { HTMLElementWalker, useHTMLElementWalkerRef } from './useHTMLElementWalker';
import { useRovingTabIndex } from './useRovingTabIndexes';
import { FlatTreeItemProps } from './useFlatTree';

export function useFlatTreeNavigation<Value = string>(flatTreeItems: FlatTreeItems<Value>) {
export function useFlatTreeNavigation<Props extends FlatTreeItemProps<unknown> = FlatTreeItemProps>(
flatTreeItems: FlatTreeItems<Props>,
) {
const { targetDocument } = useFluent_unstable();
const [treeItemWalkerRef, treeItemWalkerRootRef] = useHTMLElementWalkerRef(treeItemFilter);
const [{ rove }, rovingRootRef] = useRovingTabIndex(treeItemFilter);

function getNextElement(data: TreeNavigationData_unstable<Value>) {
function getNextElement(data: TreeNavigationData_unstable<Props['value']>) {
if (!targetDocument || !treeItemWalkerRef.current) {
return null;
}
Expand Down Expand Up @@ -43,7 +46,7 @@ export function useFlatTreeNavigation<Value = string>(flatTreeItems: FlatTreeIte
return treeItemWalker.previousElement();
}
}
const navigate = useEventCallback((data: TreeNavigationData_unstable<Value>) => {
const navigate = useEventCallback((data: TreeNavigationData_unstable<Props['value']>) => {
const nextElement = getNextElement(data);
if (nextElement) {
rove(nextElement);
Expand All @@ -66,7 +69,7 @@ function firstChild(target: HTMLElement, treeWalker: HTMLElementWalker): HTMLEle
return null;
}

function parentElement<Value = string>(flatTreeItems: FlatTreeItems<Value>, value: Value) {
function parentElement(flatTreeItems: FlatTreeItems<FlatTreeItemProps<unknown>>, value: unknown) {
const flatTreeItem = flatTreeItems.get(value);
if (flatTreeItem?.parentValue) {
const parentItem = flatTreeItems.get(flatTreeItem.parentValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
import type { ImmutableSet } from './ImmutableSet';
import type { FlatTreeItem, FlatTreeItemProps, MutableFlatTreeItem } from '../hooks/useFlatTree';
import type { FlatTreeItem, FlatTreeItemProps } from '../hooks/useFlatTree';
import * as React from 'react';

/**
* @internal
*/
export type FlatTreeItems<Value = string> = {
export type FlatTreeItems<Props extends FlatTreeItemProps<unknown>> = {
size: number;
root: FlatTreeItem<Value>;
get(key: Value): FlatTreeItem<Value> | undefined;
set(key: Value, value: FlatTreeItem<Value>): void;
getByIndex(index: number): FlatTreeItem<Value>;
root: FlatTreeItem;
get(key: Props['value']): FlatTreeItem<Props> | undefined;
set(key: Props['value'], value: FlatTreeItem<Props>): void;
getByIndex(index: number): FlatTreeItem<Props>;
};

/**
* creates a list of flat tree items
* and provides a map to access each item by id
*/
export function createFlatTreeItems<Value = string>(
flatTreeItemProps: FlatTreeItemProps<Value>[],
): FlatTreeItems<Value> {
const root = createFlatTreeRootItem<Value>();
const itemsPerValue = new Map<Value, MutableFlatTreeItem<Value>>([[flatTreeRootId as Value, root]]);
const items: MutableFlatTreeItem<Value>[] = [];
export function createFlatTreeItems<Props extends FlatTreeItemProps<unknown>>(
flatTreeItemProps: Props[],
): FlatTreeItems<Props> {
const root = createFlatTreeRootItem();
const itemsPerValue = new Map<unknown, FlatTreeItem<FlatTreeItemProps<unknown>>>([[root.value, root]]);
const items: FlatTreeItem<FlatTreeItemProps<unknown>>[] = [];

for (let index = 0; index < flatTreeItemProps.length; index++) {
const { parentValue = flatTreeRootId as Value, ...treeItemProps } = flatTreeItemProps[index];
const { parentValue = flatTreeRootId, ...treeItemProps } = flatTreeItemProps[index];

const nextItemProps: FlatTreeItemProps<Value> | undefined = flatTreeItemProps[index + 1];
const nextItemProps: Props | undefined = flatTreeItemProps[index + 1];
const currentParent = itemsPerValue.get(parentValue);
if (!currentParent) {
if (process.env.NODE_ENV === 'development') {
Expand All @@ -38,12 +38,13 @@ export function createFlatTreeItems<Value = string>(
}
break;
}
const isLeaf = nextItemProps?.parentValue !== treeItemProps.value;
const isLeaf =
treeItemProps.leaf ?? (treeItemProps.value === undefined || nextItemProps?.parentValue !== treeItemProps.value);
const currentLevel = (currentParent.level ?? 0) + 1;
const currentChildrenSize = ++currentParent.childrenSize;
const ref = React.createRef<HTMLDivElement>();

const flatTreeItem: MutableFlatTreeItem<Value> = {
const flatTreeItem: FlatTreeItem<FlatTreeItemProps<unknown>> = {
value: treeItemProps.value,
getTreeItemProps: () => ({
...treeItemProps,
Expand All @@ -64,27 +65,30 @@ export function createFlatTreeItems<Value = string>(
items.push(flatTreeItem);
}

return {
const flatTreeItems: FlatTreeItems<FlatTreeItemProps<unknown>> = {
root,
size: items.length,
getByIndex: index => items[index],
get: id => itemsPerValue.get(id),
set: (id, value) => itemsPerValue.set(id, value),
get: key => itemsPerValue.get(key),
set: (key, value) => itemsPerValue.set(key, value),
};

return flatTreeItems as FlatTreeItems<Props>;
}

export const flatTreeRootId = '__fuiFlatTreeRoot' as unknown;
export const flatTreeRootId = '__fuiFlatTreeRoot';

function createFlatTreeRootItem<Value = string>(): FlatTreeItem<Value> {
function createFlatTreeRootItem(): FlatTreeItem {
return {
ref: { current: null },
value: flatTreeRootId as Value,
value: flatTreeRootId,
parentValue: undefined,
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 { value: flatTreeRootId as Value, 'aria-setsize': -1, 'aria-level': -1, 'aria-posinset': -1, leaf: true };
return { value: flatTreeRootId, 'aria-setsize': -1, 'aria-level': -1, 'aria-posinset': -1, leaf: true };
},
childrenSize: 0,
get index() {
Expand All @@ -99,12 +103,12 @@ function createFlatTreeRootItem<Value = string>(): FlatTreeItem<Value> {
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export function* VisibleFlatTreeItemGenerator<Value = string>(
openItems: ImmutableSet<Value>,
flatTreeItems: FlatTreeItems<Value>,
export function* VisibleFlatTreeItemGenerator<Props extends FlatTreeItemProps<unknown>>(
openItems: ImmutableSet<unknown>,
flatTreeItems: FlatTreeItems<Props>,
) {
for (let index = 0, visibleIndex = 0; index < flatTreeItems.size; index++) {
const item: MutableFlatTreeItem<Value> = flatTreeItems.getByIndex(index);
const item = flatTreeItems.getByIndex(index) as FlatTreeItem<Props>;
const parent = item.parentValue ? flatTreeItems.get(item.parentValue) ?? flatTreeItems.root : flatTreeItems.root;
if (isItemVisible(item, openItems, flatTreeItems)) {
item.index = visibleIndex++;
Expand All @@ -115,10 +119,10 @@ export function* VisibleFlatTreeItemGenerator<Value = string>(
}
}

function isItemVisible<Value>(
item: FlatTreeItem<Value>,
openItems: ImmutableSet<Value>,
flatTreeItems: FlatTreeItems<Value>,
function isItemVisible(
item: FlatTreeItem<FlatTreeItemProps<unknown>>,
openItems: ImmutableSet<unknown>,
flatTreeItems: FlatTreeItems<FlatTreeItemProps<unknown>>,
) {
if (item.level === 1) {
return true;
Expand Down
Loading