@ni/nimble-components 28.0.1 → 28.0.2

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.
@@ -27,6 +27,15 @@ export declare class ListOption extends FoundationListboxOption {
27
27
  * by the filtering process.
28
28
  */
29
29
  visuallyHidden: boolean;
30
+ /**
31
+ * @internal
32
+ * This attribute is used to control the visual selected state of an option. This
33
+ * is handled independently of the public 'selected' attribute, as 'selected' is
34
+ * representative of the current value of the container control. However, while
35
+ * a dropdown is open users can navigate through the options (requiring visual
36
+ * updates) without changing the value of the container control.
37
+ */
38
+ activeOption: boolean;
30
39
  /** @internal */
31
40
  hasOverflow: boolean;
32
41
  /** @internal */
@@ -26,6 +26,15 @@ export class ListOption extends FoundationListboxOption {
26
26
  * by the filtering process.
27
27
  */
28
28
  this.visuallyHidden = false;
29
+ /**
30
+ * @internal
31
+ * This attribute is used to control the visual selected state of an option. This
32
+ * is handled independently of the public 'selected' attribute, as 'selected' is
33
+ * representative of the current value of the container control. However, while
34
+ * a dropdown is open users can navigate through the options (requiring visual
35
+ * updates) without changing the value of the container control.
36
+ */
37
+ this.activeOption = false;
29
38
  /** @internal */
30
39
  this.hasOverflow = false;
31
40
  }
@@ -55,6 +64,9 @@ __decorate([
55
64
  __decorate([
56
65
  attr({ attribute: 'visually-hidden', mode: 'boolean' })
57
66
  ], ListOption.prototype, "visuallyHidden", void 0);
67
+ __decorate([
68
+ attr({ attribute: 'active-option', mode: 'boolean' })
69
+ ], ListOption.prototype, "activeOption", void 0);
58
70
  __decorate([
59
71
  observable
60
72
  ], ListOption.prototype, "hasOverflow", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/list-option/index.ts"],"names":[],"mappings":";AAAA,OAAO,EACH,YAAY,EACZ,aAAa,IAAI,uBAAuB,EAC3C,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAStC;;GAEG;AACH,MAAM,OAAO,UAAW,SAAQ,uBAAuB;IAAvD;;QAII;;;;;;;WAOG;QAEa,WAAM,GAAG,KAAK,CAAC;QAE/B;;;;;;WAMG;QAEI,mBAAc,GAAG,KAAK,CAAC;QAE9B,gBAAgB;QAET,gBAAW,GAAG,KAAK,CAAC;IA0B/B,CAAC;IAxBG,gBAAgB;IAChB,IAAW,kBAAkB;QACzB,OAAO,IAAI,CAAC,WAAW;aAClB,aAAa,EAAE;aACf,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;aACrC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IAEe,iBAAiB;QAC7B,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE;YAC5C,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;SAC3C;IACL,CAAC;IAEO,iBAAiB,CACrB,MAA0B;QAE1B,IAAI,CAAC,MAAM,EAAE;YACT,OAAO,KAAK,CAAC;SAChB;QAED,OAAO,OAAQ,MAA0B,CAAC,cAAc,KAAK,UAAU,CAAC;IAC5E,CAAC;CACJ;AAxCmB;IADf,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;0CACK;AAUxB;IADN,IAAI,CAAC,EAAE,SAAS,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;kDAC1B;AAIvB;IADN,UAAU;+CACgB;AA4B/B,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,CAAC;IACxC,QAAQ,EAAE,aAAa;IACvB,SAAS,EAAE,uBAAuB;IAClC,QAAQ;IACR,MAAM;CACT,CAAC,CAAC;AAEH,YAAY,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAG,oBAAoB,CAAC","sourcesContent":["import {\n DesignSystem,\n ListboxOption as FoundationListboxOption\n} from '@microsoft/fast-foundation';\nimport { observable, attr } from '@microsoft/fast-element';\nimport { styles } from './styles';\nimport { template } from './template';\nimport type { ListOptionOwner } from '../patterns/dropdown/types';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'nimble-list-option': ListOption;\n }\n}\n\n/**\n * A nimble-styled HTML listbox option\n */\nexport class ListOption extends FoundationListboxOption {\n /** @internal */\n public contentSlot!: HTMLSlotElement;\n\n /**\n * The hidden state of the element.\n *\n * @public\n * @defaultValue - false\n * @remarks\n * HTML Attribute: hidden\n */\n @attr({ mode: 'boolean' })\n public override hidden = false;\n\n /**\n * @internal\n * This attribute is required to allow use-cases that offer dynamic filtering\n * (like the Select) to visually hide options that are filtered out, but still\n * allow users to use the native 'hidden' attribute without it being affected\n * by the filtering process.\n */\n @attr({ attribute: 'visually-hidden', mode: 'boolean' })\n public visuallyHidden = false;\n\n /** @internal */\n @observable\n public hasOverflow = false;\n\n /** @internal */\n public get elementTextContent(): string {\n return this.contentSlot\n .assignedNodes()\n .map(node => node.textContent?.trim())\n .join(' ');\n }\n\n public override connectedCallback(): void {\n super.connectedCallback();\n if (this.isListOptionOwner(this.parentElement)) {\n this.parentElement.registerOption(this);\n }\n }\n\n private isListOptionOwner(\n parent: HTMLElement | null\n ): parent is ListOptionOwner {\n if (!parent) {\n return false;\n }\n\n return typeof (parent as ListOptionOwner).registerOption === 'function';\n }\n}\n\nconst nimbleListOption = ListOption.compose({\n baseName: 'list-option',\n baseClass: FoundationListboxOption,\n template,\n styles\n});\n\nDesignSystem.getOrCreate().withPrefix('nimble').register(nimbleListOption());\nexport const listOptionTag = 'nimble-list-option';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/list-option/index.ts"],"names":[],"mappings":";AAAA,OAAO,EACH,YAAY,EACZ,aAAa,IAAI,uBAAuB,EAC3C,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAStC;;GAEG;AACH,MAAM,OAAO,UAAW,SAAQ,uBAAuB;IAAvD;;QAII;;;;;;;WAOG;QAEa,WAAM,GAAG,KAAK,CAAC;QAE/B;;;;;;WAMG;QAEI,mBAAc,GAAG,KAAK,CAAC;QAE9B;;;;;;;WAOG;QAEI,iBAAY,GAAG,KAAK,CAAC;QAE5B,gBAAgB;QAET,gBAAW,GAAG,KAAK,CAAC;IA0B/B,CAAC;IAxBG,gBAAgB;IAChB,IAAW,kBAAkB;QACzB,OAAO,IAAI,CAAC,WAAW;aAClB,aAAa,EAAE;aACf,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;aACrC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IAEe,iBAAiB;QAC7B,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE;YAC5C,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;SAC3C;IACL,CAAC;IAEO,iBAAiB,CACrB,MAA0B;QAE1B,IAAI,CAAC,MAAM,EAAE;YACT,OAAO,KAAK,CAAC;SAChB;QAED,OAAO,OAAQ,MAA0B,CAAC,cAAc,KAAK,UAAU,CAAC;IAC5E,CAAC;CACJ;AAnDmB;IADf,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;0CACK;AAUxB;IADN,IAAI,CAAC,EAAE,SAAS,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;kDAC1B;AAWvB;IADN,IAAI,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;gDAC1B;AAIrB;IADN,UAAU;+CACgB;AA4B/B,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,CAAC;IACxC,QAAQ,EAAE,aAAa;IACvB,SAAS,EAAE,uBAAuB;IAClC,QAAQ;IACR,MAAM;CACT,CAAC,CAAC;AAEH,YAAY,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAG,oBAAoB,CAAC","sourcesContent":["import {\n DesignSystem,\n ListboxOption as FoundationListboxOption\n} from '@microsoft/fast-foundation';\nimport { observable, attr } from '@microsoft/fast-element';\nimport { styles } from './styles';\nimport { template } from './template';\nimport type { ListOptionOwner } from '../patterns/dropdown/types';\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'nimble-list-option': ListOption;\n }\n}\n\n/**\n * A nimble-styled HTML listbox option\n */\nexport class ListOption extends FoundationListboxOption {\n /** @internal */\n public contentSlot!: HTMLSlotElement;\n\n /**\n * The hidden state of the element.\n *\n * @public\n * @defaultValue - false\n * @remarks\n * HTML Attribute: hidden\n */\n @attr({ mode: 'boolean' })\n public override hidden = false;\n\n /**\n * @internal\n * This attribute is required to allow use-cases that offer dynamic filtering\n * (like the Select) to visually hide options that are filtered out, but still\n * allow users to use the native 'hidden' attribute without it being affected\n * by the filtering process.\n */\n @attr({ attribute: 'visually-hidden', mode: 'boolean' })\n public visuallyHidden = false;\n\n /**\n * @internal\n * This attribute is used to control the visual selected state of an option. This\n * is handled independently of the public 'selected' attribute, as 'selected' is\n * representative of the current value of the container control. However, while\n * a dropdown is open users can navigate through the options (requiring visual\n * updates) without changing the value of the container control.\n */\n @attr({ attribute: 'active-option', mode: 'boolean' })\n public activeOption = false;\n\n /** @internal */\n @observable\n public hasOverflow = false;\n\n /** @internal */\n public get elementTextContent(): string {\n return this.contentSlot\n .assignedNodes()\n .map(node => node.textContent?.trim())\n .join(' ');\n }\n\n public override connectedCallback(): void {\n super.connectedCallback();\n if (this.isListOptionOwner(this.parentElement)) {\n this.parentElement.registerOption(this);\n }\n }\n\n private isListOptionOwner(\n parent: HTMLElement | null\n ): parent is ListOptionOwner {\n if (!parent) {\n return false;\n }\n\n return typeof (parent as ListOptionOwner).registerOption === 'function';\n }\n}\n\nconst nimbleListOption = ListOption.compose({\n baseName: 'list-option',\n baseClass: FoundationListboxOption,\n template,\n styles\n});\n\nDesignSystem.getOrCreate().withPrefix('nimble').register(nimbleListOption());\nexport const listOptionTag = 'nimble-list-option';\n"]}
