@api-client/ui 0.2.3 → 0.2.4

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 (203) hide show
  1. package/.vscode/settings.json +3 -3
  2. package/build/src/elements/authorization/ui/ApiKeyAuthorization.d.ts +1 -1
  3. package/build/src/elements/authorization/ui/ApiKeyAuthorization.d.ts.map +1 -1
  4. package/build/src/elements/authorization/ui/ApiKeyAuthorization.js +7 -7
  5. package/build/src/elements/authorization/ui/ApiKeyAuthorization.js.map +1 -1
  6. package/build/src/elements/authorization/ui/Authorization.styles.js +4 -4
  7. package/build/src/elements/authorization/ui/Authorization.styles.js.map +1 -1
  8. package/build/src/elements/authorization/ui/BasicAuthorization.d.ts +1 -1
  9. package/build/src/elements/authorization/ui/BasicAuthorization.d.ts.map +1 -1
  10. package/build/src/elements/authorization/ui/BasicAuthorization.js +5 -5
  11. package/build/src/elements/authorization/ui/BasicAuthorization.js.map +1 -1
  12. package/build/src/elements/authorization/ui/BearerAuthorization.d.ts +1 -1
  13. package/build/src/elements/authorization/ui/BearerAuthorization.d.ts.map +1 -1
  14. package/build/src/elements/authorization/ui/BearerAuthorization.js +3 -3
  15. package/build/src/elements/authorization/ui/BearerAuthorization.js.map +1 -1
  16. package/build/src/elements/authorization/ui/NtlmAuthorization.d.ts +1 -1
  17. package/build/src/elements/authorization/ui/NtlmAuthorization.d.ts.map +1 -1
  18. package/build/src/elements/authorization/ui/NtlmAuthorization.js +7 -7
  19. package/build/src/elements/authorization/ui/NtlmAuthorization.js.map +1 -1
  20. package/build/src/elements/authorization/ui/OAuth2Authorization.d.ts +1 -1
  21. package/build/src/elements/authorization/ui/OAuth2Authorization.d.ts.map +1 -1
  22. package/build/src/elements/authorization/ui/OAuth2Authorization.js +32 -27
  23. package/build/src/elements/authorization/ui/OAuth2Authorization.js.map +1 -1
  24. package/build/src/elements/authorization/ui/OidcAuthorization.js +4 -4
  25. package/build/src/elements/authorization/ui/OidcAuthorization.js.map +1 -1
  26. package/build/src/elements/autocomplete/autocomplete-input.d.ts +10 -0
  27. package/build/src/elements/autocomplete/autocomplete-input.d.ts.map +1 -0
  28. package/build/src/{md/text-field/ui-text-field.js → elements/autocomplete/autocomplete-input.js} +9 -9
  29. package/build/src/elements/autocomplete/autocomplete-input.js.map +1 -0
  30. package/build/src/elements/autocomplete/internals/autocomplete.d.ts +209 -0
  31. package/build/src/elements/autocomplete/internals/autocomplete.d.ts.map +1 -0
  32. package/build/src/elements/autocomplete/internals/autocomplete.js +493 -0
  33. package/build/src/elements/autocomplete/internals/autocomplete.js.map +1 -0
  34. package/build/src/elements/autocomplete/internals/autocomplete.styles.d.ts +3 -0
  35. package/build/src/elements/autocomplete/internals/autocomplete.styles.d.ts.map +1 -0
  36. package/build/src/elements/autocomplete/internals/autocomplete.styles.js +25 -0
  37. package/build/src/elements/autocomplete/internals/autocomplete.styles.js.map +1 -0
  38. package/build/src/elements/dialog/internals/DeleteCookieAction.element.d.ts +1 -1
  39. package/build/src/elements/dialog/internals/DeleteCookieAction.element.d.ts.map +1 -1
  40. package/build/src/elements/dialog/internals/DeleteCookieAction.element.js +5 -5
  41. package/build/src/elements/dialog/internals/DeleteCookieAction.element.js.map +1 -1
  42. package/build/src/elements/dialog/internals/Rename.d.ts +1 -1
  43. package/build/src/elements/dialog/internals/Rename.d.ts.map +1 -1
  44. package/build/src/elements/dialog/internals/Rename.js +3 -3
  45. package/build/src/elements/dialog/internals/Rename.js.map +1 -1
  46. package/build/src/elements/dialog/internals/SetCookieAction.element.d.ts +1 -1
  47. package/build/src/elements/dialog/internals/SetCookieAction.element.d.ts.map +1 -1
  48. package/build/src/elements/dialog/internals/SetCookieAction.element.js +9 -9
  49. package/build/src/elements/dialog/internals/SetCookieAction.element.js.map +1 -1
  50. package/build/src/elements/environment/EnvironmentEditor.d.ts +1 -1
  51. package/build/src/elements/environment/EnvironmentEditor.d.ts.map +1 -1
  52. package/build/src/elements/environment/EnvironmentEditor.js +3 -3
  53. package/build/src/elements/environment/EnvironmentEditor.js.map +1 -1
  54. package/build/src/elements/environment/EnvironmentEditor.styles.js +1 -1
  55. package/build/src/elements/environment/EnvironmentEditor.styles.js.map +1 -1
  56. package/build/src/elements/environment/ServerEditor.d.ts +1 -1
  57. package/build/src/elements/environment/ServerEditor.d.ts.map +1 -1
  58. package/build/src/elements/environment/ServerEditor.js +7 -7
  59. package/build/src/elements/environment/ServerEditor.js.map +1 -1
  60. package/build/src/elements/environment/ServerEditor.styles.js +1 -1
  61. package/build/src/elements/environment/ServerEditor.styles.js.map +1 -1
  62. package/build/src/elements/http/BodyMultipartEditor.d.ts.map +1 -1
  63. package/build/src/elements/http/BodyMultipartEditor.js +4 -0
  64. package/build/src/elements/http/BodyMultipartEditor.js.map +1 -1
  65. package/build/src/elements/http/CertificateAdd.element.d.ts +1 -1
  66. package/build/src/elements/http/CertificateAdd.element.d.ts.map +1 -1
  67. package/build/src/elements/http/CertificateAdd.element.js +8 -8
  68. package/build/src/elements/http/CertificateAdd.element.js.map +1 -1
  69. package/build/src/elements/http/CertificateAdd.styles.js +1 -1
  70. package/build/src/elements/http/CertificateAdd.styles.js.map +1 -1
  71. package/build/src/elements/http/HttpAssertions.element.js +3 -3
  72. package/build/src/elements/http/HttpAssertions.element.js.map +1 -1
  73. package/build/src/elements/http/HttpFlows.element.js +3 -3
  74. package/build/src/elements/http/HttpFlows.element.js.map +1 -1
  75. package/build/src/elements/http/HttpFlowsUi.d.ts +1 -1
  76. package/build/src/elements/http/HttpFlowsUi.d.ts.map +1 -1
  77. package/build/src/elements/http/HttpFlowsUi.js +31 -31
  78. package/build/src/elements/http/HttpFlowsUi.js.map +1 -1
  79. package/build/src/elements/http/RequestConfigElement.d.ts +1 -1
  80. package/build/src/elements/http/RequestConfigElement.d.ts.map +1 -1
  81. package/build/src/elements/http/RequestConfigElement.js +7 -7
  82. package/build/src/elements/http/RequestConfigElement.js.map +1 -1
  83. package/build/src/elements/http/UrlParamsForm.d.ts +1 -1
  84. package/build/src/elements/http/UrlParamsForm.d.ts.map +1 -1
  85. package/build/src/elements/http/UrlParamsForm.js +1 -1
  86. package/build/src/elements/http/UrlParamsForm.js.map +1 -1
  87. package/build/src/elements/project/ProjectRunner.d.ts +1 -1
  88. package/build/src/elements/project/ProjectRunner.d.ts.map +1 -1
  89. package/build/src/elements/project/ProjectRunner.js +5 -5
  90. package/build/src/elements/project/ProjectRunner.js.map +1 -1
  91. package/build/src/md/input/Input.d.ts +0 -15
  92. package/build/src/md/input/Input.d.ts.map +1 -1
  93. package/build/src/md/input/Input.js +7 -42
  94. package/build/src/md/input/Input.js.map +1 -1
  95. package/build/src/md/list/internals/List.d.ts +7 -2
  96. package/build/src/md/list/internals/List.d.ts.map +1 -1
  97. package/build/src/md/list/internals/List.js +6 -0
  98. package/build/src/md/list/internals/List.js.map +1 -1
  99. package/build/src/md/list/internals/ListItem.styles.d.ts.map +1 -1
  100. package/build/src/md/list/internals/ListItem.styles.js +8 -0
  101. package/build/src/md/list/internals/ListItem.styles.js.map +1 -1
  102. package/build/src/md/listbox/internals/Listbox.d.ts +2 -2
  103. package/build/src/md/listbox/internals/Listbox.d.ts.map +1 -1
  104. package/build/src/md/listbox/internals/Listbox.js.map +1 -1
  105. package/build/src/md/text-area/internals/TextAreaElement.d.ts.map +1 -1
  106. package/build/src/md/text-area/internals/TextAreaElement.js +0 -5
  107. package/build/src/md/text-area/internals/TextAreaElement.js.map +1 -1
  108. package/build/src/md/text-area/ui-text-area.d.ts.map +1 -1
  109. package/build/src/md/text-area/ui-text-area.js +3 -2
  110. package/build/src/md/text-area/ui-text-area.js.map +1 -1
  111. package/build/src/md/text-field/internals/{TextFieldElement.d.ts → TextField.d.ts} +2 -2
  112. package/build/src/md/text-field/internals/TextField.d.ts.map +1 -0
  113. package/build/src/md/text-field/internals/{TextFieldElement.js → TextField.js} +2 -5
  114. package/build/src/md/text-field/internals/TextField.js.map +1 -0
  115. package/build/src/md/text-field/internals/{TextField.styles.d.ts → common.styles.d.ts} +1 -1
  116. package/build/src/md/text-field/internals/common.styles.d.ts.map +1 -0
  117. package/build/src/md/text-field/internals/{TextField.styles.js → common.styles.js} +8 -94
  118. package/build/src/md/text-field/internals/common.styles.js.map +1 -0
  119. package/build/src/md/text-field/internals/filled.styles.d.ts +3 -0
  120. package/build/src/md/text-field/internals/filled.styles.d.ts.map +1 -0
  121. package/build/src/md/text-field/internals/filled.styles.js +107 -0
  122. package/build/src/md/text-field/internals/filled.styles.js.map +1 -0
  123. package/build/src/md/text-field/internals/outlined.styles.d.ts +3 -0
  124. package/build/src/md/text-field/internals/outlined.styles.d.ts.map +1 -0
  125. package/build/src/md/text-field/internals/outlined.styles.js +43 -0
  126. package/build/src/md/text-field/internals/outlined.styles.js.map +1 -0
  127. package/build/src/md/text-field/ui-filled-text-field.d.ts +11 -0
  128. package/build/src/md/text-field/ui-filled-text-field.d.ts.map +1 -0
  129. package/build/src/md/text-field/ui-filled-text-field.js +28 -0
  130. package/build/src/md/text-field/ui-filled-text-field.js.map +1 -0
  131. package/build/src/md/text-field/ui-outlined-text-field.d.ts +11 -0
  132. package/build/src/md/text-field/ui-outlined-text-field.d.ts.map +1 -0
  133. package/build/src/md/text-field/ui-outlined-text-field.js +28 -0
  134. package/build/src/md/text-field/ui-outlined-text-field.js.map +1 -0
  135. package/build/src/types/input.d.ts +1 -1
  136. package/build/src/types/input.d.ts.map +1 -1
  137. package/build/src/types/input.js.map +1 -1
  138. package/demo/elements/authorization/oauth-authorize.html +4 -4
  139. package/demo/elements/authorization/oauth-authorize.ts +1 -1
  140. package/demo/elements/autocomplete/index.html +24 -0
  141. package/demo/elements/autocomplete/index.ts +123 -0
  142. package/demo/elements/http/body-editor.ts +3 -3
  143. package/demo/elements/index.html +15 -11
  144. package/demo/md/index.html +1 -1
  145. package/demo/md/inputs/input.html +10 -15
  146. package/demo/md/inputs/input.ts +389 -101
  147. package/demo/page.css +4 -0
  148. package/package.json +1 -1
  149. package/src/elements/authorization/ui/ApiKeyAuthorization.ts +7 -7
  150. package/src/elements/authorization/ui/Authorization.styles.ts +4 -4
  151. package/src/elements/authorization/ui/BasicAuthorization.ts +5 -5
  152. package/src/elements/authorization/ui/BearerAuthorization.ts +3 -3
  153. package/src/elements/authorization/ui/NtlmAuthorization.ts +7 -7
  154. package/src/elements/authorization/ui/OAuth2Authorization.ts +32 -27
  155. package/src/elements/authorization/ui/OidcAuthorization.ts +4 -4
  156. package/src/elements/autocomplete/autocomplete-input.ts +14 -0
  157. package/src/elements/autocomplete/internals/autocomplete.styles.ts +25 -0
  158. package/src/elements/autocomplete/internals/autocomplete.ts +490 -0
  159. package/src/elements/dialog/internals/DeleteCookieAction.element.ts +5 -5
  160. package/src/elements/dialog/internals/Rename.ts +3 -3
  161. package/src/elements/dialog/internals/SetCookieAction.element.ts +9 -9
  162. package/src/elements/environment/EnvironmentEditor.styles.ts +1 -1
  163. package/src/elements/environment/EnvironmentEditor.ts +3 -3
  164. package/src/elements/environment/ServerEditor.styles.ts +1 -1
  165. package/src/elements/environment/ServerEditor.ts +7 -7
  166. package/src/elements/http/BodyMultipartEditor.ts +4 -0
  167. package/src/elements/http/CertificateAdd.element.ts +8 -8
  168. package/src/elements/http/CertificateAdd.styles.ts +1 -1
  169. package/src/elements/http/HttpAssertions.element.ts +3 -3
  170. package/src/elements/http/HttpFlows.element.ts +3 -3
  171. package/src/elements/http/HttpFlowsUi.ts +31 -31
  172. package/src/elements/http/RequestConfigElement.ts +7 -7
  173. package/src/elements/http/UrlParamsForm.ts +1 -1
  174. package/src/elements/project/ProjectRunner.ts +5 -5
  175. package/src/md/input/Input.ts +6 -21
  176. package/src/md/list/internals/List.ts +14 -2
  177. package/src/md/list/internals/ListItem.styles.ts +8 -0
  178. package/src/md/listbox/internals/Listbox.ts +2 -2
  179. package/src/md/text-area/internals/TextAreaElement.ts +0 -5
  180. package/src/md/text-area/ui-text-area.ts +3 -2
  181. package/src/md/text-field/internals/{TextFieldElement.ts → TextField.ts} +1 -4
  182. package/src/md/text-field/internals/{TextField.styles.ts → common.styles.ts} +7 -93
  183. package/src/md/text-field/internals/filled.styles.ts +107 -0
  184. package/src/md/text-field/internals/outlined.styles.ts +43 -0
  185. package/src/md/text-field/ui-filled-text-field.ts +16 -0
  186. package/src/md/text-field/ui-outlined-text-field.ts +16 -0
  187. package/src/types/input.ts +0 -1
  188. package/test/elements/authorization/basic-method.test.ts +3 -3
  189. package/test/elements/authorization/bearer-method.test.ts +2 -2
  190. package/test/elements/authorization/ntlm-method.test.ts +4 -4
  191. package/test/elements/autocomplete/autocomplete-input.spec.ts +448 -0
  192. package/test/elements/http/BodyMultipartEditorElement.test.ts +15 -16
  193. package/test/elements/http/CertificateAdd.test.ts +11 -11
  194. package/test/elements/http/HttpAssertions.test.ts +9 -9
  195. package/test/elements/http/HttpFlows.test.ts +4 -4
  196. package/build/src/md/text-field/internals/TextField.styles.d.ts.map +0 -1
  197. package/build/src/md/text-field/internals/TextField.styles.js.map +0 -1
  198. package/build/src/md/text-field/internals/TextFieldElement.d.ts.map +0 -1
  199. package/build/src/md/text-field/internals/TextFieldElement.js.map +0 -1
  200. package/build/src/md/text-field/ui-text-field.d.ts +0 -11
  201. package/build/src/md/text-field/ui-text-field.d.ts.map +0 -1
  202. package/build/src/md/text-field/ui-text-field.js.map +0 -1
  203. package/src/md/text-field/ui-text-field.ts +0 -15
