neo.mjs 6.10.10 → 6.10.11

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.
Files changed (79) hide show
  1. package/apps/ServiceWorker.mjs +2 -2
  2. package/apps/portal/view/learn/ContentTreeList.mjs +24 -12
  3. package/apps/portal/view/learn/LivePreview.mjs +28 -11
  4. package/buildScripts/createAppMinimal.mjs +391 -0
  5. package/examples/ServiceWorker.mjs +2 -2
  6. package/examples/button/base/neo-config.json +2 -1
  7. package/examples/list/chip/neo-config.json +1 -2
  8. package/package.json +72 -70
  9. package/resources/data/deck/learnneo/data/theBeatles.json +22 -0
  10. package/resources/data/deck/learnneo/p/2023-10-14T19-25-08-153Z.md +29 -20
  11. package/resources/data/deck/learnneo/p/ComponentModels.md +116 -1
  12. package/resources/data/deck/learnneo/p/Config.md +157 -0
  13. package/resources/data/deck/learnneo/p/DescribingTheUI.md +67 -1
  14. package/resources/data/deck/learnneo/p/Earthquakes.md +214 -0
  15. package/resources/data/deck/learnneo/p/Events.md +142 -1
  16. package/resources/data/deck/learnneo/p/Extending.md +116 -1
  17. package/resources/data/deck/learnneo/p/References.md +126 -0
  18. package/resources/data/deck/learnneo/p/TestLivePreview.md +28 -6
  19. package/resources/data/deck/learnneo/t.json +5 -6
  20. package/resources/data/deck/training/p/2022-12-27T21-55-30-948Z.md +1 -1
  21. package/resources/data/deck/training/p/2022-12-27T22-23-55-083Z.md +1 -1
  22. package/resources/data/deck/training/p/2022-12-29T16-00-13-223Z.md +1 -1
  23. package/resources/data/deck/training/p/2022-12-29T18-34-25-826Z.md +1 -1
  24. package/resources/data/deck/training/p/2022-12-29T18-36-56-893Z.md +1 -1
  25. package/resources/data/deck/training/p/2022-12-31T18-43-56-338Z.md +1 -1
  26. package/resources/data/deck/training/p/2022-12-31T18-51-50-682Z.md +1 -1
  27. package/resources/data/deck/training/p/2022-12-31T18-54-04-176Z.md +1 -1
  28. package/resources/data/deck/training/p/2023-01-01T17-49-18-429Z.md +1 -1
  29. package/resources/data/deck/training/p/2023-01-01T21-23-17-716Z.md +1 -1
  30. package/resources/data/deck/training/p/2023-01-06T23-21-31-685Z.md +1 -1
  31. package/resources/data/deck/training/p/2023-01-06T23-34-13-897Z.md +2 -2
  32. package/resources/data/deck/training/p/2023-01-06T23-46-36-687Z.md +1 -1
  33. package/resources/data/deck/training/p/2023-01-08T01-24-21-088Z.md +1 -1
  34. package/resources/data/deck/training/p/2023-01-08T02-11-26-333Z.md +2 -2
  35. package/resources/data/deck/training/p/2023-01-14T00-40-27-784Z.md +2 -2
  36. package/resources/data/deck/training/p/2023-07-31T00-37-21-927Z.md +2 -2
  37. package/resources/data/deck/training/p/2023-10-14T19-25-08-153Z.md +3 -3
  38. package/resources/scss/src/apps/newwebsite/Viewport.scss +32 -0
  39. package/resources/scss/src/apps/portal/learn/ContentView.scss +20 -4
  40. package/resources/scss/src/apps/portal/learn/LivePreview.scss +8 -0
  41. package/resources/scss/src/component/Base.scss +13 -4
  42. package/resources/scss/src/form/field/Select.scss +2 -5
  43. package/resources/scss/src/form/field/Text.scss +0 -1
  44. package/resources/scss/src/list/Base.scss +47 -2
  45. package/resources/scss/src/list/Chip.scss +10 -4
  46. package/resources/scss/theme-dark/list/Base.scss +11 -10
  47. package/resources/scss/theme-light/list/Base.scss +11 -10
  48. package/resources/scss/theme-neo-light/design-tokens/Components.scss +3 -0
  49. package/resources/scss/theme-neo-light/list/Base.scss +1 -0
  50. package/src/DefaultConfig.mjs +3 -3
  51. package/src/component/Base.mjs +7 -0
  52. package/src/container/Base.mjs +6 -12
  53. package/src/core/Base.mjs +5 -2
  54. package/src/data/Model.mjs +7 -0
  55. package/src/data/RecordFactory.mjs +5 -4
  56. package/src/form/field/Base.mjs +11 -0
  57. package/src/form/field/Picker.mjs +0 -1
  58. package/src/form/field/Select.mjs +208 -257
  59. package/src/form/field/Text.mjs +3 -3
  60. package/src/form/field/trigger/Base.mjs +5 -6
  61. package/src/layout/Flexbox.mjs +23 -31
  62. package/src/layout/HBox.mjs +1 -1
  63. package/src/layout/VBox.mjs +1 -1
  64. package/src/list/Base.mjs +64 -31
  65. package/src/main/DomAccess.mjs +55 -28
  66. package/src/main/DomEvents.mjs +2 -1
  67. package/src/main/DomUtils.mjs +66 -0
  68. package/src/main/addon/Navigator.mjs +332 -0
  69. package/src/manager/DomEvent.mjs +2 -1
  70. package/src/selection/ListModel.mjs +46 -82
  71. package/src/selection/Model.mjs +56 -33
  72. package/src/util/Array.mjs +5 -2
  73. package/src/util/Function.mjs +31 -0
  74. package/src/util/String.mjs +9 -0
  75. package/src/vdom/Helper.mjs +1 -2
  76. package/test/components/app.mjs +4 -3
  77. package/test/components/files/component/ChipList.mjs +125 -0
  78. package/test/components/files/form/field/Select.mjs +177 -2
  79. package/test/components/siesta.js +34 -1
