Skip to content

Commit

Permalink
chore(react-tree): refactor tree navigation (#29731)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsunderhus authored Nov 14, 2023
1 parent 69fef12 commit ee8ca9b
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 232 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore: refactor tree navigation",
"packageName": "@fluentui/react-tree",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import * as React from 'react';
import { useRootTree } from '../../hooks/useRootTree';
import { FlatTreeProps, FlatTreeState } from './FlatTree.types';
import { useFlatTreeNavigation } from './useFlatTreeNavigation';
import { HTMLElementWalker, createHTMLElementWalker } from '../../utils/createHTMLElementWalker';
import { useFluent_unstable } from '@fluentui/react-shared-contexts';
import { treeItemFilter } from '../../utils/treeItemFilter';
import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import type { TreeNavigationData_unstable, TreeNavigationEvent_unstable } from '../Tree/Tree.types';
import { useFlatTreeNavigation } from '../../hooks/useFlatTreeNavigation';
import { useSubtree } from '../../hooks/useSubtree';
import { ImmutableSet } from '../../utils/ImmutableSet';
import { ImmutableMap } from '../../utils/ImmutableMap';
Expand All @@ -24,33 +20,23 @@ export const useFlatTree_unstable: (props: FlatTreeProps, ref: React.Ref<HTMLEle
};

function useRootFlatTree(props: FlatTreeProps, ref: React.Ref<HTMLElement>): FlatTreeState {
const { navigate, initialize } = useFlatTreeNavigation();
const walkerRef = React.useRef<HTMLElementWalker>();
const { targetDocument } = useFluent_unstable();
const navigation = useFlatTreeNavigation();

const initializeWalker = React.useCallback(
(root: HTMLElement | null) => {
if (root && targetDocument) {
walkerRef.current = createHTMLElementWalker(root, targetDocument, treeItemFilter);
initialize(walkerRef.current);
}
},
[initialize, targetDocument],
return Object.assign(
useRootTree(
{
...props,
onNavigation: useEventCallback((event, data) => {
props.onNavigation?.(event, data);
if (!event.isDefaultPrevented()) {
navigation.navigate(data);
}
}),
},
useMergedRefs(ref, navigation.rootRef),
),
{ treeType: 'flat' } as const,
);

const handleNavigation = useEventCallback(
(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => {
props.onNavigation?.(event, data);
if (walkerRef.current && !event.isDefaultPrevented()) {
navigate(data, walkerRef.current);
}
},
);

return {
treeType: 'flat',
...useRootTree({ ...props, onNavigation: handleNavigation }, useMergedRefs(ref, initializeWalker)),
};
}

function useSubFlatTree(props: FlatTreeProps, ref: React.Ref<HTMLElement>): FlatTreeState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import * as React from 'react';
import { HeadlessTreeItem, HeadlessTreeItemProps, createHeadlessTree } from '../../utils/createHeadlessTree';
import { treeDataTypes } from '../../utils/tokens';
import { useFlatTreeNavigation } from './useFlatTreeNavigation';
import { useFlatTreeNavigation } from '../../hooks/useFlatTreeNavigation';
import { createNextOpenItems, useControllableOpenItems } from '../../hooks/useControllableOpenItems';
import type { TreeItemValue } from '../../TreeItem';
import { dataTreeItemValueAttrName } from '../../utils/getTreeItemValueFromElement';
Expand All @@ -17,9 +17,6 @@ import {
TreeOpenChangeEvent,
TreeProps,
} from '../Tree/Tree.types';
import { HTMLElementWalker, createHTMLElementWalker } from '../../utils/createHTMLElementWalker';
import { treeItemFilter } from '../../utils/treeItemFilter';
import { useFluent_unstable } from '@fluentui/react-shared-contexts';

export type HeadlessFlatTreeItemProps = HeadlessTreeItemProps;
export type HeadlessFlatTreeItem<Props extends HeadlessFlatTreeItemProps> = HeadlessTreeItem<Props>;
Expand Down Expand Up @@ -118,18 +115,7 @@ export function useHeadlessFlatTree_unstable<Props extends HeadlessTreeItemProps
const headlessTree = React.useMemo(() => createHeadlessTree(props), [props]);
const [openItems, setOpenItems] = useControllableOpenItems(options);
const [checkedItems, setCheckedItems] = useFlatControllableCheckedItems(options, headlessTree);
const { initialize, navigate } = useFlatTreeNavigation();
const { targetDocument } = useFluent_unstable();
const walkerRef = React.useRef<HTMLElementWalker>();
const initializeWalker = React.useCallback(
(root: HTMLElement | null) => {
if (root && targetDocument) {
walkerRef.current = createHTMLElementWalker(root, targetDocument, treeItemFilter);
initialize(walkerRef.current);
}
},
[initialize, targetDocument],
);
const navigation = useFlatTreeNavigation();

const treeRef = React.useRef<HTMLDivElement>(null);
const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
Expand Down Expand Up @@ -178,7 +164,7 @@ export function useHeadlessFlatTree_unstable<Props extends HeadlessTreeItemProps
return treeRef.current?.querySelector(`[${dataTreeItemValueAttrName}="${item.value}"]`) as HTMLElement | null;
}, []);

const ref = useMergedRefs<HTMLDivElement>(treeRef, initializeWalker);
const ref = useMergedRefs<HTMLDivElement>(treeRef, navigation.rootRef);

const getTreeProps = React.useCallback(
() => ({
Expand All @@ -198,17 +184,13 @@ export function useHeadlessFlatTree_unstable<Props extends HeadlessTreeItemProps

return React.useMemo<HeadlessFlatTree<Props>>(
() => ({
navigate: data => {
if (walkerRef.current) {
navigate(data, walkerRef.current);
}
},
navigate: navigation.navigate,
getTreeProps,
getNextNavigableItem,
getElementFromItem,
items,
}),
[navigate, getTreeProps, getNextNavigableItem, getElementFromItem, items],
[navigation.navigate, getTreeProps, getNextNavigableItem, getElementFromItem, items],
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function useNestedCheckedItems(props: Pick<TreeProps, 'checkedItems'>) {
}

export function createNextNestedCheckedItems(
data: TreeCheckedChangeData,
data: Pick<TreeCheckedChangeData, 'selectionMode' | 'value' | 'checked'>,
previousCheckedItems: ImmutableMap<TreeItemValue, 'mixed' | boolean>,
): ImmutableMap<TreeItemValue, 'mixed' | boolean> {
if (data.selectionMode === 'single') {
Expand Down
91 changes: 29 additions & 62 deletions packages/react-components/react-tree/src/components/Tree/useTree.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import * as React from 'react';
import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import type {
TreeCheckedChangeData,
TreeCheckedChangeEvent,
TreeNavigationData_unstable,
TreeNavigationEvent_unstable,
TreeOpenChangeData,
TreeOpenChangeEvent,
TreeProps,
TreeState,
} from './Tree.types';
import type { TreeProps, TreeState } from './Tree.types';
import { createNextOpenItems, useControllableOpenItems } from '../../hooks/useControllableOpenItems';
import { createNextNestedCheckedItems, useNestedCheckedItems } from './useNestedControllableCheckedItems';
import { SubtreeContext } from '../../contexts/subtreeContext';
import { useRootTree } from '../../hooks/useRootTree';
import { useSubtree } from '../../hooks/useSubtree';
import { HTMLElementWalker, createHTMLElementWalker } from '../../utils/createHTMLElementWalker';
import { treeItemFilter } from '../../utils/treeItemFilter';
import { useTreeNavigation } from './useTreeNavigation';
import { useFluent_unstable } from '@fluentui/react-shared-contexts';
import { useTreeNavigation } from '../../hooks/useTreeNavigation';
import { useTreeContext_unstable } from '../../contexts/treeContext';

export const useTree_unstable = (props: TreeProps, ref: React.Ref<HTMLElement>): TreeState => {
Expand All @@ -32,61 +20,40 @@ export const useTree_unstable = (props: TreeProps, ref: React.Ref<HTMLElement>):
function useNestedRootTree(props: TreeProps, ref: React.Ref<HTMLElement>): TreeState {
const [openItems, setOpenItems] = useControllableOpenItems(props);
const checkedItems = useNestedCheckedItems(props);
const { navigate, initialize } = useTreeNavigation();
const walkerRef = React.useRef<HTMLElementWalker>();
const { targetDocument } = useFluent_unstable();
const navigation = useTreeNavigation();

const initializeWalker = React.useCallback(
(root: HTMLElement | null) => {
if (root && targetDocument) {
walkerRef.current = createHTMLElementWalker(root, targetDocument, treeItemFilter);
initialize(walkerRef.current);
}
},
[initialize, targetDocument],
);

const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
const nextOpenItems = createNextOpenItems(data, openItems);
props.onOpenChange?.(event, {
...data,
openItems: nextOpenItems.dangerouslyGetInternalSet_unstable(),
});
setOpenItems(nextOpenItems);
});

const handleCheckedChange = useEventCallback((event: TreeCheckedChangeEvent, data: TreeCheckedChangeData) => {
if (walkerRef.current) {
const nextCheckedItems = createNextNestedCheckedItems(data, checkedItems);
props.onCheckedChange?.(event, {
...data,
checkedItems: nextCheckedItems.dangerouslyGetInternalMap_unstable(),
});
}
});
const handleNavigation = useEventCallback(
(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => {
props.onNavigation?.(event, data);
if (walkerRef.current && !event.isDefaultPrevented()) {
navigate(data, walkerRef.current);
}
},
);

return {
treeType: 'nested',
...useRootTree(
return Object.assign(
useRootTree(
{
...props,
openItems,
checkedItems,
onOpenChange: handleOpenChange,
onNavigation: handleNavigation,
onCheckedChange: handleCheckedChange,
onOpenChange: useEventCallback((event, data) => {
const nextOpenItems = createNextOpenItems(data, openItems);
props.onOpenChange?.(event, {
...data,
openItems: nextOpenItems.dangerouslyGetInternalSet_unstable(),
});
setOpenItems(nextOpenItems);
}),
onNavigation: useEventCallback((event, data) => {
props.onNavigation?.(event, data);
if (!event.isDefaultPrevented()) {
navigation.navigate(data);
}
}),
onCheckedChange: useEventCallback((event, data) => {
const nextCheckedItems = createNextNestedCheckedItems(data, checkedItems);
props.onCheckedChange?.(event, {
...data,
checkedItems: nextCheckedItems.dangerouslyGetInternalMap_unstable(),
});
}),
},
useMergedRefs(ref, initializeWalker),
useMergedRefs(ref, navigation.rootRef),
),
};
{ treeType: 'nested' } as const,
);
}

function useNestedSubtree(props: TreeProps, ref: React.Ref<HTMLElement>): TreeState {
Expand Down

This file was deleted.

Loading

0 comments on commit ee8ca9b

Please sign in to comment.