kempo-ui 0.0.40 → 0.0.41

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.
@@ -0,0 +1,557 @@
1
+ import Dropdown from '../../src/components/Dropdown.js';
2
+
3
+ const createDropdown = async (options = {}) => {
4
+ const container = document.createElement('div');
5
+ container.innerHTML = `
6
+ <k-dropdown
7
+ ${options.openDirection ? `open-direction="${options.openDirection}"` : ''}
8
+ ${options.opened ? 'opened' : ''}
9
+ ${options.closeOnSelect === false ? 'close-on-select="false"' : ''}
10
+ ${options.closeOnClickOutside === false ? 'close-on-click-outside="false"' : ''}
11
+ >
12
+ <button slot="trigger">Trigger</button>
13
+ <button data-value="item1">Item 1</button>
14
+ <button data-value="item2">Item 2</button>
15
+ <button data-value="item3">Item 3</button>
16
+ ${options.withDisabled ? '<button data-value="disabled" disabled>Disabled</button>' : ''}
17
+ ${options.withDivider ? '<hr /><button data-value="item4">Item 4</button>' : ''}
18
+ ${options.withLinks ? '<a href="#link1">Link 1</a><a href="#link2">Link 2</a>' : ''}
19
+ </k-dropdown>
20
+ `;
21
+ document.body.appendChild(container);
22
+ const dropdown = container.querySelector('k-dropdown');
23
+ await dropdown.updateComplete;
24
+ return { container, dropdown };
25
+ };
26
+
27
+ const cleanup = (container) => {
28
+ if(container && container.parentNode) {
29
+ container.parentNode.removeChild(container);
30
+ }
31
+ };
32
+
33
+ export default {
34
+ /*
35
+ Element Creation
36
+ */
37
+ 'should create dropdown element': async ({pass, fail}) => {
38
+ const { container, dropdown } = await createDropdown();
39
+ if(!dropdown) {
40
+ cleanup(container);
41
+ return fail('Dropdown element should be created');
42
+ }
43
+ if(!(dropdown instanceof Dropdown)) {
44
+ cleanup(container);
45
+ return fail('Element should be instance of Dropdown');
46
+ }
47
+ cleanup(container);
48
+ pass('Dropdown element created correctly');
49
+ },
50
+
51
+ 'should have shadow root': async ({pass, fail}) => {
52
+ const { container, dropdown } = await createDropdown();
53
+ if(!dropdown.shadowRoot) {
54
+ cleanup(container);
55
+ return fail('Dropdown should have shadow root');
56
+ }
57
+ cleanup(container);
58
+ pass('Dropdown has shadow root');
59
+ },
60
+
61
+ 'should be closed by default': async ({pass, fail}) => {
62
+ const { container, dropdown } = await createDropdown();
63
+ if(dropdown.opened !== false) {
64
+ cleanup(container);
65
+ return fail(`Expected opened to be false, got ${dropdown.opened}`);
66
+ }
67
+ cleanup(container);
68
+ pass('Dropdown is closed by default');
69
+ },
70
+
71
+ 'should have default openDirection of down left': async ({pass, fail}) => {
72
+ const { container, dropdown } = await createDropdown();
73
+ if(dropdown.openDirection !== 'down left') {
74
+ cleanup(container);
75
+ return fail(`Expected openDirection to be 'down left', got ${dropdown.openDirection}`);
76
+ }
77
+ cleanup(container);
78
+ pass('Default openDirection is down left');
79
+ },
80
+
81
+ /*
82
+ Property Reflection
83
+ */
84
+ 'should reflect opened attribute': async ({pass, fail}) => {
85
+ const { container, dropdown } = await createDropdown({ opened: true });
86
+ if(!dropdown.hasAttribute('opened')) {
87
+ cleanup(container);
88
+ return fail('Dropdown should have opened attribute');
89
+ }
90
+ cleanup(container);
91
+ pass('Opened attribute reflects correctly');
92
+ },
93
+
94
+ 'should reflect open-direction attribute': async ({pass, fail}) => {
95
+ const { container, dropdown } = await createDropdown({ openDirection: 'up right' });
96
+ if(dropdown.getAttribute('open-direction') !== 'up right') {
97
+ cleanup(container);
98
+ return fail('open-direction attribute should be up right');
99
+ }
100
+ cleanup(container);
101
+ pass('open-direction attribute reflects correctly');
102
+ },
103
+
104
+ /*
105
+ Public Methods - open()
106
+ */
107
+ 'should have open method': async ({pass, fail}) => {
108
+ const { container, dropdown } = await createDropdown();
109
+ if(typeof dropdown.open !== 'function') {
110
+ cleanup(container);
111
+ return fail('Dropdown should have open method');
112
+ }
113
+ cleanup(container);
114
+ pass('Dropdown has open method');
115
+ },
116
+
117
+ 'open() should set opened to true': async ({pass, fail}) => {
118
+ const { container, dropdown } = await createDropdown();
119
+ dropdown.open();
120
+ if(dropdown.opened !== true) {
121
+ cleanup(container);
122
+ return fail(`Expected opened to be true after open(), got ${dropdown.opened}`);
123
+ }
124
+ cleanup(container);
125
+ pass('open() sets opened to true');
126
+ },
127
+
128
+ 'open() should return this for chaining': async ({pass, fail}) => {
129
+ const { container, dropdown } = await createDropdown();
130
+ const result = dropdown.open();
131
+ if(result !== dropdown) {
132
+ cleanup(container);
133
+ return fail('open() should return this');
134
+ }
135
+ cleanup(container);
136
+ pass('open() returns this');
137
+ },
138
+
139
+ /*
140
+ Public Methods - close()
141
+ */
142
+ 'should have close method': async ({pass, fail}) => {
143
+ const { container, dropdown } = await createDropdown();
144
+ if(typeof dropdown.close !== 'function') {
145
+ cleanup(container);
146
+ return fail('Dropdown should have close method');
147
+ }
148
+ cleanup(container);
149
+ pass('Dropdown has close method');
150
+ },
151
+
152
+ 'close() should set opened to false': async ({pass, fail}) => {
153
+ const { container, dropdown } = await createDropdown({ opened: true });
154
+ dropdown.close();
155
+ if(dropdown.opened !== false) {
156
+ cleanup(container);
157
+ return fail(`Expected opened to be false after close(), got ${dropdown.opened}`);
158
+ }
159
+ cleanup(container);
160
+ pass('close() sets opened to false');
161
+ },
162
+
163
+ 'close() should return this for chaining': async ({pass, fail}) => {
164
+ const { container, dropdown } = await createDropdown();
165
+ const result = dropdown.close();
166
+ if(result !== dropdown) {
167
+ cleanup(container);
168
+ return fail('close() should return this');
169
+ }
170
+ cleanup(container);
171
+ pass('close() returns this');
172
+ },
173
+
174
+ /*
175
+ Public Methods - toggle()
176
+ */
177
+ 'should have toggle method': async ({pass, fail}) => {
178
+ const { container, dropdown } = await createDropdown();
179
+ if(typeof dropdown.toggle !== 'function') {
180
+ cleanup(container);
181
+ return fail('Dropdown should have toggle method');
182
+ }
183
+ cleanup(container);
184
+ pass('Dropdown has toggle method');
185
+ },
186
+
187
+ 'toggle() should open when closed': async ({pass, fail}) => {
188
+ const { container, dropdown } = await createDropdown();
189
+ dropdown.toggle();
190
+ if(dropdown.opened !== true) {
191
+ cleanup(container);
192
+ return fail(`Expected opened to be true after toggle(), got ${dropdown.opened}`);
193
+ }
194
+ cleanup(container);
195
+ pass('toggle() opens when closed');
196
+ },
197
+
198
+ 'toggle() should close when open': async ({pass, fail}) => {
199
+ const { container, dropdown } = await createDropdown({ opened: true });
200
+ dropdown.toggle();
201
+ if(dropdown.opened !== false) {
202
+ cleanup(container);
203
+ return fail(`Expected opened to be false after toggle(), got ${dropdown.opened}`);
204
+ }
205
+ cleanup(container);
206
+ pass('toggle() closes when open');
207
+ },
208
+
209
+ /*
210
+ Events
211
+ */
212
+ 'should dispatch opened event when opening': async ({pass, fail}) => {
213
+ const { container, dropdown } = await createDropdown();
214
+ let eventFired = false;
215
+ dropdown.addEventListener('opened', () => { eventFired = true; });
216
+ dropdown.open();
217
+ await dropdown.updateComplete;
218
+ if(!eventFired) {
219
+ cleanup(container);
220
+ return fail('opened event should be fired');
221
+ }
222
+ cleanup(container);
223
+ pass('opened event dispatched');
224
+ },
225
+
226
+ 'should dispatch closed event when closing': async ({pass, fail}) => {
227
+ const { container, dropdown } = await createDropdown({ opened: true });
228
+ let eventFired = false;
229
+ dropdown.addEventListener('closed', () => { eventFired = true; });
230
+ dropdown.close();
231
+ await dropdown.updateComplete;
232
+ if(!eventFired) {
233
+ cleanup(container);
234
+ return fail('closed event should be fired');
235
+ }
236
+ cleanup(container);
237
+ pass('closed event dispatched');
238
+ },
239
+
240
+ 'should dispatch select event when button item clicked': async ({pass, fail}) => {
241
+ const { container, dropdown } = await createDropdown({ opened: true });
242
+ let eventFired = false;
243
+ let eventDetail = null;
244
+ dropdown.addEventListener('select', e => {
245
+ eventFired = true;
246
+ eventDetail = e.detail;
247
+ });
248
+ const item = dropdown.querySelector('button[data-value="item1"]');
249
+ item.click();
250
+ if(!eventFired) {
251
+ cleanup(container);
252
+ return fail('select event should be fired');
253
+ }
254
+ if(eventDetail.value !== 'item1') {
255
+ cleanup(container);
256
+ return fail(`Expected value to be 'item1', got ${eventDetail.value}`);
257
+ }
258
+ cleanup(container);
259
+ pass('select event dispatched with correct value');
260
+ },
261
+
262
+ 'select event should include item reference': async ({pass, fail}) => {
263
+ const { container, dropdown } = await createDropdown({ opened: true });
264
+ let eventDetail = null;
265
+ dropdown.addEventListener('select', e => { eventDetail = e.detail; });
266
+ const item = dropdown.querySelector('button[data-value="item2"]');
267
+ item.click();
268
+ if(eventDetail.item !== item) {
269
+ cleanup(container);
270
+ return fail('Event detail should include item reference');
271
+ }
272
+ cleanup(container);
273
+ pass('select event includes item reference');
274
+ },
275
+
276
+ /*
277
+ Close Behavior
278
+ */
279
+ 'should close on select by default': async ({pass, fail}) => {
280
+ const { container, dropdown } = await createDropdown({ opened: true });
281
+ const item = dropdown.querySelector('button[data-value="item1"]');
282
+ item.click();
283
+ if(dropdown.opened !== false) {
284
+ cleanup(container);
285
+ return fail('Dropdown should close after selection');
286
+ }
287
+ cleanup(container);
288
+ pass('Dropdown closes on select');
289
+ },
290
+
291
+ 'should not close on select when close-on-select is false': async ({pass, fail}) => {
292
+ const { container, dropdown } = await createDropdown({ opened: true, closeOnSelect: false });
293
+ const item = dropdown.querySelector('button[data-value="item1"]');
294
+ item.click();
295
+ if(dropdown.opened !== true) {
296
+ cleanup(container);
297
+ return fail('Dropdown should remain open when close-on-select is false');
298
+ }
299
+ cleanup(container);
300
+ pass('Dropdown stays open when close-on-select is false');
301
+ },
302
+
303
+ 'should close on Escape key': async ({pass, fail}) => {
304
+ const { container, dropdown } = await createDropdown({ opened: true });
305
+ const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true });
306
+ document.dispatchEvent(event);
307
+ if(dropdown.opened !== false) {
308
+ cleanup(container);
309
+ return fail('Dropdown should close on Escape');
310
+ }
311
+ cleanup(container);
312
+ pass('Dropdown closes on Escape');
313
+ },
314
+
315
+ /*
316
+ Trigger Click
317
+ */
318
+ 'should toggle on trigger click': async ({pass, fail}) => {
319
+ const { container, dropdown } = await createDropdown();
320
+ const trigger = dropdown.querySelector('[slot="trigger"]');
321
+ trigger.click();
322
+ if(dropdown.opened !== true) {
323
+ cleanup(container);
324
+ return fail('Dropdown should open on trigger click');
325
+ }
326
+ cleanup(container);
327
+ pass('Dropdown opens on trigger click');
328
+ },
329
+
330
+ /*
331
+ Slotted Content
332
+ */
333
+ 'should style slotted buttons': async ({pass, fail}) => {
334
+ const { container, dropdown } = await createDropdown();
335
+ const buttons = dropdown.querySelectorAll('button:not([slot])');
336
+ if(buttons.length !== 3) {
337
+ cleanup(container);
338
+ return fail(`Expected 3 button items, got ${buttons.length}`);
339
+ }
340
+ cleanup(container);
341
+ pass('Slotted buttons exist');
342
+ },
343
+
344
+ 'should style slotted links': async ({pass, fail}) => {
345
+ const { container, dropdown } = await createDropdown({ withLinks: true });
346
+ const links = dropdown.querySelectorAll('a');
347
+ if(links.length !== 2) {
348
+ cleanup(container);
349
+ return fail(`Expected 2 link items, got ${links.length}`);
350
+ }
351
+ cleanup(container);
352
+ pass('Slotted links exist');
353
+ },
354
+
355
+ 'should style slotted hr as divider': async ({pass, fail}) => {
356
+ const { container, dropdown } = await createDropdown({ withDivider: true });
357
+ const divider = dropdown.querySelector('hr');
358
+ if(!divider) {
359
+ cleanup(container);
360
+ return fail('HR divider should exist');
361
+ }
362
+ cleanup(container);
363
+ pass('HR divider exists');
364
+ },
365
+
366
+ /*
367
+ Keyboard Navigation
368
+ */
369
+ 'ArrowDown should move focus to next item': async ({pass, fail}) => {
370
+ const { container, dropdown } = await createDropdown({ opened: true });
371
+ const items = dropdown.querySelectorAll('button:not([slot])');
372
+ items[0].focus();
373
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true });
374
+ document.dispatchEvent(event);
375
+ if(document.activeElement !== items[1]) {
376
+ cleanup(container);
377
+ return fail('Focus should move to next item');
378
+ }
379
+ cleanup(container);
380
+ pass('ArrowDown moves focus to next item');
381
+ },
382
+
383
+ 'ArrowUp should move focus to previous item': async ({pass, fail}) => {
384
+ const { container, dropdown } = await createDropdown({ opened: true });
385
+ const items = dropdown.querySelectorAll('button:not([slot])');
386
+ items[1].focus();
387
+ const event = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true });
388
+ document.dispatchEvent(event);
389
+ if(document.activeElement !== items[0]) {
390
+ cleanup(container);
391
+ return fail('Focus should move to previous item');
392
+ }
393
+ cleanup(container);
394
+ pass('ArrowUp moves focus to previous item');
395
+ },
396
+
397
+ 'ArrowDown should wrap to first item': async ({pass, fail}) => {
398
+ const { container, dropdown } = await createDropdown({ opened: true });
399
+ const items = dropdown.querySelectorAll('button:not([slot])');
400
+ items[2].focus();
401
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true });
402
+ document.dispatchEvent(event);
403
+ if(document.activeElement !== items[0]) {
404
+ cleanup(container);
405
+ return fail('Focus should wrap to first item');
406
+ }
407
+ cleanup(container);
408
+ pass('ArrowDown wraps to first item');
409
+ },
410
+
411
+ 'ArrowUp should wrap to last item': async ({pass, fail}) => {
412
+ const { container, dropdown } = await createDropdown({ opened: true });
413
+ const items = dropdown.querySelectorAll('button:not([slot])');
414
+ items[0].focus();
415
+ const event = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true });
416
+ document.dispatchEvent(event);
417
+ if(document.activeElement !== items[2]) {
418
+ cleanup(container);
419
+ return fail('Focus should wrap to last item');
420
+ }
421
+ cleanup(container);
422
+ pass('ArrowUp wraps to last item');
423
+ },
424
+
425
+ /*
426
+ Shadow DOM Structure
427
+ */
428
+ 'should render trigger slot': async ({pass, fail}) => {
429
+ const { container, dropdown } = await createDropdown();
430
+ const triggerSlot = dropdown.shadowRoot.querySelector('slot[name="trigger"]');
431
+ if(!triggerSlot) {
432
+ cleanup(container);
433
+ return fail('Trigger slot should be rendered');
434
+ }
435
+ cleanup(container);
436
+ pass('Trigger slot rendered');
437
+ },
438
+
439
+ 'should render menu container': async ({pass, fail}) => {
440
+ const { container, dropdown } = await createDropdown();
441
+ const menu = dropdown.shadowRoot.querySelector('#menu');
442
+ if(!menu) {
443
+ cleanup(container);
444
+ return fail('Menu container should be rendered');
445
+ }
446
+ cleanup(container);
447
+ pass('Menu container rendered');
448
+ },
449
+
450
+ 'menu should have role menu': async ({pass, fail}) => {
451
+ const { container, dropdown } = await createDropdown();
452
+ const menu = dropdown.shadowRoot.querySelector('#menu');
453
+ if(menu.getAttribute('role') !== 'menu') {
454
+ cleanup(container);
455
+ return fail('Menu should have role menu');
456
+ }
457
+ cleanup(container);
458
+ pass('Menu has correct role');
459
+ },
460
+
461
+ /*
462
+ Method Chaining
463
+ */
464
+ 'should support method chaining': async ({pass, fail}) => {
465
+ const { container, dropdown } = await createDropdown();
466
+ dropdown.open().close().toggle();
467
+ if(dropdown.opened !== true) {
468
+ cleanup(container);
469
+ return fail('Expected opened to be true after chain');
470
+ }
471
+ cleanup(container);
472
+ pass('Method chaining works correctly');
473
+ },
474
+
475
+ /*
476
+ Multiple Dropdowns
477
+ */
478
+ 'clicking another dropdown trigger should close this one': async ({pass, fail}) => {
479
+ const container1 = document.createElement('div');
480
+ container1.innerHTML = `
481
+ <k-dropdown id="dd1">
482
+ <button slot="trigger">Dropdown 1</button>
483
+ <button data-value="1">Item 1</button>
484
+ </k-dropdown>
485
+ `;
486
+ const container2 = document.createElement('div');
487
+ container2.innerHTML = `
488
+ <k-dropdown id="dd2">
489
+ <button slot="trigger">Dropdown 2</button>
490
+ <button data-value="2">Item 2</button>
491
+ </k-dropdown>
492
+ `;
493
+ document.body.appendChild(container1);
494
+ document.body.appendChild(container2);
495
+ const dd1 = container1.querySelector('k-dropdown');
496
+ const dd2 = container2.querySelector('k-dropdown');
497
+ await dd1.updateComplete;
498
+ await dd2.updateComplete;
499
+
500
+ dd1.open();
501
+ await dd1.updateComplete;
502
+
503
+ if(!dd1.opened) {
504
+ cleanup(container1);
505
+ cleanup(container2);
506
+ return fail('Dropdown 1 should be open');
507
+ }
508
+
509
+ const trigger2 = dd2.querySelector('[slot="trigger"]');
510
+ trigger2.click();
511
+ await dd2.updateComplete;
512
+
513
+ if(dd1.opened) {
514
+ cleanup(container1);
515
+ cleanup(container2);
516
+ return fail('Dropdown 1 should be closed after clicking dd2 trigger');
517
+ }
518
+
519
+ cleanup(container1);
520
+ cleanup(container2);
521
+ pass('Opening another dropdown closes the first');
522
+ },
523
+
524
+ /*
525
+ Open Direction Attribute
526
+ */
527
+ 'should accept various open-direction values': async ({pass, fail}) => {
528
+ const directions = ['down left', 'down right', 'up left', 'up right', 'left up', 'left down', 'right up', 'right down'];
529
+ for(const dir of directions) {
530
+ const { container, dropdown } = await createDropdown({ openDirection: dir });
531
+ if(dropdown.openDirection !== dir) {
532
+ cleanup(container);
533
+ return fail(`Expected openDirection to be '${dir}', got ${dropdown.openDirection}`);
534
+ }
535
+ cleanup(container);
536
+ }
537
+ pass('All open-direction values accepted');
538
+ },
539
+
540
+ /*
541
+ Disabled Items
542
+ */
543
+ 'disabled items should be skipped in keyboard navigation': async ({pass, fail}) => {
544
+ const { container, dropdown } = await createDropdown({ opened: true, withDisabled: true });
545
+ const items = dropdown.querySelectorAll('button:not([slot]):not([disabled])');
546
+ items[2].focus(); // item3
547
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true });
548
+ document.dispatchEvent(event);
549
+ // Should skip disabled and wrap to first
550
+ if(document.activeElement !== items[0]) {
551
+ cleanup(container);
552
+ return fail('Focus should skip disabled item and wrap');
553
+ }
554
+ cleanup(container);
555
+ pass('Disabled items skipped in navigation');
556
+ }
557
+ };
@@ -26,10 +26,16 @@ export default {
26
26
  return;
27
27
  }
