@nyaruka/temba-components 0.122.0 → 0.124.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 (262) hide show
  1. package/.github/copilot-instructions.md +181 -0
  2. package/.github/workflows/build.yml +3 -3
  3. package/.github/workflows/cla.yml +6 -6
  4. package/.github/workflows/copilot-setup-steps.yml +86 -0
  5. package/CHANGELOG.md +44 -0
  6. package/demo/drag-drop-demo.html +141 -0
  7. package/demo/index.html +57 -0
  8. package/demo/test-drag-drop.html +94 -0
  9. package/dist/locales/es.js +1 -0
  10. package/dist/locales/es.js.map +1 -1
  11. package/dist/locales/fr.js +1 -0
  12. package/dist/locales/fr.js.map +1 -1
  13. package/dist/locales/pt.js +1 -0
  14. package/dist/locales/pt.js.map +1 -1
  15. package/dist/temba-components.js +366 -247
  16. package/dist/temba-components.js.map +1 -1
  17. package/out-tsc/src/chart/TembaChart.js +81 -14
  18. package/out-tsc/src/chart/TembaChart.js.map +1 -1
  19. package/out-tsc/src/fields/FieldManager.js +27 -34
  20. package/out-tsc/src/fields/FieldManager.js.map +1 -1
  21. package/out-tsc/src/list/RunList.js +13 -8
  22. package/out-tsc/src/list/RunList.js.map +1 -1
  23. package/out-tsc/src/list/SortableList.js +257 -60
  24. package/out-tsc/src/list/SortableList.js.map +1 -1
  25. package/out-tsc/src/locales/es.js +1 -0
  26. package/out-tsc/src/locales/es.js.map +1 -1
  27. package/out-tsc/src/locales/fr.js +1 -0
  28. package/out-tsc/src/locales/fr.js.map +1 -1
  29. package/out-tsc/src/locales/pt.js +1 -0
  30. package/out-tsc/src/locales/pt.js.map +1 -1
  31. package/out-tsc/src/omnibox/Omnibox.js +1 -1
  32. package/out-tsc/src/omnibox/Omnibox.js.map +1 -1
  33. package/out-tsc/src/options/Options.js +36 -13
  34. package/out-tsc/src/options/Options.js.map +1 -1
  35. package/out-tsc/src/select/Select.js +226 -43
  36. package/out-tsc/src/select/Select.js.map +1 -1
  37. package/out-tsc/src/store/AppState.js +3 -3
  38. package/out-tsc/src/store/AppState.js.map +1 -1
  39. package/out-tsc/src/utils/index.js +6 -1
  40. package/out-tsc/src/utils/index.js.map +1 -1
  41. package/out-tsc/src/vectoricon/VectorIcon.js +2 -1
  42. package/out-tsc/src/vectoricon/VectorIcon.js.map +1 -1
  43. package/out-tsc/src/webchat/WebChat.js +5 -2
  44. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  45. package/out-tsc/temba-modules.js +0 -2
  46. package/out-tsc/temba-modules.js.map +1 -1
  47. package/out-tsc/test/temba-appstate-language.test.js +176 -0
  48. package/out-tsc/test/temba-appstate-language.test.js.map +1 -0
  49. package/out-tsc/test/temba-chart.test.js +125 -0
  50. package/out-tsc/test/temba-chart.test.js.map +1 -1
  51. package/out-tsc/test/temba-dropdown.test.js +317 -0
  52. package/out-tsc/test/temba-dropdown.test.js.map +1 -0
  53. package/out-tsc/test/temba-flow-editor-node.test.js +273 -0
  54. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -0
  55. package/out-tsc/test/temba-flow-editor.test.js +244 -0
  56. package/out-tsc/test/temba-flow-editor.test.js.map +1 -0
  57. package/out-tsc/test/temba-flow-plumber.test.js +145 -0
  58. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -0
  59. package/out-tsc/test/temba-flow-render.test.js +171 -0
  60. package/out-tsc/test/temba-flow-render.test.js.map +1 -0
  61. package/out-tsc/test/temba-omnibox.test.js +2 -3
  62. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  63. package/out-tsc/test/temba-run-list.test.js +588 -0
  64. package/out-tsc/test/temba-run-list.test.js.map +1 -0
  65. package/out-tsc/test/temba-select.test.js +149 -52
  66. package/out-tsc/test/temba-select.test.js.map +1 -1
  67. package/out-tsc/test/temba-sortable-list.test.js +91 -15
  68. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  69. package/out-tsc/test/temba-toast.test.js +299 -0
  70. package/out-tsc/test/temba-toast.test.js.map +1 -0
  71. package/out-tsc/test/temba-utils-index.test.js +1178 -0
  72. package/out-tsc/test/temba-utils-index.test.js.map +1 -0
  73. package/out-tsc/test/temba-webchat-lightbox-fix.test.js +42 -0
  74. package/out-tsc/test/temba-webchat-lightbox-fix.test.js.map +1 -0
  75. package/out-tsc/test/temba-webchat.test.js +816 -0
  76. package/out-tsc/test/temba-webchat.test.js.map +1 -0
  77. package/out-tsc/test/utils.test.js +33 -1
  78. package/out-tsc/test/utils.test.js.map +1 -1
  79. package/package.json +6 -8
  80. package/screenshots/truth/alert/error.png +0 -0
  81. package/screenshots/truth/alert/info.png +0 -0
  82. package/screenshots/truth/alert/warning.png +0 -0
  83. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  84. package/screenshots/truth/checkbox/checked.png +0 -0
  85. package/screenshots/truth/checkbox/default.png +0 -0
  86. package/screenshots/truth/colorpicker/default.png +0 -0
  87. package/screenshots/truth/colorpicker/focused.png +0 -0
  88. package/screenshots/truth/colorpicker/initialized.png +0 -0
  89. package/screenshots/truth/colorpicker/selected.png +0 -0
  90. package/screenshots/truth/compose/attachments-tab.png +0 -0
  91. package/screenshots/truth/compose/attachments-with-files-focused.png +0 -0
  92. package/screenshots/truth/compose/attachments-with-files.png +0 -0
  93. package/screenshots/truth/compose/intial-text.png +0 -0
  94. package/screenshots/truth/compose/no-counter.png +0 -0
  95. package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
  96. package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
  97. package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
  98. package/screenshots/truth/contacts/badges.png +0 -0
  99. package/screenshots/truth/contacts/chat-failure.png +0 -0
  100. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  101. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  102. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  103. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  104. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  105. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  106. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  107. package/screenshots/truth/content-menu/button-no-items.png +0 -0
  108. package/screenshots/truth/content-menu/items-and-buttons.png +0 -0
  109. package/screenshots/truth/counter/summary.png +0 -0
  110. package/screenshots/truth/counter/text.png +0 -0
  111. package/screenshots/truth/counter/unicode-variables.png +0 -0
  112. package/screenshots/truth/counter/unicode.png +0 -0
  113. package/screenshots/truth/counter/variable.png +0 -0
  114. package/screenshots/truth/date/date-inline.png +0 -0
  115. package/screenshots/truth/date/date.png +0 -0
  116. package/screenshots/truth/date/datetime.png +0 -0
  117. package/screenshots/truth/date/duration.png +0 -0
  118. package/screenshots/truth/date/timedate.png +0 -0
  119. package/screenshots/truth/datepicker/date-truncated-time.png +0 -0
  120. package/screenshots/truth/datepicker/date.png +0 -0
  121. package/screenshots/truth/datepicker/initial-timezone.png +0 -0
  122. package/screenshots/truth/datepicker/updated-keyboard-date.png +0 -0
  123. package/screenshots/truth/dialog/focused.png +0 -0
  124. package/screenshots/truth/dropdown/after-blur.png +0 -0
  125. package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
  126. package/screenshots/truth/dropdown/custom-arrow-size.png +0 -0
  127. package/screenshots/truth/dropdown/default.png +0 -0
  128. package/screenshots/truth/dropdown/narrow-toggle.png +0 -0
  129. package/screenshots/truth/dropdown/no-mask.png +0 -0
  130. package/screenshots/truth/dropdown/opened.png +0 -0
  131. package/screenshots/truth/dropdown/positioned.png +0 -0
  132. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  133. package/screenshots/truth/dropdown/with-mask.png +0 -0
  134. package/screenshots/truth/flow/editor-basic.png +0 -0
  135. package/screenshots/truth/label/custom.png +0 -0
  136. package/screenshots/truth/label/danger.png +0 -0
  137. package/screenshots/truth/label/dark.png +0 -0
  138. package/screenshots/truth/label/default-icon.png +0 -0
  139. package/screenshots/truth/label/no-icon.png +0 -0
  140. package/screenshots/truth/label/primary.png +0 -0
  141. package/screenshots/truth/label/secondary.png +0 -0
  142. package/screenshots/truth/label/shadow.png +0 -0
  143. package/screenshots/truth/label/tertiary.png +0 -0
  144. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  145. package/screenshots/truth/list/fields-dragging.png +0 -0
  146. package/screenshots/truth/list/fields-filtered.png +0 -0
  147. package/screenshots/truth/list/fields-hovered.png +0 -0
  148. package/screenshots/truth/list/fields.png +0 -0
  149. package/screenshots/truth/list/items-selected.png +0 -0
  150. package/screenshots/truth/list/items-updated.png +0 -0
  151. package/screenshots/truth/list/items.png +0 -0
  152. package/screenshots/truth/list/sortable-dragging.png +0 -0
  153. package/screenshots/truth/list/sortable-dropped.png +0 -0
  154. package/screenshots/truth/list/sortable.png +0 -0
  155. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  156. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  157. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  158. package/screenshots/truth/menu/menu-root.png +0 -0
  159. package/screenshots/truth/menu/menu-submenu.png +0 -0
  160. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  161. package/screenshots/truth/menu/menu-tasks.png +0 -0
  162. package/screenshots/truth/modax/form.png +0 -0
  163. package/screenshots/truth/modax/simple.png +0 -0
  164. package/screenshots/truth/omnibox/selected.png +0 -0
  165. package/screenshots/truth/options/block.png +0 -0
  166. package/screenshots/truth/run-list/basic.png +0 -0
  167. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  168. package/screenshots/truth/select/disabled-selection.png +0 -0
  169. package/screenshots/truth/select/disabled.png +0 -0
  170. package/screenshots/truth/select/embedded.png +0 -0
  171. package/screenshots/truth/select/empty-options.png +0 -0
  172. package/screenshots/truth/select/expression-selected.png +0 -0
  173. package/screenshots/truth/select/expressions.png +0 -0
  174. package/screenshots/truth/select/functions.png +0 -0
  175. package/screenshots/truth/select/local-options.png +0 -0
  176. package/screenshots/truth/select/multi-reorder-final.png +0 -0
  177. package/screenshots/truth/select/multi-reorder-initial.png +0 -0
  178. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  179. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  180. package/screenshots/truth/select/remote-options.png +0 -0
  181. package/screenshots/truth/select/search-enabled.png +0 -0
  182. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  183. package/screenshots/truth/select/search-selected-focus.png +0 -0
  184. package/screenshots/truth/select/search-selected.png +0 -0
  185. package/screenshots/truth/select/search-with-selected.png +0 -0
  186. package/screenshots/truth/select/searching.png +0 -0
  187. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  188. package/screenshots/truth/select/selected-multi.png +0 -0
  189. package/screenshots/truth/select/selected-single.png +0 -0
  190. package/screenshots/truth/select/selection-clearable.png +0 -0
  191. package/screenshots/truth/select/static-initial-value.png +0 -0
  192. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  193. package/screenshots/truth/select/truncated-selection.png +0 -0
  194. package/screenshots/truth/select/with-placeholder.png +0 -0
  195. package/screenshots/truth/select/without-placeholder.png +0 -0
  196. package/screenshots/truth/slider/custom-min-custom-max-valid-value.png +0 -0
  197. package/screenshots/truth/slider/custom-min-default-max-no-value.png +0 -0
  198. package/screenshots/truth/slider/default-min-custom-max-no-value.png +0 -0
  199. package/screenshots/truth/slider/default-min-default-max-invalid-value.png +0 -0
  200. package/screenshots/truth/slider/default-min-default-max-valid-value.png +0 -0
  201. package/screenshots/truth/slider/update-slider-on-value-change.png +0 -0
  202. package/screenshots/truth/templates/default.png +0 -0
  203. package/screenshots/truth/templates/unapproved.png +0 -0
  204. package/screenshots/truth/textinput/input-disabled.png +0 -0
  205. package/screenshots/truth/textinput/input-focused.png +0 -0
  206. package/screenshots/truth/textinput/input-form.png +0 -0
  207. package/screenshots/truth/textinput/input-inserted.png +0 -0
  208. package/screenshots/truth/textinput/input-placeholder.png +0 -0
  209. package/screenshots/truth/textinput/input-updated.png +0 -0
  210. package/screenshots/truth/textinput/input.png +0 -0
  211. package/screenshots/truth/textinput/textarea-focused.png +0 -0
  212. package/screenshots/truth/textinput/textarea.png +0 -0
  213. package/screenshots/truth/tip/bottom.png +0 -0
  214. package/screenshots/truth/tip/left.png +0 -0
  215. package/screenshots/truth/tip/right.png +0 -0
  216. package/screenshots/truth/tip/top.png +0 -0
  217. package/screenshots/truth/webchat/closed-widget.png +0 -0
  218. package/screenshots/truth/webchat/connected-state.png +0 -0
  219. package/screenshots/truth/webchat/connecting-state.png +0 -0
  220. package/screenshots/truth/webchat/disconnected-state.png +0 -0
  221. package/screenshots/truth/webchat/opened-widget.png +0 -0
  222. package/src/chart/TembaChart.ts +86 -15
  223. package/src/fields/FieldManager.ts +30 -38
  224. package/src/list/RunList.ts +11 -8
  225. package/src/list/SortableList.ts +291 -67
  226. package/src/locales/es.ts +1 -0
  227. package/src/locales/fr.ts +1 -0
  228. package/src/locales/pt.ts +1 -0
  229. package/src/omnibox/Omnibox.ts +1 -1
  230. package/src/options/Options.ts +38 -13
  231. package/src/select/Select.ts +245 -47
  232. package/src/store/AppState.ts +3 -3
  233. package/src/utils/index.ts +17 -5
  234. package/src/vectoricon/VectorIcon.ts +2 -1
  235. package/src/webchat/WebChat.ts +5 -2
  236. package/temba-modules.ts +0 -2
  237. package/test/temba-appstate-language.test.ts +218 -0
  238. package/test/temba-chart.test.ts +161 -1
  239. package/test/temba-dropdown.test.ts +444 -0
  240. package/test/temba-flow-editor-node.test.ts +344 -0
  241. package/test/temba-flow-editor.test.ts +301 -0
  242. package/test/temba-flow-plumber.test.ts +189 -0
  243. package/test/temba-flow-render.test.ts +220 -0
  244. package/test/temba-omnibox.test.ts +2 -3
  245. package/test/temba-run-list.test.ts +774 -0
  246. package/test/temba-select.test.ts +206 -78
  247. package/test/temba-sortable-list.test.ts +108 -15
  248. package/test/temba-toast.test.ts +386 -0
  249. package/test/temba-utils-index.test.ts +1547 -0
  250. package/test/temba-webchat-lightbox-fix.test.ts +57 -0
  251. package/test/temba-webchat.test.ts +1095 -0
  252. package/test/utils.test.ts +56 -2
  253. package/test-assets/list/flow-results.json +17 -0
  254. package/test-assets/list/runs.json +126 -0
  255. package/test-assets/style.css +23 -0
  256. package/web-test-runner.config.mjs +33 -7
  257. package/xliff/es.xlf +3 -0
  258. package/xliff/fr.xlf +3 -0
  259. package/xliff/pt.xlf +3 -0
  260. package/out-tsc/src/outboxmonitor/OutboxMonitor.js +0 -136
  261. package/out-tsc/src/outboxmonitor/OutboxMonitor.js.map +0 -1
  262. package/src/outboxmonitor/OutboxMonitor.ts +0 -148
