Skip to content
Docs version
@hungpvq/draggable@1.3.1@hungpvq/vue-draggable@1.3.1@hungpvq/react-draggable@1.3.1

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

PanelRoleModal?EscapeFocus on openTab trapBackground inert
Modaldialog + aria-modal="true"yesclosesfocusFirstyes (trapTabKey)yes (setModalSiblingsInert)
Popupdialog (non-modal)nocloses if focus in panelfocusFirstyes (trapTabKey)no (map stays usable)
Floatdialog (non-modal)nocloses if focus in panelfocusFirstnono
Drawerdialog (non-modal)nocloses if focus in panel*focusFirstnono
Sidebarcomplementarynocloses if focus in shell*focusFirstnono
Bottomregionnocloses if focus in shell*focusFirstnono

*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.

  • 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-root get inert + aria-hidden while open (reference-counted for nested modals)
  • 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 use setModalSiblingsInert or 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, ContextMenu ariaLabel / open-change
  • Escape + focus restore as in the table above
  • Resize handle is decorative for AT (aria-hidden)
  • Sidebar: role="complementary" + aria-labelledby
  • Bottom: role="region" + aria-labelledby / aria-label; aria-hidden when not visible; expand sets aria-controls to 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: Vue ariaLabel / React ariaLabel, default "Context menu")
  • Open/close notify: Vue @update:open, React onOpenChange(open)
  • Items: role="menuitem", aria-disabled, aria-current when active
  • Wrap list markup as <ul class="context-menu" role="presentation"> so the outer menu role owns the structure
  • Escape closes and restores focus to the opener (restoreFocus)
  • Tab / Shift+Tab cycle role="menuitem" nodes (getMenuItems; falls back to trapTabKey if 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; clearMenuTypeahead on 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)

HelperUse
getFocusableElements / focusFirst / restoreFocusFocus management
trapTabKeyDialog / menu tab cycle
setModalSiblingsInertBackground inert while modal open
getMenuItems / handleMenuKeydown / clearMenuTypeaheadMenu 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-controls id wiring on every trigger (optional future; shells already expose content host ids where useful)