You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
OINK already renders a usable server-side sidebar and provides drawer focus trapping, focus return, desktop collapse persistence, breakpoint cleanup, and a visible restore control. Issue #24 fixed cached markup and no-JavaScript visibility; this proposal concerns runtime state after that markup is available.
Problem
Apache HugeGraph encountered this gap while migrating its documentation site from Docsy to OINK. Its bilingual, versioned documentation needed reader-selected tree disclosures to survive navigation, but OINK exposes no public setter or committed-state notification.
Inside OINK, multiple private code paths write the same disclosure state independently. Closed or collapsed navigation is moved or faded off-screen but is not explicitly removed from the focus and accessibility trees.
This proposal routes every disclosure write through one OINK-owned controller and isolates hidden navigation while preserving current shell behavior. Persistence remains optional and downstream-owned.
Confirmed behavior on current main
Verified against main@93ac292. The table below separates existing behavior from confirmed gaps.
Invisible navigation remains potentially focusable; isolation must not disable the intentional hover target.
The design gap is limited to OINK's supported runtime surface: a consumer that reasonably needs committed disclosure state has no public setter or notification, and closed/collapsed navigation has no OINK-owned interaction isolation. A particular downstream workaround or custom visual treatment is not evidence of an upstream defect.
Existing behavior to preserve
This is not a sidebar redesign. The implementation must retain the following current OINK behavior:
The drawer stores its opener, traps Tab while open, restores focus on close, and closes at the desktop breakpoint: initDrawer().
Desktop collapse remains persisted and retains the deliberate 16px pointer target and hover overlay: initCollapse().
Scroll locking continues to derive from the drawer/search root state, so removing the drawer-open attribute releases it: _tokens.scss.
The desktop floating restore control remains available after explicit collapse: floating-pill.html.
Server-rendered authored state and the visible, navigable no-JavaScript fallback from #24 remain authoritative before runtime enhancement.
1. Centralize disclosure ownership
All internal and public disclosure transitions should use one controller. The exact namespace is open for discussion; a concrete minimum shape is:
controlledRegionID is the target named by the toggle's aria-controls, not a title or position-dependent index. The controller must scope lookup to the sidebar, reject or report unknown IDs predictably, and make repeated writes of the current value a no-op.
One atomic commit updates the toggle's aria-expanded, the controlled region's td-is-open class, and the localized expand/collapse label. The event fires exactly once, after all three values agree, and only when committed state changes.
The user-click handler, cached active-path hydration, responsive aside relocation, and public setter must all call this controller. A wrapper that consumes the event must never need a MutationObserver, a second click handler, or a deferred DOM read.
2. Keep persistence optional and downstream-owned
The contract should make persistence reliable without requiring OINK to own a version/locale storage schema. A downstream site may listen for committed user disclosures, store valid controlled-region IDs under its own context key, then restore them through setExpanded().
Active-path expansion remains authoritative, unknown/stale IDs are ignored, and unavailable storage falls back to server-rendered authored state. These are integration acceptance requirements, not a mandatory OINK localStorage implementation.
If script ordering cannot make restoration deterministic, expose a late-consumer-safe readiness primitive such as OinkSidebar.ready or a persistent ready marker plus event. A fire-once event without a readable marker is insufficient because a consumer loaded later can miss it.
3. Isolate hidden navigation
Disclosure state and whole-sidebar visibility are separate state machines, but OINK owns enough information to keep them consistent. Hidden navigation must not remain keyboard-focusable or exposed to assistive technology.
Surface state
Required isolation behavior
Desktop expanded
Navigation content is active and exposed.
Desktop collapsed
Navigation content is inert and aria-hidden; the external restore control and deliberate 16px hover sensor remain operable.
Desktop hover overlay
Remove isolation before revealing the panel or moving focus inside; reapply it after the overlay closes.
Mobile drawer closed
The off-screen panel is inert and aria-hidden.
Mobile drawer open
Remove isolation before focusing the close button; on close, restore focus outside the panel before reapplying isolation.
Breakpoint/navigation cleanup
Close transient surfaces and release the root scroll lock without leaving stale isolation attributes.
Do not put inert on an element that also owns the collapsed desktop pointer-enter sensor; isolate the navigation content or use a separate non-inert edge trigger so the existing hover-overlay behavior remains reachable.
Acceptance tests
Pointer and keyboard disclosure update aria-expanded, td-is-open, and the localized label atomically, then emit exactly one final-state event.
Writing the current state again emits no event; invalid IDs cannot mutate content outside the sidebar.
User click, active-path hydration, responsive relocation, and public API calls all pass through the same controller and report the correct source.
A downstream persistence fixture can restore version/locale-scoped IDs through the public setter; active path wins, stale IDs are ignored, and blocked storage keeps authored state.
Mobile closed and desktop collapsed navigation have no focusable or accessibility-visible descendants, while the desktop restore control and hover sensor remain usable.
Opening removes isolation before focus enters; closing restores focus outside before isolation returns.
Drawer focus trap, Escape/backdrop close, breakpoint cleanup, scroll unlock, desktop hover overlay, and floating restore behavior do not regress.
Duplicate initialization or downstream event consumption does not create duplicate listeners, oscillating state, or repeated events.
With JavaScript disabled, the server-rendered sidebar remains visible and navigable exactly as required by #24.
Non-goals
Do not introduce a second sidebar tree or replace server-rendered authored state.
Do not require OINK to understand a downstream site's release, version, locale, or storage migration policy.
Do not remove the desktop hover overlay, drawer focus trap, focus return, scroll locking, or floating restore control.
Do not make JavaScript necessary for basic sidebar visibility or navigation.
Do not reopen the cache-key and no-JavaScript rendering problem resolved by #24.
Downstream compatibility
Downstream sites can retain their current wrappers until this contract is available in a tagged OINK release. Upstream acceptance therefore does not block downstream deployment; after adoption, post-click persistence listeners and root-attribute MutationObserver wrappers can be removed in favor of the public committed-state contract.
Open design decisions
Should the API live on OinkSidebar, or should disclosure and surface state use separate namespaces?
Should an unknown controlled-region ID throw, return false, or produce a development warning?
Is OinkSidebar.ready needed, or can registration/restoration be deterministic through bundle order and a persistent ready marker?
Should surface open/close transitions publish a separate oink:sidebar-surface event, or remain internal while only disclosure state is public?
Note
Background
OINK already renders a usable server-side sidebar and provides drawer focus trapping, focus return, desktop collapse persistence, breakpoint cleanup, and a visible restore control. Issue #24 fixed cached markup and no-JavaScript visibility; this proposal concerns runtime state after that markup is available.
Problem
Apache HugeGraph encountered this gap while migrating its documentation site from Docsy to OINK. Its bilingual, versioned documentation needed reader-selected tree disclosures to survive navigation, but OINK exposes no public setter or committed-state notification.
Inside OINK, multiple private code paths write the same disclosure state independently. Closed or collapsed navigation is moved or faded off-screen but is not explicitly removed from the focus and accessibility trees.
This proposal routes every disclosure write through one OINK-owned controller and isolates hidden navigation while preserving current shell behavior. Persistence remains optional and downstream-owned.
Confirmed behavior on current
mainVerified against
main@93ac292. The table below separates existing behavior from confirmed gaps.initTreeToggles()contains a privatesetExpanded()closure.sidebar-nav.jswritesaria-expanded, the localized label, andtd-is-opendirectly.setGroups()independently writes the same disclosure fields.inertoraria-hidden.The design gap is limited to OINK's supported runtime surface: a consumer that reasonably needs committed disclosure state has no public setter or notification, and closed/collapsed navigation has no OINK-owned interaction isolation. A particular downstream workaround or custom visual treatment is not evidence of an upstream defect.
Existing behavior to preserve
This is not a sidebar redesign. The implementation must retain the following current OINK behavior:
initDrawer().initCollapse().floating-pill.html.1. Centralize disclosure ownership
All internal and public disclosure transitions should use one controller. The exact namespace is open for discussion; a concrete minimum shape is:
controlledRegionIDis the target named by the toggle'saria-controls, not a title or position-dependent index. The controller must scope lookup to the sidebar, reject or report unknown IDs predictably, and make repeated writes of the current value a no-op.One atomic commit updates the toggle's
aria-expanded, the controlled region'std-is-openclass, and the localized expand/collapse label. The event fires exactly once, after all three values agree, and only when committed state changes.The user-click handler, cached active-path hydration, responsive aside relocation, and public setter must all call this controller. A wrapper that consumes the event must never need a
MutationObserver, a second click handler, or a deferred DOM read.2. Keep persistence optional and downstream-owned
The contract should make persistence reliable without requiring OINK to own a version/locale storage schema. A downstream site may listen for committed user disclosures, store valid controlled-region IDs under its own context key, then restore them through
setExpanded().Active-path expansion remains authoritative, unknown/stale IDs are ignored, and unavailable storage falls back to server-rendered authored state. These are integration acceptance requirements, not a mandatory OINK
localStorageimplementation.If script ordering cannot make restoration deterministic, expose a late-consumer-safe readiness primitive such as
OinkSidebar.readyor a persistent ready marker plus event. A fire-once event without a readable marker is insufficient because a consumer loaded later can miss it.3. Isolate hidden navigation
Disclosure state and whole-sidebar visibility are separate state machines, but OINK owns enough information to keep them consistent. Hidden navigation must not remain keyboard-focusable or exposed to assistive technology.
inertandaria-hidden; the external restore control and deliberate 16px hover sensor remain operable.inertandaria-hidden.Do not put
inerton an element that also owns the collapsed desktop pointer-enter sensor; isolate the navigation content or use a separate non-inert edge trigger so the existing hover-overlay behavior remains reachable.Acceptance tests
aria-expanded,td-is-open, and the localized label atomically, then emit exactly one final-state event.source.Non-goals
Downstream compatibility
Downstream sites can retain their current wrappers until this contract is available in a tagged OINK release. Upstream acceptance therefore does not block downstream deployment; after adoption, post-click persistence listeners and root-attribute
MutationObserverwrappers can be removed in favor of the public committed-state contract.Open design decisions
OinkSidebar, or should disclosure and surface state use separate namespaces?false, or produce a development warning?OinkSidebar.readyneeded, or can registration/restoration be deterministic through bundle order and a persistent ready marker?oink:sidebar-surfaceevent, or remain internal while only disclosure state is public?