MiHCM
primitives/@mihcm/ui·v0.21.0·stable

OrgSelector

Form-field-shaped picker for hierarchical org structures (departments, teams, business units, locations). Five variants (popover/inline/columns/drawer/command) and four selection modes (single/multi/cascade/include-descendants) compose freely. Full WAI-ARIA TreeView keyboard model, lazy loading, optional virtualization for large trees.

WAI-ARIA TreeView — release blocker

OrgSelector implements the WAI-ARIA Authoring Practices TreeView pattern. All trees expose the standard tree semantics regardless of variant.

Roles & properties

  • The tree root has role="tree". In multi / cascade / include-descendants modes it also carries aria-multiselectable="true".
  • Each branch is a role="treeitem" with:
    • aria-expanded — present and set on rows that can expand.
    • aria-selected — reflects current selection in single / multi modes.
    • aria-checked (true / false / 'mixed') — used in cascade mode to express partial subtrees.
    • aria-level, aria-setsize, aria-posinset — ancestry / position information for screen readers.
    • aria-disabled="true" for non-pickable or explicitly disabled rows.
  • Groups of children render as role="group" so the relationship is unambiguous.

Keyboard

The full tree keyboard model is wired up:

KeyAction
/ Move focus to the previous / next visible row.
Collapse an expanded row, or focus its parent.
Expand a collapsed row, or focus its first child.
Home / EndJump to the first / last visible row.
Enter / SpaceToggle selection (or expand for non-pickable parents).
a–z, 0–9Typeahead — focus the next row whose label starts with the typed buffer. The buffer resets after 700 ms.
*Expand every sibling of the focused row.
TabLeaves the tree to the next focusable control. Tree manages its own roving focus.

In command variant the search input behaves like a Command palette: typing filters; / move through results; Enter selects.

Focus management

  • Each tree has a single tabstop (roving tabindex). Focus enters where the user last left it (or the first selected row, or the first row).
  • Opening the popover/drawer auto-focuses the search input when searchable is on, otherwise the first row.
  • Closing the popover returns focus to the trigger.

Form field semantics

  • label renders a real <label> linked to the trigger via id/htmlFor.
  • helpText is wired up via aria-describedby.
  • errorText toggles aria-invalid="true" and is associated via aria-describedby alongside help text.
  • required adds an asterisk and aria-required="true".

Cascade mode signals

  • Parents with a partial subtree render a checkbox in 'mixed' (indeterminate) state — both aria-checked="mixed" and a visual middle-bar fill. Colour is never the only signal.
  • Selecting an indeterminate parent selects every descendant; deselecting it clears the subtree.

Async loading

  • The expand chevron renders a spinner during loadChildren calls. The row sets aria-busy="true" while loading.
  • If loadChildren rejects, a retry affordance appears with role="button" and an action-describing aria-label.

Reduced motion / contrast

  • Expand/collapse uses CSS transitions ≤ 200 ms and respects prefers-reduced-motion.
  • Selected rows use a fill plus checkmark — colour is reinforcement, not the only signal.
  • Focus rings meet WCAG 2.1 AA 3:1 non-text contrast.

Audit checklist

  • Tree root has role="tree".
  • Each row has role="treeitem" + correct aria-level, aria-setsize, aria-posinset.
  • Cascade mode reports aria-checked="mixed" for partial subtrees.
  • / collapse/expand and move focus per the APG spec.
  • Tab does NOT cycle through every row — it leaves the tree.
  • Typeahead resets after 700 ms.
  • Trigger has an accessible name even when no value is selected.
  • Error text is exposed via aria-describedby and toggles aria-invalid.
  • 4.5:1 contrast on labels in selected, indeterminate, and disabled states.