{"version":3,"file":"tree.mjs","names":[],"sources":["../../../../package/common/tree/tree.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport type { ReactElement } from 'react';\nimport cx from 'classnames';\nimport { faChevronRight } from '@fortawesome/free-solid-svg-icons';\n\nimport VirtualList from 'components/common/virtualList';\nimport type { VirtualListHandle } from 'components/common/virtualList';\nimport { useComponentDefaults, useLabels } from '../configProvider';\nimport { renderIcon } from '../utils/renderIcon';\nimport { flattenTree } from './flattenTree';\nimport type { FlatTreeNode, TreeLabels, TreeProps, TreeValue } from './tree.types';\n\nimport styles from './tree.module.pcss';\n\nconst EMPTY_KEYS: TreeValue[] = [];\n\nconst DEFAULT_LABELS: Required<TreeLabels> = {\n  empty: 'Nothing here',\n};\n\nfunction Tree(props: TreeProps): ReactElement {\n  const {\n    nodes,\n    expandedKeys: controlledExpanded,\n    defaultExpandedKeys = EMPTY_KEYS,\n    onExpandedChange = () => {},\n    selectedKey: controlledSelected,\n    defaultSelectedKey,\n    onSelect = () => {},\n    expandOnSelect = true,\n    indent = 20,\n    toggleIcon = faChevronRight,\n    height,\n    estimatedRowHeight = 32,\n    labels,\n    disabled = false,\n    'aria-label': ariaLabel = 'Tree',\n    className,\n    style,\n    classNames,\n    styles: slotStyles,\n    ...rest\n  } = useComponentDefaults('Tree', props);\n  const text = useLabels('Tree', labels, DEFAULT_LABELS);\n\n  const nodeRefs = useRef(new Map<TreeValue, HTMLDivElement>());\n  const listRef = useRef<VirtualListHandle>(null);\n  const pendingFocusRef = useRef<TreeValue | null>(null);\n  const isWindowed = height !== undefined;\n\n  const isExpandedControlled = controlledExpanded !== undefined;\n  const [internalExpanded, setInternalExpanded] = useState<TreeValue[]>(defaultExpandedKeys);\n  const expandedKeys = isExpandedControlled ? controlledExpanded : internalExpanded;\n\n  const isSelectedControlled = controlledSelected !== undefined;\n  const [internalSelected, setInternalSelected] = useState<TreeValue | null>(defaultSelectedKey ?? null);\n  const selectedKey = isSelectedControlled ? controlledSelected : internalSelected;\n\n  const visible = flattenTree(nodes, expandedKeys);\n\n  // Windowed, the tab stop sits on the container instead of this row: a roving tabindex on a row\n  // scrolled out of the window would leave the tree with no tab stop at all.\n  const activeIndex = Math.max(\n    0,\n    visible.findIndex((entry) => entry.node.key === selectedKey),\n  );\n  const activeKey = visible[activeIndex]?.node.key;\n\n  function setExpanded(next: TreeValue[]) {\n    if (!isExpandedControlled) setInternalExpanded(next);\n    onExpandedChange(next);\n  }\n\n  function toggle(key: TreeValue) {\n    setExpanded(expandedKeys.includes(key) ? expandedKeys.filter((k) => k !== key) : [...expandedKeys, key]);\n  }\n\n  function select(entry: FlatTreeNode) {\n    if (disabled || entry.node.disabled) return;\n    if (!isSelectedControlled) setInternalSelected(entry.node.key);\n    onSelect(entry.node.key, entry.node);\n    if (expandOnSelect && entry.isBranch) toggle(entry.node.key);\n  }\n\n  function focusAt(index: number) {\n    const key = visible[index]?.node.key;\n    if (key === undefined) return;\n\n    const element = nodeRefs.current.get(key);\n    if (element) {\n      element.focus();\n      return;\n    }\n\n    // Outside the window, so it has to be scrolled in before it exists to receive focus.\n    pendingFocusRef.current = key;\n    listRef.current?.scrollToIndex(index, { align: 'center' });\n  }\n\n  useEffect(() => {\n    const key = pendingFocusRef.current;\n    if (key === null) return;\n    const element = nodeRefs.current.get(key);\n    if (element) {\n      element.focus();\n      pendingFocusRef.current = null;\n    }\n  });\n\n  function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>, entry: FlatTreeNode, index: number) {\n    if (disabled) return;\n\n    switch (event.key) {\n      case 'ArrowDown':\n        event.preventDefault();\n        focusAt(Math.min(index + 1, visible.length - 1));\n        break;\n      case 'ArrowUp':\n        event.preventDefault();\n        focusAt(Math.max(index - 1, 0));\n        break;\n      case 'ArrowRight':\n        event.preventDefault();\n        if (!entry.isBranch) break;\n        if (entry.isExpanded) focusAt(index + 1);\n        else toggle(entry.node.key);\n        break;\n      case 'ArrowLeft': {\n        event.preventDefault();\n        if (entry.isBranch && entry.isExpanded) {\n          toggle(entry.node.key);\n          break;\n        }\n        const parentIndex = visible.findIndex((e) => e.node.key === entry.parentKey);\n        if (parentIndex >= 0) focusAt(parentIndex);\n        break;\n      }\n      case 'Home':\n        event.preventDefault();\n        focusAt(0);\n        break;\n      case 'End':\n        event.preventDefault();\n        focusAt(visible.length - 1);\n        break;\n      case 'Enter':\n      case ' ':\n        event.preventDefault();\n        select(entry);\n        break;\n      default:\n        break;\n    }\n  }\n\n  function renderNode(entry: FlatTreeNode, index: number) {\n    const { node, level, isBranch, isExpanded } = entry;\n    const isSelected = node.key === selectedKey;\n    const isDisabled = disabled || node.disabled;\n\n    return (\n      <div\n        key={node.key}\n        ref={(el) => {\n          if (el) nodeRefs.current.set(node.key, el);\n          else nodeRefs.current.delete(node.key);\n        }}\n        role='treeitem'\n        aria-level={level + 1}\n        aria-setsize={entry.setSize}\n        aria-posinset={entry.posInSet}\n        aria-expanded={isBranch ? isExpanded : undefined}\n        aria-selected={isSelected}\n        aria-disabled={isDisabled || undefined}\n        tabIndex={node.key === activeKey && !isDisabled ? 0 : -1}\n        className={cx(\n          styles.treeNode,\n          isSelected && styles.treeNodeSelected,\n          isDisabled && styles.treeNodeDisabled,\n          classNames?.node,\n        )}\n        style={{ paddingLeft: level * indent, ...slotStyles?.node }}\n        onClick={() => select(entry)}\n        onKeyDown={(event) => handleKeyDown(event, entry, index)}\n      >\n        <span\n          className={cx(\n            styles.treeToggle,\n            isExpanded && styles.treeToggleOpen,\n            !isBranch && styles.treeToggleHidden,\n            classNames?.toggle,\n          )}\n          style={slotStyles?.toggle}\n          aria-hidden='true'\n          onClick={(event) => {\n            event.stopPropagation();\n            if (!isDisabled && isBranch) toggle(node.key);\n          }}\n        >\n          {isBranch && renderIcon(toggleIcon)}\n        </span>\n\n        {node.icon &&\n          renderIcon(node.icon, {\n            className: cx(styles.treeIcon, classNames?.icon),\n            style: slotStyles?.icon,\n          })}\n\n        <span className={cx(styles.treeLabel, classNames?.label)} style={slotStyles?.label}>\n          {node.label}\n        </span>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      {...rest}\n      role='tree'\n      aria-label={ariaLabel}\n      aria-disabled={disabled || undefined}\n      tabIndex={isWindowed && !disabled ? 0 : undefined}\n      onFocus={(event) => {\n        if (event.target === event.currentTarget) focusAt(activeIndex);\n      }}\n      className={cx(styles.tree, disabled && styles.treeDisabled, classNames?.root, className)}\n      style={{ ...style, ...slotStyles?.root }}\n    >\n      {visible.length === 0 ? (\n        <div className={cx(styles.treeEmpty, classNames?.empty)} style={slotStyles?.empty}>\n          {text.empty}\n        </div>\n      ) : isWindowed ? (\n        <VirtualList\n          ref={listRef}\n          items={visible}\n          height={height}\n          estimatedRowHeight={estimatedRowHeight}\n          getItemKey={(entry) => entry.node.key}\n          getItemDepth={(entry) => entry.level}\n          ariaRoles={{ container: 'none', row: 'none' }}\n          // Tree owns the keyboard: its rows carry the roving tabindex and their\n          // own key handling, and a second tab stop here would sit inside it.\n          keyboardNavigation={false}\n          renderRow={renderNode}\n        />\n      ) : (\n        visible.map(renderNode)\n      )}\n    </div>\n  );\n}\n\nexport default Tree;\n"],"mappings":"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"}