import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type ReactNode, type Ref, } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; import type { TreeNode, DropOp } from '../model/tree-model.types'; import { treeModel } from '../model/tree-model'; import { DocTreeRow } from './doc-tree-row'; import styles from '../styles/tree.module.css'; export type RenderRowProps = { node: TreeNode; level: number; isOpen: boolean; hasChildren: boolean; isSelected: boolean; isDragging: boolean; isReceivingDrop: 'before' | 'after' | 'make-child' | null; rowRef: Ref; // Roving tabindex: exactly one row in the tree carries tabIndex={0} (the // active row); every other row gets tabIndex={-1}. Consumers must spread // this onto the same element they wire rowRef to. tabIndex: 0 | -1; // Treeitem semantics for the row's focusable element. Consumers MUST spread // these onto the same element rowRef points at, so the focused element IS // the treeitem. This makes screen readers announce "treeitem" (not "link") // and replaces the descendant-text accname with the row's label, so action // button labels inside the row don't get concatenated. treeItemProps: { role: 'treeitem'; 'aria-level': number; 'aria-expanded'?: boolean; 'aria-selected'?: true; 'aria-current'?: 'page'; 'aria-label': string; 'data-row-id': string; }; toggleOpen: () => void; }; export type DocTreeProps = { data: TreeNode[]; openIds: ReadonlySet; selectedId?: string; renderRow: (props: RenderRowProps) => ReactNode; indentPerLevel?: number; rowHeight?: number; emptyState?: ReactNode; // Extra class for the engine's row wrapper (the div carrying data-selected / // data-dragging), letting consumers restyle hover/selected states. rowClassName?: string; // Extra vertical space above a row (e.g. to separate top-level groups). // Added to the row's virtualized slot and applied as padding above it. rowGap?: (node: TreeNode, level: number, index: number) => number; // Measure real row heights so rows may wrap to multiple lines; rowHeight // then only seeds the estimate. Off by default (fixed-slot fast path). dynamicRowHeight?: boolean; // Rendered inside the scroll container after the last row, so it flows // with the tree content instead of pinning to the container edge. footer?: ReactNode; onMove: (sourceId: string, op: DropOp) => void | Promise; onToggle: (id: string, isOpen: boolean) => void; onSelect?: (id: string) => void; readOnly?: boolean; disableDrag?: (node: TreeNode) => boolean; disableDrop?: (node: TreeNode) => boolean; getDragLabel: (node: TreeNode) => string; uniqueContextId?: symbol; // Accessible name for the tree itself (e.g. "Pages"). Rendered as // aria-label on the