@momentum-design/components 0.94.0 → 0.95.1

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,14 +10,12 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  import { html, nothing } from 'lit';
11
11
  import { property, query, queryAssignedElements, state } from 'lit/decorators.js';
12
12
  import { ifDefined } from 'lit/directives/if-defined.js';
13
- import { live } from 'lit/directives/live.js';
14
13
  import { KEYS } from '../../utils/keys';
15
14
  import { DataAriaLabelMixin } from '../../utils/mixins/DataAriaLabelMixin';
16
15
  import { FormInternalsMixin } from '../../utils/mixins/FormInternalsMixin';
17
16
  import { ROLE } from '../../utils/roles';
18
17
  import FormfieldWrapper from '../formfieldwrapper/formfieldwrapper.component';
19
18
  import { DEFAULTS as FORMFIELD_DEFAULTS, VALIDATION } from '../formfieldwrapper/formfieldwrapper.constants';
20
- import { TAG_NAME as OPTION_GROUP_TAG_NAME } from '../optgroup/optgroup.constants';
21
19
  import { TAG_NAME as OPTION_TAG_NAME } from '../option/option.constants';
22
20
  import { POPOVER_PLACEMENT } from '../popover/popover.constants';
23
21
  import { TYPE, VALID_TEXT_TAGS } from '../text/text.constants';
@@ -29,6 +27,8 @@ import styles from './select.styles';
29
27
  * The component ensures accessibility and usability while handling various use cases,
30
28
  * including long text truncation with tooltip support.
31
29
  *
30
+ * Every mdc-option should have a `value` attribute set to ensure proper form submission.
31
+ *
32
32
  * To set a default option, use the `selected` attribute on the `mdc-option` element.
33
33
  *
34
34
  * **Note:** Make sure to add `mdc-selectlistbox` as a child of `mdc-select` and wrap options/optgroup in it to ensure proper accessibility functionality. Read more about it in SelectListBox documentation.
@@ -41,7 +41,7 @@ import styles from './select.styles';
41
41
  *
42
42
  * @tagname mdc-select
43
43
  *
44
- * @slot default - This is a default/unnamed slot for options and/or option group.
44
+ * @slot default - This is a default/unnamed slot for Selectlistbox including options and/or option group.
45
45
  *
46
46
  * @event click - (React: onClick) This event is dispatched when the select is clicked.
47
47
  * @event change - (React: onChange) This event is dispatched when the select is changed.
@@ -68,11 +68,68 @@ class Select extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)) {
68
68
  */
69
69
  this.placement = POPOVER_PLACEMENT.BOTTOM_START;
70
70
  /** @internal */
71
- this.baseIconName = ARROW_ICON.ARROW_DOWN;
72
- /** @internal */
73
- this.selectedValue = '';
74
- /** @internal */
75
71
  this.displayPopover = false;
72
+ /** @internal */
73
+ this.initialSelectedOption = null;
74
+ }
75
+ getAllValidOptions() {
76
+ var _a;
77
+ return Array.from(((_a = this.slottedListboxes[0]) === null || _a === void 0 ? void 0 : _a.querySelectorAll(OPTION_TAG_NAME)) || []);
78
+ }
79
+ getFirstValidOption() {
80
+ var _a;
81
+ return (_a = this.slottedListboxes[0]) === null || _a === void 0 ? void 0 : _a.querySelector(OPTION_TAG_NAME);
82
+ }
83
+ getLastValidOption() {
84
+ const options = this.getAllValidOptions();
85
+ return options.length > 0 ? options[options.length - 1] : null;
86
+ }
87
+ getFirstSelectedOption() {
88
+ var _a;
89
+ return (_a = this.slottedListboxes[0]) === null || _a === void 0 ? void 0 : _a.querySelector(`${OPTION_TAG_NAME}[selected]`);
90
+ }
91
+ /**
92
+ * Handles the first updated lifecycle event.
93
+ * If an option is selected, use that as the value.
94
+ * If not, use the placeholder if it exists, otherwise use the first option.
95
+ */
96
+ async firstUpdated() {
97
+ await this.updateComplete;
98
+ this.modifyListBoxWrapper();
99
+ const firstSelectedOption = this.getFirstSelectedOption();
100
+ if (firstSelectedOption) {
101
+ this.initialSelectedOption = firstSelectedOption;
102
+ // do not fire events when setting the selected value
103
+ // which is already selected in the DOM on first update
104
+ this.setSelectedOption(firstSelectedOption);
105
+ }
106
+ else if (!this.placeholder) {
107
+ const firstValidOption = this.getFirstValidOption();
108
+ // We will show the first option as selected & fire
109
+ // and event since the selected option changed
110
+ this.setSelectedOption(firstValidOption);
111
+ this.fireEvents();
112
+ }
113
+ else if (this.placeholder) {
114
+ // If there is no default selected option
115
+ // then we call the native validity
116
+ this.setInputValidity();
117
+ }
118
+ }
119
+ updated(changedProperties) {
120
+ super.updated(changedProperties);
121
+ if (changedProperties.has('disabled') ||
122
+ changedProperties.has('softDisabled') ||
123
+ changedProperties.has('readonly')) {
124
+ if (this.disabled || this.softDisabled || this.readonly) {
125
+ // If the select is disabled, soft-disabled or readonly,
126
+ // we close the popover if it is open.
127
+ this.displayPopover = false;
128
+ }
129
+ }
130
+ if (changedProperties.has('dataAriaLabel')) {
131
+ this.modifyListBoxWrapper();
132
+ }
76
133
  }
77
134
  /**
78
135
  * Modifies the listbox wrapper to ensure it has the correct attributes
@@ -92,189 +149,143 @@ class Select extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)) {
92
149
  slottedListBox.setAttribute('aria-labelledby', TRIGGER_ID);
93
150
  }
94
151
  /**
95
- * A helper function which returns a flattened array of all valid options from within the slotted listbox.
96
- * It takes care of the edge cases where the option is either a direct child or a
97
- * child of an option group.
152
+ * A private method which is called when an option is clicked.
153
+ * It sets the selected option, removes selected from other options, updates the tabindex for all options,
154
+ * closes the popover, and fires the change and input events.
155
+ * @param event - The event which triggered this function.
98
156
  */
99
- getAllValidOptions() {
100
- var _a;
101
- const optionsList = Array.from(((_a = this.slottedListboxes[0]) === null || _a === void 0 ? void 0 : _a.children) || []);
102
- return ((optionsList === null || optionsList === void 0 ? void 0 : optionsList.map(option => {
103
- if (option.tagName.toLowerCase() === OPTION_TAG_NAME) {
104
- return option;
105
- }
106
- if (option.tagName.toLowerCase() === OPTION_GROUP_TAG_NAME) {
107
- return Array.from(option.children).filter(optgroup => optgroup.tagName.toLowerCase() === OPTION_TAG_NAME);
108
- }
109
- return [];
110
- }).flat()) || []);
157
+ handleOptionsClick(event) {
158
+ this.setSelectedOption(event.target);
159
+ this.displayPopover = false;
160
+ this.fireEvents();
111
161
  }
112
162
  /**
113
- * Updates the tabindex and selected attribute of the options.
114
- * If selectedOption is provided, it will be set as the selected option.
115
- * Otherwise, it will set the first option as the selected option.
116
- * @param selectedOption - The option which should be selected.
163
+ * Sets the selected option in the component state and updates the input element's value.
164
+ * This method ensures that only the selected option is marked as selected in the DOM,
165
+ * and updates the tabindex for all options accordingly.
166
+ * It also updates the validity of the input element based on the selected option.
167
+ * This method is called when an option is selected.
168
+ *
169
+ * @param option - The option element in DOM which gets selected.
117
170
  */
118
- updateTabIndexForAllOptions(selectedOption) {
119
- var _a;
120
- let isTabIndexSet = false;
121
- this.getAllValidOptions().forEach(option => {
122
- if (option === selectedOption) {
123
- this.setSelectedValue(option);
124
- isTabIndexSet = true;
125
- option.setAttribute('selected', '');
126
- option.setAttribute('tabindex', '0');
127
- }
128
- else {
129
- option === null || option === void 0 ? void 0 : option.setAttribute('tabindex', '-1');
130
- option === null || option === void 0 ? void 0 : option.removeAttribute('selected');
131
- }
171
+ setSelectedOption(option) {
172
+ var _a, _b;
173
+ // set the attribute 'selected' on the option in HTML and remove it from others
174
+ this.updateSelectedInChildOptions(option);
175
+ // update the tabindex for all options
176
+ this.updateTabIndexForAllOptions(option);
177
+ // set the selected option in the component state
178
+ this.selectedOption = option;
179
+ // update all form related values
180
+ this.value = (_b = (_a = this.selectedOption) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '';
181
+ this.internals.setFormValue(this.value);
182
+ this.inputElement.setAttribute('value', this.value);
183
+ this.setInputValidity();
184
+ }
185
+ /**
186
+ * Updates the tabindex of all options.
187
+ * Sets the tabindex of the selected option to '0' and others to '-1'.
188
+ *
189
+ * @param option - The option which tabIndex should be set to 0.
190
+ */
191
+ updateTabIndexForAllOptions(option) {
192
+ const options = this.getAllValidOptions();
193
+ const optionToGetTabIndex0 = option || options[0];
194
+ options.forEach(option => {
195
+ option.setAttribute('tabindex', option === optionToGetTabIndex0 ? '0' : '-1');
132
196
  });
133
- if (!isTabIndexSet) {
134
- // if no option is selected, set the first option as focused
135
- (_a = this.getAllValidOptions()[0]) === null || _a === void 0 ? void 0 : _a.setAttribute('tabindex', '0');
136
- }
137
197
  }
138
198
  /**
139
- * A private method which is called when an option is clicked.
140
- * It is used to update the tabindex and selected attribute of the options.
141
- * @param event - The event which triggered this function.
199
+ * Sets selected attribute on the selected option and removes it from all options
200
+ * @param selectedOption - The option which gets selected
142
201
  */
143
- handleOptionsClick(event) {
144
- this.updateTabIndexForAllOptions(event.target);
202
+ updateSelectedInChildOptions(selectedOption) {
203
+ selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.setAttribute('selected', 'true');
204
+ const options = this.getAllValidOptions();
205
+ options.forEach(option => {
206
+ if (option !== selectedOption) {
207
+ option.removeAttribute('selected');
208
+ }
209
+ });
145
210
  }
146
211
  /**
147
- * Sets the selected value based on the provided option element.
148
- * It retrieves the 'label' attribute of the option, if present,
149
- * otherwise it falls back to the option's text content.
150
- * @param option - The option element from which to set the selected value.
212
+ * A private method which is called to fire the change and input events.
213
+ * It dispatches the input and change events with the selected option's value and label.
151
214
  */
152
- setSelectedValue(option) {
153
- var _a, _b, _c, _d;
154
- this.selectedValueText = (_b = (_a = option === null || option === void 0 ? void 0 : option.getAttribute('label')) !== null && _a !== void 0 ? _a : option === null || option === void 0 ? void 0 : option.textContent) !== null && _b !== void 0 ? _b : '';
155
- this.selectedIcon = option === null || option === void 0 ? void 0 : option.getAttribute('prefix-icon');
156
- this.selectedValue = (_d = (_c = option === null || option === void 0 ? void 0 : option.getAttribute('value')) !== null && _c !== void 0 ? _c : option === null || option === void 0 ? void 0 : option.textContent) !== null && _d !== void 0 ? _d : '';
157
- this.value = this.selectedValue;
158
- // Set form value
159
- this.internals.setFormValue(this.selectedValue);
160
- this.manageRequired();
161
- // dispatch a change event when a value is selected
162
- this.dispatchInput(this.selectedValue);
163
- this.dispatchChange(this.selectedValue);
215
+ fireEvents() {
216
+ this.dispatchInput(this.selectedOption);
217
+ this.dispatchChange(this.selectedOption);
164
218
  }
165
219
  /**
166
- * Manages the required state of the select.
167
- * If the select is required and no value is selected,
168
- * it sets a custom validity message based on the validationMessage property.
169
- * If the select is not required or a value is selected, it clears the custom validity.
170
- * This method is called to ensure that the select behaves correctly in forms.
220
+ * Sets the validity of the input element based on the selected option.
221
+ * If the selected option is not set and the select is required,
222
+ * it sets a custom validation message.
223
+ * If the selected option is set or the select is not required,
224
+ * it clears the custom validation message.
225
+ * This method is called to ensure that the select component behaves correctly
226
+ * in form validation scenarios, especially when the select is required.
171
227
  * @internal
172
228
  */
173
- manageRequired() {
174
- if (!this.selectedValue && this.required) {
175
- if (this.validationMessage) {
176
- this.inputElement.setCustomValidity(this.validationMessage);
177
- }
178
- else {
179
- this.inputElement.setCustomValidity('');
180
- }
229
+ setInputValidity() {
230
+ if (!this.selectedOption && this.required && this.validationMessage) {
231
+ this.inputElement.setCustomValidity(this.validationMessage);
232
+ }
233
+ else {
234
+ this.inputElement.setCustomValidity('');
181
235
  }
182
236
  this.setValidity();
183
237
  }
184
238
  /**
239
+ * Resets the select to its initially selected option.
185
240
  * @internal
186
- * Resets the select to its initial state.
187
241
  */
188
242
  formResetCallback() {
189
- this.selectedValue = '';
190
- this.selectedValueText = undefined;
191
- this.selectedIcon = null;
192
- this.internals.setFormValue(this.selectedValue);
193
- this.updateTabIndexForAllOptions();
194
- this.requestUpdate();
243
+ var _a;
244
+ const optionToResetTo = this.initialSelectedOption || null;
245
+ if (((_a = this.selectedOption) === null || _a === void 0 ? void 0 : _a.value) !== (optionToResetTo === null || optionToResetTo === void 0 ? void 0 : optionToResetTo.value)) {
246
+ this.setSelectedOption(optionToResetTo);
247
+ // fire events to notify the change in case of reset
248
+ this.fireEvents();
249
+ }
195
250
  }
196
251
  /** @internal */
197
252
  formStateRestoreCallback(state) {
198
- this.selectedValue = state;
199
- this.selectedValueText = state;
200
- }
201
- dispatchChange(value) {
202
- if (!value) {
203
- return;
253
+ var _a;
254
+ const optionToRestoreTo = this.getAllValidOptions().find(option => option.value === state || option.label === state);
255
+ if (((_a = this.selectedOption) === null || _a === void 0 ? void 0 : _a.value) !== (optionToRestoreTo === null || optionToRestoreTo === void 0 ? void 0 : optionToRestoreTo.value)) {
256
+ this.setSelectedOption(optionToRestoreTo || null);
257
+ // fire events to notify the change in case of restore
258
+ this.fireEvents();
204
259
  }
260
+ }
261
+ dispatchChange(option) {
205
262
  this.dispatchEvent(new CustomEvent('change', {
206
- detail: { value },
263
+ detail: { value: option === null || option === void 0 ? void 0 : option.value, label: option === null || option === void 0 ? void 0 : option.label },
207
264
  composed: true,
208
265
  bubbles: true,
209
266
  }));
210
267
  }
211
- dispatchInput(value) {
212
- if (!value) {
213
- return;
214
- }
268
+ dispatchInput(option) {
215
269
  this.dispatchEvent(new CustomEvent('input', {
216
- detail: { value },
270
+ detail: { value: option === null || option === void 0 ? void 0 : option.value, label: option === null || option === void 0 ? void 0 : option.label },
217
271
  composed: true,
218
272
  bubbles: true,
219
273
  }));
220
274
  }
221
275
  /**
222
- * Handles the keydown event on the select element when the popover is open.
223
- * The options are as follows:
224
- * - SPACE or ENTER: Selects the currently active option and closes the popover.
225
- * - HOME: Sets focus and tabindex on the first option.
226
- * - END: Sets focus and tabindex on the last option.
227
- * - ARROW_DOWN, ARROW_UP, PAGE_DOWN, PAGE_UP: Handles navigation between options.
228
- * @param event - The keyboard event.
276
+ * Handles the click event on the visual combobox.
277
+ * If the select is disabled, soft-disabled or readonly, it does nothing.
278
+ * If the popover is already open, it closes it.
279
+ * If it is closed, it opens it.
280
+ * @param event - The mouse event which triggered this function.
229
281
  */
230
- handlePopoverOnOpen(event) {
231
- var _a;
232
- switch (event.key) {
233
- case KEYS.TAB: {
234
- const focusedOptionIndex = this.getAllValidOptions().findIndex(option => option === event.target);
235
- this.setFocusAndTabIndex(focusedOptionIndex);
236
- event.preventDefault();
237
- break;
238
- }
239
- case KEYS.SPACE:
240
- this.closePopover();
241
- event.preventDefault();
242
- break;
243
- case KEYS.ENTER:
244
- this.closePopover();
245
- event.preventDefault();
246
- // if the popover is closed, then we submit the form.
247
- (_a = this.form) === null || _a === void 0 ? void 0 : _a.requestSubmit();
248
- break;
249
- case KEYS.HOME:
250
- this.setFocusAndTabIndex(0);
251
- event.preventDefault();
252
- break;
253
- case KEYS.END:
254
- this.setFocusAndTabIndex(this.getAllValidOptions().length - 1);
255
- event.preventDefault();
256
- break;
257
- case KEYS.ARROW_DOWN:
258
- case KEYS.ARROW_UP:
259
- case KEYS.PAGE_DOWN:
260
- case KEYS.PAGE_UP:
261
- this.handleOptionsNavigation(event);
262
- event.preventDefault();
263
- break;
264
- default:
265
- break;
266
- }
267
- }
268
282
  handleClickCombobox(event) {
269
283
  if (this.disabled || this.softDisabled || this.readonly) {
270
284
  return;
271
285
  }
272
- if (this.displayPopover) {
273
- this.closePopover();
274
- }
275
- else {
276
- this.openPopover();
277
- }
286
+ // If the popover is already open, we close it.
287
+ // If it is closed, we open it.
288
+ this.displayPopover = !this.displayPopover;
278
289
  event.stopPropagation();
279
290
  }
280
291
  /**
@@ -293,173 +304,126 @@ class Select extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)) {
293
304
  switch (event.key) {
294
305
  case KEYS.ARROW_DOWN:
295
306
  case KEYS.ARROW_UP:
296
- this.openPopover();
307
+ this.displayPopover = true;
297
308
  // Prevent the default browser behavior of scrolling down
298
309
  event.preventDefault();
299
310
  break;
300
311
  case KEYS.ENTER:
301
312
  case KEYS.SPACE:
302
- this.openPopover();
313
+ this.displayPopover = true;
303
314
  // Prevent the default browser behavior of scrolling down
304
315
  event.preventDefault();
305
316
  event.stopPropagation();
306
317
  break;
307
- case KEYS.HOME:
308
- this.openPopover();
309
- this.setFocusAndTabIndex(0);
318
+ case KEYS.HOME: {
319
+ this.displayPopover = true;
320
+ const firstOption = this.getFirstValidOption();
321
+ if (firstOption) {
322
+ firstOption === null || firstOption === void 0 ? void 0 : firstOption.focus();
323
+ this.updateTabIndexForAllOptions(firstOption);
324
+ }
310
325
  event.preventDefault();
311
326
  break;
312
- case KEYS.END:
313
- this.openPopover();
314
- this.setFocusAndTabIndex(this.getAllValidOptions().length - 1);
327
+ }
328
+ case KEYS.END: {
329
+ this.displayPopover = true;
330
+ const lastOption = this.getLastValidOption();
331
+ if (lastOption) {
332
+ lastOption.focus();
333
+ this.updateTabIndexForAllOptions(lastOption);
334
+ }
315
335
  event.preventDefault();
316
336
  break;
337
+ }
317
338
  default:
318
339
  break;
319
340
  }
320
341
  }
321
342
  /**
322
- * Handles the navigation of options when the user presses
323
- * ArrowUp, ArrowDown, PageUp, or PageDown keys.
324
- * @param event - The keyboard event that triggered the navigation.
343
+ * Handles the keydown event on the select element when the popover is open.
344
+ * The options are as follows:
345
+ * - HOME: Sets focus and tabindex on the first option.
346
+ * - END: Sets focus and tabindex on the last option.
347
+ * - ARROW_DOWN, ARROW_UP, PAGE_DOWN, PAGE_UP: Handles navigation between options.
348
+ * @param event - The keyboard event.
325
349
  */
326
- handleOptionsNavigation(event) {
327
- const options = this.getAllValidOptions();
328
- const currentIndex = options.findIndex(option => option === event.target);
329
- const newIndex = this.getNewIndexBasedOnKey(event.key, currentIndex, options.length);
330
- if (newIndex !== -1) {
331
- this.setFocusAndTabIndex(newIndex);
332
- // Prevent the default browser behavior of scrolling down
333
- // when pressing ArrowUp, ArrowDown, PageUp, or PageDown keys
334
- event.preventDefault();
350
+ handlePopoverKeydown(event) {
351
+ switch (event.key) {
352
+ case KEYS.HOME: {
353
+ const firstOption = this.getFirstValidOption();
354
+ this.focusAndUpdateTabIndexes(firstOption);
355
+ event.preventDefault();
356
+ break;
357
+ }
358
+ case KEYS.END: {
359
+ const lastOption = this.getLastValidOption();
360
+ this.focusAndUpdateTabIndexes(lastOption);
361
+ event.preventDefault();
362
+ break;
363
+ }
364
+ case KEYS.ARROW_DOWN: {
365
+ const options = this.getAllValidOptions();
366
+ const currentIndex = options.findIndex(option => option === event.target);
367
+ const newIndex = Math.min(currentIndex + 1, options.length - 1);
368
+ this.focusAndUpdateTabIndexes(options[newIndex]);
369
+ event.preventDefault();
370
+ break;
371
+ }
372
+ case KEYS.ARROW_UP: {
373
+ const options = this.getAllValidOptions();
374
+ const currentIndex = options.findIndex(option => option === event.target);
375
+ const newIndex = Math.max(currentIndex - 1, 0);
376
+ this.focusAndUpdateTabIndexes(options[newIndex]);
377
+ event.preventDefault();
378
+ break;
379
+ }
380
+ case KEYS.PAGE_DOWN: {
381
+ const options = this.getAllValidOptions();
382
+ const currentIndex = options.findIndex(option => option === event.target);
383
+ const newIndex = Math.min(currentIndex + 10, options.length - 1);
384
+ this.focusAndUpdateTabIndexes(options[newIndex]);
385
+ event.preventDefault();
386
+ break;
387
+ }
388
+ case KEYS.PAGE_UP: {
389
+ const options = this.getAllValidOptions();
390
+ const currentIndex = options.findIndex(option => option === event.target);
391
+ const newIndex = Math.max(currentIndex - 10, 0);
392
+ this.focusAndUpdateTabIndexes(options[newIndex]);
393
+ event.preventDefault();
394
+ break;
395
+ }
396
+ default:
397
+ break;
335
398
  }
336
399
  }
337
400
  /**
338
- * Calculates the new index based on the pressed navigation key.
339
- * Supports ArrowUp, ArrowDown, PageUp, and PageDown keys for navigating options.
340
- * - ArrowDown: Moves focus to the next option, if available.
341
- * - ArrowUp: Moves focus to the previous option, if available.
342
- * - PageDown: Moves focus 10 options down or to the last option.
343
- * - PageUp: Moves focus 10 options up or to the first option.
344
- *
345
- * @param key - The navigation key that was pressed.
346
- * @param currentIndex - The current index of the focused option.
347
- * @param optionsLength - The total number of options.
348
- * @returns The new index to focus on, or -1 if no movement is possible.
401
+ * Focuses the given option and updates the tabindex for all options.
402
+ * @param option - The option to focus.
403
+ * @internal
349
404
  */
350
- getNewIndexBasedOnKey(key, currentIndex, optionsLength) {
351
- if (key === KEYS.ARROW_DOWN && currentIndex !== optionsLength - 1) {
352
- return currentIndex + 1;
353
- }
354
- if (key === KEYS.ARROW_UP && currentIndex > 0) {
355
- return currentIndex - 1;
356
- }
357
- if (key === KEYS.PAGE_DOWN) {
358
- // Jumps visual focus down 10 options (or to last option).
359
- return currentIndex + 10 > optionsLength ? optionsLength - 1 : currentIndex + 10;
360
- }
361
- if (key === KEYS.PAGE_UP) {
362
- // Jumps visual focus up 10 options (or to first option).
363
- return currentIndex - 10 < 0 ? 0 : currentIndex - 10;
405
+ focusAndUpdateTabIndexes(option) {
406
+ if (option) {
407
+ option.focus();
408
+ this.updateTabIndexForAllOptions(option);
364
409
  }
365
- return -1;
366
- }
367
- setFocusAndTabIndex(newIndex) {
368
- const options = this.getAllValidOptions();
369
- const targetOption = options[newIndex];
370
- if (targetOption) {
371
- targetOption.focus();
372
- options.forEach((node, index) => {
373
- const newTabindex = newIndex === index ? '0' : '-1';
374
- node === null || node === void 0 ? void 0 : node.setAttribute('tabindex', newTabindex);
375
- });
376
- }
377
- }
378
- openPopover() {
379
- this.displayPopover = true;
380
- this.baseIconName = ARROW_ICON.ARROW_UP;
381
- }
382
- closePopover() {
383
- this.displayPopover = false;
384
- this.baseIconName = ARROW_ICON.ARROW_DOWN;
385
410
  }
386
411
  /**
387
- * Handles the first updated lifecycle event.
388
- * If an option is selected, use that as the value.
389
- * If not, use the placeholder if it exists, otherwise use the first option.
412
+ * If the native input is focused, it will focus the visual combobox.
413
+ * This is to ensure that the visual combobox is focused when the native input is focused.
414
+ * For example when a form is submitted and the native input is focused,
415
+ * we want to focus the visual combobox so that the user can see the selected option
416
+ * and can interact with it.
417
+ * @internal
390
418
  */
391
- async firstUpdated() {
392
- await this.updateComplete;
393
- this.modifyListBoxWrapper();
394
- const options = this.getAllValidOptions();
395
- const selectedOptionIndex = options.findIndex(option => option === null || option === void 0 ? void 0 : option.hasAttribute('selected'));
396
- if (selectedOptionIndex !== -1) {
397
- this.setSelectedValue(options[selectedOptionIndex]);
398
- this.updateTabIndexForAllOptions(options[selectedOptionIndex]);
399
- }
400
- else if (!this.placeholder) {
401
- // We will show the first option as selected.
402
- this.setSelectedValue(options[0]);
403
- this.updateTabIndexForAllOptions();
404
- }
405
- else if (this.placeholder) {
406
- // If there is no default selected option
407
- // then we set the placeholder and call the native validity
408
- this.manageRequired();
409
- }
410
- }
411
- updated(changedProperties) {
412
- super.updated(changedProperties);
413
- if (changedProperties.has('disabled') ||
414
- changedProperties.has('softDisabled') ||
415
- changedProperties.has('readonly')) {
416
- if (this.disabled || this.softDisabled || this.readonly) {
417
- this.closePopover();
418
- }
419
- }
420
- if (changedProperties.has('dataAriaLabel')) {
421
- this.modifyListBoxWrapper();
422
- }
423
- }
424
- handleOnChange() {
425
- this.selectedValue = this.inputElement.value;
426
- this.internals.setFormValue(this.selectedValue);
419
+ handleNativeInputFocus() {
420
+ this.visualCombobox.focus();
427
421
  }
428
422
  render() {
429
- var _a, _b;
423
+ var _a, _b, _c, _d, _e, _f;
430
424
  return html `
431
425
  ${this.renderLabel()}
432
426
  <div part="container">
433
- <select
434
- part="native-select"
435
- id="${this.id}"
436
- tabindex="-1"
437
- aria-hidden="true"
438
- name="${this.name}"
439
- size="1"
440
- .value="${live(this.selectedValue)}"
441
- ?autofocus="${this.autofocus}"
442
- ?disabled="${this.disabled}"
443
- ?required="${this.required}"
444
- aria-disabled="${ifDefined(this.disabled || this.softDisabled)}"
445
- @change="${this.handleOnChange}"
446
- @mousedown="${(event) => event.preventDefault()}"
447
- >
448
- ${this.getAllValidOptions().map(option => {
449
- var _a, _b;
450
- return html `
451
- <option
452
- part="native-select"
453
- value="${(_a = option.getAttribute('value')) !== null && _a !== void 0 ? _a : ''}"
454
- label="${(_b = option.getAttribute('label')) !== null && _b !== void 0 ? _b : ''}"
455
- ?disabled="${!!option.hasAttribute('disabled')}"
456
- ?selected="${!!option.hasAttribute('selected')}"
457
- >
458
- ${option.textContent}
459
- </option>
460
- `;
461
- })}
462
- </select>
463
427
  <div
464
428
  id="${TRIGGER_ID}"
465
429
  part="base-container"
@@ -478,24 +442,48 @@ class Select extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)) {
478
442
  aria-disabled="${ifDefined(this.disabled || this.softDisabled)}"
479
443
  aria-readonly="${ifDefined(this.readonly)}"
480
444
  >
481
- ${this.selectedIcon
482
- ? html `<mdc-icon length-unit="rem" size="1" name="${this.selectedIcon}" part="selected-icon"></mdc-icon>`
445
+ ${((_b = this.selectedOption) === null || _b === void 0 ? void 0 : _b.prefixIcon)
446
+ ? html `<mdc-icon
447
+ length-unit="rem"
448
+ size="1"
449
+ name="${(_c = this.selectedOption) === null || _c === void 0 ? void 0 : _c.prefixIcon}"
450
+ part="selected-icon"
451
+ ></mdc-icon>`
483
452
  : nothing}
484
453
  <mdc-text
485
- part="base-text ${this.selectedValueText ? 'selected' : ''}"
454
+ part="base-text ${((_d = this.selectedOption) === null || _d === void 0 ? void 0 : _d.label) ? 'selected' : ''}"
486
455
  type="${TYPE.BODY_MIDSIZE_REGULAR}"
487
456
  tagname="${VALID_TEXT_TAGS.SPAN}"
488
457
  >
489
- ${(_b = this.selectedValueText) !== null && _b !== void 0 ? _b : this.placeholder}
458
+ ${(_f = (_e = this.selectedOption) === null || _e === void 0 ? void 0 : _e.label) !== null && _f !== void 0 ? _f : this.placeholder}
490
459
  </mdc-text>
491
460
  <div part="icon-container">
492
- <mdc-icon size="1" length-unit="rem" name="${this.baseIconName}"></mdc-icon>
461
+ <mdc-icon
462
+ size="1"
463
+ length-unit="rem"
464
+ name="${this.displayPopover ? ARROW_ICON.ARROW_UP : ARROW_ICON.ARROW_DOWN}"
465
+ ></mdc-icon>
493
466
  </div>
494
467
  </div>
468
+ <input
469
+ id="${this.id}"
470
+ part="native-input"
471
+ name="${this.name}"
472
+ type="text"
473
+ ?autofocus="${this.autofocus}"
474
+ ?disabled=${this.disabled}
475
+ ?required=${this.required}
476
+ ?readonly=${this.readonly}
477
+ tabindex="-1"
478
+ aria-hidden="true"
479
+ @focus=${this.handleNativeInputFocus}
480
+ @invalid=${this.setInputValidity}
481
+ aria-disabled="${ifDefined(this.disabled || this.softDisabled)}"
482
+ />
495
483
  <mdc-popover
496
484
  trigger="manual"
497
485
  triggerid="${TRIGGER_ID}"
498
- @keydown="${this.handlePopoverOnOpen}"
486
+ @keydown="${this.handlePopoverKeydown}"
499
487
  interactive
500
488
  ?visible="${this.displayPopover}"
501
489
  role=""
@@ -505,8 +493,12 @@ class Select extends FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)) {
505
493
  focus-trap
506
494
  size
507
495
  placement="${this.placement}"
508
- @closebyescape="${this.closePopover}"
509
- @closebyoutsideclick="${this.closePopover}"
496
+ @closebyescape="${() => {
497
+ this.displayPopover = false;
498
+ }}"
499
+ @closebyoutsideclick="${() => {
500
+ this.displayPopover = false;
501
+ }}"
510
502
  style="--mdc-popover-max-width: 100%; --mdc-popover-max-height: ${this.height};"
511
503
  >
512
504
  <slot @click="${this.handleOptionsClick}"></slot>
@@ -542,27 +534,15 @@ __decorate([
542
534
  __metadata("design:type", Array)
543
535
  ], Select.prototype, "slottedListboxes", void 0);
544
536
  __decorate([
545
- state(),
546
- __metadata("design:type", String)
547
- ], Select.prototype, "baseIconName", void 0);
548
- __decorate([
549
- state(),
550
- __metadata("design:type", String)
551
- ], Select.prototype, "selectedValueText", void 0);
552
- __decorate([
553
- state(),
554
- __metadata("design:type", Object)
555
- ], Select.prototype, "selectedIcon", void 0);
537
+ query(`[id="${TRIGGER_ID}"]`),
538
+ __metadata("design:type", HTMLDivElement)
539
+ ], Select.prototype, "visualCombobox", void 0);
556
540
  __decorate([
557
541
  state(),
558
542
  __metadata("design:type", Object)
559
- ], Select.prototype, "selectedValue", void 0);
543
+ ], Select.prototype, "selectedOption", void 0);
560
544
  __decorate([
561
545
  state(),
562
546
  __metadata("design:type", Object)
563
547
  ], Select.prototype, "displayPopover", void 0);
564
- __decorate([
565
- query('select'),
566
- __metadata("design:type", HTMLInputElement)
567
- ], Select.prototype, "inputElement", void 0);
568
548
  export default Select;