@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
@@ -0,0 +1,444 @@
1
+ import { assert, expect } from '@open-wc/testing';
2
+ import { Dropdown } from '../src/dropdown/Dropdown';
3
+ import { assertScreenshot, getClip, getComponent } from './utils.test';
4
+
5
+ const TAG = 'temba-dropdown';
6
+
7
+ // Helper function to wait for stable rendering
8
+ const waitForStableRender = async (dropdown: Dropdown, timeoutMs = 200) => {
9
+ await dropdown.updateComplete;
10
+ // Double wait to ensure any async positioning is complete
11
+ await new Promise((resolve) => setTimeout(resolve, timeoutMs));
12
+ await dropdown.updateComplete;
13
+ };
14
+
15
+ // Helper function to get expanded clip that includes dropdown content when open
16
+ const getDropdownClip = (dropdown: Dropdown) => {
17
+ if (!dropdown.open) {
18
+ // If closed, use regular clipping
19
+ return getClip(dropdown);
20
+ }
21
+
22
+ // For open dropdowns, include the positioned dropdown content
23
+ const dropdownDiv = dropdown.shadowRoot.querySelector(
24
+ '.dropdown'
25
+ ) as HTMLDivElement;
26
+ const dropdownBounds = dropdownDiv.getBoundingClientRect();
27
+ const componentBounds = dropdown.getBoundingClientRect();
28
+
29
+ // If dropdown content has no meaningful size, fall back to regular clip
30
+ if (dropdownBounds.width < 10 || dropdownBounds.height < 10) {
31
+ return getClip(dropdown);
32
+ }
33
+
34
+ // Create a clipping region that includes both the component and the dropdown content
35
+ const minX = Math.min(componentBounds.x, dropdownBounds.x);
36
+ const minY = Math.min(componentBounds.y, dropdownBounds.y);
37
+ const maxX = Math.max(componentBounds.right, dropdownBounds.right);
38
+ const maxY = Math.max(componentBounds.bottom, dropdownBounds.bottom);
39
+
40
+ // Clamp to reasonable bounds to avoid excessive screenshot sizes
41
+ const x = Math.max(0, minX - 10);
42
+ const y = Math.max(0, minY - 10);
43
+ const width = Math.min(1000, maxX - minX + 20);
44
+ const height = Math.min(800, maxY - minY + 20);
45
+
46
+ return { x, y, width, height };
47
+ };
48
+
49
+ const getDropdown = async (
50
+ attrs: {
51
+ open?: boolean;
52
+ dormant?: boolean;
53
+ arrowSize?: number;
54
+ margin?: number;
55
+ mask?: boolean;
56
+ } = {},
57
+ toggleSlot = '<button slot="toggle">Toggle</button>',
58
+ dropdownSlot = '<div slot="dropdown">Dropdown content</div>'
59
+ ) => {
60
+ const dropdown = (await getComponent(
61
+ TAG,
62
+ attrs,
63
+ `${toggleSlot}${dropdownSlot}`,
64
+ 400,
65
+ 300
66
+ )) as Dropdown;
67
+ await dropdown.updateComplete;
68
+ return dropdown;
69
+ };
70
+
71
+ describe(TAG, () => {
72
+ it('can be created', async () => {
73
+ const dropdown = await getDropdown();
74
+ assert.instanceOf(dropdown, Dropdown);
75
+ });
76
+
77
+ it('has correct default properties', async () => {
78
+ const dropdown = await getDropdown();
79
+
80
+ // Test expected values first
81
+ expect(dropdown.open).to.equal(false);
82
+ expect(dropdown.dormant).to.equal(true);
83
+ expect(dropdown.arrowSize).to.equal(8);
84
+ expect(dropdown.margin).to.equal(10);
85
+ expect(dropdown.mask).to.equal(false);
86
+ // Position calculation happens automatically, so styles won't be empty
87
+ expect(typeof dropdown.dropdownStyle).to.equal('object');
88
+ expect(typeof dropdown.arrowStyle).to.equal('object');
89
+
90
+ // Then screenshot
91
+ await assertScreenshot('dropdown/default', getClip(dropdown));
92
+ });
93
+
94
+ it('renders with mask when enabled', async () => {
95
+ const dropdown = await getDropdown({ mask: true });
96
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
97
+
98
+ // Open the dropdown properly by clicking
99
+ toggle.click();
100
+ await waitForStableRender(dropdown);
101
+
102
+ // Test expected values first
103
+ expect(dropdown.mask).to.equal(true);
104
+ expect(dropdown.open).to.equal(true);
105
+ expect(dropdown.dormant).to.equal(false);
106
+
107
+ // Then screenshot
108
+ await assertScreenshot('dropdown/with-mask', getDropdownClip(dropdown));
109
+ });
110
+
111
+ it('handles toggle click and opens dropdown', async () => {
112
+ const dropdown = await getDropdown();
113
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
114
+
115
+ // Verify initial state
116
+ expect(dropdown.open).to.equal(false);
117
+ expect(dropdown.dormant).to.equal(true);
118
+
119
+ // Click the toggle
120
+ toggle.click();
121
+ await waitForStableRender(dropdown);
122
+
123
+ // Verify dropdown opened
124
+ expect(dropdown.open).to.equal(true);
125
+ expect(dropdown.dormant).to.equal(false);
126
+
127
+ // Screenshot the opened state with expanded clip
128
+ await assertScreenshot('dropdown/opened', getDropdownClip(dropdown));
129
+ });
130
+
131
+ it('handles custom arrow size', async () => {
132
+ const dropdown = await getDropdown({ arrowSize: 12 });
133
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
134
+
135
+ // Open the dropdown properly by clicking
136
+ toggle.click();
137
+ await waitForStableRender(dropdown);
138
+
139
+ // Test expected values first
140
+ expect(dropdown.arrowSize).to.equal(12);
141
+ expect(dropdown.open).to.equal(true);
142
+ expect(dropdown.dormant).to.equal(false);
143
+
144
+ // Then screenshot
145
+ await assertScreenshot(
146
+ 'dropdown/custom-arrow-size',
147
+ getDropdownClip(dropdown)
148
+ );
149
+ });
150
+
151
+ it('calculates position correctly', async () => {
152
+ const dropdown = await getDropdown();
153
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
154
+
155
+ // Open the dropdown properly by clicking
156
+ toggle.click();
157
+ await dropdown.updateComplete;
158
+
159
+ // Trigger position calculation
160
+ dropdown.calculatePosition();
161
+ await waitForStableRender(dropdown);
162
+
163
+ // Verify position styles were calculated
164
+ expect(Object.keys(dropdown.dropdownStyle).length).to.be.greaterThan(0);
165
+ expect(Object.keys(dropdown.arrowStyle).length).to.be.greaterThan(0);
166
+ expect(dropdown.open).to.equal(true);
167
+ expect(dropdown.dormant).to.equal(false);
168
+
169
+ // Screenshot positioned dropdown
170
+ await assertScreenshot('dropdown/positioned', getDropdownClip(dropdown));
171
+ });
172
+
173
+ it('handles blur events to close dropdown', async () => {
174
+ const dropdown = await getDropdown();
175
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
176
+
177
+ // Open the dropdown first
178
+ toggle.click();
179
+ await dropdown.updateComplete;
180
+ expect(dropdown.open).to.equal(true);
181
+
182
+ // Simulate blur event
183
+ const dropdownDiv = dropdown.shadowRoot.querySelector(
184
+ '.dropdown'
185
+ ) as HTMLDivElement;
186
+ const blurEvent = new FocusEvent('blur', {
187
+ bubbles: true,
188
+ relatedTarget: document.body
189
+ });
190
+ dropdownDiv.dispatchEvent(blurEvent);
191
+
192
+ // Check that dropdown is closed after a short delay
193
+ await new Promise((resolve) => setTimeout(resolve, 300));
194
+ expect(dropdown.open).to.equal(false);
195
+
196
+ // Screenshot closed state
197
+ await assertScreenshot('dropdown/after-blur', getClip(dropdown));
198
+ });
199
+
200
+ it('handles blur events when focus moves within dropdown', async () => {
201
+ const dropdown = await getDropdown();
202
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
203
+
204
+ // Open the dropdown first
205
+ toggle.click();
206
+ await dropdown.updateComplete;
207
+ expect(dropdown.open).to.equal(true);
208
+
209
+ // Create an element within the dropdown
210
+ const dropdownContent = dropdown.querySelector(
211
+ '[slot="dropdown"]'
212
+ ) as HTMLElement;
213
+ const internalButton = document.createElement('button');
214
+ internalButton.textContent = 'Internal';
215
+ dropdownContent.appendChild(internalButton);
216
+
217
+ // Simulate blur event where focus moves to internal element
218
+ const dropdownDiv = dropdown.shadowRoot.querySelector(
219
+ '.dropdown'
220
+ ) as HTMLDivElement;
221
+ const blurEvent = new FocusEvent('blur', {
222
+ bubbles: true,
223
+ relatedTarget: internalButton
224
+ });
225
+ dropdownDiv.dispatchEvent(blurEvent);
226
+ await dropdown.updateComplete;
227
+
228
+ // Dropdown should remain open since focus moved within it
229
+ expect(dropdown.open).to.equal(true);
230
+ });
231
+
232
+ it('prevents opening when already open', async () => {
233
+ const dropdown = await getDropdown();
234
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
235
+
236
+ // First, open the dropdown normally
237
+ toggle.click();
238
+ await dropdown.updateComplete;
239
+ expect(dropdown.open).to.equal(true);
240
+ expect(dropdown.dormant).to.equal(false);
241
+
242
+ // Now try to click toggle again - should not call openDropdown again
243
+ // since !dropdown.open is false
244
+ const originalOpen = dropdown.open;
245
+ toggle.click();
246
+ await dropdown.updateComplete;
247
+
248
+ // Should remain in the same state
249
+ expect(dropdown.open).to.equal(originalOpen);
250
+ });
251
+
252
+ it('handles position calculation with right edge collision', async () => {
253
+ // Create dropdown positioned near right edge
254
+ const dropdown = await getDropdown(
255
+ {},
256
+ '<button slot="toggle" style="position: fixed; right: 50px; top: 100px; width: 100px; height: 30px;">Toggle</button>',
257
+ '<div slot="dropdown" style="width: 200px; height: 100px;">Wide content</div>'
258
+ );
259
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
260
+
261
+ // Open the dropdown properly by clicking
262
+ toggle.click();
263
+ await waitForStableRender(dropdown);
264
+
265
+ // Get actual element bounds to simulate collision
266
+ const dropdownDiv = dropdown.shadowRoot.querySelector(
267
+ '.dropdown'
268
+ ) as HTMLDivElement;
269
+ const originalGetBoundingClientRect = dropdownDiv.getBoundingClientRect;
270
+
271
+ // Mock getBoundingClientRect to simulate right collision
272
+ dropdownDiv.getBoundingClientRect = function () {
273
+ return {
274
+ bottom: 200,
275
+ right: window.innerWidth + 100, // Extends beyond window
276
+ top: 100,
277
+ left: window.innerWidth - 50,
278
+ width: 200,
279
+ height: 100,
280
+ x: window.innerWidth - 50,
281
+ y: 100
282
+ } as DOMRect;
283
+ };
284
+
285
+ try {
286
+ // Trigger position calculation
287
+ dropdown.calculatePosition();
288
+ await waitForStableRender(dropdown);
289
+
290
+ // Verify position was adjusted for right edge
291
+ expect(dropdown.dropdownStyle).to.have.property('left');
292
+
293
+ // Screenshot positioned dropdown
294
+ await assertScreenshot(
295
+ 'dropdown/right-edge-collision',
296
+ getDropdownClip(dropdown)
297
+ );
298
+ } finally {
299
+ // Restore original method
300
+ dropdownDiv.getBoundingClientRect = originalGetBoundingClientRect;
301
+ }
302
+ });
303
+
304
+ it('handles position calculation with bottom edge collision', async () => {
305
+ // Create dropdown positioned near bottom edge
306
+ const dropdown = await getDropdown(
307
+ {},
308
+ '<button slot="toggle" style="position: fixed; left: 100px; bottom: 50px; width: 100px; height: 30px;">Toggle</button>',
309
+ '<div slot="dropdown" style="width: 200px; height: 100px; position: absolute;">Tall content</div>'
310
+ );
311
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
312
+
313
+ // Open the dropdown properly by clicking
314
+ toggle.click();
315
+ await waitForStableRender(dropdown);
316
+
317
+ // Get actual element bounds to simulate collision
318
+ const dropdownDiv = dropdown.shadowRoot.querySelector(
319
+ '.dropdown'
320
+ ) as HTMLDivElement;
321
+ const originalGetBoundingClientRect = dropdownDiv.getBoundingClientRect;
322
+
323
+ // Mock getBoundingClientRect to simulate bottom collision
324
+ dropdownDiv.getBoundingClientRect = function () {
325
+ return {
326
+ bottom: window.innerHeight + 100, // Extends beyond window
327
+ right: 300,
328
+ top: window.innerHeight - 50,
329
+ left: 100,
330
+ width: 200,
331
+ height: 100,
332
+ x: 100,
333
+ y: window.innerHeight - 50
334
+ } as DOMRect;
335
+ };
336
+
337
+ try {
338
+ // Trigger position calculation
339
+ dropdown.calculatePosition();
340
+ await waitForStableRender(dropdown);
341
+
342
+ // Verify position was adjusted for bottom edge (bumped up)
343
+ expect(dropdown.dropdownStyle).to.have.property('top');
344
+ expect(dropdown.arrowStyle).to.have.property('transform');
345
+ expect(dropdown.arrowStyle['transform']).to.include('rotate(180deg)');
346
+
347
+ // Screenshot positioned dropdown
348
+ await assertScreenshot(
349
+ 'dropdown/bottom-edge-collision',
350
+ getDropdownClip(dropdown)
351
+ );
352
+ } finally {
353
+ // Restore original method
354
+ dropdownDiv.getBoundingClientRect = originalGetBoundingClientRect;
355
+ }
356
+ });
357
+
358
+ it('handles arrow positioning when toggle is very narrow', async () => {
359
+ const dropdown = await getDropdown(
360
+ {},
361
+ '<button slot="toggle" style="width: 5px;">•</button>',
362
+ '<div slot="dropdown">Content</div>'
363
+ );
364
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
365
+
366
+ // Open the dropdown properly by clicking
367
+ toggle.click();
368
+ await dropdown.updateComplete;
369
+
370
+ // Trigger position calculation
371
+ dropdown.calculatePosition();
372
+ await waitForStableRender(dropdown);
373
+
374
+ // Verify arrow positioning was adjusted for narrow toggle
375
+ expect(dropdown.dropdownStyle).to.have.property('marginLeft');
376
+ expect(dropdown.dropdownStyle['marginLeft']).to.equal('-10px');
377
+ expect(dropdown.open).to.equal(true);
378
+ expect(dropdown.dormant).to.equal(false);
379
+
380
+ // Screenshot with adjusted arrow
381
+ await assertScreenshot('dropdown/narrow-toggle', getDropdownClip(dropdown));
382
+ });
383
+
384
+ it('handles position calculation when toggle element is missing', async () => {
385
+ const dropdown = await getDropdown(
386
+ { open: true, dormant: false },
387
+ '', // No toggle element
388
+ '<div slot="dropdown">Content</div>'
389
+ );
390
+
391
+ // Trigger position calculation - should handle gracefully
392
+ dropdown.calculatePosition();
393
+ await dropdown.updateComplete;
394
+
395
+ // Should not crash and should have basic styles
396
+ expect(typeof dropdown.dropdownStyle).to.equal('object');
397
+ expect(typeof dropdown.arrowStyle).to.equal('object');
398
+ });
399
+
400
+ it('handles resetBlurHandler when activeFocus exists', async () => {
401
+ const dropdown = await getDropdown();
402
+ const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
403
+
404
+ // Open dropdown to trigger resetBlurHandler for the first time
405
+ toggle.click();
406
+ await dropdown.updateComplete;
407
+ expect(dropdown.open).to.equal(true);
408
+
409
+ // Now open it again - this should trigger the activeFocus cleanup branch
410
+ // First we need to close it
411
+ const dropdownDiv = dropdown.shadowRoot.querySelector(
412
+ '.dropdown'
413
+ ) as HTMLDivElement;
414
+ const blurEvent = new FocusEvent('blur', {
415
+ bubbles: true,
416
+ relatedTarget: document.body
417
+ });
418
+ dropdownDiv.dispatchEvent(blurEvent);
419
+
420
+ // Wait for close
421
+ await new Promise((resolve) => setTimeout(resolve, 300));
422
+ expect(dropdown.open).to.equal(false);
423
+ expect(dropdown.dormant).to.equal(true);
424
+
425
+ // Open again - this should trigger the cleanup in resetBlurHandler
426
+ toggle.click();
427
+ await dropdown.updateComplete;
428
+ expect(dropdown.open).to.equal(true);
429
+ });
430
+
431
+ it('renders without mask by default', async () => {
432
+ const dropdown = await getDropdown(); // No mask explicitly set
433
+
434
+ // Test expected values first - mask should be false by default
435
+ expect(dropdown.mask).to.equal(false);
436
+
437
+ // Look for mask element in shadow DOM - should not exist when mask is false
438
+ const maskElement = dropdown.shadowRoot.querySelector('.mask');
439
+ expect(maskElement).to.be.null;
440
+
441
+ // Screenshot default rendering
442
+ await assertScreenshot('dropdown/no-mask', getClip(dropdown));
443
+ });
444
+ });