@adia-ai/web-components 0.6.34 → 0.6.36

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 (280) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/color/index.js +1 -1
  3. package/components/accordion/accordion-item.yaml +2 -2
  4. package/components/accordion/accordion.js +1 -1
  5. package/components/action-list/action-item.yaml +2 -2
  6. package/components/action-list/action-list.js +1 -1
  7. package/components/agent-artifact/{class.js → agent-artifact.class.js} +1 -1
  8. package/components/agent-artifact/agent-artifact.js +1 -1
  9. package/components/agent-feedback-bar/agent-feedback-bar.js +1 -1
  10. package/components/agent-questions/agent-questions.js +1 -1
  11. package/components/agent-reasoning/agent-reasoning.js +1 -1
  12. package/components/agent-suggestions/agent-suggestions.js +1 -1
  13. package/components/alert/alert.a2ui.json +64 -1
  14. package/components/alert/{class.js → alert.class.js} +189 -2
  15. package/components/alert/alert.css +78 -0
  16. package/components/alert/alert.d.ts +14 -0
  17. package/components/alert/alert.js +1 -1
  18. package/components/alert/alert.test.js +184 -0
  19. package/components/alert/alert.yaml +114 -1
  20. package/components/avatar/avatar-group.yaml +2 -2
  21. package/components/avatar/avatar.js +1 -1
  22. package/components/badge/badge.js +1 -1
  23. package/components/block/block.js +1 -1
  24. package/components/breadcrumb/breadcrumb.js +1 -1
  25. package/components/button/button.js +1 -1
  26. package/components/calendar-grid/calendar-grid.a2ui.json +10 -0
  27. package/components/calendar-grid/{class.js → calendar-grid.class.js} +30 -4
  28. package/components/calendar-grid/calendar-grid.css +20 -0
  29. package/components/calendar-grid/calendar-grid.d.ts +4 -0
  30. package/components/calendar-grid/calendar-grid.js +1 -1
  31. package/components/calendar-grid/calendar-grid.yaml +20 -0
  32. package/components/calendar-picker/calendar-picker.js +1 -1
  33. package/components/card/card.js +1 -1
  34. package/components/chart/chart.js +1 -1
  35. package/components/chart-legend/chart-legend.js +1 -1
  36. package/components/chat-thread/chat-input.a2ui.json +1 -1
  37. package/components/chat-thread/chat-input.js +6 -1
  38. package/components/chat-thread/chat-input.yaml +4 -1
  39. package/components/chat-thread/chat-thread.js +1 -1
  40. package/components/check/check.js +1 -1
  41. package/components/code/code.js +1 -1
  42. package/components/col/col.js +1 -1
  43. package/components/color-input/color-input.js +1 -1
  44. package/components/color-picker/color-picker.js +1 -1
  45. package/components/combobox/combobox.css +12 -0
  46. package/components/combobox/combobox.js +1 -1
  47. package/components/command/command.js +1 -1
  48. package/components/date-range-picker/{class.js → date-range-picker.class.js} +19 -3
  49. package/components/date-range-picker/date-range-picker.css +55 -6
  50. package/components/date-range-picker/date-range-picker.js +1 -1
  51. package/components/datetime-picker/{class.js → datetime-picker.class.js} +1 -1
  52. package/components/datetime-picker/datetime-picker.css +7 -1
  53. package/components/datetime-picker/datetime-picker.js +1 -1
  54. package/components/demo-toggle/demo-toggle.js +1 -1
  55. package/components/description-list/description-list.js +1 -1
  56. package/components/divider/divider.js +1 -1
  57. package/components/drawer/drawer.js +1 -1
  58. package/components/embed/embed.js +1 -1
  59. package/components/empty-state/empty-state.js +1 -1
  60. package/components/feed/feed.js +1 -1
  61. package/components/field/field.js +1 -1
  62. package/components/field/field.test.js +1 -1
  63. package/components/fields/fields.js +1 -1
  64. package/components/grid/grid.js +1 -1
  65. package/components/heatmap/heatmap.js +1 -1
  66. package/components/icon/icon.js +1 -1
  67. package/components/image/image.js +1 -1
  68. package/components/index.js +3 -0
  69. package/components/inline-message/inline-message.a2ui.json +143 -0
  70. package/components/inline-message/inline-message.class.js +169 -0
  71. package/components/inline-message/inline-message.css +75 -0
  72. package/components/inline-message/inline-message.d.ts +31 -0
  73. package/components/inline-message/inline-message.examples.md +19 -0
  74. package/components/inline-message/inline-message.js +17 -0
  75. package/components/inline-message/inline-message.test.js +203 -0
  76. package/components/inline-message/inline-message.yaml +205 -0
  77. package/components/input/input.css +16 -2
  78. package/components/input/input.js +1 -1
  79. package/components/input/input.test.js +40 -0
  80. package/components/input/input.yaml +5 -4
  81. package/components/inspector/inspector.js +1 -1
  82. package/components/integration-card/integration-card.js +1 -1
  83. package/components/kbd/kbd.js +1 -1
  84. package/components/link/link.js +1 -1
  85. package/components/list/list-item.yaml +2 -2
  86. package/components/list/list.js +1 -1
  87. package/components/list-window/list-window.js +1 -1
  88. package/components/loading-overlay/loading-overlay.a2ui.json +176 -0
  89. package/components/loading-overlay/loading-overlay.class.js +203 -0
  90. package/components/loading-overlay/loading-overlay.css +81 -0
  91. package/components/loading-overlay/loading-overlay.d.ts +24 -0
  92. package/components/loading-overlay/loading-overlay.examples.md +50 -0
  93. package/components/loading-overlay/loading-overlay.js +17 -0
  94. package/components/loading-overlay/loading-overlay.test.js +257 -0
  95. package/components/loading-overlay/loading-overlay.yaml +260 -0
  96. package/components/menu/menu-divider.yaml +1 -1
  97. package/components/menu/menu-item.yaml +1 -1
  98. package/components/menu/menu.a2ui.json +3 -0
  99. package/components/menu/menu.js +1 -1
  100. package/components/menu/menu.yaml +7 -0
  101. package/components/modal/{class.js → modal.class.js} +12 -1
  102. package/components/modal/modal.css +11 -1
  103. package/components/modal/modal.js +1 -1
  104. package/components/nav/nav.js +1 -1
  105. package/components/nav-group/nav-group.js +1 -1
  106. package/components/nav-item/nav-item.js +1 -1
  107. package/components/noodles/noodles.js +1 -1
  108. package/components/option-card/option-card.js +1 -1
  109. package/components/otp-input/otp-input.js +1 -1
  110. package/components/page/page.js +1 -1
  111. package/components/pagination/pagination.js +1 -1
  112. package/components/pane/pane.js +1 -1
  113. package/components/pipeline-status/pipeline-status.js +1 -1
  114. package/components/popover/popover.a2ui.json +8 -1
  115. package/components/popover/popover.js +1 -1
  116. package/components/popover/popover.yaml +14 -1
  117. package/components/progress/progress.js +1 -1
  118. package/components/progress-row/progress-row.js +1 -1
  119. package/components/radio/radio.js +1 -1
  120. package/components/range/range.js +1 -1
  121. package/components/rating/rating.js +1 -1
  122. package/components/richtext/richtext.js +1 -1
  123. package/components/row/row.js +1 -1
  124. package/components/search/{class.js → search.class.js} +2 -0
  125. package/components/search/search.js +1 -1
  126. package/components/segment/segment.js +1 -1
  127. package/components/segmented/segmented.js +1 -1
  128. package/components/select/select.a2ui.json +58 -4
  129. package/components/select/{class.js → select.class.js} +415 -6
  130. package/components/select/select.css +158 -0
  131. package/components/select/select.d.ts +31 -1
  132. package/components/select/select.js +1 -1
  133. package/components/select/select.test.js +202 -0
  134. package/components/select/select.yaml +126 -5
  135. package/components/skeleton/skeleton.js +1 -1
  136. package/components/slider/slider.js +1 -1
  137. package/components/spinner/spinner.a2ui.json +3 -2
  138. package/components/spinner/{class.js → spinner.class.js} +33 -3
  139. package/components/spinner/spinner.css +91 -35
  140. package/components/spinner/spinner.d.ts +2 -2
  141. package/components/spinner/spinner.js +1 -1
  142. package/components/spinner/spinner.test.js +49 -11
  143. package/components/spinner/spinner.yaml +9 -1
  144. package/components/stack/stack.js +1 -1
  145. package/components/step-progress/step-progress.js +1 -1
  146. package/components/stepper/stepper-item.yaml +1 -1
  147. package/components/stepper/stepper.js +1 -1
  148. package/components/stream/stream.js +1 -1
  149. package/components/swatch/swatch.js +1 -1
  150. package/components/swiper/swiper.js +1 -1
  151. package/components/switch/switch.js +1 -1
  152. package/components/table/table.css +1 -1
  153. package/components/table/table.js +1 -1
  154. package/components/table-toolbar/{class.js → table-toolbar.class.js} +2 -1
  155. package/components/table-toolbar/table-toolbar.js +1 -1
  156. package/components/tabs/tab.yaml +2 -2
  157. package/components/tabs/tabs.js +1 -1
  158. package/components/tag/tag.a2ui.json +9 -0
  159. package/components/tag/{class.js → tag.class.js} +8 -1
  160. package/components/tag/tag.css +84 -20
  161. package/components/tag/tag.js +1 -1
  162. package/components/tag/tag.test.js +75 -1
  163. package/components/tag/tag.yaml +14 -0
  164. package/components/tags-input/tags-input.a2ui.json +337 -0
  165. package/components/tags-input/tags-input.class.js +783 -0
  166. package/components/tags-input/tags-input.css +210 -0
  167. package/components/tags-input/tags-input.d.ts +120 -0
  168. package/components/tags-input/tags-input.examples.md +92 -0
  169. package/components/tags-input/tags-input.js +17 -0
  170. package/components/tags-input/tags-input.test.js +368 -0
  171. package/components/tags-input/tags-input.yaml +367 -0
  172. package/components/text/text.js +1 -1
  173. package/components/textarea/textarea.a2ui.json +1 -1
  174. package/components/textarea/textarea.css +10 -1
  175. package/components/textarea/textarea.js +1 -1
  176. package/components/textarea/textarea.yaml +11 -8
  177. package/components/time-picker/time-picker.js +1 -1
  178. package/components/timeline/timeline-item.yaml +2 -2
  179. package/components/timeline/{class.js → timeline.class.js} +1 -1
  180. package/components/timeline/timeline.js +1 -1
  181. package/components/toast/toast.js +1 -1
  182. package/components/toggle-group/toggle-group.js +1 -1
  183. package/components/toggle-group/toggle-option.yaml +1 -1
  184. package/components/toggle-scheme/toggle-scheme.js +1 -1
  185. package/components/toolbar/toolbar-group.yaml +1 -1
  186. package/components/toolbar/toolbar.js +1 -1
  187. package/components/tooltip/tooltip.js +1 -1
  188. package/components/tree/tree-item.yaml +1 -1
  189. package/components/tree/tree.js +1 -1
  190. package/components/upload/upload.js +1 -1
  191. package/core/provider.js +19 -2
  192. package/dist/web-components.min.css +1 -1
  193. package/dist/web-components.min.js +112 -90
  194. package/package.json +3 -3
  195. package/styles/components.css +3 -0
  196. /package/components/accordion/{class.js → accordion.class.js} +0 -0
  197. /package/components/action-list/{class.js → action-list.class.js} +0 -0
  198. /package/components/agent-feedback-bar/{class.js → agent-feedback-bar.class.js} +0 -0
  199. /package/components/agent-questions/{class.js → agent-questions.class.js} +0 -0
  200. /package/components/agent-reasoning/{class.js → agent-reasoning.class.js} +0 -0
  201. /package/components/agent-suggestions/{class.js → agent-suggestions.class.js} +0 -0
  202. /package/components/avatar/{class.js → avatar.class.js} +0 -0
  203. /package/components/badge/{class.js → badge.class.js} +0 -0
  204. /package/components/block/{class.js → block.class.js} +0 -0
  205. /package/components/breadcrumb/{class.js → breadcrumb.class.js} +0 -0
  206. /package/components/button/{class.js → button.class.js} +0 -0
  207. /package/components/calendar-picker/{class.js → calendar-picker.class.js} +0 -0
  208. /package/components/card/{class.js → card.class.js} +0 -0
  209. /package/components/chart/{class.js → chart.class.js} +0 -0
  210. /package/components/chart-legend/{class.js → chart-legend.class.js} +0 -0
  211. /package/components/chat-thread/{class.js → chat-thread.class.js} +0 -0
  212. /package/components/check/{class.js → check.class.js} +0 -0
  213. /package/components/code/{class.js → code.class.js} +0 -0
  214. /package/components/col/{class.js → col.class.js} +0 -0
  215. /package/components/color-input/{class.js → color-input.class.js} +0 -0
  216. /package/components/color-picker/{class.js → color-picker.class.js} +0 -0
  217. /package/components/combobox/{class.js → combobox.class.js} +0 -0
  218. /package/components/command/{class.js → command.class.js} +0 -0
  219. /package/components/demo-toggle/{class.js → demo-toggle.class.js} +0 -0
  220. /package/components/description-list/{class.js → description-list.class.js} +0 -0
  221. /package/components/divider/{class.js → divider.class.js} +0 -0
  222. /package/components/drawer/{class.js → drawer.class.js} +0 -0
  223. /package/components/embed/{class.js → embed.class.js} +0 -0
  224. /package/components/empty-state/{class.js → empty-state.class.js} +0 -0
  225. /package/components/feed/{class.js → feed.class.js} +0 -0
  226. /package/components/field/{class.js → field.class.js} +0 -0
  227. /package/components/fields/{class.js → fields.class.js} +0 -0
  228. /package/components/grid/{class.js → grid.class.js} +0 -0
  229. /package/components/heatmap/{class.js → heatmap.class.js} +0 -0
  230. /package/components/icon/{class.js → icon.class.js} +0 -0
  231. /package/components/image/{class.js → image.class.js} +0 -0
  232. /package/components/input/{class.js → input.class.js} +0 -0
  233. /package/components/inspector/{class.js → inspector.class.js} +0 -0
  234. /package/components/integration-card/{class.js → integration-card.class.js} +0 -0
  235. /package/components/kbd/{class.js → kbd.class.js} +0 -0
  236. /package/components/link/{class.js → link.class.js} +0 -0
  237. /package/components/list/{class.js → list.class.js} +0 -0
  238. /package/components/list-window/{class.js → list-window.class.js} +0 -0
  239. /package/components/menu/{class.js → menu.class.js} +0 -0
  240. /package/components/nav/{class.js → nav.class.js} +0 -0
  241. /package/components/nav-group/{class.js → nav-group.class.js} +0 -0
  242. /package/components/nav-item/{class.js → nav-item.class.js} +0 -0
  243. /package/components/noodles/{class.js → noodles.class.js} +0 -0
  244. /package/components/option-card/{class.js → option-card.class.js} +0 -0
  245. /package/components/otp-input/{class.js → otp-input.class.js} +0 -0
  246. /package/components/page/{class.js → page.class.js} +0 -0
  247. /package/components/pagination/{class.js → pagination.class.js} +0 -0
  248. /package/components/pane/{class.js → pane.class.js} +0 -0
  249. /package/components/pipeline-status/{class.js → pipeline-status.class.js} +0 -0
  250. /package/components/popover/{class.js → popover.class.js} +0 -0
  251. /package/components/progress/{class.js → progress.class.js} +0 -0
  252. /package/components/progress-row/{class.js → progress-row.class.js} +0 -0
  253. /package/components/radio/{class.js → radio.class.js} +0 -0
  254. /package/components/range/{class.js → range.class.js} +0 -0
  255. /package/components/rating/{class.js → rating.class.js} +0 -0
  256. /package/components/richtext/{class.js → richtext.class.js} +0 -0
  257. /package/components/row/{class.js → row.class.js} +0 -0
  258. /package/components/segment/{class.js → segment.class.js} +0 -0
  259. /package/components/segmented/{class.js → segmented.class.js} +0 -0
  260. /package/components/skeleton/{class.js → skeleton.class.js} +0 -0
  261. /package/components/slider/{class.js → slider.class.js} +0 -0
  262. /package/components/stack/{class.js → stack.class.js} +0 -0
  263. /package/components/step-progress/{class.js → step-progress.class.js} +0 -0
  264. /package/components/stepper/{class.js → stepper.class.js} +0 -0
  265. /package/components/stream/{class.js → stream.class.js} +0 -0
  266. /package/components/swatch/{class.js → swatch.class.js} +0 -0
  267. /package/components/swiper/{class.js → swiper.class.js} +0 -0
  268. /package/components/switch/{class.js → switch.class.js} +0 -0
  269. /package/components/table/{class.js → table.class.js} +0 -0
  270. /package/components/tabs/{class.js → tabs.class.js} +0 -0
  271. /package/components/text/{class.js → text.class.js} +0 -0
  272. /package/components/textarea/{class.js → textarea.class.js} +0 -0
  273. /package/components/time-picker/{class.js → time-picker.class.js} +0 -0
  274. /package/components/toast/{class.js → toast.class.js} +0 -0
  275. /package/components/toggle-group/{class.js → toggle-group.class.js} +0 -0
  276. /package/components/toggle-scheme/{class.js → toggle-scheme.class.js} +0 -0
  277. /package/components/toolbar/{class.js → toolbar.class.js} +0 -0
  278. /package/components/tooltip/{class.js → tooltip.class.js} +0 -0
  279. /package/components/tree/{class.js → tree.class.js} +0 -0
  280. /package/components/upload/{class.js → upload.class.js} +0 -0
