neo.mjs 6.8.0 → 6.8.2

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.
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.8.0'
23
+ * @member {String} version='6.8.2'
24
24
  */
25
- version: '6.8.0'
25
+ version: '6.8.2'
26
26
  }
27
27
 
28
28
  /**
@@ -22,7 +22,7 @@ class HistoricalDataTable extends Container {
22
22
  * @member {Object} columnDefaults
23
23
  */
24
24
  columnDefaults: {
25
- align : 'right',
25
+ cellAlign : 'right',
26
26
  defaultSortDirection: 'DESC',
27
27
  renderer : Util.formatNumber
28
28
  },
@@ -30,7 +30,7 @@ class HistoricalDataTable extends Container {
30
30
  * @member {Object[]} columns
31
31
  */
32
32
  columns: [{
33
- align : 'left',
33
+ cellAlign: 'left',
34
34
  dataField: 'date',
35
35
  dock : 'left',
36
36
  text : 'Date',
@@ -28,7 +28,7 @@ class Table extends Container {
28
28
  * @member {Object} columnDefaults
29
29
  */
30
30
  columnDefaults: {
31
- align : 'right',
31
+ cellAlign : 'right',
32
32
  defaultSortDirection: 'DESC',
33
33
  renderer : Util.formatNumber
34
34
  },
@@ -44,7 +44,7 @@ class Table extends Container {
44
44
  renderer : Util.indexRenderer,
45
45
  width : 40
46
46
  }, {
47
- align : 'left',
47
+ cellAlign : 'left',
48
48
  dataField : 'country',
49
49
  defaultSortDirection: 'ASC',
50
50
  dock : 'left',
@@ -19,7 +19,7 @@ class Page2 extends FormPageContainer {
19
19
  module : DateField,
20
20
  labelText: 'Birthday',
21
21
  name : 'birthday',
22
- style : {marginBottom: '800px', marginTop: '800px'}
22
+ style : {marginBottom: '2000px', marginTop: '800px'}
23
23
  }]
24
24
  }
25
25
  }
@@ -22,7 +22,7 @@ class HistoricalDataTable extends Container {
22
22
  * @member {Object} columnDefaults
23
23
  */
24
24
  columnDefaults: {
25
- align : 'right',
25
+ cellAlign : 'right',
26
26
  defaultSortDirection: 'DESC',
27
27
  renderer : Util.formatNumber
28
28
  },
@@ -30,7 +30,7 @@ class HistoricalDataTable extends Container {
30
30
  * @member {Object[]} columns
31
31
  */
32
32
  columns: [{
33
- align : 'left',
33
+ cellAlign: 'left',
34
34
  dataField: 'date',
35
35
  dock : 'left',
36
36
  text : 'Date',
@@ -28,7 +28,7 @@ class Table extends Container {
28
28
  * @member {Object} columnDefaults
29
29
  */
30
30
  columnDefaults: {
31
- align : 'right',
31
+ cellAlign : 'right',
32
32
  defaultSortDirection: 'DESC',
33
33
  renderer : Util.formatNumber
34
34
  },
@@ -44,7 +44,7 @@ class Table extends Container {
44
44
  renderer : Util.indexRenderer,
45
45
  width : 40
46
46
  }, {
47
- align : 'left',
47
+ cellAlign : 'left',
48
48
  dataField : 'country',
49
49
  defaultSortDirection: 'ASC',
50
50
  dock : 'left',
@@ -20,9 +20,9 @@ class ServiceWorker extends ServiceBase {
20
20
  */
21
21
  singleton: true,
22
22
  /**
23
- * @member {String} version='6.8.0'
23
+ * @member {String} version='6.8.2'
24
24
  */
25
- version: '6.8.0'
25
+ version: '6.8.2'
26
26
  }
27
27
 
28
28
  /**
@@ -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
  })
@@ -23,7 +23,7 @@ class GridContainer extends BaseGridContainer {
23
23
  * @member {Object} columnDefaults
24
24
  */
25
25
  columnDefaults: {
26
- align : 'right',
26
+ cellAlign : 'right',
27
27
  defaultSortDirection: 'DESC',
28
28
  renderer : Util.formatNumber,
29
29
  width : 100
@@ -40,7 +40,7 @@ class GridContainer extends BaseGridContainer {
40
40
  renderer: Util.indexRenderer,
41
41
  width : 40
42
42
  }, {
43
- align : 'left',
43
+ cellAlign : 'left',
44
44
  defaultSortDirection: 'ASC',
45
45
  dock : 'left',
46
46
  field : 'country',
@@ -23,7 +23,7 @@ class TableContainer extends BaseTableContainer {
23
23
  * @member {Object} columnDefaults
24
24
  */
25
25
  columnDefaults: {
26
- align : 'right',
26
+ cellAlign : 'right',
27
27
  defaultSortDirection: 'DESC',
28
28
  renderer : Util.formatNumber
29
29
  },
@@ -39,7 +39,7 @@ class TableContainer extends BaseTableContainer {
39
39
  renderer : Util.indexRenderer,
40
40
  width : 40
41
41
  }, {
42
- align : 'left',
42
+ cellAlign : 'left',
43
43
  dataField : 'country',
44
44
  defaultSortDirection: 'ASC',
45
45
  dock : 'left',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "6.8.0",
3
+ "version": "6.8.2",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
@@ -56,7 +56,7 @@
56
56
  "neo-jsdoc": "1.0.1",
57
57
  "neo-jsdoc-x": "1.0.5",
58
58
  "postcss": "^8.4.31",
59
- "sass": "^1.68.0",
59
+ "sass": "^1.69.1",
60
60
  "showdown": "^2.1.0",
61
61
  "webpack": "^5.88.2",
62
62
  "webpack-cli": "^5.1.4",
@@ -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
+ }
@@ -16,10 +16,15 @@
16
16
  }
17
17
  }
18
18
 
19
+ // a hack since FF does no longer allow to hide the calendar icon post v109
19
20
  @-moz-document url-prefix() {
20
21
  .neo-datefield {
22
+ .neo-field-trigger {
23
+ background-color: var(--textfield-input-background-color);
24
+ }
25
+
21
26
  .neo-textfield-input {
22
- clip-path: inset(0 2em 0 0);
27
+ margin-right: -2.2em !important;
23
28
  }
24
29
  }
25
30
  }
@@ -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.0'
239
+ * @default '6.8.2'
240
240
  * @memberOf! module:Neo
241
241
  * @name config.version
242
242
  * @type String
243
243
  */
244
- version: '6.8.0'
244
+ version: '6.8.2'
245
245
  };
246
246
 
247
247
  Object.assign(DefaultConfig, {
@@ -610,13 +610,16 @@ class Base extends CoreBase {
610
610
  * @protected
611
611
  */
612
612
  afterSetHidden(value, oldValue) {
613
- let me = this;
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[value ? 'hide' : 'show']()
619
+ me[state]()
619
620
  }
621
+
622
+ me.fire(state, {id: me.id})
620
623
  }
621
624
 
622
625
  /**
@@ -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 {Object|null} picker=null
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
- return Neo.create(Container, {
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 ? false : true,
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 (this.pickerIsMounted) {
218
- picker?.unmount();
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
- let me = this;
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
- let me = this,
251
- picker = me.getPicker();
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 && !me.pickerIsMounted && me.showPicker();
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
- * @param {Function} [callback]
332
- * @param {Object} [callbackScope]
320
+ *
333
321
  */
334
- showPicker(callback, callbackScope) {
335
- let me = this,
336
- picker = me.getPicker(),
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 me = this;
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
 
@@ -4,7 +4,21 @@ import Observable from '../core/Observable.mjs';
4
4
  import Rectangle from '../util/Rectangle.mjs';
5
5
 
6
6
  const
7
- lengthRE = /^\d+\w+$/,
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,24 @@ 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
+ tabIndex = e.getAttribute('tabIndex');
42
+
43
+ // Hidden elements not tabbable
44
+ if (style.getPropertyValue('display') === 'none' || style.getPropertyValue('visibility') === 'hidden') {
45
+ return false;
46
+ }
47
+
48
+ return focusableTags[nodeName] ||
49
+ ((nodeName === 'A' || nodeName === 'LINK') && !!e.href) ||
50
+ (tabIndex != null && Number(tabIndex) >= 0) ||
51
+ e.contentEditable === 'true';
52
+ };
22
53
 
23
54
  /**
24
55
  * @class Neo.main.DomAccess
@@ -78,6 +109,7 @@ class DomAccess extends Base {
78
109
  'setBodyCls',
79
110
  'setStyle',
80
111
  'syncModalMask',
112
+ 'trapFocus',
81
113
  'windowScrollTo'
82
114
  ]
83
115
  },
@@ -106,6 +138,7 @@ class DomAccess extends Base {
106
138
  if (!me._modalMask) {
107
139
  me._modalMask = document.createElement('div');
108
140
  me._modalMask.className = 'neo-dialog-modal-mask';
141
+ me._modalMask.addEventListener('mousedown', doPreventDefault, { capture : true });
109
142
  }
110
143
 
111
144
  return me._modalMask;
@@ -638,6 +671,28 @@ class DomAccess extends Base {
638
671
  })
639
672
  }
640
673
 
674
+ /**
675
+ * @param data
676
+ * @param data.target
677
+ * @param data.relatedTarget
678
+ */
679
+ onTrappedFocusMovement({ target, relatedTarget }) {
680
+ const backwards = relatedTarget && (target.compareDocumentPosition(relatedTarget) & 4);
681
+
682
+ if (target.matches('.neo-focus-trap')) {
683
+ const
684
+ containingEement = target.parentElement,
685
+ treeWalker = containingEement.$treeWalker,
686
+ topFocusTrap = containingEement.$topFocusTrap,
687
+ bottomFocusTrap = containingEement.$bottomFocusTrap;
688
+
689
+ treeWalker.currentNode = backwards ? bottomFocusTrap : topFocusTrap;
690
+ treeWalker[backwards ? 'previousNode' : 'nextNode']();
691
+
692
+ requestAnimationFrame(() => treeWalker.currentNode.focus());
693
+ }
694
+ }
695
+
641
696
  /**
642
697
  * @param {Object} data
643
698
  * @protected
@@ -871,6 +926,53 @@ class DomAccess extends Base {
871
926
  }
872
927
  }
873
928
 
929
+ /**
930
+ * Traps (or stops trapping) focus within a Component
931
+ * @param {Object} data
932
+ * @param {String} data.id The Component to trap focus within.
933
+ * @param {Boolean} [data.trap=true] Pass `false` to stop trapping focus inside the Component.
934
+ */
935
+ async trapFocus(data) {
936
+ const
937
+ me = this,
938
+ onTrappedFocusMovement = me.$boundOnTrappedFocusMovement || (me.$boundOnTrappedFocusMovement = me.onTrappedFocusMovement.bind(me)),
939
+ subject = data.subject = me.getElement(data.id),
940
+ { trap = true } = data;
941
+
942
+ // Called before DOM has been created.
943
+ if (!subject) {
944
+ return;
945
+ }
946
+
947
+ let topFocusTrap = subject.$topFocusTrap,
948
+ bottomFocusTrap = subject.$bottomFocusTrap;
949
+
950
+ if (trap) {
951
+ if (!subject.$treeWalker) {
952
+ subject.$treeWalker = document.createTreeWalker(subject, NodeFilter.SHOW_ELEMENT, {
953
+ acceptNode : filterTabbable
954
+ });
955
+ topFocusTrap = subject.$topFocusTrap = document.createElement('div');
956
+ bottomFocusTrap = subject.$bottomFocusTrap = document.createElement('div');
957
+
958
+ // The two focus traping elements must be invisble but tabbable.
959
+ topFocusTrap.className = bottomFocusTrap.className = 'neo-focus-trap';
960
+ topFocusTrap.setAttribute('tabIndex', 0);
961
+ bottomFocusTrap.setAttribute('tabIndex', 0);
962
+
963
+ // Listen for when they gain focus and wrap focus within the encapsulating element
964
+ subject.addEventListener('focusin', onTrappedFocusMovement);
965
+ }
966
+
967
+ // Ensure content is encapsulated by the focus trap elements
968
+ subject.insertBefore(topFocusTrap, subject.firstChild);
969
+ subject.appendChild(bottomFocusTrap);
970
+ }
971
+ else {
972
+ subject.removeEventListener('focusin', onTrappedFocusMovement);
973
+ }
974
+ }
975
+
874
976
  /**
875
977
  * @param {Object} data
876
978
  * @param {String} [data.behavior='smooth'] // auto or smooth
@@ -98,8 +98,8 @@ class View extends Component {
98
98
  rendererOutput = ''
99
99
  }
100
100
 
101
- if (column.align !== 'left') {
102
- cellCls.push('neo-' + column.align)
101
+ if (column.cellAlign !== 'left') {
102
+ cellCls.push('neo-' + column.cellAlign)
103
103
  }
104
104
 
105
105
  if (!cellId) {