Tree
View .mdA tree is used to display a hierarchical view of nested data. It's useful for representing a menu structure, folder hierarchy, or nested categories at a glance.
Default
Loading interactive example. Full source is available below.
View example source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import * as React from 'react';
export const Default = (): JSXElement => {
return (<Tree aria-label="Default">
<TreeItem itemType="branch">
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch">
<TreeItemLayout>level 1, item 2</TreeItemLayout>
<Tree>
<TreeItem itemType="branch">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 1, item 3</TreeItemLayout>
</TreeItem>
</Tree>);
};
export default Default;
Resources
Best practices
Do
- Use branches to group related items into hierarchical categories and subcategories.
- Use simple, easily recognizable icons.
- Keep item labels short, clear, and grammatically consistent.
Don't
- Use complex or abstract icons that might confuse people.
- Provide more than one or two quick actions per item.
Behavior and states
- Branches expand and collapse to show or hide child nodes.
- Navigation mode lets people select an item to open a detailed view.
- Multiselect mode supports batch operations across multiple items.
- Tree item content wraps by default within the container width.
- Each tree has a parent node and one or more levels of child nodes that can expand or collapse.
- To show or hide information without regard to hierarchy, use an accordion instead.
- For information that's organized in a simpler structure, consider using a list or table instead.
API referenceGenerated CoreAI prop and type summaries
API
Generated CoreAI prop summaries, not replacement type declarations. Most native DOM/event and ARIA props are omitted. ? means optional; JSDoc shows defaults, deprecations, and conditional requirements. Defaults assume the standard CoreAI provider.
Tree API
The Tree component renders nested items in a hierarchical structure.
Use it with TreeItem component and layouts components TreeItemLayout or TreeItemPersonaLayout.
interface TreeProps {
/**
* A tree item can have various appearances:
* - 'subtle' (default): The default tree item styles.
* - 'subtle-alpha': Minimizes emphasis on hovered or focused states.
* - 'transparent': Removes background color.
* @default 'subtle'
*/
appearance?: "subtle" | "transparent" | "subtle-alpha";
as?: "div";
/**
* This refers to a list of ids of checked tree items, or a list of tuples of ids and checked state.
* Controls the state of the checked tree items.
* These property is ignored for subtrees.
*/
checkedItems?: Iterable<TreeItemValue | [TreeItemValue, TreeSelectionValue]>;
children?: any;
className?: string;
collapseMotion?: PresenceMotionSlotProps<CollapseParams> | null;
/**
* This refers to a list of ids of default opened items.
* This property is ignored for subtrees.
*/
defaultOpenItems?: Iterable<TreeItemValue>;
/**
* Indicates how navigation between a treeitem and its actions work
* - 'tree' (default): The default navigation, pressing right arrow key navigates inward the first inner children of a branch treeitem
* - 'treegrid': Pressing right arrow key navigate towards the actions of a treeitem
* @default 'tree'
*/
navigationMode?: "tree" | "treegrid";
/**
* Callback fired when the component changes value from checked state.
* These property is ignored for subtrees.
*/
onCheckedChange?: ((event: ChangeEvent<HTMLElement>, data: TreeCheckedChangeData) => void);
/**
* Callback fired when navigation happens inside the component.
* These property is ignored for subtrees.
*
* FIXME: This method is not ideal, as navigation should be handled internally by tabster.
*/
onNavigation?: ((event: KeyboardEvent<HTMLElement> | MouseEvent<HTMLElement, MouseEvent>, data: TreeNavigationDataParam) => void);
/**
* Callback fired when the component changes value from open state.
* These property is ignored for subtrees.
*/
onOpenChange?: ((event: KeyboardEvent<HTMLElement> | MouseEvent<HTMLElement, MouseEvent>, data: TreeOpenChangeData) => void);
/**
* This refers to a list of ids of opened tree items.
* Controls the state of the open tree items.
* These property is ignored for subtrees.
*/
openItems?: Iterable<TreeItemValue>;
ref?: Ref<HTMLDivElement>;
/**
* This refers to the selection mode of the tree.
* - undefined: No selection can be done.
* - 'single': Only one tree item can be selected, radio buttons are rendered.
* - 'multiselect': Multiple tree items can be selected, checkboxes are rendered.
* @default undefined
*/
selectionMode?: "multiselect" | "single";
/**
* Size of the tree item.
* @default 'medium'
*/
size?: "small" | "medium";
style?: CSSProperties;
}
FlatTree API
The FlatTree component is a variation of the Tree component that deals with a flattened data structure.
It should be used on cases where more complex interactions with a Tree is required. On simple scenarios it is advised to simply use a nested structure instead.
interface FlatTreeProps {
/**
* A tree item can have various appearances:
* - 'subtle' (default): The default tree item styles.
* - 'subtle-alpha': Minimizes emphasis on hovered or focused states.
* - 'transparent': Removes background color.
* @default 'subtle'
*/
appearance?: "subtle" | "transparent" | "subtle-alpha";
as?: "div";
/**
* This refers to a list of ids of checked tree items, or a list of tuples of ids and checked state.
* Controls the state of the checked tree items.
* These property is ignored for subtrees.
*/
checkedItems?: Iterable<TreeItemValue | [TreeItemValue, TreeSelectionValue]>;
children?: any;
className?: string;
collapseMotion?: PresenceMotionSlotProps<CollapseParams> | null;
/**
* Indicates how navigation between a treeitem and its actions work
* - 'tree' (default): The default navigation, pressing right arrow key navigates inward the first inner children of a branch treeitem
* - 'treegrid': Pressing right arrow key navigate towards the actions of a treeitem
* @default 'tree'
*/
navigationMode?: "tree" | "treegrid";
/**
* Callback fired when the component changes value from checked state.
* These property is ignored for subtrees.
*/
onCheckedChange?: ((event: ChangeEvent<HTMLElement>, data: TreeCheckedChangeData) => void);
/**
* Callback fired when navigation happens inside the component.
* These property is ignored for subtrees.
*
* FIXME: This method is not ideal, as navigation should be handled internally by tabster.
*/
onNavigation?: ((event: KeyboardEvent<HTMLElement> | MouseEvent<HTMLElement, MouseEvent>, data: TreeNavigationData_unstable) => void);
/**
* Callback fired when the component changes value from open state.
* These property is ignored for subtrees.
*/
onOpenChange?: ((event: KeyboardEvent<HTMLElement> | MouseEvent<HTMLElement, MouseEvent>, data: TreeOpenChangeData) => void);
/**
* This refers to a list of ids of opened tree items.
* Controls the state of the open tree items.
* These property is ignored for subtrees.
*/
openItems?: Iterable<TreeItemValue>;
ref?: Ref<HTMLDivElement>;
/**
* This refers to the selection mode of the tree.
* - undefined: No selection can be done.
* - 'single': Only one tree item can be selected, radio buttons are rendered.
* - 'multiselect': Multiple tree items can be selected, checkboxes are rendered.
* @default undefined
*/
selectionMode?: "multiselect" | "single";
/**
* Size of the tree item.
* @default 'medium'
*/
size?: "small" | "medium";
style?: CSSProperties;
}
FlatTreeItem API
The FlatTreeItem component represents a single item in a flat tree.
interface FlatTreeItemProps {
/**
* Defines the hierarchical level of an element within a structure.
*/
"aria-level"?: number;
/**
* Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
*/
"aria-posinset"?: number;
/**
* Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.
*/
"aria-setsize"?: number;
as?: "div";
children?: any;
className?: string;
/**
* A tree item can be a leaf or a branch
*/
itemType?: string;
onOpenChange?: ((e: KeyboardEvent<HTMLElement> | MouseEvent<HTMLElement, MouseEvent>, data: TreeItemOpenChangeData) => void);
/**
* Whether the tree item is in an open state
*
* This overrides the open value provided by the root tree,
* and ensure control of the visibility of the tree item per tree item.
*
* NOTE: controlling the open state of a tree item will not affect the open state of its children
*/
open?: boolean;
/**
* This property is inferred through context on a nested tree, and required for a flat tree.
*/
parentValue?: TreeItemValue;
ref?: Ref<HTMLDivElement>;
style?: (CSSProperties & { "--fluent-TreeItem--level"?: string | number; });
/**
* A tree item should have a well defined value, in case one is not provided by the user by this prop
* one will be inferred internally.
*/
value?: string | number;
}
TreeItem API
The TreeItem component represents a single item in a tree.
It expects a certain order of children to work properly: the first child should be the node itself,
and the second child should be a nested subtree in the form of another Tree component or a standalone TreeItem.
This order follows the same order as document traversal for the TreeWalker API in JavaScript:
https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker.
The content and layout of a TreeItem can be defined using the TreeItemLayout or TreeItemPersonaLayout component,
which should be used as a direct child of TreeItem.
When a TreeItem has nested child subtree, an expand/collapse control is displayed, allowing the user to show or hide the children.
interface TreeItemProps {
as?: "div";
children?: any;
className?: string;
/**
* A tree item can be a leaf or a branch
*/
itemType?: string;
onOpenChange?: ((e: KeyboardEvent<HTMLElement> | MouseEvent<HTMLElement, MouseEvent>, data: TreeItemOpenChangeData) => void);
/**
* Whether the tree item is in an open state
*
* This overrides the open value provided by the root tree,
* and ensure control of the visibility of the tree item per tree item.
*
* NOTE: controlling the open state of a tree item will not affect the open state of its children
*/
open?: boolean;
/**
* This property is inferred through context on a nested tree, and required for a flat tree.
*/
parentValue?: TreeItemValue;
ref?: Ref<HTMLDivElement>;
style?: (CSSProperties & { "--fluent-TreeItem--level"?: string | number; });
/**
* A tree item should have a well defined value, in case one is not provided by the user by this prop
* one will be inferred internally.
*/
value?: TreeItemValue;
}
TreeItemLayout API
The TreeItemLayout component is used as a child of TreeItem to define the content and layout of a tree item.
It provides a consistent visual structure for tree items in a Tree component.
This component should only be used as a direct child of TreeItem.
interface TreeItemLayoutProps {
/**
* Actionable elements are normally buttons, menus, or other focusable elements.
* Those elements are only visibly available if the given tree item is currently active.
*
* `actions` and `aside` slots are positioned on the exact same spot,
* so they won't be visible at the same time.
* `aside` slot is visible by default meanwhile `actions` slot are only visible when the tree item is active.
*
* `actions` slot supports a `visible` prop to force visibility of the actions.
*/
actions?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; } & { visible?: boolean | undefined; onVisibilityChange?: EventHandler<TreeItemLayoutActionVisibilityChangeData> | undefined; }> | null | undefined;
as?: "div";
/**
* Aside content is normally used to render a badge or other non-actionable content
*/
aside?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null | undefined;
children?: any;
className?: string;
/**
* Expand icon slot,
* by default renders a chevron icon to indicate opening and closing
*/
expandIcon?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null | undefined;
/**
* Icon slot that renders right after main content
*/
iconAfter?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null | undefined;
/**
* Icon slot that renders right before main content
*/
iconBefore?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null | undefined;
/**
* Content. Children of the root slot are automatically rendered here
*/
main?: NonNullable<WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null> | undefined;
ref?: Ref<HTMLDivElement>;
selector?: (Omit<ComponentProps<Partial<CheckboxSlots>, "input">, "defaultChecked" | "onChange" | "size" | "checked"> & { checked?: boolean | "mixed"; children?: undefined; defaultChecked?: boolean | "mixed"; labelPosition?: "before" | "after" | undefined; onChange?: ((ev: ChangeEvent<HTMLInputElement>, data: CheckboxOnChangeData) => void) | undefined; shape?: "circular" | "square" | undefined; size?: "medium" | "large" | undefined; } & RefAttributes<HTMLInputElement>) | (Omit<ComponentProps<Partial<RadioSlots>, "input">, "onChange" | "size"> & { value?: string | undefined; labelPosition?: "below" | "after" | undefined; disabled?: boolean | undefined; onChange?: ((ev: ChangeEvent<HTMLInputElement>, data: RadioOnChangeData) => void) | undefined; } & RefAttributes<HTMLInputElement>) | null | undefined;
style?: CSSProperties;
}
TreeItemPersonaLayout API
The TreeItemPersonaLayout component is used as a child of TreeItem to display a TreeItem with a media (typically an avatar) and a description.
It provides a more visually appealing representation of a TreeItem and is typically used to display a list of people or topics.
This component should only be used as a direct child of TreeItem.
interface TreeItemPersonaLayoutProps {
/**
* Actionable elements are normally buttons, menus, or other focusable elements.
* Those elements are only visibly available if the given tree item is currently active.
*
* `actions` and `aside` slots are positioned on the exact same spot,
* so they won't be visible at the same time.
* `aside` slot is visible by default meanwhile `actions` slot are only visible when the tree item is active.
*
* `actions` slot supports a `visible` prop to force visibility of the actions.
*/
actions?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; } & { visible?: boolean | undefined; onVisibilityChange?: EventHandler<TreeItemLayoutActionVisibilityChangeData> | undefined; }> | null | undefined;
as?: "div";
/**
* Aside content is normally used to render a badge or other non-actionable content
*/
aside?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null | undefined;
children?: any;
className?: string;
/**
* Secondary text that describes or complements the content
*/
description?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null | undefined;
/**
* Expand icon slot,
* by default renders a chevron icon to indicate opening and closing
*/
expandIcon?: WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null | undefined;
/**
* Content. Children of the root slot are automatically rendered here
*/
main?: NonNullable<WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null> | undefined;
/**
* Avatar to display.
*/
media?: NonNullable<WithSlotShorthandValue<{ as?: "div"; } & Omit<Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & { ref?: ((instance: HTMLDivElement | null) => void) | RefObject<HTMLDivElement> | null; }, "children"> & { children?: any; }> | null> | undefined;
ref?: Ref<HTMLDivElement>;
selector?: (Omit<ComponentProps<Partial<CheckboxSlots>, "input">, "defaultChecked" | "onChange" | "size" | "checked"> & { checked?: boolean | "mixed"; children?: undefined; defaultChecked?: boolean | "mixed"; labelPosition?: "before" | "after" | undefined; onChange?: ((ev: ChangeEvent<HTMLInputElement>, data: CheckboxOnChangeData) => void) | undefined; shape?: "circular" | "square" | undefined; size?: "medium" | "large" | undefined; } & RefAttributes<HTMLInputElement>) | (Omit<ComponentProps<Partial<RadioSlots>, "input">, "onChange" | "size"> & { value?: string | undefined; labelPosition?: "below" | "after" | undefined; disabled?: boolean | undefined; onChange?: ((ev: ChangeEvent<HTMLInputElement>, data: RadioOnChangeData) => void) | undefined; } & RefAttributes<HTMLInputElement>) | null | undefined;
style?: CSSProperties;
}
Implementation
CoreAI 0.12.0; Fluent 9.74.9.
Import components from @coreai-ds/react-components. CoreAI guidance above takes precedence over API demonstration examples.
Actions
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import type { TreeItemProps } from "@coreai-ds/react-components";
import * as React from 'react';
import { useRestoreFocusTarget } from "@coreai-ds/react-components";
import { MenuItem } from "@coreai-ds/react-components";
import { Menu } from "@coreai-ds/react-components";
import { MenuTrigger } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { Edit20Regular } from "@fluentui/react-icons";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import { MenuPopover } from "@coreai-ds/react-components";
import { MenuList } from "@coreai-ds/react-components";
type CustomTreeItemProps = TreeItemProps & {
children?: React.ReactNode;
};
const CustomTreeItem = ({ children, ...props }: CustomTreeItemProps) => {
const focusTargetAttribute = useRestoreFocusTarget();
const [layoutChildren, subtree] = React.Children.toArray(children);
// 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>
</>);
return (<Menu positioning="below-end" openOnContext>
<MenuTrigger disableButtonEnhancement>
<TreeItem aria-description="has actions" {...focusTargetAttribute} {...props}>
<TreeItemLayout 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>
</>}>
{layoutChildren}
</TreeItemLayout>
{subtree}
</TreeItem>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem>Edit</MenuItem>
{commonMenuItems}
</MenuList>
</MenuPopover>
</Menu>);
};
export const Actions = (): JSXElement => {
return (<Tree aria-label="Actions">
<CustomTreeItem itemType="branch">
item 1
<Tree>
<CustomTreeItem itemType="branch">
item 1-1
<Tree>
<CustomTreeItem itemType="leaf">item 1-1-1</CustomTreeItem>
<CustomTreeItem itemType="leaf">item 1-1-2</CustomTreeItem>
<CustomTreeItem itemType="leaf">item 1-1-3</CustomTreeItem>
</Tree>
</CustomTreeItem>
<CustomTreeItem itemType="leaf">item 1-2</CustomTreeItem>
<CustomTreeItem itemType="leaf">item 1-3</CustomTreeItem>
</Tree>
</CustomTreeItem>
<CustomTreeItem itemType="branch">
item 2
<Tree>
<CustomTreeItem itemType="branch">
item 2-1
<Tree>
<CustomTreeItem itemType="leaf">item 2-1-1</CustomTreeItem>
</Tree>
</CustomTreeItem>
<CustomTreeItem itemType="branch">
item 3
<Tree>
<CustomTreeItem itemType="leaf">item 3-1</CustomTreeItem>
<CustomTreeItem itemType="leaf">item 3-2</CustomTreeItem>
<CustomTreeItem itemType="leaf">item 3-3</CustomTreeItem>
</Tree>
</CustomTreeItem>
</Tree>
</CustomTreeItem>
</Tree>);
};
export default Actions;
Appearance
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { TreeItemPersonaLayout } from "@coreai-ds/react-components";
import { Avatar } from "@coreai-ds/react-components";
import * as React from 'react';
export const Appearance = (): JSXElement => {
return (<>
<Tree aria-label="Default Appearance">
<TreeItem itemType="branch">
<TreeItemLayout>Default appearance</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
<Tree aria-label="Subtle Alpha Appearance" appearance="subtle-alpha">
<TreeItem itemType="branch">
<TreeItemLayout>Subtle-alpha appearance</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
<Tree aria-label="Transparent Appearance" appearance="transparent">
<TreeItem itemType="branch">
<TreeItemLayout>Transparent appearance</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
<hr />
<Tree aria-label="Default Appearance">
<TreeItem itemType="branch">
<TreeItemPersonaLayout media={<Avatar name="Default" aria-label="Default appearance avatar placeholder" color="colorful"/>}>
Default appearance
</TreeItemPersonaLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar aria-label="Avatar placeholder" color="colorful"/>}>
level 2, item 1
</TreeItemPersonaLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar aria-label="Avatar placeholder" color="colorful"/>}>
level 2, item 2
</TreeItemPersonaLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
<Tree aria-label="Subtle Alpha Appearance" appearance="subtle-alpha">
<TreeItem itemType="branch">
<TreeItemPersonaLayout media={<Avatar aria-label="Subtle-alpha appearance avatar placeholder" name="Subtle Alpha" color="colorful"/>}>
Subtle-alpha appearance
</TreeItemPersonaLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar aria-label="Avatar placeholder" color="colorful"/>}>
level 2, item 1
</TreeItemPersonaLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar aria-label="Avatar placeholder" color="colorful"/>}>
level 2, item 2
</TreeItemPersonaLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
<Tree aria-label="Transparent Appearance" appearance="transparent">
<TreeItem itemType="branch">
<TreeItemPersonaLayout media={<Avatar aria-label="Transparent appearance avatar placeholder" name="Transparent" color="colorful"/>}>
Transparent appearance
</TreeItemPersonaLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar aria-label="Avatar placeholder" color="colorful"/>}>
level 2, item 1
</TreeItemPersonaLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar aria-label="Avatar placeholder" color="colorful"/>}>
level 2, item 2
</TreeItemPersonaLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</>);
};
export default Appearance;
Aside
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { Important16Regular } from "@fluentui/react-icons";
import type { FluentIconsProps } from "@fluentui/react-icons";
import { CounterBadge } from "@coreai-ds/react-components";
import * as React from 'react';
const iconStyleProps: FluentIconsProps = {
primaryFill: 'red',
};
const AsideContent = ({ isImportant, messageCount }: {
isImportant?: boolean;
messageCount?: number;
}) => (<>
{isImportant && <Important16Regular {...iconStyleProps}/>}
{messageCount && messageCount > 0 && <CounterBadge count={messageCount} color="danger" size="small"/>}
</>);
export const Aside = (): JSXElement => (<Tree aria-label="Aside">
<TreeItem itemType="branch" aria-description="Important, 3 message">
<TreeItemLayout aside={<AsideContent isImportant={true} messageCount={3}/>}>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf" aria-description="Important">
<TreeItemLayout aside={<AsideContent isImportant={true}/>}>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf" aria-description="2 messages">
<TreeItemLayout aside={<AsideContent messageCount={2}/>}>level 2, item 2</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch" aria-description="Important, 1 message">
<TreeItemLayout aside={<>
<AsideContent isImportant={true} messageCount={1}/>
</>}>
level 1, item 2
</TreeItemLayout>
<Tree>
<TreeItem itemType="branch" aria-description="1 message">
<TreeItemLayout aside={<AsideContent messageCount={1}/>}>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout aside={<AsideContent />}>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</TreeItem>
</Tree>);
export default Aside;
Customizing Interaction
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TreeItemValue } from "@coreai-ds/react-components";
import type { TreeOpenChangeEvent } from "@coreai-ds/react-components";
import type { TreeOpenChangeData } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
export const CustomizingInteraction = (): JSXElement => {
const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>([]);
const onOpenChange = (_e: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
if (data.type === 'Click' || data.type === 'Enter') {
alert('click on item');
return;
}
setOpenItems(data.openItems);
};
return (<Tree aria-label="Customizing Interaction" openItems={openItems} onOpenChange={onOpenChange}>
<TreeItem itemType="branch" value="default-subtree-1">
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch" value="default-subtree-2">
<TreeItemLayout>level 1, item 2</TreeItemLayout>
<Tree>
<TreeItem itemType="branch" value="default-subtree-2-1">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</TreeItem>
</Tree>);
};
export default CustomizingInteraction;
Default Open
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import * as React from 'react';
export const DefaultOpen = (): JSXElement => (<Tree aria-label="Default Open" defaultOpenItems={['default-subtree-1', 'default-subtree-2', 'default-subtree-2-1']}>
<TreeItem itemType="branch" value="default-subtree-1">
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch" value="default-subtree-2">
<TreeItemLayout>level 1, item 2</TreeItemLayout>
<Tree>
<TreeItem itemType="branch" value="default-subtree-2-1">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</TreeItem>
</Tree>);
export default DefaultOpen;
Drag And Drop
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { HeadlessFlatTreeItemProps } from "@coreai-ds/react-components";
import { useHeadlessFlatTree_unstable } from "@coreai-ds/react-components";
import type { DragEndEvent } from "@dnd-kit/core";
import { useSensors } from "@dnd-kit/core";
import { useSensor } from "@dnd-kit/core";
import { PointerSensor } from "@dnd-kit/core";
import { KeyboardSensor } from "@dnd-kit/core";
import { DndContext } from "@dnd-kit/core";
import { closestCenter } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
import { verticalListSortingStrategy } from "@dnd-kit/sortable";
import { FlatTree } from "@coreai-ds/react-components";
import type { FlatTreeItemProps } from "@coreai-ds/react-components";
import { useSortable } from "@dnd-kit/sortable";
import type { UniqueIdentifier } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";
import { FlatTreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
type FlatItem = HeadlessFlatTreeItemProps & {
layout: string;
};
const flatTreeItems: FlatItem[] = [
{ value: '1', layout: 'Parent item' },
{ value: '1-1', parentValue: '1', layout: 'Sortable item 1' },
{ value: '1-2', parentValue: '1', layout: 'Sortable item 2' },
{ value: '1-3', parentValue: '1', layout: 'Sortable item 3' },
{ value: '1-4', parentValue: '1', layout: 'Sortable item 4' },
{ value: '1-5', parentValue: '1', layout: 'Sortable item 5' },
{ value: '1-6', parentValue: '1', layout: 'Sortable item 6' },
{ value: '1-7', parentValue: '1', layout: 'Sortable item 7' },
{ value: '1-8', parentValue: '1', layout: 'Sortable item 8' },
];
const sortItems = (array: FlatItem[], from: number, to: number) => {
const newArray = array.slice();
const startIndex = from < 0 ? array.length + from : from;
const item = newArray.splice(startIndex, 1)[0];
const endIndex = to < 0 ? array.length + to : to;
newArray.splice(endIndex, 0, item);
return newArray;
};
const SortableTreeItem = ({ children, value, ...rest }: FlatTreeItemProps) => {
const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
id: value as UniqueIdentifier,
});
const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 1 : 0 };
return (<FlatTreeItem ref={setNodeRef} value={value} style={style} {...attributes} {...listeners} {...rest}>
{children}
</FlatTreeItem>);
};
export const DragAndDrop = (): JSXElement => {
const [items, setItems] = React.useState(flatTreeItems);
const virtualTree = useHeadlessFlatTree_unstable(items, {
defaultOpenItems: ['1'],
});
const handleDragEnd = React.useCallback((event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
setItems(prevItems => {
const oldIndex = prevItems.findIndex(item => item.value === active.id);
const newIndex = prevItems.findIndex(item => item.value === over?.id);
return sortItems(prevItems, oldIndex, newIndex);
});
}
}, []);
const sortableItems = items.filter(item => item.parentValue).map(item => item.value);
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor));
return (<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>
<FlatTree aria-label="Drag And Drop" {...virtualTree.getTreeProps()}>
{Array.from(virtualTree.items(), item => {
const { layout, ...itemProps } = item.getTreeItemProps();
return item.itemType === 'leaf' ? (<SortableTreeItem key={item.value} {...itemProps}>
<TreeItemLayout>{layout}</TreeItemLayout>
</SortableTreeItem>) : (<FlatTreeItem {...itemProps} key={item.value}>
<TreeItemLayout>{layout}</TreeItemLayout>
</FlatTreeItem>);
})}
</FlatTree>
</SortableContext>
</DndContext>);
};
export default DragAndDrop;
Expand Icon
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TreeItemValue } from "@coreai-ds/react-components";
import type { TreeOpenChangeEvent } from "@coreai-ds/react-components";
import type { TreeOpenChangeData } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { SubtractSquare16Regular } from "@fluentui/react-icons";
import { AddSquare16Regular } from "@fluentui/react-icons";
export const ExpandIcon = (): JSXElement => {
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
setOpenItems(curr => (data.open ? [...curr, data.value] : curr.filter(value => value !== data.value)));
};
return (<Tree aria-label="Expand Icon" openItems={openItems} onOpenChange={handleOpenChange}>
<TreeItem itemType="branch" value="tree-item-2">
<TreeItemLayout expandIcon={openItems.includes('tree-item-2') ? <SubtractSquare16Regular /> : <AddSquare16Regular />}>
level 1, item 1
</TreeItemLayout>
<Tree>
<TreeItem itemType="branch" value="tree-item-3">
<TreeItemLayout expandIcon={openItems.includes('tree-item-3') ? <SubtractSquare16Regular /> : <AddSquare16Regular />}>
level 2, item 1
</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch" value="tree-item-1">
<TreeItemLayout expandIcon={openItems.includes('tree-item-1') ? <SubtractSquare16Regular /> : <AddSquare16Regular />}>
level 1, item 2
</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>);
};
export default ExpandIcon;
Flat Tree
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TreeItemValue } from "@coreai-ds/react-components";
import type { TreeOpenChangeEvent } from "@coreai-ds/react-components";
import type { TreeOpenChangeData } from "@coreai-ds/react-components";
import { FlatTree } from "@coreai-ds/react-components";
import { FlatTreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
export const FlatTreeStory = (): JSXElement => {
const [openItems, setOpenItems] = React.useState<Set<TreeItemValue>>(() => new Set());
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
setOpenItems(data.openItems);
};
return (<FlatTree openItems={openItems} onOpenChange={handleOpenChange} aria-label="Flat Tree">
<FlatTreeItem value="1" aria-level={1} aria-setsize={2} aria-posinset={1} itemType="branch">
<TreeItemLayout>Item 1, level 1</TreeItemLayout>
</FlatTreeItem>
{openItems.has('1') && (<>
<FlatTreeItem parentValue="1" value="1-1" aria-level={2} aria-setsize={2} aria-posinset={1} itemType="leaf">
<TreeItemLayout>Item 1, level 2</TreeItemLayout>
</FlatTreeItem>
<FlatTreeItem parentValue="1" value="1-2" aria-level={2} aria-setsize={2} aria-posinset={2} itemType="leaf">
<TreeItemLayout>Item 1, level 2</TreeItemLayout>
</FlatTreeItem>
</>)}
<FlatTreeItem value="2" aria-level={1} aria-setsize={2} aria-posinset={1} itemType="branch">
<TreeItemLayout>Item 1, level 1</TreeItemLayout>
</FlatTreeItem>
{openItems.has('2') && (<>
<FlatTreeItem parentValue="2" value="2-1" aria-level={2} aria-setsize={3} aria-posinset={1} itemType="leaf">
<TreeItemLayout>Item 1, level 2</TreeItemLayout>
</FlatTreeItem>
<FlatTreeItem parentValue="2" value="2-2" aria-level={2} aria-setsize={3} aria-posinset={2} itemType="leaf">
<TreeItemLayout>Item 2, level 2</TreeItemLayout>
</FlatTreeItem>
<FlatTreeItem parentValue="2" value="2-3" aria-level={2} aria-setsize={3} aria-posinset={3} itemType="leaf">
<TreeItemLayout>Item 3, level 2</TreeItemLayout>
</FlatTreeItem>
</>)}
</FlatTree>);
};
export default FlatTreeStory;
Icon Before And After
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { Image20Regular } from "@fluentui/react-icons";
import { LockClosed20Regular } from "@fluentui/react-icons";
import { Person20Regular } from "@fluentui/react-icons";
import { Warning20Regular } from "@fluentui/react-icons";
import * as React from 'react';
export const IconBeforeAndAfter = (): JSXElement => {
return (<Tree aria-label="Icon Before & After">
<TreeItem itemType="branch">
<TreeItemLayout iconBefore={<Image20Regular />} iconAfter={<LockClosed20Regular />}>
level 1, item 1
</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout iconBefore={<Person20Regular />}>icon before</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout iconBefore={<Person20Regular />}>icon before</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch">
<TreeItemLayout iconBefore={<Image20Regular />} iconAfter={<LockClosed20Regular />}>
level 1, item 2
</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout iconAfter={<Warning20Regular />}>icon after</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>);
};
export default IconBeforeAndAfter;
Infinite Scrolling
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TreeItemValue } from "@coreai-ds/react-components";
import type { HeadlessFlatTreeItemProps } from "@coreai-ds/react-components";
import { Spinner } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { useHeadlessFlatTree_unstable } from "@coreai-ds/react-components";
import { FlatTree } from "@coreai-ds/react-components";
import { FlatTreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
/**
* This function is just for the sake of the example,
* a library for fetching data (like react-query) might be a better option
*/
function useQuery<Value>(initialValue: Value) {
const [queryResult, setQueryResult] = React.useState({ value: initialValue, isLoading: false, isLoaded: false });
const query = (fn: () => Promise<Value> | Value) => {
setQueryResult(curr => ({ ...curr, isLoading: true }));
Promise.resolve(fn()).then(nextValue => {
setQueryResult({ value: nextValue, isLoaded: true, isLoading: false });
});
};
return { ...queryResult, query } as const;
}
type CustomItem = HeadlessFlatTreeItemProps & {
name: string | React.ReactNode;
};
const ITEMS_PER_PAGE = 10;
const pinnedItems = [
{ value: 'pinned', name: 'Pinned', id: 'pinned' },
{ value: 'pinned-item-1', parentValue: 'pinned', name: 'Pinned item 1' },
{ value: 'pinned-item-2', parentValue: 'pinned', name: 'Pinned item 2' },
{ value: 'pinned-item-3', parentValue: 'pinned', name: 'Pinned item 3' },
];
const useStyles = makeStyles({
container: {
height: '400px',
paddingBottom: '10px',
overflow: 'auto',
},
screenReadersOnly: {
position: 'absolute',
width: '1px',
height: '1px',
margin: '-1',
overflow: 'hidden',
clip: 'rect(0,0,0,0)',
whiteSpace: 'nowrap',
},
});
interface FetchResult {
results: {
name: string;
}[];
}
function mockFetchPeople(page: number): Promise<FetchResult> {
return new Promise(resolve => {
setTimeout(() => {
const startIndex = page * ITEMS_PER_PAGE + 1;
const results = Array.from({ length: ITEMS_PER_PAGE }, (_, index) => ({
name: `Person ${startIndex + index}`,
}));
resolve({ results });
}, 1000);
});
}
const MAX_PAGES = 4;
const AriaLive = ({ content }: {
content: string | undefined;
}) => {
const styles = useStyles();
return (<div aria-live="polite" aria-atomic="true" className={styles.screenReadersOnly}>
{content}
</div>);
};
export const InfiniteScrolling = (): JSXElement => {
const [page, setPage] = React.useState(1);
const [isLoading, setIsLoading] = React.useState(false);
const [ariaMessage, setAriaMessage] = React.useState('');
const itemToFocusRef = React.useRef<HTMLDivElement>(null);
const [itemToFocusValue, setItemToFocusValue] = React.useState<TreeItemValue>();
const peopleItems = useQuery<CustomItem[]>([
{ value: 'people', name: 'People' },
...Array.from({ length: ITEMS_PER_PAGE }, (_, index) => ({
value: `person-${index + 1}`,
parentValue: 'people',
name: `Person ${index + 1}`,
})),
]);
const items = React.useMemo<CustomItem[]>(() => [
...pinnedItems,
...peopleItems.value,
...(isLoading
? [
{
value: 'loading-people',
parentValue: 'people',
name: <Spinner aria-label="Loading more people" size="tiny"/>,
},
]
: []),
], [isLoading, peopleItems]);
const styles = useStyles();
const flatTree = useHeadlessFlatTree_unstable(items, { defaultOpenItems: ['pinned', 'people'] });
const fetchMoreItems = async () => {
setIsLoading(true);
setAriaMessage(`loading more people...`);
const json = await mockFetchPeople(page);
const fetchedItems = json.results.map<CustomItem>(person => ({
value: `person-${person.name}`,
parentValue: 'people',
name: person.name,
}));
const firstNewItemValue = fetchedItems[0].value;
await peopleItems.query(() => [...peopleItems.value, ...fetchedItems]);
setIsLoading(false);
setPage(prev => prev + 1);
setAriaMessage(`${fetchedItems.length} new people loaded`);
setItemToFocusValue(firstNewItemValue);
};
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
const hasReachedEnd = scrollHeight - scrollTop === clientHeight;
if (!isLoading && hasReachedEnd && page < MAX_PAGES) {
fetchMoreItems();
}
};
React.useEffect(() => {
if (itemToFocusRef.current) {
itemToFocusRef.current.focus();
setItemToFocusValue(undefined);
}
}, [itemToFocusValue]);
return (<>
<FlatTree {...flatTree.getTreeProps()} aria-label="Infinite Scrolling" onScroll={handleScroll} className={styles.container}>
{Array.from(flatTree.items(), flatTreeItem => {
const { name, value, ...treeItemProps } = flatTreeItem.getTreeItemProps();
return (<FlatTreeItem {...treeItemProps} key={value} value={value} ref={value === itemToFocusValue ? itemToFocusRef : undefined}>
<TreeItemLayout>{name}</TreeItemLayout>
</FlatTreeItem>);
})}
</FlatTree>
<AriaLive content={ariaMessage}/>
</>);
};
export default InfiniteScrolling;
Inline Styling Tree Item Level
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { useSubtreeContext_unstable } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { treeItemLevelToken } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import * as React from 'react';
export const InlineStylingTreeItemLevel = (): JSXElement => {
const { level } = useSubtreeContext_unstable();
return level === 0 ? (<Tree aria-label="Inline Styling Tree Item Level">
<InlineStylingTreeItemLevel />
</Tree>) : (<TreeItem value={level} itemType="branch" style={{ [treeItemLevelToken]: level }}>
<TreeItemLayout>{`level ${level}, item 1`}</TreeItemLayout>
<Tree>
<InlineStylingTreeItemLevel />
</Tree>
</TreeItem>);
};
export default InlineStylingTreeItemLevel;
Layouts
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { TreeItemPersonaLayout } from "@coreai-ds/react-components";
import { Avatar } from "@coreai-ds/react-components";
import * as React from 'react';
export const Layouts = (): JSXElement => {
return (<>
<Tree aria-label="Default Layout">
<TreeItem itemType="branch">
<TreeItemLayout>Tree using TreeItemLayout</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
<Tree aria-label="Persona Layout">
<TreeItem itemType="branch">
<TreeItemPersonaLayout media={<Avatar image={{ alt: 'avatar' }}/>}>
Tree using TreeItemPersonaLayout
</TreeItemPersonaLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar image={{ alt: 'avatar' }}/>} description="with description">
level 2, item 1
</TreeItemPersonaLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout media={<Avatar image={{ alt: 'avatar' }} shape="square"/>} description="square shape media">
level 2, item 2
</TreeItemPersonaLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemPersonaLayout description="without media">level 2, item 3</TreeItemPersonaLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</>);
};
export default Layouts;
Lazy Loading
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import { makeStyles } from "@coreai-ds/react-components";
import { FlatTree } from "@coreai-ds/react-components";
import type { TreeItemValue } from "@coreai-ds/react-components";
import type { TreeItemOpenChangeEvent } from "@coreai-ds/react-components";
import type { TreeItemOpenChangeData } from "@coreai-ds/react-components";
import { FlatTreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { Spinner } from "@coreai-ds/react-components";
const useStyles = makeStyles({
screenReadersOnly: {
position: 'absolute',
width: '1px',
height: '1px',
margin: '-1',
overflow: 'hidden',
clip: 'rect(0,0,0,0)',
whiteSpace: 'nowrap',
},
});
type SubtreeProps = {
value: TreeItemValue;
onDataLoading?(): void;
onDataLoaded?(): void;
};
type QueryResult<Value> = {
value: Value;
state: 'idle' | 'loading' | 'loaded' | 'error';
};
/**
* This function is just for the sake of the example,
* a library for fetching data (like react-query) might be a better option
*/
function useQuery<Value>(initialValue: Value): QueryResult<Value> & {
query: (fn: () => PromiseLike<Value> | Value) => void;
} {
const [queryResult, setQueryResult] = React.useState<QueryResult<Value>>({ value: initialValue, state: 'idle' });
const query = React.useCallback((fn: () => PromiseLike<Value> | Value) => {
setQueryResult(curr => ({ ...curr, state: 'loading' }));
Promise.resolve(fn())
.then(nextValue => {
setQueryResult({ value: nextValue, state: 'loaded' });
})
.catch(() => {
setQueryResult(curr => ({ ...curr, state: 'error' }));
});
}, []);
return { ...queryResult, query } as const;
}
interface Entity {
name: string;
value: string;
}
interface Response {
results: {
name: string;
}[];
}
const mockFetch = (type: string): Promise<Response> => {
return new Promise<Response>(resolve => {
// eslint-disable-next-line @nx/workspace-no-restricted-globals
setTimeout(() => {
const mockData: Response = {
results: Array.from({ length: 10 }, (_, index) => ({
name: `${type} ${index + 1}`,
})),
};
resolve(mockData);
}, 1000);
});
};
const Subtree: React.FC<SubtreeProps> = ({ onDataLoaded, onDataLoading, value }) => {
const [open, setOpen] = React.useState(false);
// useQuery here is just a helper to simulate async data fetching
// you can use any other async data fetching library like react-query, swr, etc.
const { query, value: items, state } = useQuery<Entity[]>([]);
// we need to focus the first item when the subtree is opened
const firstItemRef = React.useRef<HTMLDivElement>(null);
const handleOpenChange = React.useCallback((e: TreeItemOpenChangeEvent, data: TreeItemOpenChangeData) => {
setOpen(data.open);
}, [setOpen]);
React.useEffect(() => {
if (open && state === 'idle') {
onDataLoading?.();
query(async () => {
// mockFetch is just a helper to simulate an API endpoint.
// you probably will be using some custom API endpoint here.
const json = await mockFetch(value.toString());
return json.results.map<Entity>(entity => ({ value: `${value}/${entity.name}`, name: entity.name }));
});
}
}, [open, onDataLoading, query, state, value]);
React.useEffect(() => {
if (open && state === 'loaded') {
onDataLoaded?.();
firstItemRef.current?.focus();
}
}, [open, state, onDataLoaded]);
return (<>
<FlatTreeItem value={value} aria-level={1} aria-setsize={3} aria-posinset={1} itemType="branch" open={open} onOpenChange={handleOpenChange}>
<TreeItemLayout expandIcon={state === 'loading' ? <Spinner size="tiny"/> : undefined}>
{value.toString()}
</TreeItemLayout>
</FlatTreeItem>
{open &&
items.map((item, index) => (<FlatTreeItem key={item.value} ref={index === 0 ? firstItemRef : null} parentValue={value} value={item.value} aria-level={2} aria-setsize={items.length} aria-posinset={index + 1} itemType="leaf">
<TreeItemLayout>{item.name}</TreeItemLayout>
</FlatTreeItem>))}
</>);
};
export const LazyLoading = (): JSXElement => {
const [ariaMessage, setAriaMessage] = React.useState('');
const styles = useStyles();
return (<>
<FlatTree aria-label="Lazy Loading">
<Subtree value="People" onDataLoaded={React.useCallback(() => setAriaMessage(`people items loaded`), [])} onDataLoading={React.useCallback(() => setAriaMessage(`loading people items...`), [])}/>
<Subtree value="Planet" onDataLoaded={React.useCallback(() => setAriaMessage(`planet items loaded`), [])} onDataLoading={React.useCallback(() => setAriaMessage(`loading planet items...`), [])}/>
<Subtree value="Starship" onDataLoaded={React.useCallback(() => setAriaMessage(`starship items loaded`), [])} onDataLoading={React.useCallback(() => setAriaMessage(`loading starship items...`), [])}/>
</FlatTree>
<div aria-live="polite" aria-atomic="true" className={styles.screenReadersOnly}>
{ariaMessage}
</div>
</>);
};
export default LazyLoading;
Manipulation
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { HeadlessFlatTreeItemProps } from "@coreai-ds/react-components";
import type { TreeItemValue } from "@coreai-ds/react-components";
import { useHeadlessFlatTree_unstable } from "@coreai-ds/react-components";
import { FlatTree } from "@coreai-ds/react-components";
import type { FlatTreeItemProps } from "@coreai-ds/react-components";
import { useRestoreFocusTarget } from "@coreai-ds/react-components";
import { Menu } from "@coreai-ds/react-components";
import { MenuTrigger } from "@coreai-ds/react-components";
import { FlatTreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { Delete20Regular } from "@fluentui/react-icons";
import { MenuPopover } from "@coreai-ds/react-components";
import { MenuList } from "@coreai-ds/react-components";
import { MenuItem } from "@coreai-ds/react-components";
type ItemProps = HeadlessFlatTreeItemProps & {
content: string;
};
const subtrees: ItemProps[][] = [
[
{ value: '1', content: 'Level 1, item 1' },
{ value: '1-1', parentValue: '1', content: 'Item 1-1' },
{ value: '1-2', parentValue: '1', content: 'Item 1-2' },
],
[
{ value: '2', content: 'Level 1, item 2' },
{ value: '2-1', parentValue: '2', content: 'Item 2-1' },
],
];
type CustomTreeItemProps = FlatTreeItemProps & {
onRemoveItem?: (value: string) => void;
};
const CustomTreeItem = React.forwardRef(({ onRemoveItem, ...props }: CustomTreeItemProps, ref: React.Ref<HTMLDivElement> | undefined) => {
const focusTargetAttribute = useRestoreFocusTarget();
const level = props['aria-level'];
const value = props.value as string;
const isItemRemovable = level !== 1 && !value.endsWith('-btn');
const handleRemoveItem = React.useCallback(() => {
onRemoveItem?.(value);
}, [value, onRemoveItem]);
return (<Menu positioning="below-end" openOnContext>
<MenuTrigger disableButtonEnhancement>
<FlatTreeItem aria-description="has actions" {...focusTargetAttribute} {...props} ref={ref}>
<TreeItemLayout actions={isItemRemovable ? (<Button aria-label="Remove item" appearance="subtle" onClick={handleRemoveItem} icon={<Delete20Regular />}/>) : undefined}>
{props.children}
</TreeItemLayout>
</FlatTreeItem>
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem onClick={handleRemoveItem}>Remove item</MenuItem>
</MenuList>
</MenuPopover>
</Menu>);
});
export const Manipulation = (): JSXElement => {
const [trees, setTrees] = React.useState(subtrees);
const itemToFocusRef = React.useRef<HTMLDivElement>(null);
const [itemToFocusValue, setItemToFocusValue] = React.useState<TreeItemValue>();
const handleClick = (value: string) => {
// casting here to string as no number values are used in this example
if (value.endsWith('-btn')) {
const subtreeIndex = Number(value[0]) - 1;
addFlatTreeItem(subtreeIndex);
}
};
const addFlatTreeItem = (subtreeIndex: number) => setTrees(currentTrees => {
const lastItem = currentTrees[subtreeIndex][currentTrees[subtreeIndex].length - 1];
const newItemValue = `${subtreeIndex + 1}-${Number(lastItem.value.toString().slice(2)) + 1}`;
setItemToFocusValue(newItemValue);
const nextSubTree: ItemProps[] = [
...currentTrees[subtreeIndex],
{
value: newItemValue,
parentValue: currentTrees[subtreeIndex][0].value,
content: `New item ${newItemValue}`,
},
];
return [...currentTrees.slice(0, subtreeIndex), nextSubTree, ...currentTrees.slice(subtreeIndex + 1)];
});
const removeFlatTreeItem = React.useCallback((value: string) => setTrees(currentTrees => {
const subtreeIndex = Number(value[0]) - 1;
const currentSubTree = trees[subtreeIndex];
const itemIndex = currentSubTree.findIndex(item => item.value === value);
const nextSubTree = trees[subtreeIndex].filter((_item, index) => index !== itemIndex);
const nextItemValue = currentSubTree[itemIndex + 1]?.value;
const prevItemValue = currentSubTree[itemIndex - 1]?.value;
setItemToFocusValue(nextItemValue || prevItemValue);
return [...currentTrees.slice(0, subtreeIndex), nextSubTree, ...currentTrees.slice(subtreeIndex + 1)];
}), [trees]);
const flatTree = useHeadlessFlatTree_unstable(React.useMemo(() => [
...trees[0],
{
value: '1-btn',
parentValue: '1',
content: 'Add new item',
},
...trees[1],
{
value: '2-btn',
parentValue: '2',
content: 'Add new item',
},
], [trees]), { defaultOpenItems: ['1', '2'] });
React.useEffect(() => {
if (itemToFocusRef.current) {
itemToFocusRef.current.focus();
setItemToFocusValue(undefined);
}
}, [itemToFocusValue]);
return (<FlatTree {...flatTree.getTreeProps()} aria-label="Manipulation">
{Array.from(flatTree.items(), item => {
const { content, ...treeItemProps } = item.getTreeItemProps();
return (<CustomTreeItem {...treeItemProps} key={item.value} onClick={() => handleClick(item.value as string)} onRemoveItem={removeFlatTreeItem} ref={item.value === itemToFocusValue ? itemToFocusRef : undefined}>
{content}
</CustomTreeItem>);
})}
</FlatTree>);
};
export default Manipulation;
Motion Custom
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { makeStyles } from "@coreai-ds/react-components";
import { tokens } from "@coreai-ds/react-components";
import * as React from 'react';
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { Persona } from "@coreai-ds/react-components";
import type { PersonaProps } from "@coreai-ds/react-components";
import { motionTokens } from "@coreai-ds/react-components";
import { Field } from "@coreai-ds/react-components";
import { Slider } from "@coreai-ds/react-components";
import { Switch } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
const useStyles = makeStyles({
controls: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalS,
marginBottom: tokens.spacingVerticalL,
padding: tokens.spacingVerticalM,
border: `${tokens.strokeWidthThicker} solid ${tokens.colorNeutralForeground3}`,
borderRadius: tokens.borderRadiusMedium,
},
});
const personaData = [
{
name: 'Kevin Sturgis',
image: 'persona-male.png',
status: 'available' as const,
useImage: true,
secondaryText: 'Available',
},
{ name: 'Sarah Chen', status: 'busy' as const, useImage: false, secondaryText: 'In a meeting' },
{
name: 'Jessica Brown',
image: 'persona-female.png',
status: 'busy' as const,
useImage: true,
secondaryText: 'Do not disturb',
},
{ name: 'Emily Johnson', status: 'available' as const, useImage: false, secondaryText: 'Available' },
{ name: 'David Kim', status: 'offline' as const, useImage: false, secondaryText: 'Offline' },
{
name: 'Michael Rodriguez',
image: 'persona-male.png',
status: 'away' as const,
useImage: true,
secondaryText: 'Away',
},
];
const avatarFor = (useImage?: boolean, image?: string): PersonaProps['avatar'] => useImage
? {
image: {
src: `https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-react-assets/${image}`,
},
}
: { color: 'colorful' };
export const MotionCustom = (): JSXElement => {
const classes = useStyles();
const [duration, setDuration] = React.useState(1000);
const [animateOpacity, setAnimateOpacity] = React.useState(true);
const renderItems = (items: typeof personaData) => items.map(item => (<TreeItem key={item.name} itemType="leaf">
<TreeItemLayout>
<Persona name={item.name} secondaryText={item.secondaryText} presence={{ status: item.status }} avatar={avatarFor(item.useImage, item.image)}/>
</TreeItemLayout>
</TreeItem>));
const easing = motionTokens.curveDecelerateMid;
return (<>
<div className={classes.controls}>
<Field label={`Duration: ${duration}ms`}>
<Slider min={100} max={2000} step={50} value={duration} onChange={(_, data) => setDuration(data.value)}/>
</Field>
<Switch label="Animate opacity" checked={animateOpacity} onChange={(_, data) => setAnimateOpacity(data.checked)}/>
</div>
<Tree aria-label="Motion Custom">
<TreeItem itemType="branch">
<TreeItemLayout>Team A</TreeItemLayout>
<Tree collapseMotion={{ duration, easing, animateOpacity }}>{renderItems(personaData.slice(0, 3))}</Tree>
</TreeItem>
<TreeItem itemType="branch">
<TreeItemLayout>Team B</TreeItemLayout>
<Tree collapseMotion={{ duration, easing, animateOpacity }}>{renderItems(personaData.slice(3))}</Tree>
</TreeItem>
<TreeItem itemType="branch">
<TreeItemLayout>Team C</TreeItemLayout>
<Tree collapseMotion={{ duration, easing, animateOpacity }}>{renderItems(personaData)}</Tree>
</TreeItem>
</Tree>
</>);
};
export default MotionCustom;
Open Item Controlled
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TreeItemOpenChangeEvent } from "@coreai-ds/react-components";
import type { TreeItemOpenChangeData } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
export const OpenItemControlled = (): JSXElement => {
const [open, setOpen] = React.useState(true);
const handleOpenChange = (event: TreeItemOpenChangeEvent, data: TreeItemOpenChangeData) => {
setOpen(data.open);
};
return (<Tree aria-label="Open Item Controlled">
<TreeItem open={open} onOpenChange={handleOpenChange} itemType="branch" value="tree-item-1">
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch" value="tree-item-2">
<TreeItemLayout>level 1, item 2</TreeItemLayout>
<Tree>
<TreeItem itemType="branch" value="tree-item-3">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</TreeItem>
</Tree>);
};
export default OpenItemControlled;
Open Items Controlled
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import * as React from 'react';
import type { TreeItemValue } from "@coreai-ds/react-components";
import type { TreeOpenChangeEvent } from "@coreai-ds/react-components";
import type { TreeOpenChangeData } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
export const OpenItemsControlled = (): JSXElement => {
const [openItems, setOpenItems] = React.useState<Iterable<TreeItemValue>>([]);
const handleOpenChange = (event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
setOpenItems(data.openItems);
};
return (<Tree aria-label="Open Items Controlled" openItems={openItems} onOpenChange={handleOpenChange}>
<TreeItem itemType="branch" value="tree-item-1">
<TreeItemLayout>level 1, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="branch" value="tree-item-2">
<TreeItemLayout>level 1, item 2</TreeItemLayout>
<Tree>
<TreeItem itemType="branch" value="tree-item-3">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</TreeItem>
</Tree>);
};
export default OpenItemsControlled;
Selection
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { useHeadlessFlatTree_unstable } from "@coreai-ds/react-components";
import type { HeadlessFlatTreeItemProps } from "@coreai-ds/react-components";
import { FlatTree } from "@coreai-ds/react-components";
import { FlatTreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import * as React from 'react';
type CustomItem = HeadlessFlatTreeItemProps & {
content: string;
};
const items: CustomItem[] = [
{ 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: '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-2', parentValue: '2', content: 'Level 2, item 2' },
{ value: '2-2-1', parentValue: '2-2', content: 'Level 3, item 1' },
{ value: '2-2-2', parentValue: '2-2', content: 'Level 3, item 2' },
{ value: '3', content: 'Level 1, item 3' },
];
const SELECTION_MODE = 'multiselect'; // change to "single" for single selection
export const Selection = (): JSXElement => {
const flatTree = useHeadlessFlatTree_unstable(items, {
defaultOpenItems: ['1', '2', '2-1', '2-2'],
defaultCheckedItems: ['1-2'],
selectionMode: SELECTION_MODE,
});
return (<FlatTree {...flatTree.getTreeProps()} aria-label="Selection">
{Array.from(flatTree.items(), flatTreeItem => {
const { content, ...treeItemProps } = flatTreeItem.getTreeItemProps();
return (<FlatTreeItem {...treeItemProps} key={flatTreeItem.value}>
<TreeItemLayout>{content}</TreeItemLayout>
</FlatTreeItem>);
})}
</FlatTree>);
};
export default Selection;
Size
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { Tree } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import * as React from 'react';
export const Size = (): JSXElement => {
return (<>
<Tree size="small" aria-label="Small Size Tree">
<TreeItem itemType="branch">
<TreeItemLayout>Small size tree</TreeItemLayout>
<Tree>
<TreeItem itemType="branch">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
<Tree aria-label="Default Size Tree">
<TreeItem itemType="branch">
<TreeItemLayout>Medium size tree</TreeItemLayout>
<Tree>
<TreeItem itemType="branch">
<TreeItemLayout>level 2, item 1</TreeItemLayout>
<Tree>
<TreeItem itemType="leaf">
<TreeItemLayout>level 3, item 1</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 2</TreeItemLayout>
</TreeItem>
<TreeItem itemType="leaf">
<TreeItemLayout>level 2, item 3</TreeItemLayout>
</TreeItem>
</Tree>
</TreeItem>
</Tree>
</>);
};
export default Size;
Use Headless Flat Tree
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type { JSXElement } from "@coreai-ds/react-components";
import { useHeadlessFlatTree_unstable } from "@coreai-ds/react-components";
import type { HeadlessFlatTreeItemProps } from "@coreai-ds/react-components";
import { useRestoreFocusTarget } from "@coreai-ds/react-components";
import { FlatTree } from "@coreai-ds/react-components";
import { Menu } from "@coreai-ds/react-components";
import { MenuTrigger } from "@coreai-ds/react-components";
import { TreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
import { Button } from "@coreai-ds/react-components";
import { Edit20Regular } from "@fluentui/react-icons";
import { MoreHorizontal20Regular } from "@fluentui/react-icons";
import { MenuPopover } from "@coreai-ds/react-components";
import { MenuList } from "@coreai-ds/react-components";
import { MenuItem } from "@coreai-ds/react-components";
import * as React from 'react';
type FlatItem = HeadlessFlatTreeItemProps & {
content: string;
};
const flatTreeItems: FlatItem[] = [
{ 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 & { content: React.ReactNode };
// const nestedTreeItems = [
// {
// value: '1',
// content: <>level 1, item 1</>,
// subtree: [
// {
// value: '1-1',
// content: <>level 2, item 1</>,
// },
// {
// value: '1-2',
// content: <>level 2, item 2</>,
// },
// {
// value: '1-3',
// content: <>level 2, item 3</>,
// },
// ],
// },
// {
// value: '2',
// content: <>level 1, item 2</>,
// subtree: [
// {
// value: '2-1',
// content: <>level 2, item 1</>,
// subtree: [
// {
// value: '2-1-1',
// content: <>level 3, item 1</>,
// subtree: [
// {
// value: '2-1-1-1',
// content: <>level 4, item 1</>,
// },
// ],
// },
// ],
// },
// ],
// },
// ];
// 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 UseHeadlessFlatTree = (): JSXElement => {
const flatTree = useHeadlessFlatTree_unstable(flatTreeItems);
const focusTargetAttribute = useRestoreFocusTarget();
return (<FlatTree {...flatTree.getTreeProps()} aria-label="Flat Tree">
{Array.from(flatTree.items(), flatTreeItem => {
const { content, ...treeItemProps } = flatTreeItem.getTreeItemProps();
return (<Menu key={flatTreeItem.value} positioning="below-end" openOnContext>
<MenuTrigger disableButtonEnhancement>
<TreeItem aria-description="has actions" {...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>);
})}
</FlatTree>);
};
export default UseHeadlessFlatTree;
Virtualization
Loading interactive example. Full source is available below.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
/** @jsxRuntime automatic */
/** @jsxImportSource @fluentui/react-jsx-runtime */
import type { JSXElement } from "@coreai-ds/react-components";
import { useHeadlessFlatTree_unstable } from "@coreai-ds/react-components";
import type { HeadlessFlatTreeItemProps } from "@coreai-ds/react-components";
import * as React from 'react';
import { FixedSizeList } from "react-window";
import type { TreeNavigationEvent_unstable } from "@coreai-ds/react-components";
import type { TreeNavigationData_unstable } from "@coreai-ds/react-components";
import type { FlatTreeProps } from "@coreai-ds/react-components";
import type { FixedSizeListProps } from "react-window";
import { useFlatTree_unstable } from "@coreai-ds/react-components";
import { useFlatTreeStyles_unstable } from "@coreai-ds/react-components";
import { useFlatTreeContextValues_unstable } from "@coreai-ds/react-components";
import { assertSlots } from "@coreai-ds/react-components";
import type { FlatTreeSlots } from "@coreai-ds/react-components";
import { TreeProvider } from "@coreai-ds/react-components";
import type { ForwardRefComponent } from "@coreai-ds/react-components";
import type { ListChildComponentProps } from "react-window";
import type { HeadlessFlatTreeItem } from "@coreai-ds/react-components";
import { FlatTreeItem } from "@coreai-ds/react-components";
import { TreeItemLayout } from "@coreai-ds/react-components";
type ItemProps = HeadlessFlatTreeItemProps & {
content: string;
};
const defaultItems: ItemProps[] = [
{
value: 'flatTreeItem_lvl-1_item-1',
content: `Level 1, item 1`,
},
...Array.from({ length: 300 }, (_, i) => ({
value: `flatTreeItem_lvl-1_item-1--child:${i}`,
parentValue: 'flatTreeItem_lvl-1_item-1',
content: `Item ${i + 1}`,
})),
{
value: 'flatTreeItem_lvl-1_item-2',
content: `Level 1, item 2`,
},
...Array.from({ length: 300 }, (_, index) => ({
value: `flatTreeItem_lvl-1_item-2--child:${index}`,
parentValue: 'flatTreeItem_lvl-1_item-2',
content: `Item ${index + 1}`,
})),
];
type FixedSizeTreeProps = Omit<FlatTreeProps, 'children'> & {
listProps: FixedSizeListProps & {
ref?: React.Ref<FixedSizeList>;
};
};
/**
* FixedSizeTree is a recomposition of Tree component that uses react-window FixedSizeList to render items.
*/
const FixedSizeTree = React.forwardRef<HTMLElement, FixedSizeTreeProps>((props, ref) => {
const state = useFlatTree_unstable(props, ref);
useFlatTreeStyles_unstable(state);
const contextValues = useFlatTreeContextValues_unstable(state);
assertSlots<FlatTreeSlots>(state);
const handleOuterRef = React.useCallback((instance: HTMLElement | null) => {
if (instance) {
// This element stays between the tree and treeitem
// Due to accessibility issues this element should have role="none"
instance.setAttribute('role', 'none');
}
}, []);
return (<TreeProvider value={contextValues.tree}>
<state.root>
<FixedSizeList outerRef={handleOuterRef} {...props.listProps}/>
</state.root>
</TreeProvider>);
}) as ForwardRefComponent<FixedSizeTreeProps>;
interface FixedSizeTreeItemProps extends ListChildComponentProps {
data: HeadlessFlatTreeItem<ItemProps>[];
}
const FixedSizeTreeItem = (props: FixedSizeTreeItemProps) => {
const flatTreeItem = props.data[props.index];
const { content, ...treeItemProps } = flatTreeItem.getTreeItemProps();
return (<FlatTreeItem {...treeItemProps} style={props.style}>
<TreeItemLayout>{content}</TreeItemLayout>
</FlatTreeItem>);
};
export const Virtualization = (): JSXElement => {
const headlessTree = useHeadlessFlatTree_unstable(defaultItems);
const listRef = React.useRef<FixedSizeList>(null);
const items = React.useMemo(() => Array.from(headlessTree.items()), [headlessTree]);
/**
* Since navigation is not possible due to the fact that not all items are rendered,
* we need to scroll to the next item and then invoke navigation.
*/
const handleNavigation = (event: TreeNavigationEvent_unstable, data: TreeNavigationData_unstable) => {
const nextItem = headlessTree.getNextNavigableItem(items, data);
if (!nextItem) {
return;
}
// if the next item is not rendered, scroll to it and try to navigate again
if (!headlessTree.getElementFromItem(nextItem)) {
event.preventDefault(); // preventing default disables internal navigation.
listRef.current?.scrollToItem(nextItem.index);
// waiting for the next animation frame to allow the list to be scrolled
return requestAnimationFrame(() => headlessTree.navigate(data));
}
};
return (<FixedSizeTree {...headlessTree.getTreeProps()} listProps={{
ref: listRef,
height: 300,
itemCount: items.length,
itemData: items,
itemSize: 32,
width: 300,
children: FixedSizeTreeItem,
}} onNavigation={handleNavigation} aria-label="Virtualization"/>);
};
export default Virtualization;