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: adds openItems and checkedItems to tree callback data",
"packageName": "@fluentui/react-tree",
"email": "[email protected]",
"dependentChangeType": "patch"
}
3 changes: 3 additions & 0 deletions packages/react-components/react-tree/etc/react-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const Tree: ForwardRefComponent<TreeProps>;
// @public (undocumented)
export type TreeCheckedChangeData = {
value: TreeItemValue;
checkedItems: Map<TreeItemValue, TreeSelectionValue>;
target: HTMLElement;
event: React_2.ChangeEvent<HTMLElement>;
type: 'Change';
Expand Down Expand Up @@ -152,6 +153,7 @@ export type TreeItemContextValue = {
itemType: TreeItemType;
value: TreeItemValue;
open: boolean;
checked?: TreeSelectionValue;
};

// @public
Expand Down Expand Up @@ -269,6 +271,7 @@ export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event'];
// @public (undocumented)
export type TreeOpenChangeData = {
open: boolean;
openItems: Set<TreeItemValue>;
value: TreeItemValue;
target: HTMLElement;
} & ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`FlatTree renders a default state 1`] = `
<div>
<div
class="fui-FlatTree"
role="baseTree"
role="tree"
>
Default FlatTree
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,56 +1,54 @@
import { useFluent_unstable } from '@fluentui/react-shared-contexts';
import { useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import { useEventCallback } from '@fluentui/react-utilities';
import { TreeNavigationData_unstable } from '../../Tree';
import { HeadlessTree, HeadlessTreeItemProps } from '../../utils/createHeadlessTree';
import { nextTypeAheadElement } from '../../utils/nextTypeAheadElement';
import { treeDataTypes } from '../../utils/tokens';
import { treeItemFilter } from '../../utils/treeItemFilter';
import { HTMLElementWalker, useHTMLElementWalkerRef } from '../../hooks/useHTMLElementWalker';
import { useRovingTabIndex } from '../../hooks/useRovingTabIndexes';
import { dataTreeItemValueAttrName, getTreeItemValueFromElement } from '../../utils/getTreeItemValueFromElement';
import { HTMLElementWalker } from '../../utils/createHTMLElementWalker';

export function useFlatTreeNavigation<Props extends HeadlessTreeItemProps>(virtualTree: HeadlessTree<Props>) {
const { targetDocument } = useFluent_unstable();
const [treeItemWalkerRef, treeItemWalkerRootRef] = useHTMLElementWalkerRef(treeItemFilter);
const [{ rove }, rovingRootRef] = useRovingTabIndex(treeItemFilter);
const { rove, initialize } = useRovingTabIndex(treeItemFilter);

function getNextElement(data: TreeNavigationData_unstable) {
if (!targetDocument || !treeItemWalkerRef.current) {
function getNextElement(data: TreeNavigationData_unstable, walker: HTMLElementWalker) {
if (!targetDocument) {
return null;
}
const treeItemWalker = treeItemWalkerRef.current;
switch (data.type) {
case treeDataTypes.Click:
return data.target;
case treeDataTypes.TypeAhead:
treeItemWalker.currentElement = data.target;
return nextTypeAheadElement(treeItemWalker, data.event.key);
walker.currentElement = data.target;
return nextTypeAheadElement(walker, data.event.key);
case treeDataTypes.ArrowLeft:
return parentElement(virtualTree, data.target, treeItemWalker);
return parentElement(virtualTree, data.target, walker);
case treeDataTypes.ArrowRight:
treeItemWalker.currentElement = data.target;
return firstChild(data.target, treeItemWalker);
walker.currentElement = data.target;
return firstChild(data.target, walker);
case treeDataTypes.End:
treeItemWalker.currentElement = treeItemWalker.root;
return treeItemWalker.lastChild();
walker.currentElement = walker.root;
return walker.lastChild();
case treeDataTypes.Home:
treeItemWalker.currentElement = treeItemWalker.root;
return treeItemWalker.firstChild();
walker.currentElement = walker.root;
return walker.firstChild();
case treeDataTypes.ArrowDown:
treeItemWalker.currentElement = data.target;
return treeItemWalker.nextElement();
walker.currentElement = data.target;
return walker.nextElement();
case treeDataTypes.ArrowUp:
treeItemWalker.currentElement = data.target;
return treeItemWalker.previousElement();
walker.currentElement = data.target;
return walker.previousElement();
}
}
const navigate = useEventCallback((data: TreeNavigationData_unstable) => {
const nextElement = getNextElement(data);
const navigate = useEventCallback((data: TreeNavigationData_unstable, walker: HTMLElementWalker) => {
const nextElement = getNextElement(data, walker);
if (nextElement) {
rove(nextElement);
}
});
return [navigate, useMergedRefs(treeItemWalkerRootRef, rovingRootRef)] as const;
return { navigate, initialize } as const;
}

function firstChild(target: HTMLElement, treeWalker: HTMLElementWalker): HTMLElement | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
TreeOpenChangeEvent,
TreeProps,
} from '../Tree/Tree.types';
import { HTMLElementWalker, createHTMLElementWalker } from '../../utils/createHTMLElementWalker';
import { treeItemFilter } from '../../utils/treeItemFilter';

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

const treeRef = React.useRef<HTMLDivElement>(null);
const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
options.onOpenChange?.(event, data);
Expand All @@ -129,7 +142,9 @@ export function useHeadlessFlatTree_unstable<Props extends HeadlessTreeItemProps
const handleNavigation = useEventCallback(
(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => {
options.onNavigation_unstable?.(event, data);
navigate(data);
if (walkerRef.current) {
navigate(data, walkerRef.current);
}
},
);

Expand Down Expand Up @@ -161,7 +176,7 @@ export function useHeadlessFlatTree_unstable<Props extends HeadlessTreeItemProps
return treeRef.current?.querySelector(`[${dataTreeItemValueAttrName}="${item.value}"]`) as HTMLElement | null;
}, []);

const ref = useMergedRefs<HTMLDivElement>(treeRef, navigationRef as React.Ref<HTMLDivElement>);
const ref = useMergedRefs<HTMLDivElement>(treeRef, initializeWalker);

const getTreeProps = React.useCallback(
() => ({
Expand All @@ -181,7 +196,17 @@ export function useHeadlessFlatTree_unstable<Props extends HeadlessTreeItemProps
const items = React.useCallback(() => headlessTree.visibleItems(openItems), [openItems, headlessTree]);

return React.useMemo<HeadlessFlatTree<Props>>(
() => ({ navigate, getTreeProps, getNextNavigableItem, getElementFromItem, items }),
() => ({
navigate: data => {
if (walkerRef.current) {
navigate(data, walkerRef.current);
}
},
getTreeProps,
getNextNavigableItem,
getElementFromItem,
items,
}),
[navigate, getTreeProps, getNextNavigableItem, getElementFromItem, items],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type TreeNavigationEvent_unstable = TreeNavigationData_unstable['event'];

export type TreeOpenChangeData = {
open: boolean;
openItems: Set<TreeItemValue>;
value: TreeItemValue;
target: HTMLElement;
} & (
Expand All @@ -45,6 +46,7 @@ export type TreeOpenChangeEvent = TreeOpenChangeData['event'];

export type TreeCheckedChangeData = {
value: TreeItemValue;
checkedItems: Map<TreeItemValue, TreeSelectionValue>;
target: HTMLElement;
event: React.ChangeEvent<HTMLElement>;
type: 'Change';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`Tree renders a default state 1`] = `
<div>
<div
class="fui-Tree"
role="baseTree"
role="tree"
>
Default Tree
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@ import { useControllableCheckedItems } from './useControllableCheckedItems';
import { useTreeContext_unstable } from '../../contexts/treeContext';
import { useRootTree } from '../../hooks/useRootTree';
import { useSubtree } from '../../hooks/useSubtree';
import { HTMLElementWalker, createHTMLElementWalker } from '../../utils/createHTMLElementWalker';
import { treeItemFilter } from '../../utils/treeItemFilter';

export const useTree_unstable = (props: TreeProps, ref: React.Ref<HTMLElement>): TreeState => {
const [openItems, setOpenItems] = useControllableOpenItems(props);
const [checkedItems] = useControllableCheckedItems(props);
const [navigate, navigationRef] = useTreeNavigation();
const { navigate, initialize } = useTreeNavigation();
const walkerRef = React.useRef<HTMLElementWalker>();
const initializeWalker = React.useCallback(
(root: HTMLElement | null) => {
if (root) {
walkerRef.current = createHTMLElementWalker(root, treeItemFilter);
initialize(walkerRef.current);
}
},
[initialize],
);

const handleOpenChange = useEventCallback((event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
props.onOpenChange?.(event, data);
Expand All @@ -33,7 +45,9 @@ export const useTree_unstable = (props: TreeProps, ref: React.Ref<HTMLElement>):
const handleNavigation = useEventCallback(
(event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => {
props.onNavigation_unstable?.(event, data);
navigate(data);
if (walkerRef.current) {
navigate(data, walkerRef.current);
}
},
);

Expand All @@ -47,7 +61,7 @@ export const useTree_unstable = (props: TreeProps, ref: React.Ref<HTMLElement>):
onCheckedChange: handleCheckedChange,
};

const baseRef = useMergedRefs(ref, navigationRef);
const baseRef = useMergedRefs(ref, initializeWalker);

const isSubtree = useTreeContext_unstable(ctx => ctx.level > 0);
// as isSubTree is static, this doesn't break rule of hooks
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { useMergedRefs } from '@fluentui/react-utilities';
import { TreeNavigationData_unstable } from './Tree.types';
import { nextTypeAheadElement } from '../../utils/nextTypeAheadElement';
import { treeDataTypes } from '../../utils/tokens';
import { treeItemFilter } from '../../utils/treeItemFilter';
import { useRovingTabIndex } from '../../hooks/useRovingTabIndexes';
import { HTMLElementWalker, useHTMLElementWalkerRef } from '../../hooks/useHTMLElementWalker';
import { HTMLElementWalker } from '../../utils/createHTMLElementWalker';

export function useTreeNavigation() {
const [{ rove }, rovingRootRef] = useRovingTabIndex(treeItemFilter);
const [walkerRef, rootRef] = useHTMLElementWalkerRef(treeItemFilter);
const { rove, initialize } = useRovingTabIndex(treeItemFilter);

const getNextElement = (data: TreeNavigationData_unstable) => {
if (!walkerRef.current) {
return;
}
const treeItemWalker = walkerRef.current;
const getNextElement = (data: TreeNavigationData_unstable, treeItemWalker: HTMLElementWalker) => {
switch (data.type) {
case treeDataTypes.Click:
return data.target;
Expand All @@ -41,13 +35,13 @@ export function useTreeNavigation() {
return treeItemWalker.previousElement();
}
};
function navigate(data: TreeNavigationData_unstable) {
const nextElement = getNextElement(data);
function navigate(data: TreeNavigationData_unstable, walker: HTMLElementWalker) {
const nextElement = getNextElement(data, walker);
if (nextElement) {
rove(nextElement);
}
}
return [navigate, useMergedRefs(rootRef, rovingRootRef)] as const;
return { navigate, initialize } as const;
}

function lastChildRecursive(walker: HTMLElementWalker) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getNativeElementProps, useId, useMergedRefs } from '@fluentui/react-uti
import { useEventCallback } from '@fluentui/react-utilities';
import { elementContains } from '@fluentui/react-portal';
import type { TreeItemProps, TreeItemState } from './TreeItem.types';
import { useTreeContext_unstable } from '../../contexts/index';
import { useTreeContext_unstable, useTreeItemContext_unstable } from '../../contexts/index';
import { dataTreeItemValueAttrName } from '../../utils/getTreeItemValueFromElement';
import { Space } from '@fluentui/keyboard-keys';
import { treeDataTypes } from '../../utils/tokens';
Expand Down Expand Up @@ -42,8 +42,14 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
const selectionRef = React.useRef<HTMLInputElement>(null);

const open = useTreeContext_unstable(ctx => ctx.openItems.has(value));
const checked = useTreeContext_unstable(ctx => ctx.checkedItems.get(value) ?? false);
const selectionMode = useTreeContext_unstable(ctx => ctx.selectionMode);
const parentChecked = useTreeItemContext_unstable(ctx => ctx.checked);
const checked = useTreeContext_unstable(ctx => {
if (selectionMode === 'multiselect' && typeof parentChecked === 'boolean') {
return parentChecked;
}
return ctx.checkedItems.get(value);
});

const handleClick = useEventCallback((event: React.MouseEvent<HTMLDivElement>) => {
onClick?.(event);
Expand Down Expand Up @@ -133,13 +139,21 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
if (isEventFromSubtree) {
return;
}
requestTreeResponse({ event, value, itemType, type: 'Change', target: event.currentTarget });
requestTreeResponse({
event,
value,
itemType,
type: 'Change',
target: event.currentTarget,
checked: checked === 'mixed' ? true : !checked,
});
});

const isBranch = itemType === 'branch';
return {
value,
open,
checked,
subtreeRef,
layoutRef,
selectionRef,
Expand All @@ -159,7 +173,8 @@ export function useTreeItem_unstable(props: TreeItemProps, ref: React.Ref<HTMLDi
role: 'treeitem',
'aria-level': level,
[dataTreeItemValueAttrName]: value,
'aria-checked': selectionMode === 'multiselect' ? checked : undefined,
'aria-checked':
selectionMode === 'multiselect' ? (checked === 'mixed' ? undefined : checked ?? false) : undefined,
'aria-selected': selectionMode === 'single' ? checked : undefined,
'aria-expanded': isBranch ? open : undefined,
onClick: handleClick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function useTreeItemContextValues_unstable(state: TreeItemState): TreeIte
isActionsVisible,
isAsideVisible,
selectionRef,
checked,
} = state;

/**
Expand All @@ -21,6 +22,7 @@ export function useTreeItemContextValues_unstable(state: TreeItemState): TreeIte
*/
const treeItem: TreeItemContextValue = {
value,
checked,
itemType,
layoutRef,
subtreeRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ export const useTreeItemLayout_unstable = (
const selectionRef = useTreeItemContext_unstable(ctx => ctx.selectionRef);
const expandIconRef = useTreeItemContext_unstable(ctx => ctx.expandIconRef);
const actionsRef = useTreeItemContext_unstable(ctx => ctx.actionsRef);
const value = useTreeItemContext_unstable(ctx => ctx.value);
const checked = useTreeContext_unstable(ctx => ctx.checkedItems.get(value) ?? false);
const checked = useTreeItemContext_unstable(ctx => ctx.checked ?? false);
const isBranch = useTreeItemContext_unstable(ctx => ctx.itemType === 'branch');

const expandIcon = resolveShorthand(props.expandIcon, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export type TreeContextValue = {
};

export type TreeItemRequest = { itemType: TreeItemType } & (
| OmitWithoutExpanding<TreeOpenChangeData, 'open'>
| OmitWithoutExpanding<TreeOpenChangeData, 'open' | 'openItems'>
| TreeNavigationData_unstable
| OmitWithoutExpanding<TreeCheckedChangeData, 'checked' | 'selectionMode'>
| OmitWithoutExpanding<TreeCheckedChangeData, 'selectionMode' | 'checkedItems'>
);

// helper type that avoids the expansion of unions while inferring it, should work exactly the same as Omit
Expand Down
Loading