@ni/nimble-components 21.8.2 → 21.9.0

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.
@@ -10,6 +10,23 @@ declare global {
10
10
  export declare class ListOption extends FoundationListboxOption {
11
11
  /** @internal */
12
12
  contentSlot: HTMLSlotElement;
13
+ /**
14
+ * The hidden state of the element.
15
+ *
16
+ * @public
17
+ * @defaultValue - false
18
+ * @remarks
19
+ * HTML Attribute: hidden
20
+ */
21
+ hidden: boolean;
22
+ /**
23
+ * @internal
24
+ * This attribute is required to allow use-cases that offer dynamic filtering
25
+ * (like the Select) to visually hide options that are filtered out, but still
26
+ * allow users to use the native 'hidden' attribute without it being affected
27
+ * by the filtering process.
28
+ */
29
+ visuallyHidden: boolean;
13
30
  /** @internal */
14
31
  hasOverflow: boolean;
15
32
  /** @internal */
@@ -1,6 +1,6 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { DesignSystem, ListboxOption as FoundationListboxOption } from '@microsoft/fast-foundation';
3
- import { observable } from '@microsoft/fast-element';
3
+ import { observable, attr } from '@microsoft/fast-element';
4
4
  import { styles } from './styles';
5
5
  import { template } from './template';
6
6
  /**
@@ -9,6 +9,23 @@ import { template } from './template';
9
9
  export class ListOption extends FoundationListboxOption {
10
10
  constructor() {
11
11
  super(...arguments);
12
+ /**
13
+ * The hidden state of the element.
14
+ *
15
+ * @public
16
+ * @defaultValue - false
17
+ * @remarks
18
+ * HTML Attribute: hidden
19
+ */
20
+ this.hidden = false;
21
+ /**
22
+ * @internal
23
+ * This attribute is required to allow use-cases that offer dynamic filtering
24
+ * (like the Select) to visually hide options that are filtered out, but still
25
+ * allow users to use the native 'hidden' attribute without it being affected
26
+ * by the filtering process.
27
+ */
28
+ this.visuallyHidden = false;
12
29
  /** @internal */
13
30
  this.hasOverflow = false;
14
31
  }
@@ -20,6 +37,12 @@ export class ListOption extends FoundationListboxOption {
20
37
  .join(' ');
21
38
  }
22
39
  }
40
+ __decorate([
41
+ attr({ mode: 'boolean' })
42
+ ], ListOption.prototype, "hidden", void 0);
43
+ __decorate([
44
+ attr({ attribute: 'visually-hidden', mode: 'boolean' })
45
+ ], ListOption.prototype, "visuallyHidden", void 0);
23
46
  __decorate([
24
47
  observable
25
48
  ], 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,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAQtC;;GAEG;AACH,MAAM,OAAO,UAAW,SAAQ,uBAAuB;IAAvD;;QAII,gBAAgB;QAET,gBAAW,GAAG,KAAK,CAAC;IAS/B,CAAC;IAPG,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;CACJ;AATG;IADC,UAAU;+CACgB;AAW/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 } from '@microsoft/fast-element';\nimport { styles } from './styles';\nimport { template } from './template';\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 /** @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\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;AAQtC;;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;IAS/B,CAAC;IAPG,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;CACJ;AAvBG;IADC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;0CACK;AAU/B;IADC,IAAI,CAAC,EAAE,SAAS,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;kDAC1B;AAI9B;IADC,UAAU;+CACgB;AAW/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';\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\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"]}
@@ -12,17 +12,6 @@ export const styles = css `
12
12
  height: ${controlHeight};
13
13
  }
14
14
 
15
- [part='start'] {
16
- display: none;
17
- }
18
-
19
- .content {
20
- padding: 8px 4px;
21
- white-space: nowrap;
22
- overflow: hidden;
23
- text-overflow: ellipsis;
24
- }
25
-
26
15
  :host([aria-selected='true']) {
27
16
  box-shadow: none;
28
17
  outline: none;
@@ -59,6 +48,21 @@ export const styles = css `
59
48
  cursor: default;
60
49
  }
61
50
 
51
+ :host([visually-hidden]) {
52
+ display: none;
53
+ }
54
+
55
+ [part='start'] {
56
+ display: none;
57
+ }
58
+
59
+ .content {
60
+ padding: 8px 4px;
61
+ white-space: nowrap;
62
+ overflow: hidden;
63
+ text-overflow: ellipsis;
64
+ }
65
+
62
66
  .content[disabled] {
63
67
  box-shadow: none;
64
68
  outline: none;
@@ -1 +1 @@
1
- {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../../src/list-option/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,EACH,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACd,QAAQ,EACR,qBAAqB,EACrB,aAAa,EAChB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAA;MACnB,OAAO,CAAC,MAAM,CAAC;;;gBAGL,QAAQ;;;kBAGN,aAAa;;;;;;;;;;;;;;;;;4BAiBH,iBAAiB;;;;4BAIjB,sBAAsB;;;;4BAItB,cAAc;;;;;;;;aAQ7B,YAAY;sCACa,gBAAgB;6BACzB,gBAAgB;;;;;;;4BAOjB,iBAAiB;;;;iBAI5B,qBAAqB;;;;;;;;;;;;CAYrC,CAAC","sourcesContent":["import { css } from '@microsoft/fast-element';\nimport { display } from '@microsoft/fast-foundation';\nimport { focusVisible } from '../utilities/style/focus';\n\nimport {\n borderHoverColor,\n fillSelectedColor,\n fillHoverSelectedColor,\n fillHoverColor,\n bodyFont,\n bodyDisabledFontColor,\n controlHeight\n} from '../theme-provider/design-tokens';\n\nexport const styles = css`\n ${display('flex')}\n\n :host {\n font: ${bodyFont};\n cursor: pointer;\n justify-content: left;\n height: ${controlHeight};\n }\n\n [part='start'] {\n display: none;\n }\n\n .content {\n padding: 8px 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n :host([aria-selected='true']) {\n box-shadow: none;\n outline: none;\n background-color: ${fillSelectedColor};\n }\n\n :host([aria-selected='true']:hover) {\n background-color: ${fillHoverSelectedColor};\n }\n\n :host(:hover) {\n background-color: ${fillHoverColor};\n }\n\n :host(:hover):host([disabled]) {\n box-shadow: none;\n background-color: transparent;\n }\n\n :host(:${focusVisible}) {\n box-shadow: 0px 0px 0px 1px ${borderHoverColor} inset;\n outline: 1px solid ${borderHoverColor};\n outline-offset: -4px;\n }\n\n :host(:active) {\n box-shadow: none;\n outline: none;\n background-color: ${fillSelectedColor};\n }\n\n :host([disabled]) {\n color: ${bodyDisabledFontColor};\n cursor: default;\n }\n\n .content[disabled] {\n box-shadow: none;\n outline: none;\n }\n\n [part='end'] {\n display: none;\n }\n`;\n"]}
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../../src/list-option/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,EACH,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,cAAc,EACd,QAAQ,EACR,qBAAqB,EACrB,aAAa,EAChB,MAAM,iCAAiC,CAAC;AAEzC,MAAM,CAAC,MAAM,MAAM,GAAG,GAAG,CAAA;MACnB,OAAO,CAAC,MAAM,CAAC;;;gBAGL,QAAQ;;;kBAGN,aAAa;;;;;;4BAMH,iBAAiB;;;;4BAIjB,sBAAsB;;;;4BAItB,cAAc;;;;;;;;aAQ7B,YAAY;sCACa,gBAAgB;6BACzB,gBAAgB;;;;;;;4BAOjB,iBAAiB;;;;iBAI5B,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BrC,CAAC","sourcesContent":["import { css } from '@microsoft/fast-element';\nimport { display } from '@microsoft/fast-foundation';\nimport { focusVisible } from '../utilities/style/focus';\n\nimport {\n borderHoverColor,\n fillSelectedColor,\n fillHoverSelectedColor,\n fillHoverColor,\n bodyFont,\n bodyDisabledFontColor,\n controlHeight\n} from '../theme-provider/design-tokens';\n\nexport const styles = css`\n ${display('flex')}\n\n :host {\n font: ${bodyFont};\n cursor: pointer;\n justify-content: left;\n height: ${controlHeight};\n }\n\n :host([aria-selected='true']) {\n box-shadow: none;\n outline: none;\n background-color: ${fillSelectedColor};\n }\n\n :host([aria-selected='true']:hover) {\n background-color: ${fillHoverSelectedColor};\n }\n\n :host(:hover) {\n background-color: ${fillHoverColor};\n }\n\n :host(:hover):host([disabled]) {\n box-shadow: none;\n background-color: transparent;\n }\n\n :host(:${focusVisible}) {\n box-shadow: 0px 0px 0px 1px ${borderHoverColor} inset;\n outline: 1px solid ${borderHoverColor};\n outline-offset: -4px;\n }\n\n :host(:active) {\n box-shadow: none;\n outline: none;\n background-color: ${fillSelectedColor};\n }\n\n :host([disabled]) {\n color: ${bodyDisabledFontColor};\n cursor: default;\n }\n\n :host([visually-hidden]) {\n display: none;\n }\n\n [part='start'] {\n display: none;\n }\n\n .content {\n padding: 8px 4px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .content[disabled] {\n box-shadow: none;\n outline: none;\n }\n\n [part='end'] {\n display: none;\n }\n`;\n"]}
@@ -30,6 +30,10 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
30
30
  errorText: string | undefined;
31
31
  errorVisible: boolean;
32
32
  filterMode: FilterMode;
33
+ /**
34
+ * @internal
35
+ */
36
+ displayPlaceholder: boolean;
33
37
  /**
34
38
  * @internal
35
39
  */
@@ -83,7 +87,7 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
83
87
  /**
84
88
  * @internal
85
89
  */
86
- committedSelectedOption: ListboxOption | undefined;
90
+ committedSelectedOption?: ListboxOption;
87
91
  /**
88
92
  * The max height for the listbox when opened.
89
93
  *
@@ -103,16 +107,6 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
103
107
  * @internal
104
108
  */
105
109
  connectedCallback(): void;
106
- /**
107
- * The list of options. This mirrors FAST's override implementation for this
108
- * member for the Combobox to support a filtered list in the dropdown.
109
- *
110
- * @public
111
- * @remarks
112
- * Overrides `Listbox.options`.
113
- */
114
- get options(): ListboxOption[];
115
- set options(value: ListboxOption[]);
116
110
  get value(): string;
117
111
  set value(next: string);
118
112
  /**
@@ -200,7 +194,7 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
200
194
  *
201
195
  * @internal
202
196
  */
203
- selectedIndexChanged(prev: number | undefined, next: number): void;
197
+ selectedIndexChanged(_: number | undefined, __: number): void;
204
198
  /**
205
199
  * Synchronize the `aria-disabled` property when the `disabled` property changes.
206
200
  *
@@ -216,6 +210,8 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
216
210
  * @internal
217
211
  */
218
212
  formResetCallback(): void;
213
+ selectNextOption(): void;
214
+ selectPreviousOption(): void;
219
215
  protected setSelectedOptions(): void;
220
216
  protected focusAndScrollOptionIntoView(): void;
221
217
  protected positionChanged(_: SelectPosition | undefined, next: SelectPosition | undefined): void;
@@ -239,7 +235,7 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
239
235
  * @override
240
236
  * @internal
241
237
  */
