Local labelled tabbed panels¶
RavetControls.TabbedPanels composes consumer-created native buttons and panel
containers. It owns their local tab semantics and selection. Consumers supply
visible tab labels, panel contents, placement, styling and host transport. Load the
shared controls asset first.
const tabs = new RavetControls.TabbedPanels(document.querySelector('#settings-tabs'), [
{tab: document.querySelector('#audio-tab'), panel: document.querySelector('#audio-panel')},
{tab: document.querySelector('#midi-tab'), panel: document.querySelector('#midi-panel')},
], {
label: 'Audio and MIDI settings',
activation: 'manual',
onActivate: ({index, reason}) => console.log(index, reason),
});
// Quiet consumer selection; retain current browser focus.
tabs.activate(1, {focus: false, notify: false});
// Retire the association before retiring its frame/window.
tabs.dispose();
The fixed profile is 1..256 distinct {tab, panel} pairs in one connected live
owner document. Each tab is a native button with explicit type="button", inside
the supplied tab-list element. Panels are connected, outside that list and
nonnested with other supplied panels/tabs. Every tab and panel needs a stable
consumer-supplied document-unique ID, 1..128 UTF-16 code units without whitespace.
Preflight checks pairwise ID uniqueness and getElementById ownership; it does
not audit arbitrary duplicate IDs elsewhere in consumer markup. The explicit
list label is nonblank, at most 256 code units. Invalid profiles reject before
attributes/listeners change.
Options are activation: 'manual' | 'automatic' (default manual),
orientation: 'horizontal' | 'vertical' (default horizontal), rtl: boolean
(default false), wrap: boolean (default true), initial in-range integer index
(default zero), and onActivate: function | null (default null). An unavailable
initial index falls back to the first eligible tab, quietly. No eligible tabs
selects null and hides every supplied panel.
Manual arrows/Home/End move roving focus without changing panel selection. The
focused unselected tab becomes the single tab stop. Native Enter/Space/click
activates it once. Automatic mode activates on changed tab focus; a following
native click on the same tab does not duplicate notification. Disabled, hidden,
inert, ARIA-disabled and CSS-hidden tabs are skipped. Modified navigation remains
available to the consumer/browser. Tab uses native browser navigation to the
selected panel and its contents; there is no focus trap. Panel containers receive
tabindex="0"; their descendants remain consumer-owned.
activate(index, {focus=true, notify=true}) rejects unavailable indices,
nonboolean options, reentry, disposal and invalid associations. focus: false
retains native focus and the existing roving position. Changing selection hides
the previous panel; consumers choosing quiet unfocused updates own any focus
inside that retiring panel. Selecting the same panel is a notification no-op,
although requested focus is attempted. selected() returns the current index,
or null when none/disposed. refresh() returns eligible indices and preserves an
eligible selection, repairing owned panel visibility. Otherwise it selects the
first eligible tab or null and emits one eligibility notification. If the retiring
selected panel contains focus, refresh transfers it to the replacement tab when
one exists. External focus is preserved; no eligible replacement leaves browser
focus fallback to the browser.
Callbacks receive frozen {index, tab, panel, reason}; reason is activate,
focus or eligibility. Null selection has null tab/panel. Attributes and panel
visibility are coherent before notification. Selection commits before optional
focus: focus handlers that remove/dispose the association cause a false return
and cancel notification; selection is not rolled back if focus fails. Reentrant
selection/refresh is rejected throughout focus and notification; disposal remains
legal. A throwing callback retains committed selection, returns false and reports
lastError() === 'Callback'. No automatic host acknowledgement or rollback is
implied. Successful operations clear that diagnostic. Focus/notification handlers that redirect
required focus or change selected-tab eligibility reject stale success; invalid
associations dispose immediately after notification, including throwing callbacks.
Eligibility changes can require the next observer checkpoint or another refresh
to choose a replacement; no stale eligible list is returned.
The widget temporarily owns list role/orientation/label (removing its original
aria-labelledby), tab role/selection/control links, panel role/labelled-by links,
hidden state and panel tabindex, and roving tab tabindex. Dispose restores the
exact original attribute values/presence and removes listeners/observer. Consumers
must not mutate owned attributes while active. Original-document observation
handles disable/hidden/inert/ARIA/class/style changes. Explicit refresh handles
stylesheet/media changes. Removal, adoption, changed IDs/button types or invalid
fixed membership/nesting disposes the whole association and restores attributes,
even if nodes are detached. Consumers dispose before hiding/removing their owner
iframe/window. Source membership is fixed; construct a new widget for new pairs.
The contract fixture
covers DOM semantics, bounds, directional selection, lifecycle, reentry and
restoration in Chromium and hidden WebKit. Trusted Chromium keys establish manual
focus versus activation, automatic selection without click duplication and native
Tab order. The hidden preview reports document.hasFocus() === false outside key
routing, so focus-listener regressions explicitly dispatch a focus event after
native focus. This is local browser evidence, not native-window focus or
screenreader validation. No dynamic membership, lazy content loading, native OS
tabs, global modal routing, layout engine, shadow-root focus engine or real-time
deadline is claimed. See ADR0192.