@@ -0,0 +1,368 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import '../../core/element.js';
3
+ import '../tag/tag.js';
4
+ import './tags-input.js';
5
+
6
+ const tick = () => new Promise((r) => queueMicrotask(r));
7
+
8
+ function mount(html) {
9
+ const wrap = document.createElement('div');
10
+ wrap.innerHTML = html;
11
+ document.body.appendChild(wrap);
12
+ return wrap.firstElementChild;
13
+ }
14
+
15
+ function typeText(el, text) {
16
+ // Drive the contenteditable surface like a real keyboard. Append the
17
+ // character into textContent + fire `input` so the host runs through its
18
+ // own input handler.
19
+ const surface = el.querySelector(':scope > [data-inline-input]');
20
+ surface.textContent = (surface.textContent || '') + text;
21
+ surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
22
+ }
23
+
24
+ function setText(el, text) {
25
+ // Like typeText but REPLACES the buffer. Use to simulate a user clearing
26
+ // a failed attempt and starting over.
27
+ const surface = el.querySelector(':scope > [data-inline-input]');
28
+ surface.textContent = text;
29
+ surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
30
+ }
31
+
32
+ function pressKey(el, key) {
33
+ const surface = el.querySelector(':scope > [data-inline-input]');
34
+ surface.focus();
35
+ surface.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
36
+ }
37
+
38
+ describe('tags-input-ui — structure + defaults', () => {
39
+ beforeEach(() => { document.body.innerHTML = ''; });
40
+
41
+ it('registers the custom element', () => {
42
+ expect(customElements.get('tags-input-ui')).toBeTruthy();
43
+ });
44
+
45
+ it('stamps a chip list, inline input, spinner, and suggestion popover', async () => {
46
+ const el = mount('<tags-input-ui></tags-input-ui>');
47
+ await tick();
48
+ expect(el.querySelector(':scope > [data-chip-list]')).toBeTruthy();
49
+ expect(el.querySelector(':scope > [data-inline-input]')).toBeTruthy();
50
+ expect(el.querySelector(':scope > [data-spinner]')).toBeTruthy();
51
+ expect(el.querySelector(':scope > [data-suggestions]')).toBeTruthy();
52
+ });
53
+
54
+ it('host carries role=group by default (no suggestions wired)', async () => {
55
+ const el = mount('<tags-input-ui></tags-input-ui>');
56
+ await tick();
57
+ expect(el.getAttribute('role')).toBe('group');
58
+ });
59
+
60
+ it('inline input carries role=searchbox + plaintext contenteditable', async () => {
61
+ const el = mount('<tags-input-ui></tags-input-ui>');
62
+ await tick();
63
+ const input = el.querySelector(':scope > [data-inline-input]');
64
+ expect(input.getAttribute('role')).toBe('searchbox');
65
+ expect(input.getAttribute('contenteditable')).toBe('plaintext-only');
66
+ });
67
+ });
68
+
69
+ describe('tags-input-ui — commit behavior', () => {
70
+ beforeEach(() => { document.body.innerHTML = ''; });
71
+
72
+ it('Enter commits the typed value as a chip', async () => {
73
+ const el = mount('<tags-input-ui></tags-input-ui>');
74
+ await tick();
75
+ typeText(el, 'hello');
76
+ pressKey(el, 'Enter');
77
+ await tick();
78
+ expect(el.value).toEqual(['hello']);
79
+ expect(el.querySelectorAll(':scope > [data-chip-list] > tag-ui').length).toBe(1);
80
+ });
81
+
82
+ it('default comma delimiter commits inline', async () => {
83
+ const el = mount('<tags-input-ui></tags-input-ui>');
84
+ await tick();
85
+ typeText(el, 'foo,');
86
+ await tick();
87
+ expect(el.value).toEqual(['foo']);
88
+ });
89
+
90
+ it('delimiter="enter" disables in-line comma commit', async () => {
91
+ const el = mount('<tags-input-ui delimiter="enter"></tags-input-ui>');
92
+ await tick();
93
+ typeText(el, 'foo,bar');
94
+ await tick();
95
+ // Nothing committed yet — the comma is treated as literal text.
96
+ expect(el.value).toEqual([]);
97
+ pressKey(el, 'Enter');
98
+ await tick();
99
+ expect(el.value).toEqual(['foo,bar']);
100
+ });
101
+
102
+ it('Backspace from empty input removes the last chip', async () => {
103
+ const el = mount('<tags-input-ui></tags-input-ui>');
104
+ await tick();
105
+ el.value = ['alpha', 'beta'];
106
+ await tick();
107
+ expect(el.value).toEqual(['alpha', 'beta']);
108
+ pressKey(el, 'Backspace');
109
+ await tick();
110
+ expect(el.value).toEqual(['alpha']);
111
+ });
112
+
113
+ it('Backspace with non-empty input only deletes a character (no chip removal)', async () => {
114
+ const el = mount('<tags-input-ui></tags-input-ui>');
115
+ await tick();
116
+ el.value = ['alpha'];
117
+ typeText(el, 'wip');
118
+ await tick();
119
+ pressKey(el, 'Backspace');
120
+ await tick();
121
+ // Chip unchanged.
122
+ expect(el.value).toEqual(['alpha']);
123
+ });
124
+
125
+ it('unique:true (default) silently dedups duplicate commits', async () => {
126
+ const el = mount('<tags-input-ui></tags-input-ui>');
127
+ await tick();
128
+ let invalidCount = 0;
129
+ el.addEventListener('invalid', () => { invalidCount += 1; });
130
+ typeText(el, 'foo');
131
+ pressKey(el, 'Enter');
132
+ await tick();
133
+ typeText(el, 'foo');
134
+ pressKey(el, 'Enter');
135
+ await tick();
136
+ expect(el.value).toEqual(['foo']);
137
+ expect(invalidCount).toBe(0);
138
+ });
139
+
140
+ it('min-length rejects too-short tokens with reason=too-short', async () => {
141
+ const el = mount('<tags-input-ui min-length="3"></tags-input-ui>');
142
+ await tick();
143
+ let detail = null;
144
+ el.addEventListener('invalid', (e) => { detail = e.detail; });
145
+ typeText(el, 'ab');
146
+ pressKey(el, 'Enter');
147
+ await tick();
148
+ expect(el.value).toEqual([]);
149
+ expect(detail?.reason).toBe('too-short');
150
+ });
151
+
152
+ it('max-length rejects too-long tokens with reason=too-long', async () => {
153
+ const el = mount('<tags-input-ui max-length="5"></tags-input-ui>');
154
+ await tick();
155
+ let detail = null;
156
+ el.addEventListener('invalid', (e) => { detail = e.detail; });
157
+ typeText(el, 'too-long-value');
158
+ pressKey(el, 'Enter');
159
+ await tick();
160
+ expect(el.value).toEqual([]);
161
+ expect(detail?.reason).toBe('too-long');
162
+ });
163
+
164
+ it('max count cap rejects further commits with reason=max', async () => {
165
+ const el = mount('<tags-input-ui max="2"></tags-input-ui>');
166
+ await tick();
167
+ let invalidDetail = null;
168
+ el.addEventListener('invalid', (e) => { invalidDetail = e.detail; });
169
+ el.addToken('one');
170
+ el.addToken('two');
171
+ expect(el.value).toEqual(['one', 'two']);
172
+ typeText(el, 'three');
173
+ pressKey(el, 'Enter');
174
+ await tick();
175
+ expect(el.value).toEqual(['one', 'two']);
176
+ expect(invalidDetail?.reason).toBe('max');
177
+ });
178
+
179
+ it('transform="lowercase" normalizes tokens on commit', async () => {
180
+ const el = mount('<tags-input-ui transform="lowercase"></tags-input-ui>');
181
+ await tick();
182
+ el.addToken('FOO');
183
+ el.addToken('Bar');
184
+ expect(el.value).toEqual(['foo', 'bar']);
185
+ });
186
+
187
+ it('transform="strip-spaces" removes all whitespace before commit', async () => {
188
+ const el = mount('<tags-input-ui transform="strip-spaces"></tags-input-ui>');
189
+ await tick();
190
+ el.addToken('hello world');
191
+ expect(el.value).toEqual(['helloworld']);
192
+ });
193
+ });
194
+
195
+ describe('tags-input-ui — validators', () => {
196
+ beforeEach(() => { document.body.innerHTML = ''; });
197
+
198
+ it('sync validateFn rejects invalid tokens with reason=validator', async () => {
199
+ const el = mount('<tags-input-ui></tags-input-ui>');
200
+ await tick();
201
+ el.validateFn = (v) => /^[a-z]+$/.test(v);
202
+ let detail = null;
203
+ el.addEventListener('invalid', (e) => { detail = e.detail; });
204
+ typeText(el, '123');
205
+ pressKey(el, 'Enter');
206
+ await tick();
207
+ expect(el.value).toEqual([]);
208
+ expect(detail?.reason).toBe('validator');
209
+ // Accepts valid input.
210
+ setText(el, 'ok');
211
+ pressKey(el, 'Enter');
212
+ await tick();
213
+ expect(el.value).toEqual(['ok']);
214
+ });
215
+
216
+ it('async validateFn flips host into [validating] then commits on resolve(true)', async () => {
217
+ const el = mount('<tags-input-ui></tags-input-ui>');
218
+ await tick();
219
+ let resolveCheck;
220
+ el.validateFn = () => new Promise((r) => { resolveCheck = r; });
221
+ typeText(el, 'pending');
222
+ pressKey(el, 'Enter');
223
+ await tick();
224
+ expect(el.hasAttribute('validating')).toBe(true);
225
+ expect(el.value).toEqual([]);
226
+ resolveCheck(true);
227
+ await tick();
228
+ await tick();
229
+ expect(el.hasAttribute('validating')).toBe(false);
230
+ expect(el.value).toEqual(['pending']);
231
+ });
232
+
233
+ it('commit event with preventDefault() rejects the token (reason=validator)', async () => {
234
+ const el = mount('<tags-input-ui></tags-input-ui>');
235
+ await tick();
236
+ el.addEventListener('commit', (e) => e.preventDefault());
237
+ let detail = null;
238
+ el.addEventListener('invalid', (e) => { detail = e.detail; });
239
+ typeText(el, 'blocked');
240
+ pressKey(el, 'Enter');
241
+ await tick();
242
+ expect(el.value).toEqual([]);
243
+ expect(detail?.reason).toBe('validator');
244
+ });
245
+ });
246
+
247
+ describe('tags-input-ui — paste-split', () => {
248
+ beforeEach(() => { document.body.innerHTML = ''; });
249
+
250
+ it('pasting "a, b, c" with default paste-split commits three tokens', async () => {
251
+ const el = mount('<tags-input-ui></tags-input-ui>');
252
+ await tick();
253
+ const surface = el.querySelector(':scope > [data-inline-input]');
254
+ const dt = new DataTransfer();
255
+ dt.setData('text/plain', 'a, b, c');
256
+ surface.dispatchEvent(new ClipboardEvent('paste', {
257
+ bubbles: true,
258
+ cancelable: true,
259
+ clipboardData: dt,
260
+ }));
261
+ await tick();
262
+ expect(el.value).toEqual(['a', 'b', 'c']);
263
+ });
264
+ });
265
+
266
+ describe('tags-input-ui — disabled / readonly / form', () => {
267
+ beforeEach(() => { document.body.innerHTML = ''; });
268
+
269
+ it('disabled blocks all interaction', async () => {
270
+ const el = mount('<tags-input-ui disabled></tags-input-ui>');
271
+ await tick();
272
+ typeText(el, 'nope');
273
+ pressKey(el, 'Enter');
274
+ await tick();
275
+ expect(el.value).toEqual([]);
276
+ });
277
+
278
+ it('readonly blocks edits but allows inspection (chips lack [removable])', async () => {
279
+ const el = mount('<tags-input-ui readonly value=\'["frozen"]\'></tags-input-ui>');
280
+ await tick();
281
+ expect(el.value).toEqual(['frozen']);
282
+ const chip = el.querySelector(':scope > [data-chip-list] > tag-ui');
283
+ expect(chip).toBeTruthy();
284
+ expect(chip.hasAttribute('removable')).toBe(false);
285
+ });
286
+
287
+ it('exposes UITagsInput extending UIFormElement (form participation)', async () => {
288
+ const el = mount('<tags-input-ui name="x"></tags-input-ui>');
289
+ await tick();
290
+ expect(typeof el.checkValidity).toBe('function');
291
+ expect(typeof el.syncValue).toBe('function');
292
+ });
293
+
294
+ it('form value serializes as JSON array under [name]', async () => {
295
+ const el = mount('<tags-input-ui name="labels"></tags-input-ui>');
296
+ await tick();
297
+ el.addToken('bug');
298
+ el.addToken('high-priority');
299
+ // happy-dom's ElementInternals tracks form value via setFormValue — read
300
+ // it back via the public `value` accessor + assert via internals' state.
301
+ expect(el.value).toEqual(['bug', 'high-priority']);
302
+ expect(el.checkValidity()).toBe(true);
303
+ });
304
+
305
+ it('required attribute reflects on the host (happy-dom internals stub limits deeper assertions)', async () => {
306
+ const el = mount('<tags-input-ui required name="x"></tags-input-ui>');
307
+ await tick();
308
+ expect(el.required).toBe(true);
309
+ expect(el.hasAttribute('required')).toBe(true);
310
+ // ElementInternals.setValidity is a no-op in happy-dom, so we can't
311
+ // assert validity.valueMissing here — gate that in browser smokes.
312
+ el.addToken('one');
313
+ expect(el.value).toEqual(['one']);
314
+ });
315
+
316
+ it('parses declarative value="[…]" as a JSON array', async () => {
317
+ const el = mount('<tags-input-ui value=\'["a","b","c"]\'></tags-input-ui>');
318
+ await tick();
319
+ expect(el.value).toEqual(['a', 'b', 'c']);
320
+ });
321
+ });
322
+
323
+ describe('tags-input-ui — change event', () => {
324
+ beforeEach(() => { document.body.innerHTML = ''; });
325
+
326
+ it('change event detail carries value + added + removed', async () => {
327
+ const el = mount('<tags-input-ui></tags-input-ui>');
328
+ await tick();
329
+ const events = [];
330
+ el.addEventListener('change', (e) => events.push(e.detail));
331
+ el.addToken('first');
332
+ el.addToken('second');
333
+ await tick();
334
+ expect(events.length).toBe(2);
335
+ expect(events[0]).toMatchObject({ added: ['first'], removed: [] });
336
+ expect(events[1]).toMatchObject({ added: ['second'], removed: [] });
337
+ expect(events[1].value).toEqual(['first', 'second']);
338
+ el.removeToken(0);
339
+ await tick();
340
+ expect(events[2]).toMatchObject({ added: [], removed: ['first'] });
341
+ });
342
+ });
343
+
344
+ describe('tags-input-ui — suggestions', () => {
345
+ beforeEach(() => { document.body.innerHTML = ''; });
346
+
347
+ it('host carries role=combobox when .suggestions is non-empty', async () => {
348
+ const el = mount('<tags-input-ui></tags-input-ui>');
349
+ await tick();
350
+ el.suggestions = ['alpha', 'beta', 'gamma'];
351
+ await tick();
352
+ expect(el.getAttribute('role')).toBe('combobox');
353
+ expect(el.getAttribute('aria-haspopup')).toBe('listbox');
354
+ });
355
+
356
+ it('clicking a suggestion option commits it', async () => {
357
+ const el = mount('<tags-input-ui></tags-input-ui>');
358
+ await tick();
359
+ el.suggestions = ['alpha', 'beta'];
360
+ typeText(el, 'a');
361
+ await tick();
362
+ const opt = el.querySelector(':scope > [data-suggestions] > [role="option"][data-value="alpha"]');
363
+ expect(opt).toBeTruthy();
364
+ opt.click();
365
+ await tick();
366
+ expect(el.value).toEqual(['alpha']);
367
+ });
368
+ });