Accessibility (a11y)
Polish notes for keyboard and assistive-tech support in @hungpvq/draggable shells (Vue + React parity).
Related: Stable API · ContextMenu · CSS tokens
SemVer: Escape-to-close when focus is inside a Stable panel shell is part of the documented contract (additive minor behavior). See skill draggable-semver-api.
Panel contracts by type
| Panel | Role | Modal? | Escape | Focus on open | Tab trap | Background inert |
|---|---|---|---|---|---|---|
| Modal | dialog + aria-modal="true" | yes | closes | focusFirst | yes (trapTabKey) | yes (setModalSiblingsInert) |
| Popup | dialog (non-modal) | no | closes if focus in panel | focusFirst | yes (trapTabKey) | no (map stays usable) |
| Float | dialog (non-modal) | no | closes if focus in panel | focusFirst | no | no |
| Drawer | dialog (non-modal) | no | closes if focus in panel* | focusFirst | no | no |
| Sidebar | complementary | no | closes if focus in shell* | focusFirst | no | no |
| Bottom | region | no | closes if focus in shell* | focusFirst | no | no |
*Escape is ignored while a switcher ContextMenu is open (menuOpen), so Esc closes the menu first.
All titled shells prefer aria-labelledby pointing at the title host; bottom keeps aria-label="Bottom panel" as a fallback name. Panel roots use tabindex="-1" so they can receive focus when needed. On close, shells call restoreFocus to the opener.
Icon-only chrome (DragButton — internal, not a public export): native disabled + descriptive aria-label; expand toggles also set aria-expanded (and aria-controls where a content host id exists). Decorative header rules use aria-hidden="true". Do not confuse with map-core MapControlButton.
Modal (DraggableModal)
role="dialog"+aria-modal="true"+aria-labelledby(header title id)- Focus moves into the dialog on open; restored to the previously focused element on close
- Tab / Shift+Tab trapped inside the dialog (
trapTabKey) - Escape closes
- Background siblings under
.draggable-rootgetinert+aria-hiddenwhile open (reference-counted for nested modals)
Popup / float
- Non-modal
role="dialog"+aria-labelledby+tabindex="-1" - Expand control:
aria-expanded; Close / Bring forward / Send back labeled - Escape closes when focus is inside the panel; focus restored on close
- Popup uses
trapTabKey(Tab cycles within the panel). Float does not usesetModalSiblingsInertor a mandatory tab trap. - Does not use
setModalSiblingsInert
Drawer
- Non-modal
role="dialog"+aria-labelledby - Switcher (when multiple drawers on an edge):
aria-haspopup="menu",aria-expanded, ContextMenuariaLabel/ open-change - Escape + focus restore as in the table above
- Resize handle is decorative for AT (
aria-hidden)
Sidebar / bottom
- Sidebar:
role="complementary"+aria-labelledby - Bottom:
role="region"+aria-labelledby/aria-label;aria-hiddenwhen not visible; expand setsaria-controlsto the content host - Switcher triggers:
aria-haspopup="menu"+aria-expanded - Escape + focus restore as in the table above
Context menu (experimental)
role="menu"+aria-orientation="vertical"+aria-label(prop: VueariaLabel/ ReactariaLabel, default"Context menu")- Open/close notify: Vue
@update:open, ReactonOpenChange(open) - Items:
role="menuitem",aria-disabled,aria-currentwhenactive - Wrap list markup as
<ul class="context-menu" role="presentation">so the outermenurole owns the structure - Escape closes and restores focus to the opener (
restoreFocus) - Tab / Shift+Tab cycle
role="menuitem"nodes (getMenuItems; falls back totrapTabKeyif none) - Arrow Up/Down (and Left/Right aliases), Home/End, Enter/Space via
handleMenuKeydown - Typeahead: printable characters jump to the next matching label (buffer resets after 500ms;
clearMenuTypeaheadon close)
ManagementControl (experimental)
Light a11y only: section titles as headings (h3), icon actions with aria-label (+ title), list rows activatable via click or Enter/Space (role="button").
Core helpers (Stable)
| Helper | Use |
|---|---|
getFocusableElements / focusFirst / restoreFocus | Focus management |
trapTabKey | Dialog / menu tab cycle |
setModalSiblingsInert | Background inert while modal open |
getMenuItems / handleMenuKeydown / clearMenuTypeahead | Menu keyboard + typeahead |
Changing these helpers’ documented behavior is a SemVer decision — see stable-api.md and skill draggable-semver-api.
Out of scope
- Keyboard drag / resize of panels driven by
vue-draggable-resizable/react-rnd - ContextMenu submenus; typeahead for multi-locale collation beyond basic character match
- Full WAI-ARIA menu-button
aria-controlsid wiring on every trigger (optional future; shells already expose content host ids where useful)