neo.mjs 6.10.10 → 6.10.12

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 (82) 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/examples/toolbar/paging/view/MainContainer.mjs +6 -1
  9. package/package.json +71 -70
  10. package/resources/data/deck/learnneo/data/theBeatles.json +22 -0
  11. package/resources/data/deck/learnneo/p/2023-10-14T19-25-08-153Z.md +32 -24
  12. package/resources/data/deck/learnneo/p/ComponentModels.md +114 -1
  13. package/resources/data/deck/learnneo/p/Config.md +157 -0
  14. package/resources/data/deck/learnneo/p/DescribingTheUI.md +67 -1
  15. package/resources/data/deck/learnneo/p/Earthquakes.md +214 -0
  16. package/resources/data/deck/learnneo/p/Events.md +142 -1
  17. package/resources/data/deck/learnneo/p/Extending.md +117 -1
  18. package/resources/data/deck/learnneo/p/References.md +126 -0
  19. package/resources/data/deck/learnneo/p/TestLivePreview.md +28 -6
  20. package/resources/data/deck/learnneo/t.json +5 -6
  21. package/resources/data/deck/training/p/2022-12-27T21-55-30-948Z.md +1 -1
  22. package/resources/data/deck/training/p/2022-12-27T22-23-55-083Z.md +1 -1
  23. package/resources/data/deck/training/p/2022-12-29T16-00-13-223Z.md +1 -1
  24. package/resources/data/deck/training/p/2022-12-29T18-34-25-826Z.md +1 -1
  25. package/resources/data/deck/training/p/2022-12-29T18-36-56-893Z.md +1 -1
  26. package/resources/data/deck/training/p/2022-12-31T18-43-56-338Z.md +1 -1
  27. package/resources/data/deck/training/p/2022-12-31T18-51-50-682Z.md +1 -1
  28. package/resources/data/deck/training/p/2022-12-31T18-54-04-176Z.md +1 -1
  29. package/resources/data/deck/training/p/2023-01-01T17-49-18-429Z.md +1 -1
  30. package/resources/data/deck/training/p/2023-01-01T21-23-17-716Z.md +1 -1
  31. package/resources/data/deck/training/p/2023-01-06T23-21-31-685Z.md +1 -1
  32. package/resources/data/deck/training/p/2023-01-06T23-34-13-897Z.md +2 -2
  33. package/resources/data/deck/training/p/2023-01-06T23-46-36-687Z.md +1 -1
  34. package/resources/data/deck/training/p/2023-01-08T01-24-21-088Z.md +1 -1
  35. package/resources/data/deck/training/p/2023-01-08T02-11-26-333Z.md +2 -2
  36. package/resources/data/deck/training/p/2023-01-14T00-40-27-784Z.md +2 -2
  37. package/resources/data/deck/training/p/2023-07-31T00-37-21-927Z.md +2 -2
  38. package/resources/data/deck/training/p/2023-10-14T19-25-08-153Z.md +3 -3
  39. package/resources/scss/src/apps/newwebsite/Viewport.scss +32 -0
  40. package/resources/scss/src/apps/portal/learn/ContentView.scss +20 -4
  41. package/resources/scss/src/apps/portal/learn/LivePreview.scss +8 -0
  42. package/resources/scss/src/component/Base.scss +13 -4
  43. package/resources/scss/src/form/field/Select.scss +2 -5
  44. package/resources/scss/src/form/field/Text.scss +0 -1
  45. package/resources/scss/src/list/Base.scss +51 -2
  46. package/resources/scss/src/list/Chip.scss +10 -4
  47. package/resources/scss/theme-dark/list/Base.scss +11 -10
  48. package/resources/scss/theme-light/list/Base.scss +11 -10
  49. package/resources/scss/theme-neo-light/design-tokens/Components.scss +3 -0
  50. package/resources/scss/theme-neo-light/list/Base.scss +1 -0
  51. package/src/DefaultConfig.mjs +3 -3
  52. package/src/component/Base.mjs +7 -0
  53. package/src/container/Base.mjs +6 -12
  54. package/src/core/Base.mjs +5 -2
  55. package/src/data/Model.mjs +7 -0
  56. package/src/data/RecordFactory.mjs +5 -4
  57. package/src/form/field/Base.mjs +11 -0
  58. package/src/form/field/Date.mjs +22 -1
  59. package/src/form/field/Picker.mjs +0 -1
  60. package/src/form/field/Select.mjs +208 -257
  61. package/src/form/field/Text.mjs +3 -3
  62. package/src/form/field/trigger/Base.mjs +5 -6
  63. package/src/layout/Flexbox.mjs +23 -31
  64. package/src/layout/HBox.mjs +1 -1
  65. package/src/layout/VBox.mjs +1 -1
  66. package/src/list/Base.mjs +64 -31
  67. package/src/main/DomAccess.mjs +55 -28
  68. package/src/main/DomEvents.mjs +2 -1
  69. package/src/main/DomUtils.mjs +66 -0
  70. package/src/main/addon/Navigator.mjs +331 -0
  71. package/src/manager/DomEvent.mjs +2 -1
  72. package/src/selection/ListModel.mjs +46 -82
  73. package/src/selection/Model.mjs +56 -33
  74. package/src/tooltip/Base.mjs +6 -2
  75. package/src/util/Array.mjs +5 -2
  76. package/src/util/Function.mjs +31 -0
  77. package/src/util/String.mjs +9 -0
  78. package/src/vdom/Helper.mjs +1 -2
  79. package/test/components/app.mjs +4 -3
  80. package/test/components/files/component/ChipList.mjs +125 -0
  81. package/test/components/files/form/field/Select.mjs +177 -2
  82. package/test/components/siesta.js +34 -1
@@ -29,6 +29,7 @@ class NeoArray extends Base {
29
29
  arr.push(item);
30
30
  }
31
31
  });
32
+ return arr;
32
33
  }
33
34
 
34
35
  /**
@@ -97,6 +98,7 @@ class NeoArray extends Base {
97
98
 
98
99
  index > -1 && arr.splice(index, 1);
99
100
  });
101
+ return arr;
100
102
  }
101
103
 
102
104
  /**
@@ -108,7 +110,7 @@ class NeoArray extends Base {
108
110
  */
109
111
  static removeAdd(arr, removeItems, addItems) {
110
112
  this.remove(arr, removeItems);
111
- this.add(arr, addItems);
113
+ return this.add(arr, addItems);
112
114
  }
113
115
 
114
116
  /**
@@ -118,7 +120,7 @@ class NeoArray extends Base {
118
120
  * @param {Boolean} [add]
119
121
  */
120
122
  static toggle(arr, item, add = !this.hasItem(arr, item)) {
121
- this[add ? 'add' : 'remove'](arr, item);
123
+ return this[add ? 'add' : 'remove'](arr, item);
122
124
  }
123
125
 
124
126
  /**
@@ -149,6 +151,7 @@ class NeoArray extends Base {
149
151
  arr.unshift(item);
150
152
  }
151
153
  });
154
+ return arr;
152
155
  }
153
156
  }
154
157
 
@@ -121,3 +121,34 @@ export function throttle(callback, scope, delay=300) {
121
121
  }
122
122
  }
123
123
  }
124
+
125
+ /**
126
+ * @param {Function} callback
127
+ * @param {Neo.core.Base} scope
128
+ * @param {Number} delay=300
129
+ * @returns {Function}
130
+ */
131
+ export function buffer(callback, scope, delay=300) {
132
+ let timeoutId;
133
+
134
+ const wrapper = function(...args) {
135
+ // callback invocation comes "delay" ms after the last call to wrapper
136
+ // so cancel any pending invocation.
137
+ clearTimeout(timeoutId);
138
+
139
+ wrapper.isPending = true;
140
+
141
+ timeoutId = setTimeout(() => {
142
+ timeoutId = 0;
143
+ wrapper.isPending = false;
144
+ callback.apply(scope, args);
145
+ }, delay);
146
+ };
147
+
148
+ wrapper.cancel = () => {
149
+ wrapper.isPending = false;
150
+ clearTimeout(timeoutId);
151
+ };
152
+
153
+ return wrapper;
154
+ }
@@ -81,6 +81,15 @@ class StringUtil extends Base {
81
81
 
82
82
  return value.replace(me.entityPattern, me.getCharFromEntity.bind(me));
83
83
  }
84
+
85
+ /**
86
+ * Returns the passed string with the first letter uncapitalized.
87
+ * @param {String} value
88
+ * @returns {String}
89
+ */
90
+ static uncapitalize(value) {
91
+ return value && value[0].toLowerCase() + value.substring(1)
92
+ }
84
93
  }
85
94
 
86
95
  Neo.applyClassConfig(StringUtil);
@@ -580,8 +580,7 @@ class Helper extends Base {
580
580
  string += ` ${key}`;
581
581
  }
582
582
  } else if (key !== 'removeDom') {
583
-
584
- string += ` ${key}="${value.replaceAll('"', '"')}"`;
583
+ string += ` ${key}="${value?.replaceAll?.('"', '"') ?? value}"`;
585
584
  }
586
585
  });
587
586
 
@@ -1,8 +1,9 @@
1
1
  // Important: You need to import all classes which you want to use inside tests here
2
2
  // (excluding base classes (prototypes) of already imported classes)
3
3
 
4
- import Button from '../../src/button/Base.mjs';
5
- import DateSelector from '../../src/component/DateSelector.mjs';
6
- import SelectField from '../../src/form/field/Select.mjs';
4
+ import '../../src/button/Base.mjs';
5
+ import '../../src/component/DateSelector.mjs';
6
+ import '../../src/form/field/Select.mjs';
7
+ import '../../src/list/Chip.mjs'
7
8
 
8
9
  export const onStart = () => Neo.app({name: 'AppEmpty'})
@@ -0,0 +1,125 @@
1
+ StartTest(t => {
2
+ let testId;
3
+
4
+ async function setup(config = {}) {
5
+ testId = await Neo.worker.App.createNeoInstance(Neo.merge({
6
+ ntype : 'chip-list',
7
+ displayField : 'firstname',
8
+ width : 300,
9
+ role : 'listbox',
10
+ itemRole : 'option',
11
+ navigator : {
12
+ wrap : true
13
+ },
14
+ store : {
15
+ keyProperty: 'githubId',
16
+ model: {
17
+ fields: [{
18
+ name: 'country',
19
+ type: 'String'
20
+ }, {
21
+ name: 'firstname',
22
+ type: 'String'
23
+ }, {
24
+ name: 'githubId',
25
+ type: 'String'
26
+ }, {
27
+ name: 'lastname',
28
+ type: 'String'
29
+ }]
30
+ },
31
+
32
+ data: [{
33
+ country : 'Germany',
34
+ firstname: 'Tobias',
35
+ githubId : 'tobiu',
36
+ lastname : 'Uhlig'
37
+ }, {
38
+ country : 'USA',
39
+ firstname: 'Rich',
40
+ githubId : 'rwaters',
41
+ lastname : 'Waters'
42
+ }, {
43
+ country : 'Germany',
44
+ firstname: 'Nils',
45
+ githubId : 'mrsunshine',
46
+ lastname : 'Dehl'
47
+ }, {
48
+ country : 'USA',
49
+ firstname: 'Gerard',
50
+ githubId : 'camtnbikerrwc',
51
+ lastname : 'Horan'
52
+ }, {
53
+ country : 'Slovakia',
54
+ firstname: 'Jozef',
55
+ githubId : 'jsakalos',
56
+ lastname : 'Sakalos'
57
+ }, {
58
+ country : 'Germany',
59
+ firstname: 'Bastian',
60
+ githubId : 'bhaustein',
61
+ lastname : 'Haustein'
62
+ }]
63
+ }
64
+ }, config));
65
+
66
+
67
+ return testId;
68
+ }
69
+
70
+ // Clear the eway for each test
71
+ t.beforeEach(async t => {
72
+ if (testId) {
73
+ await Neo.worker.App.destroyNeoInstance(testId);
74
+ testId = null;
75
+ }
76
+ });
77
+
78
+ t.it('Sanity', async t => {
79
+ await setup({ });
80
+
81
+ // Click on the *item*, *not* the focusable chip.
82
+ // We are testing that the Navigator directs focus to the focusable heart of the
83
+ // item - the Chip - which will then recieve focus and cause item activation.
84
+ await t.click('.neo-list-item', null, null, null, ['100%-1', 1]);
85
+
86
+ // That should select and activate the clicked item.
87
+ // And focus the chip inside it.
88
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item.neo-selected:nth-child(1) .neo-chip:focus');
89
+ t.selectorCountIs('.neo-list-item.neo-navigator-active-item', 1);
90
+
91
+ await t.type(null, '[END]');
92
+
93
+ // That should select and activate the last item.
94
+ // And focus the chip inside it.
95
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:not(.neo-selected):nth-child(6) .neo-chip:focus');
96
+
97
+ // Item 1 is still the only one selected, and it's not focused
98
+ t.selectorExists('.neo-list-item.neo-selected:nth-child(1) .neo-chip:not(:focus)');
99
+ t.selectorCountIs('.neo-list-item.neo-selected', 1);
100
+ t.selectorCountIs('.neo-list-item.neo-navigator-active-item', 1);
101
+
102
+ await t.type(null, '[ENTER]');
103
+
104
+ // Item 6 is now the only one selected
105
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item.neo-selected:nth-child(6) .neo-chip:focus');
106
+ t.selectorCountIs('.neo-list-item.neo-selected', 1);
107
+ t.selectorCountIs('.neo-list-item.neo-navigator-active-item', 1);
108
+
109
+ await t.type(null, '[DOWN]');
110
+
111
+ // That should select and activate the first item.
112
+ // And focus the chip inside it.
113
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:not(.neo-selected):nth-child(1) .neo-chip:focus');
114
+ t.selectorCountIs('.neo-list-item.neo-selected', 1);
115
+ t.selectorCountIs('.neo-list-item.neo-navigator-active-item', 1);
116
+
117
+ await t.type(null, '[UP]');
118
+
119
+ // Item 6 is now the only one selected
120
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item.neo-selected:nth-child(6) .neo-chip:focus');
121
+ t.selectorCountIs('.neo-list-item.neo-selected', 1);
122
+ t.selectorCountIs('.neo-list-item.neo-navigator-active-item', 1);
123
+ });
124
+
125
+ });
@@ -1,6 +1,8 @@
1
1
  StartTest(t => {
2
- t.it('Sanity', async t => {
3
- Neo.worker.App.createNeoInstance({
2
+ let testId, inputField;
3
+
4
+ async function setup(config = {}) {
5
+ testId = await Neo.worker.App.createNeoInstance(Neo.merge({
4
6
  ntype : 'selectfield',
5
7
  labelPosition: 'inline',
6
8
  labelText : 'US States',
@@ -22,6 +24,179 @@ StartTest(t => {
22
24
  }]
23
25
  }
24
26
  }
27
+ }, config))
28
+
29
+ // Wait for input element to be present
30
+ await t.waitForSelector(`#${testId} input.neo-textfield-input:not(.neo-typeahead-input)`);
31
+
32
+ // Grab input element
33
+ inputField = t.query(`#${testId} input.neo-textfield-input:not(.neo-typeahead-input)`)[0]
34
+
35
+ return testId;
36
+ }
37
+
38
+ // Clear the eway for each test
39
+ t.beforeEach(async t => {
40
+ if (testId) {
41
+ await Neo.worker.App.destroyNeoInstance(testId);
42
+ testId = null;
43
+ }
44
+ });
45
+
46
+ t.it('Editable', async t => {
47
+ await setup({
48
+ editable : false
25
49
  });
50
+ const blurEl = document.createElement('input');
51
+ document.body.appendChild(blurEl);
52
+
53
+ await t.waitFor(() => inputField.getAttribute('readonly'));
54
+
55
+ let blurCount = 0;
56
+ inputField.addEventListener('blur', () => blurCount++);
57
+
58
+ t.hasAttributeValue(inputField, 'role', 'combobox');
59
+ t.hasAttributeValue(inputField, 'aria-haspopup', 'listbox');
60
+ t.hasAttributeValue(inputField, 'aria-expanded', 'false');
61
+
62
+ await t.click(inputField);
63
+
64
+ await t.waitForSelector('.neo-picker-container');
65
+
66
+ t.hasAttributeValue(inputField, 'aria-expanded', 'true');
67
+
68
+ // Roles correct
69
+ t.hasAttributeValue('.neo-picker-container .neo-list', 'role', 'listbox');
70
+ t.selectorCountIs('.neo-picker-container .neo-list .neo-list-item[role="option"]', 59);
71
+
72
+ // Nothing selected
73
+ t.hasAttributeValue(inputField, 'aria-activedescendant', '');
74
+
75
+ // Should activate the first list item. editable : false means we can still be focused
76
+ // and select values, just that the filter input is read-only.
77
+ await t.type(null, '[DOWN]');
78
+
79
+ await t.waitForSelector('input.neo-textfield-input:not(:disabled)[aria-activedescendant="neo-list-1__AL"]');
80
+
81
+ t.hasAttributeValue(inputField, 'aria-activedescendant', 'neo-list-1__AL');
82
+
83
+ // Select that first item.
84
+ await t.type(null, '[ENTER]');
85
+
86
+ await t.waitForSelectorNotFound('.neo-picker-container:visible');
87
+
88
+ t.is(inputField.value, 'Alabama');
89
+
90
+ // Focus nebver leaves the input field
91
+ t.is(blurCount, 0);
92
+
93
+ await t.type(null, '[TAB]');
94
+
95
+ // Value still correct after blur
96
+ t.is(inputField.value, 'Alabama');
97
+ blurEl.remove();
98
+
99
+ // Now focus has left
100
+ t.is(blurCount, 1);
101
+ });
102
+
103
+ t.iit('Keyboard navigation', async t => {
104
+ await setup();
105
+ const blurEl = document.createElement('input');
106
+ document.body.appendChild(blurEl);
107
+
108
+ await t.click(inputField);
109
+ await t.type(null, '[DOWN]');
110
+
111
+ // Picker Must show with Alabama activated
112
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Alabama")');
113
+
114
+ await t.type(null, '[END]');
115
+
116
+ // Picker should go to end
117
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Wyoming")');
118
+
119
+ await t.type(null, '[UP]');
120
+
121
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Wisconsin")');
122
+
123
+ await t.type(null, '[DOWN]');
124
+
125
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Wyoming")');
126
+
127
+ await t.type(null, '[ENTER]');
128
+
129
+ await t.waitForSelectorNotFound('.neo-picker-container:visible');
130
+
131
+ t.is(inputField.value, 'Wyoming');
132
+
133
+ await t.type(null, '[DOWN]');
134
+
135
+ // Picker Must show with Wyoming activated
136
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Wyoming")');
137
+
138
+ await t.type(null, '[UP]');
139
+
140
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Wisconsin")');
141
+
142
+ await t.type(null, '[ENTER]');
143
+
144
+ await t.waitForSelectorNotFound('.neo-picker-container:visible');
145
+
146
+ t.is(inputField.value, 'Wisconsin');
147
+
148
+ await t.type(null, '[DOWN]');
149
+
150
+ // Picker Must show with Wisconsin activated
151
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Wisconsin")');
152
+
153
+ // We're single select, so only one list item must be selected
154
+ t.selectorCountIs('[aria-selected="true"]', 1);
155
+
156
+ await t.type(null, '[TAB]');
157
+
158
+ // Value still correct after blur
159
+ t.is(inputField.value, 'Wisconsin');
160
+ blurEl.remove();
161
+ });
162
+
163
+ t.it('Type to filter', async t => {
164
+ await setup();
165
+ const blurEl = document.createElement('input');
166
+ document.body.appendChild(blurEl);
167
+
168
+ await t.click(inputField);
169
+
170
+ await t.type(null, 'Mar');
171
+
172
+ // Picker Must show with Marshall Islands activated
173
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Marshall Islands")');
174
+
175
+ // Matches three states
176
+ t.selectorCountIs('.neo-picker-container .neo-list-item', 3);
177
+
178
+ // Only one is selected
179
+ t.selectorCountIs('.neo-list-item.neo-navigator-active-item', 1);
180
+
181
+ await t.type(null, '[DOWN]');
182
+
183
+ // Picker Must show with Maryland activated
184
+ await t.waitForSelector('.neo-list-item.neo-navigator-active-item:contains("Maryland")');
185
+
186
+ // Matches three states
187
+ t.selectorCountIs('.neo-picker-container .neo-list-item', 3);
188
+
189
+ // Only one is selected
190
+ t.selectorCountIs('.neo-list-item.neo-navigator-active-item', 1);
191
+
192
+ // Blur without selecting a value
193
+ await t.type(null, '[TAB]');
194
+
195
+ await t.waitFor(100)
196
+
197
+ // Inputs must have been cleared. Both typeahead and filter.
198
+ t.isDeeply(t.query(`#${testId} input`).map(i => i.value), ['', '']);
199
+
200
+ blurEl.remove();
26
201
  });
27
202
  });
@@ -31,6 +31,38 @@ project.configure({
31
31
  this.SUPER(...arguments);
32
32
  this.SUPER(t => t.waitFor(50));
33
33
  }
34
+ },
35
+
36
+ methods : {
37
+ async waitForSelectorCount(selector, root, count) {
38
+ if (typeof root === 'number') {
39
+ count = root;
40
+ root = undefined;
41
+ }
42
+ return this.waitFor(() => this.query(selector, root).length === count);
43
+ },
44
+
45
+ async waitForDomMutation(root = this.global.document.body) {
46
+ root = this.normalizeElement(root);
47
+
48
+ if (root) {
49
+ return new Promise(resolve => {
50
+ const m = new MutationObserver(() => {
51
+ m.disconnect();
52
+ resolve();
53
+ });
54
+ m.observe(root, {
55
+ subtree : true,
56
+ childList : true,
57
+ attributes : true,
58
+ characterData : true
59
+ });
60
+ setTimeout(() => {
61
+ m.disconnect();
62
+ }, this.timeout)
63
+ });
64
+ }
65
+ }
34
66
  }
35
67
  })
36
68
  });
@@ -45,7 +77,8 @@ project.plan(
45
77
  {
46
78
  group: 'component',
47
79
  items: [
48
- 'files/component/DateSelector.mjs'
80
+ 'files/component/DateSelector.mjs',
81
+ 'files/component/ChipList.mjs'
49
82
  ]
50
83
  },
51
84
  {