fix: wrong focus/tab order in dialogs

Massimo Melina committed Jul 29, 2023 at 15:44 UTC a7ed62a066642d801dfacaadf8742b7f9acf09d4
1 file changed +28
shared/dialogs.ts
+28
@@ -28,6 +28,32 @@ export const dialogsDefaults: Partial<DialogOptions> = {
28 padding: true,
29 }
30
31 +// focus trapped on current dialog (MUI already does it)
32 +const focusableSelector = ['input:not([type="hidden"])', 'button', 'select', 'textarea', 'a[href]', '[tabindex]'].map(x =>
33 + x + ':not([disabled]):not([tabindex="-1"])').join(',')
34 +window.addEventListener('keydown', ev => {
35 + if (ev.key !== 'Tab') return
36 + const dialogs = document.querySelectorAll('[role=dialog]')
37 + const dialog = dialogs[dialogs.length-1]
38 + if (!dialog) return
39 + const focusable = dialog.querySelectorAll(focusableSelector)
40 + const n = focusable.length
41 + if (!n) return
42 + const [a, b] = ev.shiftKey ? [n-1, 0] : [0, n-1]
43 + if (ev.target !== focusable[b] && isDescendant(document.activeElement, dialog)) return // default behavior
44 + ;(focusable[a] as HTMLElement).focus()
45 + ev.preventDefault()
46 +})
47 +
48 +function isDescendant(child: Node | null, parent: Node) {
49 + while (child) {
50 + if (child === parent)
51 + return true
52 + child = child.parentNode
53 + }
54 + return false
55 +}
56 +
57 export function Dialogs() {
58 const snap = useSnapshot(dialogs)
59 useEffect(() => {
@@ -57,6 +83,8 @@ function Dialog(d:DialogOptions) {
83 },
84 d.noFrame ? h(d.Content || 'div')
85 : h('div', {
86 + role: 'dialog',
87 + 'aria-modal': true,
88 className: 'dialog',
89 style: {
90 ...position(),