@api-client/ui 0.2.4 → 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.
package/.aiexclude ADDED
@@ -0,0 +1,3 @@
1
+ coverage/
2
+ .tmp/
3
+ build/
@@ -9,5 +9,8 @@
9
9
  "startactivityforresult",
10
10
  "typescale"
11
11
  ],
12
- "vscode-wtr-runner.testRunnerCommand": "npm run tsc:all && npx wtr --playwright --browsers \"chromium\""
12
+ "vscode-wtr-runner.testRunnerCommand": "npm run tsc:all && npx wtr --playwright --browsers \"chromium\"",
13
+ "geminicodeassist.customCommands": {
14
+ "add-comments": "add comments to my code. add examples whenever reasonable. focus on another developer understanding what the function is about. don't forget about class fields."
15
+ }
13
16
  }
@@ -20,10 +20,20 @@ import type UiListbox from '../../../md/listbox/internals/Listbox.js';
20
20
  * - If a `ui-list-item` has a `data-index` attribute (e.g., `data-index="name email"`), filtering will search
21
21
  * within the specified `data-*` attributes (e.g., `data-name`, `data-email`).
22
22
  * - **Mutation Awareness**: Reacts to changes in slotted suggestions, re-filtering them if necessary.
23
- * - **Event-Driven**: Notifies the parent application via an `autocomplete` event when a suggestion is selected,
24
- * without directly modifying the input's value. This allows the application author to control the input update logic.
25
- * - **Accessibility**: Designed with accessibility in mind, ensuring keyboard navigability and proper ARIA attribute
26
- * management (handled by `ui-listbox`).
23
+ * - **Event-Driven**: Notifies the application via an `autocomplete` event when a suggestion is selected.
24
+ * The component itself does not modify the input's value, giving the application author full control.
25
+ *
26
+ * ### Accessibility
27
+ *
28
+ * The `autocomplete-input` component is designed with accessibility at its core:
29
+ * - **Keyboard Navigation**: Full keyboard support for navigating suggestions (ArrowUp, ArrowDown),
30
+ * selecting (Enter), and closing (Escape), all while focus remains on the input field.
31
+ * - **ARIA Attributes**: While the `autocomplete-input` manages ARIA attributes related to the popover's
32
+ * state, the slotted `ui-listbox` is responsible for its internal ARIA roles and states
33
+ * (e.g., `role="listbox"`, `aria-activedescendant`).
34
+ * - **Labeling Suggestions**: It is **crucial** for accessibility that the slotted `ui-listbox`
35
+ * has an `aria-label` attribute. This provides a descriptive name for the list of suggestions,
36
+ * which is announced by screen readers.
27
37
  *
28
38
  * The component uses CSS anchor positioning to place the suggestions popover relative to the input.
29
39
  * Ensure your `ui-listbox` is styled appropriately for popover display (e.g., `popover="manual"`, `position-anchor`).
@@ -33,6 +43,10 @@ import type UiListbox from '../../../md/listbox/internals/Listbox.js';
33
43
  * or behave like one (have a `value` property and dispatch `input`, `focus`, `blur`, and `keydown`
34
44
  * events).
35
45
  * @slot suggestions - The `ui-listbox` element containing `ui-list-item` elements as suggestions.
46
+ * @slot anchor - An optional element that points to element that will be used as the anchor for the popover.
47
+ * This is useful if you want to position the suggestions relative to a different element than the input.
48
+ * If not provided, the input element will be used as the anchor.
49
+ * @slot Any additional content that should be rendered inside the component.
36
50
  *
37
51
  * @fires autocomplete - Dispatched when a suggestion is selected by the user (e.g., via click or Enter key).
38
52
  * The `event.detail` object contains:
@@ -47,6 +61,12 @@ import type UiListbox from '../../../md/listbox/internals/Listbox.js';
47
61
  * <ui-list-item data-value="banana">Banana</ui-list-item>
48
62
  * <ui-list-item data-value="cherry">Cherry</ui-list-item>
49
63
  * </ui-listbox>
64
+ * <!-- With aria-label for accessibility -->
65
+ * <ui-listbox slot="suggestions" aria-label="Fruit suggestions">
66
+ * <ui-list-item data-value="apple">Apple</ui-list-item>
67
+ * <ui-list-item data-value="banana">Banana</ui-list-item>
68
+ * <ui-list-item data-value="cherry">Cherry</ui-list-item>
69
+ * </ui-listbox>
50
70
  * </autocomplete-input>
51
71
  *
52
72
  * <script>
@@ -66,7 +86,7 @@ import type UiListbox from '../../../md/listbox/internals/Listbox.js';
66
86
  * ```html
67
87
  * <autocomplete-input @autocomplete="${this.handleUserSelection}">
68
88
  * <input slot="input" type="text" placeholder="Search users..." />
69
- * <ui-listbox slot="suggestions">
89
+ * <ui-listbox slot="suggestions" aria-label="User suggestions">
70
90
  * <ui-list-item data-id="1" data-name="Alice Wonderland" data-email="alice@example.com" data-index="name email">
71
91
  * Alice Wonderland
72
92
  * <span slot="supporting-text">alice@example.com</span>
@@ -93,6 +113,7 @@ import type UiListbox from '../../../md/listbox/internals/Listbox.js';
93
113
  * ```
94
114
  */