@@ -0,0 +1,332 @@
1
+ import Base from '../../core/Base.mjs';
2
+ import DomAccess from '../DomAccess.mjs';
3
+ import DomUtils from '../DomUtils.mjs';
4
+ import DomEvents from '../DomEvents.mjs';
5
+
6
+ /**
7
+ * Addon for Navigator
8
+ * @class Neo.main.addon.Navigator
9
+ * @extends Neo.core.Base
10
+ * @singleton
11
+ */
12
+ class Navigator extends Base {
13
+ static config = {
14
+ /**
15
+ * @member {String} className='Neo.main.addon.Navigator'
16
+ * @protected
17
+ */
18
+ className: 'Neo.main.addon.Navigator',
19
+ /**
20
+ * Remote method access for other workers
21
+ * @member {Object} remote={app: [//...]}
22
+ * @protected
23
+ */
24
+ remote: {
25
+ app: [
26
+ 'subscribe',
27
+ 'unsubscribe',
28
+ 'navigateTo'
29
+ ]
30
+ },
31
+ /**
32
+ * @member {Boolean} singleton=true
33
+ * @protected
34
+ */
35
+ singleton: true
36
+ }
37
+
38
+ /**
39
+ * Sets up keyboard based navigation within the passed element id.
40
+ *
41
+ * When navigation occurs from one navigable element to another, the `navigate` event
42
+ * will be fired.
43
+ * @param {*} data
44
+ * @param {String} data.id The element id to navigate in.
45
+ * @param {String} [data.eventSource] Optional - the element id to read keystrokes from.
46
+ * defaults to the main element id.
47
+ * @param {String} data.selector A CSS selector which identifies the navigable elements.
48
+ * @param {String} data.activeCls A CSS class to add to the currently active navigable element.
49
+ * @param {Boolean} wrap Pass as `true` to have navigation wrap from first to last and vice versa.
50
+ */
51
+ subscribe(data) {
52
+ const
53
+ me = this,
54
+ subject = data.subject = DomAccess.getElement(data.id),
55
+ eventSource = data.eventSource = data.eventSource ? DomAccess.getElement(data.eventSource) : subject;
56
+
57
+ subject.$navigator = data;
58
+
59
+ if (!data.activeCls) {
60
+ data.activeCls = 'neo-navigator-active-item'
61
+ }
62
+
63
+ // Finds a focusable item starting from a descendant el within one of our selector items
64
+ data.findFocusable = el => DomUtils.closest(el, el =>
65
+ // We're looking for an element that is focusable
66
+ DomUtils.isFocusable(el) &&
67
+ // And within our subject element
68
+ (subject.compcompareDocumentPosition(el) & Node.DOCUMENT_POSITION_CONTAINED_BY) &&
69
+ // And within an element that matches our selector
70
+ el.closest(data.selector)
71
+ );
72
+
73
+ // TreeWalker so that we can easily move between navigable elements within the target.
74
+ data.treeWalker = document.createTreeWalker(subject, NodeFilter.SHOW_ELEMENT, node => me.navigateNodeFilter(node, data));
75
+
76
+ // We have to know when the DOM mutates in case the active item is removed.
77
+ (data.targetMutationMonitor = new MutationObserver(e => me.navigateTargetChildListChange(e, data))).observe(subject, {
78
+ childList : true,
79
+ subtree : true
80
+ });
81
+
82
+ eventSource.addEventListener('keydown', data.l1 = e => me.navigateKeyDownHandler(e, data));
83
+ subject.addEventListener('mousedown', data.l2 = e => me.navigateMouseDownHandler(e, data));
84
+ subject.addEventListener('click', data.l3 = e => me.navigateClickHandler(e, data));
85
+ subject.addEventListener('focusin', data.l4 = e => me.navigateFocusInHandler(e, data));
86
+ }
87
+
88
+ unsubscribe(data) {
89
+ const target = DomAccess.getElement(data.id);
90
+
91
+ data = target?.$navigator;
92
+ if (data) {
93
+ delete target.$navigator;
94
+ data.targetMutationMonitor.disconnect(target);
95
+ data.eventSource.removeEventListener('keydown', data.l1);
96
+ target.removeEventListener('mousedown', data.l2);
97
+ target.removeEventListener('click', data.l3);
98
+ target.removeEventListener('focusin', data.l4);
99
+ }
100
+ }
101
+
102
+ navigateTargetChildListChange(mutations, data) {
103
+ // Active item gone.
104
+ // Try to activate the item at the same index;
105
+ if (data.activeItem && !data.subject.contains(data.activeItem)) {
106
+ const allItems = data.subject.querySelectorAll(data.selector);
107
+
108
+ allItems.length && this.navigateTo(allItems[Math.max(Math.min(data.activeIndex, allItems.length - 1), 0)], data);
109
+ }
110
+ }
111
+
112
+ navigateFocusInHandler(e, data) {
113
+ const target = e.target.closest(data.selector);
114
+
115
+ // If our targets are focusable and recieve focus, that is a navigation.
116
+ if (target) {
117
+ this.setActiveItem(target, data);
118
+ }
119
+ }
120
+
121
+ navigateClickHandler(e, data) {
122
+ const target = e.target.closest(data.selector);
123
+
124
+ // If there was a focusable under the mouse, mousedown will have focused it and and we
125
+ // will have respond to that in navigateFocusInHandler.
126
+ // If not, we navigate programatically.
127
+ if (target && !data.findFocusable(target)) {
128
+ this.navigateTo(target, data);
129
+ }
130
+ }
131
+
132
+ navigateMouseDownHandler(e, data) {
133
+ const target = e.target.closest(data.selector);
134
+
135
+ // If there is a focusable undet the mouse, it will take focus, and we respond to that in navigateFocusInHandler.
136
+ // If not, we have to programatically activate on click, but we must not draw focus away from
137
+ // where it is, so preventDefault
138
+ if (target && !data.findFocusable(target)) {
139
+ e.preventDefault();
140
+ }
141
+ }
142
+
143
+ navigateKeyDownHandler(keyEvent, data) {
144
+ const
145
+ me = this,
146
+ {
147
+ subject,
148
+ wrap
149
+ } = data,
150
+ firstItem = subject.querySelector(data.selector);
151
+
152
+ if (!data.nextKey && firstItem) {
153
+ const
154
+ containerStyle = getComputedStyle(subject),
155
+ itemStyle = getComputedStyle(firstItem);
156
+
157
+ // Detect what the next and prev keys should be.
158
+ // Child elements layed out horizontally.
159
+ if (containerStyle.display === 'flex' && containerStyle.flexDirection === 'row'
160
+ || itemStyle.display === 'inline' || itemStyle.display === 'inline-block') {
161
+ data.previousKey = 'ArrowLeft';
162
+ data.nextKey = 'ArrowRight';
163
+ }
164
+ // Child elements layed out vertically.
165
+ else {
166
+ data.previousKey = 'ArrowUp';
167
+ data.nextKey = 'ArrowDown';
168
+ }
169
+ }
170
+
171
+ let { key } = keyEvent,
172
+ newActiveElement;
173
+
174
+ switch(key) {
175
+ case data.previousKey:
176
+ newActiveElement = me.navigateGetAdjacent(-1, data);
177
+ if (!newActiveElement && wrap) {
178
+ newActiveElement = subject.querySelector(`${data.selector}:last-of-type`);
179
+ }
180
+ break;
181
+ case data.nextKey:
182
+ newActiveElement = me.navigateGetAdjacent(1, data);
183
+ if (!newActiveElement && wrap) {
184
+ newActiveElement = subject.querySelector(data.selector);
185
+ }
186
+ break;
187
+ case 'Home':
188
+ newActiveElement = subject.querySelector(data.selector);
189
+ break;
190
+ case 'End':
191
+ newActiveElement = subject.querySelector(`${data.selector}:last-of-type`);
192
+ break;
193
+ case 'Enter':
194
+ if (data.activeItem) {
195
+ const
196
+ rect = data.activeItem.getBoundingClientRect(),
197
+ clientX = rect.x + (rect.width / 2),
198
+ clientY = rect.y + (rect.height / 2);
199
+
200
+ data.activeItem.dispatchEvent(new MouseEvent('click', {
201
+ bubbles : true,
202
+ altKey : Neo.altKeyDown,
203
+ ctrlKey : Neo.controlKeyDown,
204
+ metaKey : Neo.metaKeyDown,
205
+ shiftKey : Neo.shiftKeyDown,
206
+ clientX,
207
+ clientY
208
+ }))
209
+ }
210
+ }
211
+
212
+ if (newActiveElement) {
213
+ keyEvent.preventDefault();
214
+ me.navigateTo(newActiveElement, data);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Navigates to the passed
220
+ * @param {String|Number} newActiveElement The id of the new active element in the subject
221
+ * element, or the index of the item.
222
+ * @param {Object} data The data block as passed to {@link #subscribe}
223
+ * @returns
224
+ */
225
+ navigateTo(newActiveElement, data) {
226
+ if (!data.subject) {
227
+ // If subject has been unmounted, we cannot navigate
228
+ if (!(data = DomAccess.getElement(data.id)?.$navigator)) {
229
+ return;
230
+ }
231
+ }
232
+
233
+ // Can navigate by index. This is useful if the active item is deleted.
234
+ // We can navigate to the same index and preserve UI stability.
235
+ if (typeof newActiveElement === 'number') {
236
+ newActiveElement = data.subject.querySelectorAll(data.selector)[newActiveElement];
237
+ }
238
+ else if (typeof newActiveElement === 'string') {
239
+ newActiveElement = DomAccess.getElement(newActiveElement);
240
+ }
241
+
242
+ // Find a focusable element which may be the item, or inside the item to draw focus to.
243
+ // For example a Chip list in which .neo-list-items contain focusable Chips.
244
+ const focusTarget = [newActiveElement, ...newActiveElement.querySelectorAll('*')].find(DomUtils.isFocusable);
245
+
246
+ // If the item contains a focusable, we focus it and then react in navigateFocusInHandler
247
+ if (focusTarget) {
248
+ focusTarget.focus();
249
+ }
250
+ // If not, we programatically navigate there
251
+ else {
252
+ this.setActiveItem(newActiveElement, data);
253
+ }
254
+ }
255
+
256
+ setActiveItem(newActiveElement, data) {
257
+ const allItems = Array.from(data.subject.querySelectorAll(data.selector));
258
+
259
+ // Can navigate by index. This is useful if the active item is deleted.
260
+ // We can navigate to the same index and preserve UI stability.
261
+ if (typeof newActiveElement === 'number') {
262
+ newActiveElement = allItems[Math.max(Math.min(newActiveElement, allItems.length - 1), 0)];
263
+ }
264
+
265
+ data.previousActiveIndex = data.activeIndex;
266
+ (data.previousActiveItem = data.activeItem)?.classList.remove(data.activeCls);
267
+ (data.activeItem = newActiveElement)?.classList.add(data.activeCls);
268
+ data.activeIndex = newActiveElement ? allItems.indexOf(newActiveElement) : -1;
269
+
270
+ newActiveElement.scrollIntoView({
271
+ block : 'nearest',
272
+ inline : 'nearest',
273
+ nehavior : 'smooth'
274
+ })
275
+
276
+ DomEvents.sendMessageToApp({
277
+ type : 'neonavigate',
278
+ target : data.id,
279
+ path : [{
280
+ id : data.id
281
+ }],
282
+ activeItem : data.activeItem.id,
283
+ previousActiveItem : data.previousActiveItem?.id,
284
+ activeIndex : data.activeIndex,
285
+ previousActiveIndex : data.previousActiveIndex,
286
+ altKey : Neo.altKeyDown,
287
+ ctrlKey : Neo.controlKeyDown,
288
+ metaKey : Neo.metaKeyDown,
289
+ shiftKey : Neo.shiftKeyDown
290
+ })
291
+ }
292
+
293
+ navigateGetAdjacent(direction = 1, data) {
294
+ const { treeWalker } = data;
295
+
296
+ // Walk forwards or backwards to the next or previous node which matches our selector
297
+ treeWalker.currentNode = this.navigatorGetActiveItem(data) || data.subject;
298
+ treeWalker[direction < 0 ? 'previousNode' : 'nextNode']();
299
+
300
+ // Found a target in the requested direction
301
+ if (treeWalker.currentNode) {
302
+ if (treeWalker.currentNode !== data.activeItem) {
303
+ return treeWalker.currentNode;
304
+ }
305
+ }
306
+ // Could not find target in requested direction, then wrap if configured to do so
307
+ else if (data.wrap !== false) {
308
+ const allItems = data.subject.querySelector(data.selector);
309
+
310
+ return allItems[direction === 1 ? 0 : allItems.length - 1];
311
+ }
312
+ }
313
+
314
+ navigatorGetActiveItem(data) {
315
+ let activeItem = data.activeItem && DomAccess.getElement(data.activeItem.id);
316
+
317
+ if (!activeItem && ('activeIndex' in data)) {
318
+ const allItems = data.subject.querySelectorAll(data.selector);
319
+
320
+ activeItem = allItems[Math.max(Math.min(data.activeIndex, allItems.length - 1), 0)];
321
+ }
322
+ return activeItem;
323
+ }
324
+
325
+ navigateNodeFilter(node, data) {
326
+ return node.offsetParent && node.matches?.(data.selector) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
327
+ }
328
+ }
329
+
330
+ let instance = Neo.applyClassConfig(Navigator);
331
+
332
+ export default instance;
@@ -32,7 +32,8 @@ const globalDomEvents = [
32
32
  'mouseup',
33
33
  'scroll',
34
34
  'selectionchange',
35
- 'wheel'
35
+ 'wheel',
36
+ 'neonavigate'
36
37
  ];
37
38
 
38
39
  /**
@@ -1,3 +1,4 @@
1
+ import NeoArray from '../util/Array.mjs';
1
2
  import Model from './Model.mjs';
2
3
 
3
4
  /**
@@ -15,120 +16,64 @@ class ListModel extends Model {
15
16
  * @member {String} ntype='selection-listmodel'
16
17
  * @protected
17
18
  */
18
- ntype: 'selection-listmodel',
19
- /**
20
- * @member {Boolean} stayInList=true
21
- */
22
- stayInList: true
23
- }
24
-
25
- /**
26
- * @param {Object} data
27
- */
28
- onKeyDownDown(data) {
29
- !this.view.disableSelection && this.onNavKey(data, 1);
19
+ ntype: 'selection-listmodel'
30
20
  }
31
21
 
32
22
  /**
23
+ * Placeholder method to get overridden by class extension list menu.ListModel
33
24
  * @param {Object} data
34
25
  */
35
- onKeyDownEnter(data) {
36
- let view = this.view;
37
-
38
- !view.disableSelection && view.onKeyDownEnter?.(this.getSelection()[0]);
39
- }
26
+ onKeyDownEscape(data) {}
40
27
 
41
28
  /**
42
29
  * Placeholder method to get overridden by class extension list menu.ListModel
43
30
  * @param {Object} data
44
31
  */
45
- onKeyDownEscape(data) {}
32
+ onKeyDownDown(data) {}
46
33
 
47
34
  /**
35
+ * Placeholder method to get overridden by class extension list menu.ListModel
48
36
  * @param {Object} data
49
37
  */
50
- onKeyDownLeft(data) {
51
- this.onKeyDownUp(data);
52
- }
38
+ onKeyDownEnter(data) {}
53
39
 
54
40
  /**
41
+ * Placeholder method to get overridden by class extension list menu.ListModel
55
42
  * @param {Object} data
56
43
  */
57
- onKeyDownRight(data) {
58
- this.onKeyDownDown(data);
59
- }
44
+ onKeyDownLeft(data) {}
60
45
 
61
46
  /**
47
+ * Placeholder method to get overridden by class extension list menu.ListModel
62
48
  * @param {Object} data
63
49
  */
64
- onKeyDownUp(data) {
65
- !this.view.disableSelection && this.onNavKey(data, -1);
66
- }
50
+ onKeyDownRight(data) {}
67
51
 
68
52
  /**
53
+ * Placeholder method to get overridden by class extension list menu.ListModel
69
54
  * @param {Object} data
70
- * @param {Number} step
71
55
  */
72
- onNavKey(data, step) {
73
- let me = this,
74
- view = me.view,
75
- store = view.store,
76
- maxItems = store.getCount(),
77
- preventSelection = false,
78
- index, item, itemId, node, record, recordId;
79
-
80
- for (node of data.path) {
81
- if (node.cls.includes(view.itemCls)) {
82
- item = node.id;
83
- break;
84
- }
85
- }
56
+ onKeyDownUp(data) {}
86
57
 
87
- item = item || me.items?.[0];
58
+ onListClick({ currentTarget }) {
59
+ const { view } = this;
88
60
 
89
- if (item) {
90
- recordId = view.getItemRecordId(item);
91
- index = store.indexOf(recordId) + step;
92
- record = store.getAt(index);
61
+ if (!view.disableSelection) {
62
+ const record = view.store.get(view.getItemRecordId(currentTarget));
93
63
 
94
- while (record?.[view.disabledField] === true || record?.isHeader === true) {
95
- index += step;
96
- record = store.getAt(index)
64
+ if (record) {
65
+ this.select(record);
97
66
  }
98
-
99
- if (index < 0) {
100
- if (me.stayInList) {
101
- index = maxItems - 1;
102
- } else {
103
- preventSelection = true;
104
- me.deselectAll();
105
- view.fire('selectPreFirstItem')
106
- }
107
- } else if (index >= maxItems) {
108
- if (me.stayInList) {
109
- index = 0;
110
-
111
- while (store.getAt(index)?.isHeader === true) {
112
- index++;
113
- }
114
- } else {
115
- preventSelection = true;
116
- me.deselectAll();
117
- view.fire('selectPostLastItem')
118
- }
119
- }
120
- } else {
121
- index = 0
122
67
  }
68
+ }
123
69
 
124
- if (!preventSelection) {
125
- record = store.getAt(index);
126
- itemId = view.getItemId(record[me.view.getKeyProperty()]);
70
+ onListNavigate(data) {
71
+ const
72
+ { view } = this,
73
+ { store } = view;
127
74
 
128
- me.select(itemId);
129
- view.focus(itemId);
130
- view.fire('itemNavigate', record)
131
- }
75
+ data.record = store.getAt(Math.min(data.activeIndex, store.getCount()));
76
+ view.fire('itemNavigate', data);
132
77
  }
133
78
 
134
79
  /**
@@ -141,6 +86,26 @@ class ListModel extends Model {
141
86
  id = me.id,
142
87
  view = me.view;
143
88
 
89
+ view.addDomListeners([{
90
+ click : me.onListClick,
91
+
92
+ // Should be `.${view.itemCls}:not(.neo-disabled,.neo-list-header)`
93
+ // TODO parse delegate selectors
94
+ delegate : path => {
95
+ for (let i = 0, { length } = path; i < length; i++) {
96
+ const { cls } = path[i];
97
+
98
+ if (cls.includes(view.itemCls) && !cls.includes('neo-disabled') && !cls.includes('neo-list-header')) {
99
+ return i;
100
+ }
101
+ }
102
+ },
103
+ scope : me
104
+ }, {
105
+ neonavigate : me.onListNavigate,
106
+ scope : me
107
+ }])
108
+
144
109
  view.keys?._keys.push(
145
110
  {fn: 'onKeyDownDown' ,key: 'Down' ,scope: id},
146
111
  {fn: 'onKeyDownEnter' ,key: 'Enter' ,scope: id},
@@ -161,7 +126,6 @@ class ListModel extends Model {
161
126
 
162
127
  if (itemId) {
163
128
  this.select(itemId);
164
- view.focus(itemId)
165
129
  }
166
130
  }
167
131
 
@@ -94,20 +94,32 @@ class Model extends Base {
94
94
  * @param {String} [selectedCls]
95
95
  */
96
96
  deselect(item, silent, itemCollection=this.items, selectedCls) {
97
- let me = this,
98
- view = me.view,
99
- node = view.getVdomChild(item), // todo: support for nodes (right now limited to ids)
100
- cls;
97
+ // We hold vdom ids for now, so all incoming selections must be converted.
98
+ item = item.isRecord ? view.getItemId(item) : Neo.isObject(item) ? item.id : item;
99
+
100
+ if (itemCollection.includes(item)) {
101
+ let me = this,
102
+ view = me.view,
103
+ node = view.getVdomChild(item);
104
+
105
+ if (node) {
106
+ node.cls = NeoArray.remove(node.cls || [], selectedCls || me.selectedCls);
107
+ node['aria-selected'] = false;
108
+ }
101
109
 
102
- if (node) {
103
- cls = node.cls || [];
104
- NeoArray.remove(cls, selectedCls || me.selectedCls);
105
- node.cls = cls;
106
- }
110
+ NeoArray.remove(itemCollection, item);
107
111
 
108
- NeoArray.remove(itemCollection, item);
112
+ if (!silent) {
113
+ view.update();
109
114
 
110
- !silent && view.update();
115
+ me.fire('selectionChange', {
116
+ selection : itemCollection
117
+ });
118
+ }
119
+ }
120
+ else if (!silent) {
121
+ this.fire('noChange');
122
+ }
111
123
  }
112
124
 
113
125
  /**
@@ -118,12 +130,21 @@ class Model extends Base {
118
130
  items = [...me.items],
119
131
  view = me.view;
120
132
 
121
- items.forEach(item => {
122
- me.deselect(item, true);
123
- });
133
+ if (items.length) {
134
+ items.forEach(item => {
135
+ me.deselect(item, true);
136
+ });
137
+
138
+ if (!silent && items.length > 0) {
139
+ view.update();
140
+ }
124
141
 
125
- if (!silent && items.length > 0) {
126
- view.update();
142
+ me.fire('selectionChange', {
143
+ selection : this.items
144
+ });
145
+ }
146
+ else if (!silent) {
147
+ me.fire('noChange');
127
148
  }
128
149
  }
129
150
 
@@ -196,27 +217,24 @@ class Model extends Base {
196
217
  * @param {String} [selectedCls]
197
218
  */
198
219
  select(items, itemCollection=this.items, selectedCls) {
199
- items = Array.isArray(items) ? items : [items];
200
-
201
220
  let me = this,
202
221
  view = me.view,
203
- vdom = view.vdom,
204
- cls;
222
+ vdom = view.vdom;
223
+
224
+ // We hold vdom ids for now, so all incoming selections must be converted.
225
+ items = (items = Array.isArray(items) ? items : [items]).map(item => item.isRecord ? view.getItemId(item) : Neo.isObject(item) ? item.id : item)
205
226
 
206
227
  if (!Neo.isEqual(itemCollection, items)) {
207
228
  if (me.singleSelect) {
208
229
  me.deselectAll(true);
209
230
  }
210
231
 
211
- items.forEach(node => {
212
- if (typeof node === 'string') {
213
- node = view.getVdomChild(node);
214
- }
215
-
232
+ items.forEach((node, i) => {
233
+ node = view.getVdomChild(node);
234
+
216
235
  if (node) {
217
- cls = node.cls || [];
218
- NeoArray.add(cls, selectedCls || me.selectedCls);
219
- node.cls = cls;
236
+ node.cls = NeoArray.add(node.cls || [], selectedCls || me.selectedCls);
237
+ node['aria-selected'] = true;
220
238
  }
221
239
  });
222
240
 
@@ -225,6 +243,13 @@ class Model extends Base {
225
243
  view[view.silentSelect ? '_vdom' : 'vdom'] = vdom;
226
244
 
227
245
  view.onSelect?.(items);
246
+
247
+ me.fire('selectionChange', {
248
+ selection : itemCollection
249
+ });
250
+ }
251
+ else {
252
+ me.fire('noChange');
228
253
  }
229
254
  }
230
255
 
@@ -232,12 +257,10 @@ class Model extends Base {
232
257
  * @param {Object} item
233
258
  */
234
259
  toggleSelection(item) {
235
- let me = this;
236
-
237
- if (me.isSelected(item)) {
238
- me.deselect(item);
260
+ if (this.isSelected(item)) {
261
+ this.deselect(item);
239
262
  } else {
240
- me.select(item);
263
+ this.select(item);
241
264
  }
242
265
  }
243
266