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
Expand Up @@ -8,57 +8,67 @@ import {
useHeadlessFlatTree_unstable,
HeadlessFlatTreeItemProps,
} from '@fluentui/react-tree';
import {
Button,
Menu,
MenuItem,
MenuList,
MenuPopover,
MenuTrigger,
useRestoreFocusTarget,
} from '@fluentui/react-components';
import { Edit20Regular, MoreHorizontal20Regular } from '@fluentui/react-icons';

type FlatItem = HeadlessFlatTreeItemProps & { layout: string };
type FlatItem = HeadlessFlatTreeItemProps & { content: string };

const flatTreeItems: FlatItem[] = [
{ value: '1', layout: 'Level 1, item 1' },
{ value: '1-1', parentValue: '1', layout: 'Level 2, item 1' },
{ value: '1-2', parentValue: '1', layout: 'Level 2, item 2' },
{ value: '1-3', parentValue: '1', layout: 'Level 2, item 3' },
{ value: '2', layout: 'Level 1, item 2' },
{ value: '2-1', parentValue: '2', layout: 'Level 2, item 1' },
{ value: '2-1-1', parentValue: '2-1', layout: 'Level 3, item 1' },
{ value: '2-1-1-1', parentValue: '2-1-1', layout: 'Level 4, item 1' },
{ value: '1', content: 'Level 1, item 1' },
{ value: '1-1', parentValue: '1', content: 'Level 2, item 1' },
{ value: '1-2', parentValue: '1', content: 'Level 2, item 2' },
{ value: '1-3', parentValue: '1', content: 'Level 2, item 3' },
{ value: '2', content: 'Level 1, item 2' },
{ value: '2-1', parentValue: '2', content: 'Level 2, item 1' },
{ value: '2-1-1', parentValue: '2-1', content: 'Level 3, item 1' },
{ value: '2-1-1-1', parentValue: '2-1-1', content: 'Level 4, item 1' },
];

// // EXAMPLE OF NESTED TREE ITEMS BEING FLATTEN BY `flattenTree`:
// type Item = TreeItemProps & { layout: React.ReactNode };
// type Item = TreeItemProps & { content: React.ReactNode };

