Skip to content

Forward aria-label / aria-labelledby to the role="tree" element #325

@eiffel205566

Description

@eiffel205566

Summary

Allow consumers to set an accessible name on the tree by accepting aria-label
and aria-labelledby as props on <Tree>, and forwarding them onto the
internal role="tree" element rendered by DefaultContainer.

Background

Per the W3C Treeview pattern,
the element with role="tree" should have an accessible name supplied via
aria-label or aria-labelledby. Today react-arborist (3.4.3) does not
expose any prop to do this — TreeProps<T> has no aria-label /
aria-labelledby fields, and DefaultContainer renders
<div role="tree" …> with hardcoded attributes that don't accept extra props.

The same gap exists at the row level: each row gets role="treeitem" plus
aria-level / aria-selected / aria-expanded (great), but no
aria-label / aria-labelledby. Consumers who do not control the row's
visible text DOM end up with treeitems that have no accessible name. Note that
this is workable today via a custom renderRow (which receives node and
attrs), but the tree-container case has no equivalent escape hatch.

Current workaround (what we're forced to do)

Because there is no public API:

// Forward aria-label onto the role="tree" element via DOM mutation
const treeWrapperRef = useRef<HTMLDivElement>(null)

useEffect(() => {
  const tree = treeWrapperRef.current?.querySelector('[role="tree"]')
  if (tree && ariaLabel) tree.setAttribute('aria-label', ariaLabel)
}, [ariaLabel])

return (
  <div ref={treeWrapperRef}>
    <Tree {...} />
  </div>
)

This works but:

  • is a side-effect that mutates DOM produced by another component,
  • can race with arborist's own renders,
  • is invisible to React DevTools and to anyone reading the JSX,
  • doesn't compose with SSR or strict-mode double-invocation cleanly.

`renderContainer` doesn't help — `DefaultContainer` is not exported, so
overriding it means re-implementing ~230 LOC of keyboard handling
(Backspace delete, Tab/Shift+Tab focus traversal, arrow keys, type-ahead
search, etc.).

Proposed API

<Tree
  data={data}
  aria-label="File explorer"
  // or
  aria-labelledby="file-explorer-heading"
>
  …
</Tree>

Internally, `DefaultContainer` would spread the relevant ARIA attributes onto
its `role="tree"` div:

// DefaultContainer
return (
  <div
    role="tree"
    aria-label={tree.props['aria-label']}
    aria-labelledby={tree.props['aria-labelledby']}
    aria-multiselectable={!tree.props.disableMultiSelection || undefined}
    style={{ height: tree.height, width: tree.width, … }}
    // … existing handlers
  >
    …
  </div>
)

(`aria-multiselectable` would be a useful bonus while we're touching this
element — currently always omitted, even though the tree supports
multi-selection.)

A symmetrical option for rows (`<Tree treeItemAriaLabel={(node) => node.data.name}>`
or just always spreading any `aria-label` from `attrs`) would also be welcome,
though that's already workable via `renderRow`.

Prior accessibility work

Thanks for the previous a11y improvements — #198 (aria-level / role
violations) and #223 (aria-expanded) made the row attributes excellent.
This issue is the natural next step for the container.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions