@nyaruka/temba-components 0.123.0 → 0.124.1

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 (146) hide show
  1. package/.github/copilot-instructions.md +22 -4
  2. package/CHANGELOG.md +21 -0
  3. package/TEST_OPTIMIZATION.md +158 -0
  4. package/demo/alert/example.html +65 -0
  5. package/demo/button/example.html +71 -0
  6. package/demo/chart/example.html +56 -0
  7. package/demo/checkbox/example.html +72 -0
  8. package/demo/compose/example.html +72 -0
  9. package/demo/data/images/gus.png +0 -0
  10. package/demo/data/images/purrington.jpg +0 -0
  11. package/demo/data/server/opened-tickets.json +40 -0
  12. package/demo/data/server/response-time.json +27 -0
  13. package/demo/datepicker/example.html +69 -0
  14. package/demo/dialog/example.html +107 -0
  15. package/demo/dropdown/example.html +99 -0
  16. package/demo/index.html +152 -430
  17. package/demo/misc/example.html +72 -0
  18. package/demo/progress/example.html +59 -0
  19. package/demo/select/drag-and-drop.html +142 -0
  20. package/demo/select/example.html +82 -0
  21. package/demo/select/multi.html +73 -0
  22. package/demo/slider/example.html +59 -0
  23. package/demo/sortable-list/example.html +99 -0
  24. package/demo/styles.css +183 -0
  25. package/demo/tabs/example.html +91 -0
  26. package/demo/textinput/completion.html +56 -0
  27. package/demo/textinput/example.html +61 -0
  28. package/dist/temba-components.js +323 -191
  29. package/dist/temba-components.js.map +1 -1
  30. package/out-tsc/src/chart/TembaChart.js +19 -16
  31. package/out-tsc/src/chart/TembaChart.js.map +1 -1
  32. package/out-tsc/src/fields/FieldManager.js +27 -34
  33. package/out-tsc/src/fields/FieldManager.js.map +1 -1
  34. package/out-tsc/src/flow/Editor.js +1 -1
  35. package/out-tsc/src/flow/Editor.js.map +1 -1
  36. package/out-tsc/src/list/SortableList.js +257 -60
  37. package/out-tsc/src/list/SortableList.js.map +1 -1
  38. package/out-tsc/src/omnibox/Omnibox.js +1 -1
  39. package/out-tsc/src/omnibox/Omnibox.js.map +1 -1
  40. package/out-tsc/src/select/Select.js +198 -38
  41. package/out-tsc/src/select/Select.js.map +1 -1
  42. package/out-tsc/src/thumbnail/Thumbnail.js +1 -1
  43. package/out-tsc/src/thumbnail/Thumbnail.js.map +1 -1
  44. package/out-tsc/src/webchat/WebChat.js +5 -2
  45. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  46. package/out-tsc/test/temba-chart.test.js +1 -1
  47. package/out-tsc/test/temba-chart.test.js.map +1 -1
  48. package/out-tsc/test/temba-compose.test.js +6 -30
  49. package/out-tsc/test/temba-compose.test.js.map +1 -1
  50. package/out-tsc/test/temba-contact-chat.test.js +1 -2
  51. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  52. package/out-tsc/test/temba-dropdown.test.js +1 -1
  53. package/out-tsc/test/temba-dropdown.test.js.map +1 -1
  54. package/out-tsc/test/temba-flow-editor-node.test.js +273 -0
  55. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -0
  56. package/out-tsc/test/temba-flow-editor.test.js +244 -0
  57. package/out-tsc/test/temba-flow-editor.test.js.map +1 -0
  58. package/out-tsc/test/temba-flow-plumber.test.js +145 -0
  59. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -0
  60. package/out-tsc/test/temba-flow-render.test.js +171 -0
  61. package/out-tsc/test/temba-flow-render.test.js.map +1 -0
  62. package/out-tsc/test/temba-omnibox.test.js +6 -3
  63. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  64. package/out-tsc/test/temba-select.test.js +183 -53
  65. package/out-tsc/test/temba-select.test.js.map +1 -1
  66. package/out-tsc/test/temba-sortable-list.test.js +91 -15
  67. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  68. package/out-tsc/test/temba-toast.test.js +1 -2
  69. package/out-tsc/test/temba-toast.test.js.map +1 -1
  70. package/out-tsc/test/temba-utils-index.test.js +2 -2
  71. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  72. package/out-tsc/test/temba-webchat-lightbox-fix.test.js +42 -0
  73. package/out-tsc/test/temba-webchat-lightbox-fix.test.js.map +1 -0
  74. package/out-tsc/test/utils.test.js +58 -0
  75. package/out-tsc/test/utils.test.js.map +1 -1
  76. package/package.json +2 -3
  77. package/screenshots/truth/flow/editor-basic.png +0 -0
  78. package/screenshots/truth/list/fields-dragging.png +0 -0
  79. package/screenshots/truth/list/sortable-dragging.png +0 -0
  80. package/screenshots/truth/list/sortable-dropped.png +0 -0
  81. package/screenshots/truth/list/sortable.png +0 -0
  82. package/screenshots/truth/omnibox/selected.png +0 -0
  83. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  84. package/screenshots/truth/select/disabled-selection.png +0 -0
  85. package/screenshots/truth/select/disabled.png +0 -0
  86. package/screenshots/truth/select/embedded.png +0 -0
  87. package/screenshots/truth/select/empty-options.png +0 -0
  88. package/screenshots/truth/select/expression-selected.png +0 -0
  89. package/screenshots/truth/select/expressions.png +0 -0
  90. package/screenshots/truth/select/functions.png +0 -0
  91. package/screenshots/truth/select/local-options.png +0 -0
  92. package/screenshots/truth/select/multi-reorder-final.png +0 -0
  93. package/screenshots/truth/select/multi-reorder-initial.png +0 -0
  94. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  95. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  96. package/screenshots/truth/select/remote-options.png +0 -0
  97. package/screenshots/truth/select/search-enabled.png +0 -0
  98. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  99. package/screenshots/truth/select/search-selected-focus.png +0 -0
  100. package/screenshots/truth/select/search-selected.png +0 -0
  101. package/screenshots/truth/select/search-with-selected.png +0 -0
  102. package/screenshots/truth/select/searching.png +0 -0
  103. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  104. package/screenshots/truth/select/selected-multi.png +0 -0
  105. package/screenshots/truth/select/selected-single.png +0 -0
  106. package/screenshots/truth/select/selection-clearable.png +0 -0
  107. package/screenshots/truth/select/static-initial-value.png +0 -0
  108. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  109. package/screenshots/truth/select/truncated-selection.png +0 -0
  110. package/screenshots/truth/select/with-placeholder.png +0 -0
  111. package/screenshots/truth/select/without-placeholder.png +0 -0
  112. package/screenshots/truth/templates/default.png +0 -0
  113. package/screenshots/truth/templates/unapproved.png +0 -0
  114. package/screenshots/truth/webchat/connected-state.png +0 -0
  115. package/src/chart/TembaChart.ts +20 -16
  116. package/src/fields/FieldManager.ts +30 -38
  117. package/src/flow/Editor.ts +1 -1
  118. package/src/list/SortableList.ts +291 -67
  119. package/src/omnibox/Omnibox.ts +1 -1
  120. package/src/select/Select.ts +213 -42
  121. package/src/thumbnail/Thumbnail.ts +1 -1
  122. package/src/webchat/WebChat.ts +5 -2
  123. package/test/temba-chart.test.ts +1 -1
  124. package/test/temba-compose.test.ts +11 -38
  125. package/test/temba-contact-chat.test.ts +4 -6
  126. package/test/temba-dropdown.test.ts +1 -1
  127. package/test/temba-flow-editor-node.test.ts +344 -0
  128. package/test/temba-flow-editor.test.ts +301 -0
  129. package/test/temba-flow-plumber.test.ts +189 -0
  130. package/test/temba-flow-render.test.ts +220 -0
  131. package/test/temba-omnibox.test.ts +7 -3
  132. package/test/temba-select.test.ts +247 -79
  133. package/test/temba-sortable-list.test.ts +108 -15
  134. package/test/temba-toast.test.ts +2 -2
  135. package/test/temba-utils-index.test.ts +2 -2
  136. package/test/temba-webchat-lightbox-fix.test.ts +57 -0
  137. package/test/utils.test.ts +88 -0
  138. package/web-test-runner.config.mjs +4 -2
  139. package/.storybook/main.js +0 -14
  140. package/.storybook/preview-head.html +0 -1
  141. package/.storybook/preview.js +0 -17
  142. package/demo/agents.html +0 -147
  143. package/demo/old.html +0 -573
  144. package/demo/remote.html +0 -3
  145. package/screenshots/truth/compose/attachments-with-files-focused.png +0 -0
  146. package/stories/temba-checkbox.stories.md +0 -37
@@ -1,16 +1,16 @@
1
- import * as sinon from 'sinon';
1
+ import Sinon, * as sinon from 'sinon';
2
2
  import { fixture, expect, assert } from '@open-wc/testing';
3
3
  import { useFakeTimers } from 'sinon';
4
4
  import { Options } from '../src/options/Options';
5
5
  import { Select, SelectOption } from '../src/select/Select';
6
6
  import {
7
7
  assertScreenshot,
8
- checkTimers,
9
8
  getClip,
9
+ getOptions,
10
10
  loadStore,
11
- mouseClickElement
11
+ openAndClick,
12
+ openSelect
12
13
  } from './utils.test';
13
- import { CustomEventType } from '../src/interfaces';
14
14
 
15
15
  const colors = [
16
16
  { name: 'Red', value: '0' },
@@ -20,7 +20,7 @@ const colors = [
20
20
 
21
21
  export const createSelect = async (clock, def: string) => {
22
22
  const parentNode = document.createElement('div');
23
- parentNode.setAttribute('style', 'width: 250px;');
23
+ parentNode.setAttribute('style', 'width: 400px;');
24
24
 
25
25
  const select: Select<SelectOption> = await fixture(def, { parentNode });
26
26
  clock.runAll();
@@ -28,66 +28,10 @@ export const createSelect = async (clock, def: string) => {
28
28
  return select;
29
29
  };
30
30
 
31
- export const open = async (clock, select: Select<SelectOption>) => {
32
- if (!select.endpoint) {
33
- await mouseClickElement(select);
34
- await clock.runAll();
35
- await clock.runAll();
36
- return select;
37
- }
38
-
39
- const promise = new Promise<Select<SelectOption>>((resolve) => {
40
- select.addEventListener(
41
- CustomEventType.FetchComplete,
42
- async () => {
43
- await clock.runAll();
44
- resolve(select);
45
- },
46
- { once: true }
47
- );
48
- });
49
-
50
- await mouseClickElement(select);
51
- await clock.runAll();
52
-
53
- return promise;
54
- };
55
-
56
31
  export const clear = (select: Select<SelectOption>) => {
57
32
  (select.shadowRoot.querySelector('.clear-button') as HTMLDivElement).click();
58
33
  };
59
34
 
60
- export const getOptions = (select: Select<SelectOption>): Options => {
61
- return select.shadowRoot.querySelector('temba-options[visible]');
62
- };
63
-
64
- export const clickOption = async (
65
- clock: any,
66
- select: Select<SelectOption>,
67
- index: number
68
- ) => {
69
- const options = getOptions(select);
70
- const option = options.shadowRoot.querySelector(
71
- `[data-option-index="${index}"]`
72
- ) as HTMLDivElement;
73
-
74
- await mouseClickElement(option);
75
- await options.updateComplete;
76
- await select.updateComplete;
77
- await clock.runAll();
78
-
79
- checkTimers(clock);
80
- };
81
-
82
- export const openAndClick = async (
83
- clock: any,
84
- select: Select<SelectOption>,
85
- index: number
86
- ) => {
87
- await open(clock, select);
88
- await clickOption(clock, select, index);
89
- };
90
-
91
35
  export const getSelectHTML = (
92
36
  options: SelectOption[] = colors,
93
37
  attrs: any = { placeholder: 'Select a color', name: 'color' },
@@ -143,7 +87,7 @@ const getClipWithOptions = (select: Select<any>) => {
143
87
  };
144
88
 
145
89
  describe('temba-select', () => {
146
- let clock: any;
90
+ let clock: Sinon.SinonFakeTimers;
147
91
  beforeEach(function () {
148
92
  clock = useFakeTimers();
149
93
  clock.tick(400);
@@ -190,7 +134,7 @@ describe('temba-select', () => {
190
134
  expect(select.disabled).to.equal(true);
191
135
 
192
136
  // make sure we can't select anymore
193
- await open(clock, select);
137
+ await openSelect(clock, select);
194
138
  expect(select.isOpen()).to.equal(false);
195
139
  await assertScreenshot('select/disabled-multi-selection', getClip(select));
196
140
  });
@@ -211,7 +155,7 @@ describe('temba-select', () => {
211
155
 
212
156
  it('shows options when opened', async () => {
213
157
  const select = await createSelect(clock, getSelectHTML());
214
- await open(clock, select);
158
+ await openSelect(clock, select);
215
159
  const options = getOptions(select);
216
160
  assert.instanceOf(options, Options);
217
161
 
@@ -241,7 +185,7 @@ describe('temba-select', () => {
241
185
  );
242
186
 
243
187
  // attempt to open the select with no options
244
- await open(clock, select);
188
+ await openSelect(clock, select);
245
189
 
246
190
  // should show options dropdown even though there are no options
247
191
  const options = getOptions(select);
@@ -297,13 +241,13 @@ describe('temba-select', () => {
297
241
  expect(select.values[0].name).to.equal('Green');
298
242
 
299
243
  // for single selection our current selection should be in the list and focused
300
- await open(clock, select);
244
+ await openSelect(clock, select);
301
245
  assert.equal(select.cursorIndex, 1);
302
246
  assert.equal(select.visibleOptions.length, 3);
303
247
 
304
248
  // now lets do a search, we should see our selection (green) and one other (red)
305
249
  await typeInto('temba-select', 're', false);
306
- await open(clock, select);
250
+ await openSelect(clock, select);
307
251
  assert.equal(select.visibleOptions.length, 2);
308
252
 
309
253
  await assertScreenshot(
@@ -369,7 +313,7 @@ describe('temba-select', () => {
369
313
  assert(changeEvent.called, 'change event not fired');
370
314
 
371
315
  changeEvent.resetHistory();
372
- await open(clock, select);
316
+ await openSelect(clock, select);
373
317
  assert.equal(select.visibleOptions.length, 0);
374
318
  assert(!changeEvent.called, 'change event should not be fired');
375
319
 
@@ -404,6 +348,230 @@ describe('temba-select', () => {
404
348
  });
405
349
  });
406
350
 
351
+ describe('drag and drop reordering', () => {
352
+ it('handles drag and drop with swap-based logic', async () => {
353
+ const select = await createSelect(
354
+ clock,
355
+ getSelectHTML(
356
+ [
357
+ { name: 'Red', value: '0', selected: true },
358
+ { name: 'Green', value: '1', selected: true },
359
+ { name: 'Blue', value: '2', selected: true }
360
+ ],
361
+ {
362
+ placeholder: 'Select colors',
363
+ multi: true
364
+ }
365
+ )
366
+ );
367
+
368
+ // Verify initial order: Red, Green, Blue
369
+ expect(select.values.length).to.equal(3);
370
+ expect(select.values[0].name).to.equal('Red');
371
+ expect(select.values[1].name).to.equal('Green');
372
+ expect(select.values[2].name).to.equal('Blue');
373
+
374
+ const sortableList = select.shadowRoot.querySelector(
375
+ 'temba-sortable-list'
376
+ );
377
+ expect(sortableList).to.not.be.null;
378
+
379
+ // Example 1: Pick up Blue (index 2), drop between Red and Green
380
+ // Expected result: Red, Blue, Green (swap [1,2])
381
+ const blueItem = sortableList.querySelector('#selected-2');
382
+ const greenItem = sortableList.querySelector('#selected-1');
383
+ expect(blueItem).to.not.be.null;
384
+ expect(greenItem).to.not.be.null;
385
+
386
+ const blueBounds = blueItem.getBoundingClientRect();
387
+ const greenBounds = greenItem.getBoundingClientRect();
388
+
389
+ // Start drag from Blue item
390
+ await moveMouse(blueBounds.left + 10, blueBounds.top + 10);
391
+ await mouseDown();
392
+
393
+ // Drag to position between Red and Green (left side of Green)
394
+ await moveMouse(greenBounds.left - 5, greenBounds.top + 10);
395
+ await waitFor(100);
396
+ await mouseUp();
397
+ clock.runAll();
398
+
399
+ // Verify result: Red, Blue, Green (Green and Blue swapped)
400
+ expect(select.values.length).to.equal(3);
401
+ expect(select.values[0].name).to.equal('Red');
402
+ expect(select.values[1].name).to.equal('Blue');
403
+ expect(select.values[2].name).to.equal('Green');
404
+
405
+ // Reset for next test
406
+ select.values = [
407
+ { name: 'Red', value: '0', selected: true },
408
+ { name: 'Green', value: '1', selected: true },
409
+ { name: 'Blue', value: '2', selected: true }
410
+ ];
411
+ await select.updateComplete;
412
+
413
+ // Example 2: Pick up Red (index 0), drop at end
414
+ // Expected result: Green, Blue, Red (swap [0,2])
415
+ const redItem = sortableList.querySelector('#selected-0');
416
+ const redBounds = redItem.getBoundingClientRect();
417
+ const blueItemBounds = sortableList
418
+ .querySelector('#selected-2')
419
+ .getBoundingClientRect();
420
+
421
+ // Start drag from Red item
422
+ await moveMouse(redBounds.left + 10, redBounds.top + 10);
423
+ await mouseDown();
424
+
425
+ // Drag to end position (right side of Blue)
426
+ await moveMouse(blueItemBounds.right + 5, blueItemBounds.top + 10);
427
+ await waitFor(100);
428
+ await mouseUp();
429
+ clock.runAll();
430
+
431
+ // Verify result: Green, Blue, Red (Red and Blue swapped)
432
+ expect(select.values.length).to.equal(3);
433
+ expect(select.values[0].name).to.equal('Green');
434
+ expect(select.values[1].name).to.equal('Blue');
435
+ expect(select.values[2].name).to.equal('Red');
436
+
437
+ // Reset for next test
438
+ select.values = [
439
+ { name: 'Red', value: '0', selected: true },
440
+ { name: 'Green', value: '1', selected: true },
441
+ { name: 'Blue', value: '2', selected: true }
442
+ ];
443
+ await select.updateComplete;
444
+
445
+ // Example 3: Pick up Green (index 1), drop at same position
446
+ // Expected result: No change, no event
447
+ const greenItemNew = sortableList.querySelector('#selected-1');
448
+ const greenBoundsNew = greenItemNew.getBoundingClientRect();
449
+
450
+ // Start drag from Green item
451
+ await moveMouse(greenBoundsNew.left + 10, greenBoundsNew.top + 10);
452
+ await mouseDown();
453
+
454
+ // Drag slightly but return to same position
455
+ await moveMouse(greenBoundsNew.left + 15, greenBoundsNew.top + 10);
456
+ await moveMouse(greenBoundsNew.left + 10, greenBoundsNew.top + 10);
457
+ await waitFor(100);
458
+ await mouseUp();
459
+ clock.runAll();
460
+
461
+ // Verify result: No change
462
+ expect(select.values.length).to.equal(3);
463
+ expect(select.values[0].name).to.equal('Red');
464
+ expect(select.values[1].name).to.equal('Green');
465
+ expect(select.values[2].name).to.equal('Blue');
466
+ });
467
+
468
+ it('does not show sortable list for single item', async () => {
469
+ const select = await createSelect(
470
+ clock,
471
+ getSelectHTML([{ name: 'Red', value: '0', selected: true }], {
472
+ placeholder: 'Select a color',
473
+ multi: true
474
+ })
475
+ );
476
+
477
+ // Should not have a sortable list with only one item
478
+ const sortableList = select.shadowRoot.querySelector(
479
+ 'temba-sortable-list'
480
+ );
481
+ expect(sortableList).to.be.null;
482
+
483
+ // Should still show the selected item normally
484
+ expect(select.values.length).to.equal(1);
485
+ expect(select.values[0].name).to.equal('Red');
486
+ });
487
+
488
+ it('does not show sortable list for non-multi select', async () => {
489
+ const select = await createSelect(
490
+ clock,
491
+ getSelectHTML([{ name: 'Red', value: '0', selected: true }], {
492
+ placeholder: 'Select a color',
493
+ multi: false
494
+ })
495
+ );
496
+
497
+ // Should not have a sortable list for single select
498
+ const sortableList = select.shadowRoot.querySelector(
499
+ 'temba-sortable-list'
500
+ );
501
+ expect(sortableList).to.be.null;
502
+
503
+ expect(select.values.length).to.equal(1);
504
+ expect(select.values[0].name).to.equal('Red');
505
+ });
506
+ });
507
+
508
+ describe('tags functionality', () => {
509
+ it('shows selected item text when typing second tag', async () => {
510
+ const select = await createSelect(
511
+ clock,
512
+ getSelectHTML([], {
513
+ placeholder: 'Enter tags',
514
+ multi: true,
515
+ searchable: true,
516
+ tags: true
517
+ })
518
+ );
519
+
520
+ // Add first tag programmatically (simulating user adding first tag)
521
+ select.addValue({ name: 'Yes', value: 'Yes' });
522
+ await select.updateComplete;
523
+ expect(select.values.length).to.equal(1);
524
+ expect(select.values[0].name).to.equal('Yes');
525
+
526
+ // Check that the first tag is displayed with text
527
+ let selectedItems = select.shadowRoot.querySelectorAll('.selected-item');
528
+ expect(selectedItems.length).to.equal(1);
529
+ expect(selectedItems[0].textContent).to.contain('Yes');
530
+
531
+ // Start typing second tag (this should not hide the first tag's text)
532
+ await typeInto('temba-select', 'No', false, false);
533
+
534
+ // Check that first tag text is still visible while typing second tag
535
+ selectedItems = select.shadowRoot.querySelectorAll('.selected-item');
536
+ expect(selectedItems.length).to.equal(1);
537
+
538
+ // The selected item should still contain the text "Yes"
539
+ const firstItemText = selectedItems[0].textContent;
540
+ expect(firstItemText).to.contain('Yes');
541
+ });
542
+
543
+ it('hides selected item text when typing in single-select mode', async () => {
544
+ const select = await createSelect(
545
+ clock,
546
+ getSelectHTML(colors, {
547
+ placeholder: 'Select a color',
548
+ searchable: true
549
+ })
550
+ );
551
+
552
+ // Select an option first
553
+ await openAndClick(clock, select, 0); // Select Red
554
+ expect(select.values.length).to.equal(1);
555
+ expect(select.values[0].name).to.equal('Red');
556
+
557
+ // Check that the selected item is displayed with text when not typing
558
+ let selectedItems = select.shadowRoot.querySelectorAll('.selected-item');
559
+ expect(selectedItems.length).to.equal(1);
560
+ expect(selectedItems[0].textContent).to.contain('Red');
561
+
562
+ // Start typing in the search box
563
+ await typeInto('temba-select', 'gr', false, false);
564
+
565
+ // Check that selected item text is hidden while typing (preserving single-select behavior)
566
+ selectedItems = select.shadowRoot.querySelectorAll('.selected-item');
567
+ expect(selectedItems.length).to.equal(1);
568
+
569
+ // The selected item should NOT contain the text "Red" when typing
570
+ const itemText = selectedItems[0].textContent.trim();
571
+ expect(itemText).to.not.contain('Red');
572
+ });
573
+ });
574
+
407
575
  describe('static options', () => {
408
576
  it('accepts an initial value', async () => {
409
577
  const select = await createSelect(
@@ -434,7 +602,7 @@ describe('temba-select', () => {
434
602
  })
435
603
  );
436
604
 
437
- await open(clock, select);
605
+ await openSelect(clock, select);
438
606
  await assertScreenshot(
439
607
  'select/remote-options',
440
608
  getClipWithOptions(select)
@@ -453,7 +621,7 @@ describe('temba-select', () => {
453
621
  );
454
622
 
455
623
  await typeInto('temba-select', 're', false);
456
- await open(clock, select);
624
+ await openSelect(clock, select);
457
625
  assert.equal(select.visibleOptions.length, 2);
458
626
 
459
627
  await assertScreenshot('select/searching', getClipWithOptions(select));
@@ -490,7 +658,7 @@ describe('temba-select', () => {
490
658
  })
491
659
  );
492
660
 
493
- await open(clock, select);
661
+ await openSelect(clock, select);
494
662
 
495
663
  // should have all three pages visible right away
496
664
  assert.equal(select.visibleOptions.length, 15);
@@ -508,14 +676,14 @@ describe('temba-select', () => {
508
676
  );
509
677
 
510
678
  // wait for updates from fetching three pages
511
- await open(clock, select);
679
+ await openSelect(clock, select);
512
680
  assert.equal(select.visibleOptions.length, 15);
513
681
 
514
682
  // close and reopen
515
683
  select.blur();
516
684
  await clock.tick(250);
517
685
 
518
- await open(clock, select);
686
+ await openSelect(clock, select);
519
687
  assert.equal(select.visibleOptions.length, 15);
520
688
 
521
689
  // close and reopen once more (previous bug failed on third opening)
@@ -536,7 +704,7 @@ describe('temba-select', () => {
536
704
  );
537
705
 
538
706
  await typeInto('temba-select', 'Hi there @contact', false);
539
- await open(clock, select);
707
+ await openSelect(clock, select);
540
708
 
541
709
  assert.equal(select.completionOptions.length, 14);
542
710
  await assertScreenshot('select/expressions', getClipWithOptions(select));
@@ -596,7 +764,7 @@ describe('temba-select', () => {
596
764
  await openAndClick(clock, select, 1);
597
765
 
598
766
  // now open and look at focus
599
- await open(clock, select);
767
+ await openSelect(clock, select);
600
768
  await assertScreenshot(
601
769
  'select/search-selected-focus',
602
770
  getClipWithOptions(select)
@@ -616,11 +784,11 @@ describe('temba-select', () => {
616
784
  // select the first option
617
785
  await openAndClick(clock, select, 0);
618
786
  await openAndClick(clock, select, 0);
619
- await open(clock, select);
787
+ await openSelect(clock, select);
620
788
 
621
789
  // now lets do a search, we should see our selection (green) and one other (red)
622
790
  await typeInto('temba-select', 're', false);
623
- await open(clock, select);
791
+ await openSelect(clock, select);
624
792
 
625
793
  // should have two things selected and active query and no matching options
626
794
  await assertScreenshot(
@@ -642,7 +810,7 @@ describe('temba-select', () => {
642
810
  );
643
811
 
644
812
  await typeInto('temba-select', 'look at @(max(m', false);
645
- await open(clock, select);
813
+ await openSelect(clock, select);
646
814
 
647
815
  await assertScreenshot('select/functions', getClipWithOptions(select));
648
816
  });
@@ -650,7 +818,7 @@ describe('temba-select', () => {
650
818
  it('should truncate selection if necessesary', async () => {
651
819
  const options = [
652
820
  {
653
- name: 'this_is_a_long_selection_to_make_sure_it_truncates',
821
+ name: 'this_is_a_long_selection_to_make_sure_it_truncates_but_it_needs_to_be_longer',
654
822
  value: '0'
655
823
  }
656
824
  ];
@@ -3,52 +3,145 @@ import { html, TemplateResult } from 'lit';
3
3
  import { CustomEventType } from '../src/interfaces';
4
4
  import { SortableList } from '../src/list/SortableList';
5
5
  import { assertScreenshot, getClip } from './utils.test';
6
+ import Sinon, { useFakeTimers } from 'sinon';
6
7
 
7
8
  const BORING_LIST = html`
8
9
  <temba-sortable-list>
9
- <div class="sortable" id="chicken" style="padding:10px">Chicken</div>
10
- <div class="sortable" id="fish" style="padding:10px">Fish</div>
10
+ <style>
11
+ .sortable {
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ text-align: center;
16
+ height: 20px;
17
+ }
18
+ </style>
19
+ <div class="sortable" id="chicken" style="">Chicken</div>
20
+ <div class="sortable" id="fish">Fish</div>
21
+ </temba-sortable-list>
22
+ `;
23
+
24
+ const HORIZONTAL_LIST = html`
25
+ <temba-sortable-list horizontal>
26
+ <style>
27
+ .sortable {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ text-align: center;
32
+ height: 20px;
33
+ width: 50px;
34
+ }
35
+ </style>
36
+ <div class="sortable" id="red">Red</div>
37
+ <div class="sortable" id="blue">Blue</div>
38
+ <div class="sortable" id="green">Green</div>
11
39
  </temba-sortable-list>
12
40
  `;
13
41
 
14
42
  const createSorter = async (def: TemplateResult) => {
15
43
  const parentNode = document.createElement('div');
16
- parentNode.setAttribute('style', 'width: 200px;');
44
+ parentNode.setAttribute('style', 'width: 100px;');
17
45
  return (await fixture(def, { parentNode })) as SortableList;
18
46
  };
19
47
 
20
48
  describe('temba-sortable-list', () => {
49
+ let clock: Sinon.SinonFakeTimers;
50
+ beforeEach(function () {
51
+ clock = useFakeTimers();
52
+ clock.runAll();
53
+ });
54
+
55
+ afterEach(function () {
56
+ clock.restore();
57
+ });
58
+
21
59
  it('renders default', async () => {
22
60
  const list: SortableList = await createSorter(BORING_LIST);
23
61
  await assertScreenshot('list/sortable', getClip(list));
24
62
  });
25
63
 
26
- it('drags', async () => {
64
+ it('can get ids of sortable elements', async () => {
27
65
  const list: SortableList = await createSorter(BORING_LIST);
28
- const orderChanged = oneEvent(list, CustomEventType.OrderChanged, false);
29
- const updated = oneEvent(list, 'change', false);
66
+ await list.updateComplete;
67
+
68
+ const ids = list.getIds();
69
+ expect(ids).to.deep.equal(['chicken', 'fish']);
70
+ });
30
71
 
72
+ it('works with horizontal layout', async () => {
73
+ const list: SortableList = await createSorter(HORIZONTAL_LIST);
74
+ await list.updateComplete;
75
+
76
+ const ids = list.getIds();
77
+ expect(ids).to.deep.equal(['red', 'blue', 'green']);
78
+
79
+ // Test horizontal drag behavior
31
80
  const bounds = list.getBoundingClientRect();
81
+ const orderChanged = oneEvent(list, CustomEventType.OrderChanged, false);
32
82
 
33
- await moveMouse(bounds.left + 20, bounds.bottom - 10);
83
+ // Drag the first item (red) to after the second item (blue)
84
+ await moveMouse(bounds.left + 10, bounds.top + 10);
34
85
  await mouseDown();
35
- await moveMouse(bounds.left + 30, bounds.top + 20);
86
+ await moveMouse(bounds.left + 80, bounds.top + 10);
87
+ await mouseUp();
88
+ clock.runAll();
36
89
 
37
- // we should fire an order changed event
38
90
  const orderEvent = await orderChanged;
39
91
  expect(orderEvent.detail).to.deep.equal({
40
- from: 'fish',
41
- to: 'chicken',
42
- fromIdx: 1,
43
- toIdx: 0
92
+ swap: [0, 2]
44
93
  });
94
+ });
95
+
96
+ it('handles prepareGhost callback', async () => {
97
+ const list: SortableList = await createSorter(BORING_LIST);
98
+ let ghostPrepared = false;
99
+
100
+ list.prepareGhost = (ghost: HTMLElement) => {
101
+ ghostPrepared = true;
102
+ ghost.style.backgroundColor = 'red';
103
+ };
104
+
105
+ const bounds = list.getBoundingClientRect();
106
+
107
+ // Start dragging to trigger ghost creation
108
+ await moveMouse(bounds.left + 20, bounds.bottom - 10);
109
+ await mouseDown();
110
+ await moveMouse(bounds.left + 30, bounds.bottom - 10);
111
+
112
+ expect(ghostPrepared).to.be.true;
113
+
114
+ // Clean up
115
+ await mouseUp();
116
+ clock.runAll();
117
+ });
118
+
119
+ it('drags', async () => {
120
+ const list: SortableList = await createSorter(BORING_LIST);
121
+ const updated = oneEvent(list, 'change', false);
122
+
123
+ const bounds = list.getBoundingClientRect();
124
+
125
+ await moveMouse(bounds.left + 20, bounds.bottom - 10);
126
+ await mouseDown();
127
+ await moveMouse(bounds.left + 20, bounds.top + 5);
45
128
 
46
129
  // should be hovered
47
130
  await assertScreenshot('list/sortable-dragging', getClip(list));
48
131
 
49
- // now lets drop, it'll look the same as before dragging since
50
- // its the consuming elements job to do the reordering
132
+ // now lets drop - this will fire the order changed event
133
+ const orderChanged = oneEvent(list, CustomEventType.OrderChanged, false);
51
134
  await mouseUp();
135
+ clock.runAll();
136
+ await list.updateComplete;
137
+ clock.runAll();
138
+
139
+ // we should fire an order changed event on drop
140
+ const orderEvent = await orderChanged;
141
+ expect(orderEvent.detail).to.deep.equal({
142
+ swap: [1, 0]
143
+ });
144
+
52
145
  await assertScreenshot('list/sortable-dropped', getClip(list));
53
146
 
54
147
  // we should fire a change event
@@ -17,6 +17,7 @@ export const createToast = async (attrs: any = {}) => {
17
17
  describe('temba-toast', () => {
18
18
  it('can be created', async () => {
19
19
  const toast = await createToast();
20
+
20
21
  assert.instanceOf(toast, Toast);
21
22
  expect(toast.messages).to.deep.equal([]);
22
23
  expect(toast.staleDuration).to.equal(5000);
@@ -127,8 +128,7 @@ describe('temba-toast', () => {
127
128
  // Initially not visible
128
129
  expect(toast.messages[0].visible).to.be.undefined;
129
130
 
130
- // Wait for the timeout
131
- await new Promise((resolve) => setTimeout(resolve, 150));
131
+ await waitFor(200);
132
132
 
133
133
  expect(toast.messages[0].visible).to.be.true;
134
134
  });
@@ -597,7 +597,7 @@ describe('utils/index', () => {
597
597
  expect(fn.calledOnce).to.be.true;
598
598
  expect(fn.calledWith('arg3')).to.be.true;
599
599
  done();
600
- }, 100);
600
+ }, 75);
601
601
  });
602
602
 
603
603
  it('calls immediately when immediate flag is true', () => {
@@ -632,7 +632,7 @@ describe('utils/index', () => {
632
632
  throttledFn('arg4');
633
633
  expect(fn.callCount).to.equal(2);
634
634
  done();
635
- }, 100);
635
+ }, 75);
636
636
  });
637
637
  });
638
638