95
115
  export default class Autocomplete extends LitElement {
116
+ protected createRenderRoot(): HTMLElement | DocumentFragment;
96
117
  /**
97
118
  * The MutationObserver instance used to watch for changes in slotted children.
98
119
  */
@@ -102,6 +123,19 @@ export default class Autocomplete extends LitElement {
102
123
  * This is used for CSS anchoring and to ensure unique IDs for accessibility.
103
124
  */
104
125
  protected accessor inputId: string;
126
+ /**
127
+ * The position area for the suggestions popover.
128
+ * This can be 'bottom' or 'top', depending on available space.
129
+ * Default is 'bottom'.
130
+ *
131
+ * Note, this is set dynamically based on available space
132
+ * and the position of the input element. This only sets the initial value.
133
+ *
134
+ * @attribute
135
+ * @type {'bottom' | 'top'}
136
+ * @default 'bottom'
137
+ */
138
+ accessor positionArea: 'bottom' | 'top';
105
139
  /**
106
140
  * The reference to the slotted input element.
107
141
  * This should be an `HTMLInputElement` or behave like one.
@@ -194,6 +228,20 @@ export default class Autocomplete extends LitElement {
194
228
  * Opens the suggestions popover if it's not already open and there are visible items.
195
229
  */
196
230
  protected openSuggestions(): void;
231
+ /**
232
+ * Decides the position area for the popover based on available space.
233
+ * It checks if there is enough space below or above the anchor element and decides accordingly.
234
+ *
235
+ * We need to do this because we set the popover height to `100%`, `-webkit-fill-available`, or `-moz-available`
236
+ * and it makes the popover to always open at the bottom, even if there is no space. The `position-try-fallbacks`
237
+ * will not work in this case, because from its perspective there is always enough space, even if that will cause
238
+ * the popover to have a height of just a few pixels.
239
+ *
240
+ * @param popover The popover element to position.
241
+ * @param anchor The anchor element relative to which the popover will be positioned.
242
+ * @returns 'top' or 'bottom' based on available space.
243
+ */
244
+ protected decidePositionArea(popover: HTMLElement, anchor: HTMLElement): 'top' | 'bottom';
197
245
  /**
198
246
  * Closes the suggestions popover if it's open and clears any highlighted item.
199
247
  */
@@ -1 +1 @@
1
- {"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../../../../../src/elements/autocomplete/internals/autocomplete.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,KAAK,CAAA;AAEtE,OAAO,KAAK,SAAS,MAAM,0CAA0C,CAAA;AAIrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2FG;AACH,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,UAAU;IAClD;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAErC;;;OAGG;IACM,SAAS,CAAC,QAAQ,CAAC,OAAO,SAAK;IAExC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,WAAW,GAAG,IAAI,CAAA;IAC1D;;;OAGG;IACH,SAAS,CAAC,cAAc,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAE3C;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAEQ,iBAAiB,IAAI,IAAI;IAMzB,oBAAoB,IAAI,IAAI;cAQlB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI;IAKzD;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBjC;;;;OAIG;IACH,SAAS,CAAC,eAAe,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI;IAyB5D;;OAEG;IACH,SAAS,CAAC,mBAAmB,IAAI,IAAI;IAUrC;;OAEG;IACH,SAAS,CAAC,yBAAyB,IAAI,IAAI;IAY3C;;;;OAIG;IACH,SAAS,CAAC,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAwB9C;;;;OAIG;IACH,SAAS,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,SAAS,GAAG,IAAI;IAmB/D;;;OAGG;IAEH,SAAS,CAAC,gBAAgB,IAAI,IAAI;IAalC;;;;OAIG;IAEH,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAMzC;;;;;;OAMG;IAEH,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IA4BnD;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAazC;;;OAGG;IAEH,SAAS,CAAC,eAAe,IAAI,IAAI;IAajC;;;;OAIG;IAEH,SAAS,CAAC,sBAAsB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAa1D;;;OAGG;IAEH,SAAS,CAAC,2BAA2B,IAAI,IAAI;IAS7C;;OAEG;IACH,SAAS,CAAC,eAAe,IAAI,IAAI;IAMjC;;OAEG;IACH,SAAS,CAAC,gBAAgB,IAAI,IAAI;IAMlC;;;;OAIG;IACH,SAAS,CAAC,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;cA6C7B,MAAM,IAAI,cAAc;CAM5C"}
1
+ {"version":3,"file":"autocomplete.d.ts","sourceRoot":"","sources":["../../../../../src/elements/autocomplete/internals/autocomplete.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,KAAK,CAAA;AAEtE,OAAO,KAAK,SAAS,MAAM,0CAA0C,CAAA;AAIrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+GG;AACH,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,UAAU;cAC/B,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAIrE;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;IAErC;;;OAGG;IACM,SAAS,CAAC,QAAQ,CAAC,OAAO,SAAK;IAExC;;;;;;;;;;;OAWG;IACS,QAAQ,CAAC,YAAY,EAAE,QAAQ,GAAG,KAAK,CAAW;IAE9D;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,WAAW,GAAG,IAAI,CAAA;IAC1D;;;OAGG;IACH,SAAS,CAAC,cAAc,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAE3C;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAEQ,iBAAiB,IAAI,IAAI;IASzB,oBAAoB,IAAI,IAAI;cAQlB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,IAAI;IAKzD;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;;;OAIG;IACH,SAAS,CAAC,eAAe,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,IAAI;IAyB5D;;OAEG;IACH,SAAS,CAAC,mBAAmB,IAAI,IAAI;IAUrC;;OAEG;IACH,SAAS,CAAC,yBAAyB,IAAI,IAAI;IAY3C;;;;OAIG;IACH,SAAS,CAAC,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IA0B9C;;;;OAIG;IACH,SAAS,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,SAAS,GAAG,IAAI;IAmB/D;;;OAGG;IAEH,SAAS,CAAC,gBAAgB,IAAI,IAAI;IAalC;;;;OAIG;IAEH,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAMzC;;;;;;OAMG;IAEH,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IA4BnD;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAazC;;;OAGG;IAEH,SAAS,CAAC,eAAe,IAAI,IAAI;IAajC;;;;OAIG;IAEH,SAAS,CAAC,sBAAsB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAa1D;;;OAGG;IAEH,SAAS,CAAC,2BAA2B,IAAI,IAAI;IAS7C;;OAEG;IACH,SAAS,CAAC,eAAe,IAAI,IAAI;IAcjC;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,GAAG,QAAQ;IAuBzF;;OAEG;IACH,SAAS,CAAC,gBAAgB,IAAI,IAAI;IAMlC;;;;OAIG;IACH,SAAS,CAAC,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;cA6C7B,MAAM,IAAI,cAAc;CAmC5C"}
@@ -1,6 +1,6 @@
1
1
  import { __esDecorate, __runInitializers } from "tslib";
2
2
  import { html, LitElement } from 'lit';
3
- import { state } from 'lit/decorators.js';
3
+ import { property, state } from 'lit/decorators.js';
4
4
  import { bound } from '../../../decorators/bound.js';
5
5
  let Autocomplete = (() => {
6
6
  let _classSuper = LitElement;
@@ -8,6 +8,9 @@ let Autocomplete = (() => {
8
8
  let _inputId_decorators;
9
9
  let _inputId_initializers = [];
10
10
  let _inputId_extraInitializers = [];
11
+ let _positionArea_decorators;
12
+ let _positionArea_initializers = [];
13
+ let _positionArea_extraInitializers = [];
11
14
  let _handleInputFocus_decorators;
12
15
  let _handleInput_decorators;
13
16
  let _handleKeydown_decorators;
@@ -18,6 +21,7 @@ let Autocomplete = (() => {
18
21
  static {
19
22
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
20
23
  _inputId_decorators = [state()];
24
+ _positionArea_decorators = [property()];
21
25
  _handleInputFocus_decorators = [bound];
22
26
  _handleInput_decorators = [bound];
23
27
  _handleKeydown_decorators = [bound];
@@ -25,6 +29,7 @@ let Autocomplete = (() => {
25
29
  _handleSuggestionSelect_decorators = [bound];
26
30
  _handleSuggestionsSlotChange_decorators = [bound];
27
31
  __esDecorate(this, null, _inputId_decorators, { kind: "accessor", name: "inputId", static: false, private: false, access: { has: obj => "inputId" in obj, get: obj => obj.inputId, set: (obj, value) => { obj.inputId = value; } }, metadata: _metadata }, _inputId_initializers, _inputId_extraInitializers);
32
+ __esDecorate(this, null, _positionArea_decorators, { kind: "accessor", name: "positionArea", static: false, private: false, access: { has: obj => "positionArea" in obj, get: obj => obj.positionArea, set: (obj, value) => { obj.positionArea = value; } }, metadata: _metadata }, _positionArea_initializers, _positionArea_extraInitializers);
28
33
  __esDecorate(this, null, _handleInputFocus_decorators, { kind: "method", name: "handleInputFocus", static: false, private: false, access: { has: obj => "handleInputFocus" in obj, get: obj => obj.handleInputFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
29
34
  __esDecorate(this, null, _handleInput_decorators, { kind: "method", name: "handleInput", static: false, private: false, access: { has: obj => "handleInput" in obj, get: obj => obj.handleInput }, metadata: _metadata }, null, _instanceExtraInitializers);
30
35
  __esDecorate(this, null, _handleKeydown_decorators, { kind: "method", name: "handleKeydown", static: false, private: false, access: { has: obj => "handleKeydown" in obj, get: obj => obj.handleKeydown }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -33,14 +38,25 @@ let Autocomplete = (() => {
33
38
  __esDecorate(this, null, _handleSuggestionsSlotChange_decorators, { kind: "method", name: "handleSuggestionsSlotChange", static: false, private: false, access: { has: obj => "handleSuggestionsSlotChange" in obj, get: obj => obj.handleSuggestionsSlotChange }, metadata: _metadata }, null, _instanceExtraInitializers);
34
39
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
35
40
  }
41
+ createRenderRoot() {
42
+ return this;
43
+ }
36
44
  /**
37
45
  * The MutationObserver instance used to watch for changes in slotted children.
38
46
  */
39
47
  observer = __runInitializers(this, _instanceExtraInitializers);
40
48
  #inputId_accessor_storage = __runInitializers(this, _inputId_initializers, ''
41
49
  /**
42
- * The reference to the slotted input element.
43
- * This should be an `HTMLInputElement` or behave like one.
50
+ * The position area for the suggestions popover.
51
+ * This can be 'bottom' or 'top', depending on available space.
52
+ * Default is 'bottom'.
53
+ *
54
+ * Note, this is set dynamically based on available space
55
+ * and the position of the input element. This only sets the initial value.
56
+ *
57
+ * @attribute
58
+ * @type {'bottom' | 'top'}
59
+ * @default 'bottom'
44
60
  */
45
61
  );
46
62
  /**
@@ -49,11 +65,31 @@ let Autocomplete = (() => {
49
65
  */
50
66
  get inputId() { return this.#inputId_accessor_storage; }
51
67
  set inputId(value) { this.#inputId_accessor_storage = value; }
68
+ #positionArea_accessor_storage = (__runInitializers(this, _inputId_extraInitializers), __runInitializers(this, _positionArea_initializers, 'bottom'
52
69
  /**
53
70
  * The reference to the slotted input element.
54
71
  * This should be an `HTMLInputElement` or behave like one.
55
72
  */
56
- inputRef = __runInitializers(this, _inputId_extraInitializers);
73
+ ));
74
+ /**
75
+ * The position area for the suggestions popover.
76
+ * This can be 'bottom' or 'top', depending on available space.
77
+ * Default is 'bottom'.
78
+ *
79
+ * Note, this is set dynamically based on available space
80
+ * and the position of the input element. This only sets the initial value.
81
+ *
82
+ * @attribute
83
+ * @type {'bottom' | 'top'}
84
+ * @default 'bottom'
85
+ */
86
+ get positionArea() { return this.#positionArea_accessor_storage; }
87
+ set positionArea(value) { this.#positionArea_accessor_storage = value; }
88
+ /**
89
+ * The reference to the slotted input element.
90
+ * This should be an `HTMLInputElement` or behave like one.
91
+ */
92
+ inputRef = __runInitializers(this, _positionArea_extraInitializers);
57
93
  /**
58
94
  * The reference to the slotted suggestions element, which should be a `ui-listbox`.
59
95
  * This is used to manage the suggestions popover and filtering.
@@ -69,6 +105,9 @@ let Autocomplete = (() => {
69
105
  super.connectedCallback();
70
106
  this.observer = new MutationObserver(this.handleMutations.bind(this));
71
107
  this.observer.observe(this, { childList: true });
108
+ if (!this.id) {
109
+ this.id = `autocomplete-${Math.random().toString(36).substring(2, 15)}`;
110
+ }
72
111
  }
73
112
  disconnectedCallback() {
74
113
  super.disconnectedCallback();
@@ -87,20 +126,13 @@ let Autocomplete = (() => {
87
126
  */
88
127
  async firstSetup() {
89
128
  await this.updateComplete;
90
- const input = this.shadowRoot?.querySelector('slot[name="input"]');
91
- const suggestions = this.shadowRoot?.querySelector('slot[name="suggestions"]');
129
+ const input = this.querySelector('[slot="input"]');
130
+ const suggestions = this.querySelector('[slot="suggestions"]');
92
131
  if (input) {
93
- const elements = input.assignedElements();
94
- if (elements.length > 0 && elements[0] instanceof HTMLElement) {
95
- this.setupInput(elements[0]);
96
- }
132
+ this.setupInput(input);
97
133
  }
98
134
  if (suggestions) {
99
- const elements = suggestions.assignedElements();
100
- if (elements.length > 0 && elements[0] instanceof HTMLElement) {
101
- // Assuming the slotted element is ui-listbox or compatible
102
- this.setupSuggestions(elements[0]);
103
- }
135
+ this.setupSuggestions(suggestions);
104
136
  }
105
137
  }
106
138
  /**
@@ -174,8 +206,10 @@ let Autocomplete = (() => {
174
206
  else {
175
207
  this.inputId = input.id;
176
208
  }
209
+ const anchorElement = this.querySelector('[slot="anchor"]');
177
210
  // Ensure CSS anchor positioning can work
178
- input.style.setProperty('anchor-name', `--${this.inputId}`);
211
+ const anchor = anchorElement || input;
212
+ anchor.style.setProperty('anchor-name', `--${this.inputId}`);
179
213
  this.inputRef = input; // Assuming it behaves like an input
180
214
  this.inputRef.addEventListener('focus', this.handleInputFocus);
181
215
  this.inputRef.addEventListener('input', this.handleInput);
@@ -330,9 +364,52 @@ let Autocomplete = (() => {
330
364
  * Opens the suggestions popover if it's not already open and there are visible items.
331
365
  */
332
366
  openSuggestions() {
333
- if (this.suggestionsRef && !this.suggestionsRef.matches(':popover-open')) {
334
- this.suggestionsRef.showPopover();
367
+ const popover = this.suggestionsRef;
368
+ if (!popover || popover.matches(':popover-open')) {
369
+ return;
370
+ }
371
+ // we need to open the popover first to make any measurements
372
+ popover.showPopover();
373
+ const anchor = this.querySelector('[slot="anchor"]') || this.inputRef;
374
+ if (!anchor) {
375
+ return;
335
376
  }
377
+ this.positionArea = this.decidePositionArea(popover, anchor);
378
+ }
379
+ /**
380
+ * Decides the position area for the popover based on available space.
381
+ * It checks if there is enough space below or above the anchor element and decides accordingly.
382
+ *
383
+ * We need to do this because we set the popover height to `100%`, `-webkit-fill-available`, or `-moz-available`
384
+ * and it makes the popover to always open at the bottom, even if there is no space. The `position-try-fallbacks`
385
+ * will not work in this case, because from its perspective there is always enough space, even if that will cause
386
+ * the popover to have a height of just a few pixels.
387
+ *
388
+ * @param popover The popover element to position.
389
+ * @param anchor The anchor element relative to which the popover will be positioned.
390
+ * @returns 'top' or 'bottom' based on available space.
391
+ */
392
+ decidePositionArea(popover, anchor) {
393
+ let newArea = 'bottom';
394
+ // Get bounding rectangles
395
+ const anchorRect = anchor.getBoundingClientRect();
396
+ const viewportHeight = window.innerHeight;
397
+ const spaceBelow = viewportHeight - anchorRect.bottom;
398
+ const spaceAbove = anchorRect.top;
399
+ // Estimate a typical/minimum height the popover might need to be useful
400
+ const popoverThresholdHeight = popover.scrollHeight || 150;
401
+ if (spaceBelow < popoverThresholdHeight && spaceAbove > spaceBelow && spaceAbove > popoverThresholdHeight) {
402
+ // Not enough space below, but more (and sufficient) space above
403
+ newArea = 'top';
404
+ }
405
+ else if (spaceBelow < popoverThresholdHeight && spaceAbove > spaceBelow) {
406
+ // Not enough space below, and space above is more than space below (even if not "sufficient")
407
+ newArea = 'top';
408
+ }
409
+ else {
410
+ newArea = 'bottom'; // Default to bottom if enough space or if top is worse
411
+ }
412
+ return newArea;
336
413
  }
337
414
  /**
338
415
  * Closes the suggestions popover if it's open and clears any highlighted item.
@@ -390,9 +467,38 @@ let Autocomplete = (() => {
390
467
  }
391
468
  }
392
469
  render() {
470
+ const { id, positionArea } = this;
393
471
  return html `
394
- <slot name="input"></slot>
395
- <slot name="suggestions"></slot>
472
+ <style>
473
+ #${id} {
474
+ display: inline-block;
475
+
476
+ [popover] {
477
+ border: none;
478
+ margin: 0;
479
+ position-area: ${positionArea};
480
+ position-try-fallbacks:
481
+ flip-block,
482
+ flip-inline,
483
+ flip-block flip-inline;
484
+ width: anchor-size(width);
485
+
486
+ box-shadow: var(--md-sys-elevation-1);
487
+ border-radius: var(--md-sys-shape-corner-medium);
488
+
489
+ overflow: auto;
490
+ /* We try 100% and then vendor options which are more accurate */
491
+ height: 100%;
492
+ height: -webkit-fill-available;
493
+ height: -moz-available;
494
+ max-height: max-content;
495
+ }
496
+
497
+ [popover]:not(:popover-open) {
498
+ display: none;
499
+ }
500
+ }
501
+ </style>
396
502
  `;
397
503
  }
398
504
  };
@@ -417,10 +523,20 @@ let Autocomplete = (() => {
417
523
  * - If a `ui-list-item` has a `data-index` attribute (e.g., `data-index="name email"`), filtering will search
418
524
  * within the specified `data-*` attributes (e.g., `data-name`, `data-email`).
419
525
  * - **Mutation Awareness**: Reacts to changes in slotted suggestions, re-filtering them if necessary.
420
- * - **Event-Driven**: Notifies the parent application via an `autocomplete` event when a suggestion is selected,
421
- * without directly modifying the input's value. This allows the application author to control the input update logic.
422
- * - **Accessibility**: Designed with accessibility in mind, ensuring keyboard navigability and proper ARIA attribute
423
- * management (handled by `ui-listbox`).
526
+ * - **Event-Driven**: Notifies the application via an `autocomplete` event when a suggestion is selected.
527
+ * The component itself does not modify the input's value, giving the application author full control.
528
+ *
529
+ * ### Accessibility
530
+ *
531
+ * The `autocomplete-input` component is designed with accessibility at its core:
532
+ * - **Keyboard Navigation**: Full keyboard support for navigating suggestions (ArrowUp, ArrowDown),
533
+ * selecting (Enter), and closing (Escape), all while focus remains on the input field.
534
+ * - **ARIA Attributes**: While the `autocomplete-input` manages ARIA attributes related to the popover's
535
+ * state, the slotted `ui-listbox` is responsible for its internal ARIA roles and states
536
+ * (e.g., `role="listbox"`, `aria-activedescendant`).
537
+ * - **Labeling Suggestions**: It is **crucial** for accessibility that the slotted `ui-listbox`
538
+ * has an `aria-label` attribute. This provides a descriptive name for the list of suggestions,
539
+ * which is announced by screen readers.
424
540
  *
425
541
  * The component uses CSS anchor positioning to place the suggestions popover relative to the input.
426
542
  * Ensure your `ui-listbox` is styled appropriately for popover display (e.g., `popover="manual"`, `position-anchor`).
@@ -430,6 +546,10 @@ let Autocomplete = (() => {
430
546
  * or behave like one (have a `value` property and dispatch `input`, `focus`, `blur`, and `keydown`
431
547
  * events).
432
548
  * @slot suggestions - The `ui-listbox` element containing `ui-list-item` elements as suggestions.
549
+ * @slot anchor - An optional element that points to element that will be used as the anchor for the popover.
550
+ * This is useful if you want to position the suggestions relative to a different element than the input.
551
+ * If not provided, the input element will be used as the anchor.
552
+ * @slot Any additional content that should be rendered inside the component.
433
553
  *
434
554
  * @fires autocomplete - Dispatched when a suggestion is selected by the user (e.g., via click or Enter key).
435
555
  * The `event.detail` object contains:
@@ -444,6 +564,12 @@ let Autocomplete = (() => {
444
564
  * <ui-list-item data-value="banana">Banana</ui-list-item>
445
565
  * <ui-list-item data-value="cherry">Cherry</ui-list-item>
446
566
  * </ui-listbox>
567
+ * <!-- With aria-label for accessibility -->
568
+ * <ui-listbox slot="suggestions" aria-label="Fruit suggestions">
569
+ * <ui-list-item data-value="apple">Apple</ui-list-item>
570
+ * <ui-list-item data-value="banana">Banana</ui-list-item>
571
+ * <ui-list-item data-value="cherry">Cherry</ui-list-item>
572
+ * </ui-listbox>
447
573
  * </autocomplete-input>
448
574
  *
449
575
  * <script>
@@ -463,7 +589,7 @@ let Autocomplete = (() => {
463
589
  * ```html
464
590
  * <autocomplete-input @autocomplete="${this.handleUserSelection}">
465
591
  * <input slot="input" type="text" placeholder="Search users..." />
466
- * <ui-listbox slot="suggestions">
592
+ * <ui-listbox slot="suggestions" aria-label="User suggestions">
467
593
  * <ui-list-item data-id="1" data-name="Alice Wonderland" data-email="alice@example.com" data-index="name email">
468
594
  * Alice Wonderland
469
595
  * <span slot="supporting-text">alice@example.com</span>
@@ -1 +1 @@
1
- {"version":3,"file":"autocomplete.js","sourceRoot":"","sources":["../../../../../src/elements/autocomplete/internals/autocomplete.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, LitElement, PropertyValues, TemplateResult } from 'lit'\nimport { state } from 'lit/decorators.js'\nimport type UiListbox from '../../../md/listbox/internals/Listbox.js'\nimport type UiListItem from '../../../md/list/internals/ListItem.js'\nimport { bound } from '../../../decorators/bound.js'\n\n/**\n * An accessible and performant autocomplete component that enhances a text input with a list of suggestions.\n *\n * The `autocomplete-input` component provides a flexible way to add autocomplete functionality\n * to any text input. It works by coordinating a slotted input element with a slotted `ui-listbox`\n * containing suggestions.\n *\n * Key Features:\n *\n * - **Popover Management**: Automatically shows and hides the suggestions popover based on input focus\n * and user interaction.\n * - **Keyboard Navigation**: Allows users to navigate suggestions using ArrowUp, ArrowDown keys directly\n * from the input. Enter selects a suggestion, and Escape closes the popover.\n * The suggestion list itself never gains focus.\n * - **Dynamic Filtering**: Filters the list of suggestions as the user types into the input. Items not matching\n * the query are hidden (not removed).\n * - By default, filtering checks `item.dataset.value` and then `item.textContent`.\n * - If a `ui-list-item` has a `data-index` attribute (e.g., `data-index=\"name email\"`), filtering will search\n * within the specified `data-*` attributes (e.g., `data-name`, `data-email`).\n * - **Mutation Awareness**: Reacts to changes in slotted suggestions, re-filtering them if necessary.\n * - **Event-Driven**: Notifies the parent application via an `autocomplete` event when a suggestion is selected,\n * without directly modifying the input's value. This allows the application author to control the input update logic.\n * - **Accessibility**: Designed with accessibility in mind, ensuring keyboard navigability and proper ARIA attribute\n * management (handled by `ui-listbox`).\n *\n * The component uses CSS anchor positioning to place the suggestions popover relative to the input.\n * Ensure your `ui-listbox` is styled appropriately for popover display (e.g., `popover=\"manual\"`, `position-anchor`).\n * The component will manage `showPopover()` and `hidePopover()` calls.\n *\n * @slot input - The input element that will be used for autocomplete. This element should be an `HTMLInputElement`\n * or behave like one (have a `value` property and dispatch `input`, `focus`, `blur`, and `keydown`\n * events).\n * @slot suggestions - The `ui-listbox` element containing `ui-list-item` elements as suggestions.\n *\n * @fires autocomplete - Dispatched when a suggestion is selected by the user (e.g., via click or Enter key).\n * The `event.detail` object contains:\n * - `item`: The selected `UiListItem` instance.\n *\n * @example\n * ```html\n * <autocomplete-input @autocomplete=\"${this.handleAutocompleteSelection}\">\n * <input slot=\"input\" type=\"text\" placeholder=\"Search fruits...\" />\n * <ui-listbox slot=\"suggestions\">\n * <ui-list-item data-value=\"apple\">Apple</ui-list-item>\n * <ui-list-item data-value=\"banana\">Banana</ui-list-item>\n * <ui-list-item data-value=\"cherry\">Cherry</ui-list-item>\n * </ui-listbox>\n * </autocomplete-input>\n *\n * <script>\n * // In your component/script\n * handleAutocompleteSelection(event) {\n * const selectedItem = event.detail.item;\n * const inputElement = this.shadowRoot.querySelector('input[slot=\"input\"]');\n * if (inputElement) {\n * inputElement.value = selectedItem.dataset.value || selectedItem.textContent;\n * }\n * console.log('Selected:', selectedItem.dataset.value);\n * }\n * </script>\n * ```\n *\n * @example Dynamic filtering with `data-index`\n * ```html\n * <autocomplete-input @autocomplete=\"${this.handleUserSelection}\">\n * <input slot=\"input\" type=\"text\" placeholder=\"Search users...\" />\n * <ui-listbox slot=\"suggestions\">\n * <ui-list-item data-id=\"1\" data-name=\"Alice Wonderland\" data-email=\"alice@example.com\" data-index=\"name email\">\n * Alice Wonderland\n * <span slot=\"supporting-text\">alice@example.com</span>\n * </ui-list-item>\n * <ui-list-item data-id=\"2\" data-name=\"Bob The Builder\" data-email=\"bob@example.com\" data-index=\"name email\">\n * Bob The Builder\n * <span slot=\"supporting-text\">bob@example.com</span>\n * </ui-list-item>\n * </ui-listbox>\n * </autocomplete-input>\n *\n * <script>\n * // In your component/script\n * handleUserSelection(event) {\n * const selectedItem = event.detail.item;\n * const inputElement = this.shadowRoot.querySelector('input[slot=\"input\"]');\n * if (inputElement) {\n * // You might want to display the name, but store the ID\n * inputElement.value = selectedItem.dataset.name;\n * }\n * console.log('Selected user ID:', selectedItem.dataset.id);\n * }\n * </script>\n * ```\n */\nexport default class Autocomplete extends LitElement {\n /**\n * The MutationObserver instance used to watch for changes in slotted children.\n */\n protected observer?: MutationObserver\n\n /**\n * The ID of the input element, generated if not provided.\n * This is used for CSS anchoring and to ensure unique IDs for accessibility.\n */\n @state() protected accessor inputId = ''\n\n /**\n * The reference to the slotted input element.\n * This should be an `HTMLInputElement` or behave like one.\n */\n protected inputRef?: HTMLInputElement | HTMLElement | null\n /**\n * The reference to the slotted suggestions element, which should be a `ui-listbox`.\n * This is used to manage the suggestions popover and filtering.\n */\n protected suggestionsRef?: UiListbox | null\n\n /**\n * Checks if the suggestions popover is currently open.\n */\n get opened(): boolean {\n return this.suggestionsRef?.matches(':popover-open') || false\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n this.observer = new MutationObserver(this.handleMutations.bind(this))\n this.observer.observe(this, { childList: true })\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.observer?.disconnect()\n this.observer = undefined\n this.clearInputListeners()\n this.clearSuggestionsListeners()\n }\n\n protected override firstUpdated(cp: PropertyValues): void {\n super.firstUpdated(cp)\n this.firstSetup()\n }\n\n /**\n * Performs initial setup after the first update, ensuring that slotted input\n * and suggestions elements are configured.\n */\n async firstSetup(): Promise<void> {\n await this.updateComplete\n const input = this.shadowRoot?.querySelector('slot[name=\"input\"]') as HTMLSlotElement | null\n const suggestions = this.shadowRoot?.querySelector('slot[name=\"suggestions\"]') as HTMLSlotElement | null\n if (input) {\n const elements = input.assignedElements()\n if (elements.length > 0 && elements[0] instanceof HTMLElement) {\n this.setupInput(elements[0])\n }\n }\n if (suggestions) {\n const elements = suggestions.assignedElements()\n if (elements.length > 0 && elements[0] instanceof HTMLElement) {\n // Assuming the slotted element is ui-listbox or compatible\n this.setupSuggestions(elements[0] as UiListbox)\n }\n }\n }\n\n /**\n * Handles mutations observed on the component's slotted children.\n * This is used to set up or tear down input and suggestions elements when they are added or removed.\n * @param mutations An array of MutationRecord objects.\n */\n protected handleMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n if (mutation.type === 'childList') {\n for (const node of mutation.removedNodes) {\n if (node instanceof HTMLElement) {\n if (node.slot === 'input' && this.inputRef === node) {\n this.clearInputListeners()\n } else if (node.slot === 'suggestions' && this.suggestionsRef === node) {\n this.clearSuggestionsListeners()\n }\n }\n }\n for (const node of mutation.addedNodes) {\n if (node instanceof HTMLElement) {\n if (node.slot === 'input') {\n this.setupInput(node)\n } else if (node.slot === 'suggestions') {\n this.setupSuggestions(node as UiListbox)\n }\n }\n }\n }\n }\n }\n\n /**\n * Clears event listeners from the current input reference and resets it.\n */\n protected clearInputListeners(): void {\n if (this.inputRef) {\n this.inputRef.removeEventListener('focus', this.handleInputFocus)\n this.inputRef.removeEventListener('input', this.handleInput)\n this.inputRef.removeEventListener('keydown', this.handleKeydown as EventListener)\n this.inputRef.removeEventListener('blur', this.handleInputBlur)\n }\n this.inputRef = null\n }\n\n /**\n * Clears event listeners from the current suggestions reference and resets it.\n */\n protected clearSuggestionsListeners(): void {\n if (this.suggestionsRef) {\n this.suggestionsRef.removeEventListener('select', this.handleSuggestionSelect as EventListener)\n // If ui-listbox uses a slot for its items, listen to its slotchange\n this.suggestionsRef.removeEventListener('itemschange', this.handleSuggestionsSlotChange)\n if (this.suggestionsRef.matches(':popover-open')) {\n this.suggestionsRef.hidePopover()\n }\n }\n this.suggestionsRef = null\n }\n\n /**\n * Sets up the slotted input element.\n * Assigns an ID if necessary, sets up CSS anchoring, and attaches event listeners.\n * @param input The HTMLElement to be used as the input.\n */\n protected setupInput(input: HTMLElement): void {\n this.clearInputListeners() // Clear any old listeners\n if (!input.id) {\n this.inputId = `autocomplete-input-${Math.random().toString(36).substring(2, 15)}`\n input.id = this.inputId\n } else {\n this.inputId = input.id\n }\n // Ensure CSS anchor positioning can work\n input.style.setProperty('anchor-name', `--${this.inputId}`)\n\n this.inputRef = input as HTMLInputElement // Assuming it behaves like an input\n this.inputRef.addEventListener('focus', this.handleInputFocus)\n this.inputRef.addEventListener('input', this.handleInput)\n this.inputRef.addEventListener('keydown', this.handleKeydown as EventListener)\n this.inputRef.addEventListener('blur', this.handleInputBlur)\n\n // Apply initial filtering if suggestions are already present\n if (this.suggestionsRef) {\n this.suggestionsRef.style.setProperty('position-anchor', `--${this.inputId}`)\n this.filterSuggestions((this.inputRef as HTMLInputElement).value || '')\n }\n }\n\n /**\n * Sets up the slotted suggestions element (assumed to be a `UiListbox`).\n * Configures popover behavior, CSS anchoring, and attaches event listeners.\n * @param suggestionsElement The `UiListbox` element to be used for suggestions.\n */\n protected setupSuggestions(suggestionsElement: UiListbox): void {\n this.clearSuggestionsListeners() // Clear any old listeners\n\n this.suggestionsRef = suggestionsElement\n this.suggestionsRef.popover = 'manual'\n this.suggestionsRef.tabIndex = -1 // Prevent direct focus\n\n if (this.inputId) {\n this.suggestionsRef.style.setProperty('position-anchor', `--${this.inputId}`)\n }\n\n this.suggestionsRef.addEventListener('select', this.handleSuggestionSelect as EventListener)\n // The `List` dispatches `itemschange` when the slot changes, so we can listen to it.\n this.suggestionsRef.addEventListener('itemschange', this.handleSuggestionsSlotChange)\n\n // Initial filter\n this.filterSuggestions(this.inputRef ? (this.inputRef as HTMLInputElement).value : '')\n }\n\n /**\n * Handles the focus event on the input element.\n * Opens the suggestions popover if there are items to display.\n */\n @bound\n protected handleInputFocus(): void {\n const items = this.suggestionsRef?.items\n if (!items || items.length === 0) {\n // If no suggestions, do not open\n return\n }\n const active = items.find((item) => !item.hidden)\n if (active) {\n this.openSuggestions()\n this.filterSuggestions(this.inputRef ? (this.inputRef as HTMLInputElement).value : '')\n }\n }\n\n /**\n * Handles the input event on the input element.\n * Filters suggestions based on the input query and opens/closes the popover accordingly.\n * @param event The input event.\n */\n @bound\n protected handleInput(event: Event): void {\n this.openSuggestions()\n const query = (event.target as HTMLInputElement).value\n this.filterSuggestions(query)\n }\n\n /**\n * Handles keydown events on the input element for navigating and selecting suggestions.\n * - ArrowDown/ArrowUp: Navigates the suggestion list.\n * - Enter: Selects the highlighted suggestion.\n * - Escape: Closes the suggestions popover.\n * @param event The keyboard event.\n */\n @bound\n protected handleKeydown(event: KeyboardEvent): void {\n if (!this.suggestionsRef) return\n\n const { key } = event\n const isSuggestionsOpen = this.suggestionsRef.matches(':popover-open')\n\n if (['ArrowDown', 'ArrowUp'].includes(key)) {\n event.preventDefault()\n if (!isSuggestionsOpen) {\n this.openSuggestions()\n // Give popover a moment to open before trying to highlight\n requestAnimationFrame(() => this.navigateList(key))\n } else {\n this.navigateList(key)\n }\n } else if (key === 'Enter') {\n if (isSuggestionsOpen && this.suggestionsRef.highlightListItem) {\n event.preventDefault()\n this.suggestionsRef.notifySelect(this.suggestionsRef.highlightListItem)\n }\n } else if (key === 'Escape') {\n if (isSuggestionsOpen) {\n event.preventDefault()\n this.closeSuggestions()\n }\n }\n }\n\n /**\n * Navigates the suggestion list based on the pressed key.\n * @param key The key that was pressed (ArrowDown or ArrowUp).\n */\n protected navigateList(key: string): void {\n if (!this.suggestionsRef) return\n switch (key) {\n case 'ArrowDown':\n this.suggestionsRef.highlightNext()\n break\n case 'ArrowUp':\n this.suggestionsRef.highlightPrevious()\n break\n // Don't handle Home or End keys here, as they break the usability of the input.\n }\n }\n\n /**\n * Handles the blur event on the input element.\n * Closes the suggestions popover if focus moves outside the autocomplete component.\n */\n @bound\n protected handleInputBlur(): void {\n // We use the manual popover mode, so we need to close suggestions\n // when the input loses focus, but only the current active element is not part of the suggestions.\n requestAnimationFrame(() => {\n if (\n !this.inputRef?.contains(document.activeElement) &&\n (!this.suggestionsRef || !this.suggestionsRef.contains(document.activeElement))\n ) {\n this.closeSuggestions()\n }\n })\n }\n\n /**\n * Handles the `select` event dispatched by the `ui-listbox` when a suggestion is chosen.\n * Dispatches an `autocomplete` event and closes the popover.\n * @param event The custom event from `ui-listbox`.\n */\n @bound\n protected handleSuggestionSelect(event: CustomEvent): void {\n event.stopPropagation()\n const selectedItem = event.detail.item as UiListItem\n this.dispatchEvent(\n new CustomEvent('autocomplete', {\n detail: { item: selectedItem },\n bubbles: false,\n composed: false,\n })\n )\n this.closeSuggestions()\n }\n\n /**\n * Handles the `itemschange` event dispatched by the `ui-listbox` when its slotted items change.\n * Re-filters the suggestions.\n */\n @bound\n protected handleSuggestionsSlotChange(): void {\n const value = this.inputRef ? (this.inputRef as HTMLInputElement).value : ''\n if (!value) {\n return\n }\n this.openSuggestions()\n this.filterSuggestions(value)\n }\n\n /**\n * Opens the suggestions popover if it's not already open and there are visible items.\n */\n protected openSuggestions(): void {\n if (this.suggestionsRef && !this.suggestionsRef.matches(':popover-open')) {\n this.suggestionsRef.showPopover()\n }\n }\n\n /**\n * Closes the suggestions popover if it's open and clears any highlighted item.\n */\n protected closeSuggestions(): void {\n if (this.suggestionsRef && this.suggestionsRef.matches(':popover-open')) {\n this.suggestionsRef.hidePopover()\n }\n }\n\n /**\n * Filters the suggestions based on the provided query.\n * Hides items that do not match and manages the highlighted item state.\n * @param query The search query string.\n */\n protected filterSuggestions(query: string): void {\n if (!this.suggestionsRef) {\n return\n }\n\n const lowerCaseQuery = query.toLowerCase().trim()\n let firstVisibleItem: UiListItem | null = null\n\n // The `items` getter in `List.ts` (parent of UiListbox) correctly gets assigned elements.\n const items = this.suggestionsRef.items as UiListItem[]\n\n for (const item of items) {\n let matches = false\n if (lowerCaseQuery === '') {\n matches = true\n } else if (item.dataset.index) {\n const indexFields = item.dataset.index.split(' ').filter(Boolean)\n for (const field of indexFields) {\n const valueToSearch = item.dataset[field] || ''\n if (valueToSearch.toLowerCase().includes(lowerCaseQuery)) {\n matches = true\n break\n }\n }\n } else {\n const valueToSearch = item.dataset.value || item.textContent || ''\n matches = valueToSearch.toLowerCase().includes(lowerCaseQuery)\n }\n item.hidden = !matches\n if (matches && !firstVisibleItem) {\n firstVisibleItem = item\n }\n }\n\n // If the currently highlighted item is now hidden, try to highlight the first visible one or clear.\n if (this.suggestionsRef.highlightListItem?.hidden) {\n // the highlightListItem clears the highlighted item if it is not passed an argument.\n this.suggestionsRef?.highlightItem(firstVisibleItem)\n }\n if (!firstVisibleItem) {\n // Close if no items are visible\n this.closeSuggestions()\n }\n }\n\n protected override render(): TemplateResult {\n return html`\n <slot name=\"input\"></slot>\n <slot name=\"suggestions\"></slot>\n `\n }\n}\n"]}
1
+ {"version":3,"file":"autocomplete.js","sourceRoot":"","sources":["../../../../../src/elements/autocomplete/internals/autocomplete.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, LitElement, PropertyValues, TemplateResult } from 'lit'\nimport { property, state } from 'lit/decorators.js'\nimport type UiListbox from '../../../md/listbox/internals/Listbox.js'\nimport type UiListItem from '../../../md/list/internals/ListItem.js'\nimport { bound } from '../../../decorators/bound.js'\n\n/**\n * An accessible and performant autocomplete component that enhances a text input with a list of suggestions.\n *\n * The `autocomplete-input` component provides a flexible way to add autocomplete functionality\n * to any text input. It works by coordinating a slotted input element with a slotted `ui-listbox`\n * containing suggestions.\n *\n * Key Features:\n *\n * - **Popover Management**: Automatically shows and hides the suggestions popover based on input focus\n * and user interaction.\n * - **Keyboard Navigation**: Allows users to navigate suggestions using ArrowUp, ArrowDown keys directly\n * from the input. Enter selects a suggestion, and Escape closes the popover.\n * The suggestion list itself never gains focus.\n * - **Dynamic Filtering**: Filters the list of suggestions as the user types into the input. Items not matching\n * the query are hidden (not removed).\n * - By default, filtering checks `item.dataset.value` and then `item.textContent`.\n * - If a `ui-list-item` has a `data-index` attribute (e.g., `data-index=\"name email\"`), filtering will search\n * within the specified `data-*` attributes (e.g., `data-name`, `data-email`).\n * - **Mutation Awareness**: Reacts to changes in slotted suggestions, re-filtering them if necessary.\n * - **Event-Driven**: Notifies the application via an `autocomplete` event when a suggestion is selected.\n * The component itself does not modify the input's value, giving the application author full control.\n *\n * ### Accessibility\n *\n * The `autocomplete-input` component is designed with accessibility at its core:\n * - **Keyboard Navigation**: Full keyboard support for navigating suggestions (ArrowUp, ArrowDown),\n * selecting (Enter), and closing (Escape), all while focus remains on the input field.\n * - **ARIA Attributes**: While the `autocomplete-input` manages ARIA attributes related to the popover's\n * state, the slotted `ui-listbox` is responsible for its internal ARIA roles and states\n * (e.g., `role=\"listbox\"`, `aria-activedescendant`).\n * - **Labeling Suggestions**: It is **crucial** for accessibility that the slotted `ui-listbox`\n * has an `aria-label` attribute. This provides a descriptive name for the list of suggestions,\n * which is announced by screen readers.\n *\n * The component uses CSS anchor positioning to place the suggestions popover relative to the input.\n * Ensure your `ui-listbox` is styled appropriately for popover display (e.g., `popover=\"manual\"`, `position-anchor`).\n * The component will manage `showPopover()` and `hidePopover()` calls.\n *\n * @slot input - The input element that will be used for autocomplete. This element should be an `HTMLInputElement`\n * or behave like one (have a `value` property and dispatch `input`, `focus`, `blur`, and `keydown`\n * events).\n * @slot suggestions - The `ui-listbox` element containing `ui-list-item` elements as suggestions.\n * @slot anchor - An optional element that points to element that will be used as the anchor for the popover.\n * This is useful if you want to position the suggestions relative to a different element than the input.\n * If not provided, the input element will be used as the anchor.\n * @slot Any additional content that should be rendered inside the component.\n *\n * @fires autocomplete - Dispatched when a suggestion is selected by the user (e.g., via click or Enter key).\n * The `event.detail` object contains:\n * - `item`: The selected `UiListItem` instance.\n *\n * @example\n * ```html\n * <autocomplete-input @autocomplete=\"${this.handleAutocompleteSelection}\">\n * <input slot=\"input\" type=\"text\" placeholder=\"Search fruits...\" />\n * <ui-listbox slot=\"suggestions\">\n * <ui-list-item data-value=\"apple\">Apple</ui-list-item>\n * <ui-list-item data-value=\"banana\">Banana</ui-list-item>\n * <ui-list-item data-value=\"cherry\">Cherry</ui-list-item>\n * </ui-listbox>\n * <!-- With aria-label for accessibility -->\n * <ui-listbox slot=\"suggestions\" aria-label=\"Fruit suggestions\">\n * <ui-list-item data-value=\"apple\">Apple</ui-list-item>\n * <ui-list-item data-value=\"banana\">Banana</ui-list-item>\n * <ui-list-item data-value=\"cherry\">Cherry</ui-list-item>\n * </ui-listbox>\n * </autocomplete-input>\n *\n * <script>\n * // In your component/script\n * handleAutocompleteSelection(event) {\n * const selectedItem = event.detail.item;\n * const inputElement = this.shadowRoot.querySelector('input[slot=\"input\"]');\n * if (inputElement) {\n * inputElement.value = selectedItem.dataset.value || selectedItem.textContent;\n * }\n * console.log('Selected:', selectedItem.dataset.value);\n * }\n * </script>\n * ```\n *\n * @example Dynamic filtering with `data-index`\n * ```html\n * <autocomplete-input @autocomplete=\"${this.handleUserSelection}\">\n * <input slot=\"input\" type=\"text\" placeholder=\"Search users...\" />\n * <ui-listbox slot=\"suggestions\" aria-label=\"User suggestions\">\n * <ui-list-item data-id=\"1\" data-name=\"Alice Wonderland\" data-email=\"alice@example.com\" data-index=\"name email\">\n * Alice Wonderland\n * <span slot=\"supporting-text\">alice@example.com</span>\n * </ui-list-item>\n * <ui-list-item data-id=\"2\" data-name=\"Bob The Builder\" data-email=\"bob@example.com\" data-index=\"name email\">\n * Bob The Builder\n * <span slot=\"supporting-text\">bob@example.com</span>\n * </ui-list-item>\n * </ui-listbox>\n * </autocomplete-input>\n *\n * <script>\n * // In your component/script\n * handleUserSelection(event) {\n * const selectedItem = event.detail.item;\n * const inputElement = this.shadowRoot.querySelector('input[slot=\"input\"]');\n * if (inputElement) {\n * // You might want to display the name, but store the ID\n * inputElement.value = selectedItem.dataset.name;\n * }\n * console.log('Selected user ID:', selectedItem.dataset.id);\n * }\n * </script>\n * ```\n */\nexport default class Autocomplete extends LitElement {\n protected override createRenderRoot(): HTMLElement | DocumentFragment {\n return this\n }\n\n /**\n * The MutationObserver instance used to watch for changes in slotted children.\n */\n protected observer?: MutationObserver\n\n /**\n * The ID of the input element, generated if not provided.\n * This is used for CSS anchoring and to ensure unique IDs for accessibility.\n */\n @state() protected accessor inputId = ''\n\n /**\n * The position area for the suggestions popover.\n * This can be 'bottom' or 'top', depending on available space.\n * Default is 'bottom'.\n *\n * Note, this is set dynamically based on available space\n * and the position of the input element. This only sets the initial value.\n *\n * @attribute\n * @type {'bottom' | 'top'}\n * @default 'bottom'\n */\n @property() accessor positionArea: 'bottom' | 'top' = 'bottom'\n\n /**\n * The reference to the slotted input element.\n * This should be an `HTMLInputElement` or behave like one.\n */\n protected inputRef?: HTMLInputElement | HTMLElement | null\n /**\n * The reference to the slotted suggestions element, which should be a `ui-listbox`.\n * This is used to manage the suggestions popover and filtering.\n */\n protected suggestionsRef?: UiListbox | null\n\n /**\n * Checks if the suggestions popover is currently open.\n */\n get opened(): boolean {\n return this.suggestionsRef?.matches(':popover-open') || false\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n this.observer = new MutationObserver(this.handleMutations.bind(this))\n this.observer.observe(this, { childList: true })\n if (!this.id) {\n this.id = `autocomplete-${Math.random().toString(36).substring(2, 15)}`\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n this.observer?.disconnect()\n this.observer = undefined\n this.clearInputListeners()\n this.clearSuggestionsListeners()\n }\n\n protected override firstUpdated(cp: PropertyValues): void {\n super.firstUpdated(cp)\n this.firstSetup()\n }\n\n /**\n * Performs initial setup after the first update, ensuring that slotted input\n * and suggestions elements are configured.\n */\n async firstSetup(): Promise<void> {\n await this.updateComplete\n const input = this.querySelector('[slot=\"input\"]') as HTMLElement | null\n const suggestions = this.querySelector('[slot=\"suggestions\"]') as HTMLElement | null\n if (input) {\n this.setupInput(input)\n }\n if (suggestions) {\n this.setupSuggestions(suggestions as UiListbox)\n }\n }\n\n /**\n * Handles mutations observed on the component's slotted children.\n * This is used to set up or tear down input and suggestions elements when they are added or removed.\n * @param mutations An array of MutationRecord objects.\n */\n protected handleMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n if (mutation.type === 'childList') {\n for (const node of mutation.removedNodes) {\n if (node instanceof HTMLElement) {\n if (node.slot === 'input' && this.inputRef === node) {\n this.clearInputListeners()\n } else if (node.slot === 'suggestions' && this.suggestionsRef === node) {\n this.clearSuggestionsListeners()\n }\n }\n }\n for (const node of mutation.addedNodes) {\n if (node instanceof HTMLElement) {\n if (node.slot === 'input') {\n this.setupInput(node)\n } else if (node.slot === 'suggestions') {\n this.setupSuggestions(node as UiListbox)\n }\n }\n }\n }\n }\n }\n\n /**\n * Clears event listeners from the current input reference and resets it.\n */\n protected clearInputListeners(): void {\n if (this.inputRef) {\n this.inputRef.removeEventListener('focus', this.handleInputFocus)\n this.inputRef.removeEventListener('input', this.handleInput)\n this.inputRef.removeEventListener('keydown', this.handleKeydown as EventListener)\n this.inputRef.removeEventListener('blur', this.handleInputBlur)\n }\n this.inputRef = null\n }\n\n /**\n * Clears event listeners from the current suggestions reference and resets it.\n */\n protected clearSuggestionsListeners(): void {\n if (this.suggestionsRef) {\n this.suggestionsRef.removeEventListener('select', this.handleSuggestionSelect as EventListener)\n // If ui-listbox uses a slot for its items, listen to its slotchange\n this.suggestionsRef.removeEventListener('itemschange', this.handleSuggestionsSlotChange)\n if (this.suggestionsRef.matches(':popover-open')) {\n this.suggestionsRef.hidePopover()\n }\n }\n this.suggestionsRef = null\n }\n\n /**\n * Sets up the slotted input element.\n * Assigns an ID if necessary, sets up CSS anchoring, and attaches event listeners.\n * @param input The HTMLElement to be used as the input.\n */\n protected setupInput(input: HTMLElement): void {\n this.clearInputListeners() // Clear any old listeners\n if (!input.id) {\n this.inputId = `autocomplete-input-${Math.random().toString(36).substring(2, 15)}`\n input.id = this.inputId\n } else {\n this.inputId = input.id\n }\n const anchorElement = this.querySelector('[slot=\"anchor\"]') as HTMLElement | null\n // Ensure CSS anchor positioning can work\n const anchor = anchorElement || input\n anchor.style.setProperty('anchor-name', `--${this.inputId}`)\n\n this.inputRef = input as HTMLInputElement // Assuming it behaves like an input\n this.inputRef.addEventListener('focus', this.handleInputFocus)\n this.inputRef.addEventListener('input', this.handleInput)\n this.inputRef.addEventListener('keydown', this.handleKeydown as EventListener)\n this.inputRef.addEventListener('blur', this.handleInputBlur)\n\n // Apply initial filtering if suggestions are already present\n if (this.suggestionsRef) {\n this.suggestionsRef.style.setProperty('position-anchor', `--${this.inputId}`)\n this.filterSuggestions((this.inputRef as HTMLInputElement).value || '')\n }\n }\n\n /**\n * Sets up the slotted suggestions element (assumed to be a `UiListbox`).\n * Configures popover behavior, CSS anchoring, and attaches event listeners.\n * @param suggestionsElement The `UiListbox` element to be used for suggestions.\n */\n protected setupSuggestions(suggestionsElement: UiListbox): void {\n this.clearSuggestionsListeners() // Clear any old listeners\n\n this.suggestionsRef = suggestionsElement\n this.suggestionsRef.popover = 'manual'\n this.suggestionsRef.tabIndex = -1 // Prevent direct focus\n\n if (this.inputId) {\n this.suggestionsRef.style.setProperty('position-anchor', `--${this.inputId}`)\n }\n\n this.suggestionsRef.addEventListener('select', this.handleSuggestionSelect as EventListener)\n // The `List` dispatches `itemschange` when the slot changes, so we can listen to it.\n this.suggestionsRef.addEventListener('itemschange', this.handleSuggestionsSlotChange)\n\n // Initial filter\n this.filterSuggestions(this.inputRef ? (this.inputRef as HTMLInputElement).value : '')\n }\n\n /**\n * Handles the focus event on the input element.\n * Opens the suggestions popover if there are items to display.\n */\n @bound\n protected handleInputFocus(): void {\n const items = this.suggestionsRef?.items\n if (!items || items.length === 0) {\n // If no suggestions, do not open\n return\n }\n const active = items.find((item) => !item.hidden)\n if (active) {\n this.openSuggestions()\n this.filterSuggestions(this.inputRef ? (this.inputRef as HTMLInputElement).value : '')\n }\n }\n\n /**\n * Handles the input event on the input element.\n * Filters suggestions based on the input query and opens/closes the popover accordingly.\n * @param event The input event.\n */\n @bound\n protected handleInput(event: Event): void {\n this.openSuggestions()\n const query = (event.target as HTMLInputElement).value\n this.filterSuggestions(query)\n }\n\n /**\n * Handles keydown events on the input element for navigating and selecting suggestions.\n * - ArrowDown/ArrowUp: Navigates the suggestion list.\n * - Enter: Selects the highlighted suggestion.\n * - Escape: Closes the suggestions popover.\n * @param event The keyboard event.\n */\n @bound\n protected handleKeydown(event: KeyboardEvent): void {\n if (!this.suggestionsRef) return\n\n const { key } = event\n const isSuggestionsOpen = this.suggestionsRef.matches(':popover-open')\n\n if (['ArrowDown', 'ArrowUp'].includes(key)) {\n event.preventDefault()\n if (!isSuggestionsOpen) {\n this.openSuggestions()\n // Give popover a moment to open before trying to highlight\n requestAnimationFrame(() => this.navigateList(key))\n } else {\n this.navigateList(key)\n }\n } else if (key === 'Enter') {\n if (isSuggestionsOpen && this.suggestionsRef.highlightListItem) {\n event.preventDefault()\n this.suggestionsRef.notifySelect(this.suggestionsRef.highlightListItem)\n }\n } else if (key === 'Escape') {\n if (isSuggestionsOpen) {\n event.preventDefault()\n this.closeSuggestions()\n }\n }\n }\n\n /**\n * Navigates the suggestion list based on the pressed key.\n * @param key The key that was pressed (ArrowDown or ArrowUp).\n */\n protected navigateList(key: string): void {\n if (!this.suggestionsRef) return\n switch (key) {\n case 'ArrowDown':\n this.suggestionsRef.highlightNext()\n break\n case 'ArrowUp':\n this.suggestionsRef.highlightPrevious()\n break\n // Don't handle Home or End keys here, as they break the usability of the input.\n }\n }\n\n /**\n * Handles the blur event on the input element.\n * Closes the suggestions popover if focus moves outside the autocomplete component.\n */\n @bound\n protected handleInputBlur(): void {\n // We use the manual popover mode, so we need to close suggestions\n // when the input loses focus, but only the current active element is not part of the suggestions.\n requestAnimationFrame(() => {\n if (\n !this.inputRef?.contains(document.activeElement) &&\n (!this.suggestionsRef || !this.suggestionsRef.contains(document.activeElement))\n ) {\n this.closeSuggestions()\n }\n })\n }\n\n /**\n * Handles the `select` event dispatched by the `ui-listbox` when a suggestion is chosen.\n * Dispatches an `autocomplete` event and closes the popover.\n * @param event The custom event from `ui-listbox`.\n */\n @bound\n protected handleSuggestionSelect(event: CustomEvent): void {\n event.stopPropagation()\n const selectedItem = event.detail.item as UiListItem\n this.dispatchEvent(\n new CustomEvent('autocomplete', {\n detail: { item: selectedItem },\n bubbles: false,\n composed: false,\n })\n )\n this.closeSuggestions()\n }\n\n /**\n * Handles the `itemschange` event dispatched by the `ui-listbox` when its slotted items change.\n * Re-filters the suggestions.\n */\n @bound\n protected handleSuggestionsSlotChange(): void {\n const value = this.inputRef ? (this.inputRef as HTMLInputElement).value : ''\n if (!value) {\n return\n }\n this.openSuggestions()\n this.filterSuggestions(value)\n }\n\n /**\n * Opens the suggestions popover if it's not already open and there are visible items.\n */\n protected openSuggestions(): void {\n const popover = this.suggestionsRef\n if (!popover || popover.matches(':popover-open')) {\n return\n }\n // we need to open the popover first to make any measurements\n popover.showPopover()\n const anchor = (this.querySelector('[slot=\"anchor\"]') as HTMLElement | null) || this.inputRef\n if (!anchor) {\n return\n }\n this.positionArea = this.decidePositionArea(popover, anchor)\n }\n\n /**\n * Decides the position area for the popover based on available space.\n * It checks if there is enough space below or above the anchor element and decides accordingly.\n *\n * We need to do this because we set the popover height to `100%`, `-webkit-fill-available`, or `-moz-available`\n * and it makes the popover to always open at the bottom, even if there is no space. The `position-try-fallbacks`\n * will not work in this case, because from its perspective there is always enough space, even if that will cause\n * the popover to have a height of just a few pixels.\n *\n * @param popover The popover element to position.\n * @param anchor The anchor element relative to which the popover will be positioned.\n * @returns 'top' or 'bottom' based on available space.\n */\n protected decidePositionArea(popover: HTMLElement, anchor: HTMLElement): 'top' | 'bottom' {\n let newArea: 'top' | 'bottom' = 'bottom'\n // Get bounding rectangles\n const anchorRect = anchor.getBoundingClientRect()\n const viewportHeight = window.innerHeight\n\n const spaceBelow = viewportHeight - anchorRect.bottom\n const spaceAbove = anchorRect.top\n\n // Estimate a typical/minimum height the popover might need to be useful\n const popoverThresholdHeight = popover.scrollHeight || 150\n if (spaceBelow < popoverThresholdHeight && spaceAbove > spaceBelow && spaceAbove > popoverThresholdHeight) {\n // Not enough space below, but more (and sufficient) space above\n newArea = 'top'\n } else if (spaceBelow < popoverThresholdHeight && spaceAbove > spaceBelow) {\n // Not enough space below, and space above is more than space below (even if not \"sufficient\")\n newArea = 'top'\n } else {\n newArea = 'bottom' // Default to bottom if enough space or if top is worse\n }\n return newArea\n }\n\n /**\n * Closes the suggestions popover if it's open and clears any highlighted item.\n */\n protected closeSuggestions(): void {\n if (this.suggestionsRef && this.suggestionsRef.matches(':popover-open')) {\n this.suggestionsRef.hidePopover()\n }\n }\n\n /**\n * Filters the suggestions based on the provided query.\n * Hides items that do not match and manages the highlighted item state.\n * @param query The search query string.\n */\n protected filterSuggestions(query: string): void {\n if (!this.suggestionsRef) {\n return\n }\n\n const lowerCaseQuery = query.toLowerCase().trim()\n let firstVisibleItem: UiListItem | null = null\n\n // The `items` getter in `List.ts` (parent of UiListbox) correctly gets assigned elements.\n const items = this.suggestionsRef.items as UiListItem[]\n\n for (const item of items) {\n let matches = false\n if (lowerCaseQuery === '') {\n matches = true\n } else if (item.dataset.index) {\n const indexFields = item.dataset.index.split(' ').filter(Boolean)\n for (const field of indexFields) {\n const valueToSearch = item.dataset[field] || ''\n if (valueToSearch.toLowerCase().includes(lowerCaseQuery)) {\n matches = true\n break\n }\n }\n } else {\n const valueToSearch = item.dataset.value || item.textContent || ''\n matches = valueToSearch.toLowerCase().includes(lowerCaseQuery)\n }\n item.hidden = !matches\n if (matches && !firstVisibleItem) {\n firstVisibleItem = item\n }\n }\n\n // If the currently highlighted item is now hidden, try to highlight the first visible one or clear.\n if (this.suggestionsRef.highlightListItem?.hidden) {\n // the highlightListItem clears the highlighted item if it is not passed an argument.\n this.suggestionsRef?.highlightItem(firstVisibleItem)\n }\n if (!firstVisibleItem) {\n // Close if no items are visible\n this.closeSuggestions()\n }\n }\n\n protected override render(): TemplateResult {\n const { id, positionArea } = this\n return html`\n <style>\n #${id} {\n display: inline-block;\n\n [popover] {\n border: none;\n margin: 0;\n position-area: ${positionArea};\n position-try-fallbacks:\n flip-block,\n flip-inline,\n flip-block flip-inline;\n width: anchor-size(width);\n\n box-shadow: var(--md-sys-elevation-1);\n border-radius: var(--md-sys-shape-corner-medium);\n\n overflow: auto;\n /* We try 100% and then vendor options which are more accurate */\n height: 100%;\n height: -webkit-fill-available;\n height: -moz-available;\n max-height: max-content;\n }\n\n [popover]:not(:popover-open) {\n display: none;\n }\n }\n </style>\n `\n }\n}\n"]}
@@ -14,6 +14,46 @@
14
14
  <link href="../../../src/styles/m3/tokens.css" rel="stylesheet" type="text/css" />
15
15
  <link href="../../../src/styles/m3/theme.css" rel="stylesheet" type="text/css" />
16
16
  <link href="../../page.css" rel="stylesheet" type="text/css" />
17
+ <style>
18
+ .share-input {
19
+ .input-container {
20
+ min-height: 3em;
21
+ border: none;
22
+ padding: 4px 12px;
23
+ display: flex;
24
+ flex-wrap: wrap;
25
+ box-sizing: border-box;
26
+ gap: 4px;
27
+ border-radius: 4px;
28
+ background-color: var(--md-sys-color-surface-container-lowest);
29
+
30
+ outline-color: var(--md-sys-color-primary);
31
+ outline-style: solid;
32
+ outline-width: 1px;
33
+ outline-offset: -1px;
34
+ transition:
35
+ outline-width 0.14s ease-in-out,
36
+ outline-offset 0.14s ease-in-out;
37
+
38
+ &:focus-within {
39
+ outline-width: 2px;
40
+ outline-offset: -2px;
41
+ }
42
+
43
+ .native-input {
44
+ flex: 1;
45
+ outline: none;
46
+ border: none;
47
+ background: transparent;
48
+ color: var(--md-sys-color-on-surface);
49
+ font-size: 1rem;
50
+ font-family: var(--md-sys-typescale-body-large-font-family);
51
+ line-height: var(--md-sys-typescale-body-large-line-height);
52
+ padding: 0;
53
+ }
54
+ }
55
+ }
56
+ </style>
17
57
  </head>
18
58
 
19
59
  <body class="demo">
@@ -15,7 +15,9 @@ class ComponentDemoPage extends DemoPage {
15
15
 
16
16
  @reactive() accessor fixedValue = ''
17
17
  @reactive() accessor dynamicValue = ''
18
+ @reactive() accessor dynamicCustomValue = ''
18
19
  @reactive() accessor users: IUser[] = []
20
+ @reactive() accessor customUsers: IUser[] = []
19
21
 
20
22
  userMock = new User()
21
23
 
@@ -37,22 +39,33 @@ class ComponentDemoPage extends DemoPage {
37
39
  handleDynamicInput(event: InputEvent): void {
38
40
  const target = event.target as HTMLInputElement
39
41
  this.dynamicValue = target.value
40
- this.generateMockUsers(target.value)
42
+ this.generateMockUsers(target.value, 'users')
41
43
  }
42
44
 
43
- generateMockUsers(startsWith: string): void {
45
+ generateMockUsers(startsWith: string, prop: 'users' | 'customUsers'): void {
44
46
  const timeout = this.userMock.types.number({ min: 100, max: 1000 })
45
47
  const results = this.userMock.types.number({ min: 0, max: 30 })
46
48
  setTimeout(() => {
47
- this.users = Array.from({ length: results }, () => {
49
+ this[prop] = Array.from({ length: results }, () => {
48
50
  const item = this.userMock.user()
49
51
  item.name = item.name.startsWith(startsWith) ? item.name : `${startsWith}${item.name}`
50
52
  return item
51
53
  })
52
- console.log('Generated users:', this.users)
54
+ console.log('Generated users:', this[prop])
53
55
  }, timeout)
54
56
  }
55
57
 
58
+ handleDynamicCustomInput(event: InputEvent): void {
59
+ const target = event.target as HTMLInputElement
60
+ this.dynamicCustomValue = target.value
61
+ this.generateMockUsers(target.value, 'customUsers')
62
+ }
63
+
64
+ handleDynamicCustomAutocomplete(event: CustomEvent<{ item: HTMLElement }>): void {
65
+ const item = event.detail.item
66
+ this.dynamicCustomValue = item.dataset.name || ''
67
+ }
68
+
56
69
  override contentTemplate(): TemplateResult {
57
70
  return html`
58
71
  <a href="../">Back</a>
@@ -116,6 +129,41 @@ class ComponentDemoPage extends DemoPage {
116
129
  </autocomplete-input>
117
130
  </div>
118
131
  </section>
132
+
133
+ <section class="demo-section">
134
+ <h2 class="title-large">Custom Input</h2>
135
+
136
+ <div class="demo-row">
137
+ <autocomplete-input @autocomplete="${this.handleDynamicCustomAutocomplete}">
138
+ <div class="share-input" slot="anchor">
139
+ <div class="input-container">
140
+ <input
141
+ type="text"
142
+ slot="input"
143
+ autocomplete="off"
144
+ placeholder="Enter name or email"
145
+ name="dynamic-custom"
146
+ class="native-input"
147
+ .value="${live(this.dynamicCustomValue)}"
148
+ @input="${this.handleDynamicCustomInput}"
149
+ />
150
+ </div>
151
+ </div>
152
+ <ui-listbox slot="suggestions"
153
+ >${this.customUsers.map(
154
+ (user) =>
155
+ html`<ui-list-item
156
+ data-name="${user.name}"
157
+ data-email="${user.email[0].email}"
158
+ data-index="name email"
159
+ role="option"
160
+ >${user.name}<span slot="supporting-text">${user.email[0].email}</span></ui-list-item
161
+ >`
162
+ )}</ui-listbox
163
+ >
164
+ </autocomplete-input>
165
+ </div>
166
+ </section>
119
167
  `
120
168
  }
121
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.js",
@@ -1,5 +1,5 @@
1
1
  import { html, LitElement, PropertyValues, TemplateResult } from 'lit'
2
- import { state } from 'lit/decorators.js'
2
+ import { property, state } from 'lit/decorators.js'
3
3
  import type UiListbox from '../../../md/listbox/internals/Listbox.js'
4
4
  import type UiListItem from '../../../md/list/internals/ListItem.js'
5
5
  import { bound } from '../../../decorators/bound.js'
@@ -24,10 +24,20 @@ import { bound } from '../../../decorators/bound.js'
24
24
  * - If a `ui-list-item` has a `data-index` attribute (e.g., `data-index="name email"`), filtering will search
25
25
  * within the specified `data-*` attributes (e.g., `data-name`, `data-email`).
26
26
  * - **Mutation Awareness**: Reacts to changes in slotted suggestions, re-filtering them if necessary.
27
- * - **Event-Driven**: Notifies the parent application via an `autocomplete` event when a suggestion is selected,
28
- * without directly modifying the input's value. This allows the application author to control the input update logic.
29
- * - **Accessibility**: Designed with accessibility in mind, ensuring keyboard navigability and proper ARIA attribute
30
- * management (handled by `ui-listbox`).
27
+ * - **Event-Driven**: Notifies the application via an `autocomplete` event when a suggestion is selected.
28
+ * The component itself does not modify the input's value, giving the application author full control.
29
+ *
30
+ * ### Accessibility
31
+ *
32
+ * The `autocomplete-input` component is designed with accessibility at its core:
33
+ * - **Keyboard Navigation**: Full keyboard support for navigating suggestions (ArrowUp, ArrowDown),
34
+ * selecting (Enter), and closing (Escape), all while focus remains on the input field.
35
+ * - **ARIA Attributes**: While the `autocomplete-input` manages ARIA attributes related to the popover's
36
+ * state, the slotted `ui-listbox` is responsible for its internal ARIA roles and states
37
+ * (e.g., `role="listbox"`, `aria-activedescendant`).
38
+ * - **Labeling Suggestions**: It is **crucial** for accessibility that the slotted `ui-listbox`
39
+ * has an `aria-label` attribute. This provides a descriptive name for the list of suggestions,
40
+ * which is announced by screen readers.
31
41
  *
32
42
  * The component uses CSS anchor positioning to place the suggestions popover relative to the input.
33
43
  * Ensure your `ui-listbox` is styled appropriately for popover display (e.g., `popover="manual"`, `position-anchor`).
@@ -37,6 +47,10 @@ import { bound } from '../../../decorators/bound.js'
37
47
  * or behave like one (have a `value` property and dispatch `input`, `focus`, `blur`, and `keydown`
38
48
  * events).
39
49
  * @slot suggestions - The `ui-listbox` element containing `ui-list-item` elements as suggestions.
50
+ * @slot anchor - An optional element that points to element that will be used as the anchor for the popover.
51
+ * This is useful if you want to position the suggestions relative to a different element than the input.
52
+ * If not provided, the input element will be used as the anchor.
53
+ * @slot Any additional content that should be rendered inside the component.
40
54
  *
41
55
  * @fires autocomplete - Dispatched when a suggestion is selected by the user (e.g., via click or Enter key).
42
56
  * The `event.detail` object contains:
@@ -51,6 +65,12 @@ import { bound } from '../../../decorators/bound.js'
51
65
  * <ui-list-item data-value="banana">Banana</ui-list-item>
52
66
  * <ui-list-item data-value="cherry">Cherry</ui-list-item>
53
67
  * </ui-listbox>
68
+ * <!-- With aria-label for accessibility -->
69
+ * <ui-listbox slot="suggestions" aria-label="Fruit suggestions">
70
+ * <ui-list-item data-value="apple">Apple</ui-list-item>
71
+ * <ui-list-item data-value="banana">Banana</ui-list-item>
72
+ * <ui-list-item data-value="cherry">Cherry</ui-list-item>
73
+ * </ui-listbox>
54
74
  * </autocomplete-input>
55
75
  *
56
76
  * <script>
@@ -70,7 +90,7 @@ import { bound } from '../../../decorators/bound.js'
70
90
  * ```html
71
91
  * <autocomplete-input @autocomplete="${this.handleUserSelection}">
72
92
  * <input slot="input" type="text" placeholder="Search users..." />
73
- * <ui-listbox slot="suggestions">
93
+ * <ui-listbox slot="suggestions" aria-label="User suggestions">
74
94
  * <ui-list-item data-id="1" data-name="Alice Wonderland" data-email="alice@example.com" data-index="name email">
75
95
  * Alice Wonderland
76
96
  * <span slot="supporting-text">alice@example.com</span>
@@ -97,6 +117,10 @@ import { bound } from '../../../decorators/bound.js'
97
117
  * ```
98
118
  */
99
119
  export default class Autocomplete extends LitElement {
120
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
121
+ return this
122
+ }
123
+
100
124
  /**
101
125
  * The MutationObserver instance used to watch for changes in slotted children.
102
126
  */
@@ -108,6 +132,20 @@ export default class Autocomplete extends LitElement {
108
132
  */
109
133
  @state() protected accessor inputId = ''
110
134
 
135
+ /**
136
+ * The position area for the suggestions popover.
137
+ * This can be 'bottom' or 'top', depending on available space.
138
+ * Default is 'bottom'.
139
+ *
140
+ * Note, this is set dynamically based on available space
141
+ * and the position of the input element. This only sets the initial value.
142
+ *
143
+ * @attribute
144
+ * @type {'bottom' | 'top'}
145
+ * @default 'bottom'
146
+ */
147
+ @property() accessor positionArea: 'bottom' | 'top' = 'bottom'
148
+
111
149
  /**
112
150
  * The reference to the slotted input element.
113
151
  * This should be an `HTMLInputElement` or behave like one.
@@ -130,6 +168,9 @@ export default class Autocomplete extends LitElement {
130
168
  super.connectedCallback()
131
169
  this.observer = new MutationObserver(this.handleMutations.bind(this))
132
170
  this.observer.observe(this, { childList: true })
171
+ if (!this.id) {
172
+ this.id = `autocomplete-${Math.random().toString(36).substring(2, 15)}`
173
+ }
133
174
  }
134
175
 
135
176
  override disconnectedCallback(): void {
@@ -151,20 +192,13 @@ export default class Autocomplete extends LitElement {
151
192
  */
152
193
  async firstSetup(): Promise<void> {
153
194
  await this.updateComplete
154
- const input = this.shadowRoot?.querySelector('slot[name="input"]') as HTMLSlotElement | null
155
- const suggestions = this.shadowRoot?.querySelector('slot[name="suggestions"]') as HTMLSlotElement | null
195
+ const input = this.querySelector('[slot="input"]') as HTMLElement | null
196
+ const suggestions = this.querySelector('[slot="suggestions"]') as HTMLElement | null
156
197
  if (input) {
157
- const elements = input.assignedElements()
158
- if (elements.length > 0 && elements[0] instanceof HTMLElement) {
159
- this.setupInput(elements[0])
160
- }
198
+ this.setupInput(input)
161
199
  }
162
200
  if (suggestions) {
163
- const elements = suggestions.assignedElements()
164
- if (elements.length > 0 && elements[0] instanceof HTMLElement) {
165
- // Assuming the slotted element is ui-listbox or compatible
166
- this.setupSuggestions(elements[0] as UiListbox)
167
- }
201
+ this.setupSuggestions(suggestions as UiListbox)
168
202
  }
169
203
  }
170
204
 
@@ -239,8 +273,10 @@ export default class Autocomplete extends LitElement {
239
273
  } else {
240
274
  this.inputId = input.id
241
275
  }
276
+ const anchorElement = this.querySelector('[slot="anchor"]') as HTMLElement | null
242
277
  // Ensure CSS anchor positioning can work
243
- input.style.setProperty('anchor-name', `--${this.inputId}`)
278
+ const anchor = anchorElement || input
279
+ anchor.style.setProperty('anchor-name', `--${this.inputId}`)
244
280
 
245
281
  this.inputRef = input as HTMLInputElement // Assuming it behaves like an input
246
282
  this.inputRef.addEventListener('focus', this.handleInputFocus)
@@ -417,9 +453,53 @@ export default class Autocomplete extends LitElement {
417
453
  * Opens the suggestions popover if it's not already open and there are visible items.
418
454
  */
419
455
  protected openSuggestions(): void {
420
- if (this.suggestionsRef && !this.suggestionsRef.matches(':popover-open')) {
421
- this.suggestionsRef.showPopover()
456
+ const popover = this.suggestionsRef
457
+ if (!popover || popover.matches(':popover-open')) {
458
+ return
459
+ }
460
+ // we need to open the popover first to make any measurements
461
+ popover.showPopover()
462
+ const anchor = (this.querySelector('[slot="anchor"]') as HTMLElement | null) || this.inputRef
463
+ if (!anchor) {
464
+ return
465
+ }
466
+ this.positionArea = this.decidePositionArea(popover, anchor)
467
+ }
468
+
469
+ /**
470
+ * Decides the position area for the popover based on available space.
471
+ * It checks if there is enough space below or above the anchor element and decides accordingly.
472
+ *
473
+ * We need to do this because we set the popover height to `100%`, `-webkit-fill-available`, or `-moz-available`
474
+ * and it makes the popover to always open at the bottom, even if there is no space. The `position-try-fallbacks`
475
+ * will not work in this case, because from its perspective there is always enough space, even if that will cause
476
+ * the popover to have a height of just a few pixels.
477
+ *
478
+ * @param popover The popover element to position.
479
+ * @param anchor The anchor element relative to which the popover will be positioned.
480
+ * @returns 'top' or 'bottom' based on available space.
481
+ */
482
+ protected decidePositionArea(popover: HTMLElement, anchor: HTMLElement): 'top' | 'bottom' {
483
+ let newArea: 'top' | 'bottom' = 'bottom'
484
+ // Get bounding rectangles
485
+ const anchorRect = anchor.getBoundingClientRect()
486
+ const viewportHeight = window.innerHeight
487
+
488
+ const spaceBelow = viewportHeight - anchorRect.bottom
489
+ const spaceAbove = anchorRect.top
490
+
491
+ // Estimate a typical/minimum height the popover might need to be useful
492
+ const popoverThresholdHeight = popover.scrollHeight || 150
493
+ if (spaceBelow < popoverThresholdHeight && spaceAbove > spaceBelow && spaceAbove > popoverThresholdHeight) {
494
+ // Not enough space below, but more (and sufficient) space above
495
+ newArea = 'top'
496
+ } else if (spaceBelow < popoverThresholdHeight && spaceAbove > spaceBelow) {
497
+ // Not enough space below, and space above is more than space below (even if not "sufficient")
498
+ newArea = 'top'
499
+ } else {
500
+ newArea = 'bottom' // Default to bottom if enough space or if top is worse
422
501
  }
502
+ return newArea
423
503
  }
424
504
 
425
505
  /**
@@ -482,9 +562,38 @@ export default class Autocomplete extends LitElement {
482
562
  }
483
563
 
484
564
  protected override render(): TemplateResult {
565
+ const { id, positionArea } = this
485
566
  return html`
486
- <slot name="input"></slot>
487
- <slot name="suggestions"></slot>
567
+ <style>
568
+ #${id} {
569
+ display: inline-block;
570
+
571
+ [popover] {
572
+ border: none;
573
+ margin: 0;
574
+ position-area: ${positionArea};
575
+ position-try-fallbacks:
576
+ flip-block,
577
+ flip-inline,
578
+ flip-block flip-inline;
579
+ width: anchor-size(width);
580
+
581
+ box-shadow: var(--md-sys-elevation-1);
582
+ border-radius: var(--md-sys-shape-corner-medium);
583
+
584
+ overflow: auto;
585
+ /* We try 100% and then vendor options which are more accurate */
586
+ height: 100%;
587
+ height: -webkit-fill-available;
588
+ height: -moz-available;
589
+ max-height: max-content;
590
+ }
591
+
592
+ [popover]:not(:popover-open) {
593
+ display: none;
594
+ }
595
+ }
596
+ </style>
488
597
  `
489
598
  }
490
599
  }
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-expressions */
2
- import { fixture, expect, html, oneEvent, nextFrame, aTimeout } from '@open-wc/testing'
2
+ import { fixture, expect, html, oneEvent, nextFrame, aTimeout, assert } from '@open-wc/testing'
3
3
  import sinon from 'sinon'
4
4
 
5
5
  import { AutocompleteInput } from '../../../src/elements/autocomplete/autocomplete-input.js'
@@ -16,7 +16,7 @@ describe('AutocompleteInput', () => {
16
16
  return fixture(html`
17
17
  <autocomplete-input>
18
18
  <input id="test-input" slot="input" type="text" placeholder="Search..." />
19
- <ui-listbox slot="suggestions">
19
+ <ui-listbox slot="suggestions" aria-label="Suggestions">
20
20
  <ui-list-item data-value="apple">Apple</ui-list-item>
21
21
  <ui-list-item data-value="banana">Banana</ui-list-item>
22
22
  <ui-list-item data-value="cherry" data-index="value customField" data-custom-field="Sweet Cherry"
@@ -31,7 +31,7 @@ describe('AutocompleteInput', () => {
31
31
  return fixture(html`
32
32
  <autocomplete-input>
33
33
  <input slot="input" type="text" placeholder="Search..." />
34
- <ui-listbox slot="suggestions"></ui-listbox>
34
+ <ui-listbox slot="suggestions" aria-label="Suggestions"></ui-listbox>
35
35
  </autocomplete-input>
36
36
  `)
37
37
  }
@@ -45,13 +45,11 @@ describe('AutocompleteInput', () => {
45
45
  }
46
46
 
47
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
48
+ return el.querySelector('[slot="input"]') as HTMLInputElement | null
50
49
  }
51
50
 
52
51
  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
52
+ return el.querySelector('[slot="suggestions"]') as MdListbox | null
55
53
  }
56
54
 
57
55
  function getSuggestionItems(suggestionsEl: MdListbox): MdListItem[] {
@@ -445,4 +443,201 @@ describe('AutocompleteInput', () => {
445
443
  expect(suggestions.matches(':popover-open')).to.be.false
446
444
  })
447
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
+ })
448
643
  })