@instructure/ui-a11y-utils 8.12.0 → 8.12.1-snapshot.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +27 -0
- package/es/FocusRegion.js +15 -8
- package/es/FocusRegionManager.js +8 -4
- package/es/FocusRegionOptions.js +1 -0
- package/es/KeyboardFocusRegion.js +28 -35
- package/es/ScreenReaderFocusRegion.js +10 -6
- package/lib/FocusRegion.js +10 -7
- package/lib/FocusRegionManager.js +5 -2
- package/lib/FocusRegionOptions.js +1 -0
- package/lib/KeyboardFocusRegion.js +26 -34
- package/lib/ScreenReaderFocusRegion.js +7 -4
- package/package.json +12 -11
- package/src/FocusRegion.ts +42 -65
- package/src/FocusRegionManager.ts +35 -35
- package/src/FocusRegionOptions.ts +80 -0
- package/src/KeyboardFocusRegion.ts +58 -108
- package/src/ScreenReaderFocusRegion.ts +36 -78
- package/src/hasVisibleChildren.ts +2 -5
- package/src/index.ts +1 -0
- package/src/scopeTab.ts +7 -4
- package/types/FocusRegion.d.ts +22 -15
- package/types/FocusRegion.d.ts.map +1 -1
- package/types/FocusRegionManager.d.ts +23 -14
- package/types/FocusRegionManager.d.ts.map +1 -1
- package/types/FocusRegionOptions.d.ts +56 -0
- package/types/FocusRegionOptions.d.ts.map +1 -0
- package/types/KeyboardFocusRegion.d.ts +24 -22
- package/types/KeyboardFocusRegion.d.ts.map +1 -1
- package/types/ScreenReaderFocusRegion.d.ts +19 -17
- package/types/ScreenReaderFocusRegion.d.ts.map +1 -1
- package/types/hasVisibleChildren.d.ts +2 -1
- package/types/hasVisibleChildren.d.ts.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/scopeTab.d.ts +2 -1
- package/types/scopeTab.d.ts.map +1 -1
|
@@ -41,71 +41,65 @@ import { logError as error } from '@instructure/console'
|
|
|
41
41
|
import keycode from 'keycode'
|
|
42
42
|
|
|
43
43
|
import { scopeTab } from './scopeTab'
|
|
44
|
+
import type { UIElement } from '@instructure/shared-types'
|
|
45
|
+
import { FocusRegionOptions } from './FocusRegionOptions'
|
|
44
46
|
|
|
45
47
|
class KeyboardFocusRegion {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
private readonly _options: FocusRegionOptions
|
|
49
|
+
private _focusLaterElement: Element | null = null
|
|
50
|
+
private _needToFocus = false
|
|
51
|
+
private _listeners: ReturnType<typeof addEventListener>[] = []
|
|
52
|
+
private _raf: RequestAnimationFrameType[] = []
|
|
53
|
+
private _active = false
|
|
54
|
+
private _wasDocumentClick?: boolean
|
|
55
|
+
public _contextElement?: Node
|
|
56
|
+
|
|
57
|
+
constructor(element: UIElement, options: FocusRegionOptions) {
|
|
58
|
+
this._contextElement = findDOMNode(element) as Node
|
|
50
59
|
this._options = options || {
|
|
51
60
|
shouldContainFocus: true,
|
|
52
61
|
shouldReturnFocus: true,
|
|
53
|
-
// @ts-expect-error ts-migrate(6133) FIXME: 'event' is declared but its value is never read.
|
|
54
|
-
onBlur: (event) => {},
|
|
55
|
-
// @ts-expect-error ts-migrate(6133) FIXME: 'event' is declared but its value is never read.
|
|
56
|
-
onDismiss: (event) => {},
|
|
57
62
|
defaultFocusElement: null
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_options' does not exist on type 'Keyboa... Remove this comment to see the full error message
|
|
61
64
|
if (this._options.shouldReturnFocus) {
|
|
62
|
-
this._focusLaterElement = this.
|
|
65
|
+
this._focusLaterElement = getActiveElement(this.doc)
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
public _contextElement: null | undefined | Node | Window = null
|
|
67
|
-
_focusLaterElement: Element | null = null
|
|
68
|
-
_needToFocus = false
|
|
69
|
-
_listeners = []
|
|
70
|
-
_raf: RequestAnimationFrameType[] = []
|
|
71
|
-
_active = false
|
|
72
|
-
|
|
73
69
|
get focused() {
|
|
74
70
|
return containsActiveElement(this._contextElement)
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
get shouldContainFocus() {
|
|
78
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_options' does not exist on type 'Keyboa... Remove this comment to see the full error message
|
|
79
74
|
const { shouldContainFocus } = this._options
|
|
80
75
|
return (
|
|
81
76
|
shouldContainFocus === true ||
|
|
82
77
|
(Array.isArray(shouldContainFocus) &&
|
|
83
|
-
|
|
84
|
-
shouldContainFocus.includes['keyboard'])
|
|
78
|
+
shouldContainFocus.includes('keyboard'))
|
|
85
79
|
)
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
get focusable() {
|
|
82
|
+
get focusable(): Element[] {
|
|
89
83
|
return findFocusable(this._contextElement, () => true, true) || []
|
|
90
84
|
}
|
|
91
85
|
|
|
92
|
-
get tabbable() {
|
|
86
|
+
get tabbable(): Element[] {
|
|
93
87
|
return findTabbable(this._contextElement) || []
|
|
94
88
|
}
|
|
95
89
|
|
|
96
|
-
get firstTabbable() {
|
|
90
|
+
get firstTabbable(): Element | undefined {
|
|
97
91
|
return this.tabbable[0]
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
get lastTabbable() {
|
|
94
|
+
get lastTabbable(): Element | undefined {
|
|
101
95
|
return this.tabbable.pop()
|
|
102
96
|
}
|
|
103
97
|
|
|
104
|
-
get firstFocusable() {
|
|
98
|
+
get firstFocusable(): Element | undefined {
|
|
105
99
|
return this.focusable[0]
|
|
106
100
|
}
|
|
107
101
|
|
|
108
|
-
get lastFocusable() {
|
|
102
|
+
get lastFocusable(): Element | undefined {
|
|
109
103
|
return this.focusable.pop()
|
|
110
104
|
}
|
|
111
105
|
|
|
@@ -117,65 +111,50 @@ class KeyboardFocusRegion {
|
|
|
117
111
|
return ownerWindow(this._contextElement)
|
|
118
112
|
}
|
|
119
113
|
|
|
120
|
-
get
|
|
121
|
-
return getActiveElement(this.doc)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
get defaultFocusElement(): any {
|
|
125
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_options' does not exist on type 'Keyboa... Remove this comment to see the full error message
|
|
114
|
+
get defaultFocusElement() {
|
|
126
115
|
const { defaultFocusElement } = this._options
|
|
127
|
-
|
|
128
|
-
const element = findDOMNode(
|
|
116
|
+
const element: Node | undefined = findDOMNode(
|
|
129
117
|
typeof defaultFocusElement === 'function'
|
|
130
118
|
? defaultFocusElement()
|
|
131
119
|
: defaultFocusElement
|
|
132
|
-
)
|
|
133
|
-
|
|
120
|
+
) as Node
|
|
134
121
|
if (
|
|
135
122
|
element &&
|
|
136
123
|
this._contextElement &&
|
|
137
|
-
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
138
124
|
this._contextElement.contains(element)
|
|
139
125
|
) {
|
|
140
126
|
return element
|
|
141
127
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (firstTabbable) {
|
|
145
|
-
return firstTabbable
|
|
128
|
+
if (this.firstTabbable) {
|
|
129
|
+
return this.firstTabbable
|
|
146
130
|
}
|
|
147
|
-
|
|
148
131
|
if (
|
|
149
132
|
this._contextElement &&
|
|
150
133
|
this.focusable.includes(this._contextElement as Element)
|
|
151
134
|
) {
|
|
152
135
|
return this._contextElement
|
|
153
136
|
}
|
|
154
|
-
|
|
155
137
|
return null
|
|
156
138
|
}
|
|
157
139
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this._contextElement = findDOMNode(element)
|
|
140
|
+
updateElement(element: UIElement) {
|
|
141
|
+
this._contextElement = findDOMNode(element) as Node
|
|
161
142
|
}
|
|
162
143
|
|
|
163
144
|
focusDefaultElement() {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (defaultFocusElement) {
|
|
167
|
-
defaultFocusElement.focus()
|
|
145
|
+
if (this.defaultFocusElement) {
|
|
146
|
+
;(this.defaultFocusElement as HTMLElement).focus()
|
|
168
147
|
} else {
|
|
169
|
-
if (shouldContainFocus) {
|
|
148
|
+
if (this.shouldContainFocus) {
|
|
170
149
|
// Blur the active element to place focus on the document body
|
|
171
|
-
|
|
172
|
-
|
|
150
|
+
getActiveElement(this.doc) &&
|
|
151
|
+
(getActiveElement(this.doc) as HTMLElement).blur()
|
|
173
152
|
error(
|
|
174
153
|
true,
|
|
175
154
|
`
|
|
176
|
-
[KeyboardFocusRegion] No \`defaultFocusElement\` was provided and
|
|
177
|
-
was set to \`true\`. Focus has
|
|
178
|
-
|
|
155
|
+
[KeyboardFocusRegion] No \`defaultFocusElement\` was provided and
|
|
156
|
+
\`shouldContainFocus\` was set to \`true\` or \`keyboard\`. Focus has
|
|
157
|
+
been moved to the document body instead.`
|
|
179
158
|
)
|
|
180
159
|
}
|
|
181
160
|
}
|
|
@@ -194,12 +173,10 @@ class KeyboardFocusRegion {
|
|
|
194
173
|
}
|
|
195
174
|
|
|
196
175
|
blur() {
|
|
197
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_options' does not exist on type 'Keyboa... Remove this comment to see the full error message
|
|
198
176
|
if (this._options.shouldReturnFocus && this._focusLaterElement) {
|
|
199
177
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
} catch (e: any) {
|
|
178
|
+
;(this._focusLaterElement as HTMLElement).focus()
|
|
179
|
+
} catch (e: unknown) {
|
|
203
180
|
error(
|
|
204
181
|
false,
|
|
205
182
|
`
|
|
@@ -212,46 +189,31 @@ class KeyboardFocusRegion {
|
|
|
212
189
|
}
|
|
213
190
|
}
|
|
214
191
|
|
|
215
|
-
|
|
216
|
-
handleDismiss = (event) => {
|
|
217
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_options' does not exist on type 'Keyboa... Remove this comment to see the full error message
|
|
218
|
-
this._options.onDismiss(event)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'event' implicitly has an 'any' type.
|
|
222
|
-
handleKeyDown = (event) => {
|
|
192
|
+
handleKeyDown = (event: KeyboardEvent) => {
|
|
223
193
|
if (event.keyCode === keycode.codes.tab) {
|
|
224
|
-
// @ts-expect-error ts-migrate(2554) FIXME: Expected 3 arguments, but got 2.
|
|
225
194
|
scopeTab(this._contextElement, event)
|
|
226
195
|
}
|
|
227
196
|
}
|
|
228
197
|
|
|
229
|
-
|
|
230
|
-
handleClick = (event) => {
|
|
231
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_wasDocumentClick' does not exist on typ... Remove this comment to see the full error message
|
|
198
|
+
handleClick = () => {
|
|
232
199
|
this._wasDocumentClick = true
|
|
233
200
|
}
|
|
234
201
|
|
|
235
|
-
|
|
236
|
-
handleWindowBlur = (event) => {
|
|
237
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_wasDocumentClick' does not exist on typ... Remove this comment to see the full error message
|
|
202
|
+
handleWindowBlur = () => {
|
|
238
203
|
if (this._wasDocumentClick) {
|
|
239
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_wasDocumentClick' does not exist on typ... Remove this comment to see the full error message
|
|
240
204
|
this._wasDocumentClick = false
|
|
241
205
|
return
|
|
242
206
|
}
|
|
243
207
|
this._needToFocus = true
|
|
244
208
|
}
|
|
245
209
|
|
|
246
|
-
|
|
247
|
-
handleFocus = (event) => {
|
|
210
|
+
handleFocus = () => {
|
|
248
211
|
if (this._needToFocus) {
|
|
249
212
|
this._needToFocus = false
|
|
250
213
|
|
|
251
214
|
if (!this._contextElement) {
|
|
252
215
|
return
|
|
253
216
|
}
|
|
254
|
-
|
|
255
217
|
// need to see how jQuery shims document.on('focusin') so we don't need the
|
|
256
218
|
// setTimeout, firefox doesn't support focusin, if it did, we could focus
|
|
257
219
|
// the element outside of a setTimeout. Side-effect of this implementation
|
|
@@ -268,62 +230,55 @@ class KeyboardFocusRegion {
|
|
|
268
230
|
}
|
|
269
231
|
}
|
|
270
232
|
|
|
271
|
-
|
|
272
|
-
handleFirstTabbableKeyDown = (event) => {
|
|
233
|
+
handleFirstTabbableKeyDown = (event: KeyboardEvent) => {
|
|
273
234
|
if (event.keyCode === keycode.codes.tab && event.shiftKey) {
|
|
274
|
-
|
|
275
|
-
this._options.onBlur(event)
|
|
235
|
+
this._options.onBlur?.(event)
|
|
276
236
|
}
|
|
277
237
|
}
|
|
278
238
|
|
|
279
|
-
|
|
280
|
-
handleLastTabbableKeyDown = (event) => {
|
|
239
|
+
handleLastTabbableKeyDown = (event: KeyboardEvent) => {
|
|
281
240
|
if (event.keyCode === keycode.codes.tab && !event.shiftKey) {
|
|
282
|
-
|
|
283
|
-
this._options.onBlur(event)
|
|
241
|
+
this._options.onBlur?.(event)
|
|
284
242
|
}
|
|
285
243
|
}
|
|
286
244
|
|
|
287
245
|
activate() {
|
|
288
246
|
const { defaultFocusElement, shouldContainFocus } = this
|
|
289
|
-
|
|
290
247
|
if (!this._active) {
|
|
291
248
|
if (defaultFocusElement || shouldContainFocus) {
|
|
292
249
|
if (shouldContainFocus) {
|
|
293
250
|
this._listeners.push(
|
|
294
|
-
|
|
295
|
-
|
|
251
|
+
addEventListener(
|
|
252
|
+
this.doc,
|
|
253
|
+
'keydown',
|
|
254
|
+
this.handleKeyDown as EventListener
|
|
255
|
+
)
|
|
296
256
|
)
|
|
297
257
|
} else {
|
|
298
258
|
this._listeners.push(
|
|
299
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ remove(): void; }' is not assi... Remove this comment to see the full error message
|
|
300
259
|
addEventListener(
|
|
301
|
-
this.firstTabbable || defaultFocusElement
|
|
260
|
+
this.firstTabbable || defaultFocusElement!,
|
|
302
261
|
'keydown',
|
|
303
|
-
this.handleFirstTabbableKeyDown
|
|
262
|
+
this.handleFirstTabbableKeyDown as EventListener
|
|
304
263
|
)
|
|
305
264
|
)
|
|
306
265
|
this._listeners.push(
|
|
307
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ remove(): void; }' is not assi... Remove this comment to see the full error message
|
|
308
266
|
addEventListener(
|
|
309
|
-
this.lastTabbable || defaultFocusElement
|
|
267
|
+
this.lastTabbable || defaultFocusElement!,
|
|
310
268
|
'keydown',
|
|
311
|
-
this.handleLastTabbableKeyDown
|
|
269
|
+
this.handleLastTabbableKeyDown as EventListener
|
|
312
270
|
)
|
|
313
271
|
)
|
|
314
272
|
}
|
|
315
273
|
|
|
316
274
|
this._listeners.push(
|
|
317
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ remove(): void; }' is not assi... Remove this comment to see the full error message
|
|
318
275
|
addEventListener(this.doc, 'click', this.handleClick, true)
|
|
319
276
|
)
|
|
320
277
|
|
|
321
278
|
this._listeners.push(
|
|
322
|
-
|
|
323
|
-
addEventListener(this.win, 'blur', this.handleWindowBlur, false)
|
|
279
|
+
addEventListener(this.win!, 'blur', this.handleWindowBlur, false)
|
|
324
280
|
)
|
|
325
281
|
this._listeners.push(
|
|
326
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ remove(): void; }' is not assi... Remove this comment to see the full error message
|
|
327
282
|
addEventListener(this.doc, 'focus', this.handleFocus, true)
|
|
328
283
|
)
|
|
329
284
|
|
|
@@ -335,17 +290,11 @@ class KeyboardFocusRegion {
|
|
|
335
290
|
deactivate() {
|
|
336
291
|
if (this._active) {
|
|
337
292
|
this._listeners.forEach((listener) => {
|
|
338
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property 'remove' does not exist on type 'never'.
|
|
339
293
|
listener.remove()
|
|
340
294
|
})
|
|
341
295
|
this._listeners = []
|
|
342
|
-
|
|
343
296
|
this._raf.forEach((request) => request.cancel())
|
|
344
297
|
this._raf = []
|
|
345
|
-
|
|
346
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_preventCloseOnDocumentClick' does not e... Remove this comment to see the full error message
|
|
347
|
-
this._preventCloseOnDocumentClick = false
|
|
348
|
-
|
|
349
298
|
this._active = false
|
|
350
299
|
}
|
|
351
300
|
}
|
|
@@ -357,11 +306,12 @@ export {
|
|
|
357
306
|
* ---
|
|
358
307
|
* category: utilities/a11y
|
|
359
308
|
* ---
|
|
360
|
-
*
|
|
309
|
+
*
|
|
361
310
|
* Class for focus operations.
|
|
362
311
|
* - Scoping focus within a given context (DOM node),
|
|
363
312
|
* - Mark active element for focus later
|
|
364
313
|
* - Return focus to the marked element
|
|
314
|
+
* @module KeyboardFocusRegion
|
|
365
315
|
*/
|
|
366
316
|
KeyboardFocusRegion
|
|
367
317
|
}
|
|
@@ -23,42 +23,38 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { logWarn as warn } from '@instructure/console'
|
|
26
|
+
import { FocusRegionOptions } from './FocusRegionOptions'
|
|
26
27
|
|
|
27
28
|
class ScreenReaderFocusRegion {
|
|
29
|
+
private _parents: HTMLElement[] = []
|
|
30
|
+
private _nodes: Element[] = []
|
|
31
|
+
private _liveRegion: (Element | undefined)[]
|
|
32
|
+
private _contextElement?: Element | Node
|
|
33
|
+
private _options: FocusRegionOptions
|
|
34
|
+
private _observer: MutationObserver | null = null
|
|
35
|
+
private _attributes: [Element, string, string][] = []
|
|
36
|
+
|
|
28
37
|
constructor(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
options = {
|
|
38
|
+
element: Element | Node,
|
|
39
|
+
options: FocusRegionOptions = {
|
|
32
40
|
shouldContainFocus: true,
|
|
33
41
|
liveRegion: []
|
|
34
42
|
}
|
|
35
43
|
) {
|
|
36
44
|
const liveRegion =
|
|
37
45
|
typeof options.liveRegion === 'function'
|
|
38
|
-
?
|
|
39
|
-
options.liveRegion()
|
|
46
|
+
? options.liveRegion()
|
|
40
47
|
: options.liveRegion
|
|
41
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_liveRegion' does not exist on type 'Scr... Remove this comment to see the full error message
|
|
42
48
|
this._liveRegion = Array.isArray(liveRegion) ? liveRegion : [liveRegion]
|
|
43
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_contextElement' does not exist on type ... Remove this comment to see the full error message
|
|
44
49
|
this._contextElement = element
|
|
45
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_options' does not exist on type 'Screen... Remove this comment to see the full error message
|
|
46
50
|
this._options = options
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
_attributes = []
|
|
51
|
-
_nodes = []
|
|
52
|
-
_parents = []
|
|
53
|
-
|
|
54
|
-
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'element' implicitly has an 'any' type.
|
|
55
|
-
updateElement(element) {
|
|
56
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_contextElement' does not exist on type ... Remove this comment to see the full error message
|
|
53
|
+
updateElement(element: Element | Node) {
|
|
57
54
|
this._contextElement = element
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
muteNode(node) {
|
|
57
|
+
muteNode(node: Element) {
|
|
62
58
|
if (node && node.tagName.toLowerCase() !== 'script') {
|
|
63
59
|
// When we are trapping screen reader focus on an element that
|
|
64
60
|
// is deep inside the DOM, we can't apply aria-hidden to the
|
|
@@ -74,20 +70,15 @@ class ScreenReaderFocusRegion {
|
|
|
74
70
|
const value = node.getAttribute(attribute)
|
|
75
71
|
|
|
76
72
|
if (value !== null) {
|
|
77
|
-
// @ts-expect-error ts-migrate(2322) FIXME: Type 'any' is not assignable to type 'never'.
|
|
78
73
|
this._attributes.push([node, attribute, value])
|
|
79
74
|
node.removeAttribute(attribute)
|
|
80
75
|
}
|
|
81
76
|
})
|
|
82
|
-
|
|
83
|
-
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
84
|
-
this._observer.observe(node, { childList: true })
|
|
77
|
+
this._observer!.observe(node, { childList: true })
|
|
85
78
|
}
|
|
86
79
|
}
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
hideNodes(nodes) {
|
|
90
|
-
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'node' implicitly has an 'any' type.
|
|
81
|
+
hideNodes(nodes: Element[]) {
|
|
91
82
|
nodes.forEach((node) => {
|
|
92
83
|
const ariaLive =
|
|
93
84
|
typeof node.getAttribute === 'function' &&
|
|
@@ -98,21 +89,16 @@ class ScreenReaderFocusRegion {
|
|
|
98
89
|
node.tagName.toLowerCase() !== 'script' &&
|
|
99
90
|
ariaLive !== 'assertive' &&
|
|
100
91
|
ariaLive !== 'polite' &&
|
|
101
|
-
|
|
102
|
-
this._parents.indexOf(node) === -1 &&
|
|
103
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
|
92
|
+
this._parents.indexOf(node as HTMLElement) === -1 &&
|
|
104
93
|
this._nodes.indexOf(node) === -1 &&
|
|
105
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_liveRegion' does not exist on type 'Scr... Remove this comment to see the full error message
|
|
106
94
|
this._liveRegion.indexOf(node) === -1 &&
|
|
107
|
-
|
|
108
|
-
!this._contextElement.contains(node)
|
|
95
|
+
!this._contextElement!.contains(node)
|
|
109
96
|
) {
|
|
110
97
|
if (node.tagName.toLowerCase() !== 'iframe') {
|
|
111
98
|
this.hideNode(node)
|
|
112
99
|
}
|
|
113
100
|
|
|
114
101
|
const iframeBodies = this.parseIframeBodies(node)
|
|
115
|
-
|
|
116
102
|
iframeBodies.forEach((iframeBody) => {
|
|
117
103
|
this.hideNode(iframeBody)
|
|
118
104
|
})
|
|
@@ -120,32 +106,26 @@ class ScreenReaderFocusRegion {
|
|
|
120
106
|
})
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
hideNode(node) {
|
|
109
|
+
hideNode(node: Element) {
|
|
125
110
|
if (node.getAttribute('aria-hidden') !== 'true') {
|
|
126
111
|
node.setAttribute('aria-hidden', 'true')
|
|
127
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
|
128
112
|
this._nodes.push(node)
|
|
129
113
|
}
|
|
130
114
|
}
|
|
131
115
|
|
|
132
|
-
|
|
133
|
-
handleDOMMutation = (records) => {
|
|
134
|
-
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'record' implicitly has an 'any' type.
|
|
116
|
+
handleDOMMutation = (records: MutationRecord[]) => {
|
|
135
117
|
records.forEach((record) => {
|
|
136
|
-
const addedNodes = Array.from(record.addedNodes)
|
|
137
|
-
const removedNodes = Array.from(record.removedNodes)
|
|
118
|
+
const addedNodes = Array.from(record.addedNodes) as Element[]
|
|
119
|
+
const removedNodes = Array.from(record.removedNodes) as Element[]
|
|
138
120
|
|
|
139
121
|
this.hideNodes(addedNodes)
|
|
140
122
|
|
|
141
123
|
removedNodes.forEach((removedNode) => {
|
|
142
124
|
// Node has been removed from the DOM, make sure it is
|
|
143
125
|
// removed from our list of hidden nodes as well
|
|
144
|
-
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
|
|
145
126
|
if (removedNode.tagName.toLowerCase() !== 'iframe') {
|
|
146
127
|
this.restoreNode(removedNode)
|
|
147
128
|
}
|
|
148
|
-
|
|
149
129
|
const iframeBodies = this.parseIframeBodies(removedNode)
|
|
150
130
|
iframeBodies.forEach((iframeBody) => {
|
|
151
131
|
this.restoreNode(iframeBody)
|
|
@@ -154,9 +134,7 @@ class ScreenReaderFocusRegion {
|
|
|
154
134
|
})
|
|
155
135
|
}
|
|
156
136
|
|
|
157
|
-
|
|
158
|
-
restoreNode(removedNode) {
|
|
159
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
|
137
|
+
restoreNode(removedNode: Element) {
|
|
160
138
|
const index = this._nodes.indexOf(removedNode)
|
|
161
139
|
|
|
162
140
|
if (index >= 0) {
|
|
@@ -165,26 +143,22 @@ class ScreenReaderFocusRegion {
|
|
|
165
143
|
}
|
|
166
144
|
}
|
|
167
145
|
|
|
168
|
-
|
|
169
|
-
parseIframeBodies(node) {
|
|
146
|
+
parseIframeBodies(node: Element): HTMLElement[] {
|
|
170
147
|
if (!node) return []
|
|
171
|
-
|
|
172
|
-
let iframes = []
|
|
173
|
-
|
|
148
|
+
let iframes: HTMLIFrameElement[] = []
|
|
174
149
|
if (node.tagName.toLowerCase() === 'iframe') {
|
|
175
|
-
iframes.push(node)
|
|
150
|
+
iframes.push(node as HTMLIFrameElement)
|
|
176
151
|
} else {
|
|
177
152
|
if (node.getElementsByTagName) {
|
|
178
153
|
iframes = Array.from(node.getElementsByTagName('iframe'))
|
|
179
154
|
}
|
|
180
155
|
}
|
|
181
|
-
|
|
182
156
|
return iframes
|
|
183
157
|
.map((iframe) => {
|
|
184
158
|
let body = null
|
|
185
159
|
try {
|
|
186
|
-
body = iframe.contentDocument
|
|
187
|
-
} catch (e:
|
|
160
|
+
body = iframe.contentDocument!.body
|
|
161
|
+
} catch (e: unknown) {
|
|
188
162
|
warn(
|
|
189
163
|
false,
|
|
190
164
|
`[ui-a11y] could not find a document for iframe: ${e} ${iframe}`
|
|
@@ -192,60 +166,43 @@ class ScreenReaderFocusRegion {
|
|
|
192
166
|
}
|
|
193
167
|
return body
|
|
194
168
|
})
|
|
195
|
-
.filter((body) => body !== null)
|
|
169
|
+
.filter((body) => body !== null) as HTMLElement[]
|
|
196
170
|
}
|
|
197
171
|
|
|
198
172
|
activate() {
|
|
199
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_options' does not exist on type 'Screen... Remove this comment to see the full error message
|
|
200
173
|
if (!this._options.shouldContainFocus) {
|
|
201
174
|
return
|
|
202
175
|
}
|
|
203
|
-
|
|
204
|
-
// @ts-expect-error ts-migrate(2322) FIXME: Type 'MutationObserver' is not assignable to type ... Remove this comment to see the full error message
|
|
205
176
|
this._observer = new MutationObserver(this.handleDOMMutation)
|
|
206
|
-
|
|
207
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property '_contextElement' does not exist on type ... Remove this comment to see the full error message
|
|
208
177
|
let node = this._contextElement
|
|
209
|
-
|
|
210
178
|
while (
|
|
211
179
|
node &&
|
|
212
|
-
node.nodeType ===
|
|
213
|
-
node.tagName.toLowerCase() !== 'body'
|
|
180
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
181
|
+
(node as Element).tagName.toLowerCase() !== 'body'
|
|
214
182
|
) {
|
|
215
|
-
const parent = node.parentElement
|
|
216
|
-
|
|
183
|
+
const parent = node.parentElement
|
|
217
184
|
if (parent) {
|
|
218
|
-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'any' is not assignable to parame... Remove this comment to see the full error message
|
|
219
185
|
this._parents.push(parent)
|
|
220
|
-
|
|
221
186
|
this.muteNode(parent)
|
|
222
|
-
|
|
223
187
|
this.hideNodes(Array.prototype.slice.call(parent.childNodes))
|
|
224
188
|
}
|
|
225
|
-
|
|
226
|
-
node = node.parentNode // should never be null, will default to doc element
|
|
189
|
+
node = node.parentNode as Element // should never be null, will default to doc element
|
|
227
190
|
}
|
|
228
191
|
}
|
|
229
192
|
|
|
230
193
|
deactivate() {
|
|
231
194
|
if (this._observer) {
|
|
232
|
-
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
|
|
233
195
|
this._observer.disconnect()
|
|
234
196
|
this._observer = null
|
|
235
197
|
}
|
|
236
|
-
|
|
237
198
|
this._nodes.forEach((node) => {
|
|
238
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property 'removeAttribute' does not exist on type ... Remove this comment to see the full error message
|
|
239
199
|
node.removeAttribute('aria-hidden')
|
|
240
200
|
})
|
|
241
201
|
this._nodes = []
|
|
242
|
-
|
|
243
202
|
this._attributes.forEach((attribute) => {
|
|
244
|
-
// @ts-expect-error ts-migrate(2339) FIXME: Property 'setAttribute' does not exist on type 'ne... Remove this comment to see the full error message
|
|
245
203
|
attribute[0].setAttribute(attribute[1], attribute[2] || '')
|
|
246
204
|
})
|
|
247
205
|
this._attributes = []
|
|
248
|
-
|
|
249
206
|
this._parents = []
|
|
250
207
|
}
|
|
251
208
|
}
|
|
@@ -256,10 +213,11 @@ export {
|
|
|
256
213
|
* ---
|
|
257
214
|
* category: utilities/a11y
|
|
258
215
|
* ---
|
|
259
|
-
*
|
|
216
|
+
*
|
|
260
217
|
* Utility that hides all DOM elements outside of a specified node. Used,
|
|
261
218
|
* for example, in overlay components where we want to restrict the screen
|
|
262
|
-
* readers to the overlay content
|
|
219
|
+
* readers to the overlay content.
|
|
220
|
+
* @module ScreenReaderFocusRegion
|
|
263
221
|
*/
|
|
264
222
|
ScreenReaderFocusRegion
|
|
265
223
|
}
|
|
@@ -22,21 +22,18 @@
|
|
|
22
22
|
* SOFTWARE.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import React from 'react'
|
|
25
|
+
import React, { ReactNode } from 'react'
|
|
26
26
|
import { matchComponentTypes } from '@instructure/ui-react-utils'
|
|
27
27
|
|
|
28
28
|
import { ScreenReaderContent } from '@instructure/ui-a11y-content'
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
function hasVisibleChildren(children) {
|
|
30
|
+
function hasVisibleChildren(children: ReactNode) {
|
|
32
31
|
let visible = false
|
|
33
|
-
|
|
34
32
|
React.Children.forEach(children, (child) => {
|
|
35
33
|
if (child && !matchComponentTypes(child, [ScreenReaderContent])) {
|
|
36
34
|
visible = true
|
|
37
35
|
}
|
|
38
36
|
})
|
|
39
|
-
|
|
40
37
|
return visible
|
|
41
38
|
}
|
|
42
39
|
|
package/src/index.ts
CHANGED
|
@@ -28,3 +28,4 @@ export { hasVisibleChildren } from './hasVisibleChildren'
|
|
|
28
28
|
export { KeyboardFocusRegion } from './KeyboardFocusRegion'
|
|
29
29
|
export { scopeTab } from './scopeTab'
|
|
30
30
|
export { ScreenReaderFocusRegion } from './ScreenReaderFocusRegion'
|
|
31
|
+
export type { FocusRegionOptions } from './FocusRegionOptions'
|
package/src/scopeTab.ts
CHANGED
|
@@ -29,9 +29,13 @@ import {
|
|
|
29
29
|
containsActiveElement,
|
|
30
30
|
getActiveElement
|
|
31
31
|
} from '@instructure/ui-dom-utils'
|
|
32
|
+
import type { UIElement } from '@instructure/shared-types'
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
function scopeTab(
|
|
35
|
+
element: UIElement | undefined,
|
|
36
|
+
event: KeyboardEvent,
|
|
37
|
+
onLeavingFinalTabbable?: () => void
|
|
38
|
+
) {
|
|
35
39
|
const node = findDOMNode(element)
|
|
36
40
|
const tabbable = findTabbable(node)
|
|
37
41
|
|
|
@@ -66,8 +70,7 @@ function scopeTab(element, event, onLeavingFinalTabbable) {
|
|
|
66
70
|
|
|
67
71
|
event.preventDefault()
|
|
68
72
|
const target = tabbable[event.shiftKey ? tabbable.length - 1 : 0]
|
|
69
|
-
|
|
70
|
-
target.focus()
|
|
73
|
+
;(target as HTMLElement).focus()
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
export default scopeTab
|