Default

Loading interactive example. Full source is available below.

View example source
Download 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.

Setup and dependencies.

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.

Download 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 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.

Download 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 { 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.

Download 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 { 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.

Download source
// 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.

Download 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 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.

Download source
// 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.

Download source
// 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.

Download source
// 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.

Download 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 { 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.

Download source
// 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.

Download source
// 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.

Download 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 { 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.

Download source
// 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.

Download source
// 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.

Download source
// 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;

Navigation Mode Tree Grid

Loading interactive example. Full source is available below.

Download 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 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 NavigationModeTreeGrid = (): JSXElement => {
    return (<Tree navigationMode="treegrid" 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 NavigationModeTreeGrid;

Open Item Controlled

Loading interactive example. Full source is available below.

Download source
// 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.

Download source
// 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.

Download source
// 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.

Download 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 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.

Download source
// 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.

Download source
// 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;