@@ -0,0 +1,448 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-expressions */
2
+ import { fixture, expect, html, oneEvent, nextFrame, aTimeout } from '@open-wc/testing'
3
+ import sinon from 'sinon'
4
+
5
+ import { AutocompleteInput } from '../../../src/elements/autocomplete/autocomplete-input.js'
6
+ import '../../../src/elements/autocomplete/autocomplete-input.js' // Registers the element
7
+
8
+ import '../../../src/md/list/ui-list-item.js'
9
+ import '../../../src/md/listbox/ui-listbox.js'
10
+
11
+ import type MdListItem from '../../../src/md/list/internals/ListItem.js'
12
+ import type MdListbox from '../../../src/md/listbox/internals/Listbox.js'
13
+
14
+ describe('AutocompleteInput', () => {
15
+ async function basicFixture(): Promise<AutocompleteInput> {
16
+ return fixture(html`
17
+ <autocomplete-input>
18
+ <input id="test-input" slot="input" type="text" placeholder="Search..." />
19
+ <ui-listbox slot="suggestions">
20
+ <ui-list-item data-value="apple">Apple</ui-list-item>
21
+ <ui-list-item data-value="banana">Banana</ui-list-item>
22
+ <ui-list-item data-value="cherry" data-index="value customField" data-custom-field="Sweet Cherry"
23
+ >Cherry</ui-list-item
24
+ >
25
+ </ui-listbox>
26
+ </autocomplete-input>
27
+ `)
28
+ }
29
+
30
+ async function noSuggestionsFixture(): Promise<AutocompleteInput> {
31
+ return fixture(html`
32
+ <autocomplete-input>
33
+ <input slot="input" type="text" placeholder="Search..." />
34
+ <ui-listbox slot="suggestions"></ui-listbox>
35
+ </autocomplete-input>
36
+ `)
37
+ }
38
+
39
+ async function noListboxFixture(): Promise<AutocompleteInput> {
40
+ return fixture(html`
41
+ <autocomplete-input>
42
+ <input slot="input" type="text" placeholder="Search..." />
43
+ </autocomplete-input>
44
+ `)
45
+ }
46
+
47
+ function getSlottedInput(el: AutocompleteInput): HTMLInputElement | null {
48
+ const slot = el.shadowRoot!.querySelector('slot[name="input"]') as HTMLSlotElement
49
+ return (slot.assignedElements({ flatten: true })[0] as HTMLInputElement) || null
50
+ }
51
+
52
+ function getSlottedSuggestions(el: AutocompleteInput): MdListbox | null {
53
+ const slot = el.shadowRoot!.querySelector('slot[name="suggestions"]') as HTMLSlotElement
54
+ return (slot.assignedElements({ flatten: true })[0] as MdListbox) || null
55
+ }
56
+
57
+ function getSuggestionItems(suggestionsEl: MdListbox): MdListItem[] {
58
+ // Assuming MdListbox.items returns the slotted items that are MdListItem
59
+ return (suggestionsEl.items as MdListItem[]).filter(
60
+ (item) => item instanceof HTMLElement && item.matches('ui-list-item')
61
+ )
62
+ }
63
+
64
+ describe('Initialization', () => {
65
+ it('correctly identifies slotted input and suggestions', async () => {
66
+ const el = await basicFixture()
67
+ await el.updateComplete // Ensure firstUpdated has run
68
+
69
+ const input = getSlottedInput(el)
70
+ const suggestions = getSlottedSuggestions(el)
71
+
72
+ expect(input).to.exist
73
+ expect(input?.id).to.equal('test-input')
74
+ expect(suggestions).to.exist
75
+ expect(suggestions?.popover).to.equal('manual')
76
+ })
77
+
78
+ it('generates an ID for the input if not provided and sets up anchor names', async () => {
79
+ const el = (await fixture(html`
80
+ <autocomplete-input>
81
+ <input slot="input" type="text" />
82
+ <ui-listbox slot="suggestions"></ui-listbox>
83
+ </autocomplete-input>
84
+ `)) as AutocompleteInput
85
+ await el.updateComplete
86
+ await nextFrame() // Allow for internal state updates
87
+
88
+ const input = getSlottedInput(el)
89
+ const suggestions = getSlottedSuggestions(el)
90
+
91
+ expect(input?.id).to.match(/^autocomplete-input-/)
92
+ // @ts-expect-error _inputId is a protected member
93
+ const internalInputId = el.inputId
94
+ expect(input?.style.getPropertyValue('anchor-name')).to.equal(`--${internalInputId}`)
95
+ expect(suggestions?.style.getPropertyValue('position-anchor')).to.equal(`--${internalInputId}`)
96
+ })
97
+
98
+ it('popover is initially closed', async () => {
99
+ const el = await basicFixture()
100
+ await el.updateComplete
101
+ const suggestions = getSlottedSuggestions(el)
102
+ expect(suggestions?.matches(':popover-open')).to.be.false
103
+ expect(el.opened).to.be.false
104
+ })
105
+ })
106
+
107
+ describe('Popover behavior', () => {
108
+ it('opens on input focus with suggestions', async () => {
109
+ const el = await basicFixture()
110
+ await el.updateComplete
111
+ const input = getSlottedInput(el)!
112
+ const suggestions = getSlottedSuggestions(el)!
113
+
114
+ input.focus()
115
+ await nextFrame() // Allow focus event to propagate and popover to open
116
+
117
+ expect(suggestions.matches(':popover-open')).to.be.true
118
+ expect(el.opened).to.be.true
119
+ })
120
+
121
+ it('does not open on input focus without suggestions', async () => {
122
+ const el = await noSuggestionsFixture()
123
+ await el.updateComplete
124
+ const input = getSlottedInput(el)!
125
+ const suggestions = getSlottedSuggestions(el)!
126
+
127
+ input.focus()
128
+ await nextFrame()
129
+
130
+ expect(suggestions.matches(':popover-open')).to.be.false
131
+ })
132
+
133
+ it('does not open on input focus if suggestions element is missing', async () => {
134
+ const el = await noListboxFixture()
135
+ await el.updateComplete
136
+ const input = getSlottedInput(el)!
137
+
138
+ input.focus()
139
+ await nextFrame()
140
+ // No suggestions ref, so opened should be false
141
+ expect(el.opened).to.be.false
142
+ })
143
+
144
+ it('closes on input blur', async () => {
145
+ const el = await basicFixture()
146
+ await el.updateComplete
147
+ const input = getSlottedInput(el)!
148
+ const suggestions = getSlottedSuggestions(el)!
149
+
150
+ input.focus()
151
+ await nextFrame()
152
+ expect(suggestions.matches(':popover-open')).to.be.true
153
+
154
+ input.blur()
155
+ // Blur handler uses requestAnimationFrame
156
+ await aTimeout(0) // Wait for rAF
157
+ await nextFrame() // Wait for potential re-render
158
+
159
+ expect(suggestions.matches(':popover-open')).to.be.false
160
+ expect(el.opened).to.be.false
161
+ })
162
+
163
+ it('closes on Escape key', async () => {
164
+ const el = await basicFixture()
165
+ await el.updateComplete
166
+ const input = getSlottedInput(el)!
167
+ const suggestions = getSlottedSuggestions(el)!
168
+
169
+ input.focus()
170
+ await nextFrame()
171
+ expect(suggestions.matches(':popover-open')).to.be.true
172
+
173
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, composed: true }))
174
+ await nextFrame()
175
+
176
+ expect(suggestions.matches(':popover-open')).to.be.false
177
+ })
178
+ })
179
+
180
+ describe('Filtering', () => {
181
+ let el: AutocompleteInput
182
+ let input: HTMLInputElement
183
+ let suggestionsBox: MdListbox
184
+ let items: MdListItem[]
185
+
186
+ beforeEach(async () => {
187
+ el = await basicFixture()
188
+ await el.updateComplete
189
+ input = getSlottedInput(el)!
190
+ suggestionsBox = getSlottedSuggestions(el)!
191
+ items = getSuggestionItems(suggestionsBox)
192
+ input.focus() // Open popover for filtering tests
193
+ await nextFrame()
194
+ })
195
+
196
+ it('shows all items for empty query', () => {
197
+ input.value = ''
198
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
199
+ items.forEach((item) => expect(item.hidden).to.be.false)
200
+ })
201
+
202
+ it('filters items based on data-value', async () => {
203
+ input.value = 'app'
204
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
205
+ await nextFrame()
206
+
207
+ expect(items[0].hidden).to.be.false // Apple
208
+ expect(items[1].hidden).to.be.true // Banana
209
+ expect(items[2].hidden).to.be.true // Cherry
210
+ })
211
+
212
+ it('filters items based on textContent if data-value is not present or does not match', async () => {
213
+ // Modify first item to not have data-value for this test
214
+ items[0].removeAttribute('data-value')
215
+ items[0].textContent = 'Pineapple'
216
+ await nextFrame()
217
+
218
+ input.value = 'pine'
219
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
220
+ await nextFrame()
221
+
222
+ expect(items[0].hidden).to.be.false // Pineapple (via textContent)
223
+ expect(items[1].hidden).to.be.true // Banana
224
+ expect(items[2].hidden).to.be.true // Cherry
225
+ })
226
+
227
+ it('filters items based on data-index fields', async () => {
228
+ input.value = 'Sweet' // Matches data-custom-field="Sweet Cherry" via data-index on items[2]
229
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
230
+ await nextFrame()
231
+
232
+ expect(items[0].hidden).to.be.true // Apple
233
+ expect(items[1].hidden).to.be.true // Banana
234
+ expect(items[2].hidden).to.be.false // Cherry
235
+ })
236
+
237
+ it('is case-insensitive', async () => {
238
+ input.value = 'BaNaNa'
239
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
240
+ await nextFrame()
241
+
242
+ expect(items[0].hidden).to.be.true // Apple
243
+ expect(items[1].hidden).to.be.false // Banana
244
+ expect(items[2].hidden).to.be.true // Cherry
245
+ })
246
+
247
+ it('closes popover if no items match', async () => {
248
+ input.value = 'nonexistent'
249
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
250
+ await nextFrame()
251
+ await aTimeout(0) // filterSuggestions might call closeSuggestions, which might be async
252
+
253
+ items.forEach((item) => expect(item.hidden).to.be.true)
254
+ expect(suggestionsBox.matches(':popover-open')).to.be.false
255
+ })
256
+ })
257
+
258
+ describe('Keyboard navigation', () => {
259
+ let el: AutocompleteInput
260
+ let input: HTMLInputElement
261
+ let suggestionsBox: MdListbox
262
+
263
+ beforeEach(async () => {
264
+ el = await basicFixture()
265
+ await el.updateComplete
266
+ input = getSlottedInput(el)!
267
+ suggestionsBox = getSlottedSuggestions(el)!
268
+ // For keyboard nav, listbox needs to handle highlight internally.
269
+ // We spy on its methods.
270
+ sinon.spy(suggestionsBox, 'highlightNext')
271
+ sinon.spy(suggestionsBox, 'highlightPrevious')
272
+ sinon.spy(suggestionsBox, 'notifySelect')
273
+ })
274
+
275
+ afterEach(() => {
276
+ sinon.restore()
277
+ })
278
+
279
+ it('ArrowDown highlights next item and opens popover if closed', async () => {
280
+ expect(suggestionsBox.matches(':popover-open')).to.be.false
281
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, composed: true }))
282
+ // handleKeydown uses rAF if popover was closed
283
+ await aTimeout(0) // for rAF
284
+ await nextFrame() // for popover to open and highlight
285
+
286
+ expect(suggestionsBox.matches(':popover-open')).to.be.true
287
+ expect(suggestionsBox.highlightNext).to.have.been.calledOnce
288
+ })
289
+
290
+ it('ArrowUp highlights previous item', async () => {
291
+ input.focus() // Open popover
292
+ await nextFrame()
293
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, composed: true }))
294
+ await nextFrame()
295
+ expect(suggestionsBox.highlightPrevious).to.have.been.calledOnce
296
+ })
297
+
298
+ it('Enter selects highlighted item and dispatches event', async () => {
299
+ input.focus() // Open popover
300
+ await nextFrame()
301
+
302
+ // Simulate item being highlighted by listbox (e.g., first item)
303
+ const items = getSuggestionItems(suggestionsBox)
304
+ suggestionsBox.highlightListItem = items[0]
305
+ await nextFrame()
306
+
307
+ const eventPromise = oneEvent(el, 'autocomplete')
308
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true }))
309
+
310
+ const event = await eventPromise
311
+ expect(suggestionsBox.notifySelect).to.have.been.calledWith(items[0])
312
+ expect(event.detail.item).to.equal(items[0])
313
+ expect(suggestionsBox.matches(':popover-open')).to.be.false // Popover should close
314
+ })
315
+
316
+ it('Enter does nothing if popover closed or no item highlighted', async () => {
317
+ expect(suggestionsBox.matches(':popover-open')).to.be.false
318
+ suggestionsBox.highlightListItem = null // Ensure no item is highlighted
319
+
320
+ const spy = sinon.spy()
321
+ el.addEventListener('autocomplete', spy)
322
+
323
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true }))
324
+ await nextFrame()
325
+
326
+ expect(spy).not.to.have.been.called
327
+ expect(suggestionsBox.notifySelect).not.to.have.been.called
328
+ })
329
+ })
330
+
331
+ describe('Suggestion selection (click)', () => {
332
+ it('selects item on click and dispatches event', async () => {
333
+ const el = await basicFixture()
334
+ await el.updateComplete
335
+ const input = getSlottedInput(el)!
336
+ const suggestionsBox = getSlottedSuggestions(el)!
337
+ const items = getSuggestionItems(suggestionsBox)
338
+
339
+ input.focus() // Open popover
340
+ await nextFrame()
341
+
342
+ const eventPromise = oneEvent(el, 'autocomplete')
343
+ // Simulate click on the first item.
344
+ // The `ui-listbox` should dispatch 'select' upon item click.
345
+ // We simulate the 'select' event from the listbox.
346
+ suggestionsBox.dispatchEvent(
347
+ new CustomEvent('select', {
348
+ detail: { item: items[1] }, // Select 'Banana'
349
+ bubbles: true, // Ensure it bubbles to autocomplete-input if needed by its listener
350
+ composed: true,
351
+ })
352
+ )
353
+
354
+ const event = await eventPromise
355
+ expect(event.detail.item).to.equal(items[1])
356
+ expect(suggestionsBox.matches(':popover-open')).to.be.false // Popover should close
357
+ })
358
+ })
359
+
360
+ describe('Dynamic content', () => {
361
+ it('handles dynamically added input', async () => {
362
+ const el = await fixture<AutocompleteInput>(html`<autocomplete-input></autocomplete-input>`)
363
+ await el.updateComplete
364
+
365
+ const inputEl = document.createElement('input')
366
+ inputEl.slot = 'input'
367
+ inputEl.type = 'text'
368
+ el.appendChild(inputEl)
369
+ await nextFrame() // MutationObserver is async
370
+ await el.updateComplete // Ensure component processes the new input
371
+
372
+ const slottedInput = getSlottedInput(el)
373
+ expect(slottedInput).to.equal(inputEl)
374
+ expect(inputEl.id).to.match(/^autocomplete-input-/)
375
+ })
376
+
377
+ it('handles dynamically added suggestions', async () => {
378
+ const el = await fixture<AutocompleteInput>(
379
+ html`<autocomplete-input><input slot="input" id="dyn-input" /></autocomplete-input>`
380
+ )
381
+ await el.updateComplete
382
+ const input = getSlottedInput(el)!
383
+
384
+ const suggestionsEl = document.createElement('ui-listbox') as MdListbox
385
+ suggestionsEl.slot = 'suggestions'
386
+ el.appendChild(suggestionsEl)
387
+ await nextFrame() // MutationObserver
388
+ await el.updateComplete
389
+
390
+ const slottedSuggestions = getSlottedSuggestions(el)
391
+ expect(slottedSuggestions).to.equal(suggestionsEl)
392
+ expect(suggestionsEl.popover).to.equal('manual')
393
+ expect(suggestionsEl.style.getPropertyValue('position-anchor')).to.equal(`--${input.id}`)
394
+ })
395
+
396
+ it('re-filters when suggestion items change (via itemschange event)', async () => {
397
+ const el = await basicFixture()
398
+ await el.updateComplete
399
+ const input = getSlottedInput(el)!
400
+ const suggestionsBox = getSlottedSuggestions(el)!
401
+ let items = getSuggestionItems(suggestionsBox)
402
+
403
+ input.focus()
404
+ input.value = 'ban' // Should show "Banana"
405
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
406
+ await nextFrame()
407
+ expect(items[0].hidden).to.be.true // Apple
408
+ expect(items[1].hidden).to.be.false // Banana
409
+
410
+ // Add a new item that matches "ban"
411
+ const newItem = document.createElement('ui-list-item') as MdListItem
412
+ newItem.dataset.value = 'bandana'
413
+ newItem.textContent = 'Bandana'
414
+ suggestionsBox.appendChild(newItem)
415
+
416
+ // Simulate 'itemschange' event from listbox
417
+ suggestionsBox.dispatchEvent(new CustomEvent('itemschange'))
418
+ await nextFrame() // Allow handler to run
419
+
420
+ items = getSuggestionItems(suggestionsBox) // Re-fetch items
421
+ expect(items.find((i) => i.dataset.value === 'bandana')?.hidden).to.be.false
422
+ expect(items.find((i) => i.dataset.value === 'banana')?.hidden).to.be.false
423
+ expect(items.find((i) => i.dataset.value === 'apple')?.hidden).to.be.true
424
+ })
425
+ })
426
+
427
+ describe('`opened` property', () => {
428
+ it('reflects popover state', async () => {
429
+ const el = await basicFixture()
430
+ await el.updateComplete
431
+ const input = getSlottedInput(el)!
432
+ const suggestions = getSlottedSuggestions(el)!
433
+
434
+ expect(el.opened).to.be.false
435
+
436
+ input.focus()
437
+ await nextFrame()
438
+ expect(el.opened).to.be.true
439
+ expect(suggestions.matches(':popover-open')).to.be.true
440
+
441
+ // @ts-expect-error protected method
442
+ el.closeSuggestions()
443
+ await nextFrame()
444
+ expect(el.opened).to.be.false
445
+ expect(suggestions.matches(':popover-open')).to.be.false
446
+ })
447
+ })
448
+ })
@@ -1,4 +1,4 @@
1
- import { assert, fixture, nextFrame, html, aTimeout } from '@open-wc/testing'
1
+ import { assert, fixture, nextFrame, html, oneEvent } from '@open-wc/testing'
2
2
  import sinon from 'sinon'
