busy-office-ui
Menu

JS behaviors

The framework is CSS-first, not CSS-only. 26 small behaviors (zero dependencies) cover what the platform can't do or what accessibility demands: each is a call-once init using document-level event delegation, so DOM swaps (HTMX, Alpine, anything) never require re-binding.

The inits generated

InitWhat it addsDOM hooks it reads
initDialogs() Dialog behavior via event delegation — no per-element binding, no DOM flags. data-dialog-trigger, data-dismissible, data-state
initDataTables() Optional table behavior (~1 kB): wires select-all, keeps an O(1) data-any-selected attribute on the container (so CSS needn't re-scan every row via :has() on large grids), and maintains a live "n… .bo-data-table-container, .bo-data-table__row-select, .bo-data-table__select-all, .bo-data-table__selection-count, data-any-selected, data-selected-count-override
initDataGrid() Opt-in ARIA grid pattern for .bo-data-table (WAI-ARIA APG "Grid", the interactive-widgets variant). .bo-data-table, .bo-data-table-container, .bo-data-table__row-select, .bo-data-table__select-all, data-col, data-grid-nav, data-table
initRowEdit() Multi-row inline edit: tracks which rows have unsaved changes, shows a per-row Save/Cancel affordance, and dispatches a `bo:row-save` event for your code to persist (fetch, hx-patch, form submit —… .bo-btn, .bo-btn--ghost, .bo-btn--icon, .bo-btn--secondary, .bo-btn--sm, .bo-data-table, .bo-data-table__row-edit-actions, .bo-icon, .bo-icon--close, .bo-icon--save, .bo-input, .bo-input--seamless, data-row-edit, data-row-edit-cancel, data-row-edit-dirty, data-row-edit-save, data-row-id, data-row-state
initTableToolbar() Optional data-table toolbar actions: column visibility + export. .bo-btn, .bo-btn--secondary, .bo-checkbox, .bo-data-table, .bo-data-table-container, .bo-data-table__toolbar, .bo-dropdown__menu, data-col, data-col-toggle, data-multiselect, data-multiselect-label, data-table-export, data-table-export-format
initStickyCols() Multiple frozen columns (roadmap 72.1) — opt-in via [data-sticky-cols], "2" | "3" only. .bo-data-table, .bo-data-table--sticky-col, .bo-sticky-w-1, .bo-sticky-w-2, data-sticky-cols, data-table
initTableSum() Declarative realtime column totals — the auto-sum half of Slice 18's subtotal contract (a deliberate, named exception to "you do the data"; the other half is listening to `bo:cell-change` yourself for… .bo-data-table__col--numeric, data-decimals, data-sum-of
initTreeTable() Expand/collapse for hierarchical table rows — document-level delegation, call initTreeTable() once. .bo-data-table, .bo-tree-table, .bo-tree-table__toggle, data-sum-of, data-tree-level, data-tree-table
initLoadMore() Load-more pagination ("pull up to see more") — the infinite-scroll alternative to page-number `.bo-pagination`. .bo-btn, .bo-btn--secondary, .bo-data-table, .bo-data-table-container, .bo-data-table__footer, .bo-pagination, data-load-more-auto, data-table-load-more
initWindowedList() Windowed list: server-chunked rows with client-side memory release (roadmap 30.4b) — for SCANNING a large list, not searching one. .bo-app-shell__main, .bo-btn, .bo-btn--secondary, .bo-data-table, .bo-data-table-container, .bo-data-table__footer, .bo-data-table__row-select, .bo-data-table__spacer, .bo-density-row-height, data-chunk-id, data-chunk-offset, data-chunk-size, data-evicted, data-grid-nav, data-load-more-auto, data-row-id, data-selected-count-override, data-table, data-table-load-more, data-table-total-rows, data-window-chunks, data-windowed, data-windowed-selection-host
initAnchorNav() Anchor navigation for long detail screens (the "object page" floorplan). .bo-anchor-landing-offset, .bo-app-shell__main, .bo-motion-duration-slow, .bo-pagination, .bo-sidebar-nav, .bo-sticky-w-1, data-anchor-collapse, data-anchor-nav, data-state
initCollapsibleCards() Collapsible card sections (.bo-widget). .bo-widget__collapse, data-collapse-trigger, data-state
initSavedViews() Saved-view / filter-bar persistence. .bo-filter-bar, data-saved-views
initWizard() Multi-step wizard flow — one panel visible at a time, with a bo-stepper kept in sync. .bo-stepper, .bo-stepper__step, data-state, data-wizard, data-wizard-back, data-wizard-current, data-wizard-next, data-wizard-panel, data-wizard-submit
initQuantity() Increment/decrement buttons for .bo-quantity — document-level delegation, call initQuantity() once. .bo-money__currency, .bo-quantity, .bo-quantity__input, .bo-quantity__step, .bo-quantity__unit-select, data-decimals, data-quantity-step
initMoneyField() Currency-aware money field — a currency <select> linked to an amount input: changing currency updates the input's `step` (decimal precision) and reformats the current value to that precision. .bo-input, .bo-input--numeric, .bo-money, .bo-money__amount, .bo-money__currency, .bo-select, data-decimals
initGroupedNumber() Grouped display for numeric inputs — thousands separators via `Intl.NumberFormat`, formatted ON BLUR, never while typing (123.1: what Reckon ships, what SAP's own guidance steers to after live… .bo-input, .bo-input--numeric, data-decimals, data-grouped, data-locale
initScanInput() RF/handheld barcode-scanner input. .bo-input, .bo-input--code, .bo-visually-hidden, data-scan-flash, data-scan-input, data-scan-result, data-scan-status, data-scan-terminator
initFileDropzone() Drag-and-drop convenience for a `.bo-file-dropzone`. .bo-file-dropzone, .bo-file-input, .bo-visually-hidden, data-dragover, data-file-dropzone
initTagInput() Multi-value tag/chip entry (Fiori Token/MultiInput; Ant/Carbon Tag) — multi-tag cost centers on a record, multiple approval-routing recipients. .bo-alert, .bo-tag-input, .bo-tag-input__field, .bo-tag-input__remove, .bo-tag-input__tag, data-grid
initValidationSummary() Inline validation summary. .bo-alert, .bo-alert--danger, data-validation-summary, data-validation-summary-box
initTabs() ARIA tabs behavior via document-level delegation — call initTabs() once. .bo-tabs--vertical, .bo-tabs__list, .bo-tabs__tab, data-overflow
initDropdowns() Popover-based dropdown menus. .bo-btn, .bo-btn--secondary, .bo-checkbox, .bo-dropdown__item, .bo-dropdown__menu, .bo-dropdown__menu--end, data-multiselect, data-multiselect-count, data-multiselect-label
initContextMenu() Right-click (contextmenu) trigger for the SAME popover menu component initDropdowns() already opens on click (roadmap 73.1). .bo-checkbox, .bo-dropdown__item, .bo-dropdown__menu, data-col-toggle, data-context-menu
initCombobox() Editable combobox with list autocomplete — WAI-ARIA APG combobox pattern (single-select, list autocomplete, manual selection). .bo-combobox, .bo-combobox__group, .bo-combobox__listbox, .bo-combobox__option, .bo-combobox__option-label, .bo-input, .bo-visually-hidden, data-name, data-open-on-focus, data-value
initAlerts() Dismiss behavior for alerts and toasts: any .bo-alert__dismiss button removes its enclosing .bo-alert. .bo-alert, .bo-alert__dismiss, .bo-toast, data-state

Keyboard support generated

The four behaviors that own their own key handling — everything else is native (a plain <input>, a native <dialog>'s own focus trap) or has no keyboard surface (delegation-only behaviors like alerts). Extracted from source, so it can't drift from what's actually wired.

BehaviorKeyDoes
initCombobox() ArrowDown / ArrowUp open the list (filtering first if closed), then move the active option; clamps at the ends
Enter commit the active option (dispatches bo:combobox-select)
Enter (list open, nothing active) swallowed, never submits the form; commits the sole match if there is exactly one
Escape close without changing the value (native popover light-dismiss)
Tab focus leaves the widget: the list closes and the active option clears
(focus after a commit) the committed text is selected, so typing browses the full list again
initDataGrid() ArrowRight / ArrowLeft / ArrowDown / ArrowUp move the cell cursor; clamps at the grid edges
Home / End jump to the first/last cell in the row; with Ctrl/Cmd, the first/last cell in the grid
Enter focus the cell's one interactive widget, if it has one
Escape return focus from a widget to its parent cell
initDialogs() Escape close (native showModal(); skipped if data-dismissible="false")
Tab / Shift+Tab cycle focus inside the dialog, wrapping at the ends (focus trap)
initTabs() ArrowRight / ArrowLeft move to the next/previous tab in a horizontal strip, wrapping; activates immediately
ArrowDown / ArrowUp the same, in a vertical rail (.bo-tabs--vertical)
Home / End jump to the first/last tab; activates immediately

The delegation model

Each init installs one document-level listener and resolves its target at event time; element-level listeners (a dialog's close handler) bind lazily on first use, tracked in a WeakSet — never serialized into the DOM. What you can rely on:

  • Triggers may render before their dialog exists.
  • Swapped-in content works without re-init.
  • Clone / morph swaps can't strand stale state.

Event wiring is swap-proof; derived state is re-computed. The one HTMX-aware line in core JS is the htmx:after:swap event name a table container listens for:

// Bound table containers re-derive selection state automatically when an
// htmx:after:swap bubbles through them. Any OTHER swap mechanism calls:
import { refreshDataTable } from "@busy-office/ui/js";
refreshDataTable(container);

// Only NEW containers swapped in later need re-binding:
document.body.addEventListener("htmx:after:swap", (e) => initDataTables(e.target));

Intent events

Behaviors report user intent through DOM events — your code decides what to persist. The full generated list (name, target, when it fires, payload) is the intent-event index; it is look-up material, not something to read once.

State attributes generated

Two attributes, deliberately kept separate rather than merged into one lifecycle vocabulary — a component is either OPEN or BUSY, and conflating the two would make "is this dialog open while its save is in flight" unrepresentable in one value.

AttributeValuesMeans
data-state closed|closing|current|done|open|pending|rejected|resolved structural: is this open or closed (dialog, offcanvas, dashboard cards, the motion module's collapse), or where a multi-step control is in its own sequence (stepper, approval-workflow's timeline). Same attribute name, two genuinely different domains — read it per component, never assume the full value set applies everywhere it appears.
data-loading true busy: an async request is in flight (button, data-table). The opt-in @busy-office/ui/css/htmx module bridges it onto HTMX's own request state — .htmx-request [data-loading] disables pointer events for the duration with no JS wiring on your part.

Where a component's states don't fit either shape, it gets its own attribute rather than overloading these two: data-row-state (dirty|error|warning) on data-table rows, data-day (closed|holiday|selected|today) on the calendar. A shared name across unrelated domains is a worse contract than two honestly different ones.

The Save sequence

The pattern behind every Save/Submit/Approve button in this framework's pattern catalogue, as it actually composes today — no dedicated "loading state" component, because the pieces above already cover it:

  1. Idle — the button reads "Save".
  2. Saving — your JS (or the HTMX bridge, for free) sets data-loading="true" and aria-busy="true" on the button, and swaps its text to "Saving…". Two channels: the text change is what a sighted user reads; aria-busy is what a screen reader announces. Neither alone is the contract.
  3. Saved (or Error) — clear both attributes and restore the button's resting label, or route to your error state (a field's aria-invalid, a summary, a toast — pattern- specific, not this button's job).

Deliberately no spinner icon or success checkmark on the button itself — a real, measured decision, not an oversight: an opacity-dimmed solid-fill button drops to roughly 3.24:1 text contrast (axe color-contrast), under the 4.5:1 minimum. The button's own visible channel stays the text swap plus cursor: progress; a spinner or checkmark belongs beside or inside the button as your own markup if the pattern wants one — the framework does not force a specific glyph onto every Save action.

Import paths

import { initDialogs } from "@busy-office/ui/js";          // barrel
import { initDialogs } from "@busy-office/ui/js/behaviors/dialog.js"; // granular
↑↓ move Enter open Esc close