neo.mjs 6.8.0 → 6.8.1
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/apps/ServiceWorker.mjs +2 -2
- package/apps/form/view/pages/Page2.mjs +1 -1
- package/examples/ServiceWorker.mjs +2 -2
- package/examples/dialog/DemoDialog.mjs +1 -0
- package/examples/dialog/MainContainer.mjs +1 -0
- package/package.json +1 -1
- package/resources/scss/src/dialog/Base.scss +12 -0
- package/src/DefaultConfig.mjs +2 -2
- package/src/component/Base.mjs +5 -2
- package/src/dialog/Base.mjs +58 -2
- package/src/dialog/header/Toolbar.mjs +23 -24
- package/src/form/field/Picker.mjs +32 -64
- package/src/main/DomAccess.mjs +102 -2
package/apps/ServiceWorker.mjs
CHANGED
@@ -132,6 +132,7 @@ class DemoDialog extends Dialog {
|
|
132
132
|
index : nextIndex,
|
133
133
|
listeners : {close: me.onWindowClose, scope: me},
|
134
134
|
modal : me.app.mainView.down({valueLabelText: 'Modal'}).checked,
|
135
|
+
trapFocus : true,
|
135
136
|
optionalAnimateTargetId: button.id,
|
136
137
|
style : {left: me.getOffset(), top: me.getOffset()},
|
137
138
|
title : 'Dialog ' + nextIndex
|
@@ -90,6 +90,7 @@ class MainContainer extends Viewport {
|
|
90
90
|
boundaryContainerId : me.boundaryContainerId,
|
91
91
|
listeners : {close: me.onWindowClose, scope: me},
|
92
92
|
modal : me.down({valueLabelText: 'Modal'}).checked,
|
93
|
+
trapFocus : true,
|
93
94
|
optionalAnimateTargetId: data.component.id,
|
94
95
|
title : 'Dialog 1'
|
95
96
|
})
|
package/package.json
CHANGED
@@ -61,6 +61,12 @@
|
|
61
61
|
}
|
62
62
|
}
|
63
63
|
|
64
|
+
&.neo-centered {
|
65
|
+
left : 50%;
|
66
|
+
top : 50%;
|
67
|
+
transform : translate(-50%, -50%);
|
68
|
+
}
|
69
|
+
|
64
70
|
&.neo-panel {
|
65
71
|
.neo-footer-toolbar {
|
66
72
|
border : none;
|
@@ -103,3 +109,9 @@
|
|
103
109
|
}
|
104
110
|
}
|
105
111
|
}
|
112
|
+
|
113
|
+
// A focusable, but zero-sized element used to grab and redirect focus in focus-trapped modals
|
114
|
+
.neo-focus-trap {
|
115
|
+
position : absolute;
|
116
|
+
clip : rect(0, 0, 0, 0);
|
117
|
+
}
|
package/src/DefaultConfig.mjs
CHANGED
@@ -236,12 +236,12 @@ const DefaultConfig = {
|
|
236
236
|
useVdomWorker: true,
|
237
237
|
/**
|
238
238
|
* buildScripts/injectPackageVersion.mjs will update this value
|
239
|
-
* @default '6.8.
|
239
|
+
* @default '6.8.1'
|
240
240
|
* @memberOf! module:Neo
|
241
241
|
* @name config.version
|
242
242
|
* @type String
|
243
243
|
*/
|
244
|
-
version: '6.8.
|
244
|
+
version: '6.8.1'
|
245
245
|
};
|
246
246
|
|
247
247
|
Object.assign(DefaultConfig, {
|
package/src/component/Base.mjs
CHANGED
@@ -610,13 +610,16 @@ class Base extends CoreBase {
|
|
610
610
|
* @protected
|
611
611
|
*/
|
612
612
|
afterSetHidden(value, oldValue) {
|
613
|
-
let me
|
613
|
+
let me = this,
|
614
|
+
state = value ? 'hide' : 'show';
|
614
615
|
|
615
616
|
if (value && oldValue === undefined && me.hideMode === 'removeDom') {
|
616
617
|
me.vdom.removeDom = true
|
617
618
|
} else if (value || oldValue !== undefined) {
|
618
|
-
me[
|
619
|
+
me[state]()
|
619
620
|
}
|
621
|
+
|
622
|
+
me.fire(state, {id: me.id})
|
620
623
|
}
|
621
624
|
|
622
625
|
/**
|
package/src/dialog/Base.mjs
CHANGED
@@ -127,7 +127,20 @@ class Base extends Panel {
|
|
127
127
|
/**
|
128
128
|
* @member {String|null} title_=null
|
129
129
|
*/
|
130
|
-
title_: null
|
130
|
+
title_: null,
|
131
|
+
/**
|
132
|
+
* Set to `true` to have tabbing wrap within this Dialog.
|
133
|
+
*
|
134
|
+
* Should be used with `modal`.
|
135
|
+
* @member {Boolean} trapFocus_=false
|
136
|
+
*/
|
137
|
+
trapFocus_: false,
|
138
|
+
/**
|
139
|
+
* Set to `true` to have this Dialog centered in the viewport.
|
140
|
+
*
|
141
|
+
* @member {Boolean} centered_=false
|
142
|
+
*/
|
143
|
+
centered_: false
|
131
144
|
}
|
132
145
|
|
133
146
|
/**
|
@@ -141,7 +154,7 @@ class Base extends Panel {
|
|
141
154
|
|
142
155
|
me.createHeader();
|
143
156
|
|
144
|
-
if (!me.animateTargetId) {
|
157
|
+
if (!me.animateTargetId && !me.centered) {
|
145
158
|
Neo.assignDefaults(style, {
|
146
159
|
left : '50%',
|
147
160
|
top : '50%',
|
@@ -185,6 +198,17 @@ class Base extends Panel {
|
|
185
198
|
super.afterSetAppName(value, oldValue)
|
186
199
|
}
|
187
200
|
|
201
|
+
/**
|
202
|
+
* Triggered after the centered config got changed
|
203
|
+
* @param {Boolean} value
|
204
|
+
* @param {Boolean} oldValue
|
205
|
+
* @protected
|
206
|
+
*/
|
207
|
+
afterSetCentered(value, oldValue) {
|
208
|
+
NeoArray.toggle(this.vdom.cls, 'neo-centered', value);
|
209
|
+
this.update();
|
210
|
+
}
|
211
|
+
|
188
212
|
/**
|
189
213
|
* Triggered after the draggable config got changed
|
190
214
|
* @param {Boolean} value
|
@@ -252,6 +276,19 @@ class Base extends Panel {
|
|
252
276
|
me.rendered && me.syncModalMask()
|
253
277
|
}
|
254
278
|
|
279
|
+
/**
|
280
|
+
* Triggered after the mounted config got changed
|
281
|
+
* @param {Boolean} value
|
282
|
+
* @param {Boolean} oldValue
|
283
|
+
* @protected
|
284
|
+
*/
|
285
|
+
afterSetMounted(value, oldValue) {
|
286
|
+
super.afterSetMounted(value, oldValue);
|
287
|
+
|
288
|
+
// Ensure focus trapping is up-to-date, enabled or disabled.
|
289
|
+
this.syncTrapFocus()
|
290
|
+
}
|
291
|
+
|
255
292
|
/**
|
256
293
|
* Triggered after the resizable config got changed
|
257
294
|
* @param {Boolean} value
|
@@ -289,6 +326,16 @@ class Base extends Panel {
|
|
289
326
|
}
|
290
327
|
}
|
291
328
|
|
329
|
+
/**
|
330
|
+
* Triggered after the trapFocus config got changed
|
331
|
+
* @param {Boolean} value
|
332
|
+
* @param {Boolean} oldValue
|
333
|
+
* @protected
|
334
|
+
*/
|
335
|
+
afterSetTrapFocus(value, oldValue) {
|
336
|
+
this.syncTrapFocus()
|
337
|
+
}
|
338
|
+
|
292
339
|
/**
|
293
340
|
*
|
294
341
|
*/
|
@@ -676,6 +723,15 @@ class Base extends Panel {
|
|
676
723
|
// This should sync the visibility and position of the modal mask element.
|
677
724
|
Neo.main.DomAccess.syncModalMask({ id, modal: this.modal })
|
678
725
|
}
|
726
|
+
|
727
|
+
/**
|
728
|
+
*
|
729
|
+
*/
|
730
|
+
syncTrapFocus() {
|
731
|
+
if (this.mounted) {
|
732
|
+
Neo.main.DomAccess.trapFocus({ id: this.id, trap: this.trapFocus })
|
733
|
+
}
|
734
|
+
}
|
679
735
|
}
|
680
736
|
|
681
737
|
Neo.applyClassConfig(Base);
|
@@ -11,35 +11,34 @@ class Toolbar extends Base {
|
|
11
11
|
* @protected
|
12
12
|
*/
|
13
13
|
className: 'Neo.dialog.header.Toolbar',
|
14
|
+
/**
|
15
|
+
* @member {Object} actionMap
|
16
|
+
*/
|
17
|
+
actionMap: {
|
18
|
+
close : () => ({action: 'close', iconCls: 'far fa-window-close'}),
|
19
|
+
maximize: () => ({action: 'maximize', iconCls: 'far fa-window-maximize'})
|
20
|
+
},
|
21
|
+
/**
|
22
|
+
* You can define the action order and directly add custom actions.
|
23
|
+
* @example
|
24
|
+
* {
|
25
|
+
* actions: [
|
26
|
+
* 'close',
|
27
|
+
* 'maximize',
|
28
|
+
* {action: 'help', iconCls: 'far fa-circle-question'}
|
29
|
+
* ]
|
30
|
+
* }
|
31
|
+
*
|
32
|
+
* You can also extend the actionMap if needed.
|
33
|
+
* @member {Object[]|String[]|null} actions=['maximize','close']
|
34
|
+
*/
|
35
|
+
actions: ['maximize', 'close'],
|
14
36
|
/**
|
15
37
|
* @member {String|null} title=null
|
16
38
|
*/
|
17
39
|
title_: null
|
18
40
|
}
|
19
41
|
|
20
|
-
/**
|
21
|
-
* @member {Object} actionMap
|
22
|
-
*/
|
23
|
-
actionMap = {
|
24
|
-
close : () => ({action: 'close', iconCls: 'far fa-window-close'}),
|
25
|
-
maximize: () => ({action: 'maximize', iconCls: 'far fa-window-maximize'})
|
26
|
-
}
|
27
|
-
/**
|
28
|
-
* You can define the action order and directly add custom actions.
|
29
|
-
* @example
|
30
|
-
* {
|
31
|
-
* actions: [
|
32
|
-
* 'close',
|
33
|
-
* 'maximize',
|
34
|
-
* {action: 'help', iconCls: 'far fa-circle-question'}
|
35
|
-
* ]
|
36
|
-
* }
|
37
|
-
*
|
38
|
-
* You can also extend the actionMap if needed.
|
39
|
-
* @member {Object[]|String[]|null} actions=['maximize','close']
|
40
|
-
*/
|
41
|
-
actions = ['maximize', 'close']
|
42
|
-
|
43
42
|
/**
|
44
43
|
* Triggered after the title config got changed
|
45
44
|
* @param {String} value
|
@@ -83,7 +82,7 @@ class Toolbar extends Base {
|
|
83
82
|
|
84
83
|
me.items = items;
|
85
84
|
|
86
|
-
super.createItems()
|
85
|
+
super.createItems()
|
87
86
|
}
|
88
87
|
|
89
88
|
/**
|
@@ -45,7 +45,7 @@ class Picker extends Text {
|
|
45
45
|
Escape: 'onKeyDownEscape'
|
46
46
|
},
|
47
47
|
/**
|
48
|
-
* @member {
|
48
|
+
* @member {Neo.container.Base|null} picker=null
|
49
49
|
* @protected
|
50
50
|
*/
|
51
51
|
picker: null,
|
@@ -107,7 +107,7 @@ class Picker extends Text {
|
|
107
107
|
me.addDomListeners({
|
108
108
|
click: me.onInputClick,
|
109
109
|
scope: me
|
110
|
-
})
|
110
|
+
})
|
111
111
|
}
|
112
112
|
|
113
113
|
/**
|
@@ -120,7 +120,7 @@ class Picker extends Text {
|
|
120
120
|
let cls = this.cls;
|
121
121
|
|
122
122
|
NeoArray.toggle(cls, 'neo-not-editable', !value);
|
123
|
-
this.cls = cls
|
123
|
+
this.cls = cls
|
124
124
|
}
|
125
125
|
|
126
126
|
/**
|
@@ -131,10 +131,10 @@ class Picker extends Text {
|
|
131
131
|
*/
|
132
132
|
afterSetMounted(value, oldValue) {
|
133
133
|
if (value === false && oldValue && this.pickerIsMounted) {
|
134
|
-
this.picker.hide()
|
134
|
+
this.picker.hide()
|
135
135
|
}
|
136
136
|
|
137
|
-
super.afterSetMounted(value, oldValue)
|
137
|
+
super.afterSetMounted(value, oldValue)
|
138
138
|
}
|
139
139
|
|
140
140
|
/**
|
@@ -160,18 +160,19 @@ class Picker extends Text {
|
|
160
160
|
{ pickerWidth } = me,
|
161
161
|
pickerComponent = me.createPickerComponent();
|
162
162
|
|
163
|
-
|
163
|
+
me.picker = Neo.create(Container, {
|
164
164
|
parentId : 'document.body',
|
165
165
|
floating : true,
|
166
166
|
align : {
|
167
167
|
edgeAlign : pickerWidth ? 't0-b0' : 't-b',
|
168
|
-
matchSize : pickerWidth
|
168
|
+
matchSize : !pickerWidth,
|
169
169
|
axisLock : true,
|
170
170
|
target : me.getInputWrapperId()
|
171
171
|
},
|
172
172
|
appName : me.appName,
|
173
173
|
cls : ['neo-picker-container', 'neo-container'],
|
174
174
|
height : me.pickerHeight,
|
175
|
+
hidden : true,
|
175
176
|
id : me.getPickerId(),
|
176
177
|
items : pickerComponent ? [pickerComponent] : [],
|
177
178
|
maxHeight: me.pickerMaxHeight,
|
@@ -188,16 +189,18 @@ class Picker extends Text {
|
|
188
189
|
for (item of data.oldPath) {
|
189
190
|
if (item.id === me.id) {
|
190
191
|
insideField = true;
|
191
|
-
break
|
192
|
+
break
|
192
193
|
}
|
193
194
|
}
|
194
195
|
|
195
196
|
if (!insideField) {
|
196
197
|
me.hidePicker();
|
197
|
-
super.onFocusLeave(data)
|
198
|
+
super.onFocusLeave(data)
|
198
199
|
}
|
199
200
|
}
|
200
201
|
});
|
202
|
+
|
203
|
+
return me.picker
|
201
204
|
}
|
202
205
|
|
203
206
|
/**
|
@@ -205,7 +208,7 @@ class Picker extends Text {
|
|
205
208
|
* @returns {Neo.component.Base|null}
|
206
209
|
*/
|
207
210
|
createPickerComponent() {
|
208
|
-
return null
|
211
|
+
return null
|
209
212
|
}
|
210
213
|
|
211
214
|
/**
|
@@ -214,12 +217,12 @@ class Picker extends Text {
|
|
214
217
|
destroy(...args) {
|
215
218
|
let picker = this.picker;
|
216
219
|
|
217
|
-
if (
|
218
|
-
picker
|
220
|
+
if (picker?.hidden === false) {
|
221
|
+
picker.unmount()
|
219
222
|
}
|
220
223
|
|
221
224
|
picker?.destroy();
|
222
|
-
super.destroy(...args)
|
225
|
+
super.destroy(...args)
|
223
226
|
}
|
224
227
|
|
225
228
|
/**
|
@@ -227,36 +230,22 @@ class Picker extends Text {
|
|
227
230
|
* @returns {Neo.container.Base}
|
228
231
|
*/
|
229
232
|
getPicker() {
|
230
|
-
|
231
|
-
|
232
|
-
if (!me.picker) {
|
233
|
-
me.picker = me.createPicker();
|
234
|
-
}
|
235
|
-
|
236
|
-
return me.picker;
|
233
|
+
return this.picker || this.createPicker()
|
237
234
|
}
|
238
235
|
|
239
236
|
/**
|
240
237
|
* @returns {String}
|
241
238
|
*/
|
242
239
|
getPickerId() {
|
243
|
-
return `${this.id}__picker
|
240
|
+
return `${this.id}__picker`
|
244
241
|
}
|
245
242
|
|
246
243
|
/**
|
247
244
|
*
|
248
245
|
*/
|
249
246
|
async hidePicker() {
|
250
|
-
|
251
|
-
picker =
|
252
|
-
|
253
|
-
// avoid breaking selection model cls updates
|
254
|
-
await me.timeout(30);
|
255
|
-
|
256
|
-
if (me.pickerIsMounted) {
|
257
|
-
picker.unmount();
|
258
|
-
|
259
|
-
me.pickerIsMounted = false;
|
247
|
+
if (this.picker) {
|
248
|
+
this.picker.hidden = true
|
260
249
|
}
|
261
250
|
}
|
262
251
|
|
@@ -269,7 +258,7 @@ class Picker extends Text {
|
|
269
258
|
|
270
259
|
let me = this;
|
271
260
|
|
272
|
-
me.showPickerOnFocus &&
|
261
|
+
me.showPickerOnFocus && me.showPicker()
|
273
262
|
}
|
274
263
|
|
275
264
|
/**
|
@@ -284,13 +273,13 @@ class Picker extends Text {
|
|
284
273
|
for (item of data.oldPath) {
|
285
274
|
if (item.id === me.getPickerId()) {
|
286
275
|
insidePicker = true;
|
287
|
-
break
|
276
|
+
break
|
288
277
|
}
|
289
278
|
}
|
290
279
|
|
291
280
|
if (!insidePicker) {
|
292
281
|
me.hidePicker();
|
293
|
-
super.onFocusLeave(data)
|
282
|
+
super.onFocusLeave(data)
|
294
283
|
}
|
295
284
|
}
|
296
285
|
|
@@ -308,7 +297,7 @@ class Picker extends Text {
|
|
308
297
|
* @protected
|
309
298
|
*/
|
310
299
|
onKeyDownEnter(data, callback, callbackScope) {
|
311
|
-
!this.pickerIsMounted && this.showPicker(callback, callbackScope)
|
300
|
+
!this.pickerIsMounted && this.showPicker(callback, callbackScope)
|
312
301
|
}
|
313
302
|
|
314
303
|
/**
|
@@ -316,7 +305,7 @@ class Picker extends Text {
|
|
316
305
|
* @protected
|
317
306
|
*/
|
318
307
|
onKeyDownEscape(data) {
|
319
|
-
this.pickerIsMounted && this.hidePicker()
|
308
|
+
this.pickerIsMounted && this.hidePicker()
|
320
309
|
}
|
321
310
|
|
322
311
|
/**
|
@@ -324,44 +313,23 @@ class Picker extends Text {
|
|
324
313
|
* @protected
|
325
314
|
*/
|
326
315
|
onPickerTriggerClick() {
|
327
|
-
this.editable && this.togglePicker()
|
316
|
+
this.editable && this.togglePicker()
|
328
317
|
}
|
329
318
|
|
330
319
|
/**
|
331
|
-
*
|
332
|
-
* @param {Object} [callbackScope]
|
320
|
+
*
|
333
321
|
*/
|
334
|
-
showPicker(
|
335
|
-
let
|
336
|
-
|
337
|
-
listenerId;
|
338
|
-
|
339
|
-
if (!me.pickerIsMounting) {
|
340
|
-
me.pickerIsMounting = true;
|
341
|
-
|
342
|
-
listenerId = picker.on('mounted', () => {
|
343
|
-
picker.un('mounted', listenerId);
|
344
|
-
|
345
|
-
me.pickerIsMounting = false;
|
346
|
-
me.pickerIsMounted = true;
|
347
|
-
callback?.apply(callbackScope || me);
|
348
|
-
});
|
349
|
-
|
350
|
-
picker.render(true);
|
351
|
-
}
|
322
|
+
showPicker() {
|
323
|
+
let picker = this.getPicker();
|
324
|
+
picker.hidden = false
|
352
325
|
}
|
353
326
|
|
354
327
|
/**
|
355
328
|
*
|
356
329
|
*/
|
357
330
|
togglePicker() {
|
358
|
-
let
|
359
|
-
|
360
|
-
if (me.pickerIsMounted) {
|
361
|
-
me.hidePicker();
|
362
|
-
} else {
|
363
|
-
me.showPicker();
|
364
|
-
}
|
331
|
+
let picker = this.getPicker();
|
332
|
+
picker.hidden = !picker.hidden
|
365
333
|
}
|
366
334
|
}
|
367
335
|
|
package/src/main/DomAccess.mjs
CHANGED
@@ -4,7 +4,21 @@ import Observable from '../core/Observable.mjs';
|
|
4
4
|
import Rectangle from '../util/Rectangle.mjs';
|
5
5
|
|
6
6
|
const
|
7
|
-
|
7
|
+
doPreventDefault = e => e.preventDefault(),
|
8
|
+
filterTabbable = e => !e.classList.contains('neo-focus-trap') && isTabbable(e) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
|
9
|
+
lengthRE = /^\d+\w+$/,
|
10
|
+
|
11
|
+
focusableTags = {
|
12
|
+
BODY : 1,
|
13
|
+
BUTTON : 1,
|
14
|
+
EMBED : 1,
|
15
|
+
IFRAME : 1,
|
16
|
+
INPUT : 1,
|
17
|
+
OBJECT : 1,
|
18
|
+
SELECT : 1,
|
19
|
+
TEXTAREA : 1
|
20
|
+
},
|
21
|
+
|
8
22
|
fontSizeProps = [
|
9
23
|
'font-family',
|
10
24
|
'font-kerning',
|
@@ -18,7 +32,22 @@ const
|
|
18
32
|
'text-decoration',
|
19
33
|
'text-transform',
|
20
34
|
'word-break'
|
21
|
-
]
|
35
|
+
],
|
36
|
+
|
37
|
+
isTabbable = e => {
|
38
|
+
const
|
39
|
+
{ nodeName } = e,
|
40
|
+
style = getComputedStyle(e);
|
41
|
+
|
42
|
+
if (style.getPropertyValue('display') === 'none' || style.getPropertyValue('visibility') === 'hidden') {
|
43
|
+
return false;
|
44
|
+
}
|
45
|
+
|
46
|
+
return focusableTags[nodeName] ||
|
47
|
+
((nodeName === 'A' || nodeName === 'LINK') && !!e.href) ||
|
48
|
+
e.getAttribute('tabIndex') != null ||
|
49
|
+
e.contentEditable === 'true';
|
50
|
+
};
|
22
51
|
|
23
52
|
/**
|
24
53
|
* @class Neo.main.DomAccess
|
@@ -78,6 +107,7 @@ class DomAccess extends Base {
|
|
78
107
|
'setBodyCls',
|
79
108
|
'setStyle',
|
80
109
|
'syncModalMask',
|
110
|
+
'trapFocus',
|
81
111
|
'windowScrollTo'
|
82
112
|
]
|
83
113
|
},
|
@@ -106,6 +136,7 @@ class DomAccess extends Base {
|
|
106
136
|
if (!me._modalMask) {
|
107
137
|
me._modalMask = document.createElement('div');
|
108
138
|
me._modalMask.className = 'neo-dialog-modal-mask';
|
139
|
+
me._modalMask.addEventListener('mousedown', doPreventDefault, { capture : true });
|
109
140
|
}
|
110
141
|
|
111
142
|
return me._modalMask;
|
@@ -638,6 +669,28 @@ class DomAccess extends Base {
|
|
638
669
|
})
|
639
670
|
}
|
640
671
|
|
672
|
+
/**
|
673
|
+
* @param data
|
674
|
+
* @param data.target
|
675
|
+
* @param data.relatedTarget
|
676
|
+
*/
|
677
|
+
onTrappedFocusMovement({ target, relatedTarget }) {
|
678
|
+
const backwards = relatedTarget && (target.compareDocumentPosition(relatedTarget) & 4);
|
679
|
+
|
680
|
+
if (target.matches('.neo-focus-trap')) {
|
681
|
+
const
|
682
|
+
containingEement = target.parentElement,
|
683
|
+
treeWalker = containingEement.$treeWalker,
|
684
|
+
topFocusTrap = containingEement.$topFocusTrap,
|
685
|
+
bottomFocusTrap = containingEement.$bottomFocusTrap;
|
686
|
+
|
687
|
+
treeWalker.currentNode = backwards ? bottomFocusTrap : topFocusTrap;
|
688
|
+
treeWalker[backwards ? 'previousNode' : 'nextNode']();
|
689
|
+
|
690
|
+
requestAnimationFrame(() => treeWalker.currentNode.focus());
|
691
|
+
}
|
692
|
+
}
|
693
|
+
|
641
694
|
/**
|
642
695
|
* @param {Object} data
|
643
696
|
* @protected
|
@@ -871,6 +924,53 @@ class DomAccess extends Base {
|
|
871
924
|
}
|
872
925
|
}
|
873
926
|
|
927
|
+
/**
|
928
|
+
* Traps (or stops trapping) focus within a Component
|
929
|
+
* @param {Object} data
|
930
|
+
* @param {String} data.id The Component to trap focus within.
|
931
|
+
* @param {Boolean} [data.trap=true] Pass `false` to stop trapping focus inside the Component.
|
932
|
+
*/
|
933
|
+
async trapFocus(data) {
|
934
|
+
const
|
935
|
+
me = this,
|
936
|
+
onTrappedFocusMovement = me.$boundOnTrappedFocusMovement || (me.$boundOnTrappedFocusMovement = me.onTrappedFocusMovement.bind(me)),
|
937
|
+
subject = data.subject = me.getElement(data.id),
|
938
|
+
{ trap = true } = data;
|
939
|
+
|
940
|
+
// Called before DOM has been created.
|
941
|
+
if (!subject) {
|
942
|
+
return;
|
943
|
+
}
|
944
|
+
|
945
|
+
let topFocusTrap = subject.$topFocusTrap,
|
946
|
+
bottomFocusTrap = subject.$bottomFocusTrap;
|
947
|
+
|
948
|
+
if (trap) {
|
949
|
+
if (!subject.$treeWalker) {
|
950
|
+
subject.$treeWalker = document.createTreeWalker(subject, NodeFilter.SHOW_ELEMENT, {
|
951
|
+
acceptNode : filterTabbable
|
952
|
+
});
|
953
|
+
topFocusTrap = subject.$topFocusTrap = document.createElement('div');
|
954
|
+
bottomFocusTrap = subject.$bottomFocusTrap = document.createElement('div');
|
955
|
+
|
956
|
+
// The two focus traping elements must be invisble but tabbable.
|
957
|
+
topFocusTrap.className = bottomFocusTrap.className = 'neo-focus-trap';
|
958
|
+
topFocusTrap.setAttribute('tabIndex', 0);
|
959
|
+
bottomFocusTrap.setAttribute('tabIndex', 0);
|
960
|
+
|
961
|
+
// Listen for when they gain focus and wrap focus within the encapsulating element
|
962
|
+
subject.addEventListener('focusin', onTrappedFocusMovement);
|
963
|
+
}
|
964
|
+
|
965
|
+
// Ensure content is encapsulated by the focus trap elements
|
966
|
+
subject.insertBefore(topFocusTrap, subject.firstChild);
|
967
|
+
subject.appendChild(bottomFocusTrap);
|
968
|
+
}
|
969
|
+
else {
|
970
|
+
subject.removeEventListener('focusin', onTrappedFocusMovement);
|
971
|
+
}
|
972
|
+
}
|
973
|
+
|
874
974
|
/**
|
875
975
|
* @param {Object} data
|
876
976
|
* @param {String} [data.behavior='smooth'] // auto or smooth
|