@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.
- package/.github/copilot-instructions.md +22 -4
- package/CHANGELOG.md +21 -0
- package/TEST_OPTIMIZATION.md +158 -0
- package/demo/alert/example.html +65 -0
- package/demo/button/example.html +71 -0
- package/demo/chart/example.html +56 -0
- package/demo/checkbox/example.html +72 -0
- package/demo/compose/example.html +72 -0
- package/demo/data/images/gus.png +0 -0
- package/demo/data/images/purrington.jpg +0 -0
- package/demo/data/server/opened-tickets.json +40 -0
- package/demo/data/server/response-time.json +27 -0
- package/demo/datepicker/example.html +69 -0
- package/demo/dialog/example.html +107 -0
- package/demo/dropdown/example.html +99 -0
- package/demo/index.html +152 -430
- package/demo/misc/example.html +72 -0
- package/demo/progress/example.html +59 -0
- package/demo/select/drag-and-drop.html +142 -0
- package/demo/select/example.html +82 -0
- package/demo/select/multi.html +73 -0
- package/demo/slider/example.html +59 -0
- package/demo/sortable-list/example.html +99 -0
- package/demo/styles.css +183 -0
- package/demo/tabs/example.html +91 -0
- package/demo/textinput/completion.html +56 -0
- package/demo/textinput/example.html +61 -0
- package/dist/temba-components.js +323 -191
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +19 -16
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/fields/FieldManager.js +27 -34
- package/out-tsc/src/fields/FieldManager.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +1 -1
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +257 -60
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/omnibox/Omnibox.js +1 -1
- package/out-tsc/src/omnibox/Omnibox.js.map +1 -1
- package/out-tsc/src/select/Select.js +198 -38
- package/out-tsc/src/select/Select.js.map +1 -1
- package/out-tsc/src/thumbnail/Thumbnail.js +1 -1
- package/out-tsc/src/thumbnail/Thumbnail.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +5 -2
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/test/temba-chart.test.js +1 -1
- package/out-tsc/test/temba-chart.test.js.map +1 -1
- package/out-tsc/test/temba-compose.test.js +6 -30
- package/out-tsc/test/temba-compose.test.js.map +1 -1
- package/out-tsc/test/temba-contact-chat.test.js +1 -2
- package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
- package/out-tsc/test/temba-dropdown.test.js +1 -1
- package/out-tsc/test/temba-dropdown.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +273 -0
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor.test.js +244 -0
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -0
- package/out-tsc/test/temba-flow-plumber.test.js +145 -0
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -0
- package/out-tsc/test/temba-flow-render.test.js +171 -0
- package/out-tsc/test/temba-flow-render.test.js.map +1 -0
- package/out-tsc/test/temba-omnibox.test.js +6 -3
- package/out-tsc/test/temba-omnibox.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +183 -53
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-sortable-list.test.js +91 -15
- package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
- package/out-tsc/test/temba-toast.test.js +1 -2
- package/out-tsc/test/temba-toast.test.js.map +1 -1
- package/out-tsc/test/temba-utils-index.test.js +2 -2
- package/out-tsc/test/temba-utils-index.test.js.map +1 -1
- package/out-tsc/test/temba-webchat-lightbox-fix.test.js +42 -0
- package/out-tsc/test/temba-webchat-lightbox-fix.test.js.map +1 -0
- package/out-tsc/test/utils.test.js +58 -0
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +2 -3
- package/screenshots/truth/flow/editor-basic.png +0 -0
- package/screenshots/truth/list/fields-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dropped.png +0 -0
- package/screenshots/truth/list/sortable.png +0 -0
- package/screenshots/truth/omnibox/selected.png +0 -0
- package/screenshots/truth/select/disabled-multi-selection.png +0 -0
- package/screenshots/truth/select/disabled-selection.png +0 -0
- package/screenshots/truth/select/disabled.png +0 -0
- package/screenshots/truth/select/embedded.png +0 -0
- package/screenshots/truth/select/empty-options.png +0 -0
- package/screenshots/truth/select/expression-selected.png +0 -0
- package/screenshots/truth/select/expressions.png +0 -0
- package/screenshots/truth/select/functions.png +0 -0
- package/screenshots/truth/select/local-options.png +0 -0
- package/screenshots/truth/select/multi-reorder-final.png +0 -0
- package/screenshots/truth/select/multi-reorder-initial.png +0 -0
- package/screenshots/truth/select/multi-with-endpoint.png +0 -0
- package/screenshots/truth/select/multiple-initial-values.png +0 -0
- package/screenshots/truth/select/remote-options.png +0 -0
- package/screenshots/truth/select/search-enabled.png +0 -0
- package/screenshots/truth/select/search-multi-no-matches.png +0 -0
- package/screenshots/truth/select/search-selected-focus.png +0 -0
- package/screenshots/truth/select/search-selected.png +0 -0
- package/screenshots/truth/select/search-with-selected.png +0 -0
- package/screenshots/truth/select/searching.png +0 -0
- package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
- package/screenshots/truth/select/selected-multi.png +0 -0
- package/screenshots/truth/select/selected-single.png +0 -0
- package/screenshots/truth/select/selection-clearable.png +0 -0
- package/screenshots/truth/select/static-initial-value.png +0 -0
- package/screenshots/truth/select/static-initial-via-selected.png +0 -0
- package/screenshots/truth/select/truncated-selection.png +0 -0
- package/screenshots/truth/select/with-placeholder.png +0 -0
- package/screenshots/truth/select/without-placeholder.png +0 -0
- package/screenshots/truth/templates/default.png +0 -0
- package/screenshots/truth/templates/unapproved.png +0 -0
- package/screenshots/truth/webchat/connected-state.png +0 -0
- package/src/chart/TembaChart.ts +20 -16
- package/src/fields/FieldManager.ts +30 -38
- package/src/flow/Editor.ts +1 -1
- package/src/list/SortableList.ts +291 -67
- package/src/omnibox/Omnibox.ts +1 -1
- package/src/select/Select.ts +213 -42
- package/src/thumbnail/Thumbnail.ts +1 -1
- package/src/webchat/WebChat.ts +5 -2
- package/test/temba-chart.test.ts +1 -1
- package/test/temba-compose.test.ts +11 -38
- package/test/temba-contact-chat.test.ts +4 -6
- package/test/temba-dropdown.test.ts +1 -1
- package/test/temba-flow-editor-node.test.ts +344 -0
- package/test/temba-flow-editor.test.ts +301 -0
- package/test/temba-flow-plumber.test.ts +189 -0
- package/test/temba-flow-render.test.ts +220 -0
- package/test/temba-omnibox.test.ts +7 -3
- package/test/temba-select.test.ts +247 -79
- package/test/temba-sortable-list.test.ts +108 -15
- package/test/temba-toast.test.ts +2 -2
- package/test/temba-utils-index.test.ts +2 -2
- package/test/temba-webchat-lightbox-fix.test.ts +57 -0
- package/test/utils.test.ts +88 -0
- package/web-test-runner.config.mjs +4 -2
- package/.storybook/main.js +0 -14
- package/.storybook/preview-head.html +0 -1
- package/.storybook/preview.js +0 -17
- package/demo/agents.html +0 -147
- package/demo/old.html +0 -573
- package/demo/remote.html +0 -3
- package/screenshots/truth/compose/attachments-with-files-focused.png +0 -0
- 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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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
|
-
<
|
|
10
|
-
|
|
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:
|
|
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('
|
|
64
|
+
it('can get ids of sortable elements', async () => {
|
|
27
65
|
const list: SortableList = await createSorter(BORING_LIST);
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|
|
50
|
-
|
|
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
|
package/test/temba-toast.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
},
|
|
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
|
-
},
|
|
635
|
+
}, 75);
|
|
636
636
|
});
|
|
637
637
|
});
|
|
638
638
|
|