@@ -103,7 +103,7 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
103
103
  get collapsible(): boolean;
104
104
  private _value;
105
105
  private forcedPosition;
106
- private indexWhenOpened?;
106
+ private openActiveIndex?;
107
107
  /**
108
108
  * @internal
109
109
  */
@@ -196,6 +196,12 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
196
196
  * @internal
197
197
  */
198
198
  selectedIndexChanged(_: number | undefined, __: number): void;
199
+ /**
200
+ * @internal
201
+ * Fork of Listbox implementation, so that the selectedIndex is not changed while the dropdown
202
+ * is open.
203
+ */
204
+ typeaheadBufferChanged(_: string, __: string): void;
199
205
  /**
200
206
  * Synchronize the `aria-disabled` property when the `disabled` property changes.
201
207
  *
@@ -211,14 +217,29 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
211
217
  * @internal
212
218
  */
213
219
  formResetCallback(): void;
220
+ /**
221
+ * @internal
222
+ */
214
223
  selectNextOption(): void;
224
+ /**
225
+ * @internal
226
+ */
215
227
  selectPreviousOption(): void;
228
+ /**
229
+ * @internal
230
+ */
231
+ selectFirstOption(): void;
232
+ /**
233
+ * @internal
234
+ */
235
+ selectLastOption(): void;
216
236
  /**
217
237
  * @internal
218
238
  */
