kempo-ui 0.2.3 → 0.3.0

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 (36) hide show
  1. package/dist/components/Context.js +1 -0
  2. package/dist/components/Table.js +6 -5
  3. package/docs/components/aside-items.html +1 -0
  4. package/docs/components/aside-menu.html +1 -0
  5. package/docs/components/aside-push.html +1 -0
  6. package/docs/components/aside.html +6 -2
  7. package/docs/components/context.html +231 -0
  8. package/docs/components/nav.html +7 -2
  9. package/docs/components/table.html +13 -1
  10. package/docs/components/tableControls.html +2 -1
  11. package/docs/components/tableCustomFields.html +28 -2
  12. package/docs/components/tableFetchRecords.html +2 -1
  13. package/docs/components/tableFieldSortHide.html +2 -1
  14. package/docs/components/tablePagination.html +2 -1
  15. package/docs/components/tableRecordEditing.html +33 -1
  16. package/docs/components/tableRecordFiltering.html +2 -1
  17. package/docs/components/tableRecordHiding.html +2 -1
  18. package/docs/components/tableRecordSearching.html +2 -1
  19. package/docs/components/tableRecordSelection.html +2 -1
  20. package/docs/components/tableRowControls.html +2 -1
  21. package/docs/components/tableServerSync.html +1 -0
  22. package/docs/components/tableSorting.html +2 -1
  23. package/docs/icons/npm.svg +1 -0
  24. package/docs/index.html +6 -0
  25. package/docs/nav-1.inc.html +5 -2
  26. package/docs/nav.inc.html +4 -1
  27. package/docs/src/components/Context.js +1 -0
  28. package/docs/src/components/Table.js +6 -5
  29. package/docs/utils/elevation.html +6 -2
  30. package/icons/npm.svg +1 -0
  31. package/llm.txt +1 -0
  32. package/package.json +1 -1
  33. package/src/components/Context.js +84 -0
  34. package/src/components/Table.js +7 -4
  35. package/tests/components/Context.browser-test.js +238 -0
  36. package/tests/components/Table.browser-test.js +132 -0
