a11y: workaround for faulty JAWS on first open dialog
Massimo Melina committed
Mar 31, 2024 at 11:01 UTC
cd0d9308633f413c3ac4bde49dc1e9ad4217948b
2 files changed
+17
-12
frontend/src/dialog.css
+1
@@ -43,6 +43,7 @@
43
}
44
.dialog-title {
45
font-size: 120%;
46
+ font-weight: normal;
47
margin: .3em 0;
48
padding: 0 0.5em; /* avoid reaching margins */
49
min-height: 1.2em;
shared/dialogs.ts
+16
-12
@@ -37,17 +37,22 @@ export const focusableSelector = ['input:not([type="hidden"])', 'button', 'selec
37
x + ':not([disabled]):not([tabindex="-1"])').join(',')
38
window.addEventListener('keydown', ev => {
39
if (ev.key !== 'Tab') return
40
+ if (tabCycle(ev.target, ev.shiftKey))
41
+ ev.preventDefault()
42
+})
43
+
44
+function tabCycle(target: EventTarget | null, invert=false) {
45
const dialogs = document.querySelectorAll('[role$=dialog]')
46
const dialog = dialogs[dialogs.length-1]
47
if (!dialog) return
48
const focusable = dialog.querySelectorAll(focusableSelector)
49
const n = focusable.length
50
if (!n) return
46
- const [a, b] = ev.shiftKey ? [n-1, 0] : [0, n-1]
47
- if (ev.target !== focusable[b] && isDescendant(document.activeElement, dialog)) return // default behavior
51
+ const [a, b] = invert ? [n-1, 0] : [0, n-1]
52
+ if (target !== focusable[b] && isDescendant(document.activeElement, dialog)) return // default behavior
53
;(focusable[a] as HTMLElement).focus()
49
- ev.preventDefault()
50
-})
54
+ return true
55
+}
56
57
function isDescendant(child: Node | null, parent: Node) {
58
while (child) {
@@ -87,12 +92,12 @@ function Dialog(d: DialogOptions) {
92
const ref = useRef<HTMLElement>()
93
const [shiftY, setShiftY] = useState(0)
94
useEffect(()=>{
90
- if (!ref.current) return
91
- ref.current.focus()
92
- if (d.position) {
93
- const rect = ref.current.querySelector('.dialog')!.getBoundingClientRect()
94
- setShiftY(Math.min(0, rect.top, window.innerHeight - rect.bottom))
95
- }
95
+ const el = ref.current?.querySelector('.dialog') as HTMLElement | undefined
96
+ if (!el) return
97
+ tabCycle(el) // focus first thing inside dialog. This makes JAWS behave
98
+ if (!d.position) return
99
+ const rect = el.getBoundingClientRect()
100
+ setShiftY(Math.min(0, rect.top, window.innerHeight - rect.bottom))
101
}, [])
102
d = { closable: true, ...dialogsDefaults, ...d }
103
if (d.Container)
@@ -100,7 +105,6 @@ function Dialog(d: DialogOptions) {
105
return h('div', {
106
ref,
107
className: 'dialog-backdrop '+(d.className||''),
103
- tabIndex: 0,
108
onKeyDown,
109
onClick: (ev: any) => d.closable
110
&& ev.target === ev.currentTarget // this test will tell us if really the backdrop was clicked
@@ -129,7 +133,7 @@ function Dialog(d: DialogOptions) {
133
className: 'dialog-icon dialog-type' + (typeof d.icon === 'string' ? ' dialog-icon-text' : ''),
134
'aria-hidden': true,
135
}, componentOrNode(d.icon)),
132
- h('div', { className: 'dialog-title' }, componentOrNode(d.title)),
136
+ h('h1', { className: 'dialog-title' }, componentOrNode(d.title)),
137
h('div', { className: 'dialog-content' }, h(d.Content || 'div'))
138
)
139
)