219
239
  registerOption(option: ListOption): void;
220
240
  protected setSelectedOptions(): void;
221
241
  protected focusAndScrollOptionIntoView(): void;
242
+ protected getTypeaheadMatches(): ListboxOption[];
222
243
  protected positionChanged(_: SelectPosition | undefined, next: SelectPosition | undefined): void;
223
244
  /**
224
245
  * Updates the proxy's size property when the size attribute changes.
@@ -249,6 +270,8 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
249
270
  * @internal
250
271
  */
251
272
  protected setDefaultSelectedOption(): void;
273
+ private setActiveOption;
274
+ private focusAndScrollActiveOptionIntoView;
252
275
  private committedSelectedOptionChanged;
253
276
  private setPositioning;
254
277
  /**
@@ -271,7 +294,6 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
271
294
  * @internal
272
295
  */
273
296
  private setProxyOptions;
274
- private clearSelection;
275
297
  private filterChanged;
276
298
  private maxHeightChanged;
277
299
  private initializeOpenState;
@@ -2,7 +2,7 @@ import { __decorate } from "tslib";
2
2
  // Based on: https://github.com/microsoft/fast/blob/%40microsoft/fast-foundation_v2.49.5/packages/web-components/fast-foundation/src/select/select.ts
3
3
  import { attr, html, observable, Observable, volatile } from '@microsoft/fast-element';
4
4
  import { DesignSystem, Select as FoundationSelect, SelectPosition, applyMixins, StartEnd, DelegatesARIASelect } from '@microsoft/fast-foundation';
