@momentum-design/components 0.122.17 → 0.122.18
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/browser/index.js +205 -204
- package/dist/browser/index.js.map +3 -3
- package/dist/components/select/select.component.d.ts +33 -3
- package/dist/components/select/select.component.js +145 -45
- package/dist/custom-elements.json +2954 -2820
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +3 -3
- package/package.json +1 -1
|
@@ -66,6 +66,8 @@ declare const Select_base: import("../../utils/mixins/index.types").Constructor<
|
|
|
66
66
|
* @cssproperty --mdc-select-listbox-width - The width of the listbox inside the select (default: `--mdc-select-width`).
|
|
67
67
|
*/
|
|
68
68
|
declare class Select extends Select_base implements AssociatedFormControl {
|
|
69
|
+
/** @internal */
|
|
70
|
+
private itemsStore;
|
|
69
71
|
/**
|
|
70
72
|
* The placeholder text which will be shown on the text if provided.
|
|
71
73
|
*/
|
|
@@ -126,11 +128,18 @@ declare class Select extends Select_base implements AssociatedFormControl {
|
|
|
126
128
|
/** @internal */
|
|
127
129
|
displayPopover: boolean;
|
|
128
130
|
/** @internal */
|
|
131
|
+
private animationFrameId?;
|
|
132
|
+
/** @internal */
|
|
129
133
|
private initialSelectedOption;
|
|
130
134
|
/** @internal */
|
|
131
|
-
private
|
|
135
|
+
private debounceSearch?;
|
|
136
|
+
/** @internal */
|
|
137
|
+
private debounceTime;
|
|
138
|
+
/** @internal */
|
|
139
|
+
private searchString;
|
|
132
140
|
constructor();
|
|
133
141
|
connectedCallback(): void;
|
|
142
|
+
disconnectedCallback(): void;
|
|
134
143
|
/** @internal */
|
|
135
144
|
get navItems(): Option[];
|
|
136
145
|
/**
|
|
@@ -218,16 +227,35 @@ declare class Select extends Select_base implements AssociatedFormControl {
|
|
|
218
227
|
* @param event - The mouse event which triggered this function.
|
|
219
228
|
*/
|
|
220
229
|
private handleClickCombobox;
|
|
230
|
+
private setupDebounceSearch;
|
|
231
|
+
private debounceSearchKey;
|
|
232
|
+
/**
|
|
233
|
+
* Filters the given option labels based on the given search key.
|
|
234
|
+
* It returns a new array of options that have labels starting with the given search key case-insensitive.
|
|
235
|
+
*
|
|
236
|
+
* @param options - The options to filter.
|
|
237
|
+
* @param searchKey - The search key to filter by.
|
|
238
|
+
* @returns The filtered options.
|
|
239
|
+
*/
|
|
240
|
+
private filterOptionsBySearchKey;
|
|
241
|
+
/**
|
|
242
|
+
* Handles the selection of an option based on the filter string.
|
|
243
|
+
* It will select the first option from the filtered list if it is not empty.
|
|
244
|
+
* If the filtered list is empty, it will do nothing.
|
|
245
|
+
* @param searchKey - The filter string to search for options.
|
|
246
|
+
*/
|
|
247
|
+
private handleSelectedOptionBasedOnFilter;
|
|
221
248
|
/**
|
|
222
249
|
* Handles the keydown event on the select element when the popover is closed.
|
|
223
250
|
* The options are as follows:
|
|
224
|
-
* - ARROW_DOWN, ARROW_UP, SPACE: Opens the popover and prevents the default scrolling behavior.
|
|
225
|
-
* - ENTER: Opens the popover, prevents default scrolling, and submits the form if the popover is closed.
|
|
251
|
+
* - ARROW_DOWN, ARROW_UP, ENTER, SPACE: Opens the popover and prevents the default scrolling behavior.
|
|
226
252
|
* - HOME: Opens the popover and sets focus and tabindex on the first option.
|
|
227
253
|
* - END: Opens the popover and sets focus and tabindex on the last option.
|
|
254
|
+
* - Any key: Opens the popover and sets focus on the first option which starts with the key.
|
|
228
255
|
* @param event - The keyboard event.
|
|
229
256
|
*/
|
|
230
257
|
private handleKeydownCombobox;
|
|
258
|
+
private resetTabIndexAndSetFocusAfterUpdate;
|
|
231
259
|
/**
|
|
232
260
|
* If the native input is focused, it will focus the visual combobox.
|
|
233
261
|
* This is to ensure that the visual combobox is focused when the native input is focused.
|
|
@@ -237,6 +265,8 @@ declare class Select extends Select_base implements AssociatedFormControl {
|
|
|
237
265
|
* @internal
|
|
238
266
|
*/
|
|
239
267
|
private handleNativeInputFocus;
|
|
268
|
+
private handleSelectedOptionByKeyInput;
|
|
269
|
+
private handleKeydownPopover;
|
|
240
270
|
render(): import("lit-html").TemplateResult<1>;
|
|
241
271
|
static styles: Array<CSSResult>;
|
|
242
272
|
}
|
|
@@ -24,6 +24,7 @@ import { DEFAULTS as FORMFIELD_DEFAULTS, VALIDATION } from '../formfieldwrapper/
|
|
|
24
24
|
import { TAG_NAME as OPTION_TAG_NAME } from '../option/option.constants';
|
|
25
25
|
import { DEFAULTS as POPOVER_DEFAULTS, POPOVER_PLACEMENT } from '../popover/popover.constants';
|
|
26
26
|
import { TYPE, VALID_TEXT_TAGS } from '../text/text.constants';
|
|
27
|
+
import { debounce } from '../../utils/debounce';
|
|
27
28
|
import { ARROW_ICON, LISTBOX_ID, TRIGGER_ID } from './select.constants';
|
|
28
29
|
import styles from './select.styles';
|
|
29
30
|
/**
|
|
@@ -136,16 +137,68 @@ class Select extends ListNavigationMixin(CaptureDestroyEventForChildElement(Auto
|
|
|
136
137
|
/** @internal */
|
|
137
138
|
this.initialSelectedOption = null;
|
|
138
139
|
/** @internal */
|
|
140
|
+
this.debounceTime = 500;
|
|
141
|
+
/** @internal */
|
|
142
|
+
this.searchString = '';
|
|
143
|
+
/** @internal */
|
|
144
|
+
this.onStoreUpdate = (option, changeType, index, options) => {
|
|
145
|
+
switch (changeType) {
|
|
146
|
+
case 'added':
|
|
147
|
+
option.setAttribute('tabindex', '-1');
|
|
148
|
+
break;
|
|
149
|
+
case 'removed': {
|
|
150
|
+
if (index === -1 || options.length === 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
let newIndex = index + 1;
|
|
154
|
+
if (newIndex >= options.length) {
|
|
155
|
+
newIndex = index - 1;
|
|
156
|
+
}
|
|
157
|
+
if (newIndex === -1 && this.displayPopover) {
|
|
158
|
+
this.displayPopover = false;
|
|
159
|
+
this.handleNativeInputFocus();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (option.tabIndex === 0) {
|
|
163
|
+
this.resetTabIndexes(newIndex);
|
|
164
|
+
}
|
|
165
|
+
if (option.hasAttribute('selected')) {
|
|
166
|
+
let newOption = null;
|
|
167
|
+
// If there is no placeholder, then we set the first option as selected option.
|
|
168
|
+
// If the the first option is about to removed then we set the next (second) option as selected.
|
|
169
|
+
// The next (second) option will become first one, when the option is fully removed.
|
|
170
|
+
if (!this.placeholder) {
|
|
171
|
+
newOption = index === 0 ? options[newIndex] : options[0];
|
|
172
|
+
}
|
|
173
|
+
this.setSelectedOption(newOption);
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/** @internal */
|
|
182
|
+
this.isValidItem = (item) => item.matches(`${OPTION_TAG_NAME}:not([disabled])`);
|
|
183
|
+
this.addEventListener(LIFE_CYCLE_EVENTS.MODIFIED, this.handleModifiedEvent);
|
|
139
184
|
this.itemsStore = new ElementStore(this, {
|
|
140
185
|
isValidItem: this.isValidItem,
|
|
141
186
|
onStoreUpdate: this.onStoreUpdate,
|
|
142
187
|
});
|
|
143
|
-
this.addEventListener(LIFE_CYCLE_EVENTS.MODIFIED, this.handleModifiedEvent);
|
|
144
188
|
}
|
|
145
189
|
connectedCallback() {
|
|
146
190
|
super.connectedCallback();
|
|
147
191
|
this.loop = 'false';
|
|
148
192
|
this.initialFocus = 0;
|
|
193
|
+
this.setupDebounceSearch();
|
|
194
|
+
}
|
|
195
|
+
disconnectedCallback() {
|
|
196
|
+
var _a;
|
|
197
|
+
super.disconnectedCallback();
|
|
198
|
+
// cancel any pending debounced action and clear DOM timeouts
|
|
199
|
+
(_a = this.debounceSearch) === null || _a === void 0 ? void 0 : _a.cancel();
|
|
200
|
+
// cancel any pending animation frames
|
|
201
|
+
window.cancelAnimationFrame(this.animationFrameId);
|
|
149
202
|
}
|
|
150
203
|
/** @internal */
|
|
151
204
|
get navItems() {
|
|
@@ -198,36 +251,6 @@ class Select extends ListNavigationMixin(CaptureDestroyEventForChildElement(Auto
|
|
|
198
251
|
}
|
|
199
252
|
}
|
|
200
253
|
/** @internal */
|
|
201
|
-
onStoreUpdate(option, changeType, index) {
|
|
202
|
-
switch (changeType) {
|
|
203
|
-
case 'added':
|
|
204
|
-
option.setAttribute('tabindex', '-1');
|
|
205
|
-
break;
|
|
206
|
-
case 'removed': {
|
|
207
|
-
if (index === -1 || option.tabIndex !== 0) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
let newIndex = index + 1;
|
|
211
|
-
if (newIndex >= this.navItems.length) {
|
|
212
|
-
newIndex = index - 1;
|
|
213
|
-
}
|
|
214
|
-
if (newIndex === -1) {
|
|
215
|
-
this.displayPopover = false;
|
|
216
|
-
this.handleNativeInputFocus();
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
this.resetTabIndexes(newIndex);
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
default:
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
/** @internal */
|
|
227
|
-
isValidItem(item) {
|
|
228
|
-
return item.matches(`${OPTION_TAG_NAME}:not([disabled])`);
|
|
229
|
-
}
|
|
230
|
-
/** @internal */
|
|
231
254
|
getFirstSelectedOption() {
|
|
232
255
|
return this.navItems.find(option => option.hasAttribute('selected'));
|
|
233
256
|
}
|
|
@@ -461,13 +484,64 @@ class Select extends ListNavigationMixin(CaptureDestroyEventForChildElement(Auto
|
|
|
461
484
|
this.displayPopover = !this.displayPopover;
|
|
462
485
|
event.stopPropagation();
|
|
463
486
|
}
|
|
487
|
+
setupDebounceSearch() {
|
|
488
|
+
this.debounceSearch = debounce(() => {
|
|
489
|
+
// for every 500ms, we will reset the search string.
|
|
490
|
+
this.searchString = '';
|
|
491
|
+
}, this.debounceTime);
|
|
492
|
+
}
|
|
493
|
+
debounceSearchKey(letter) {
|
|
494
|
+
var _a;
|
|
495
|
+
(_a = this.debounceSearch) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
496
|
+
// add most recent letter to saved search string
|
|
497
|
+
this.searchString += letter;
|
|
498
|
+
return this.searchString;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Filters the given option labels based on the given search key.
|
|
502
|
+
* It returns a new array of options that have labels starting with the given search key case-insensitive.
|
|
503
|
+
*
|
|
504
|
+
* @param options - The options to filter.
|
|
505
|
+
* @param searchKey - The search key to filter by.
|
|
506
|
+
* @returns The filtered options.
|
|
507
|
+
*/
|
|
508
|
+
filterOptionsBySearchKey(options, searchKey) {
|
|
509
|
+
return options.filter(option => { var _a; return (_a = option.getAttribute('label')) === null || _a === void 0 ? void 0 : _a.toLowerCase().startsWith(searchKey.toLowerCase()); });
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Handles the selection of an option based on the filter string.
|
|
513
|
+
* It will select the first option from the filtered list if it is not empty.
|
|
514
|
+
* If the filtered list is empty, it will do nothing.
|
|
515
|
+
* @param searchKey - The filter string to search for options.
|
|
516
|
+
*/
|
|
517
|
+
handleSelectedOptionBasedOnFilter(searchKey) {
|
|
518
|
+
const startIndex = this.navItems.findIndex(option => option.tabIndex === 0) + 1;
|
|
519
|
+
const orderedOptions = [...this.navItems.slice(startIndex), ...this.navItems.slice(0, startIndex)];
|
|
520
|
+
// First, we search for an exact match with then entire search key
|
|
521
|
+
const filteredResults = this.filterOptionsBySearchKey(orderedOptions, searchKey);
|
|
522
|
+
let newOption = null;
|
|
523
|
+
if (filteredResults.length) {
|
|
524
|
+
// If the key is an exact match, then we set the first option
|
|
525
|
+
[newOption] = filteredResults;
|
|
526
|
+
}
|
|
527
|
+
else if (searchKey.split('').every(letter => letter === searchKey[0])) {
|
|
528
|
+
// If the key is same, then we cycle through all options which start with the same letter
|
|
529
|
+
const nextOptionFromList = this.navItems[startIndex];
|
|
530
|
+
const optionsWhichStartWithSameLetter = this.filterOptionsBySearchKey(orderedOptions, searchKey[0]);
|
|
531
|
+
const nextPossibleOption = optionsWhichStartWithSameLetter.filter(option => option === nextOptionFromList);
|
|
532
|
+
newOption = nextPossibleOption.length ? nextPossibleOption[0] : optionsWhichStartWithSameLetter[0];
|
|
533
|
+
}
|
|
534
|
+
if (this.navItems.indexOf(newOption) !== -1) {
|
|
535
|
+
this.resetTabIndexAndSetFocusAfterUpdate(this.navItems.indexOf(newOption));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
464
538
|
/**
|
|
465
539
|
* Handles the keydown event on the select element when the popover is closed.
|
|
466
540
|
* The options are as follows:
|
|
467
|
-
* - ARROW_DOWN, ARROW_UP, SPACE: Opens the popover and prevents the default scrolling behavior.
|
|
468
|
-
* - ENTER: Opens the popover, prevents default scrolling, and submits the form if the popover is closed.
|
|
541
|
+
* - ARROW_DOWN, ARROW_UP, ENTER, SPACE: Opens the popover and prevents the default scrolling behavior.
|
|
469
542
|
* - HOME: Opens the popover and sets focus and tabindex on the first option.
|
|
470
543
|
* - END: Opens the popover and sets focus and tabindex on the last option.
|
|
544
|
+
* - Any key: Opens the popover and sets focus on the first option which starts with the key.
|
|
471
545
|
* @param event - The keyboard event.
|
|
472
546
|
*/
|
|
473
547
|
handleKeydownCombobox(event) {
|
|
@@ -477,32 +551,48 @@ class Select extends ListNavigationMixin(CaptureDestroyEventForChildElement(Auto
|
|
|
477
551
|
switch (event.key) {
|
|
478
552
|
case KEYS.ARROW_DOWN:
|
|
479
553
|
case KEYS.ARROW_UP:
|
|
480
|
-
this.displayPopover = true;
|
|
481
|
-
// Prevent the default browser behavior of scrolling down
|
|
482
|
-
event.preventDefault();
|
|
483
|
-
event.stopPropagation();
|
|
484
|
-
break;
|
|
485
554
|
case KEYS.ENTER:
|
|
486
555
|
case KEYS.SPACE:
|
|
487
556
|
this.displayPopover = true;
|
|
488
|
-
// Prevent the default browser behavior of scrolling down
|
|
489
|
-
event.preventDefault();
|
|
490
557
|
event.stopPropagation();
|
|
491
558
|
break;
|
|
492
559
|
case KEYS.HOME: {
|
|
493
560
|
this.displayPopover = true;
|
|
494
|
-
this.
|
|
495
|
-
event.preventDefault();
|
|
561
|
+
this.resetTabIndexAndSetFocusAfterUpdate(0);
|
|
496
562
|
break;
|
|
497
563
|
}
|
|
498
564
|
case KEYS.END: {
|
|
499
565
|
this.displayPopover = true;
|
|
500
|
-
this.
|
|
501
|
-
event.preventDefault();
|
|
566
|
+
this.resetTabIndexAndSetFocusAfterUpdate(this.navItems.length - 1);
|
|
502
567
|
break;
|
|
503
568
|
}
|
|
504
|
-
default:
|
|
569
|
+
default: {
|
|
570
|
+
if (event.key.length === 1) {
|
|
571
|
+
this.displayPopover = true;
|
|
572
|
+
this.handleSelectedOptionByKeyInput(event.key);
|
|
573
|
+
}
|
|
505
574
|
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
event.preventDefault();
|
|
578
|
+
event.stopPropagation();
|
|
579
|
+
}
|
|
580
|
+
resetTabIndexAndSetFocusAfterUpdate(newOptionIndex) {
|
|
581
|
+
if (this.displayPopover) {
|
|
582
|
+
// When the popover is opened (`this.displayPopover` is true), the underlying DOM (especially
|
|
583
|
+
// the select listbox inside the popover) may not yet be fully rendered or attached to the layout tree.
|
|
584
|
+
// Calling `resetTabIndexAndSetFocus()` immediately in the same frame would fail because
|
|
585
|
+
// the listbox or its scroll container might still have a height of `0` or not be ready for focus.
|
|
586
|
+
// Wrapping the call inside `window.requestAnimationFrame()` defers the execution until the next
|
|
587
|
+
// browser paint cycle — ensuring that:
|
|
588
|
+
// 1. The DOM updates from Lit’s rendering cycle are flushed.
|
|
589
|
+
// 2. The popover and its scroll container are laid out and measurable.
|
|
590
|
+
// 3. The correct element can safely receive focus and scroll into view.
|
|
591
|
+
this.animationFrameId = window.requestAnimationFrame(() => {
|
|
592
|
+
// We need to reset the tabindex after the component renders,
|
|
593
|
+
// so that the dropdown will open and the focus can be set.
|
|
594
|
+
this.resetTabIndexAndSetFocus(newOptionIndex);
|
|
595
|
+
});
|
|
506
596
|
}
|
|
507
597
|
}
|
|
508
598
|
/**
|
|
@@ -516,6 +606,15 @@ class Select extends ListNavigationMixin(CaptureDestroyEventForChildElement(Auto
|
|
|
516
606
|
handleNativeInputFocus() {
|
|
517
607
|
this.visualCombobox.focus();
|
|
518
608
|
}
|
|
609
|
+
handleSelectedOptionByKeyInput(searchKey) {
|
|
610
|
+
const searchString = this.debounceSearchKey(searchKey);
|
|
611
|
+
this.handleSelectedOptionBasedOnFilter(searchString);
|
|
612
|
+
}
|
|
613
|
+
handleKeydownPopover(event) {
|
|
614
|
+
if (event.key.length === 1) {
|
|
615
|
+
this.handleSelectedOptionByKeyInput(event.key);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
519
618
|
render() {
|
|
520
619
|
var _a, _b, _c, _d, _e, _f;
|
|
521
620
|
return html `
|
|
@@ -589,6 +688,7 @@ class Select extends ListNavigationMixin(CaptureDestroyEventForChildElement(Auto
|
|
|
589
688
|
focus-back-to-trigger
|
|
590
689
|
focus-trap
|
|
591
690
|
size
|
|
691
|
+
@keydown="${this.handleKeydownPopover}"
|
|
592
692
|
boundary="${ifDefined(this.boundary)}"
|
|
593
693
|
strategy="${ifDefined(this.strategy)}"
|
|
594
694
|
placement="${this.placement}"
|