Mixins / foldkit-mixins

Behavior: reusable element-level interaction

A Behavior is a reusable bundle of element-level interaction that does not own application state. Its attributes are built while resolving from the view's input and h, so it can only emit Messages from the view's own Message universe. Like an agent capability, a Behavior points one way — toward a Message — and never gets a Model setter.

ts
const Focus = Behavior.forSlots(FieldSlots)<FieldInput, Message>({
  input: Behavior.slot({
    requires: { capability: Capability.TextInput },
    attributes: ({ input, h }) => (input.invalid ? [h.AriaInvalid(true)] : []),
    mount: input => MeasureField(input.value), // your own Mount.define* action
  }),
})

requires is checked against the slot contract at definition time. A behavior that requires Event.Click cannot attach to a slot that did not publish click. Multiple mount contributions compose into one OnMount per element.

A slot rendered once per item (rows, tabs, options) tells the Behavior which repetition it is resolving through the second argument of attrs:

ts
h.ul(slots.list.attrs(), items.map((item, index) =>
  h.li(slots.row.attrs([h.Key(item.id)], { index, id: item.id, count: items.length }), [item.label]),
))

The Behavior receives it as item ({ index, id?, count? }) in attributes and as the second argument of mount, so one Behavior can write tabindex="0" on the current row and -1 on the rest, or aria-posinset and aria-setsize on each. Without an item argument, item is undefined and the Behavior decorates the slot as a single element.

Ready-made Behaviors

Behaviors holds Behaviors that own no state. The first is Collection: describe the parent's array once, and a view and the Behaviors on it agree about identity, order, and disabled items.

ts
import { Behaviors } from 'foldkit-mixins'

const items = Behaviors.Collection.of(input.tools, {
  id: tool => tool.id,
  disabled: tool => tool.disabled,
}) // ids, size, at, indexOf, isDisabled, enabled, slotItem

const Ids = Behaviors.Collection.behavior(ToolbarSlots)<ToolbarInput, Message>({
  item: 'tool',
  items: input => Behaviors.Collection.of(input.tools, { id: tool => tool.id }),
  posInSet: true,
})

// in the view: slots.tool.attrs([h.Key(tool.id)], items.slotItem(index))

of is pure and refuses two items with one id (mixins:duplicate-item-id), since an id is what aria-activedescendant and aria-controls rely on. The Behavior writes id, aria-disabled, and with posInSet aria-posinset and aria-setsize, reading the item context the view passes; it contributes nothing to a slot resolved without one. Behaviors that need state, such as a roving tab stop or a press, live in foldkit-primitives as a Bundle plus its Behavior.

Four more stateless Behaviors say a parent's state in ARIA, so a custom view gets it right without hand-writing it. Each takes slot names and functions of the view's input; none owns anything.

  • Behaviors.Disclosure.behavior(Slots)<Input, Message>({ trigger, content, open, id }) writes aria-expanded and aria-controls on the trigger, id and hidden on the content. The floor is <details>.
  • Behaviors.ToggleState.behavior(Slots)({ control, state, as: 'checked' | 'pressed' }) writes aria-checked (checkbox, switch, radio, option) or aria-pressed (a toggle button) from a boolean | 'mixed'.
  • Behaviors.FieldAssociation.behavior(Slots)({ control, label, description?, error?, id, invalid?, required? }) derives <id>, <id>-label, <id>-description, <id>-error from one base id (FieldAssociation.ids(id) for a view that renders them itself) and writes id, for, aria-labelledby, aria-describedby (the error joins while invalid), aria-invalid, aria-required. Derived, not minted, so a resumed page and a test agree.
  • Behaviors.SpinValue.behavior(Slots)({ control, value, onChange, min?, max?, step?, page?, text? }) writes the spinbutton role and aria-value*, and steps the parent's number with ArrowUp and ArrowDown by step, PageUp and PageDown by page (default ten steps), Home and End to the bounds, clamped and default-prevented; SpinValue.spin(value, key, modifiers, bounds) is the pure step. A press-and-hold repeat and the wheel are not handled: Foldkit's wheel attribute carries no delta, and a repeat is a timer the Model would own.