import { Accessibility, hierarchicalTreeItemBehavior, hierarchicalSubtreeBehavior, HierarchicalTreeItemBehaviorProps, } from '@fluentui/accessibility'; import { ComponentWithAs, getFirstFocusable, useTelemetry, useFluentContext, getElementType, useUnhandledProps, useAccessibility, useStyles, } from '@fluentui/react-bindings'; import * as customPropTypes from '@fluentui/react-proptypes'; import { Ref } from '@fluentui/react-component-ref'; import * as _ from 'lodash'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import { HierarchicalTree, HierarchicalTreeProps } from './HierarchicalTree'; import { HierarchicalTreeTitle, HierarchicalTreeTitleProps } from './HierarchicalTreeTitle'; import { childrenExist, createShorthandFactory, commonPropTypes, UIComponentProps, ChildrenComponentProps, rtlTextContainer, } from '../../utils'; import { ComponentEventHandler, ShorthandRenderFunction, ShorthandValue, ShorthandCollection, FluentComponentStaticProps, } from '../../types'; export interface HierarchicalTreeItemSlotClassNames { subtree: string; } export interface HierarchicalTreeItemProps extends UIComponentProps, ChildrenComponentProps { /** Accessibility behavior if overridden by the user. */ accessibility?: Accessibility; /** Only allow one subtree to be open at a time. */ exclusive?: boolean; /** The index of the item among its sibbling */ index?: number; /** Array of props for sub tree. */ items?: ShorthandValue | ShorthandCollection; /** Called when a tree title is clicked. */ onTitleClick?: ComponentEventHandler; /** Whether or not the subtree of the item is in the open state. */ open?: boolean; /** * A custom render iterator for rendering each Accordion panel title. * The default component, props, and children are available for each panel title. * * @param Component - The computed component for this slot. * @param props - The computed props for this slot. * @param children - The computed children for this slot. */ renderItemTitle?: ShorthandRenderFunction; /** Properties for TreeTitle. */ title?: ShorthandValue; } export const hierarchicalTreeItemClassName = 'ui-hierarchicaltree__item'; export const hierarchicalTreeItemSlotClassNames: HierarchicalTreeItemSlotClassNames = { subtree: `${hierarchicalTreeItemClassName}__subtree`, }; export type HierarchicalTreeItemStyles = never; /** * A TreeItem renders an item of a Tree. * * @accessibility * Implements [ARIA TreeView](https://www.w3.org/TR/wai-aria-practices-1.1/#TreeView) design pattern. */ export const HierarchicalTreeItem: ComponentWithAs<'li', HierarchicalTreeItemProps> & FluentComponentStaticProps = props => { const context = useFluentContext(); const { setStart, setEnd } = useTelemetry(HierarchicalTreeItem.displayName, context.telemetry); setStart(); const { items, title, renderItemTitle, open, exclusive, children, className, design, styles, variables } = props; const ElementType = getElementType(props); const unhandledProps = useUnhandledProps(HierarchicalTreeItem.handledProps, props); const actionHandlers = { performClick: e => { e.preventDefault(); e.stopPropagation(); _.invoke(props, 'onTitleClick', e, props); }, receiveFocus: e => { e.preventDefault(); e.stopPropagation(); // Focuses the title if the event comes from a child item. if (eventComesFromChildItem(e)) { itemRef.current.focus(); } }, collapse: e => { e.preventDefault(); e.stopPropagation(); // Handle click on title if the keyboard event was dispatched on that title if (!eventComesFromChildItem(e)) { handleTitleClick(e); } }, expand: e => { e.preventDefault(); e.stopPropagation(); handleTitleClick(e); }, focusSubtree: e => { e.preventDefault(); e.stopPropagation(); const element = getFirstFocusable(treeRef.current, treeRef.current, true); if (element) { element.focus(); } }, }; const getA11yProps = useAccessibility(props.accessibility, { debugName: HierarchicalTreeItem.displayName, actionHandlers, mapPropsToBehavior: () => ({ hasItems: items && !!(items as HierarchicalTreeItemProps[]).length, open, }), rtl: context.rtl, }); const itemRef = React.useRef(); const treeRef = React.useRef(); const { classes } = useStyles(HierarchicalTreeItem.displayName, { className: hierarchicalTreeItemClassName, mapPropsToInlineStyles: () => ({ className, design, styles, variables, }), rtl: context.rtl, }); const eventComesFromChildItem = e => { return e.currentTarget !== e.target; }; const handleTitleClick = e => { _.invoke(props, 'onTitleClick', e, props); }; const handleTitleOverrides = (predefinedProps: HierarchicalTreeTitleProps) => ({ onClick: (e, titleProps) => { handleTitleClick(e); _.invoke(predefinedProps, 'onClick', e, titleProps); }, }); const renderContent = () => { const hasSubtree = !_.isNil(items); return ( <> {HierarchicalTreeTitle.create(title, { defaultProps: () => ({ open, hasSubtree, as: hasSubtree ? 'span' : 'a', }), render: renderItemTitle, overrideProps: handleTitleOverrides, })} {hasSubtree && open && ( {HierarchicalTree.create(items, { defaultProps: () => getA11yProps('item', { accessibility: hierarchicalSubtreeBehavior, className: hierarchicalTreeItemSlotClassNames.subtree, exclusive, renderItemTitle, }), })} )} ); }; const element = ( {childrenExist(children) ? children : renderContent()} ); setEnd(); return element; }; HierarchicalTreeItem.displayName = 'HierarchicalTreeItem'; HierarchicalTreeItem.propTypes = { ...commonPropTypes.createCommon({ content: false, }), items: customPropTypes.collectionShorthand, index: PropTypes.number, exclusive: PropTypes.bool, onTitleClick: PropTypes.func, open: PropTypes.bool, renderItemTitle: PropTypes.func, title: customPropTypes.itemShorthand, }; HierarchicalTreeItem.defaultProps = { as: 'li', accessibility: hierarchicalTreeItemBehavior, }; HierarchicalTreeItem.handledProps = Object.keys(HierarchicalTreeItem.propTypes) as any; HierarchicalTreeItem.create = createShorthandFactory({ Component: HierarchicalTreeItem, mappedProp: 'title', });