mirror of https://github.com/docusealco/docuseal
- H7: Add user_menu.js custom element for navbar dropdown keyboard navigation
(ArrowDown/Up, Enter/Space, Escape, roving tabindex, aria-expanded) (WCAG 2.1.1)
- H8: Add aria-label + fallback text to signature, initials, and QR canvas elements (WCAG 1.1.1)
- M3: Add role="textbox" + aria-label to field name contenteditable in area.vue (WCAG 4.1.2)
- M4: Add aria-hidden/aria-expanded to toggle_visible.js and field_condition.js (WCAG 4.1.2)
- M5: Add aria-label ("Show/Hide password") + aria-pressed to password_input.js toggle (WCAG 4.1.2)
- M6: Move focus to first input on addItem; announce removal via announcePolite in dynamic_list.js (WCAG 2.4.3)
- M7: Announce "Copied to clipboard" via announcePolite in clipboard_copy.js (WCAG 4.1.3)
- M9: Fix placeholder contrast text-neutral-400 → text-neutral-600 in contenteditable.vue (WCAG 1.4.3)
- M9: Fix placeholder contrast before:text-base-content/30 → /60 in template_builder/area.vue (WCAG 1.4.3)
- M11: Add persistent aria-live="polite" region for QR code appearance in signature_step.vue (WCAG 4.1.3)
- Fix missed alert() in initials_step.vue → initialsError data prop + live region (WCAG 4.1.3)
- Add canvas accessibility i18n keys: signature_drawing_pad, initials_drawing_pad, qr_code_for_mobile_signature
- Add field_name i18n key to template_builder for contenteditable aria-label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pull/599/head
parent
3b9145d9e8
commit
bf4bbedc28
@ -0,0 +1,80 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this._trigger = this.querySelector('[aria-haspopup]')
|
||||||
|
this._menu = this.querySelector('ul')
|
||||||
|
|
||||||
|
if (!this._trigger || !this._menu) return
|
||||||
|
|
||||||
|
this._menu.setAttribute('role', 'menu')
|
||||||
|
this._menu.querySelectorAll('a[href], button').forEach((el) => {
|
||||||
|
el.setAttribute('role', 'menuitem')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.addEventListener('focusin', this._onFocusin)
|
||||||
|
this.addEventListener('focusout', this._onFocusout)
|
||||||
|
this._trigger.addEventListener('keydown', this._onTriggerKeydown)
|
||||||
|
this._menu.addEventListener('keydown', this._onMenuKeydown)
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFocusin = () => {
|
||||||
|
this._trigger.setAttribute('aria-expanded', 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
_onFocusout = (e) => {
|
||||||
|
if (!this.contains(e.relatedTarget)) {
|
||||||
|
this._trigger.setAttribute('aria-expanded', 'false')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onTriggerKeydown = (e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
this._focusItem(0)
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
this._focusItem(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMenuKeydown = (e) => {
|
||||||
|
const items = this._menuItems()
|
||||||
|
const idx = items.indexOf(document.activeElement)
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
items[(idx + 1) % items.length]?.focus()
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
items[(idx - 1 + items.length) % items.length]?.focus()
|
||||||
|
} else if (e.key === 'Home') {
|
||||||
|
e.preventDefault()
|
||||||
|
items[0]?.focus()
|
||||||
|
} else if (e.key === 'End') {
|
||||||
|
e.preventDefault()
|
||||||
|
items[items.length - 1]?.focus()
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
this._closeMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_menuItems () {
|
||||||
|
return Array.from(this._menu.querySelectorAll('a[href], button:not([disabled])'))
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusItem (idx) {
|
||||||
|
const items = this._menuItems()
|
||||||
|
const target = idx >= 0 ? items[idx] : items[items.length + idx]
|
||||||
|
target?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
_closeMenu () {
|
||||||
|
// Force-hide while focusing trigger to prevent CSS :focus-within from re-opening it
|
||||||
|
this._menu.style.setProperty('display', 'none', 'important')
|
||||||
|
this._trigger.setAttribute('aria-expanded', 'false')
|
||||||
|
this._trigger.focus()
|
||||||
|
this._trigger.addEventListener('blur', () => {
|
||||||
|
this._menu.style.removeProperty('display')
|
||||||
|
}, { once: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue