@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.
- package/dist/all-components-bundle.js +160 -49
- package/dist/all-components-bundle.js.map +1 -1
- package/dist/all-components-bundle.min.js +4707 -4679
- package/dist/all-components-bundle.min.js.map +1 -1
- package/dist/esm/list-option/index.d.ts +9 -0
- package/dist/esm/list-option/index.js +12 -0
- package/dist/esm/list-option/index.js.map +1 -1
- package/dist/esm/select/index.d.ts +24 -2
- package/dist/esm/select/index.js +120 -49
- package/dist/esm/select/index.js.map +1 -1
- package/dist/esm/select/styles.js +13 -1
- package/dist/esm/select/styles.js.map +1 -1
- package/dist/esm/select/testing/select.pageobject.d.ts +6 -1
- package/dist/esm/select/testing/select.pageobject.js +46 -5
- package/dist/esm/select/testing/select.pageobject.js.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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
|
|
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;
|
package/dist/esm/select/index.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
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 (
|
|
327
|
-
|
|
326
|
+
if (currentActiveIndex === -1) {
|
|
327
|
+
currentActiveIndex = this.selectedIndex;
|
|
328
328
|
}
|
|
329
|
-
if (this.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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();
|