@@ -0,0 +1,238 @@
1
+ import Context from '../../src/components/Context.js';
2
+ import LightComponent from '../../src/components/LightComponent.js';
3
+
4
+ const createContext = async () => {
5
+ const container = document.createElement('div');
6
+ container.innerHTML = `<k-context></k-context>`;
7
+ document.body.appendChild(container);
8
+ const el = container.querySelector('k-context');
9
+ await el.updateComplete;
10
+ return { container, el };
11
+ };
12
+
13
+ const cleanup = (container) => {
14
+ if(container && container.parentNode){
15
+ container.parentNode.removeChild(container);
16
+ }
17
+ };
18
+
19
+ export default {
20
+ /*
21
+ Element Creation
22
+ */
23
+ 'should create context element': async ({pass, fail}) => {
24
+ const { container, el } = await createContext();
25
+ if(!(el instanceof Context)){
26
+ cleanup(container);
27
+ return fail('Element should be instance of Context');
28
+ }
29
+ cleanup(container);
30
+ pass('Context element created correctly');
31
+ },
32
+
33
+ 'should extend LightComponent': async ({pass, fail}) => {
34
+ const { container, el } = await createContext();
35
+ if(!(el instanceof LightComponent)){
36
+ cleanup(container);
37
+ return fail('Context should extend LightComponent');
38
+ }
39
+ cleanup(container);
40
+ pass('Context extends LightComponent');
41
+ },
42
+
43
+ 'should have display contents': async ({pass, fail}) => {
44
+ const { container, el } = await createContext();
45
+ const display = getComputedStyle(el).display;
46
+ if(display !== 'contents'){
47
+ cleanup(container);
48
+ return fail(`Expected display to be contents, got "${display}"`);
49
+ }
50
+ cleanup(container);
51
+ pass('Context element has display contents');
52
+ },
53
+
54
+ 'should have default data of \'{}\'': async ({pass, fail}) => {
55
+ const { container, el } = await createContext();
56
+ if(el.data !== '{}'){
57
+ cleanup(container);
58
+ return fail(`Expected data to be '{}', got '${el.data}'`);
59
+ }
60
+ cleanup(container);
61
+ pass('Default data is \'{}\'');
62
+ },
63
+
64
+ 'data attribute reflects JSON state': async ({pass, fail}) => {
65
+ const { container, el } = await createContext();
66
+ el.set('count', 5);
67
+ await el.updateComplete;
68
+ let parsed;
69
+ try { parsed = JSON.parse(el.getAttribute('data')); } catch(e) {
70
+ cleanup(container);
71
+ return fail('data attribute is not valid JSON');
72
+ }
73
+ if(parsed.count !== 5){
74
+ cleanup(container);
75
+ return fail(`Expected data attribute to contain count:5, got ${el.getAttribute('data')}`);
76
+ }
77
+ cleanup(container);
78
+ pass('data attribute reflects JSON state');
79
+ },
80
+
81
+ /*
82
+ set() and get()
83
+ */
84
+ 'should set and get a value': async ({pass, fail}) => {
85
+ const { container, el } = await createContext();
86
+ el.set('user', { name: 'Alice' });
87
+ const value = el.get('user');
88
+ if(!value || value.name !== 'Alice'){
89
+ cleanup(container);
90
+ return fail(`Expected {name:'Alice'}, got ${JSON.stringify(value)}`);
91
+ }
92
+ cleanup(container);
93
+ pass('set and get work correctly');
94
+ },
95
+
96
+ /*
97
+ has()
98
+ */
99
+ 'has() returns true for existing key': async ({pass, fail}) => {
100
+ const { container, el } = await createContext();
101
+ el.set('theme', 'dark');
102
+ if(!el.has('theme')){
103
+ cleanup(container);
104
+ return fail('has() should return true for existing key');
105
+ }
106
+ cleanup(container);
107
+ pass('has() returns true for existing key');
108
+ },
109
+
110
+ 'has() returns false for missing key': async ({pass, fail}) => {
111
+ const { container, el } = await createContext();
112
+ if(el.has('missing')){
113
+ cleanup(container);
114
+ return fail('has() should return false for missing key');
115
+ }
116
+ cleanup(container);
117
+ pass('has() returns false for missing key');
118
+ },
119
+
120
+ /*
121
+ Events
122
+ */
123
+ 'set() fires context:create for new key': async ({pass, fail}) => {
124
+ const { container, el } = await createContext();
125
+ let received = null;
126
+ el.addEventListener('context:create', e => { received = e.detail; });
127
+ el.set('count', 0);
128
+ if(!received || received.key !== 'count' || received.value !== 0){
129
+ cleanup(container);
130
+ return fail(`Expected context:create with {key:'count',value:0}, got ${JSON.stringify(received)}`);
131
+ }
132
+ cleanup(container);
133
+ pass('context:create fired for new key');
134
+ },
135
+
136
+ 'set() fires context:set for existing key': async ({pass, fail}) => {
137
+ const { container, el } = await createContext();
138
+ el.set('count', 0);
139
+ let received = null;
140
+ el.addEventListener('context:set', e => { received = e.detail; });
141
+ el.set('count', 1);
142
+ if(!received || received.key !== 'count' || received.value !== 1 || received.oldValue !== 0){
143
+ cleanup(container);
144
+ return fail(`Expected context:set with {key:'count',value:1,oldValue:0}, got ${JSON.stringify(received)}`);
145
+ }
146
+ cleanup(container);
147
+ pass('context:set fired for existing key update');
148
+ },
149
+
150
+ 'set() does not fire context:create for second set': async ({pass, fail}) => {
151
+ const { container, el } = await createContext();
152
+ el.set('count', 0);
153
+ let createFired = false;
154
+ el.addEventListener('context:create', () => { createFired = true; });
155
+ el.set('count', 1);
156
+ if(createFired){
157
+ cleanup(container);
158
+ return fail('context:create should not fire when updating existing key');
159
+ }
160
+ cleanup(container);
161
+ pass('context:create not fired on update');
162
+ },
163
+
164
+ 'delete() removes a key and fires context:delete': async ({pass, fail}) => {
165
+ const { container, el } = await createContext();
166
+ el.set('temp', 42);
167
+ let received = null;
168
+ el.addEventListener('context:delete', e => { received = e.detail; });
169
+ el.delete('temp');
170
+ if(el.has('temp')){
171
+ cleanup(container);
172
+ return fail('Key should be removed after delete()');
173
+ }
174
+ if(!received || received.key !== 'temp' || received.value !== 42){
175
+ cleanup(container);
176
+ return fail(`Expected context:delete with {key:'temp',value:42}, got ${JSON.stringify(received)}`);
177
+ }
178
+ cleanup(container);
179
+ pass('delete() removes key and fires context:delete');
180
+ },
181
+
182
+ 'delete() on missing key does nothing': async ({pass, fail}) => {
183
+ const { container, el } = await createContext();
184
+ let fired = false;
185
+ el.addEventListener('context:delete', () => { fired = true; });
186
+ el.delete('nonexistent');
187
+ if(fired){
188
+ cleanup(container);
189
+ return fail('context:delete should not fire for missing key');
190
+ }
191
+ cleanup(container);
192
+ pass('delete() on missing key is a no-op');
193
+ },
194
+
195
+ /*
196
+ clear()
197
+ */
198
+ 'clear() removes all keys and fires context:delete for each': async ({pass, fail}) => {
199
+ const { container, el } = await createContext();
200
+ el.set('a', 1);
201
+ el.set('b', 2);
202
+ const deleted = [];
203
+ el.addEventListener('context:delete', e => deleted.push(e.detail.key));
204
+ el.clear();
205
+ if(Object.keys(el.getData()).length !== 0){
206
+ cleanup(container);
207
+ return fail('getData() should return empty object after clear()');
208
+ }
209
+ if(deleted.length !== 2 || !deleted.includes('a') || !deleted.includes('b')){
210
+ cleanup(container);
211
+ return fail(`Expected context:delete for both keys, got: ${JSON.stringify(deleted)}`);
212
+ }
213
+ cleanup(container);
214
+ pass('clear() removes all keys');
215
+ },
216
+
217
+ /*
218
+ getData()
219
+ */
220
+ 'getData() returns a copy of all data': async ({pass, fail}) => {
221
+ const { container, el } = await createContext();
222
+ el.set('x', 10);
223
+ el.set('y', 20);
224
+ const data = el.getData();
225
+ if(data.x !== 10 || data.y !== 20){
226
+ cleanup(container);
227
+ return fail(`Expected {x:10,y:20}, got ${JSON.stringify(data)}`);
228
+ }
229
+ // ensure it's a copy
230
+ data.x = 999;
231
+ if(el.get('x') !== 10){
232
+ cleanup(container);
233
+ return fail('getData() should return a copy, not a reference');
234
+ }
235
+ cleanup(container);
236
+ pass('getData() returns a shallow copy of all data');
237
+ }
238
+ };
@@ -1115,5 +1115,137 @@ export default {
1115
1115
  }
