diff --git a/packages/react-components/react-tree/library/src/components/Tree/Tree.types.ts b/packages/react-components/react-tree/library/src/components/Tree/Tree.types.ts index 92f6f8824ee01..77be380a3201d 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/Tree.types.ts +++ b/packages/react-components/react-tree/library/src/components/Tree/Tree.types.ts @@ -3,7 +3,7 @@ import type { PresenceMotionSlotProps } from '@fluentui/react-motion'; import type { ComponentProps, ComponentState, SelectionMode, Slot } from '@fluentui/react-utilities'; import type { TreeContextValue, SubtreeContextValue } from '../../contexts'; import type { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Enter, Home } from '@fluentui/keyboard-keys'; -import type { TreeItemValue } from '../TreeItem/TreeItem.types'; +import type { TreeItemType, TreeItemValue } from '../TreeItem/TreeItem.types'; import { CheckboxProps } from '@fluentui/react-checkbox'; import { RadioProps } from '@fluentui/react-radio'; @@ -146,6 +146,8 @@ export type TreeProps = ComponentProps & { */ onNavigation?(event: TreeNavigationEvent_unstable, data: TreeNavigationDataParam): void; + onNavigationIn?(event: React.SyntheticEvent, data: { value: TreeItemValue; itemType: TreeItemType }): void; + /** * This refers to the selection mode of the tree. * - undefined: No selection can be done. @@ -170,8 +172,14 @@ export type TreeProps = ComponentProps & { * such as checked value and type of interaction that created the event. */ onCheckedChange?(event: TreeCheckedChangeEvent, data: TreeCheckedChangeData): void; + + imperativeRef?: React.RefObject; }; +interface TreeImperativeRef { + focus(value: TreeItemValue): void; +} + /** * State used in rendering Tree */ diff --git a/packages/react-components/react-tree/library/src/components/Tree/useTree.ts b/packages/react-components/react-tree/library/src/components/Tree/useTree.ts index 625dd6d5fe299..3f6c1a33754cd 100644 --- a/packages/react-components/react-tree/library/src/components/Tree/useTree.ts +++ b/packages/react-components/react-tree/library/src/components/Tree/useTree.ts @@ -10,6 +10,7 @@ import { useTreeNavigation } from '../../hooks/useTreeNavigation'; import { useTreeContext_unstable } from '../../contexts/treeContext'; import { ImmutableSet } from '../../utils/ImmutableSet'; import { ImmutableMap } from '../../utils/ImmutableMap'; +import { TreeItemValue } from '../TreeItem/TreeItem.types'; export const useTree_unstable = (props: TreeProps, ref: React.Ref): TreeState => { 'use no memo'; @@ -28,6 +29,16 @@ function useNestedRootTree(props: TreeProps, ref: React.Ref): TreeS const checkedItems = useNestedCheckedItems(props); const navigation = useTreeNavigation(props.navigationMode); + React.useImperativeHandle( + props.imperativeRef, + () => ({ + focus: (value: TreeItemValue) => { + navigation.focusOnItem(value); + }, + }), + [navigation], + ); + return Object.assign( useRootTree( { @@ -47,6 +58,7 @@ function useNestedRootTree(props: TreeProps, ref: React.Ref): TreeS if (!event.isDefaultPrevented()) { navigation.navigate(data, { preventScroll: data.isScrollPrevented(), + onNavigateIn: props.onNavigationIn, }); } }), diff --git a/packages/react-components/react-tree/library/src/components/TreeItem/useTreeItem.tsx b/packages/react-components/react-tree/library/src/components/TreeItem/useTreeItem.tsx index a458bb60a70dd..a26b05d6fa148 100644 --- a/packages/react-components/react-tree/library/src/components/TreeItem/useTreeItem.tsx +++ b/packages/react-components/react-tree/library/src/components/TreeItem/useTreeItem.tsx @@ -68,6 +68,20 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref(null); const treeItemRef = React.useRef(null); + const treeItemPropertiesRef = React.useCallback( + (el: HTMLElement | null) => { + if (el) { + // TODO use a symbol to hide this from public API + // @ts-expect-error + el._treeItem = { + value, + itemType, + }; + } + }, + [value, itemType], + ); + if (process.env.NODE_ENV !== 'production') { // This is acceptable since the NODE_ENV will not change during runtime @@ -331,7 +345,7 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref, forceUpdateRovingTabIndex, + focusOnItem, } as const; } diff --git a/packages/react-components/react-tree/stories/src/Tree/TreeDefault.stories.tsx b/packages/react-components/react-tree/stories/src/Tree/TreeDefault.stories.tsx index b2647a7457996..388034368eb69 100644 --- a/packages/react-components/react-tree/stories/src/Tree/TreeDefault.stories.tsx +++ b/packages/react-components/react-tree/stories/src/Tree/TreeDefault.stories.tsx @@ -1,39 +1,115 @@ import * as React from 'react'; -import { Tree, TreeItem, TreeItemLayout } from '@fluentui/react-components'; +import { Tree, TreeItem, TreeItemLayout, TreeProps, Select, Button } from '@fluentui/react-components'; + +const values = [ + 'maxMustermann', + 'johnDoe', + 'pierreDupont', + 'janNovak', + 'janeDoe', + 'erikaMustermann', + 'general', + 'engPlanning', + 'pmDiscussion', +]; export const Default = () => { + const [checked, setChecked] = React.useState(['maxMustermann']); + const [itemToSelect, setItemToSelect] = React.useState('maxMustermann'); + const onCheckedChange: TreeProps['onCheckedChange'] = (e, data) => { + setChecked([data.value as string]); + }; + + const onNavigationIn: TreeProps['onNavigationIn'] = (e, data) => { + console.log(data); + if (data.itemType === 'leaf') { + setChecked([data.value as string]); + } + }; + + // This should be built into an enhanced tree item + const onClick = (e: React.MouseEvent) => { + const treeItem = e.nativeEvent.composedPath().find(el => (el as HTMLElement).getAttribute('role') === 'treeitem'); + // @ts-expect-error + const value = treeItem._treeItem?.value; + // @ts-expect-error + const itemType = treeItem._treeItem?.itemType; + if (value && itemType === 'leaf') { + setChecked([value]); + } + }; + + const imperativeRef: NonNullable = React.useRef(null); + + const selectedItem = checked[0]; + React.useEffect(() => { + imperativeRef.current?.focus(selectedItem); + }, [selectedItem]); + return ( - - - level 1, item 1 - - - level 2, item 1 - - - level 2, item 2 - - - level 2, item 3 - - - - - level 1, item 2 - - - level 2, item 1 - - - level 3, item 1 - - - - - - - level 1, item 3 - - + <> +
Selected: {checked[0]}
+
+ + +
+ + + Favorites + + + Max Mustermann + + + John Doe + + + Pierre Dupont + + + + + Chats + + + Jan Novak + + + Jane Doe + + + Erika Mustermann + + + + + Teams and Channels + + + General + + + Eng planning + + + PM discussion + + + + + ); };