5
- import { keyArrowDown, keyArrowUp, keyEnd, keyEnter, keyEscape, keyHome, keySpace, keyTab, uniqueId } from '@microsoft/fast-web-utilities';
5
+ import { findLastIndex, keyArrowDown, keyArrowUp, keyEnd, keyEnter, keyEscape, keyHome, keySpace, uniqueId } from '@microsoft/fast-web-utilities';
6
6
  import { arrowExpanderDown16X16 } from '@ni/nimble-tokens/dist/icons/js';
7
7
  import { styles } from './styles';
8
8
  import { DropdownAppearance } from '../patterns/dropdown/types';
@@ -75,7 +75,9 @@ export class Select extends FormAssociatedSelect {
75
75
  connectedCallback() {
76
76
  super.connectedCallback();
77
77
  this.forcedPosition = !!this.positionAttribute;
78
- this.initializeOpenState();
78
+ if (this.open) {
79
+ this.initializeOpenState();
80
+ }
79
81
  }
80
82
  get value() {
81
83
  Observable.track(this, 'value');
@@ -174,7 +176,7 @@ export class Select extends FormAssociatedSelect {
174
176
  }
175
177
  super.clickHandler(e);
176
178
  this.open = this.collapsible && !this.open;
177
- if (!this.open && this.indexWhenOpened !== this.selectedIndex) {
179
+ if (!this.open && this.selectedIndex !== -1) {
178
180
  this.updateValue(true);
179
181
  }
180
182
  }
@@ -286,22 +288,19 @@ export class Select extends FormAssociatedSelect {
286
288
  */
287
289
  inputHandler(e) {
288
290
  this.filter = this.filterInput?.value ?? '';
289
- this.clearSelection();
290
291
  this.filterOptions();
291
- if (this.filteredOptions.length > 0) {
292
- const enabledOptions = this.filteredOptions.filter(o => !o.disabled);
293
- if (enabledOptions.length > 0) {
294
- enabledOptions[0].selected = true;
295
- }
296
- else {
297
- // only filtered option is disabled
298
- this.selectedOptions = [];
299
- this.selectedIndex = -1;
300
- }
301
- }
302
- else if (this.committedSelectedOption) {
303
- this.committedSelectedOption.selected = true;
304
- }
292
+ const enabledOptions = this.filteredOptions.filter(o => !o.disabled);
293
+ let activeOptionIndex = this.filter !== ''
294
+ ? this.openActiveIndex ?? this.selectedIndex
295
+ : this.selectedIndex;
296
+ if (enabledOptions.length > 0
297
+ && !enabledOptions.find(o => o === this.options[activeOptionIndex])) {
298
+ activeOptionIndex = this.options.indexOf(enabledOptions[0]);
299
+ }
300
+ else if (enabledOptions.length === 0) {
301
+ activeOptionIndex = -1;
302
+ }
303
+ this.setActiveOption(activeOptionIndex);
305
304
  if (e.inputType.includes('deleteContent') || !this.filter.length) {
306
305
  return true;
307
306
  }
@@ -322,11 +321,13 @@ export class Select extends FormAssociatedSelect {
322
321
  return true;
323
322
  }
324
323
  if (!this.options?.includes(focusTarget)) {
324
+ let currentActiveIndex = this.openActiveIndex ?? this.selectedIndex;
325
325
  this.open = false;
326
- if (this.selectedIndex === -1) {
327
- this.selectedIndex = this.indexWhenOpened;
326
+ if (currentActiveIndex === -1) {
327
+ currentActiveIndex = this.selectedIndex;
328
328
  }
329
- if (this.indexWhenOpened !== this.selectedIndex) {
329
+ if (this.selectedIndex !== currentActiveIndex) {
330
+ this.selectedIndex = currentActiveIndex;
330
331
  this.updateValue(true);
331
332
  }
332
333
  }
@@ -336,11 +337,13 @@ export class Select extends FormAssociatedSelect {
336
337
  * @internal
337
338
  */
338
339
  keydownHandler(e) {
340
+ const initialSelectedIndex = this.selectedIndex;
339
341
  super.keydownHandler(e);
340
342
  const key = e.key;
341
343
  if (e.ctrlKey || e.shiftKey) {
342
344
  return true;
343
345
  }
346
+ let currentActiveIndex = this.openActiveIndex ?? this.selectedIndex;
344
347
  switch (key) {
345
348
  case keySpace: {
346
349
  // when dropdown is open allow user to enter a space for filter text
@@ -381,27 +384,19 @@ export class Select extends FormAssociatedSelect {
381
384
  e.preventDefault();
382
385
  this.open = false;
383
386
  }
384
- if (this.selectedIndex !== this.indexWhenOpened) {
385
- this.options[this.selectedIndex].selected = false;
386
- this.selectedIndex = this.indexWhenOpened;
387
- }
387
+ currentActiveIndex = this.selectedIndex;
388
388
  this.focus();
389
389
  break;
390
390
  }
391
- case keyTab: {
392
- if (this.collapsible && this.open) {
393
- e.preventDefault();
394
- this.open = false;
395
- }
396
- return true;
397
- }
398
391
  default: {
399
392
  break;
400
393
  }
401
394
  }
402
- if (!this.open && this.indexWhenOpened !== this.selectedIndex) {
395
+ if (!this.open && this.selectedIndex !== currentActiveIndex) {
396
+ this.selectedIndex = currentActiveIndex;
397
+ }
398
+ if (!this.open && initialSelectedIndex !== this.selectedIndex) {
403
399
  this.updateValue(true);
404
- this.indexWhenOpened = this.selectedIndex;
405
400
  }
406
401
  return !(key === keyArrowDown || key === keyArrowUp);
407
402
  }
@@ -419,8 +414,28 @@ export class Select extends FormAssociatedSelect {
419
414
  // implementation handles skipping non-selected disabled options for the initial
420
415
  // selected value.
421
416
  this.setSelectedOptions();
417
+ if (this.open) {
418
+ this.setActiveOption(this.selectedIndex);
419
+ }
422
420
  this.updateValue();
423
421
  }
422
+ /**
423
+ * @internal
424
+ * Fork of Listbox implementation, so that the selectedIndex is not changed while the dropdown
425
+ * is open.
426
+ */
427
+ typeaheadBufferChanged(_, __) {
428
+ if (this.$fastController.isConnected) {
429
+ const typeaheadMatches = this.getTypeaheadMatches();
430
+ if (typeaheadMatches.length) {
431
+ const activeOptionIndex = this.options.indexOf(typeaheadMatches[0]);
432
+ if (!(this.open && this.filterMode !== FilterMode.none)) {
433
+ this.setActiveOption(activeOptionIndex);
434
+ }
435
+ }
436
+ this.typeaheadExpired = false;
437
+ }
438
+ }
424
439
  /**
425
440
  * Synchronize the `aria-disabled` property when the `disabled` property changes.
426
441
  *
@@ -449,30 +464,52 @@ export class Select extends FormAssociatedSelect {
449
464
  this.selectedIndex = 0;
450
465
  }
451
466
  }
467
+ /**
468
+ * @internal
469
+ */
452
470
  selectNextOption() {
453
471
  // don't call super.selectNextOption as that relies on side-effecty
454
472
  // behavior to not select disabled option (which no longer works)
455
- for (let i = this.selectedIndex + 1; i < this.options.length; i++) {
473
+ const startIndex = this.openActiveIndex ?? this.selectedIndex;
474
+ for (let i = startIndex + 1; i < this.options.length; i++) {
456
475
  const listOption = this.options[i];
457
476
  if (isNimbleListOption(listOption)
458
477
  && isOptionSelectable(listOption)) {
459
- this.selectedIndex = i;
478
+ this.setActiveOption(i);
460
479
  break;
461
480
  }
462
481
  }
463
482
  }
483
+ /**
484
+ * @internal
485
+ */
464
486
  selectPreviousOption() {
465
487
  // don't call super.selectPreviousOption as that relies on side-effecty
466
488
  // behavior to not select disabled option (which no longer works)
467
- for (let i = this.selectedIndex - 1; i >= 0; i--) {
489
+ const startIndex = this.openActiveIndex ?? this.selectedIndex;
490
+ for (let i = startIndex - 1; i >= 0; i--) {
468
491
  const listOption = this.options[i];
469
492
  if (isNimbleListOption(listOption)
470
493
  && isOptionSelectable(listOption)) {
471
- this.selectedIndex = i;
494
+ this.setActiveOption(i);
472
495
  break;
473
496
  }
474
497
  }
475
498
  }
499
+ /**
500
+ * @internal
501
+ */
502
+ selectFirstOption() {
503
+ const newActiveOptionIndex = this.options.findIndex(o => isNimbleListOption(o) && isOptionSelectable(o));
504
+ this.setActiveOption(newActiveOptionIndex);
505
+ }
506
+ /**
507
+ * @internal
508
+ */
509
+ selectLastOption() {
510
+ const newActiveOptionIndex = findLastIndex(this.options, o => isNimbleListOption(o) && isOptionSelectable(o));
511
+ this.setActiveOption(newActiveOptionIndex);
512
+ }
476
513
  /**
477
514
  * @internal
478
515
  */
@@ -505,6 +542,11 @@ export class Select extends FormAssociatedSelect {
505
542
  });
506
543
  }
507
544
  }
545
+ getTypeaheadMatches() {
546
+ const matches = super.getTypeaheadMatches();
547
+ // Don't allow placeholder to be matched
548
+ return matches.filter(o => !o.hidden && !o.disabled);
549
+ }
508
550
  positionChanged(_, next) {
509
551
  this.positionAttribute = next;
510
552
  this.setPositioning();
@@ -530,9 +572,13 @@ export class Select extends FormAssociatedSelect {
530
572
  }
531
573
  if (this.open) {
532
574
  this.initializeOpenState();
533
- this.indexWhenOpened = this.selectedIndex;
534
575
  return;
535
576
  }
577
+ const activeOption = this.options[this.openActiveIndex ?? this.selectedIndex];
578
+ if (isNimbleListOption(activeOption)) {
579
+ activeOption.activeOption = false;
580
+ }
581
+ this.openActiveIndex = undefined;
536
582
  this.filter = '';
537
583
  if (this.filterInput) {
538
584
  this.filterInput.value = '';
@@ -599,6 +645,40 @@ export class Select extends FormAssociatedSelect {
599
645
  }
600
646
  this.committedSelectedOption = options[this.selectedIndex];
601
647
  }
648
+ setActiveOption(newActiveIndex) {
649
+ const activeOption = this.options[newActiveIndex];
650
+ if (this.open) {
651
+ if (isNimbleListOption(activeOption)) {
652
+ activeOption.activeOption = true;
653
+ }
654
+ const previousActiveIndex = this.openActiveIndex ?? this.selectedIndex;
655
+ const previousActiveOption = this.options[previousActiveIndex];
656
+ if (previousActiveIndex !== newActiveIndex
657
+ && isNimbleListOption(previousActiveOption)) {
658
+ previousActiveOption.activeOption = false;
659
+ }
660
+ this.openActiveIndex = newActiveIndex;
661
+ this.focusAndScrollActiveOptionIntoView();
662
+ }
663
+ else {
664
+ this.selectedIndex = newActiveIndex;
665
+ }
666
+ this.ariaActiveDescendant = activeOption?.id ?? '';
667
+ }
668
+ focusAndScrollActiveOptionIntoView() {
669
+ const optionToFocus = this.options[this.openActiveIndex ?? this.selectedIndex];
670
+ // Copied from FAST: To ensure that the browser handles both `focus()` and
671
+ // `scrollIntoView()`, the timing here needs to guarantee that they happen on
672
+ // different frames. Since this function is typically called from the `openChanged`
673
+ // observer, `DOM.queueUpdate` causes the calls to be grouped into the same frame.
674
+ // To prevent this, `requestAnimationFrame` is used instead of `DOM.queueUpdate`.
675
+ if (optionToFocus !== undefined && this.contains(optionToFocus)) {
676
+ optionToFocus.focus();
677
+ requestAnimationFrame(() => {
678
+ optionToFocus.scrollIntoView({ block: 'nearest' });
679
+ });
680
+ }
681
+ }
602
682
  committedSelectedOptionChanged() {
603
683
  this.updateDisplayValue();
604
684
  }
@@ -694,11 +774,6 @@ export class Select extends FormAssociatedSelect {
694
774
  });
695
775
  }
696
776
  }
697
- clearSelection() {
698
- this.options.forEach(option => {
699
- option.selected = false;
700
- });
701
- }
702
777
  filterChanged() {
703
778
  this.filterOptions();
704
779
  }
@@ -706,12 +781,8 @@ export class Select extends FormAssociatedSelect {
706
781
  this.updateListboxMaxHeightCssVariable();
707
782
  }
708
783
  initializeOpenState() {
709
- if (!this.open) {
710
- this.ariaExpanded = 'false';
711
- this.ariaControls = '';
712
- return;
713
- }
714
784
  this.committedSelectedOption = this.options[this.selectedIndex];
785
+ this.setActiveOption(this.selectedIndex);
715
786
  this.ariaControls = this.listboxId;
716
787
  this.ariaExpanded = 'true';
717
788
  this.setPositioning();