@api-client/ui 0.2.3 → 0.2.5

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 (204) hide show
  1. package/.aiexclude +3 -0
  2. package/.vscode/settings.json +6 -3
  3. package/build/src/elements/authorization/ui/ApiKeyAuthorization.d.ts +1 -1
  4. package/build/src/elements/authorization/ui/ApiKeyAuthorization.d.ts.map +1 -1
  5. package/build/src/elements/authorization/ui/ApiKeyAuthorization.js +7 -7
  6. package/build/src/elements/authorization/ui/ApiKeyAuthorization.js.map +1 -1
  7. package/build/src/elements/authorization/ui/Authorization.styles.js +4 -4
  8. package/build/src/elements/authorization/ui/Authorization.styles.js.map +1 -1
  9. package/build/src/elements/authorization/ui/BasicAuthorization.d.ts +1 -1
  10. package/build/src/elements/authorization/ui/BasicAuthorization.d.ts.map +1 -1
  11. package/build/src/elements/authorization/ui/BasicAuthorization.js +5 -5
  12. package/build/src/elements/authorization/ui/BasicAuthorization.js.map +1 -1
  13. package/build/src/elements/authorization/ui/BearerAuthorization.d.ts +1 -1
  14. package/build/src/elements/authorization/ui/BearerAuthorization.d.ts.map +1 -1
  15. package/build/src/elements/authorization/ui/BearerAuthorization.js +3 -3
  16. package/build/src/elements/authorization/ui/BearerAuthorization.js.map +1 -1
  17. package/build/src/elements/authorization/ui/NtlmAuthorization.d.ts +1 -1
  18. package/build/src/elements/authorization/ui/NtlmAuthorization.d.ts.map +1 -1
  19. package/build/src/elements/authorization/ui/NtlmAuthorization.js +7 -7
  20. package/build/src/elements/authorization/ui/NtlmAuthorization.js.map +1 -1
  21. package/build/src/elements/authorization/ui/OAuth2Authorization.d.ts +1 -1
  22. package/build/src/elements/authorization/ui/OAuth2Authorization.d.ts.map +1 -1
  23. package/build/src/elements/authorization/ui/OAuth2Authorization.js +32 -27
  24. package/build/src/elements/authorization/ui/OAuth2Authorization.js.map +1 -1
  25. package/build/src/elements/authorization/ui/OidcAuthorization.js +4 -4
  26. package/build/src/elements/authorization/ui/OidcAuthorization.js.map +1 -1
  27. package/build/src/elements/autocomplete/autocomplete-input.d.ts +10 -0
  28. package/build/src/elements/autocomplete/autocomplete-input.d.ts.map +1 -0
  29. package/build/src/{md/text-field/ui-text-field.js → elements/autocomplete/autocomplete-input.js} +9 -9
  30. package/build/src/elements/autocomplete/autocomplete-input.js.map +1 -0
  31. package/build/src/elements/autocomplete/internals/autocomplete.d.ts +257 -0
  32. package/build/src/elements/autocomplete/internals/autocomplete.d.ts.map +1 -0
  33. package/build/src/elements/autocomplete/internals/autocomplete.js +619 -0
  34. package/build/src/elements/autocomplete/internals/autocomplete.js.map +1 -0
  35. package/build/src/elements/autocomplete/internals/autocomplete.styles.d.ts +3 -0
  36. package/build/src/elements/autocomplete/internals/autocomplete.styles.d.ts.map +1 -0
  37. package/build/src/elements/autocomplete/internals/autocomplete.styles.js +25 -0
  38. package/build/src/elements/autocomplete/internals/autocomplete.styles.js.map +1 -0
  39. package/build/src/elements/dialog/internals/DeleteCookieAction.element.d.ts +1 -1
  40. package/build/src/elements/dialog/internals/DeleteCookieAction.element.d.ts.map +1 -1
  41. package/build/src/elements/dialog/internals/DeleteCookieAction.element.js +5 -5
  42. package/build/src/elements/dialog/internals/DeleteCookieAction.element.js.map +1 -1
  43. package/build/src/elements/dialog/internals/Rename.d.ts +1 -1
  44. package/build/src/elements/dialog/internals/Rename.d.ts.map +1 -1
  45. package/build/src/elements/dialog/internals/Rename.js +3 -3
  46. package/build/src/elements/dialog/internals/Rename.js.map +1 -1
  47. package/build/src/elements/dialog/internals/SetCookieAction.element.d.ts +1 -1
  48. package/build/src/elements/dialog/internals/SetCookieAction.element.d.ts.map +1 -1
  49. package/build/src/elements/dialog/internals/SetCookieAction.element.js +9 -9
  50. package/build/src/elements/dialog/internals/SetCookieAction.element.js.map +1 -1
  51. package/build/src/elements/environment/EnvironmentEditor.d.ts +1 -1
  52. package/build/src/elements/environment/EnvironmentEditor.d.ts.map +1 -1
  53. package/build/src/elements/environment/EnvironmentEditor.js +3 -3
  54. package/build/src/elements/environment/EnvironmentEditor.js.map +1 -1
  55. package/build/src/elements/environment/EnvironmentEditor.styles.js +1 -1
  56. package/build/src/elements/environment/EnvironmentEditor.styles.js.map +1 -1
  57. package/build/src/elements/environment/ServerEditor.d.ts +1 -1
  58. package/build/src/elements/environment/ServerEditor.d.ts.map +1 -1
  59. package/build/src/elements/environment/ServerEditor.js +7 -7
  60. package/build/src/elements/environment/ServerEditor.js.map +1 -1
  61. package/build/src/elements/environment/ServerEditor.styles.js +1 -1
  62. package/build/src/elements/environment/ServerEditor.styles.js.map +1 -1
  63. package/build/src/elements/http/BodyMultipartEditor.d.ts.map +1 -1
  64. package/build/src/elements/http/BodyMultipartEditor.js +4 -0
  65. package/build/src/elements/http/BodyMultipartEditor.js.map +1 -1
  66. package/build/src/elements/http/CertificateAdd.element.d.ts +1 -1
  67. package/build/src/elements/http/CertificateAdd.element.d.ts.map +1 -1
  68. package/build/src/elements/http/CertificateAdd.element.js +8 -8
  69. package/build/src/elements/http/CertificateAdd.element.js.map +1 -1
  70. package/build/src/elements/http/CertificateAdd.styles.js +1 -1
  71. package/build/src/elements/http/CertificateAdd.styles.js.map +1 -1
  72. package/build/src/elements/http/HttpAssertions.element.js +3 -3
  73. package/build/src/elements/http/HttpAssertions.element.js.map +1 -1
  74. package/build/src/elements/http/HttpFlows.element.js +3 -3
  75. package/build/src/elements/http/HttpFlows.element.js.map +1 -1
  76. package/build/src/elements/http/HttpFlowsUi.d.ts +1 -1
  77. package/build/src/elements/http/HttpFlowsUi.d.ts.map +1 -1
  78. package/build/src/elements/http/HttpFlowsUi.js +31 -31
  79. package/build/src/elements/http/HttpFlowsUi.js.map +1 -1
  80. package/build/src/elements/http/RequestConfigElement.d.ts +1 -1
  81. package/build/src/elements/http/RequestConfigElement.d.ts.map +1 -1
  82. package/build/src/elements/http/RequestConfigElement.js +7 -7
  83. package/build/src/elements/http/RequestConfigElement.js.map +1 -1
  84. package/build/src/elements/http/UrlParamsForm.d.ts +1 -1
  85. package/build/src/elements/http/UrlParamsForm.d.ts.map +1 -1
  86. package/build/src/elements/http/UrlParamsForm.js +1 -1
  87. package/build/src/elements/http/UrlParamsForm.js.map +1 -1
  88. package/build/src/elements/project/ProjectRunner.d.ts +1 -1
  89. package/build/src/elements/project/ProjectRunner.d.ts.map +1 -1
  90. package/build/src/elements/project/ProjectRunner.js +5 -5
  91. package/build/src/elements/project/ProjectRunner.js.map +1 -1
  92. package/build/src/md/input/Input.d.ts +0 -15
  93. package/build/src/md/input/Input.d.ts.map +1 -1
  94. package/build/src/md/input/Input.js +7 -42
  95. package/build/src/md/input/Input.js.map +1 -1
  96. package/build/src/md/list/internals/List.d.ts +7 -2
  97. package/build/src/md/list/internals/List.d.ts.map +1 -1
  98. package/build/src/md/list/internals/List.js +6 -0
  99. package/build/src/md/list/internals/List.js.map +1 -1
  100. package/build/src/md/list/internals/ListItem.styles.d.ts.map +1 -1
  101. package/build/src/md/list/internals/ListItem.styles.js +8 -0
  102. package/build/src/md/list/internals/ListItem.styles.js.map +1 -1
  103. package/build/src/md/listbox/internals/Listbox.d.ts +2 -2
  104. package/build/src/md/listbox/internals/Listbox.d.ts.map +1 -1
  105. package/build/src/md/listbox/internals/Listbox.js.map +1 -1
  106. package/build/src/md/text-area/internals/TextAreaElement.d.ts.map +1 -1
  107. package/build/src/md/text-area/internals/TextAreaElement.js +0 -5
  108. package/build/src/md/text-area/internals/TextAreaElement.js.map +1 -1
  109. package/build/src/md/text-area/ui-text-area.d.ts.map +1 -1
  110. package/build/src/md/text-area/ui-text-area.js +3 -2
  111. package/build/src/md/text-area/ui-text-area.js.map +1 -1
  112. package/build/src/md/text-field/internals/{TextFieldElement.d.ts → TextField.d.ts} +2 -2
  113. package/build/src/md/text-field/internals/TextField.d.ts.map +1 -0
  114. package/build/src/md/text-field/internals/{TextFieldElement.js → TextField.js} +2 -5
  115. package/build/src/md/text-field/internals/TextField.js.map +1 -0
  116. package/build/src/md/text-field/internals/{TextField.styles.d.ts → common.styles.d.ts} +1 -1
  117. package/build/src/md/text-field/internals/common.styles.d.ts.map +1 -0
  118. package/build/src/md/text-field/internals/{TextField.styles.js → common.styles.js} +8 -94
  119. package/build/src/md/text-field/internals/common.styles.js.map +1 -0
  120. package/build/src/md/text-field/internals/filled.styles.d.ts +3 -0
  121. package/build/src/md/text-field/internals/filled.styles.d.ts.map +1 -0
  122. package/build/src/md/text-field/internals/filled.styles.js +107 -0
  123. package/build/src/md/text-field/internals/filled.styles.js.map +1 -0
  124. package/build/src/md/text-field/internals/outlined.styles.d.ts +3 -0
  125. package/build/src/md/text-field/internals/outlined.styles.d.ts.map +1 -0
  126. package/build/src/md/text-field/internals/outlined.styles.js +43 -0
  127. package/build/src/md/text-field/internals/outlined.styles.js.map +1 -0
  128. package/build/src/md/text-field/ui-filled-text-field.d.ts +11 -0
  129. package/build/src/md/text-field/ui-filled-text-field.d.ts.map +1 -0
  130. package/build/src/md/text-field/ui-filled-text-field.js +28 -0
  131. package/build/src/md/text-field/ui-filled-text-field.js.map +1 -0
  132. package/build/src/md/text-field/ui-outlined-text-field.d.ts +11 -0
  133. package/build/src/md/text-field/ui-outlined-text-field.d.ts.map +1 -0
  134. package/build/src/md/text-field/ui-outlined-text-field.js +28 -0
  135. package/build/src/md/text-field/ui-outlined-text-field.js.map +1 -0
  136. package/build/src/types/input.d.ts +1 -1
  137. package/build/src/types/input.d.ts.map +1 -1
  138. package/build/src/types/input.js.map +1 -1
  139. package/demo/elements/authorization/oauth-authorize.html +4 -4
  140. package/demo/elements/authorization/oauth-authorize.ts +1 -1
  141. package/demo/elements/autocomplete/index.html +64 -0
  142. package/demo/elements/autocomplete/index.ts +171 -0
  143. package/demo/elements/http/body-editor.ts +3 -3
  144. package/demo/elements/index.html +15 -11
  145. package/demo/md/index.html +1 -1
  146. package/demo/md/inputs/input.html +10 -15
  147. package/demo/md/inputs/input.ts +389 -101
  148. package/demo/page.css +4 -0
  149. package/package.json +1 -1
  150. package/src/elements/authorization/ui/ApiKeyAuthorization.ts +7 -7
  151. package/src/elements/authorization/ui/Authorization.styles.ts +4 -4
  152. package/src/elements/authorization/ui/BasicAuthorization.ts +5 -5
  153. package/src/elements/authorization/ui/BearerAuthorization.ts +3 -3
  154. package/src/elements/authorization/ui/NtlmAuthorization.ts +7 -7
  155. package/src/elements/authorization/ui/OAuth2Authorization.ts +32 -27
  156. package/src/elements/authorization/ui/OidcAuthorization.ts +4 -4
  157. package/src/elements/autocomplete/autocomplete-input.ts +14 -0
  158. package/src/elements/autocomplete/internals/autocomplete.styles.ts +25 -0
  159. package/src/elements/autocomplete/internals/autocomplete.ts +599 -0
  160. package/src/elements/dialog/internals/DeleteCookieAction.element.ts +5 -5
  161. package/src/elements/dialog/internals/Rename.ts +3 -3
  162. package/src/elements/dialog/internals/SetCookieAction.element.ts +9 -9
  163. package/src/elements/environment/EnvironmentEditor.styles.ts +1 -1
  164. package/src/elements/environment/EnvironmentEditor.ts +3 -3
  165. package/src/elements/environment/ServerEditor.styles.ts +1 -1
  166. package/src/elements/environment/ServerEditor.ts +7 -7
  167. package/src/elements/http/BodyMultipartEditor.ts +4 -0
  168. package/src/elements/http/CertificateAdd.element.ts +8 -8
  169. package/src/elements/http/CertificateAdd.styles.ts +1 -1
  170. package/src/elements/http/HttpAssertions.element.ts +3 -3
  171. package/src/elements/http/HttpFlows.element.ts +3 -3
  172. package/src/elements/http/HttpFlowsUi.ts +31 -31
  173. package/src/elements/http/RequestConfigElement.ts +7 -7
  174. package/src/elements/http/UrlParamsForm.ts +1 -1
  175. package/src/elements/project/ProjectRunner.ts +5 -5
  176. package/src/md/input/Input.ts +6 -21
  177. package/src/md/list/internals/List.ts +14 -2
  178. package/src/md/list/internals/ListItem.styles.ts +8 -0
  179. package/src/md/listbox/internals/Listbox.ts +2 -2
  180. package/src/md/text-area/internals/TextAreaElement.ts +0 -5
  181. package/src/md/text-area/ui-text-area.ts +3 -2
  182. package/src/md/text-field/internals/{TextFieldElement.ts → TextField.ts} +1 -4
  183. package/src/md/text-field/internals/{TextField.styles.ts → common.styles.ts} +7 -93
  184. package/src/md/text-field/internals/filled.styles.ts +107 -0
  185. package/src/md/text-field/internals/outlined.styles.ts +43 -0
  186. package/src/md/text-field/ui-filled-text-field.ts +16 -0
  187. package/src/md/text-field/ui-outlined-text-field.ts +16 -0
  188. package/src/types/input.ts +0 -1
  189. package/test/elements/authorization/basic-method.test.ts +3 -3
  190. package/test/elements/authorization/bearer-method.test.ts +2 -2
  191. package/test/elements/authorization/ntlm-method.test.ts +4 -4
  192. package/test/elements/autocomplete/autocomplete-input.spec.ts +643 -0
  193. package/test/elements/http/BodyMultipartEditorElement.test.ts +15 -16
  194. package/test/elements/http/CertificateAdd.test.ts +11 -11
  195. package/test/elements/http/HttpAssertions.test.ts +9 -9
  196. package/test/elements/http/HttpFlows.test.ts +4 -4
  197. package/build/src/md/text-field/internals/TextField.styles.d.ts.map +0 -1
  198. package/build/src/md/text-field/internals/TextField.styles.js.map +0 -1
  199. package/build/src/md/text-field/internals/TextFieldElement.d.ts.map +0 -1
  200. package/build/src/md/text-field/internals/TextFieldElement.js.map +0 -1
  201. package/build/src/md/text-field/ui-text-field.d.ts +0 -11
  202. package/build/src/md/text-field/ui-text-field.d.ts.map +0 -1
  203. package/build/src/md/text-field/ui-text-field.js.map +0 -1
  204. package/src/md/text-field/ui-text-field.ts +0 -15
