@sproutsocial/seeds-react-tree 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/Tree.tsx","../src/Common/treeContext.ts","../src/Common/useTreeState.ts","../src/TreeStyles.tsx","../src/TreeItem.tsx","../src/Common/useTreeKeyboard.ts","../src/Common/treeNavigation.ts","../src/TreeCombobox.tsx","../src/Common/filterTree.ts"],"sourcesContent":["export { Tree, type TreeProps } from \"./Tree\";\nexport { TreeItem, type TreeItemProps } from \"./TreeItem\";\nexport { TreeCombobox, type TreeComboboxProps } from \"./TreeCombobox\";\nexport type {\n TreeItemData,\n TreeSelectionMode,\n TreeSelectableNodes,\n TreeSelectionIndicator,\n TreeSelectionIndicatorState,\n} from \"./Common/types\";\n","import * as React from \"react\";\nimport {\n TreeContext,\n TreeItemContext,\n type TreeContextValue,\n} from \"./Common/treeContext\";\nimport { useTreeState } from \"./Common/useTreeState\";\nimport type {\n TreeSelectableNodes,\n TreeSelectionIndicator,\n TreeSelectionMode,\n} from \"./Common/types\";\nimport { TreeRoot } from \"./TreeStyles\";\n\nexport type TreeProps = {\n children: React.ReactNode;\n /** Accessible name for the tree. One of `aria-label` / `aria-labelledby` is required. */\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n\n selectionMode?: TreeSelectionMode;\n selectableNodes?: TreeSelectableNodes;\n\n defaultExpanded?: ReadonlyArray<string>;\n expanded?: ReadonlyArray<string>;\n onExpandedChange?: (expanded: string[]) => void;\n\n defaultSelected?: ReadonlyArray<string>;\n selected?: ReadonlyArray<string>;\n onSelectionChange?: (selected: string[]) => void;\n\n /**\n * Optional render function for a per-item selection indicator (e.g. radio or\n * checkbox). Returns null/undefined for no indicator.\n */\n renderSelectionIndicator?: TreeSelectionIndicator;\n\n /** Item id to receive focus first; defaults to the first treeitem in DOM order. */\n defaultFocusedId?: string;\n /**\n * Controlled focused id. When set, the host owns which item is \"active\" —\n * the roving tabindex target in DOM-focus mode, or the `aria-activedescendant`\n * target in `manageDomFocus={false}` mode.\n */\n focusedId?: string | null;\n onFocusedIdChange?: (id: string | null) => void;\n\n /**\n * When `false`, Tree does not put treeitems in the tab order and does not\n * call `.focus()` on them. Used by `TreeCombobox` so DOM focus can stay on\n * its input while `focusedId` drives `aria-activedescendant`. Defaults to\n * `true`.\n */\n manageDomFocus?: boolean;\n\n className?: string;\n id?: string;\n};\n\nexport const Tree = React.forwardRef<HTMLUListElement, TreeProps>(function Tree(\n props,\n forwardedRef\n) {\n const {\n children,\n selectionMode = \"none\",\n selectableNodes = \"all\",\n defaultExpanded,\n expanded: expandedProp,\n onExpandedChange,\n defaultSelected,\n selected: selectedProp,\n onSelectionChange,\n renderSelectionIndicator,\n defaultFocusedId,\n focusedId: focusedIdProp,\n onFocusedIdChange,\n manageDomFocus = true,\n className,\n id,\n } = props;\n\n const ariaLabel = props[\"aria-label\"];\n const ariaLabelledBy = props[\"aria-labelledby\"];\n\n const innerRef = React.useRef<HTMLUListElement>(null);\n React.useImperativeHandle(forwardedRef, () => innerRef.current!, []);\n\n const { expanded, toggleExpanded, selected, toggleSelected } = useTreeState({\n selectionMode,\n selectableNodes,\n defaultExpanded,\n expanded: expandedProp,\n onExpandedChange,\n defaultSelected,\n selected: selectedProp,\n onSelectionChange,\n });\n\n const isFocusedControlled = focusedIdProp !== undefined;\n const [internalFocusedId, setInternalFocusedId] = React.useState<\n string | null\n >(defaultFocusedId ?? null);\n const focusedId = isFocusedControlled\n ? focusedIdProp ?? null\n : internalFocusedId;\n\n const setFocusedId = React.useCallback(\n (nextId: string | null) => {\n if (!isFocusedControlled) setInternalFocusedId(nextId);\n onFocusedIdChange?.(nextId);\n },\n [isFocusedControlled, onFocusedIdChange]\n );\n\n const [hasFocused, setHasFocused] = React.useState(false);\n\n // If the focused id points at an item that no longer exists in the DOM\n // (e.g. it was filtered out by a wrapper), reset it to the first visible\n // item so the tree always has a tabbable target. Without this, Tab into\n // the tree lands nowhere and the user has to click to recover.\n React.useLayoutEffect(() => {\n const root = innerRef.current;\n if (!root || focusedId === null) return;\n const stillExists = root.querySelector(\n `[role=\"treeitem\"][data-treeitem-id=\"${CSS.escape(focusedId)}\"]`\n );\n if (stillExists) return;\n const firstVisible = root.querySelector<HTMLElement>('[role=\"treeitem\"]');\n setFocusedId(firstVisible?.dataset.treeitemId ?? null);\n });\n\n const ctxValue = React.useMemo<TreeContextValue>(\n () => ({\n focusedId,\n setFocusedId,\n expanded,\n toggleExpanded,\n selected,\n toggleSelected,\n selectionMode,\n selectableNodes,\n renderSelectionIndicator,\n rootRef: innerRef,\n hasFocused,\n setHasFocused,\n manageDomFocus,\n }),\n [\n focusedId,\n setFocusedId,\n expanded,\n toggleExpanded,\n selected,\n toggleSelected,\n selectionMode,\n selectableNodes,\n renderSelectionIndicator,\n hasFocused,\n manageDomFocus,\n ]\n );\n\n return (\n <TreeContext.Provider value={ctxValue}>\n <TreeItemContext.Provider value={{ level: 1 }}>\n <TreeRoot\n ref={innerRef}\n role=\"tree\"\n id={id}\n className={className}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-multiselectable={selectionMode === \"multiple\" ? true : undefined}\n >\n {children}\n </TreeRoot>\n </TreeItemContext.Provider>\n </TreeContext.Provider>\n );\n});\n","import { createContext, useContext } from \"react\";\nimport type {\n TreeSelectableNodes,\n TreeSelectionIndicator,\n TreeSelectionMode,\n} from \"./types\";\n\nexport type TreeContextValue = {\n /** Roving tabindex / active descendant target. */\n focusedId: string | null;\n setFocusedId: (id: string | null) => void;\n /** Set of expanded branch ids. */\n expanded: ReadonlySet<string>;\n toggleExpanded: (id: string, next?: boolean) => void;\n /** Set of selected ids. */\n selected: ReadonlySet<string>;\n toggleSelected: (id: string, hasChildren: boolean) => void;\n selectionMode: TreeSelectionMode;\n selectableNodes: TreeSelectableNodes;\n renderSelectionIndicator?: TreeSelectionIndicator;\n /** DOM node of the <ul role=\"tree\"> root. Used for DOM-order navigation. */\n rootRef: React.RefObject<HTMLUListElement>;\n /** Whether the tree has received focus yet (controls initial roving tabindex). */\n hasFocused: boolean;\n setHasFocused: (v: boolean) => void;\n /**\n * When true (default), Tree manages DOM focus: items participate in the\n * tab order and `.focus()` is called as the focused id changes. When false,\n * the host (e.g. a combobox input) owns DOM focus and just consumes\n * `focusedId` to drive `aria-activedescendant`.\n */\n manageDomFocus: boolean;\n};\n\nexport const TreeContext = createContext<TreeContextValue | null>(null);\n\nexport function useTreeContext(): TreeContextValue {\n const ctx = useContext(TreeContext);\n if (!ctx) {\n throw new Error(\"TreeItem must be rendered inside a <Tree>.\");\n }\n return ctx;\n}\n\nexport type TreeItemContextValue = {\n /** 1-based depth used for aria-level. */\n level: number;\n};\n\nexport const TreeItemContext = createContext<TreeItemContextValue>({\n level: 1,\n});\n","import { useCallback, useMemo, useState } from \"react\";\nimport type { TreeSelectableNodes, TreeSelectionMode } from \"./types\";\n\ntype UseTreeStateOptions = {\n selectionMode: TreeSelectionMode;\n selectableNodes: TreeSelectableNodes;\n defaultExpanded?: ReadonlyArray<string>;\n expanded?: ReadonlyArray<string>;\n onExpandedChange?: (expanded: string[]) => void;\n defaultSelected?: ReadonlyArray<string>;\n selected?: ReadonlyArray<string>;\n onSelectionChange?: (selected: string[]) => void;\n};\n\nexport function useTreeState(opts: UseTreeStateOptions) {\n const {\n selectionMode,\n selectableNodes,\n defaultExpanded,\n expanded: expandedProp,\n onExpandedChange,\n defaultSelected,\n selected: selectedProp,\n onSelectionChange,\n } = opts;\n\n const [uncontrolledExpanded, setUncontrolledExpanded] = useState<Set<string>>(\n () => new Set(defaultExpanded ?? [])\n );\n const [uncontrolledSelected, setUncontrolledSelected] = useState<Set<string>>(\n () => new Set(defaultSelected ?? [])\n );\n\n const expanded = useMemo(\n () => (expandedProp ? new Set(expandedProp) : uncontrolledExpanded),\n [expandedProp, uncontrolledExpanded]\n );\n\n const selected = useMemo(\n () => (selectedProp ? new Set(selectedProp) : uncontrolledSelected),\n [selectedProp, uncontrolledSelected]\n );\n\n const toggleExpanded = useCallback(\n (id: string, next?: boolean) => {\n const current = expandedProp\n ? new Set(expandedProp)\n : uncontrolledExpanded;\n const willOpen = next ?? !current.has(id);\n const updated = new Set(current);\n if (willOpen) {\n updated.add(id);\n } else {\n updated.delete(id);\n }\n if (!expandedProp) {\n setUncontrolledExpanded(updated);\n }\n onExpandedChange?.(Array.from(updated));\n },\n [expandedProp, uncontrolledExpanded, onExpandedChange]\n );\n\n const toggleSelected = useCallback(\n (id: string, hasChildren: boolean) => {\n if (selectionMode === \"none\") return;\n if (selectableNodes === \"leaves\" && hasChildren) return;\n\n const current = selectedProp\n ? new Set(selectedProp)\n : uncontrolledSelected;\n const updated = new Set<string>();\n\n if (selectionMode === \"single\") {\n if (!current.has(id)) {\n updated.add(id);\n }\n } else {\n current.forEach((v) => updated.add(v));\n if (updated.has(id)) {\n updated.delete(id);\n } else {\n updated.add(id);\n }\n }\n\n if (!selectedProp) {\n setUncontrolledSelected(updated);\n }\n onSelectionChange?.(Array.from(updated));\n },\n [\n selectionMode,\n selectableNodes,\n selectedProp,\n uncontrolledSelected,\n onSelectionChange,\n ]\n );\n\n return {\n expanded,\n toggleExpanded,\n selected,\n toggleSelected,\n };\n}\n","import styled, { css } from \"styled-components\";\n\nexport const TreeRoot = styled.ul`\n list-style: none;\n margin: 0;\n padding: 0;\n font-family: ${({ theme }) => theme.fontFamily};\n ${({ theme }) => theme.typography[300]}\n color: ${({ theme }) => theme.colors.text.body};\n`;\n\nexport const TreeItemRow = styled.div<{\n $level: number;\n $selected: boolean;\n $disabled: boolean;\n}>`\n display: flex;\n align-items: center;\n gap: ${({ theme }) => theme.space[300]};\n padding: ${({ theme }) => theme.space[300]};\n padding-left: ${({ theme, $level }) =>\n `calc(${theme.space[300]} + ${$level - 1} * ${theme.space[500]})`};\n border-radius: ${({ theme }) => theme.radii[400]};\n cursor: pointer;\n user-select: none;\n background: transparent;\n transition: background-color ${({ theme }) => theme.duration.fast}\n ${({ theme }) => theme.easing.ease_in};\n\n &:hover {\n background: ${({ theme }) => theme.colors.listItem.background.hover};\n }\n\n ${({ $selected, theme }) =>\n $selected &&\n css`\n background: ${theme.colors.listItem.background.hover};\n font-weight: ${theme.fontWeights.semibold};\n `}\n\n ${({ $disabled }) =>\n $disabled &&\n css`\n opacity: 0.4;\n cursor: not-allowed;\n pointer-events: none;\n `}\n`;\n\nexport const TreeItemEl = styled.li`\n list-style: none;\n outline: none;\n\n /*\n * Tree rows stack tightly, so the standard outset Seeds focusRing bleeds\n * into adjacent rows. Use an inset outline so the ring is drawn just inside\n * the row's edge and never overlaps siblings or children.\n *\n * The same ring is drawn when [data-treeitem-active] is set so combobox\n * hosts that drive the tree via aria-activedescendant (no real DOM focus)\n * still get a visible \"active\" indicator on the row.\n */\n &:focus-visible > ${TreeItemRow}, &[data-treeitem-active] > ${TreeItemRow} {\n outline: 2px solid\n ${({ theme }) => theme.colors.button.primary.background.base};\n outline-offset: -2px;\n }\n`;\n\nexport const TreeItemGroup = styled.ul`\n list-style: none;\n margin: 0;\n padding: 0;\n`;\n\nexport const TreeItemLabel = styled.span`\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n`;\n\nexport const TreeItemIcon = styled.span`\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n`;\n\nexport const TreeItemChevron = styled.button.attrs({\n type: \"button\",\n tabIndex: -1,\n})<{ $expanded: boolean }>`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: ${({ theme }) => theme.space[500]};\n height: ${({ theme }) => theme.space[500]};\n padding: 0;\n border: none;\n background: transparent;\n color: ${({ theme }) => theme.colors.icon.base};\n cursor: pointer;\n border-radius: ${({ theme }) => theme.radii[300]};\n transition: transform ${({ theme }) => theme.duration.fast}\n ${({ theme }) => theme.easing.ease_in};\n transform: ${({ $expanded }) =>\n $expanded ? \"rotate(0deg)\" : \"rotate(-90deg)\"};\n\n &:hover {\n background: ${({ theme }) => theme.colors.listItem.background.hover};\n }\n`;\n","import * as React from \"react\";\nimport { Icon } from \"@sproutsocial/seeds-react-icon\";\nimport { TreeItemContext, useTreeContext } from \"./Common/treeContext\";\nimport { useTreeKeyboard } from \"./Common/useTreeKeyboard\";\nimport { treeItemDomId } from \"./Common/treeNavigation\";\nimport {\n TreeItemChevron,\n TreeItemEl,\n TreeItemGroup,\n TreeItemIcon,\n TreeItemLabel,\n TreeItemRow,\n} from \"./TreeStyles\";\n\nexport type TreeItemProps = {\n id: string;\n label: React.ReactNode;\n icon?: React.ReactNode;\n disabled?: boolean;\n children?: React.ReactNode;\n /**\n * Optional override for the row click target. When provided, this is used\n * as the accessible name for the row (otherwise the rendered `label` is).\n */\n \"aria-label\"?: string;\n};\n\nfunction hasTreeItemChildren(children: React.ReactNode): boolean {\n let found = false;\n React.Children.forEach(children, (child) => {\n if (found) return;\n if (!React.isValidElement(child)) return;\n const childType = child.type;\n if (childType === React.Fragment) {\n found = hasTreeItemChildren(\n (child as React.ReactElement<{ children?: React.ReactNode }>).props\n .children\n );\n return;\n }\n // Any other element child counts as a tree item. Consumers should only\n // place TreeItems (or components that render one) inside a TreeItem.\n found = true;\n });\n return found;\n}\n\nexport function TreeItem(props: TreeItemProps) {\n const { id, label, icon, disabled = false, children } = props;\n const ctx = useTreeContext();\n const { level } = React.useContext(TreeItemContext);\n\n const hasChildren = React.useMemo(\n () => hasTreeItemChildren(children),\n [children]\n );\n\n const isExpanded = ctx.expanded.has(id);\n const isSelected = ctx.selected.has(id);\n const selectionMode = ctx.selectionMode;\n\n // Roving tabindex: this item is tabbable when it's the focused id. When the\n // host (e.g. a combobox) drives focus externally, no treeitem is tabbable.\n const itemRef = React.useRef<HTMLLIElement>(null);\n const isTabbable = ctx.manageDomFocus && ctx.focusedId === id;\n\n // On first mount of the very first item, register ourselves as the initial\n // roving target if nothing else has claimed it. Skipped when the host owns\n // focus — the host decides when (if ever) to set an initial active id.\n React.useEffect(() => {\n if (!ctx.manageDomFocus) return;\n if (ctx.focusedId !== null) return;\n const root = ctx.rootRef.current;\n if (!root) return;\n const first = root.querySelector<HTMLElement>('[role=\"treeitem\"]');\n if (first === itemRef.current) {\n ctx.setFocusedId(id);\n }\n // We only want this on mount; deps intentionally minimal.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const handleKeyDown = useTreeKeyboard(ctx);\n\n const handleRowClick = (e: React.MouseEvent) => {\n if (disabled) return;\n // Ignore clicks that originated on the chevron — it owns its own handler.\n const target = e.target as HTMLElement;\n if (target.closest(\"[data-tree-chevron]\")) return;\n\n ctx.setFocusedId(id);\n if (ctx.manageDomFocus) itemRef.current?.focus();\n\n if (hasChildren && selectionMode === \"none\") {\n ctx.toggleExpanded(id);\n return;\n }\n if (hasChildren && ctx.selectableNodes === \"leaves\") {\n ctx.toggleExpanded(id);\n return;\n }\n if (selectionMode !== \"none\") {\n ctx.toggleSelected(id, hasChildren);\n }\n };\n\n const handleChevronClick = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (disabled) return;\n ctx.setFocusedId(id);\n if (ctx.manageDomFocus) itemRef.current?.focus();\n ctx.toggleExpanded(id);\n };\n\n const ariaSelected =\n selectionMode === \"single\" && ctx.selectableNodes !== \"leaves\"\n ? isSelected\n : undefined;\n const ariaChecked =\n selectionMode === \"multiple\"\n ? isSelected\n : selectionMode === \"single\" && ctx.selectableNodes === \"leaves\"\n ? isSelected\n : undefined;\n\n const groupId = `${id}__group`;\n const labelId = `${id}__label`;\n\n return (\n <TreeItemEl\n ref={itemRef}\n id={treeItemDomId(id)}\n role=\"treeitem\"\n data-treeitem-id={id}\n data-treeitem-active={\n !ctx.manageDomFocus && ctx.focusedId === id ? true : undefined\n }\n aria-level={level}\n aria-expanded={hasChildren ? isExpanded : undefined}\n aria-selected={ariaSelected}\n aria-checked={ariaChecked}\n aria-disabled={disabled || undefined}\n aria-labelledby={labelId}\n aria-owns={hasChildren && isExpanded ? groupId : undefined}\n tabIndex={isTabbable ? 0 : -1}\n onKeyDown={(e) => {\n if (!ctx.manageDomFocus) return;\n handleKeyDown(e, {\n id,\n hasChildren,\n isExpanded,\n isDisabled: disabled,\n level,\n });\n }}\n onClick={(e) => {\n // Branch treeitems contain descendant LIs whose row clicks bubble up.\n // Only handle the click if it originated within this LI's own row —\n // not within a descendant treeitem.\n const target = e.target as HTMLElement;\n const nearestTreeitem = target.closest('[role=\"treeitem\"]');\n if (nearestTreeitem !== e.currentTarget) return;\n handleRowClick(e);\n }}\n onMouseDown={(e) => {\n // In external-focus (combobox) mode, mousedown on a tabindex=-1\n // treeitem would still steal DOM focus from the host input. Prevent\n // it so focus stays where the host put it; onClick still fires.\n if (!ctx.manageDomFocus) e.preventDefault();\n }}\n onFocus={() => {\n if (!ctx.manageDomFocus) return;\n if (!ctx.hasFocused) ctx.setHasFocused(true);\n if (ctx.focusedId !== id) ctx.setFocusedId(id);\n }}\n >\n <TreeItemRow\n data-treeitem-row\n $level={level}\n $selected={isSelected}\n $disabled={disabled}\n >\n {ctx.renderSelectionIndicator &&\n !(ctx.selectableNodes === \"leaves\" && hasChildren)\n ? ctx.renderSelectionIndicator({\n selected: isSelected,\n disabled,\n selectionMode,\n })\n : null}\n\n {icon != null ? <TreeItemIcon aria-hidden>{icon}</TreeItemIcon> : null}\n <TreeItemLabel id={labelId}>{label}</TreeItemLabel>\n\n {hasChildren ? (\n <TreeItemChevron\n data-tree-chevron\n aria-hidden\n $expanded={isExpanded}\n onClick={handleChevronClick}\n >\n <Icon name=\"chevron-down-outline\" size=\"small\" />\n </TreeItemChevron>\n ) : null}\n </TreeItemRow>\n\n {hasChildren ? (\n <TreeItemGroup role=\"group\" id={groupId} hidden={!isExpanded}>\n <TreeItemContext.Provider value={{ level: level + 1 }}>\n {children}\n </TreeItemContext.Provider>\n </TreeItemGroup>\n ) : null}\n </TreeItemEl>\n );\n}\n\nTreeItem.displayName = \"TreeItem\";\n","import { useCallback, useRef } from \"react\";\nimport type { TreeContextValue } from \"./treeContext\";\nimport {\n computeTreeNavigation,\n getTreeItemId,\n getVisibleTreeItems,\n} from \"./treeNavigation\";\n\nconst TYPEAHEAD_TIMEOUT_MS = 500;\n\nfunction focusItem(el: HTMLElement | undefined) {\n if (!el) return;\n el.focus();\n}\n\n/**\n * Keyboard handler for the WAI-ARIA treeview pattern, bound to each TreeItem\n * via `onKeyDown` when DOM focus lives on treeitems (roving tabindex). The\n * heavy lifting — what to focus next, what to expand/select — is delegated to\n * `computeTreeNavigation` so a combobox host that drives the tree externally\n * can share the exact same navigation rules.\n */\nexport function useTreeKeyboard(ctx: TreeContextValue) {\n const typeaheadBufferRef = useRef(\"\");\n const typeaheadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const handleTypeahead = useCallback(\n (char: string, current: HTMLElement, visible: HTMLElement[]) => {\n if (typeaheadTimerRef.current) clearTimeout(typeaheadTimerRef.current);\n typeaheadBufferRef.current = (\n typeaheadBufferRef.current + char\n ).toLowerCase();\n typeaheadTimerRef.current = setTimeout(() => {\n typeaheadBufferRef.current = \"\";\n }, TYPEAHEAD_TIMEOUT_MS);\n\n const buffer = typeaheadBufferRef.current;\n const currentIndex = visible.indexOf(current);\n const ordered = [\n ...visible.slice(currentIndex + 1),\n ...visible.slice(0, currentIndex + 1),\n ];\n const match = ordered.find((el) =>\n (el.textContent ?? \"\").trim().toLowerCase().startsWith(buffer)\n );\n if (match) {\n const id = getTreeItemId(match);\n if (id) ctx.setFocusedId(id);\n focusItem(match);\n }\n },\n [ctx]\n );\n\n return useCallback(\n (\n e: React.KeyboardEvent<HTMLLIElement>,\n itemMeta: {\n id: string;\n hasChildren: boolean;\n isExpanded: boolean;\n isDisabled: boolean;\n level: number;\n }\n ) => {\n if (itemMeta.isDisabled) return;\n // Branch treeitems contain their descendants' LIs, so keydowns bubble up\n // through every ancestor TreeItem. Only handle the keydown on the LI\n // that actually has focus, otherwise ancestor handlers fight the\n // descendant for control of the roving tabindex.\n if (e.target !== e.currentTarget) return;\n\n const root = ctx.rootRef.current;\n const current = e.currentTarget;\n\n const result = computeTreeNavigation(root, current, e.key, {\n id: itemMeta.id,\n hasChildren: itemMeta.hasChildren,\n isExpanded: itemMeta.isExpanded,\n level: itemMeta.level,\n });\n\n if (result.preventDefault) e.preventDefault();\n\n if (result.expandToggle) {\n ctx.toggleExpanded(result.expandToggle.id, result.expandToggle.next);\n }\n if (result.selectToggle) {\n ctx.toggleSelected(\n result.selectToggle.id,\n result.selectToggle.hasChildren\n );\n }\n if (result.expandSiblings) {\n result.expandSiblings.forEach((id) => ctx.toggleExpanded(id, true));\n }\n if (result.nextFocusedId) {\n ctx.setFocusedId(result.nextFocusedId);\n if (ctx.manageDomFocus) {\n const next = root?.querySelector<HTMLElement>(\n `[data-treeitem-id=\"${CSS.escape(result.nextFocusedId)}\"]`\n );\n focusItem(next ?? undefined);\n }\n }\n\n // Printable-char typeahead fires only when the navigation table didn't\n // claim the key. Skip it when DOM focus is external (combobox mode) —\n // there the host input is the type target, not the tree.\n if (\n !result.preventDefault &&\n ctx.manageDomFocus &&\n e.key.length === 1 &&\n /\\S/.test(e.key) &&\n !e.ctrlKey &&\n !e.metaKey &&\n !e.altKey\n ) {\n handleTypeahead(e.key, current, getVisibleTreeItems(root));\n }\n },\n [ctx, handleTypeahead]\n );\n}\n","/**\n * Pure navigation logic for the WAI-ARIA treeview pattern.\n *\n * `useTreeKeyboard` uses this when DOM focus lives on the focused treeitem\n * (roving tabindex). A combobox-style host that keeps DOM focus on an input\n * and reflects the active item via `aria-activedescendant` uses the same\n * function — it just skips the `.focus()` step.\n */\n\nexport type TreeNavigationResult = {\n /** True when the caller should call `event.preventDefault()`. */\n preventDefault: boolean;\n /** Item the host should make active next (roving focus / active descendant). */\n nextFocusedId?: string;\n /** Branch to expand or collapse. */\n expandToggle?: { id: string; next?: boolean };\n /** Item whose selection should be toggled (Enter / Space). */\n selectToggle?: { id: string; hasChildren: boolean };\n /** Sibling branch ids to expand (the `*` key). */\n expandSiblings?: string[];\n};\n\nexport function getVisibleTreeItems(root: HTMLElement | null): HTMLElement[] {\n if (!root) return [];\n const all = Array.from(\n root.querySelectorAll<HTMLElement>('[role=\"treeitem\"]')\n );\n return all.filter((el) => {\n if (el.getAttribute(\"aria-disabled\") === \"true\") return false;\n let parent = el.parentElement;\n while (parent && parent !== root) {\n if (\n parent.getAttribute(\"role\") === \"group\" &&\n parent.hasAttribute(\"hidden\")\n ) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n}\n\nexport function getTreeItemId(el: HTMLElement): string | null {\n return el.dataset.treeitemId ?? null;\n}\n\n/** DOM `id` we apply to each treeitem so a combobox host can target it via `aria-activedescendant`. */\nexport function treeItemDomId(itemId: string): string {\n return `${itemId}__item`;\n}\n\nexport type TreeNavigationMeta = {\n id: string;\n hasChildren: boolean;\n isExpanded: boolean;\n level: number;\n};\n\nexport function computeTreeNavigation(\n root: HTMLElement | null,\n currentEl: HTMLElement | null,\n key: string,\n meta: TreeNavigationMeta | null\n): TreeNavigationResult {\n const visible = getVisibleTreeItems(root);\n if (visible.length === 0) return { preventDefault: false };\n\n const idOf = (el: HTMLElement | undefined): string | undefined => {\n if (!el) return undefined;\n return getTreeItemId(el) ?? undefined;\n };\n\n // No current item — ArrowDown/Home land on the first visible item;\n // ArrowUp/End land on the last. (Used by the combobox when the user\n // first presses an arrow with no item highlighted.)\n if (!currentEl || !meta) {\n if (key === \"ArrowDown\" || key === \"Home\") {\n return { preventDefault: true, nextFocusedId: idOf(visible[0]) };\n }\n if (key === \"ArrowUp\" || key === \"End\") {\n return {\n preventDefault: true,\n nextFocusedId: idOf(visible[visible.length - 1]),\n };\n }\n return { preventDefault: false };\n }\n\n const currentIndex = visible.indexOf(currentEl);\n if (currentIndex === -1) return { preventDefault: false };\n\n const clamp = (i: number) => Math.max(0, Math.min(visible.length - 1, i));\n\n switch (key) {\n case \"ArrowDown\":\n return {\n preventDefault: true,\n nextFocusedId: idOf(visible[clamp(currentIndex + 1)]),\n };\n case \"ArrowUp\":\n return {\n preventDefault: true,\n nextFocusedId: idOf(visible[clamp(currentIndex - 1)]),\n };\n case \"ArrowRight\": {\n if (meta.hasChildren && !meta.isExpanded) {\n return {\n preventDefault: true,\n expandToggle: { id: meta.id, next: true },\n };\n }\n if (meta.hasChildren && meta.isExpanded) {\n return {\n preventDefault: true,\n nextFocusedId: idOf(visible[clamp(currentIndex + 1)]),\n };\n }\n return { preventDefault: true };\n }\n case \"ArrowLeft\": {\n if (meta.hasChildren && meta.isExpanded) {\n return {\n preventDefault: true,\n expandToggle: { id: meta.id, next: false },\n };\n }\n let parent: HTMLElement | null = currentEl.parentElement;\n while (parent && parent !== root) {\n if (parent.getAttribute(\"role\") === \"group\") {\n const parentItem = parent.parentElement;\n if (parentItem?.getAttribute(\"role\") === \"treeitem\") {\n return {\n preventDefault: true,\n nextFocusedId: idOf(parentItem),\n };\n }\n }\n parent = parent.parentElement;\n }\n return { preventDefault: true };\n }\n case \"Home\":\n return { preventDefault: true, nextFocusedId: idOf(visible[0]) };\n case \"End\":\n return {\n preventDefault: true,\n nextFocusedId: idOf(visible[visible.length - 1]),\n };\n case \"Enter\":\n case \" \":\n return {\n preventDefault: true,\n selectToggle: { id: meta.id, hasChildren: meta.hasChildren },\n };\n case \"*\": {\n const siblings = Array.from(\n currentEl.parentElement?.children ?? []\n ).filter(\n (el): el is HTMLElement =>\n el instanceof HTMLElement && el.getAttribute(\"role\") === \"treeitem\"\n );\n const toExpand = siblings\n .filter((s) => s.getAttribute(\"aria-expanded\") === \"false\")\n .map((s) => getTreeItemId(s))\n .filter((id): id is string => id !== null);\n return { preventDefault: true, expandSiblings: toExpand };\n }\n }\n return { preventDefault: false };\n}\n","import * as React from \"react\";\nimport styled from \"styled-components\";\nimport { Icon } from \"@sproutsocial/seeds-react-icon\";\nimport { focusRing } from \"@sproutsocial/seeds-react-mixins\";\nimport { Tree, type TreeProps } from \"./Tree\";\nimport { TreeItem } from \"./TreeItem\";\nimport { filterTree } from \"./Common/filterTree\";\nimport { computeTreeNavigation, treeItemDomId } from \"./Common/treeNavigation\";\nimport type { TreeItemData } from \"./Common/types\";\n\nconst Root = styled.div`\n display: flex;\n flex-direction: column;\n gap: ${({ theme }) => theme.space[300]};\n`;\n\nconst InputGroup = styled.div`\n display: flex;\n align-items: center;\n gap: ${({ theme }) => theme.space[200]};\n padding: ${({ theme }) => theme.space[200]} ${({ theme }) => theme.space[300]};\n border-radius: ${({ theme }) => theme.radii[500]};\n border: 1px solid ${({ theme }) => theme.colors.form.border.base};\n background: ${({ theme }) => theme.colors.form.background.base};\n color: ${({ theme }) => theme.colors.icon.base};\n\n &:focus-within {\n ${focusRing}\n }\n`;\n\nconst ComboboxInput = styled.input`\n flex: 1;\n border: none;\n background: transparent;\n outline: none;\n font-family: ${({ theme }) => theme.fontFamily};\n ${({ theme }) => theme.typography[300]}\n color: ${({ theme }) => theme.colors.text.body};\n\n &::placeholder {\n color: ${({ theme }) => theme.colors.text.subtext};\n }\n`;\n\nconst EmptyState = styled.div`\n padding: ${({ theme }) => theme.space[400]};\n text-align: center;\n color: ${({ theme }) => theme.colors.text.subtext};\n ${({ theme }) => theme.typography[300]}\n`;\n\nconst NAV_KEYS = new Set([\n \"ArrowDown\",\n \"ArrowUp\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n \"Enter\",\n]);\n\nexport type TreeComboboxProps = Omit<\n TreeProps,\n | \"children\"\n | \"focusedId\"\n | \"onFocusedIdChange\"\n | \"defaultFocusedId\"\n | \"manageDomFocus\"\n | \"aria-label\"\n | \"aria-labelledby\"\n> & {\n items: ReadonlyArray<TreeItemData>;\n /** Placeholder for the combobox input. */\n placeholder?: string;\n /** Text shown when the query has no matches. */\n emptyText?: string;\n /** Accessible name for the combobox. One of `aria-label` / `aria-labelledby` is required. */\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n /** Controlled query value. */\n query?: string;\n /** Uncontrolled initial query. */\n defaultQuery?: string;\n onQueryChange?: (query: string) => void;\n};\n\n/**\n * Combobox + tree pattern. Implements the WAI-ARIA combobox pattern with a\n * tree as the popup (`aria-haspopup=\"tree\"`). DOM focus stays on the input;\n * `aria-activedescendant` points at the active treeitem, which the input's\n * arrow-key handler moves through the visible tree. Selection and expansion\n * reuse the underlying Tree's logic — Enter on the input synthesizes a click\n * on the active row, ArrowRight/Left compute the new expanded set and pass\n * it through Tree's controlled `expanded` prop.\n */\nexport function TreeCombobox(props: TreeComboboxProps) {\n const {\n items,\n placeholder = \"Search...\",\n emptyText = \"No results found.\",\n query: queryProp,\n defaultQuery = \"\",\n onQueryChange,\n selectionMode = \"none\",\n selectableNodes = \"all\",\n defaultExpanded,\n expanded: expandedProp,\n onExpandedChange,\n defaultSelected,\n selected: selectedProp,\n onSelectionChange,\n renderSelectionIndicator,\n id,\n className,\n } = props;\n const ariaLabel = props[\"aria-label\"];\n const ariaLabelledBy = props[\"aria-labelledby\"];\n\n const [uncontrolledQuery, setUncontrolledQuery] =\n React.useState(defaultQuery);\n const query = queryProp ?? uncontrolledQuery;\n const setQuery = (next: string) => {\n if (queryProp === undefined) setUncontrolledQuery(next);\n onQueryChange?.(next);\n };\n\n // Track the user's last \"real\" expansion state so it can be restored when\n // the query clears, matching the prior SearchableTree behavior.\n const [userExpanded, setUserExpanded] = React.useState<string[]>(() => [\n ...(defaultExpanded ?? expandedProp ?? []),\n ]);\n\n const { items: visibleItems, forceExpanded } = React.useMemo(\n () => filterTree([...items], query),\n [items, query]\n );\n\n const isFiltering = query.trim().length > 0;\n const effectiveExpanded = isFiltering\n ? Array.from(new Set([...userExpanded, ...forceExpanded]))\n : expandedProp ?? userExpanded;\n\n const handleExpandedChange = (next: string[]) => {\n if (!isFiltering) {\n setUserExpanded(next);\n }\n onExpandedChange?.(next);\n };\n\n const [focusedId, setFocusedId] = React.useState<string | null>(null);\n\n const treeRef = React.useRef<HTMLUListElement>(null);\n const reactId = React.useId();\n const treeDomId = id ? `${id}-tree` : `${reactId}-tree`;\n\n const findTreeItemEl = (itemId: string): HTMLElement | null =>\n treeRef.current?.querySelector<HTMLElement>(\n `[role=\"treeitem\"][data-treeitem-id=\"${CSS.escape(itemId)}\"]`\n ) ?? null;\n\n const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Escape\") {\n if (query.length > 0) {\n e.preventDefault();\n setQuery(\"\");\n setFocusedId(null);\n }\n return;\n }\n if (!NAV_KEYS.has(e.key)) return;\n\n const currentEl = focusedId ? findTreeItemEl(focusedId) : null;\n const meta =\n currentEl && focusedId\n ? {\n id: focusedId,\n hasChildren: currentEl.getAttribute(\"aria-expanded\") !== null,\n isExpanded: currentEl.getAttribute(\"aria-expanded\") === \"true\",\n level: parseInt(currentEl.getAttribute(\"aria-level\") ?? \"1\", 10),\n }\n : null;\n\n // Enter: replay a row click on the active treeitem so we get TreeItem's\n // exact selection/expansion semantics (single vs multi, leaves-only,\n // branch-vs-leaf) for free.\n if (e.key === \"Enter\") {\n if (!currentEl) return;\n e.preventDefault();\n const row = currentEl.querySelector<HTMLElement>(\"[data-treeitem-row]\");\n row?.click();\n return;\n }\n\n const result = computeTreeNavigation(\n treeRef.current,\n currentEl,\n e.key,\n meta\n );\n if (result.preventDefault) e.preventDefault();\n\n if (result.expandToggle) {\n const current = new Set(effectiveExpanded);\n const willOpen =\n result.expandToggle.next ?? !current.has(result.expandToggle.id);\n if (willOpen) current.add(result.expandToggle.id);\n else current.delete(result.expandToggle.id);\n handleExpandedChange(Array.from(current));\n }\n if (result.nextFocusedId) {\n setFocusedId(result.nextFocusedId);\n }\n };\n\n // If a filter change drops the active item out of the visible set, the\n // Tree's recovery layout-effect rebases focusedId via onFocusedIdChange.\n // We accept that and don't reset focusedId on filter ourselves — clearing\n // happens explicitly on Escape.\n\n const showEmpty = visibleItems.length === 0;\n\n return (\n <Root className={className}>\n <InputGroup>\n <Icon name=\"magnifying-glass-outline\" size=\"small\" aria-hidden />\n <ComboboxInput\n type=\"text\"\n role=\"combobox\"\n id={id}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-expanded={!showEmpty}\n aria-controls={treeDomId}\n aria-haspopup=\"tree\"\n aria-autocomplete=\"list\"\n aria-activedescendant={\n focusedId ? treeItemDomId(focusedId) : undefined\n }\n placeholder={placeholder}\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={handleInputKeyDown}\n />\n </InputGroup>\n\n <Tree\n ref={treeRef}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n id={treeDomId}\n manageDomFocus={false}\n selectionMode={selectionMode}\n selectableNodes={selectableNodes}\n expanded={effectiveExpanded}\n onExpandedChange={handleExpandedChange}\n selected={selectedProp}\n defaultSelected={defaultSelected}\n onSelectionChange={onSelectionChange}\n renderSelectionIndicator={renderSelectionIndicator}\n focusedId={focusedId}\n onFocusedIdChange={setFocusedId}\n >\n {visibleItems.map((item) => (\n <TreeNode key={item.id} item={item} />\n ))}\n </Tree>\n\n {showEmpty ? (\n <EmptyState role=\"status\" aria-live=\"polite\">\n {emptyText}\n </EmptyState>\n ) : null}\n </Root>\n );\n}\n\nfunction TreeNode({ item }: { item: TreeItemData }) {\n return (\n <TreeItem\n id={item.id}\n label={item.label}\n icon={item.icon}\n disabled={item.disabled}\n >\n {item.children?.map((child) => (\n <TreeNode key={child.id} item={child} />\n ))}\n </TreeItem>\n );\n}\n","import type { TreeItemData } from \"./types\";\n\nexport type FilterResult = {\n /** Items to render (parents with no matching descendants are removed). */\n items: TreeItemData[];\n /** Ids of branches that should be force-expanded so matches are visible. */\n forceExpanded: string[];\n};\n\n/**\n * Returns the subtree of items where `label` (or any descendant label) matches\n * `query`, plus the set of branch ids that should be opened to reveal matches.\n *\n * Why: TreeCombobox needs both a filtered list and the ancestor ids of every\n * match so users can see what their search hit.\n * How to apply: pass the result to <Tree expanded={...}>.\n */\nexport function filterTree(\n items: ReadonlyArray<TreeItemData>,\n query: string\n): FilterResult {\n const normalized = query.trim().toLowerCase();\n if (!normalized) {\n return { items: [...items], forceExpanded: [] };\n }\n\n const forceExpanded = new Set<string>();\n\n const walk = (node: TreeItemData): TreeItemData | null => {\n const matches = node.label.toLowerCase().includes(normalized);\n const filteredChildren = (node.children ?? [])\n .map(walk)\n .filter((c): c is TreeItemData => c !== null);\n\n if (filteredChildren.length > 0 && node.children) {\n forceExpanded.add(node.id);\n }\n\n if (matches || filteredChildren.length > 0) {\n return {\n ...node,\n children: node.children ? filteredChildren : undefined,\n };\n }\n return null;\n };\n\n const filtered = items.map(walk).filter((n): n is TreeItemData => n !== null);\n\n return { items: filtered, forceExpanded: Array.from(forceExpanded) };\n}\n"],"mappings":"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","names":["import_react","styled","Tree","React","import_react","import_jsx_runtime","React","import_styled_components","import_seeds_react_icon","import_jsx_runtime","styled"]}
package/jest.config.js ADDED
@@ -0,0 +1,12 @@
1
+ const baseConfig = require("@sproutsocial/seeds-testing");
2
+
3
+ /**
4
+ * @type {import('jest').Config}
5
+ */
6
+ const config = {
7
+ ...baseConfig,
8
+ displayName: "seeds-react-tree",
9
+ setupFilesAfterEnv: ["@sproutsocial/seeds-testing/setupAfterEnv"],
10
+ };
11
+
12
+ module.exports = config;
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@sproutsocial/seeds-react-tree",
3
+ "version": "0.3.1",
4
+ "description": "Seeds React Tree (WAI-ARIA treeview)",
5
+ "author": "Sprout Social, Inc.",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "module": "dist/esm/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/esm/index.js",
14
+ "require": "./dist/index.js"
15
+ },
16
+ "./base": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/esm/index.js",
19
+ "require": "./dist/index.js"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "build": "tsup --dts",
24
+ "build:debug": "tsup --dts --metafile",
25
+ "dev": "tsup --watch --dts",
26
+ "clean": "rm -rf .turbo dist",
27
+ "clean:modules": "rm -rf node_modules",
28
+ "typecheck": "tsc --noEmit",
29
+ "test": "jest",
30
+ "test:watch": "jest --watch --coverage=false"
31
+ },
32
+ "dependencies": {
33
+ "@sproutsocial/seeds-react-icon": "^2.4.0",
34
+ "@sproutsocial/seeds-react-mixins": "^4.3.7",
35
+ "@sproutsocial/seeds-react-system-props": "^3.1.1",
36
+ "@sproutsocial/seeds-react-theme": "^4.1.0",
37
+ "@sproutsocial/seeds-react-utilities": "^4.3.0"
38
+ },
39
+ "devDependencies": {
40
+ "@sproutsocial/eslint-config-seeds": "*",
41
+ "@sproutsocial/seeds-react-button": "^2.2.1",
42
+ "@sproutsocial/seeds-react-popout": "^2.5.9",
43
+ "@sproutsocial/seeds-react-testing-library": "*",
44
+ "@sproutsocial/seeds-testing": "*",
45
+ "@sproutsocial/seeds-tsconfig": "*",
46
+ "@types/react": "^18.0.0",
47
+ "@types/react-dom": "^18.0.0",
48
+ "@types/styled-components": "^5.1.26",
49
+ "react": "^18.0.0",
50
+ "react-dom": "^18.0.0",
51
+ "tsup": "^8.3.4",
52
+ "typescript": "^5.2.2"
53
+ },
54
+ "peerDependencies": {
55
+ "react": "^17.0.0 || ^18.0.0",
56
+ "styled-components": "^5.2.3"
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ }
61
+ }
@@ -0,0 +1,51 @@
1
+ import type { TreeItemData } from "./types";
2
+
3
+ export type FilterResult = {
4
+ /** Items to render (parents with no matching descendants are removed). */
5
+ items: TreeItemData[];
6
+ /** Ids of branches that should be force-expanded so matches are visible. */
7
+ forceExpanded: string[];
8
+ };
9
+
10
+ /**
11
+ * Returns the subtree of items where `label` (or any descendant label) matches
12
+ * `query`, plus the set of branch ids that should be opened to reveal matches.
13
+ *
14
+ * Why: TreeCombobox needs both a filtered list and the ancestor ids of every
15
+ * match so users can see what their search hit.
16
+ * How to apply: pass the result to <Tree expanded={...}>.
17
+ */
18
+ export function filterTree(
19
+ items: ReadonlyArray<TreeItemData>,
20
+ query: string
21
+ ): FilterResult {
22
+ const normalized = query.trim().toLowerCase();
23
+ if (!normalized) {
24
+ return { items: [...items], forceExpanded: [] };
25
+ }
26
+
27
+ const forceExpanded = new Set<string>();
28
+
29
+ const walk = (node: TreeItemData): TreeItemData | null => {
30
+ const matches = node.label.toLowerCase().includes(normalized);
31
+ const filteredChildren = (node.children ?? [])
32
+ .map(walk)
33
+ .filter((c): c is TreeItemData => c !== null);
34
+
35
+ if (filteredChildren.length > 0 && node.children) {
36
+ forceExpanded.add(node.id);
37
+ }
38
+
39
+ if (matches || filteredChildren.length > 0) {
40
+ return {
41
+ ...node,
42
+ children: node.children ? filteredChildren : undefined,
43
+ };
44
+ }
45
+ return null;
46
+ };
47
+
48
+ const filtered = items.map(walk).filter((n): n is TreeItemData => n !== null);
49
+
50
+ return { items: filtered, forceExpanded: Array.from(forceExpanded) };
51
+ }
@@ -0,0 +1,52 @@
1
+ import { createContext, useContext } from "react";
2
+ import type {
3
+ TreeSelectableNodes,
4
+ TreeSelectionIndicator,
5
+ TreeSelectionMode,
6
+ } from "./types";
7
+
8
+ export type TreeContextValue = {
9
+ /** Roving tabindex / active descendant target. */
10
+ focusedId: string | null;
11
+ setFocusedId: (id: string | null) => void;
12
+ /** Set of expanded branch ids. */
13
+ expanded: ReadonlySet<string>;
14
+ toggleExpanded: (id: string, next?: boolean) => void;
15
+ /** Set of selected ids. */
16
+ selected: ReadonlySet<string>;
17
+ toggleSelected: (id: string, hasChildren: boolean) => void;
18
+ selectionMode: TreeSelectionMode;
19
+ selectableNodes: TreeSelectableNodes;
20
+ renderSelectionIndicator?: TreeSelectionIndicator;
21
+ /** DOM node of the <ul role="tree"> root. Used for DOM-order navigation. */
22
+ rootRef: React.RefObject<HTMLUListElement>;
23
+ /** Whether the tree has received focus yet (controls initial roving tabindex). */
24
+ hasFocused: boolean;
25
+ setHasFocused: (v: boolean) => void;
26
+ /**
27
+ * When true (default), Tree manages DOM focus: items participate in the
28
+ * tab order and `.focus()` is called as the focused id changes. When false,
29
+ * the host (e.g. a combobox input) owns DOM focus and just consumes
30
+ * `focusedId` to drive `aria-activedescendant`.
31
+ */
32
+ manageDomFocus: boolean;
33
+ };
34
+
35
+ export const TreeContext = createContext<TreeContextValue | null>(null);
36
+
37
+ export function useTreeContext(): TreeContextValue {
38
+ const ctx = useContext(TreeContext);
39
+ if (!ctx) {
40
+ throw new Error("TreeItem must be rendered inside a <Tree>.");
41
+ }
42
+ return ctx;
43
+ }
44
+
45
+ export type TreeItemContextValue = {
46
+ /** 1-based depth used for aria-level. */
47
+ level: number;
48
+ };
49
+
50
+ export const TreeItemContext = createContext<TreeItemContextValue>({
51
+ level: 1,
52
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Pure navigation logic for the WAI-ARIA treeview pattern.
3
+ *
4
+ * `useTreeKeyboard` uses this when DOM focus lives on the focused treeitem
5
+ * (roving tabindex). A combobox-style host that keeps DOM focus on an input
6
+ * and reflects the active item via `aria-activedescendant` uses the same
7
+ * function — it just skips the `.focus()` step.
8
+ */
9
+
10
+ export type TreeNavigationResult = {
11
+ /** True when the caller should call `event.preventDefault()`. */
12
+ preventDefault: boolean;
13
+ /** Item the host should make active next (roving focus / active descendant). */
14
+ nextFocusedId?: string;
15
+ /** Branch to expand or collapse. */
16
+ expandToggle?: { id: string; next?: boolean };
17
+ /** Item whose selection should be toggled (Enter / Space). */
18
+ selectToggle?: { id: string; hasChildren: boolean };
19
+ /** Sibling branch ids to expand (the `*` key). */
20
+ expandSiblings?: string[];
21
+ };
22
+
23
+ export function getVisibleTreeItems(root: HTMLElement | null): HTMLElement[] {
24
+ if (!root) return [];
25
+ const all = Array.from(
26
+ root.querySelectorAll<HTMLElement>('[role="treeitem"]')
27
+ );
28
+ return all.filter((el) => {
29
+ if (el.getAttribute("aria-disabled") === "true") return false;
30
+ let parent = el.parentElement;
31
+ while (parent && parent !== root) {
32
+ if (
33
+ parent.getAttribute("role") === "group" &&
34
+ parent.hasAttribute("hidden")
35
+ ) {
36
+ return false;
37
+ }
38
+ parent = parent.parentElement;
39
+ }
40
+ return true;
41
+ });
42
+ }
43
+
44
+ export function getTreeItemId(el: HTMLElement): string | null {
45
+ return el.dataset.treeitemId ?? null;
46
+ }
47
+
48
+ /** DOM `id` we apply to each treeitem so a combobox host can target it via `aria-activedescendant`. */
49
+ export function treeItemDomId(itemId: string): string {
50
+ return `${itemId}__item`;
51
+ }
52
+
53
+ export type TreeNavigationMeta = {
54
+ id: string;
55
+ hasChildren: boolean;
56
+ isExpanded: boolean;
57
+ level: number;
58
+ };
59
+
60
+ export function computeTreeNavigation(
61
+ root: HTMLElement | null,
62
+ currentEl: HTMLElement | null,
63
+ key: string,
64
+ meta: TreeNavigationMeta | null
65
+ ): TreeNavigationResult {
66
+ const visible = getVisibleTreeItems(root);
67
+ if (visible.length === 0) return { preventDefault: false };
68
+
69
+ const idOf = (el: HTMLElement | undefined): string | undefined => {
70
+ if (!el) return undefined;
71
+ return getTreeItemId(el) ?? undefined;
72
+ };
73
+
74
+ // No current item — ArrowDown/Home land on the first visible item;
75
+ // ArrowUp/End land on the last. (Used by the combobox when the user
76
+ // first presses an arrow with no item highlighted.)
77
+ if (!currentEl || !meta) {
78
+ if (key === "ArrowDown" || key === "Home") {
79
+ return { preventDefault: true, nextFocusedId: idOf(visible[0]) };
80
+ }
81
+ if (key === "ArrowUp" || key === "End") {
82
+ return {
83
+ preventDefault: true,
84
+ nextFocusedId: idOf(visible[visible.length - 1]),
85
+ };
86
+ }
87
+ return { preventDefault: false };
88
+ }
89
+
90
+ const currentIndex = visible.indexOf(currentEl);
91
+ if (currentIndex === -1) return { preventDefault: false };
92
+
93
+ const clamp = (i: number) => Math.max(0, Math.min(visible.length - 1, i));
94
+
95
+ switch (key) {
96
+ case "ArrowDown":
97
+ return {
98
+ preventDefault: true,
99
+ nextFocusedId: idOf(visible[clamp(currentIndex + 1)]),
100
+ };
101
+ case "ArrowUp":
102
+ return {
103
+ preventDefault: true,
104
+ nextFocusedId: idOf(visible[clamp(currentIndex - 1)]),
105
+ };
106
+ case "ArrowRight": {
107
+ if (meta.hasChildren && !meta.isExpanded) {
108
+ return {
109
+ preventDefault: true,
110
+ expandToggle: { id: meta.id, next: true },
111
+ };
112
+ }
113
+ if (meta.hasChildren && meta.isExpanded) {
114
+ return {
115
+ preventDefault: true,
116
+ nextFocusedId: idOf(visible[clamp(currentIndex + 1)]),
117
+ };
118
+ }
119
+ return { preventDefault: true };
120
+ }
121
+ case "ArrowLeft": {
122
+ if (meta.hasChildren && meta.isExpanded) {
123
+ return {
124
+ preventDefault: true,
125
+ expandToggle: { id: meta.id, next: false },
126
+ };
127
+ }
128
+ let parent: HTMLElement | null = currentEl.parentElement;
129
+ while (parent && parent !== root) {
130
+ if (parent.getAttribute("role") === "group") {
131
+ const parentItem = parent.parentElement;
132
+ if (parentItem?.getAttribute("role") === "treeitem") {
133
+ return {
134
+ preventDefault: true,
135
+ nextFocusedId: idOf(parentItem),
136
+ };
137
+ }
138
+ }
139
+ parent = parent.parentElement;
140
+ }
141
+ return { preventDefault: true };
142
+ }
143
+ case "Home":
144
+ return { preventDefault: true, nextFocusedId: idOf(visible[0]) };
145
+ case "End":
146
+ return {
147
+ preventDefault: true,
148
+ nextFocusedId: idOf(visible[visible.length - 1]),
149
+ };
150
+ case "Enter":
151
+ case " ":
152
+ return {
153
+ preventDefault: true,
154
+ selectToggle: { id: meta.id, hasChildren: meta.hasChildren },
155
+ };
156
+ case "*": {
157
+ const siblings = Array.from(
158
+ currentEl.parentElement?.children ?? []
159
+ ).filter(
160
+ (el): el is HTMLElement =>
161
+ el instanceof HTMLElement && el.getAttribute("role") === "treeitem"
162
+ );
163
+ const toExpand = siblings
164
+ .filter((s) => s.getAttribute("aria-expanded") === "false")
165
+ .map((s) => getTreeItemId(s))
166
+ .filter((id): id is string => id !== null);
167
+ return { preventDefault: true, expandSiblings: toExpand };
168
+ }
169
+ }
170
+ return { preventDefault: false };
171
+ }
@@ -0,0 +1,23 @@
1
+ import type * as React from "react";
2
+
3
+ export type TreeItemData = {
4
+ id: string;
5
+ label: string;
6
+ icon?: React.ReactNode;
7
+ disabled?: boolean;
8
+ children?: TreeItemData[];
9
+ };
10
+
11
+ export type TreeSelectionMode = "single" | "multiple" | "none";
12
+
13
+ export type TreeSelectableNodes = "all" | "leaves";
14
+
15
+ export type TreeSelectionIndicatorState = {
16
+ selected: boolean;
17
+ disabled: boolean;
18
+ selectionMode: TreeSelectionMode;
19
+ };
20
+
21
+ export type TreeSelectionIndicator = (
22
+ state: TreeSelectionIndicatorState
23
+ ) => React.ReactNode;
@@ -0,0 +1,124 @@
1
+ import { useCallback, useRef } from "react";
2
+ import type { TreeContextValue } from "./treeContext";
3
+ import {
4
+ computeTreeNavigation,
5
+ getTreeItemId,
6
+ getVisibleTreeItems,
7
+ } from "./treeNavigation";
8
+
9
+ const TYPEAHEAD_TIMEOUT_MS = 500;
10
+
11
+ function focusItem(el: HTMLElement | undefined) {
12
+ if (!el) return;
13
+ el.focus();
14
+ }
15
+
16
+ /**
17
+ * Keyboard handler for the WAI-ARIA treeview pattern, bound to each TreeItem
18
+ * via `onKeyDown` when DOM focus lives on treeitems (roving tabindex). The
19
+ * heavy lifting — what to focus next, what to expand/select — is delegated to
20
+ * `computeTreeNavigation` so a combobox host that drives the tree externally
21
+ * can share the exact same navigation rules.
22
+ */
23
+ export function useTreeKeyboard(ctx: TreeContextValue) {
24
+ const typeaheadBufferRef = useRef("");
25
+ const typeaheadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
26
+
27
+ const handleTypeahead = useCallback(
28
+ (char: string, current: HTMLElement, visible: HTMLElement[]) => {
29
+ if (typeaheadTimerRef.current) clearTimeout(typeaheadTimerRef.current);
30
+ typeaheadBufferRef.current = (
31
+ typeaheadBufferRef.current + char
32
+ ).toLowerCase();
33
+ typeaheadTimerRef.current = setTimeout(() => {
34
+ typeaheadBufferRef.current = "";
35
+ }, TYPEAHEAD_TIMEOUT_MS);
36
+
37
+ const buffer = typeaheadBufferRef.current;
38
+ const currentIndex = visible.indexOf(current);
39
+ const ordered = [
40
+ ...visible.slice(currentIndex + 1),
41
+ ...visible.slice(0, currentIndex + 1),
42
+ ];
43
+ const match = ordered.find((el) =>
44
+ (el.textContent ?? "").trim().toLowerCase().startsWith(buffer)
45
+ );
46
+ if (match) {
47
+ const id = getTreeItemId(match);
48
+ if (id) ctx.setFocusedId(id);
49
+ focusItem(match);
50
+ }
51
+ },
52
+ [ctx]
53
+ );
54
+
55
+ return useCallback(
56
+ (
57
+ e: React.KeyboardEvent<HTMLLIElement>,
58
+ itemMeta: {
59
+ id: string;
60
+ hasChildren: boolean;
61
+ isExpanded: boolean;
62
+ isDisabled: boolean;
63
+ level: number;
64
+ }
65
+ ) => {
66
+ if (itemMeta.isDisabled) return;
67
+ // Branch treeitems contain their descendants' LIs, so keydowns bubble up
68
+ // through every ancestor TreeItem. Only handle the keydown on the LI
69
+ // that actually has focus, otherwise ancestor handlers fight the
70
+ // descendant for control of the roving tabindex.
71
+ if (e.target !== e.currentTarget) return;
72
+
73
+ const root = ctx.rootRef.current;
74
+ const current = e.currentTarget;
75
+
76
+ const result = computeTreeNavigation(root, current, e.key, {
77
+ id: itemMeta.id,
78
+ hasChildren: itemMeta.hasChildren,
79
+ isExpanded: itemMeta.isExpanded,
80
+ level: itemMeta.level,
81
+ });
82
+
83
+ if (result.preventDefault) e.preventDefault();
84
+
85
+ if (result.expandToggle) {
86
+ ctx.toggleExpanded(result.expandToggle.id, result.expandToggle.next);
87
+ }
88
+ if (result.selectToggle) {
89
+ ctx.toggleSelected(
90
+ result.selectToggle.id,
91
+ result.selectToggle.hasChildren
92
+ );
93
+ }
94
+ if (result.expandSiblings) {
95
+ result.expandSiblings.forEach((id) => ctx.toggleExpanded(id, true));
96
+ }
97
+ if (result.nextFocusedId) {
98
+ ctx.setFocusedId(result.nextFocusedId);
99
+ if (ctx.manageDomFocus) {
100
+ const next = root?.querySelector<HTMLElement>(
101
+ `[data-treeitem-id="${CSS.escape(result.nextFocusedId)}"]`
102
+ );
103
+ focusItem(next ?? undefined);
104
+ }
105
+ }
106
+
107
+ // Printable-char typeahead fires only when the navigation table didn't
108
+ // claim the key. Skip it when DOM focus is external (combobox mode) —
109
+ // there the host input is the type target, not the tree.
110
+ if (
111
+ !result.preventDefault &&
112
+ ctx.manageDomFocus &&
113
+ e.key.length === 1 &&
114
+ /\S/.test(e.key) &&
115
+ !e.ctrlKey &&
116
+ !e.metaKey &&
117
+ !e.altKey
118
+ ) {
119
+ handleTypeahead(e.key, current, getVisibleTreeItems(root));
120
+ }
121
+ },
122
+ [ctx, handleTypeahead]
123
+ );
124
+ }