242
- protected selectedOptionsChanged(prev: ListboxOption[] | undefined, next: ListboxOption[]): void;
238
+ protected selectedOptionsChanged(_prev: ListboxOption[] | undefined, next: ListboxOption[]): void;
243
239
  /**
244
240
  * Sets the selected index to match the first option with the selected attribute, or
245
241
  * the first selectable option.
@@ -248,6 +244,7 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
248
244
  * @internal
249
245
  */
250
246
  protected setDefaultSelectedOption(): void;
247
+ private committedSelectedOptionChanged;
251
248
  private setPositioning;
252
249
  /**
253
250
  * Filter available options by text value.
@@ -274,7 +271,6 @@ export declare class Select extends FormAssociatedSelect implements ErrorPattern
274
271
  private maxHeightChanged;
275
272
  private initializeOpenState;
276
273
  private updateListboxMaxHeightCssVariable;
277
- private updateSelectedIndexFromFilteredSet;
278
274
  }
279
275
  export declare const selectTag = "nimble-select";
280
276
  export {};
@@ -1,7 +1,7 @@
1
1
  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
- import { DesignSystem, Select as FoundationSelect, SelectPosition, applyMixins, StartEnd, DelegatesARIASelect, Listbox } from '@microsoft/fast-foundation';
4
+ import { DesignSystem, Select as FoundationSelect, SelectPosition, applyMixins, StartEnd, DelegatesARIASelect } from '@microsoft/fast-foundation';
5
5
  import { keyArrowDown, keyArrowUp, keyEnd, keyEnter, keyEscape, keyHome, keySpace, keyTab, uniqueId } from '@microsoft/fast-web-utilities';
6
6
  import { arrowExpanderDown16X16 } from '@ni/nimble-tokens/dist/icons/js';
7
7
  import { styles } from './styles';
@@ -9,9 +9,13 @@ import { DropdownAppearance } from '../patterns/dropdown/types';
9
9
  import { errorTextTemplate } from '../patterns/error/template';
10
10
  import { iconExclamationMarkTag } from '../icons/exclamation-mark';
11
11
  import { template } from './template';
12
+ import { ListOption } from '../list-option';
12
13
  import { FilterMode } from './types';
13
14
  import { diacriticInsensitiveStringNormalizer } from '../utilities/models/string-normalizers';
14
15
  import { FormAssociatedSelect } from './models/select-form-associated';
16
+ const isNimbleListOption = (el) => {
17
+ return el instanceof ListOption;
18
+ };
15
19
  /**
16
20
  * A nimble-styled HTML select.
17
21
  */