28
28
 
29
- const stylesheetLink = component.shadowRoot.querySelector('link[href="/kempo.min.css"]');
29
+ const stylesheetLink = component.shadowRoot.querySelector('link[rel="stylesheet"]');
30
30
  if(!stylesheetLink) {
31
31
  document.body.removeChild(component);
32
- fail('Shadow root should contain kempo.min.css link');
32
+ fail('Shadow root should contain stylesheet link');
33
+ return;
34
+ }
35
+
36
+ if(!stylesheetLink.href.includes('kempo')) {
37
+ document.body.removeChild(component);
38
+ fail('Stylesheet should reference kempo css');
33
39
  return;
34
40
  }
35
41
 
@@ -22,16 +22,16 @@ export default {
22
22
  return;
23
23
  }
24
24
 
25
- const stylesheetLink = component.shadowRoot.querySelector('link[href="/kempo.min.css"]');
25
+ const stylesheetLink = component.shadowRoot.querySelector('link[rel="stylesheet"]');
26
26
  if(!stylesheetLink) {
27
27
  document.body.removeChild(component);
28
- fail('Shadow root should contain kempo.min.css link');
28
+ fail('Shadow root should contain stylesheet link');
29
29
  return;
30
30
  }
31
31
 
32
- if(stylesheetLink.rel !== 'stylesheet') {
32
+ if(!stylesheetLink.href.includes('kempo')) {
33
33
  document.body.removeChild(component);
34
- fail('Link should have rel="stylesheet"');
34
+ fail('Stylesheet should reference kempo css');
35
35
  return;
36
36
  }
37
37
 
@@ -100,7 +100,7 @@ export default {
100
100
 
101
101
  await new Promise(resolve => setTimeout(resolve, 0));
102
102
 
103
- const stylesheetBefore = component.shadowRoot.querySelector('link[href="/kempo.min.css"]');
103
+ const stylesheetBefore = component.shadowRoot.querySelector('link[rel="stylesheet"]');
104
104
  if(!stylesheetBefore) {
105
105
  document.body.removeChild(component);
106
106
  fail('Stylesheet should exist before re-render');
@@ -110,7 +110,7 @@ export default {
110
110
  component.requestUpdate();
111
111
  await component.updateComplete;
112
112
 
113
- const stylesheetAfter = component.shadowRoot.querySelector('link[href="/kempo.min.css"]');
113
+ const stylesheetAfter = component.shadowRoot.querySelector('link[rel="stylesheet"]');
114
114
  if(!stylesheetAfter) {
115
115
  document.body.removeChild(component);
116
116
  fail('Stylesheet should persist after re-render');