1116
1116
 
1117
1117
  pass('Table has static editors');
1118
+ },
1119
+
1120
+ /*
1121
+ Non-Editable Field Tests
1122
+ */
1123
+ 'should not create input for editable:false field when editing': async ({pass, fail}) => {
1124
+ const table = await createTable();
1125
+ table.setData({
1126
+ fields: [
1127
+ { name: 'id', label: 'ID', editable: false },
1128
+ { name: 'name', label: 'Name' },
1129
+ { name: 'age', label: 'Age' }
1130
+ ],
1131
+ records: [...sampleRecords]
1132
+ });
1133
+ await table.updateComplete;
1134
+
1135
+ const record = table.records[0];
1136
+ table.editRecord(record);
1137
+
1138
+ const idCell = table.shadowRoot.querySelector(`.record[data-index="0"] .cell[data-field="id"]`);
1139
+ if(!idCell){
1140
+ cleanup(table);
1141
+ fail('Could not find id cell');
1142
+ return;
1143
+ }
1144
+
1145
+ const input = idCell.querySelector('input, select');
1146
+ if(input){
1147
+ cleanup(table);
1148
+ fail('Non-editable field should not have an input in edit mode');
1149
+ return;
1150
+ }
1151
+
1152
+ cleanup(table);
1153
+ pass('editable:false field does not show input in edit mode');
1154
+ },
1155
+
1156
+ 'should preserve editable:false field value when saving': async ({pass, fail}) => {
1157
+ const table = await createTable();
1158
+ table.setData({
1159
+ fields: [
1160
+ { name: 'id', label: 'ID', editable: false },
1161
+ { name: 'name', label: 'Name' },
1162
+ { name: 'age', label: 'Age' }
1163
+ ],
1164
+ records: [...sampleRecords]
1165
+ });
1166
+ await table.updateComplete;
1167
+
1168
+ const record = table.records[0];
1169
+ const originalId = record.id;
1170
+ table.editRecord(record);
1171
+
1172
+ const nameCell = table.shadowRoot.querySelector(`.record[data-index="0"] .cell[data-field="name"]`);
1173
+ const nameInput = nameCell ? nameCell.querySelector('input') : null;
1174
+ if(nameInput) nameInput.value = 'Updated Name';
1175
+
1176
+ table.saveEditedRecord(record);
1177
+ await table.updateComplete;
1178
+
1179
+ if(record.id !== originalId){
1180
+ cleanup(table);
1181
+ fail(`Non-editable field 'id' should not change: expected ${originalId}, got ${record.id}`);
1182
+ return;
1183
+ }
1184
+
1185
+ cleanup(table);
1186
+ pass('editable:false field value is preserved after save');
1187
+ },
1188
+
1189
+ 'should display value for editable:false field during edit mode': async ({pass, fail}) => {
1190
+ const table = await createTable();
1191
+ table.setData({
1192
+ fields: [
1193
+ { name: 'id', label: 'ID', editable: false },
1194
+ { name: 'name', label: 'Name' }
1195
+ ],
1196
+ records: [...sampleRecords]
1197
+ });
1198
+ await table.updateComplete;
1199
+
1200
+ const record = table.records[0];
1201
+ table.editRecord(record);
1202
+
1203
+ const idCell = table.shadowRoot.querySelector(`.record[data-index="0"] .cell[data-field="id"]`);
1204
+ if(!idCell){
1205
+ cleanup(table);
1206
+ fail('Could not find id cell');
1207
+ return;
1208
+ }
1209
+
1210
+ if(!idCell.textContent.trim()){
1211
+ cleanup(table);
1212
+ fail('Non-editable field cell should display its value during edit mode');
1213
+ return;
1214
+ }
1215
+
1216
+ cleanup(table);
1217
+ pass('editable:false field displays its value during edit mode');
1218
+ },
1219
+
1220
+ 'should treat fields without editable property as editable by default': async ({pass, fail}) => {
1221
+ const table = await createTable();
1222
+ table.setData({
1223
+ fields: [
1224
+ { name: 'id', label: 'ID' },
1225
+ { name: 'name', label: 'Name' }
1226
+ ],
1227
+ records: [...sampleRecords]
1228
+ });
1229
+ await table.updateComplete;
1230
+
1231
+ const record = table.records[0];
1232
+ table.editRecord(record);
1233
+
1234
+ const idCell = table.shadowRoot.querySelector(`.record[data-index="0"] .cell[data-field="id"]`);
1235
+ if(!idCell){
1236
+ cleanup(table);
1237
+ fail('Could not find id cell');
1238
+ return;
1239
+ }
1240
+
1241
+ const input = idCell.querySelector('input, select');
1242
+ if(!input){
1243
+ cleanup(table);
1244
+ fail('Field without editable property should default to editable (have an input)');
1245
+ return;
1246
+ }
1247
+
1248
+ cleanup(table);
1249
+ pass('Field without editable property defaults to editable');
1118
1250
  }
1119
1251
  };