Primitives / foldkit-primitives/interaction

TreeNavigation

namespace · packages/primitives/src/interaction/tree-navigation.ts:16

export * as TreeNavigation from './tree-navigation.js'

Keyboard navigation of a tree (a file explorer, a page's layers, a nested menu), after the WAI-ARIA tree pattern: one tab stop, Up and Down through the rows that are showing, Right to open a row or step into it, Left to close it or step out to its parent, Home and End to the first and last row.

The Model slice is the current row's id and which rows are open. Openness is stored as the rows toggled away from a default, so a layers panel that starts with everything open and a file tree that starts closed are the same bundle with a different openByDefault, and neither stores every row.

Which rows exist is the view's to say, each with its parent, in tree order. What a key does is a pure function of those rows and the Model; the Behavior wires it to the slots and writes the tree's ARIA attributes.

In the guides

Members

TreeNavigation.Action

type · packages/primitives/src/interaction/tree-navigation.ts:139

type Action =
  | {
      readonly _tag: 'Focus'
      readonly id: string
    }
  | {
      readonly _tag: 'Open'
      readonly id: string
    }
  | {
      readonly _tag: 'Close'
      readonly id: string
    }

What a key does: move to a row, or open or close one.

TreeNavigation.Args

const · packages/primitives/src/interaction/tree-navigation.ts:42

const Args: Schema.Struct<{ readonly openByDefault: Schema.Boolean }>

TreeNavigation.behavior

function · packages/primitives/src/interaction/tree-navigation.ts:225

function behavior<Field extends string>(
  declared: Declared<typeof bundle, Field>,
  args: Args,
): <Slots>(
  slots: Slots,
) => <Input extends { readonly [K in Field]: Model }, ParentMessage>(
  options: BehaviorOptions<Input, Slots>,
) => Behavior.NamedBehavior<Slots, Input, ParentMessage>

Wires a placed TreeNavigation to the slots. The container's keys move focus to the row they pick, or open or close the current row with focus left where it is. Each showing row gets its id, role="treeitem", aria-level (and the same level as --fk-tree-level, so one calc indents any depth), aria-posinset, aria-setsize, aria-expanded when it has children, aria-disabled when disabled, a roving tabindex, and OnFocus reporting it current. Every handled key is default-prevented.

TreeNavigation.BehaviorOptions

interface · packages/primitives/src/interaction/tree-navigation.ts:195

interface BehaviorOptions<Input, Slots> {
  readonly container: keyof Slots & string
  readonly item: keyof Slots & string
  readonly rows: (input: Input) => ReadonlyArray<Row>
  readonly domId?: (id: string) => string
  readonly direction?: (input: Input) => 'ltr' | 'rtl'
}

TreeNavigation.bundle

const · packages/primitives/src/interaction/tree-navigation.ts:62

const bundle: Bundle.Bundle<
  'TreeNavigation',
  { readonly openByDefault: boolean },
  { readonly current: string | null; readonly toggled: readonly string[] },
  | { readonly _tag: 'Opened'; readonly id: string }
  | { readonly _tag: 'Closed'; readonly id: string }
  | { readonly _tag: 'Focused'; readonly id: string },
  never,
  never,
  never,
  void,
  {},
  {}
>

TreeNavigation.isOpen

function · packages/primitives/src/interaction/tree-navigation.ts:49

function isOpen(model: Model, args: Args, id: string): boolean

Whether a row is open, given the default and what was toggled.

TreeNavigation.Message

const · packages/primitives/src/interaction/tree-navigation.ts:34

const Message: MessageUnion<{
  readonly Focused: { readonly id: Schema.String }
  readonly Opened: { readonly id: Schema.String }
  readonly Closed: { readonly id: Schema.String }
}>

TreeNavigation.Model

const · packages/primitives/src/interaction/tree-navigation.ts:26

const Model: Schema.Struct<{
  readonly current: Schema.NullOr<Schema.String>
  readonly toggled: Schema.$Array<Schema.String>
}>

TreeNavigation.move

function · packages/primitives/src/interaction/tree-navigation.ts:153

function move(
  rows: ReadonlyArray<Shown>,
  current: string | null,
  key: string,
  modifiers: KeyboardModifiers,
  model: Model,
  args: Args,
  direction?: 'ltr' | 'rtl',
): Action | undefined

What a key does from the current row, over the rows that are showing, or undefined when it is not a key the tree handles. Disabled rows are stepped over. With nothing current, Down and Home land on the first row, Up and End on the last. Right and Left swap under rtl.

TreeNavigation.Row

interface · packages/primitives/src/interaction/tree-navigation.ts:83

interface Row {
  readonly id: string
  readonly parent: string | null
  readonly branch: boolean
  readonly disabled?: boolean
}

One row of the tree, in tree order: a parent comes before its children.

TreeNavigation.Shown

interface · packages/primitives/src/interaction/tree-navigation.ts:93

interface Shown extends Row {
  readonly level: number
  readonly position: number
  readonly siblings: number
}

A row as it is showing: where it sits, for its ARIA attributes.

TreeNavigation.shown

function · packages/primitives/src/interaction/tree-navigation.ts:106

function shown(
  rows: ReadonlyArray<Row>,
  model: Model,
  args: Args,
): ReadonlyArray<Shown>

The rows that are showing: every row whose ancestors are all open, with its level and its place among its siblings. A row whose parent is not a row is treated as top-level.

TreeNavigation.tabStop

function · packages/primitives/src/interaction/tree-navigation.ts:190

function tabStop(
  rows: ReadonlyArray<Shown>,
  current: string | null,
): string | undefined

The row that holds the tab stop: the current one while it is showing, else the first.