@@ -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
 
@@ -234,6 +178,33 @@ describe('temba-select', () => {
234
178
  await assertScreenshot('select/embedded', getClipWithOptions(select));
235
179
  });
236
180
 
181
+ it('shows no options message when opening with empty options', async () => {
182
+ const select = await createSelect(
183
+ clock,
184
+ getSelectHTML([], { placeholder: 'Select an option' })
185
+ );
186
+
187
+ // attempt to open the select with no options
188
+ await openSelect(clock, select);
189
+
190
+ // should show options dropdown even though there are no options
191
+ const options = getOptions(select);
192
+ assert.instanceOf(options, Options);
193
+
194
+ // the options dropdown should be visible
195
+ assert.isTrue(
196
+ options.shadowRoot
197
+ .querySelector('.options-container')
198
+ .classList.contains('show')
199
+ );
200
+
201
+ // should contain a "No options" message
202
+ const noOptionsText = options.shadowRoot.textContent;
203
+ assert.include(noOptionsText.toLowerCase(), 'no options');
204
+
205
+ await assertScreenshot('select/empty-options', getClipWithOptions(select));
206
+ });
207
+
237
208
  describe('single selection', () => {
238
209
  it('can select a single option', async () => {
239
210
  const select = await createSelect(clock, getSelectHTML());
@@ -270,13 +241,13 @@ describe('temba-select', () => {
270
241
  expect(select.values[0].name).to.equal('Green');
271
242
 
272
243
  // for single selection our current selection should be in the list and focused
273
- await open(clock, select);
244
+ await openSelect(clock, select);
274
245
  assert.equal(select.cursorIndex, 1);
275
246
  assert.equal(select.visibleOptions.length, 3);
276
247
 
277
248
  // now lets do a search, we should see our selection (green) and one other (red)
278
249
  await typeInto('temba-select', 're', false);
279
- await open(clock, select);
250
+ await openSelect(clock, select);
280
251
  assert.equal(select.visibleOptions.length, 2);
281
252
 
282
253
  await assertScreenshot(
@@ -342,7 +313,7 @@ describe('temba-select', () => {
342
313
  assert(changeEvent.called, 'change event not fired');
343
314
 
344
315
  changeEvent.resetHistory();
345
- await open(clock, select);
316
+ await openSelect(clock, select);
346
317
  assert.equal(select.visibleOptions.length, 0);
347
318
  assert(!changeEvent.called, 'change event should not be fired');
348
319
 
@@ -377,6 +348,163 @@ describe('temba-select', () => {
377
348
  });
378
349
  });
379
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
+
380
508
  describe('static options', () => {
381
509
  it('accepts an initial value', async () => {
382
510
  const select = await createSelect(
@@ -407,7 +535,7 @@ describe('temba-select', () => {
407
535
  })
408
536
  );
409
537
 
410
- await open(clock, select);
538
+ await openSelect(clock, select);
411
539
  await assertScreenshot(
412
540
  'select/remote-options',
413
541
  getClipWithOptions(select)
@@ -426,7 +554,7 @@ describe('temba-select', () => {
426
554
  );
427
555
 
428
556
  await typeInto('temba-select', 're', false);
429
- await open(clock, select);
557
+ await openSelect(clock, select);
430
558
  assert.equal(select.visibleOptions.length, 2);
431
559
 
432
560
  await assertScreenshot('select/searching', getClipWithOptions(select));
@@ -463,7 +591,7 @@ describe('temba-select', () => {
463
591
  })
464
592
  );
465
593
 
466
- await open(clock, select);
594
+ await openSelect(clock, select);
467
595
 
468
596
  // should have all three pages visible right away
469
597
  assert.equal(select.visibleOptions.length, 15);
@@ -481,14 +609,14 @@ describe('temba-select', () => {
481
609
  );
482
610
 
483
611
  // wait for updates from fetching three pages
484
- await open(clock, select);
612
+ await openSelect(clock, select);
485
613
  assert.equal(select.visibleOptions.length, 15);
486
614
 
487
615
  // close and reopen
488
616
  select.blur();
489
617
  await clock.tick(250);
490
618
 
491
- await open(clock, select);
619
+ await openSelect(clock, select);
492
620
  assert.equal(select.visibleOptions.length, 15);
493
621
 
494
622
  // close and reopen once more (previous bug failed on third opening)
@@ -509,7 +637,7 @@ describe('temba-select', () => {
509
637
  );
510
638
 
511
639
  await typeInto('temba-select', 'Hi there @contact', false);
512
- await open(clock, select);
640
+ await openSelect(clock, select);
513
641
 
514
642
  assert.equal(select.completionOptions.length, 14);
515
643
  await assertScreenshot('select/expressions', getClipWithOptions(select));
@@ -569,7 +697,7 @@ describe('temba-select', () => {
569
697
  await openAndClick(clock, select, 1);
570
698
 
571
699
  // now open and look at focus
572
- await open(clock, select);
700
+ await openSelect(clock, select);
573
701
  await assertScreenshot(
574
702
  'select/search-selected-focus',
575
703
  getClipWithOptions(select)
@@ -589,11 +717,11 @@ describe('temba-select', () => {
589
717
  // select the first option
590
718
  await openAndClick(clock, select, 0);
591
719
  await openAndClick(clock, select, 0);
592
- await open(clock, select);
720
+ await openSelect(clock, select);
593
721
 
594
722
  // now lets do a search, we should see our selection (green) and one other (red)
595
723
  await typeInto('temba-select', 're', false);
596
- await open(clock, select);
724
+ await openSelect(clock, select);
597
725
 
598
726
  // should have two things selected and active query and no matching options
599
727
  await assertScreenshot(
@@ -615,7 +743,7 @@ describe('temba-select', () => {
615
743
  );
616
744
 
617
745
  await typeInto('temba-select', 'look at @(max(m', false);
618
- await open(clock, select);
746
+ await openSelect(clock, select);
619
747
 
620
748
  await assertScreenshot('select/functions', getClipWithOptions(select));
621
749
  });
@@ -623,7 +751,7 @@ describe('temba-select', () => {
623
751
  it('should truncate selection if necessesary', async () => {
624
752
  const options = [
625
753
  {
626
- name: 'this_is_a_long_selection_to_make_sure_it_truncates',
754
+ name: 'this_is_a_long_selection_to_make_sure_it_truncates_but_it_needs_to_be_longer',
627
755
  value: '0'
628
756
  }
629
757
  ];
@@ -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