Skip to content

Commit

Permalink
feat(react-tree): makes useFlatTree generic (#27682)
Browse files Browse the repository at this point in the history
* feat(react-tree): makes useFlatTree generic

* chore: remove redundancies on types
  • Loading branch information
bsunderhus authored Apr 26, 2023
1 parent cbe5229 commit 53c5190
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 190 deletions.
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": "bernardo.sunderhus@gmail.com",
"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

0 comments on commit 53c5190

Please sign in to comment.