@@ -21,6 +25,10 @@ export class Select extends FormAssociatedSelect {
21
25
  this.appearance = DropdownAppearance.underline;
22
26
  this.errorVisible = false;
23
27
  this.filterMode = FilterMode.none;
28
+ /**
29
+ * @internal
30
+ */
31
+ this.displayPlaceholder = false;
24
32
  /**
25
33
  * @internal
26
34
  */
@@ -41,10 +49,6 @@ export class Select extends FormAssociatedSelect {
41
49
  * @internal
42
50
  */
43
51
  this.filter = '';
44
- /**
45
- * @internal
46
- */
47
- this.committedSelectedOption = undefined;
48
52
  /**
49
53
  * The max height for the listbox when opened.
50
54
  *
@@ -70,24 +74,6 @@ export class Select extends FormAssociatedSelect {
70
74
  this.forcedPosition = !!this.positionAttribute;
71
75
  this.initializeOpenState();
72
76
  }
73
- /**
74
- * The list of options. This mirrors FAST's override implementation for this
75
- * member for the Combobox to support a filtered list in the dropdown.
76
- *
77
- * @public
78
- * @remarks
79
- * Overrides `Listbox.options`.
80
- */
81
- get options() {
82
- Observable.track(this, 'options');
83
- return this.filteredOptions?.length
84
- ? this.filteredOptions
85
- : this._options;
86
- }
87
- set options(value) {
88
- this._options = value;
89
- Observable.notify(this, 'options');
90
- }
91
77
  get value() {
92
78
  Observable.track(this, 'value');
93
79
  return this._value;
@@ -95,8 +81,6 @@ export class Select extends FormAssociatedSelect {
95
81
  set value(next) {
96
82
  const prev = this._value;
97
83
  let newValue = next;
98
- // use 'options' here instead of '_options' as 'selectedIndex' may be relative
99
- // to filtered set
100
84
  if (this.options?.length) {
101
85
  const newValueIndex = this.options.findIndex(el => el.value === newValue);
102
86
  const prevSelectedValue = this.options[this.selectedIndex]?.value ?? null;
@@ -112,7 +96,7 @@ export class Select extends FormAssociatedSelect {
112
96
  this._value = newValue;
113
97
  super.valueChanged(prev, newValue);
114
98
  if (!this.open) {
115
- this.committedSelectedOption = this._options.find(o => o.value === newValue);
99
+ this.committedSelectedOption = this.options.find(o => o.value === newValue);
116
100
  }
117
101
  Observable.notify(this, 'value');
118
102
  if (this.collapsible) {
@@ -148,14 +132,18 @@ export class Select extends FormAssociatedSelect {
148
132
  */
149
133
  slottedOptionsChanged(prev, next) {
150
134
  const value = this.value;
151
- this._options.forEach(o => {
135
+ this.options.forEach(o => {
152
136
  const notifier = Observable.getNotifier(o);
153
137
  notifier.unsubscribe(this, 'value');
138
+ notifier.unsubscribe(this, 'hidden');
139
+ notifier.unsubscribe(this, 'disabled');
154
140
  });
155
141
  super.slottedOptionsChanged(prev, next);
156
- this._options.forEach(o => {
142
+ this.options.forEach(o => {
157
143
  const notifier = Observable.getNotifier(o);
158
144
  notifier.subscribe(this, 'value');
145
+ notifier.subscribe(this, 'hidden');
146
+ notifier.subscribe(this, 'disabled');
159
147
  });
160
148
  this.setProxyOptions();
161
149
  this.updateValue();
@@ -177,9 +165,6 @@ export class Select extends FormAssociatedSelect {
177
165
  }
178
166
  if (this.open) {
179
167
  const captured = e.target.closest('option,[role=option]');
180
- if (!captured?.disabled) {
181
- this.updateSelectedIndexFromFilteredSet();
182
- }
183
168
  if (captured?.disabled) {
184
169
  return;
185
170
  }
@@ -200,9 +185,34 @@ export class Select extends FormAssociatedSelect {
200
185
  * @override
201
186
  */
202
187
  handleChange(source, propertyName) {
203
- super.handleChange(source, propertyName);
204
- if (propertyName === 'value') {
205
- this.updateValue();
188
+ // don't call super.handleChange so hidden options can be selected programmatically
189
+ const sourceElement = source;
190
+ switch (propertyName) {
191
+ case 'value': {
192
+ this.updateValue();
193
+ break;
194
+ }
195
+ case 'selected': {
196
+ if (isNimbleListOption(sourceElement)) {
197
+ this.selectedIndex = this.options.indexOf(sourceElement);
198
+ }
199
+ this.setSelectedOptions();
200
+ this.updateDisplayValue();
201
+ break;
202
+ }
203
+ case 'hidden': {
204
+ if (isNimbleListOption(sourceElement)) {
205
+ sourceElement.visuallyHidden = sourceElement.hidden;
206
+ }
207
+ this.updateDisplayValue();
208
+ break;
209
+ }
210
+ case 'disabled': {
211
+ this.updateDisplayValue();
212
+ break;
213
+ }
214
+ default:
215
+ break;
206
216
  }
207
217
  }
208
218
  /**
@@ -253,6 +263,14 @@ export class Select extends FormAssociatedSelect {
253
263
  * @internal
254
264
  */
255
265
  updateDisplayValue() {
266
+ if (this.committedSelectedOption?.disabled
267
+ && this.committedSelectedOption?.hidden
268
+ && this.committedSelectedOption?.selected) {
269
+ this.displayPlaceholder = true;
270
+ }
271
+ else {
272
+ this.displayPlaceholder = false;
273
+ }
256
274
  if (this.collapsible) {
257
275
  Observable.notify(this, 'displayValue');
258
276
  }
@@ -265,14 +283,9 @@ export class Select extends FormAssociatedSelect {
265
283
  */
266
284
  inputHandler(e) {
267
285
  this.filter = this.filterInput?.value ?? '';
268
- if (!this.committedSelectedOption) {
269
- this.committedSelectedOption = this._options.find(option => option.selected);
270
- }
271
286
  this.clearSelection();
272
287
  this.filterOptions();
273
- if (this.filteredOptions.length > 0
274
- && this.committedSelectedOption
275
- && !this.filteredOptions.includes(this.committedSelectedOption)) {
288
+ if (this.filteredOptions.length > 0) {
276
289
  const enabledOptions = this.filteredOptions.filter(o => !o.disabled);
277
290
  if (enabledOptions.length > 0) {
278
291
  enabledOptions[0].selected = true;
@@ -296,7 +309,6 @@ export class Select extends FormAssociatedSelect {
296
309
  * @internal
297
310
  */
298
311
  focusoutHandler(e) {
299
- this.updateSelectedIndexFromFilteredSet();
300
312
  super.focusoutHandler(e);
301
313
  if (!this.open) {
302
314
  return true;
@@ -308,6 +320,9 @@ export class Select extends FormAssociatedSelect {
308
320
  }
309
321
  if (!this.options?.includes(focusTarget)) {
310
322
  this.open = false;
323
+ if (this.selectedIndex === -1) {
324
+ this.selectedIndex = this.indexWhenOpened;
325
+ }
311
326
  if (this.indexWhenOpened !== this.selectedIndex) {
312
327
  this.updateValue(true);
313
328
  }
@@ -349,7 +364,6 @@ export class Select extends FormAssociatedSelect {
349
364
  || this.filteredOptions.every(o => o.disabled)) {
350
365
  return false;
351
366
  }
352
- this.updateSelectedIndexFromFilteredSet();
353
367
  this.open = !this.open;
354
368
  if (!this.open) {
355
369
  this.focus();
@@ -357,21 +371,16 @@ export class Select extends FormAssociatedSelect {
357
371
  break;
358
372
  }
359
373
  case keyEscape: {
360
- // clear filter as update to "selectedIndex" will result in processing
361
- // "options" and not "_options"
362
- this.filter = '';
363
- if (this.committedSelectedOption) {
364
- this.clearSelection();
365
- this.selectedIndex = this._options.indexOf(this.committedSelectedOption);
374
+ if (!this.open) {
375
+ break;
366
376
  }
367
377
  if (this.collapsible && this.open) {
368
378
  e.preventDefault();
369
379
  this.open = false;
370
380
  }
371
- // reset 'selected' state otherwise the selected state doesn't stick.
372
- const selectedOption = this._options[this.selectedIndex];
373
- if (selectedOption) {
374
- selectedOption.selected = true;
381
+ if (this.selectedIndex !== this.indexWhenOpened) {
382
+ this.options[this.selectedIndex].selected = false;
383
+ this.selectedIndex = this.indexWhenOpened;
375
384
  }
376
385
  this.focus();
377
386
  break;
@@ -401,8 +410,12 @@ export class Select extends FormAssociatedSelect {
401
410
  *
402
411
  * @internal
403
412
  */
404
- selectedIndexChanged(prev, next) {
405
- super.selectedIndexChanged(prev, next);
413
+ selectedIndexChanged(_, __) {
414
+ // Don't call super.selectedIndexChanged as this will disallow disabled options
415
+ // from being valid initial selected values. Our setDefaultSelectedOption
416
+ // implementation handles skipping non-selected disabled options for the initial
417
+ // selected value.
418
+ this.setSelectedOptions();
406
419
  this.updateValue();
407
420
  }
408
421
  /**
@@ -433,6 +446,26 @@ export class Select extends FormAssociatedSelect {
433
446
  this.selectedIndex = 0;
434
447
  }
435
448
  }
449
+ selectNextOption() {
450
+ // don't call super.selectNextOption as that relies on side-effecty
451
+ // behavior to not select disabled option (which no longer works)
452
+ for (let i = this.selectedIndex + 1; i < this.options.length; i++) {
453
+ if (!this.options[i]?.disabled) {
454
+ this.selectedIndex = i;
455
+ break;
456
+ }
457
+ }
458
+ }
459
+ selectPreviousOption() {
460
+ // don't call super.selectPreviousOption as that relies on side-effecty
461
+ // behavior to not select disabled option (which no longer works)
462
+ for (let i = this.selectedIndex - 1; i >= 0; i--) {
463
+ if (!this.options[i]?.disabled) {
464
+ this.selectedIndex = i;
465
+ break;
466
+ }
467
+ }
468
+ }
436
469
  // Prevents parent classes from resetting selectedIndex to a positive
437
470
  // value while filtering, which can result in a disabled option being
438
471
  // selected.
@@ -494,9 +527,14 @@ export class Select extends FormAssociatedSelect {
494
527
  * @override
495
528
  * @internal
496
529
  */
497
- selectedOptionsChanged(prev, next) {
498
- super.selectedOptionsChanged(prev, next);
530
+ selectedOptionsChanged(_prev, next) {
531
+ // don't call super.selectedOptionsChanged so we don't filter out hidden elements
532
+ // when updating 'selected' state (copied relevant super implementation)
499
533
  this.options?.forEach((o, i) => {
534
+ const notifier = Observable.getNotifier(o);
535
+ notifier.unsubscribe(this, 'selected');
536
+ o.selected = next.includes(o);
537
+ notifier.subscribe(this, 'selected');
500
538
  const proxyOption = this.proxy?.options.item(i);
501
539
  if (proxyOption) {
502
540
  proxyOption.selected = o.selected;
@@ -512,15 +550,37 @@ export class Select extends FormAssociatedSelect {
512
550
  */
513
551
  setDefaultSelectedOption() {
514
552
  const options = this.options
515
- ?? Array.from(this.children).filter(o => Listbox.slottedOptionFilter(o));
516
- const selectedIndex = options?.findIndex(el => el.hasAttribute('selected')
517
- || el.selected
518
- || el.value === this.value);
553
+ ?? Array.from(this.children).filter(o => isNimbleListOption(o));
554
+ const optionIsSelected = (option) => {
555
+ return option.hasAttribute('selected') || option.selected;
556
+ };
557
+ const optionIsDisabled = (option) => {
558
+ return option.hasAttribute('disabled') || option.disabled;
559
+ };
560
+ let selectedIndex = -1;
561
+ let firstValidOptionIndex = -1;
562
+ for (let i = 0; i < options?.length; i++) {
563
+ const option = options[i];
564
+ if (optionIsSelected(option) || option?.value === this.value) {
565
+ selectedIndex = i;
566
+ }
567
+ if (firstValidOptionIndex === -1 && !optionIsDisabled(option)) {
568
+ firstValidOptionIndex = i;
569
+ }
570
+ }
519
571
  if (selectedIndex !== -1) {
520
572
  this.selectedIndex = selectedIndex;
521
- return;
522
573
  }
523
- this.selectedIndex = 0;
574
+ else if (firstValidOptionIndex !== -1) {
575
+ this.selectedIndex = firstValidOptionIndex;
576
+ }
577
+ else {
578
+ this.selectedIndex = 0;
579
+ }
580
+ this.committedSelectedOption = options[this.selectedIndex];
581
+ }
582
+ committedSelectedOptionChanged() {
583
+ this.updateDisplayValue();
524
584
  }
525
585
  setPositioning() {
526
586
  if (!this.$fastController.isConnected) {
@@ -556,15 +616,24 @@ export class Select extends FormAssociatedSelect {
556
616
  filterOptions() {
557
617
  const filter = this.filter.toLowerCase();
558
618
  if (filter) {
559
- this.filteredOptions = this._options.filter(option => {
560
- return diacriticInsensitiveStringNormalizer(option.text).includes(diacriticInsensitiveStringNormalizer(filter));
619
+ this.filteredOptions = this.options.filter(option => {
620
+ const normalizedFilter = diacriticInsensitiveStringNormalizer(filter);
621
+ return (!option.hidden
622
+ && diacriticInsensitiveStringNormalizer(option.text).includes(normalizedFilter));
561
623
  });
562
624
  }
563
625
  else {
564
- this.filteredOptions = this._options;
626
+ this.filteredOptions = this.options.filter(option => !option.hidden);
565
627
  }
566
- this._options.forEach(o => {
567
- o.hidden = !this.filteredOptions.includes(o);
628
+ this.options.forEach(o => {
629
+ if (isNimbleListOption(o)) {
630
+ if (!this.filteredOptions.includes(o)) {
631
+ o.visuallyHidden = true;
632
+ }
633
+ else {
634
+ o.visuallyHidden = false;
635
+ }
636
+ }
568
637
  });
569
638
  }
570
639
  /**
@@ -622,7 +691,7 @@ export class Select extends FormAssociatedSelect {
622
691
  this.ariaControls = '';
623
692
  return;
624
693
  }
625
- this.committedSelectedOption = this._options[this.selectedIndex];
694
+ this.committedSelectedOption = this.options[this.selectedIndex];
626
695
  this.ariaControls = this.listboxId;
627
696
  this.ariaExpanded = 'true';
628
697
  this.setPositioning();
@@ -633,24 +702,6 @@ export class Select extends FormAssociatedSelect {
633
702
  this.listbox.style.setProperty('--ni-private-select-max-height', `${this.maxHeight}px`);
634
703
  }
635
704
  }
636
- updateSelectedIndexFromFilteredSet() {
637
- const selectedItem = this.filteredOptions.length > 0
638
- ? this.options[this.selectedIndex]
639
- ?? this.committedSelectedOption
640
- : this.committedSelectedOption;
641
- if (!selectedItem) {
642
- return;
643
- }
644
- // Clear filter so any logic resolving against 'this.options' resolves against all options,
645
- // since selectedIndex should be relative to entire set.
646
- this.filter = '';
647
- // translate selectedIndex for filtered list to selectedIndex for all items
648
- this.selectedIndex = this._options.indexOf(selectedItem);
649
- // force selected to true again if the selection hasn't actually changed
650
- if (selectedItem === this.committedSelectedOption) {
651
- selectedItem.selected = true;
652
- }
653
- }
654
705
  }
655
706
  __decorate([
656
707
  attr
@@ -667,6 +718,9 @@ __decorate([
667
718
  __decorate([
668
719
  attr({ attribute: 'filter-mode' })
669
720
  ], Select.prototype, "filterMode", void 0);
721
+ __decorate([
722
+ observable
723
+ ], Select.prototype, "displayPlaceholder", void 0);
670
724
  __decorate([
671
725
  attr({ attribute: 'open', mode: 'boolean' })
672
726
  ], Select.prototype, "open", void 0);
@@ -703,6 +757,9 @@ __decorate([
703
757
  __decorate([
704
758
  volatile
705
759
  ], Select.prototype, "collapsible", null);
760
+ __decorate([
761
+ volatile
762
+ ], Select.prototype, "displayValue", null);
706
763
  const nimbleSelect = Select.compose({
707
764
  baseName: 'select',
708
765
  baseClass: FoundationSelect,