// const nestedTreeItems = [
// {
// value: '1',
// layout: <>level 1, item 1</>,
// content: <>level 1, item 1</>,
// subtree: [
// {
// value: '1-1',
// layout: <>level 2, item 1</>,
// content: <>level 2, item 1</>,
// },
// {
// value: '1-2',
// layout: <>level 2, item 2</>,
// content: <>level 2, item 2</>,
// },
// {
// value: '1-3',
// layout: <>level 2, item 3</>,
// content: <>level 2, item 3</>,
// },
// ],
// },
// {
// value: '2',
// layout: <>level 1, item 2</>,
// content: <>level 1, item 2</>,
// subtree: [
// {
// value: '2-1',
// layout: <>level 2, item 1</>,
// content: <>level 2, item 1</>,
// subtree: [
// {
// value: '2-1-1',
// layout: <>level 3, item 1</>,
// content: <>level 3, item 1</>,
// subtree: [
// {
// value: '2-1-1-1',
// layout: <>level 4, item 1</>,
// content: <>level 4, item 1</>,
// },
// ],
// },
Expand All @@ -70,16 +80,51 @@ const flatTreeItems: FlatItem[] = [

// const flatTreeItems = flattenTree_unstable<Item>(nestedTreeItems);

const ActionsExample = () => (
<>
<Button aria-label="Edit" appearance="subtle" icon={<Edit20Regular />} />
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>

<MenuPopover>
<MenuList>
<MenuItem>New</MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
</>
);

export const FlatTree = () => {
const virtualTree = useHeadlessFlatTree_unstable(flatTreeItems);
const flatTree = useHeadlessFlatTree_unstable(flatTreeItems);
const focusTargetAttribute = useRestoreFocusTarget();

return (
<Tree {...virtualTree.getTreeProps()} aria-label="Tree">
{Array.from(virtualTree.items(), item => {
const { layout, ...itemProps } = item.getTreeItemProps();
<Tree {...flatTree.getTreeProps()} aria-label="Tree">
{Array.from(flatTree.items(), flatTreeItem => {
const { content, ...treeItemProps } = flatTreeItem.getTreeItemProps();
return (
<TreeItem {...itemProps} key={item.value}>
<TreeItemLayout>{layout}</TreeItemLayout>
</TreeItem>
<Menu key={flatTreeItem.value} positioning="below-end" openOnContext>
<MenuTrigger disableButtonEnhancement>
<TreeItem aria-description="has context menu" {...focusTargetAttribute} {...treeItemProps}>
<TreeItemLayout actions={<ActionsExample />}>{content}</TreeItemLayout>
</TreeItem>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem>Edit</MenuItem>
<MenuItem>New</MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
})}
</Tree>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Tree, TreeItem, TreeItemLayout, TreeItemProps } from '@fluentui/react-tree';
import { Tree, TreeItem as FuiTreeItem, TreeItemLayout, TreeItemProps as FuiTreeItemProps } from '@fluentui/react-tree';
import { Edit20Regular, MoreHorizontal20Regular } from '@fluentui/react-icons';
import {
Button,
Expand All @@ -8,112 +8,119 @@ import {
MenuList,
MenuPopover,
MenuTrigger,
Slot,
getSlots,
ComponentProps,
resolveShorthand,
useRestoreFocusTarget,
} from '@fluentui/react-components';

const ActionsExample = () => {
return (
<>
<Button aria-label="Edit" appearance="subtle" icon={<Edit20Regular />} />
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>
type TreeItemSlots = { layout: Slot<typeof TreeItemLayout> };
type TreeItemProps = FuiTreeItemProps & ComponentProps<Partial<TreeItemSlots>>;

const TreeItem = ({ layout, children, ...props }: TreeItemProps) => {
const focusTargetAttribute = useRestoreFocusTarget();
const [layoutChildren, subtree] = React.Children.toArray(children);

<MenuPopover>
<MenuList>
<MenuItem>New </MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
// same items to be used between contextmenu and actions
const commonMenuItems = (
<>
<MenuItem>New </MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</>
);
};

const ActionableTreeItem = ({ children, ...rest }: TreeItemProps) => {
const [open, setOpen] = React.useState(false);
const focusTargetAttribute = useRestoreFocusTarget();
const [layout, subtree] = React.Children.toArray(children);
const { slots, slotProps } = getSlots<TreeItemSlots>({
components: { layout: TreeItemLayout },
layout: resolveShorthand(layout, {
required: true,
defaultProps: {
actions: (
<>
<Button aria-label="Edit" appearance="subtle" icon={<Edit20Regular />} />
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button aria-label="More options" appearance="subtle" icon={<MoreHorizontal20Regular />} />
</MenuTrigger>
<MenuPopover>
<MenuList>{commonMenuItems}</MenuList>
</MenuPopover>
</Menu>
</>
),
children: layoutChildren,
},
}),
});

return (
<TreeItem
onContextMenu={e => {
if (e.isDefaultPrevented()) {
return;
}
e.preventDefault();
setOpen(true);
}}
{...focusTargetAttribute}
{...rest}
>
<Menu open={open} positioning={'below-end'} onOpenChange={(_, data) => setOpen(data.open)} openOnContext>
<MenuTrigger disableButtonEnhancement>
<TreeItemLayout actions={<ActionsExample />}>{layout}</TreeItemLayout>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem>Edit</MenuItem>
<MenuItem>New</MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
{subtree}
</TreeItem>
<Menu positioning="below-end" openOnContext>
<MenuTrigger disableButtonEnhancement>
<FuiTreeItem aria-description="has context menu" {...focusTargetAttribute} {...props}>
<slots.layout {...slotProps.layout} />
{subtree}
</FuiTreeItem>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem>Edit</MenuItem>
{commonMenuItems}
</MenuList>
</MenuPopover>
</Menu>
);
};

export const Actions = () => {
return (
<Tree aria-label="Tree">
<ActionableTreeItem itemType="branch">
level 1, item 1
<TreeItem itemType="branch">
item 1
<Tree>
<ActionableTreeItem itemType="branch">
level 2, item 1
<TreeItem itemType="branch">
item 1-1
<Tree>
<ActionableTreeItem itemType="leaf">level 3, item 1</ActionableTreeItem>
<ActionableTreeItem itemType="leaf">level 3, item 2</ActionableTreeItem>
<TreeItem itemType="leaf">item 1-1-1</TreeItem>
<TreeItem itemType="leaf">item 1-1-2</TreeItem>
<TreeItem itemType="leaf">item 1-1-3</TreeItem>
</Tree>
</ActionableTreeItem>
<ActionableTreeItem itemType="leaf">level 2, item 2</ActionableTreeItem>
<ActionableTreeItem itemType="leaf">level 2, item 3</ActionableTreeItem>
</TreeItem>
<TreeItem itemType="leaf">item 1-2</TreeItem>
<TreeItem itemType="leaf">item 1-3</TreeItem>
</Tree>
</ActionableTreeItem>
<ActionableTreeItem itemType="branch">
level 1, item 2
</TreeItem>
<TreeItem itemType="branch">
item 2
<Tree>
<ActionableTreeItem itemType="branch">
level 2, item 1
<TreeItem itemType="branch">
item 2-1
<Tree>
<ActionableTreeItem itemType="leaf">level 3, item 1</ActionableTreeItem>
<TreeItem itemType="leaf">item 2-1-1</TreeItem>
</Tree>
</ActionableTreeItem>
</TreeItem>

<ActionableTreeItem itemType="branch">
level 2, item 2
<TreeItem itemType="branch">
item 3
<Tree>
<ActionableTreeItem itemType="leaf">level 2, item 1</ActionableTreeItem>
<ActionableTreeItem itemType="leaf">level 2, item 2</ActionableTreeItem>
<TreeItem itemType="leaf">item 3-1</TreeItem>
<TreeItem itemType="leaf">item 3-2</TreeItem>
<TreeItem itemType="leaf">item 3-3</TreeItem>
</Tree>
</ActionableTreeItem>
</TreeItem>
</Tree>
</ActionableTreeItem>
</TreeItem>
</Tree>
);
};

Actions.parameters = {
docs: {
description: {
story: `In addition to \`aside\` content, both tree item layouts support \`actions\` prop that can be used for tasks such as edit, rename, or triggering a menu. These action buttons are initially hidden but are shown on hover or can be controlled by the \`visible\` property, ensuring they take priority over the \`aside\` content when needed.
story: `In addition to the \`aside\` content, both tree item layouts support \`actions\` prop that can be used for tasks such as edit, rename, or triggering a menu. These action buttons are initially hidden but are shown on hover or can be controlled by the \`visible\` property, ensuring they take priority over the \`aside\` content when needed.

> ⚠️ Actions prop is \`aria-hidden\` by default. Always include a context menu to ensure that actions are accessible for all users.`,
> ⚠️ The \`actions\` prop is \`aria-hidden\` by default. Always implement a context menu to ensure that these actions are accessible to all users. Additionally, include an \`aria-description\` or \`aria-describedby\` on tree items with actions or context menus to indicate interactions, such as "has context menu."`,
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

- **Use the `aria-label` attribute on the root of the `Tree` component to provide an accessible name for the tree:** This attribute helps screen readers to understand the purpose of the tree, making it more accessible and inclusive.

- **If you provide additional buttons or functionality within tree items, make them accessible with a context menu:** Ensure that any additional actions or features in tree items are accessible for both mouse and keyboard users by implementing a context menu. The additional functionality should not disturb keyboard navigation within the tree.
- **If you provide additional buttons or functionality within tree items, make them accessible with a context menu:**
- Ensure that any additional actions or features in tree items are accessible for both mouse and keyboard users by implementing a context menu.
- Include an `aria-description` or `aria-describedby` on tree items with actions/context menu to offer more descriptive information about the interactions, e.g., "has context menu."
- The added functionality should not disrupt keyboard navigation within the tree.

</details>
Loading