@@ -0,0 +1,643 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-expressions */
2
+ import { fixture, expect, html, oneEvent, nextFrame, aTimeout, assert } 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" aria-label="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" aria-label="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
+ return el.querySelector('[slot="input"]') as HTMLInputElement | null
49
+ }
50
+
51
+ function getSlottedSuggestions(el: AutocompleteInput): MdListbox | null {
52
+ return el.querySelector('[slot="suggestions"]') as MdListbox | null
53
+ }
54
+
55
+ function getSuggestionItems(suggestionsEl: MdListbox): MdListItem[] {
56
+ // Assuming MdListbox.items returns the slotted items that are MdListItem
57
+ return (suggestionsEl.items as MdListItem[]).filter(
58
+ (item) => item instanceof HTMLElement && item.matches('ui-list-item')
59
+ )
60
+ }
61
+
62
+ describe('Initialization', () => {
63
+ it('correctly identifies slotted input and suggestions', async () => {
64
+ const el = await basicFixture()
65
+ await el.updateComplete // Ensure firstUpdated has run
66
+
67
+ const input = getSlottedInput(el)
68
+ const suggestions = getSlottedSuggestions(el)
69
+
70
+ expect(input).to.exist
71
+ expect(input?.id).to.equal('test-input')
72
+ expect(suggestions).to.exist
73
+ expect(suggestions?.popover).to.equal('manual')
74
+ })
75
+
76
+ it('generates an ID for the input if not provided and sets up anchor names', async () => {
77
+ const el = (await fixture(html`
78
+ <autocomplete-input>
79
+ <input slot="input" type="text" />
80
+ <ui-listbox slot="suggestions"></ui-listbox>
81
+ </autocomplete-input>
82
+ `)) as AutocompleteInput
83
+ await el.updateComplete
84
+ await nextFrame() // Allow for internal state updates
85
+
86
+ const input = getSlottedInput(el)
87
+ const suggestions = getSlottedSuggestions(el)
88
+
89
+ expect(input?.id).to.match(/^autocomplete-input-/)
90
+ // @ts-expect-error _inputId is a protected member
91
+ const internalInputId = el.inputId
92
+ expect(input?.style.getPropertyValue('anchor-name')).to.equal(`--${internalInputId}`)
93
+ expect(suggestions?.style.getPropertyValue('position-anchor')).to.equal(`--${internalInputId}`)
94
+ })
95
+
96
+ it('popover is initially closed', async () => {
97
+ const el = await basicFixture()
98
+ await el.updateComplete
99
+ const suggestions = getSlottedSuggestions(el)
100
+ expect(suggestions?.matches(':popover-open')).to.be.false
101
+ expect(el.opened).to.be.false
102
+ })
103
+ })
104
+
105
+ describe('Popover behavior', () => {
106
+ it('opens on input focus with suggestions', async () => {
107
+ const el = await basicFixture()
108
+ await el.updateComplete
109
+ const input = getSlottedInput(el)!
110
+ const suggestions = getSlottedSuggestions(el)!
111
+
112
+ input.focus()
113
+ await nextFrame() // Allow focus event to propagate and popover to open
114
+
115
+ expect(suggestions.matches(':popover-open')).to.be.true
116
+ expect(el.opened).to.be.true
117
+ })
118
+
119
+ it('does not open on input focus without suggestions', async () => {
120
+ const el = await noSuggestionsFixture()
121
+ await el.updateComplete
122
+ const input = getSlottedInput(el)!
123
+ const suggestions = getSlottedSuggestions(el)!
124
+
125
+ input.focus()
126
+ await nextFrame()
127
+
128
+ expect(suggestions.matches(':popover-open')).to.be.false
129
+ })
130
+
131
+ it('does not open on input focus if suggestions element is missing', async () => {
132
+ const el = await noListboxFixture()
133
+ await el.updateComplete
134
+ const input = getSlottedInput(el)!
135
+
136
+ input.focus()
137
+ await nextFrame()
138
+ // No suggestions ref, so opened should be false
139
+ expect(el.opened).to.be.false
140
+ })
141
+
142
+ it('closes on input blur', async () => {
143
+ const el = await basicFixture()
144
+ await el.updateComplete
145
+ const input = getSlottedInput(el)!
146
+ const suggestions = getSlottedSuggestions(el)!
147
+
148
+ input.focus()
149
+ await nextFrame()
150
+ expect(suggestions.matches(':popover-open')).to.be.true
151
+
152
+ input.blur()
153
+ // Blur handler uses requestAnimationFrame
154
+ await aTimeout(0) // Wait for rAF
155
+ await nextFrame() // Wait for potential re-render
156
+
157
+ expect(suggestions.matches(':popover-open')).to.be.false
158
+ expect(el.opened).to.be.false
159
+ })
160
+
161
+ it('closes on Escape key', async () => {
162
+ const el = await basicFixture()
163
+ await el.updateComplete
164
+ const input = getSlottedInput(el)!
165
+ const suggestions = getSlottedSuggestions(el)!
166
+
167
+ input.focus()
168
+ await nextFrame()
169
+ expect(suggestions.matches(':popover-open')).to.be.true
170
+
171
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, composed: true }))
172
+ await nextFrame()
173
+
174
+ expect(suggestions.matches(':popover-open')).to.be.false
175
+ })
176
+ })
177
+
178
+ describe('Filtering', () => {
179
+ let el: AutocompleteInput
180
+ let input: HTMLInputElement
181
+ let suggestionsBox: MdListbox
182
+ let items: MdListItem[]
183
+
184
+ beforeEach(async () => {
185
+ el = await basicFixture()
186
+ await el.updateComplete
187
+ input = getSlottedInput(el)!
188
+ suggestionsBox = getSlottedSuggestions(el)!
189
+ items = getSuggestionItems(suggestionsBox)
190
+ input.focus() // Open popover for filtering tests
191
+ await nextFrame()
192
+ })
193
+
194
+ it('shows all items for empty query', () => {
195
+ input.value = ''
196
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
197
+ items.forEach((item) => expect(item.hidden).to.be.false)
198
+ })
199
+
200
+ it('filters items based on data-value', async () => {
201
+ input.value = 'app'
202
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
203
+ await nextFrame()
204
+
205
+ expect(items[0].hidden).to.be.false // Apple
206
+ expect(items[1].hidden).to.be.true // Banana
207
+ expect(items[2].hidden).to.be.true // Cherry
208
+ })
209
+
210
+ it('filters items based on textContent if data-value is not present or does not match', async () => {
211
+ // Modify first item to not have data-value for this test
212
+ items[0].removeAttribute('data-value')
213
+ items[0].textContent = 'Pineapple'
214
+ await nextFrame()
215
+
216
+ input.value = 'pine'
217
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
218
+ await nextFrame()
219
+
220
+ expect(items[0].hidden).to.be.false // Pineapple (via textContent)
221
+ expect(items[1].hidden).to.be.true // Banana
222
+ expect(items[2].hidden).to.be.true // Cherry
223
+ })
224
+
225
+ it('filters items based on data-index fields', async () => {
226
+ input.value = 'Sweet' // Matches data-custom-field="Sweet Cherry" via data-index on items[2]
227
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
228
+ await nextFrame()
229
+
230
+ expect(items[0].hidden).to.be.true // Apple
231
+ expect(items[1].hidden).to.be.true // Banana
232
+ expect(items[2].hidden).to.be.false // Cherry
233
+ })
234
+
235
+ it('is case-insensitive', async () => {
236
+ input.value = 'BaNaNa'
237
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
238
+ await nextFrame()
239
+
240
+ expect(items[0].hidden).to.be.true // Apple
241
+ expect(items[1].hidden).to.be.false // Banana
242
+ expect(items[2].hidden).to.be.true // Cherry
243
+ })
244
+
245
+ it('closes popover if no items match', async () => {
246
+ input.value = 'nonexistent'
247
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
248
+ await nextFrame()
249
+ await aTimeout(0) // filterSuggestions might call closeSuggestions, which might be async
250
+
251
+ items.forEach((item) => expect(item.hidden).to.be.true)
252
+ expect(suggestionsBox.matches(':popover-open')).to.be.false
253
+ })
254
+ })
255
+
256
+ describe('Keyboard navigation', () => {
257
+ let el: AutocompleteInput
258
+ let input: HTMLInputElement
259
+ let suggestionsBox: MdListbox
260
+
261
+ beforeEach(async () => {
262
+ el = await basicFixture()
263
+ await el.updateComplete
264
+ input = getSlottedInput(el)!
265
+ suggestionsBox = getSlottedSuggestions(el)!
266
+ // For keyboard nav, listbox needs to handle highlight internally.
267
+ // We spy on its methods.
268
+ sinon.spy(suggestionsBox, 'highlightNext')
269
+ sinon.spy(suggestionsBox, 'highlightPrevious')
270
+ sinon.spy(suggestionsBox, 'notifySelect')
271
+ })
272
+
273
+ afterEach(() => {
274
+ sinon.restore()
275
+ })
276
+
277
+ it('ArrowDown highlights next item and opens popover if closed', async () => {
278
+ expect(suggestionsBox.matches(':popover-open')).to.be.false
279
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, composed: true }))
280
+ // handleKeydown uses rAF if popover was closed
281
+ await aTimeout(0) // for rAF
282
+ await nextFrame() // for popover to open and highlight
283
+
284
+ expect(suggestionsBox.matches(':popover-open')).to.be.true
285
+ expect(suggestionsBox.highlightNext).to.have.been.calledOnce
286
+ })
287
+
288
+ it('ArrowUp highlights previous item', async () => {
289
+ input.focus() // Open popover
290
+ await nextFrame()
291
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true, composed: true }))
292
+ await nextFrame()
293
+ expect(suggestionsBox.highlightPrevious).to.have.been.calledOnce
294
+ })
295
+
296
+ it('Enter selects highlighted item and dispatches event', async () => {
297
+ input.focus() // Open popover
298
+ await nextFrame()
299
+
300
+ // Simulate item being highlighted by listbox (e.g., first item)
301
+ const items = getSuggestionItems(suggestionsBox)
302
+ suggestionsBox.highlightListItem = items[0]
303
+ await nextFrame()
304
+
305
+ const eventPromise = oneEvent(el, 'autocomplete')
306
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true }))
307
+
308
+ const event = await eventPromise
309
+ expect(suggestionsBox.notifySelect).to.have.been.calledWith(items[0])
310
+ expect(event.detail.item).to.equal(items[0])
311
+ expect(suggestionsBox.matches(':popover-open')).to.be.false // Popover should close
312
+ })
313
+
314
+ it('Enter does nothing if popover closed or no item highlighted', async () => {
315
+ expect(suggestionsBox.matches(':popover-open')).to.be.false
316
+ suggestionsBox.highlightListItem = null // Ensure no item is highlighted
317
+
318
+ const spy = sinon.spy()
319
+ el.addEventListener('autocomplete', spy)
320
+
321
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, composed: true }))
322
+ await nextFrame()
323
+
324
+ expect(spy).not.to.have.been.called
325
+ expect(suggestionsBox.notifySelect).not.to.have.been.called
326
+ })
327
+ })
328
+
329
+ describe('Suggestion selection (click)', () => {
330
+ it('selects item on click and dispatches event', async () => {
331
+ const el = await basicFixture()
332
+ await el.updateComplete
333
+ const input = getSlottedInput(el)!
334
+ const suggestionsBox = getSlottedSuggestions(el)!
335
+ const items = getSuggestionItems(suggestionsBox)
336
+
337
+ input.focus() // Open popover
338
+ await nextFrame()
339
+
340
+ const eventPromise = oneEvent(el, 'autocomplete')
341
+ // Simulate click on the first item.
342
+ // The `ui-listbox` should dispatch 'select' upon item click.
343
+ // We simulate the 'select' event from the listbox.
344
+ suggestionsBox.dispatchEvent(
345
+ new CustomEvent('select', {
346
+ detail: { item: items[1] }, // Select 'Banana'
347
+ bubbles: true, // Ensure it bubbles to autocomplete-input if needed by its listener
348
+ composed: true,
349
+ })
350
+ )
351
+
352
+ const event = await eventPromise
353
+ expect(event.detail.item).to.equal(items[1])
354
+ expect(suggestionsBox.matches(':popover-open')).to.be.false // Popover should close
355
+ })
356
+ })
357
+
358
+ describe('Dynamic content', () => {
359
+ it('handles dynamically added input', async () => {
360
+ const el = await fixture<AutocompleteInput>(html`<autocomplete-input></autocomplete-input>`)
361
+ await el.updateComplete
362
+
363
+ const inputEl = document.createElement('input')
364
+ inputEl.slot = 'input'
365
+ inputEl.type = 'text'
366
+ el.appendChild(inputEl)
367
+ await nextFrame() // MutationObserver is async
368
+ await el.updateComplete // Ensure component processes the new input
369
+
370
+ const slottedInput = getSlottedInput(el)
371
+ expect(slottedInput).to.equal(inputEl)
372
+ expect(inputEl.id).to.match(/^autocomplete-input-/)
373
+ })
374
+
375
+ it('handles dynamically added suggestions', async () => {
376
+ const el = await fixture<AutocompleteInput>(
377
+ html`<autocomplete-input><input slot="input" id="dyn-input" /></autocomplete-input>`
378
+ )
379
+ await el.updateComplete
380
+ const input = getSlottedInput(el)!
381
+
382
+ const suggestionsEl = document.createElement('ui-listbox') as MdListbox
383
+ suggestionsEl.slot = 'suggestions'
384
+ el.appendChild(suggestionsEl)
385
+ await nextFrame() // MutationObserver
386
+ await el.updateComplete
387
+
388
+ const slottedSuggestions = getSlottedSuggestions(el)
389
+ expect(slottedSuggestions).to.equal(suggestionsEl)
390
+ expect(suggestionsEl.popover).to.equal('manual')
391
+ expect(suggestionsEl.style.getPropertyValue('position-anchor')).to.equal(`--${input.id}`)
392
+ })
393
+
394
+ it('re-filters when suggestion items change (via itemschange event)', async () => {
395
+ const el = await basicFixture()
396
+ await el.updateComplete
397
+ const input = getSlottedInput(el)!
398
+ const suggestionsBox = getSlottedSuggestions(el)!
399
+ let items = getSuggestionItems(suggestionsBox)
400
+
401
+ input.focus()
402
+ input.value = 'ban' // Should show "Banana"
403
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
404
+ await nextFrame()
405
+ expect(items[0].hidden).to.be.true // Apple
406
+ expect(items[1].hidden).to.be.false // Banana
407
+
408
+ // Add a new item that matches "ban"
409
+ const newItem = document.createElement('ui-list-item') as MdListItem
410
+ newItem.dataset.value = 'bandana'
411
+ newItem.textContent = 'Bandana'
412
+ suggestionsBox.appendChild(newItem)
413
+
414
+ // Simulate 'itemschange' event from listbox
415
+ suggestionsBox.dispatchEvent(new CustomEvent('itemschange'))
416
+ await nextFrame() // Allow handler to run
417
+
418
+ items = getSuggestionItems(suggestionsBox) // Re-fetch items
419
+ expect(items.find((i) => i.dataset.value === 'bandana')?.hidden).to.be.false
420
+ expect(items.find((i) => i.dataset.value === 'banana')?.hidden).to.be.false
421
+ expect(items.find((i) => i.dataset.value === 'apple')?.hidden).to.be.true
422
+ })
423
+ })
424
+
425
+ describe('`opened` property', () => {
426
+ it('reflects popover state', async () => {
427
+ const el = await basicFixture()
428
+ await el.updateComplete
429
+ const input = getSlottedInput(el)!
430
+ const suggestions = getSlottedSuggestions(el)!
431
+
432
+ expect(el.opened).to.be.false
433
+
434
+ input.focus()
435
+ await nextFrame()
436
+ expect(el.opened).to.be.true
437
+ expect(suggestions.matches(':popover-open')).to.be.true
438
+
439
+ // @ts-expect-error protected method
440
+ el.closeSuggestions()
441
+ await nextFrame()
442
+ expect(el.opened).to.be.false
443
+ expect(suggestions.matches(':popover-open')).to.be.false
444
+ })
445
+ })
446
+
447
+ describe('Popover Positioning', () => {
448
+ let el: AutocompleteInput
449
+ let input: HTMLInputElement
450
+ let suggestionsBox: MdListbox
451
+ let getBoundingClientRectStub: sinon.SinonStub
452
+
453
+ // Define a standard popover height for consistent threshold calculation
454
+ const popoverVisibleHeight = 200 // px, used for scrollHeight
455
+
456
+ beforeEach(async () => {
457
+ el = await basicFixture() // This fixture has suggestions
458
+ await el.updateComplete // Ensures inputRef is set up
459
+ input = getSlottedInput(el)!
460
+ suggestionsBox = getSlottedSuggestions(el)!
461
+
462
+ // Set a known height for the suggestions box to make its scrollHeight predictable
463
+ suggestionsBox.style.height = `${popoverVisibleHeight}px`
464
+ suggestionsBox.style.display = 'block' // Ensure it's not display:none from other CSS
465
+ suggestionsBox.style.overflow = 'auto'
466
+ // Ensure it has some content to have a scrollHeight if not stubbed
467
+ if (getSuggestionItems(suggestionsBox).length === 0) {
468
+ const item = document.createElement('ui-list-item')
469
+ item.textContent = 'Dummy Item'
470
+ suggestionsBox.appendChild(item)
471
+ }
472
+ await nextFrame() // Allow DOM to update for scrollHeight calculation
473
+
474
+ // @ts-expect-error accessing protected member `inputRef`
475
+ const anchorEl = el.inputRef as HTMLElement // Default anchor
476
+ if (!anchorEl) {
477
+ throw new Error('el.inputRef was not initialized')
478
+ }
479
+ getBoundingClientRectStub = sinon.stub(anchorEl, 'getBoundingClientRect')
480
+ })
481
+
482
+ afterEach(() => {
483
+ sinon.restore() // Restores window.innerHeight and the getBoundingClientRectStub
484
+ })
485
+
486
+ function setAnchorPosition(
487
+ anchorRect: Partial<DOMRect & { height: number; width: number }>,
488
+ viewportHeight: number
489
+ ) {
490
+ getBoundingClientRectStub.returns(anchorRect as DOMRect)
491
+ // Stub window.innerHeight for this call, will be restored by sinon.restore() in afterEach
492
+ sinon.stub(window, 'innerHeight').get(() => viewportHeight)
493
+ }
494
+
495
+ it('positions at bottom when ample space below', async () => {
496
+ // Anchor near top, viewport large. popoverThresholdHeight will be popoverVisibleHeight (200)
497
+ setAnchorPosition({ top: 50, bottom: 80, height: 30, x: 0, y: 50, width: 100, left: 0, right: 100 }, 800)
498
+ // spaceBelow = 800 - 80 = 720. 720 > 200.
499
+
500
+ input.focus() // Triggers openSuggestions
501
+ await el.updateComplete // For positionArea state change and re-render
502
+ await nextFrame() // For DOM to reflect changes
503
+
504
+ expect(el.positionArea).to.equal('bottom')
505
+ })
506
+
507
+ it('positions at top when insufficient space below but sufficient space above', async () => {
508
+ // popoverThresholdHeight = 200
509
+ // Anchor bottom: 750. Viewport: 800. spaceBelow = 50. (50 < 200)
510
+ // Anchor top: 500. spaceAbove = 500. (500 > 50) && (500 > 200) -> true
511
+ setAnchorPosition({ top: 500, bottom: 750, height: 250, x: 0, y: 500, width: 100, left: 0, right: 100 }, 800)
512
+
513
+ input.focus()
514
+ await el.updateComplete
515
+ await nextFrame()
516
+
517
+ expect(el.positionArea).to.equal('top')
518
+ })
519
+
520
+ it('positions at top when insufficient space below and more space above', async () => {
521
+ // popoverThresholdHeight = 200
522
+ // spaceBelow = 50 (anchor.bottom = viewportHeight - 50)
523
+ // spaceAbove = 100 (anchor.top = 100)
524
+ // viewportHeight = anchor.top + anchor.height + spaceBelow = 100 + 30 + 50 = 180
525
+ // Test case: spaceBelow=50, spaceAbove=100. popoverThresholdHeight=200.
526
+ // (50 < 200) is true.
527
+ // (100 > 50) is true.
528
+ // (100 > 200) is false. -> first 'top' condition fails.
529
+ // Second 'top' condition: (spaceBelow < popoverThresholdHeight && spaceAbove > spaceBelow) -> true.
530
+ setAnchorPosition({ top: 100, bottom: 130, height: 30, x: 0, y: 100, width: 100, left: 0, right: 100 }, 180)
531
+
532
+ input.focus()
533
+ await el.updateComplete
534
+ await nextFrame()
535
+
536
+ expect(el.positionArea).to.equal('top')
537
+ })
538
+
539
+ it('positions at bottom when insufficient space below and also insufficient (or less) space above', async () => {
540
+ // popoverThresholdHeight = 200
541
+ // spaceBelow = 50 (anchor.bottom = viewportHeight - 50)
542
+ // spaceAbove = 40 (anchor.top = 40)
543
+ // viewportHeight = anchor.top + anchor.height + spaceBelow = 40 + 30 + 50 = 120
544
+ // Test case: spaceBelow=50, spaceAbove=40.
545
+ // (50 < 200) is true.
546
+ // (40 > 50) is false. -> both 'top' conditions fail. Defaults to 'bottom'.
547
+ setAnchorPosition({ top: 40, bottom: 70, height: 30, x: 0, y: 40, width: 100, left: 0, right: 100 }, 120)
548
+
549
+ input.focus()
550
+ await el.updateComplete
551
+ await nextFrame()
552
+
553
+ expect(el.positionArea).to.equal('bottom')
554
+ })
555
+
556
+ it('uses fallback threshold of 150px if popover scrollHeight is 0', async () => {
557
+ // Stub scrollHeight to be 0 for this test.
558
+ const scrollHeightStub = sinon.stub(suggestionsBox, 'scrollHeight').get(() => 0)
559
+
560
+ // popoverThresholdHeight will be 150 (the fallback).
561
+ // Scenario: spaceBelow = 100, spaceAbove = 200.
562
+ // (100 < 150) is true.
563
+ // (200 > 100) is true. (200 > 150) is true. -> Should be 'top'.
564
+ setAnchorPosition({ top: 500, bottom: 700, height: 200, x: 0, y: 500, width: 100, left: 0, right: 100 }, 800)
565
+ // spaceBelow = 800 - 700 = 100
566
+ // spaceAbove = 500 (mistake in manual calc above, should be 500)
567
+ // Corrected: spaceBelow = 100. spaceAbove = 500. Threshold = 150.
568
+ // (100 < 150) -> true
569
+ // (500 > 100) -> true. (500 > 150) -> true. Result: 'top'.
570
+
571
+ input.focus()
572
+ await el.updateComplete
573
+ await nextFrame()
574
+
575
+ expect(el.positionArea).to.equal('top')
576
+ scrollHeightStub.restore()
577
+ })
578
+
579
+ it('uses slotted anchor for positioning if provided', async () => {
580
+ // Need to create a new fixture for this specific setup
581
+ el = await fixture(html`
582
+ <autocomplete-input>
583
+ <div slot="anchor" id="custom-anchor" style="height: 20px; width: 100px; border: 1px solid red;"></div>
584
+ <input slot="input" type="text" />
585
+ <ui-listbox slot="suggestions" style="height: ${popoverVisibleHeight}px; overflow: auto;">
586
+ <ui-list-item>Item 1</ui-list-item>
587
+ </ui-listbox>
588
+ </autocomplete-input>
589
+ `)
590
+ await el.updateComplete
591
+ input = getSlottedInput(el)! // Still need to focus input
592
+ const customAnchor = el.querySelector('#custom-anchor') as HTMLElement
593
+
594
+ getBoundingClientRectStub.restore() // remove stub from default input anchor
595
+ getBoundingClientRectStub = sinon.stub(customAnchor, 'getBoundingClientRect')
596
+
597
+ // Scenario: custom anchor is near bottom, should open top. popoverThresholdHeight = 200.
598
+ setAnchorPosition({ top: 720, bottom: 750, height: 30, x: 0, y: 720, width: 100, left: 0, right: 100 }, 800)
599
+
600
+ input.focus() // Triggers openSuggestions
601
+ await el.updateComplete
602
+ await nextFrame()
603
+
604
+ expect(el.positionArea).to.equal('top')
605
+ expect(getBoundingClientRectStub.called).to.be.true
606
+ })
607
+ })
608
+
609
+ describe('Accessibility', () => {
610
+ it('is accessible when initially rendered', async () => {
611
+ const el = await basicFixture()
612
+ await el.updateComplete
613
+ await assert.isAccessible(el)
614
+ })
615
+
616
+ it('is accessible when popover is open', async () => {
617
+ const el = await basicFixture()
618
+ await el.updateComplete
619
+ const input = getSlottedInput(el)!
620
+
621
+ input.focus() // Opens popover
622
+ await nextFrame() // Allow popover to open
623
+ await el.updateComplete // Ensure state updates related to opening are done
624
+
625
+ await assert.isAccessible(el)
626
+ })
627
+
628
+ it('is accessible when popover is open and an item is highlighted', async () => {
629
+ const el = await basicFixture()
630
+ await el.updateComplete
631
+ const input = getSlottedInput(el)!
632
+
633
+ input.focus() // Opens popover
634
+ await nextFrame()
635
+
636
+ // Simulate highlighting the first item
637
+ input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true, composed: true }))
638
+ await aTimeout(0) // for rAF in handleKeydown
639
+ await nextFrame() // for highlight to apply
640
+ await assert.isAccessible(el)
641
+ })
642
+ })
643
+ })