@momentum-design/components 0.112.5 → 0.112.7
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 +624 -438
- package/dist/browser/index.js.map +4 -4
- package/dist/components/buttonlink/buttonlink.component.d.ts +1 -1
- package/dist/components/buttonlink/buttonlink.component.js +6 -31
- package/dist/components/combobox/combobox.component.d.ts +261 -7
- package/dist/components/combobox/combobox.component.js +737 -9
- package/dist/components/combobox/combobox.constants.d.ts +8 -1
- package/dist/components/combobox/combobox.constants.js +8 -1
- package/dist/components/combobox/combobox.events.d.ts +27 -0
- package/dist/components/combobox/combobox.events.js +35 -0
- package/dist/components/combobox/combobox.styles.js +122 -1
- package/dist/components/combobox/combobox.types.d.ts +22 -1
- package/dist/components/combobox/combobox.types.js +0 -1
- package/dist/components/combobox/index.d.ts +5 -0
- package/dist/components/combobox/index.js +5 -0
- package/dist/components/divider/divider.styles.js +4 -0
- package/dist/components/input/input.styles.js +10 -10
- package/dist/components/link/link.component.d.ts +1 -1
- package/dist/components/link/link.component.js +5 -29
- package/dist/components/linksimple/linksimple.component.d.ts +7 -2
- package/dist/components/linksimple/linksimple.component.js +11 -8
- package/dist/components/optgroup/optgroup.styles.js +3 -0
- package/dist/components/option/option.styles.d.ts +2 -2
- package/dist/components/option/option.styles.js +36 -23
- package/dist/components/popover/popover.component.d.ts +2 -2
- package/dist/components/popover/popover.component.js +1 -1
- package/dist/components/popover/popover.constants.d.ts +5 -1
- package/dist/components/popover/popover.constants.js +7 -2
- package/dist/components/popover/popover.types.d.ts +3 -2
- package/dist/custom-elements.json +7601 -6446
- package/dist/react/combobox/index.d.ts +71 -4
- package/dist/react/combobox/index.js +60 -4
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +5 -5
- package/package.json +1 -1
@@ -1,20 +1,748 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
6
|
+
};
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
9
|
+
};
|
10
|
+
import { html, nothing } from 'lit';
|
11
|
+
import { property, query, queryAssignedElements, state } from 'lit/decorators.js';
|
12
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
13
|
+
import { live } from 'lit/directives/live.js';
|
14
|
+
import { ElementStore } from '../../utils/controllers/ElementStore';
|
15
|
+
import { KEYS } from '../../utils/keys';
|
16
|
+
import { AutoFocusOnMountMixin } from '../../utils/mixins/AutoFocusOnMountMixin';
|
17
|
+
import { DataAriaLabelMixin } from '../../utils/mixins/DataAriaLabelMixin';
|
18
|
+
import { FormInternalsMixin } from '../../utils/mixins/FormInternalsMixin';
|
19
|
+
import { CaptureDestroyEventForChildElement } from '../../utils/mixins/lifecycle/CaptureDestroyEventForChildElement';
|
20
|
+
import { LIFE_CYCLE_EVENTS } from '../../utils/mixins/lifecycle/lifecycle.contants';
|
21
|
+
import { ROLE } from '../../utils/roles';
|
22
|
+
import { TAG_NAME as DIVIDER_TAG_NAME } from '../divider/divider.constants';
|
23
|
+
import FormfieldWrapper from '../formfieldwrapper/formfieldwrapper.component';
|
24
|
+
import { DEFAULTS as FORMFIELD_DEFAULTS, VALIDATION } from '../formfieldwrapper/formfieldwrapper.constants';
|
25
|
+
import Input from '../input/input.component';
|
26
|
+
import { AUTO_COMPLETE } from '../input/input.constants';
|
27
|
+
import { TAG_NAME as OPTIONGROUP_TAG_NAME } from '../optgroup/optgroup.constants';
|
28
|
+
import { TAG_NAME as OPTION_TAG_NAME } from '../option/option.constants';
|
29
|
+
import { DEFAULTS as POPOVER_DEFAULTS, POPOVER_PLACEMENT, TRIGGER } from '../popover/popover.constants';
|
30
|
+
import { TAG_NAME as SELECTLISTBOX_TAG_NAME } from '../selectlistbox/selectlistbox.constants';
|
31
|
+
import { AUTOCOMPLETE_LIST, ICON_NAME, TRIGGER_ID } from './combobox.constants';
|
32
|
+
import { ComboboxEventManager } from './combobox.events';
|
3
33
|
import styles from './combobox.styles';
|
4
34
|
/**
|
5
|
-
*
|
35
|
+
* The Combobox component is a text-based dropdown control that allows users to select an option from a predefined list.
|
36
|
+
* Users can type text to filter the options and select their desired choice.
|
37
|
+
*
|
38
|
+
* When the user starts typing, the filter uses a "starts with" search and displays options based on the text entered by the user.
|
39
|
+
* If the user entered text that doesn't match with any of the options, then the text in the `no-result-text` attribute will be displayed.
|
40
|
+
*
|
41
|
+
* If there is no text in the `no-result-text` attribute then nothing will be shown.
|
42
|
+
*
|
43
|
+
* Combobox is designed to work with `mdc-option` for individual options and `mdc-optgroup` for grouping related options.
|
44
|
+
* The component ensures accessibility and usability while handling various use cases, including long text truncation with tooltip support.
|
45
|
+
*
|
46
|
+
* Every mdc-option should have a `value` attribute set to ensure proper form submission.
|
47
|
+
*
|
48
|
+
* To set a default option, use the `selected` attribute on the `mdc-option` element.
|
49
|
+
*
|
50
|
+
* **Note:** Make sure to add `mdc-selectlistbox` as a child of `mdc-combobox` and wrap options/optgroup in it to ensure proper accessibility functionality. Read more about it in SelectListBox documentation.
|
51
|
+
*
|
52
|
+
* If you need to use `mdc-tooltip` with any options, make sure to place the tooltip component outside the `mdc-selectlistbox` element. Read more about it in Options documentation.
|
53
|
+
*
|
54
|
+
* To understand more about combobox and its patterns, refer to this [WCAG example](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/).
|
55
|
+
*
|
56
|
+
* @dependency mdc-buttonsimple
|
57
|
+
* @dependency mdc-icon
|
58
|
+
* @dependency mdc-input
|
59
|
+
* @dependency mdc-listitem
|
60
|
+
* @dependency mdc-popover
|
6
61
|
*
|
7
62
|
* @tagname mdc-combobox
|
8
63
|
*
|
9
|
-
* @slot default - This is a default/unnamed slot
|
64
|
+
* @slot default - This is a default/unnamed slot for Selectlistbox including options and/or option group.
|
10
65
|
*
|
11
|
-
* @
|
66
|
+
* @event click - (React: onClick) This event is dispatched when the combobox is clicked.
|
67
|
+
* @event change - (React: onChange) This event is dispatched when the combobox is changed.
|
68
|
+
* @event input - (React: onInput) This event is dispatched when the combobox is changed.
|
69
|
+
* @event keydown - (React: onKeyDown) This event is dispatched when a key is pressed down on the combobox.
|
70
|
+
* @event focus - (React: onFocus) This event is dispatched when the combobox receives focus.
|
71
|
+
*
|
72
|
+
* @cssproperty --mdc-combobox-border-color - The border color of the combobox
|
73
|
+
* @cssproperty --mdc-combobox-icon-color - The icon color of the combobox
|
74
|
+
* @cssproperty --mdc-combobox-listbox-height - The height of the listbox inside the combobox
|
75
|
+
* @cssproperty --mdc-combobox-listbox-width - The width of the listbox inside the combobox
|
76
|
+
* @cssproperty --mdc-combobox-width - The width of the combobox
|
77
|
+
* @cssproperty --mdc-combobox-hover-background-color - The background color of the combobox when hovered
|
78
|
+
* @cssproperty --mdc-combobox-focused-background-color - The background color of the combobox when focused
|
79
|
+
* @cssproperty --mdc-combobox-error-border-color - The border color of the combobox when in error state
|
80
|
+
* @cssproperty --mdc-combobox-warning-border-color - The border color of the combobox when in warning state
|
81
|
+
* @cssproperty --mdc-combobox-success-border-color - The border color of the combobox when in success state
|
82
|
+
* @cssproperty --mdc-combobox-primary-border-color - The border color of the combobox when in primary state
|
83
|
+
* @cssproperty --mdc-combobox-text-color-disabled - The text color of the combobox when disabled
|
84
|
+
* @cssproperty --mdc-combobox-focused-border-color - The border color of the combobox when focused
|
85
|
+
*
|
86
|
+
* @csspart internal-native-input - The internal native input element of the combobox.
|
87
|
+
* @csspart mdc-input - The input element of the combobox.
|
88
|
+
* @csspart no-result-text - The no result text element of the combobox.
|
89
|
+
* @csspart combobox__base - The base container element of the combobox.
|
90
|
+
* @csspart combobox__button - The button element of the combobox.
|
91
|
+
* @csspart combobox__button-icon - The icon element of the button of the combobox.
|
12
92
|
*/
|
13
|
-
class Combobox extends
|
93
|
+
class Combobox extends CaptureDestroyEventForChildElement(AutoFocusOnMountMixin(FormInternalsMixin(DataAriaLabelMixin(FormfieldWrapper)))) {
|
94
|
+
/** @internal */
|
95
|
+
get navItems() {
|
96
|
+
return this.itemsStore.items;
|
97
|
+
}
|
98
|
+
constructor() {
|
99
|
+
super();
|
100
|
+
/**
|
101
|
+
* readonly attribute of the combobox field. If true, the combobox is read-only.
|
102
|
+
* @default false
|
103
|
+
*/
|
104
|
+
this.readonly = false;
|
105
|
+
/**
|
106
|
+
* The placement of the popover within Combobox.
|
107
|
+
* This defines the position of the popover relative to the combobox input field.
|
108
|
+
*
|
109
|
+
* Possible values:
|
110
|
+
* - 'top-start'
|
111
|
+
* - 'bottom-start'
|
112
|
+
* @default 'bottom-start'
|
113
|
+
*/
|
114
|
+
this.placement = POPOVER_PLACEMENT.BOTTOM_START;
|
115
|
+
/**
|
116
|
+
* This describes the clipping element(s) or area relative to which the overflow of the popover will be checked.
|
117
|
+
* The default is 'clippingAncestors', which are the overflow ancestors that can cause the element to be clipped.
|
118
|
+
* Possible values:
|
119
|
+
* - 'clippingAncestors'
|
120
|
+
* - any css selector
|
121
|
+
*
|
122
|
+
* @default 'clippingAncestors'
|
123
|
+
*
|
124
|
+
* @see [Floating UI - boundary](https://floating-ui.com/docs/detectOverflow#boundary)
|
125
|
+
*/
|
126
|
+
this.boundary = POPOVER_DEFAULTS.BOUNDARY;
|
127
|
+
/**
|
128
|
+
* The strategy of the popover within Combobox.
|
129
|
+
* This determines how the popover is positioned in the DOM.
|
130
|
+
*
|
131
|
+
* In case `boundary` is set to something other than 'clippingAncestors',
|
132
|
+
* it might be necessary to set the `strategy` to 'fixed' to ensure that the popover
|
133
|
+
* is not getting clipped by scrollable containers enclosing the combobox.
|
134
|
+
*
|
135
|
+
* @default absolute
|
136
|
+
* @see [Floating UI - strategy](https://floating-ui.com/docs/computePosition#strategy)
|
137
|
+
*/
|
138
|
+
this.strategy = POPOVER_DEFAULTS.STRATEGY;
|
139
|
+
/**
|
140
|
+
* The z-index of the popover within Combobox.
|
141
|
+
*
|
142
|
+
* Override this to make sure this stays on top of other components.
|
143
|
+
* @default 1000
|
144
|
+
*/
|
145
|
+
this.popoverZIndex = POPOVER_DEFAULTS.Z_INDEX;
|
146
|
+
/** @internal */
|
147
|
+
this.isOpen = false;
|
148
|
+
/** @internal */
|
149
|
+
this.filteredValue = '';
|
150
|
+
/** @internal */
|
151
|
+
this.initialSelectedOption = null;
|
152
|
+
this.handleUpdateError = (error) => {
|
153
|
+
if (this.onerror) {
|
154
|
+
this.onerror(error);
|
155
|
+
}
|
156
|
+
};
|
157
|
+
/**
|
158
|
+
* Update the focus when an item is removed.
|
159
|
+
* If there is a next item, focus it. If not, focus the previous item.
|
160
|
+
*
|
161
|
+
* @internal
|
162
|
+
*/
|
163
|
+
this.handleDestroyEvent = (event) => {
|
164
|
+
const destroyedElement = event.target;
|
165
|
+
if (!this.isValidItem(destroyedElement) || destroyedElement.tabIndex !== 0) {
|
166
|
+
return;
|
167
|
+
}
|
168
|
+
const destroyedItemIndex = this.navItems.findIndex(node => node === destroyedElement);
|
169
|
+
if (destroyedItemIndex === -1) {
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
let newIndex = destroyedItemIndex + 1;
|
173
|
+
if (newIndex >= this.navItems.length) {
|
174
|
+
newIndex = destroyedItemIndex - 1;
|
175
|
+
}
|
176
|
+
};
|
177
|
+
this.addEventListener(LIFE_CYCLE_EVENTS.DESTROYED, this.handleDestroyEvent);
|
178
|
+
// This must be initialized after the destroyed event listener
|
179
|
+
// to keep the element in the itemStore in order to move the focus correctly
|
180
|
+
this.itemsStore = new ElementStore(this, {
|
181
|
+
isValidItem: this.isValidItem,
|
182
|
+
});
|
183
|
+
}
|
184
|
+
connectedCallback() {
|
185
|
+
super.connectedCallback();
|
186
|
+
this.updateComplete
|
187
|
+
.then(() => {
|
188
|
+
if (this.inputElement) {
|
189
|
+
this.setInputValidity();
|
190
|
+
this.internals.setFormValue(this.inputElement.value);
|
191
|
+
}
|
192
|
+
})
|
193
|
+
.catch(this.handleUpdateError);
|
194
|
+
}
|
195
|
+
isValidItem(item) {
|
196
|
+
return item.matches(`${OPTION_TAG_NAME}:not([disabled])`);
|
197
|
+
}
|
198
|
+
openPopover() {
|
199
|
+
this.isOpen = true;
|
200
|
+
}
|
201
|
+
closePopover() {
|
202
|
+
this.isOpen = false;
|
203
|
+
}
|
204
|
+
toggleDropdown() {
|
205
|
+
this.isOpen = !this.isOpen;
|
206
|
+
}
|
207
|
+
compareOptionWithValue(option, value) {
|
208
|
+
const optionValue = option.getAttribute('label') || '';
|
209
|
+
return optionValue.toLowerCase().startsWith(value === null || value === void 0 ? void 0 : value.toLowerCase());
|
210
|
+
}
|
211
|
+
getFirstSelectedOption() {
|
212
|
+
return this.navItems.find(el => el.hasAttribute('selected'));
|
213
|
+
}
|
214
|
+
getVisibleOptions(internalValue) {
|
215
|
+
return this.navItems.filter(option => this.compareOptionWithValue(option, internalValue));
|
216
|
+
}
|
217
|
+
setSelectedValue(option) {
|
218
|
+
// this.value is the actual value of the component
|
219
|
+
this.value = (option === null || option === void 0 ? void 0 : option.getAttribute('value')) || '';
|
220
|
+
// this.filteredValue is the visible label of the component
|
221
|
+
this.filteredValue = (option === null || option === void 0 ? void 0 : option.getAttribute('label')) || '';
|
222
|
+
this.internals.setFormValue(this.value);
|
223
|
+
this.updateHiddenOptions();
|
224
|
+
this.updateSelectedOption(option);
|
225
|
+
this.setInputValidity();
|
226
|
+
this.resetHelpText();
|
227
|
+
ComboboxEventManager.onInputCombobox(this, option);
|
228
|
+
ComboboxEventManager.onChangeCombobox(this, option);
|
229
|
+
}
|
230
|
+
/**
|
231
|
+
* Resets the selected value to an empty string and clears the form value.
|
232
|
+
* This method is called when there is a change on the input.
|
233
|
+
*/
|
234
|
+
resetSelectedValue() {
|
235
|
+
this.value = '';
|
236
|
+
this.internals.setFormValue(this.value);
|
237
|
+
this.resetHelpText();
|
238
|
+
}
|
239
|
+
resetHelpText() {
|
240
|
+
if (this.invalidCustomValueText && this.helpText === this.invalidCustomValueText) {
|
241
|
+
this.helpText = '';
|
242
|
+
this.helpTextType = VALIDATION.DEFAULT;
|
243
|
+
}
|
244
|
+
}
|
245
|
+
/**
|
246
|
+
* This function is called when the attribute changes.
|
247
|
+
* It updates the validity of the input field based on the input field's validity.
|
248
|
+
*
|
249
|
+
* @param name - attribute name
|
250
|
+
* @param old - old value
|
251
|
+
* @param value - new value
|
252
|
+
*/
|
253
|
+
attributeChangedCallback(name, old, value) {
|
254
|
+
super.attributeChangedCallback(name, old, value);
|
255
|
+
if (name === 'validation-message') {
|
256
|
+
this.updateComplete
|
257
|
+
.then(() => {
|
258
|
+
this.setInputValidity();
|
259
|
+
})
|
260
|
+
.catch(this.handleUpdateError);
|
261
|
+
}
|
262
|
+
}
|
263
|
+
async firstUpdated(_changedProperties) {
|
264
|
+
await this.updateComplete;
|
265
|
+
// set the element to auto focus if autoFocusOnMount is set to true
|
266
|
+
// before running the super method, so that the AutoFocusOnMountMixin can use it
|
267
|
+
// to focus the correct element
|
268
|
+
if (this.inputElement && this.autoFocusOnMount) {
|
269
|
+
this.elementToAutoFocus = this.inputElement;
|
270
|
+
}
|
271
|
+
super.firstUpdated(_changedProperties);
|
272
|
+
const firstSelectedOption = this.getFirstSelectedOption();
|
273
|
+
if (firstSelectedOption) {
|
274
|
+
this.initialSelectedOption = firstSelectedOption;
|
275
|
+
this.setSelectedValue(firstSelectedOption);
|
276
|
+
}
|
277
|
+
else if (this.value) {
|
278
|
+
const validOption = this.navItems.find(option => option.value === this.value);
|
279
|
+
this.setSelectedValue(validOption);
|
280
|
+
}
|
281
|
+
else if (this.placeholder) {
|
282
|
+
this.setInputValidity();
|
283
|
+
}
|
284
|
+
this.navItems.forEach(option => {
|
285
|
+
option.setAttribute('tabindex', '-1');
|
286
|
+
});
|
287
|
+
}
|
288
|
+
updated(changedProperties) {
|
289
|
+
super.updated(changedProperties);
|
290
|
+
if (changedProperties.has('slottedListboxes') || changedProperties.has('isOpen')) {
|
291
|
+
// Ensure the element exists and has the ariaControlsElements property
|
292
|
+
if (this.visualCombobox && 'ariaControlsElements' in this.visualCombobox) {
|
293
|
+
this.visualCombobox.ariaControlsElements = this.slottedListboxes;
|
294
|
+
}
|
295
|
+
if (this.dropDownButton && 'ariaControlsElements' in this.dropDownButton) {
|
296
|
+
this.dropDownButton.ariaControlsElements = this.slottedListboxes;
|
297
|
+
}
|
298
|
+
}
|
299
|
+
if (changedProperties.has('disabled') || changedProperties.has('readonly')) {
|
300
|
+
if (this.disabled || this.readonly) {
|
301
|
+
// If the combobox is disabled or readonly,
|
302
|
+
// we close the popover if it is open.
|
303
|
+
this.closePopover();
|
304
|
+
}
|
305
|
+
}
|
306
|
+
}
|
307
|
+
/**
|
308
|
+
* Sets the validity of the input element based on the selected option.
|
309
|
+
* If the selected option is not set and the combobox is required,
|
310
|
+
* it sets a custom validation message.
|
311
|
+
* If the selected option is set or the combobox is not required,
|
312
|
+
* it clears the custom validation message.
|
313
|
+
* This method is called to ensure that the combobox component behaves correctly
|
314
|
+
* in form validation scenarios, especially when the combobox is required.
|
315
|
+
* @internal
|
316
|
+
*/
|
317
|
+
setInputValidity() {
|
318
|
+
var _a, _b;
|
319
|
+
if (!this.value && this.required) {
|
320
|
+
if (this.validationMessage) {
|
321
|
+
(_a = this.inputElement) === null || _a === void 0 ? void 0 : _a.setCustomValidity(this.validationMessage);
|
322
|
+
}
|
323
|
+
else {
|
324
|
+
(_b = this.inputElement) === null || _b === void 0 ? void 0 : _b.setCustomValidity('');
|
325
|
+
}
|
326
|
+
this.setValidity();
|
327
|
+
}
|
328
|
+
else {
|
329
|
+
this.internals.setValidity({});
|
330
|
+
}
|
331
|
+
}
|
332
|
+
/**
|
333
|
+
* Resets the combobox to its initially selected option.
|
334
|
+
* @internal
|
335
|
+
*/
|
336
|
+
formResetCallback() {
|
337
|
+
var _a;
|
338
|
+
const optionToResetTo = this.initialSelectedOption || null;
|
339
|
+
// Restore the selected option
|
340
|
+
this.setSelectedValue(optionToResetTo);
|
341
|
+
// Reset the filtered text (typed value shown in input)
|
342
|
+
this.filteredValue = (_a = optionToResetTo === null || optionToResetTo === void 0 ? void 0 : optionToResetTo.label) !== null && _a !== void 0 ? _a : '';
|
343
|
+
// Force revalidation after reset
|
344
|
+
this.setInputValidity();
|
345
|
+
}
|
346
|
+
/** @internal */
|
347
|
+
formStateRestoreCallback(state) {
|
348
|
+
const optionToRestoreTo = this.navItems.find(option => option.value === state || option.label === state);
|
349
|
+
this.setSelectedValue(optionToRestoreTo || null);
|
350
|
+
}
|
351
|
+
/**
|
352
|
+
* When the native input is focused, visually highlight the dropdown options (the "visual combobox"),
|
353
|
+
* so users can see which option is active, even though the actual DOM focus remains on the input box.
|
354
|
+
* This ensures that after actions like form submission, users can still interact with the dropdown options
|
355
|
+
* through visual cues, while keyboard focus stays on the input field.
|
356
|
+
* @internal
|
357
|
+
*/
|
358
|
+
handleNativeInputFocus() {
|
359
|
+
this.visualCombobox.focus();
|
360
|
+
}
|
361
|
+
/**
|
362
|
+
* Resets the focused option in the dropdown list.
|
363
|
+
* This method removes the 'data-focused' attribute from all options that currently have it,
|
364
|
+
* effectively clearing any visual indication of focus within the dropdown.
|
365
|
+
* It is typically called when the user navigates away from the dropdown or when the dropdown is closed,
|
366
|
+
* ensuring that no option remains visually highlighted as focused.
|
367
|
+
* @internal
|
368
|
+
*/
|
369
|
+
resetFocusedOption() {
|
370
|
+
this.navItems
|
371
|
+
.filter(option => option.hasAttribute('data-focused'))
|
372
|
+
.forEach(option => this.updateOptionAttributes(option, false));
|
373
|
+
}
|
374
|
+
updateSelectedOption(newOption) {
|
375
|
+
this.navItems.forEach(option => {
|
376
|
+
option.removeAttribute('selected');
|
377
|
+
});
|
378
|
+
newOption === null || newOption === void 0 ? void 0 : newOption.setAttribute('selected', '');
|
379
|
+
}
|
380
|
+
/**
|
381
|
+
* Updates the visual focus state of a specific option in the dropdown list based on 'data-focused' attribute.
|
382
|
+
* It also updates the 'aria-selected' attribute for a11y purposes.
|
383
|
+
*
|
384
|
+
* @param option - The option element to update focus state for.
|
385
|
+
* @param value - The new focus state to set (true for focused, false for unfocused).
|
386
|
+
*/
|
387
|
+
updateOptionAttributes(option, value) {
|
388
|
+
if (option === undefined)
|
389
|
+
return;
|
390
|
+
if (value) {
|
391
|
+
option.setAttribute('data-focused', '');
|
392
|
+
}
|
393
|
+
else {
|
394
|
+
option.removeAttribute('data-focused');
|
395
|
+
}
|
396
|
+
option.setAttribute('aria-selected', value.toString());
|
397
|
+
}
|
398
|
+
/**
|
399
|
+
* Handles the blur event of the combobox.
|
400
|
+
* This method is called when the combobox loses focus.
|
401
|
+
* It checks if the combobox has a focused option and sets the selected value to that option and closes the popover.
|
402
|
+
* If the combobox does not have a focused option and the filtered value is not empty,
|
403
|
+
* it sets the help text to the invalid custom value text and closes the popover.
|
404
|
+
* It also updates the input validity.
|
405
|
+
*/
|
406
|
+
handleBlurChange() {
|
407
|
+
const options = this.getVisibleOptions(this.filteredValue);
|
408
|
+
const activeIndex = options.findIndex(option => option.hasAttribute('data-focused'));
|
409
|
+
if (activeIndex !== -1) {
|
410
|
+
this.setSelectedValue(options[activeIndex]);
|
411
|
+
this.closePopover();
|
412
|
+
return;
|
413
|
+
}
|
414
|
+
if (activeIndex === -1 &&
|
415
|
+
this.filteredValue !== '' &&
|
416
|
+
this.invalidCustomValueText &&
|
417
|
+
!this.getFirstSelectedOption()) {
|
418
|
+
this.helpText = this.invalidCustomValueText;
|
419
|
+
this.helpTextType = VALIDATION.ERROR;
|
420
|
+
}
|
421
|
+
// In common cases (when no selection made and focus moved away), close the popover.
|
422
|
+
this.setInputValidity();
|
423
|
+
}
|
424
|
+
updateFocusAndScrollIntoView(options, oldIndex, newIndex) {
|
425
|
+
var _a;
|
426
|
+
this.updateOptionAttributes(options[oldIndex], false);
|
427
|
+
this.updateOptionAttributes(options[newIndex], true);
|
428
|
+
(_a = options[newIndex]) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ block: 'nearest' });
|
429
|
+
}
|
430
|
+
handleInputKeydown(event) {
|
431
|
+
const options = this.getVisibleOptions(this.filteredValue);
|
432
|
+
const activeIndex = options.findIndex(option => option.hasAttribute('data-focused'));
|
433
|
+
switch (event.key) {
|
434
|
+
case KEYS.ARROW_DOWN: {
|
435
|
+
this.openPopover();
|
436
|
+
const newIndex = options.length - 1 === activeIndex ? 0 : activeIndex + 1;
|
437
|
+
this.updateFocusAndScrollIntoView(options, activeIndex, newIndex);
|
438
|
+
event.preventDefault();
|
439
|
+
break;
|
440
|
+
}
|
441
|
+
case KEYS.ARROW_UP: {
|
442
|
+
this.openPopover();
|
443
|
+
const newIndex = activeIndex === -1 || activeIndex === 0 ? options.length - 1 : activeIndex - 1;
|
444
|
+
this.updateFocusAndScrollIntoView(options, activeIndex, newIndex);
|
445
|
+
event.preventDefault();
|
446
|
+
break;
|
447
|
+
}
|
448
|
+
case KEYS.ENTER: {
|
449
|
+
if (activeIndex === -1)
|
450
|
+
return;
|
451
|
+
this.setSelectedValue(options[activeIndex]);
|
452
|
+
if (this.isOpen === true) {
|
453
|
+
this.closePopover();
|
454
|
+
}
|
455
|
+
break;
|
456
|
+
}
|
457
|
+
case KEYS.ESCAPE: {
|
458
|
+
if (activeIndex !== -1) {
|
459
|
+
this.updateOptionAttributes(options[activeIndex], false);
|
460
|
+
}
|
461
|
+
if (options.length && this.shouldDisplayPopover(options.length)) {
|
462
|
+
this.closePopover();
|
463
|
+
}
|
464
|
+
else {
|
465
|
+
this.resetSelectedValue();
|
466
|
+
// clear the visible value
|
467
|
+
this.filteredValue = '';
|
468
|
+
}
|
469
|
+
break;
|
470
|
+
}
|
471
|
+
case KEYS.TAB: {
|
472
|
+
this.closePopover();
|
473
|
+
break;
|
474
|
+
}
|
475
|
+
case KEYS.HOME:
|
476
|
+
case KEYS.END: {
|
477
|
+
this.resetFocusedOption();
|
478
|
+
break;
|
479
|
+
}
|
480
|
+
default:
|
481
|
+
break;
|
482
|
+
}
|
483
|
+
}
|
484
|
+
/**
|
485
|
+
* Updates the hidden state of options based on the current filtered value.
|
486
|
+
* If an option does not match the current filtered value, it is hidden.
|
487
|
+
* Otherwise, it is made visible.
|
488
|
+
* Additionally, it updates the hidden state of option groups and dividers based on the current filtered value.
|
489
|
+
*/
|
490
|
+
updateHiddenOptions() {
|
491
|
+
this.navItems.forEach(option => {
|
492
|
+
if (!this.compareOptionWithValue(option, this.filteredValue)) {
|
493
|
+
option.setAttribute('data-hidden', '');
|
494
|
+
this.hideOptionGroupAndDivider(option);
|
495
|
+
}
|
496
|
+
else {
|
497
|
+
option.removeAttribute('data-hidden');
|
498
|
+
this.showOptionGroupAndDivider(option);
|
499
|
+
}
|
500
|
+
});
|
501
|
+
}
|
502
|
+
hideOptionGroupAndDivider(option) {
|
503
|
+
var _a, _b, _c;
|
504
|
+
if ((_a = option.parentElement) === null || _a === void 0 ? void 0 : _a.matches(OPTIONGROUP_TAG_NAME)) {
|
505
|
+
const optionGroupChildren = (_b = Array.from(option.parentElement.children)) === null || _b === void 0 ? void 0 : _b.filter(option => !option.hasAttribute('data-hidden'));
|
506
|
+
if (optionGroupChildren.length === 0) {
|
507
|
+
option.parentElement.setAttribute('data-hidden', '');
|
508
|
+
if ((_c = option.parentElement.nextElementSibling) === null || _c === void 0 ? void 0 : _c.matches(DIVIDER_TAG_NAME)) {
|
509
|
+
option.parentElement.nextElementSibling.setAttribute('data-hidden', '');
|
510
|
+
}
|
511
|
+
}
|
512
|
+
}
|
513
|
+
}
|
514
|
+
showOptionGroupAndDivider(option) {
|
515
|
+
var _a, _b, _c;
|
516
|
+
if ((_a = option.parentElement) === null || _a === void 0 ? void 0 : _a.matches(OPTIONGROUP_TAG_NAME)) {
|
517
|
+
const optionGroupChildren = (_b = Array.from(option.parentElement.children)) === null || _b === void 0 ? void 0 : _b.filter(option => !option.hasAttribute('data-hidden'));
|
518
|
+
if (optionGroupChildren.length > 0) {
|
519
|
+
option.parentElement.removeAttribute('data-hidden');
|
520
|
+
if ((_c = option.parentElement.nextElementSibling) === null || _c === void 0 ? void 0 : _c.matches(DIVIDER_TAG_NAME)) {
|
521
|
+
option.parentElement.nextElementSibling.removeAttribute('data-hidden');
|
522
|
+
}
|
523
|
+
}
|
524
|
+
}
|
525
|
+
}
|
526
|
+
handleInputChange(event) {
|
527
|
+
var _a;
|
528
|
+
this.filteredValue = event.target.value;
|
529
|
+
this.resetSelectedValue();
|
530
|
+
this.resetFocusedOption();
|
531
|
+
this.updateHiddenOptions();
|
532
|
+
// remove the selected attribute on input change
|
533
|
+
(_a = this.getFirstSelectedOption()) === null || _a === void 0 ? void 0 : _a.removeAttribute('selected');
|
534
|
+
if (this.isOpen === false) {
|
535
|
+
this.openPopover();
|
536
|
+
}
|
537
|
+
}
|
538
|
+
handleOptionsClick(event) {
|
539
|
+
var _a;
|
540
|
+
// ensure we get the actual option element even if the click target is a child node
|
541
|
+
const option = (_a = event.target.closest(OPTION_TAG_NAME)) !== null && _a !== void 0 ? _a : null;
|
542
|
+
if (option && !option.hasAttribute('disabled')) {
|
543
|
+
this.setSelectedValue(option);
|
544
|
+
this.closePopover();
|
545
|
+
// Focus on the combobox after click
|
546
|
+
this.updateComplete.then(() => this.handleNativeInputFocus()).catch(this.handleUpdateError);
|
547
|
+
}
|
548
|
+
}
|
549
|
+
shouldDisplayPopover(optionsLength) {
|
550
|
+
if (this.disabled || this.readonly) {
|
551
|
+
return false;
|
552
|
+
}
|
553
|
+
if (optionsLength) {
|
554
|
+
return this.isOpen;
|
555
|
+
}
|
556
|
+
if (this.noResultText) {
|
557
|
+
return true;
|
558
|
+
}
|
559
|
+
return false;
|
560
|
+
}
|
561
|
+
/**
|
562
|
+
* Renders the native input element.
|
563
|
+
* This input is hidden and is used for internal purposes only.
|
564
|
+
* The value of the selected option is set as the value of this input.
|
565
|
+
* @internal
|
566
|
+
*/
|
567
|
+
renderNativeInput() {
|
568
|
+
return html `
|
569
|
+
<input
|
570
|
+
id="${this.inputId}"
|
571
|
+
name="${this.name}"
|
572
|
+
type="text"
|
573
|
+
.value="${live(this.value)}"
|
574
|
+
aria-hidden="true"
|
575
|
+
part="internal-native-input"
|
576
|
+
tabindex="-1"
|
577
|
+
?required=${this.required}
|
578
|
+
?disabled=${this.disabled}
|
579
|
+
?readonly=${this.readonly}
|
580
|
+
autocomplete="${AUTO_COMPLETE.OFF}"
|
581
|
+
@focus=${this.handleNativeInputFocus}
|
582
|
+
@invalid=${this.setInputValidity}
|
583
|
+
/>
|
584
|
+
`;
|
585
|
+
}
|
586
|
+
/**
|
587
|
+
* Renders the base input element with accessibility.
|
588
|
+
* This input is displayed on the screen and is used for user interaction only.
|
589
|
+
* The label of the selected option is set as the value of this input.
|
590
|
+
* @internal
|
591
|
+
*/
|
592
|
+
renderBaseInput() {
|
593
|
+
var _a;
|
594
|
+
return html `
|
595
|
+
<input
|
596
|
+
id="${this.id}"
|
597
|
+
slot="input"
|
598
|
+
?disabled="${this.disabled}"
|
599
|
+
.value="${live(this.filteredValue)}"
|
600
|
+
autocomplete="${AUTO_COMPLETE.OFF}"
|
601
|
+
class="input"
|
602
|
+
part="mdc-input"
|
603
|
+
placeholder="${ifDefined(this.placeholder)}"
|
604
|
+
role="${ROLE.COMBOBOX}"
|
605
|
+
?readonly="${this.readonly}"
|
606
|
+
?required="${this.required}"
|
607
|
+
@input=${this.handleInputChange}
|
608
|
+
@keydown=${this.handleInputKeydown}
|
609
|
+
@blur="${this.handleBlurChange}"
|
610
|
+
aria-autocomplete="${AUTOCOMPLETE_LIST}"
|
611
|
+
aria-describedby="${ifDefined(this.helpText ? FORMFIELD_DEFAULTS.HELPER_TEXT_ID : '')}"
|
612
|
+
aria-disabled="${this.disabled ? 'true' : 'false'}"
|
613
|
+
aria-expanded="${this.isOpen ? 'true' : 'false'}"
|
614
|
+
aria-haspopup="${ROLE.LISTBOX}"
|
615
|
+
aria-invalid="${this.helpTextType === VALIDATION.ERROR ? 'true' : 'false'}"
|
616
|
+
aria-label="${(_a = this.dataAriaLabel) !== null && _a !== void 0 ? _a : ''}"
|
617
|
+
aria-labelledby="${this.label ? FORMFIELD_DEFAULTS.HEADING_ID : ''}"
|
618
|
+
aria-readonly="${this.readonly ? 'true' : 'false'}"
|
619
|
+
aria-required="${this.required ? 'true' : 'false'}"
|
620
|
+
/>
|
621
|
+
`;
|
622
|
+
}
|
623
|
+
renderNoResultsText(optionsLength) {
|
624
|
+
return optionsLength === 0 && this.noResultText
|
625
|
+
? html `<mdc-listitem part="no-result-text" tabindex="-1" role="" label="${this.noResultText}"></mdc-listitem>`
|
626
|
+
: nothing;
|
627
|
+
}
|
14
628
|
render() {
|
15
|
-
|
16
|
-
|
629
|
+
var _a;
|
630
|
+
const options = this.getVisibleOptions(this.filteredValue);
|
631
|
+
return html `
|
632
|
+
${this.renderLabel()}
|
633
|
+
<div part="combobox__base" id="${TRIGGER_ID}">
|
634
|
+
${this.renderNativeInput()}
|
635
|
+
<mdc-input
|
636
|
+
@click="${() => this.toggleDropdown()}"
|
637
|
+
?disabled="${this.disabled}"
|
638
|
+
?readonly="${this.readonly}"
|
639
|
+
help-text-type="${this.helpTextType}"
|
640
|
+
>
|
641
|
+
${this.renderBaseInput()}
|
642
|
+
</mdc-input>
|
643
|
+
<mdc-buttonsimple
|
644
|
+
@click="${() => this.toggleDropdown()}"
|
645
|
+
part="combobox__button"
|
646
|
+
?disabled="${this.disabled}"
|
647
|
+
tabindex="-1"
|
648
|
+
aria-expanded="${this.isOpen ? 'true' : 'false'}"
|
649
|
+
aria-label="${(_a = this.dataAriaLabel) !== null && _a !== void 0 ? _a : ''}"
|
650
|
+
>
|
651
|
+
<mdc-icon
|
652
|
+
part="combobox__button-icon"
|
653
|
+
name="${this.shouldDisplayPopover(options.length) ? ICON_NAME.ARROW_UP : ICON_NAME.ARROW_DOWN}"
|
654
|
+
size="1"
|
655
|
+
length-unit="rem"
|
656
|
+
></mdc-icon>
|
657
|
+
</mdc-buttonsimple>
|
658
|
+
<mdc-popover
|
659
|
+
?visible="${this.shouldDisplayPopover(options.length)}"
|
660
|
+
@closebyescape="${() => {
|
661
|
+
this.closePopover();
|
662
|
+
}}"
|
663
|
+
@closebyoutsideclick="${() => {
|
664
|
+
this.closePopover();
|
665
|
+
this.handleNativeInputFocus();
|
666
|
+
}}"
|
667
|
+
backdrop
|
668
|
+
backdrop-append-to="${ifDefined(this.backdropAppendTo)}"
|
669
|
+
boundary="${ifDefined(this.boundary)}"
|
670
|
+
disable-aria-expanded
|
671
|
+
exportparts="popover-content"
|
672
|
+
hide-on-escape
|
673
|
+
hide-on-outside-click
|
674
|
+
is-backdrop-invisible
|
675
|
+
placement="${this.placement}"
|
676
|
+
role=""
|
677
|
+
size
|
678
|
+
strategy="${ifDefined(this.strategy)}"
|
679
|
+
trigger="${TRIGGER.MANUAL}"
|
680
|
+
triggerid="${TRIGGER_ID}"
|
681
|
+
z-index="${ifDefined(this.popoverZIndex)}"
|
682
|
+
>
|
683
|
+
${this.renderNoResultsText(options.length)}
|
684
|
+
<slot @click="${this.handleOptionsClick}"></slot>
|
685
|
+
</mdc-popover>
|
686
|
+
</div>
|
687
|
+
${this.renderHelperText()}
|
688
|
+
`;
|
17
689
|
}
|
18
690
|
}
|
19
|
-
Combobox.styles = [...
|
691
|
+
Combobox.styles = [...FormfieldWrapper.styles, ...Input.styles, ...styles];
|
692
|
+
__decorate([
|
693
|
+
property({ type: String }),
|
694
|
+
__metadata("design:type", String)
|
695
|
+
], Combobox.prototype, "placeholder", void 0);
|
696
|
+
__decorate([
|
697
|
+
property({ type: Boolean }),
|
698
|
+
__metadata("design:type", Object)
|
699
|
+
], Combobox.prototype, "readonly", void 0);
|
700
|
+
__decorate([
|
701
|
+
property({ type: String, reflect: true }),
|
702
|
+
__metadata("design:type", String)
|
703
|
+
], Combobox.prototype, "placement", void 0);
|
704
|
+
__decorate([
|
705
|
+
property({ type: String, attribute: 'no-result-text', reflect: true }),
|
706
|
+
__metadata("design:type", String)
|
707
|
+
], Combobox.prototype, "noResultText", void 0);
|
708
|
+
__decorate([
|
709
|
+
property({ type: String, attribute: 'invalid-custom-value-text', reflect: true }),
|
710
|
+
__metadata("design:type", String)
|
711
|
+
], Combobox.prototype, "invalidCustomValueText", void 0);
|
712
|
+
__decorate([
|
713
|
+
property({ type: String, reflect: true }),
|
714
|
+
__metadata("design:type", String)
|
715
|
+
], Combobox.prototype, "boundary", void 0);
|
716
|
+
__decorate([
|
717
|
+
property({ type: String, reflect: true }),
|
718
|
+
__metadata("design:type", String)
|
719
|
+
], Combobox.prototype, "strategy", void 0);
|
720
|
+
__decorate([
|
721
|
+
property({ type: Number, reflect: true, attribute: 'popover-z-index' }),
|
722
|
+
__metadata("design:type", Number)
|
723
|
+
], Combobox.prototype, "popoverZIndex", void 0);
|
724
|
+
__decorate([
|
725
|
+
property({ type: String, reflect: true, attribute: 'backdrop-append-to' }),
|
726
|
+
__metadata("design:type", String)
|
727
|
+
], Combobox.prototype, "backdropAppendTo", void 0);
|
728
|
+
__decorate([
|
729
|
+
query(`[role="${ROLE.COMBOBOX}"]`),
|
730
|
+
__metadata("design:type", HTMLInputElement)
|
731
|
+
], Combobox.prototype, "visualCombobox", void 0);
|
732
|
+
__decorate([
|
733
|
+
query(`[part="combobox__button"]`),
|
734
|
+
__metadata("design:type", HTMLElement)
|
735
|
+
], Combobox.prototype, "dropDownButton", void 0);
|
736
|
+
__decorate([
|
737
|
+
queryAssignedElements({ selector: SELECTLISTBOX_TAG_NAME }),
|
738
|
+
__metadata("design:type", Array)
|
739
|
+
], Combobox.prototype, "slottedListboxes", void 0);
|
740
|
+
__decorate([
|
741
|
+
state(),
|
742
|
+
__metadata("design:type", Object)
|
743
|
+
], Combobox.prototype, "isOpen", void 0);
|
744
|
+
__decorate([
|
745
|
+
state(),
|
746
|
+
__metadata("design:type", Object)
|
747
|
+
], Combobox.prototype, "filteredValue", void 0);
|
20
748
|
export default Combobox;
|