3
3
  import { loadMonaco } from '../MonacoSetup.js'
4
4
  import BodyMultipartEditorElement, { hasFormDataSupport } from '../../../src/elements/http/BodyMultipartEditor.js'
@@ -23,7 +23,7 @@ describe('elements', () => {
23
23
  const file = new File(['blob-value'], 'file.txt', { type: 'text/plain' })
24
24
  form.append('file', file, 'file.txt')
25
25
  element.value = form
26
- await aTimeout(20)
26
+ await oneEvent(element, 'modelupdate')
27
27
  return element
28
28
  }
29
29
 
@@ -172,19 +172,19 @@ describe('elements', () => {
172
172
  element.model = [{ name: 'a', value: 'b', enabled: true }]
173
173
  // @ts-expect-error for testing
174
174
  element.value = undefined
175
- await aTimeout(20)
175
+ await oneEvent(element, 'modelupdate')
176
176
  assert.deepEqual(element.model, [])
177
177
  })
178
178
 
179
179
  it('generates the view model', async () => {
180
180
  element.value = form
181
- await aTimeout(20)
181
+ await oneEvent(element, 'modelupdate')
182
182
  assert.lengthOf(element.model, 3)
183
183
  })
184
184
 
185
185
  it('generated model has restored text part property', async () => {
186
186
  element.value = form
187
- await aTimeout(20)
187
+ await oneEvent(element, 'modelupdate')
188
188
  const [item] = element.model
189
189
 
190
190
  assert.deepEqual(item, {
@@ -199,7 +199,7 @@ describe('elements', () => {
199
199
 
200
200
  it('generated model has restored text blob part property', async () => {
201
201
  element.value = form
202
- await aTimeout(20)
202
+ await oneEvent(element, 'modelupdate')
203
203
  const item = element.model[1]
204
204
  assert.deepEqual(item, {
205
205
  enabled: true,
@@ -214,7 +214,7 @@ describe('elements', () => {
214
214
 
215
215
  it('generated model has restored file part property', async () => {
216
216
  element.value = form
217
- await aTimeout(20)
217
+ await oneEvent(element, 'modelupdate')
218
218
  const item = element.model[2]
219
219
  assert.deepEqual(item, {
220
220
  enabled: true,
@@ -230,7 +230,7 @@ describe('elements', () => {
230
230
  it('updates an existing model', async () => {
231
231
  element.model = [{ name: 'test', value: 'true', enabled: true }]
232
232
  element.value = form
233
- await aTimeout(20)
233
+ await oneEvent(element, 'modelupdate')
234
234
  assert.equal(element.model[0].name, 'text')
235
235
  })
236
236
 
@@ -239,7 +239,7 @@ describe('elements', () => {
239
239
  const spy = sinon.spy()
240
240
  element.addEventListener('change', spy)
241
241
  element.value = form
242
- await aTimeout(20)
242
+ await oneEvent(element, 'modelupdate')
243
243
  assert.isFalse(spy.called)
244
244
  })
245
245
  })
@@ -277,7 +277,7 @@ describe('elements', () => {
277
277
  const form = new FormData()
278
278
  form.append('a', 'b')
279
279
  element.value = form
280
- await aTimeout(20)
280
+ await oneEvent(element, 'modelupdate')
281
281
  element.model = undefined
282
282
  const it = element.value.entries().next()
283
283
  assert.isTrue(it.done)
@@ -581,7 +581,7 @@ describe('elements', () => {
581
581
  const input = element.shadowRoot!.querySelector('.value-input input[data-index="1"]') as HTMLInputElement
582
582
  input.value = '* *'
583
583
  input.dispatchEvent(new Event('change'))
584
- await aTimeout(20)
584
+ await oneEvent(element, 'change')
585
585
 
586
586
  const item = element.model[1]
587
587
 
@@ -599,7 +599,7 @@ describe('elements', () => {
599
599
  const input = element.shadowRoot!.querySelector('.value-input input[data-index="1"]') as HTMLInputElement
600
600
  input.value = '* *'
601
601
  input.dispatchEvent(new Event('change'))
602
- await aTimeout(10)
602
+ await oneEvent(element, 'change')
603
603
  assert.isTrue(spy.calledOnce)
604
604
  })
605
605
  })
@@ -615,7 +615,7 @@ describe('elements', () => {
615
615
  input.value = 'text/other'
616
616
  input.dispatchEvent(new Event('change'))
617
617
 
618
- await aTimeout(20)
618
+ await oneEvent(element, 'change')
619
619
  const item = element.model[1]
620
620
 
621
621
  assert.deepEqual(item, {
@@ -631,7 +631,7 @@ describe('elements', () => {
631
631
  input.value = 'text/x'
632
632
  input.dispatchEvent(new Event('change'))
633
633
 
634
- await aTimeout(20)
634
+ await oneEvent(element, 'change')
635
635
  const item = element.model[0]
636
636
 
637
637
  assert.deepEqual(item, {
@@ -649,8 +649,7 @@ describe('elements', () => {
649
649
  const input = element.shadowRoot!.querySelector('.mime-input input[data-index="0"]') as HTMLInputElement
650
650
  input.value = 'text/x'
651
651
  input.dispatchEvent(new Event('change'))
652
-
653
- await aTimeout(20)
652
+ await oneEvent(element, 'change')
654
653
  assert.isTrue(spy.called)
655
654
  })
656
655
  })
@@ -31,7 +31,7 @@ describe.skip('elements', () => {
31
31
  })
32
32
 
33
33
  it('renders the certificate name input', async () => {
34
- const input = element.shadowRoot!.querySelector('ui-text-field[name="name"]')! as Input
34
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="name"]')! as Input
35
35
  assert.ok(input, 'has the input')
36
36
  })
37
37
 
@@ -46,12 +46,12 @@ describe.skip('elements', () => {
46
46
  })
47
47
 
48
48
  it('renders the key password filed', async () => {
49
- const input = element.shadowRoot!.querySelector('ui-text-field[name="keyPassword"]') as Input
49
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="keyPassword"]') as Input
50
50
  assert.ok(input, 'has the input')
51
51
  })
52
52
 
53
53
  it('does not render the certificate password filed', async () => {
54
- const input = element.shadowRoot!.querySelector('ui-text-field[name="certificatePassword"]')
54
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="certificatePassword"]')
55
55
  assert.notOk(input, 'has no input')
56
56
  })
57
57
  })
@@ -64,7 +64,7 @@ describe.skip('elements', () => {
64
64
  })
65
65
 
66
66
  it('renders the certificate name input', async () => {
67
- const input = element.shadowRoot!.querySelector('ui-text-field[name="name"]')! as Input
67
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="name"]')! as Input
68
68
  assert.ok(input, 'has the input')
69
69
  })
70
70
 
@@ -79,12 +79,12 @@ describe.skip('elements', () => {
79
79
  })
80
80
 
81
81
  it('does not render the key password filed', async () => {
82
- const input = element.shadowRoot!.querySelector('ui-text-field[name="keyPassword"]')
82
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="keyPassword"]')
83
83
  assert.notOk(input, 'has no input')
84
84
  })
85
85
 
86
86
  it('renders the certificate password input', async () => {
87
- const input = element.shadowRoot!.querySelector('ui-text-field[name="certificatePassword"]')! as Input
87
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="certificatePassword"]')! as Input
88
88
  assert.ok(input, 'has the input')
89
89
  })
90
90
  })
@@ -387,14 +387,14 @@ describe.skip('elements', () => {
387
387
  element.addEventListener(EventTypes.Store.File.create, spy)
388
388
  await element.submit()
389
389
  assert.isFalse(spy.called, 'does not dispatch store event')
390
- const input = element.shadowRoot!.querySelector('ui-text-field[name="name"]')! as Input
390
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="name"]')! as Input
391
391
  assert.isTrue(input.invalid, 'the input is invalid')
392
392
  })
393
393
 
394
394
  it('informs when the certificate is missing', async () => {
395
395
  const key = new File(['test'], 'key.key', { type: 'text/plain' })
396
396
  await element.processDroppedFile(key, 'key')
397
- const input = element.shadowRoot!.querySelector('ui-text-field[name="name"]')! as Input
397
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="name"]')! as Input
398
398
  input.value = 'test'
399
399
  input.notifyChange()
400
400
  await input.updateComplete
@@ -408,7 +408,7 @@ describe.skip('elements', () => {
408
408
  it('informs when the key is missing', async () => {
409
409
  const cert = new File(['test'], 'key.key', { type: 'text/plain' })
410
410
  await element.processDroppedFile(cert, 'certificate')
411
- const input = element.shadowRoot!.querySelector('ui-text-field[name="name"]')! as Input
411
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="name"]')! as Input
412
412
  input.value = 'test'
413
413
  input.notifyChange()
414
414
  await input.updateComplete
@@ -424,7 +424,7 @@ describe.skip('elements', () => {
424
424
  const key = new File(['test'], 'key.key', { type: 'text/plain' })
425
425
  await element.processDroppedFile(cert, 'certificate')
426
426
  await element.processDroppedFile(key, 'key')
427
- const input = element.shadowRoot!.querySelector('ui-text-field[name="name"]')! as Input
427
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="name"]')! as Input
428
428
  input.value = 'test'
429
429
  input.notifyChange()
430
430
  await input.updateComplete
@@ -445,7 +445,7 @@ describe.skip('elements', () => {
445
445
  const key = new File(['test'], 'key.key', { type: 'text/plain' })
446
446
  await element.processDroppedFile(cert, 'certificate')
447
447
  await element.processDroppedFile(key, 'key')
448
- const input = element.shadowRoot!.querySelector('ui-text-field[name="name"]')! as Input
448
+ const input = element.shadowRoot!.querySelector('ui-filled-text-field[name="name"]')! as Input
449
449
  input.value = 'test'
450
450
  input.notifyChange()
451
451
  await input.updateComplete