@aurodesignsystem-dev/auro-formkit 0.0.0-pr1503.2 → 0.0.0-pr1505.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.
- package/components/checkbox/demo/customize.min.js +2 -2
- package/components/checkbox/demo/getting-started.min.js +2 -2
- package/components/checkbox/demo/index.min.js +2 -2
- package/components/checkbox/dist/index.js +2 -2
- package/components/checkbox/dist/registered.js +2 -2
- package/components/combobox/demo/customize.md +7 -3
- package/components/combobox/demo/customize.min.js +1450 -1662
- package/components/combobox/demo/getting-started.min.js +1450 -1662
- package/components/combobox/demo/index.min.js +1450 -1662
- package/components/combobox/dist/auro-combobox.d.ts +5 -0
- package/components/combobox/dist/index.js +504 -141
- package/components/combobox/dist/registered.js +504 -141
- package/components/counter/demo/customize.min.js +3 -3
- package/components/counter/demo/index.min.js +3 -3
- package/components/counter/dist/index.js +3 -3
- package/components/counter/dist/registered.js +3 -3
- package/components/datepicker/demo/api.md +2 -0
- package/components/datepicker/demo/customize.md +195 -33
- package/components/datepicker/demo/customize.min.js +145 -60
- package/components/datepicker/demo/index.min.js +137 -59
- package/components/datepicker/dist/index.js +137 -59
- package/components/datepicker/dist/registered.js +137 -59
- package/components/datepicker/dist/src/auro-datepicker.d.ts +2 -0
- package/components/dropdown/demo/customize.min.js +1 -1
- package/components/dropdown/demo/getting-started.min.js +1 -1
- package/components/dropdown/demo/index.min.js +1 -1
- package/components/dropdown/dist/index.js +1 -1
- package/components/dropdown/dist/registered.js +1 -1
- package/components/form/demo/customize.min.js +2209 -2237
- package/components/form/demo/getting-started.min.js +2209 -2237
- package/components/form/demo/index.min.js +2209 -2237
- package/components/form/demo/registerDemoDeps.min.js +2207 -2235
- package/components/input/demo/customize.md +56 -55
- package/components/input/demo/customize.min.js +131 -56
- package/components/input/demo/getting-started.min.js +131 -56
- package/components/input/demo/index.md +2 -2
- package/components/input/demo/index.min.js +131 -56
- package/components/input/dist/base-input.d.ts +16 -0
- package/components/input/dist/index.js +131 -56
- package/components/input/dist/registered.js +131 -56
- package/components/menu/demo/api.md +41 -45
- package/components/menu/demo/customize.md +0 -28
- package/components/menu/demo/index.min.js +779 -1354
- package/components/menu/dist/auro-menu.d.ts +95 -110
- package/components/menu/dist/auro-menuoption.d.ts +32 -138
- package/components/menu/dist/index.js +753 -1308
- package/components/menu/dist/registered.js +765 -1308
- package/components/radio/demo/customize.min.js +2 -2
- package/components/radio/demo/getting-started.min.js +2 -2
- package/components/radio/demo/index.min.js +2 -2
- package/components/radio/dist/index.js +2 -2
- package/components/radio/dist/registered.js +2 -2
- package/components/select/demo/customize.md +7 -3
- package/components/select/demo/customize.min.js +1072 -1616
- package/components/select/demo/getting-started.min.js +1072 -1616
- package/components/select/demo/index.min.js +1072 -1616
- package/components/select/dist/index.js +46 -15
- package/components/select/dist/registered.js +46 -15
- package/components/select/dist/selectUtils.d.ts +12 -0
- package/custom-elements.json +384 -1022
- package/package.json +1 -3
- package/components/menu/dist/auro-menu.context.d.ts +0 -238
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { css, LitElement, html } from 'lit';
|
|
2
|
-
import { createContext, ContextProvider, ContextConsumer } from '@lit/context';
|
|
3
2
|
import { classMap } from 'lit/directives/class-map.js';
|
|
4
3
|
import { unsafeStatic, literal, html as html$1 } from 'lit/static-html.js';
|
|
5
4
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
@@ -114,720 +113,6 @@ class AuroElement extends LitElement {
|
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
/* eslint-disable */
|
|
118
|
-
|
|
119
|
-
class MenuService {
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* PROPERTIES AND GETTERS
|
|
123
|
-
*/
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Gets the list of registered menu options.
|
|
127
|
-
* @returns {AuroMenuOption[]}
|
|
128
|
-
*/
|
|
129
|
-
get menuOptions() {
|
|
130
|
-
return this._menuOptions;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Gets the currently highlighted option.
|
|
135
|
-
* @returns {AuroMenuOption|null}
|
|
136
|
-
*/
|
|
137
|
-
get highlightedOption() {
|
|
138
|
-
return this._menuOptions[this.highlightedIndex] || null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Gets the current value(s) of the selected option(s).
|
|
143
|
-
* @returns {string|string[]|undefined}
|
|
144
|
-
*/
|
|
145
|
-
get currentValue() {
|
|
146
|
-
const values = (this.selectedOptions || []).map(option => option.value);
|
|
147
|
-
return this.multiSelect ? values : values[0];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Gets the label(s) of the currently selected option(s).
|
|
152
|
-
* @returns {string}
|
|
153
|
-
*/
|
|
154
|
-
get currentLabel() {
|
|
155
|
-
const labels = (this.selectedOptions || []).map(option => option.textContent);
|
|
156
|
-
return this.multiSelect ? labels.join(", ") : labels[0] || '';
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Gets the string representation of the current value(s).
|
|
161
|
-
* For multi-select, this is a JSON stringified array.
|
|
162
|
-
* @returns {string|undefined}
|
|
163
|
-
*/
|
|
164
|
-
get stringValue() {
|
|
165
|
-
const { currentValue } = this;
|
|
166
|
-
|
|
167
|
-
if (Array.isArray(currentValue)) {
|
|
168
|
-
if (currentValue.length > 0) {
|
|
169
|
-
return JSON.stringify(currentValue);
|
|
170
|
-
}
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (typeof currentValue === 'string') {
|
|
175
|
-
if (currentValue.length > 0) {
|
|
176
|
-
return currentValue;
|
|
177
|
-
}
|
|
178
|
-
return undefined;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Future: handle other types here (e.g., number, object, etc.)
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Gets the key(s) of the currently selected option(s).
|
|
187
|
-
* @returns {string|string[]|undefined}
|
|
188
|
-
*/
|
|
189
|
-
get currentKeys() {
|
|
190
|
-
const keys = (this.selectedOptions || []).map(option => option.key);
|
|
191
|
-
return this.multiSelect ? keys : keys[0];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* CONSTRUCTOR
|
|
196
|
-
*/
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Creates a new MenuService instance.
|
|
200
|
-
* @param {Object} options - The options object.
|
|
201
|
-
* @param {AuroMenu} options.host - The host element that this service will control. Required.
|
|
202
|
-
* @throws {Error} If the host is not provided.
|
|
203
|
-
*/
|
|
204
|
-
constructor({ host } = {}) {
|
|
205
|
-
|
|
206
|
-
// Ensure a host was passed
|
|
207
|
-
if (!host) {
|
|
208
|
-
throw new Error("MenuService requires a host element.");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Attach the service to the host
|
|
212
|
-
this.host = host;
|
|
213
|
-
this.host.addController(this);
|
|
214
|
-
|
|
215
|
-
// Set default properties
|
|
216
|
-
this.size = undefined;
|
|
217
|
-
this.shape = undefined;
|
|
218
|
-
this.noCheckmark = undefined;
|
|
219
|
-
this.disabled = undefined;
|
|
220
|
-
this.matchWord = undefined;
|
|
221
|
-
this.multiSelect = undefined;
|
|
222
|
-
this.allowDeselect = undefined;
|
|
223
|
-
this.selectAllMatchingOptions = undefined;
|
|
224
|
-
|
|
225
|
-
this.highlightedIndex = -1;
|
|
226
|
-
|
|
227
|
-
this._menuOptions = [];
|
|
228
|
-
this._subscribers = [];
|
|
229
|
-
this.internalUpdateInProgress = false;
|
|
230
|
-
this.selectedOptions = [];
|
|
231
|
-
this._pendingValue = null;
|
|
232
|
-
this._pendingRetryScheduled = false;
|
|
233
|
-
this._pendingRetryCount = 0;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* PROPERTY SYNCING
|
|
238
|
-
*/
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Handles host updates.
|
|
242
|
-
* This is a lit reactive lifecycle method.
|
|
243
|
-
* This comes from the Lit controller interface provided by adding this service as a controller to the host.
|
|
244
|
-
* See constructor for `this.host.addController(this)`
|
|
245
|
-
* You can read more about Lit reactive controllers here: https://lit.dev/docs/composition/controllers/
|
|
246
|
-
*/
|
|
247
|
-
hostUpdated() {
|
|
248
|
-
|
|
249
|
-
// Reset selection if multiSelect mode changes
|
|
250
|
-
if (this.host.multiSelect !== this.multiSelect) {
|
|
251
|
-
this.selectedOptions = [];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Update properties on host update
|
|
255
|
-
this.setProperties({
|
|
256
|
-
size: this.host.size,
|
|
257
|
-
shape: this.host.shape,
|
|
258
|
-
noCheckmark: this.host.noCheckmark,
|
|
259
|
-
disabled: this.host.disabled,
|
|
260
|
-
matchWord: this.host.matchWord,
|
|
261
|
-
multiSelect: this.host.multiSelect,
|
|
262
|
-
allowDeselect: this.host.allowDeselect,
|
|
263
|
-
selectAllMatchingOptions: this.host.selectAllMatchingOptions
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Handles host disconnection and memory cleanup.
|
|
269
|
-
*/
|
|
270
|
-
hostDisconnected() {
|
|
271
|
-
this._subscribers = [];
|
|
272
|
-
this._menuOptions = [];
|
|
273
|
-
this._pendingValue = null;
|
|
274
|
-
this._pendingRetryScheduled = false;
|
|
275
|
-
this._pendingRetryCount = 0;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Sets a property value if it exists on the instance and the value has changed.
|
|
280
|
-
* @param {string} property
|
|
281
|
-
* @param {any} value
|
|
282
|
-
*/
|
|
283
|
-
setProperty(property, value) {
|
|
284
|
-
|
|
285
|
-
// Only update if we are tracking the property in this service
|
|
286
|
-
if (this.hasOwnProperty(property)) {
|
|
287
|
-
|
|
288
|
-
// Check if the value has changed
|
|
289
|
-
const valueChanged = this[property] !== value;
|
|
290
|
-
|
|
291
|
-
// Update and notify if changed
|
|
292
|
-
if (valueChanged) {
|
|
293
|
-
this[property] = value;
|
|
294
|
-
this.notify({ property, value });
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Sets multiple properties on the instance.
|
|
301
|
-
* @param {Object} properties - Key-value pairs of properties to set.
|
|
302
|
-
*/
|
|
303
|
-
setProperties(properties) {
|
|
304
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
305
|
-
this.setProperty(key, value);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* MENU OPTION HIGHLIGHTING
|
|
311
|
-
*/
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Highlights the next active option in the menu.
|
|
315
|
-
*/
|
|
316
|
-
highlightNext() {
|
|
317
|
-
this.moveHighlightedOption("next");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Highlights the previous active option in the menu.
|
|
322
|
-
*/
|
|
323
|
-
highlightPrevious() {
|
|
324
|
-
this.moveHighlightedOption("previous");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Moves the highlighted option in the specified direction.
|
|
329
|
-
* @param {string} direction - The direction to move the highlight ("next" or "previous").
|
|
330
|
-
*/
|
|
331
|
-
moveHighlightedOption(direction) {
|
|
332
|
-
|
|
333
|
-
// Get the active options
|
|
334
|
-
const activeOptions = this._menuOptions.filter(option => option.isActive);
|
|
335
|
-
|
|
336
|
-
// Get the currently active option
|
|
337
|
-
const currentActiveOption = activeOptions[activeOptions.indexOf(this.highlightedOption)];
|
|
338
|
-
|
|
339
|
-
// Determine the new index based on the currently active option and direction
|
|
340
|
-
let newIndex = currentActiveOption
|
|
341
|
-
? direction === "previous"
|
|
342
|
-
? activeOptions.indexOf(currentActiveOption) - 1
|
|
343
|
-
: activeOptions.indexOf(currentActiveOption) + 1
|
|
344
|
-
: direction === "previous"
|
|
345
|
-
? activeOptions.length - 1
|
|
346
|
-
: 0;
|
|
347
|
-
|
|
348
|
-
// Wrap around the index if needed
|
|
349
|
-
newIndex = newIndex < 0 ? activeOptions.length - 1 : newIndex >= activeOptions.length ? 0 : newIndex;
|
|
350
|
-
|
|
351
|
-
// Get the new active option and set it as highlighted
|
|
352
|
-
const newActiveOption = activeOptions[newIndex];
|
|
353
|
-
this.setHighlightedOption(newActiveOption);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Sets the highlighted index to the specified option.
|
|
358
|
-
* @param {AuroMenuOption} option - The option to highlight.
|
|
359
|
-
*/
|
|
360
|
-
setHighlightedOption(option) {
|
|
361
|
-
|
|
362
|
-
if (!option) return;
|
|
363
|
-
|
|
364
|
-
// Get the index of the option to highlight
|
|
365
|
-
const index = this._menuOptions.indexOf(option);
|
|
366
|
-
|
|
367
|
-
// Update highlighted index
|
|
368
|
-
this.highlightedIndex = index;
|
|
369
|
-
|
|
370
|
-
// Notify subscribers of highlight change
|
|
371
|
-
this.notify({ type: 'highlightChange', option, index: this.highlightedIndex });
|
|
372
|
-
|
|
373
|
-
// Dispatch the change event
|
|
374
|
-
this.dispatchChangeEvent('auroMenu-activatedOption', option);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Sets the highlighted option to the option at the specified index if it exists.
|
|
379
|
-
* @param {number} index
|
|
380
|
-
*/
|
|
381
|
-
setHighlightedIndex(index) {
|
|
382
|
-
const option = this._menuOptions[index] || null;
|
|
383
|
-
this.setHighlightedOption(option);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Selects the currently highlighted option.
|
|
388
|
-
*/
|
|
389
|
-
selectHighlightedOption() {
|
|
390
|
-
if (this.highlightedOption) {
|
|
391
|
-
this.toggleOption(this.highlightedOption);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* SELECTION AND DESELECTION METHODS
|
|
397
|
-
*/
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Selects one or more options in a batch operation
|
|
401
|
-
* @param {AuroMenuOption|AuroMenuOption[]} options - Single option or array of options to select
|
|
402
|
-
*/
|
|
403
|
-
selectOptions(options) {
|
|
404
|
-
let optionsToSelect = Array.isArray(options) ? options : [options];
|
|
405
|
-
|
|
406
|
-
// Filter out options that are inactive
|
|
407
|
-
optionsToSelect = optionsToSelect.filter(option => option.isActive);
|
|
408
|
-
|
|
409
|
-
if (!optionsToSelect.length) return;
|
|
410
|
-
|
|
411
|
-
if (this.multiSelect) {
|
|
412
|
-
this.selectedOptions = [...(this.selectedOptions || []), ...optionsToSelect];
|
|
413
|
-
} else {
|
|
414
|
-
// In single select mode, only take the last option
|
|
415
|
-
this.selectedOptions = [optionsToSelect[optionsToSelect.length - 1]];
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
this.stageUpdate();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Deselects one or more options in a batch operation
|
|
423
|
-
* @param {AuroMenuOption|AuroMenuOption[]} options - Single option or array of options to deselect
|
|
424
|
-
*/
|
|
425
|
-
deselectOptions(options) {
|
|
426
|
-
const optionsToDeselect = Array.isArray(options) ? options : [options];
|
|
427
|
-
|
|
428
|
-
if (!optionsToDeselect.length) return;
|
|
429
|
-
|
|
430
|
-
// Check if deselection should be prevented
|
|
431
|
-
const shouldPreventDeselect = !this.allowDeselect && !this.multiSelect;
|
|
432
|
-
const isOnlySelectedOption = this.selectedOptions.length === 1 && optionsToDeselect.includes(this.selectedOptions[0]);
|
|
433
|
-
|
|
434
|
-
// Prevent deselecting the only selected option if not allowed
|
|
435
|
-
if (shouldPreventDeselect && isOnlySelectedOption) {
|
|
436
|
-
optionsToDeselect.forEach(option => {
|
|
437
|
-
option.selected = true;
|
|
438
|
-
});
|
|
439
|
-
this.dispatchChangeEvent('auroMenu-deselectPrevented', {
|
|
440
|
-
values: optionsToDeselect
|
|
441
|
-
});
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const optionsSet = new Set(optionsToDeselect);
|
|
446
|
-
const previousCount = this.selectedOptions.length;
|
|
447
|
-
this.selectedOptions = (this.selectedOptions || [])
|
|
448
|
-
.filter(opt => !optionsSet.has(opt));
|
|
449
|
-
|
|
450
|
-
if (this.selectedOptions.length < previousCount) {
|
|
451
|
-
this.stageUpdate();
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Selects a single option.
|
|
457
|
-
* @param {AuroMenuOption} option
|
|
458
|
-
*/
|
|
459
|
-
selectOption(option) {
|
|
460
|
-
this.selectOptions(option);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Deselects a single option.
|
|
465
|
-
* @param {AuroMenuOption} option
|
|
466
|
-
*/
|
|
467
|
-
deselectOption(option) {
|
|
468
|
-
this.deselectOptions(option);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Toggles the selection state of a single option.
|
|
473
|
-
* @param {AuroMenuOption} option
|
|
474
|
-
*/
|
|
475
|
-
toggleOption(option) {
|
|
476
|
-
if (option.selected) {
|
|
477
|
-
this.deselectOption(option);
|
|
478
|
-
} else {
|
|
479
|
-
this.selectOption(option);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Selects options based on their value(s) when compared to a passed value or values.
|
|
485
|
-
* Value or values are normalized to an array of strings that can be matched to option keys.
|
|
486
|
-
* @param {string|number|Array<string|number>} value - The value(s) to select.
|
|
487
|
-
*/
|
|
488
|
-
selectByValue(value) {
|
|
489
|
-
const isEmptyValue = value === undefined ||
|
|
490
|
-
value === null ||
|
|
491
|
-
(Array.isArray(value) && value.length === 0) ||
|
|
492
|
-
(typeof value === 'string' && value.trim() === '');
|
|
493
|
-
|
|
494
|
-
// Early exit for invalid/empty values
|
|
495
|
-
if (isEmptyValue) {
|
|
496
|
-
this.selectedOptions.forEach(opt => opt.selected = false);
|
|
497
|
-
this.selectedOptions = [];
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// If an internal update cycle is still in progress, defer value application
|
|
502
|
-
// rather than dropping it.
|
|
503
|
-
if (this.internalUpdateInProgress || this.host.internalUpdateInProgress) {
|
|
504
|
-
this.queuePendingValue(value);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Normalize values to array of strings
|
|
509
|
-
const normalizedValues = this._getNormalizedValues(value);
|
|
510
|
-
|
|
511
|
-
// Validate for single-select mode
|
|
512
|
-
let validatedValues = normalizedValues;
|
|
513
|
-
if (normalizedValues.length > 1 && !this.multiSelect) {
|
|
514
|
-
console.warn("MenuService - Multiple values provided for single-select menu. Only the first value will be selected.");
|
|
515
|
-
validatedValues = [normalizedValues[0]];
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (this._menuOptions.length === 0) {
|
|
519
|
-
this.queuePendingValue(value);
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Find matching options by comparing available options to validated values
|
|
524
|
-
const trackedKeys = new Set();
|
|
525
|
-
const optionsToSelect = this._menuOptions.filter(option => {
|
|
526
|
-
const passesFilter = validatedValues.includes(option.key);
|
|
527
|
-
const alreadyTracked = trackedKeys.has(option.key);
|
|
528
|
-
const isActive = option.isActive;
|
|
529
|
-
|
|
530
|
-
trackedKeys.add(option.key);
|
|
531
|
-
|
|
532
|
-
// Include the option in the options to be selected if it passes the filter check and
|
|
533
|
-
// either hasn't been tracked yet or selectAllMatchingOptions is true
|
|
534
|
-
return isActive && passesFilter && (!alreadyTracked || (alreadyTracked && this.selectAllMatchingOptions));
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// Handle no matches: clear existing selection, but do not dispatch an intermediate
|
|
538
|
-
// undefined value that can overwrite the host value in parent components.
|
|
539
|
-
if (!optionsToSelect.length) {
|
|
540
|
-
const hasUnresolvedKeys = this._menuOptions.some((option) => option.isActive && option.key == null);
|
|
541
|
-
|
|
542
|
-
if (hasUnresolvedKeys) {
|
|
543
|
-
this.queuePendingValue(value);
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
this.clearPendingValue();
|
|
548
|
-
|
|
549
|
-
if (this.selectedOptions.length > 0) {
|
|
550
|
-
this.selectedOptions = [];
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Always notify so the host resets any stale invalid value, even when
|
|
554
|
-
// selectedOptions was already empty (e.g. double-clicking set-invalid).
|
|
555
|
-
this.stageUpdate({ reason: 'no-match' });
|
|
556
|
-
|
|
557
|
-
// Dispatch failure event if no matches found
|
|
558
|
-
if (validatedValues.length) {
|
|
559
|
-
this.dispatchChangeEvent('auroMenu-selectValueFailure', {
|
|
560
|
-
message: 'No matching options found for the provided value(s).',
|
|
561
|
-
values: validatedValues
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
this.clearPendingValue();
|
|
569
|
-
|
|
570
|
-
if (this.optionsArraysMatch(optionsToSelect, this.selectedOptions)) {
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Apply programmatic selection as a single transaction and emit one final state.
|
|
575
|
-
this.selectedOptions = optionsToSelect;
|
|
576
|
-
this.stageUpdate();
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Queues a pending value and schedules a bounded retry.
|
|
581
|
-
* @param {string|number|Array<string|number>} value - The value to retry.
|
|
582
|
-
*/
|
|
583
|
-
queuePendingValue(value) {
|
|
584
|
-
this._pendingValue = value;
|
|
585
|
-
|
|
586
|
-
if (this._pendingRetryScheduled || this._pendingRetryCount >= 5) {
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
this._pendingRetryScheduled = true;
|
|
591
|
-
this._pendingRetryCount += 1;
|
|
592
|
-
|
|
593
|
-
setTimeout(() => {
|
|
594
|
-
this._pendingRetryScheduled = false;
|
|
595
|
-
|
|
596
|
-
if (this._pendingValue == null) {
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const pendingValue = this._pendingValue;
|
|
601
|
-
this.selectByValue(pendingValue);
|
|
602
|
-
}, 0);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Clears pending retry state.
|
|
607
|
-
*/
|
|
608
|
-
clearPendingValue() {
|
|
609
|
-
this._pendingValue = null;
|
|
610
|
-
this._pendingRetryScheduled = false;
|
|
611
|
-
this._pendingRetryCount = 0;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Resets the selected options to an empty array.
|
|
616
|
-
*/
|
|
617
|
-
reset() {
|
|
618
|
-
const previousOptions = [...this.selectedOptions];
|
|
619
|
-
previousOptions.forEach(opt => opt.selected = false);
|
|
620
|
-
this.selectedOptions = [];
|
|
621
|
-
|
|
622
|
-
// Single update after clearing all
|
|
623
|
-
if (previousOptions.length) {
|
|
624
|
-
this.stageUpdate();
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* SUBSCRIPTION, NOTIFICATION AND EVENT DISPATCH METHODS
|
|
630
|
-
*/
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Subscribes a callback to menu service events.
|
|
634
|
-
* @param {Function} callback - The callback to invoke on events.
|
|
635
|
-
*/
|
|
636
|
-
subscribe(callback) {
|
|
637
|
-
this._subscribers.push(callback);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Remove a previously subscribed callback from menu service events.
|
|
642
|
-
* @param {Function} callback
|
|
643
|
-
*/
|
|
644
|
-
unsubscribe(callback) {
|
|
645
|
-
this._subscribers = this._subscribers.filter(cb => cb !== callback);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Stages an update to notify subscribers of state and value changes.
|
|
650
|
-
*/
|
|
651
|
-
stageUpdate(meta = {}) {
|
|
652
|
-
this.notifyStateChange(meta);
|
|
653
|
-
this.notifyValueChange(meta);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Notifies subscribers of a menu service event.
|
|
658
|
-
* All notifications are sent to all subscribers.
|
|
659
|
-
* @param {string} event - The event to send to subscribers.
|
|
660
|
-
*/
|
|
661
|
-
notify(event) {
|
|
662
|
-
this._subscribers.forEach(callback => callback(event));
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/**
|
|
666
|
-
* Notifies subscribers of a state change (selected options has changed).
|
|
667
|
-
*/
|
|
668
|
-
notifyStateChange(meta = {}) {
|
|
669
|
-
this.notify({
|
|
670
|
-
type: 'stateChange',
|
|
671
|
-
selectedOptions: this.selectedOptions,
|
|
672
|
-
...meta
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Notifies subscribers of a value change (current value has changed).
|
|
678
|
-
*/
|
|
679
|
-
notifyValueChange(meta = {}) {
|
|
680
|
-
|
|
681
|
-
// Prepare details for the event
|
|
682
|
-
const details = {
|
|
683
|
-
value: this.currentValue,
|
|
684
|
-
stringValue: this.stringValue,
|
|
685
|
-
keys: this.currentKeys,
|
|
686
|
-
options: this.selectedOptions,
|
|
687
|
-
label: this.currentLabel
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
// If only one option is selected, include its index
|
|
691
|
-
if (this.selectedOptions.length === 1) details.index = this._menuOptions.indexOf(this.selectedOptions[0]);
|
|
692
|
-
|
|
693
|
-
this.notify({
|
|
694
|
-
type: 'valueChange',
|
|
695
|
-
...meta,
|
|
696
|
-
...details
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Dispatches a custom event from the host element.
|
|
702
|
-
* @param {string} eventName
|
|
703
|
-
* @param {any} detail
|
|
704
|
-
*/
|
|
705
|
-
dispatchChangeEvent(eventName, detail) {
|
|
706
|
-
this.host.dispatchEvent(new CustomEvent(eventName, {
|
|
707
|
-
bubbles: true,
|
|
708
|
-
cancelable: false,
|
|
709
|
-
composed: true,
|
|
710
|
-
detail
|
|
711
|
-
}));
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* MENU OPTION MANAGEMENT METHODS
|
|
716
|
-
*/
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Adds a menu option to the service's list.
|
|
720
|
-
* @param {AuroMenuOption} option - the option to track
|
|
721
|
-
*/
|
|
722
|
-
addMenuOption(option) {
|
|
723
|
-
this._menuOptions.push(option);
|
|
724
|
-
this.notify({ type: 'optionsChange', options: this._menuOptions });
|
|
725
|
-
|
|
726
|
-
if (this._pendingValue != null) {
|
|
727
|
-
this.queuePendingValue(this._pendingValue);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Removes a menu option from the service's list.
|
|
733
|
-
* @param {AuroMenuOption} option - the option to remove
|
|
734
|
-
*/
|
|
735
|
-
removeMenuOption(option) {
|
|
736
|
-
this._menuOptions = this._menuOptions.filter(opt => opt !== option);
|
|
737
|
-
this.notify({ type: 'optionsChange', options: this._menuOptions });
|
|
738
|
-
|
|
739
|
-
if (this._menuOptions.length === 0) {
|
|
740
|
-
this.clearPendingValue();
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* UTILITIES
|
|
746
|
-
*/
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* Normalizes a value or array of values into an array of strings for option selection.
|
|
750
|
-
* This function ensures that input values are consistently formatted for matching menu options.
|
|
751
|
-
*
|
|
752
|
-
* @param {string|number|Array<string|number>} value - The value(s) to normalize.
|
|
753
|
-
* @returns {Array<string>} An array of string values suitable for option matching.
|
|
754
|
-
* @throws {Error} If any value is not a string or number.
|
|
755
|
-
*/
|
|
756
|
-
_getNormalizedValues(value) {
|
|
757
|
-
let values = value;
|
|
758
|
-
|
|
759
|
-
// Handle JSON string and single value string input
|
|
760
|
-
if (!Array.isArray(values) && typeof values === 'string') {
|
|
761
|
-
|
|
762
|
-
// Attempt to parse as JSON array
|
|
763
|
-
try {
|
|
764
|
-
|
|
765
|
-
// Normalize single quotes to double quotes for JSON parsing
|
|
766
|
-
// This will not handle complex cases but will cover basic usage
|
|
767
|
-
const parseValue = values.replace(/'([^']*?)'/g, '"$1"');
|
|
768
|
-
|
|
769
|
-
// Attempt parse
|
|
770
|
-
const parsed = JSON.parse(parseValue);
|
|
771
|
-
|
|
772
|
-
// Ensure parsed value is an array
|
|
773
|
-
if (!Array.isArray(parsed)) throw new Error('Not an array');
|
|
774
|
-
|
|
775
|
-
// Set values to parsed array
|
|
776
|
-
values = parsed;
|
|
777
|
-
} catch (err) {
|
|
778
|
-
|
|
779
|
-
// If parsing fails, treat as single value
|
|
780
|
-
values = [value];
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Handle a single number being passed
|
|
785
|
-
if (typeof values === 'number') {
|
|
786
|
-
values = [String(values)];
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Coerce each value to string and validate types
|
|
790
|
-
values.forEach((val, index) => {
|
|
791
|
-
|
|
792
|
-
// Throw an error for invalid value types
|
|
793
|
-
if (typeof val !== 'string' && typeof val !== 'number') {
|
|
794
|
-
throw new Error('Value contains invalid value type. Supported types are string and number.');
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Convert numbers to strings for consistency
|
|
798
|
-
if (typeof val === 'number') {
|
|
799
|
-
values[index] = String(val);
|
|
800
|
-
}
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
// Return the resulting array of string values
|
|
804
|
-
return values;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Returns whether two arrays of options contain the same elements.
|
|
809
|
-
* @param {AuroMenuOption[]} arr1 - First array of options.
|
|
810
|
-
* @param {AuroMenuOption[]} arr2 - Second array of options.
|
|
811
|
-
* @returns {boolean} True if arrays match, false otherwise.
|
|
812
|
-
*/
|
|
813
|
-
optionsArraysMatch(arr1, arr2) {
|
|
814
|
-
if (arr1.length !== arr2.length) return false;
|
|
815
|
-
|
|
816
|
-
const set1 = new Set(arr1);
|
|
817
|
-
const set2 = new Set(arr2);
|
|
818
|
-
|
|
819
|
-
for (let item of set1) {
|
|
820
|
-
if (!set2.has(item)) {
|
|
821
|
-
return false;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
return true;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const MenuContext = createContext('menu-context');
|
|
830
|
-
|
|
831
116
|
// Copyright (c) Alaska Air. All right reserved. Licensed under the Apache-2.0 license
|
|
832
117
|
// See LICENSE in the project root for license information.
|
|
833
118
|
|
|
@@ -915,6 +200,18 @@ class AuroLibraryRuntimeUtils {
|
|
|
915
200
|
// See LICENSE in the project root for license information.
|
|
916
201
|
|
|
917
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Validates if an option can be interacted with.
|
|
205
|
+
* @private
|
|
206
|
+
* @param {HTMLElement} option - The option to check.
|
|
207
|
+
* @returns {boolean} True if option is interactive.
|
|
208
|
+
*/
|
|
209
|
+
function isOptionInteractive(option) {
|
|
210
|
+
return !option.hasAttribute('hidden') &&
|
|
211
|
+
!option.hasAttribute('disabled') &&
|
|
212
|
+
!option.hasAttribute('static');
|
|
213
|
+
}
|
|
214
|
+
|
|
918
215
|
/**
|
|
919
216
|
* Helper method to dispatch custom events.
|
|
920
217
|
* @param {HTMLElement} element - Element to dispatch event from.
|
|
@@ -935,7 +232,7 @@ function dispatchMenuEvent(element, eventName, detail = null) {
|
|
|
935
232
|
element.dispatchEvent(new CustomEvent(eventName, eventConfig));
|
|
936
233
|
}
|
|
937
234
|
|
|
938
|
-
/* eslint-disable no-underscore-dangle */
|
|
235
|
+
/* eslint-disable no-underscore-dangle, no-magic-numbers, max-lines, no-extra-parens, max-depth */
|
|
939
236
|
// Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
|
|
940
237
|
// See LICENSE in the project root for license information.
|
|
941
238
|
|
|
@@ -949,16 +246,12 @@ function dispatchMenuEvent(element, eventName, detail = null) {
|
|
|
949
246
|
* @event {CustomEvent<any>} auroMenu-customEventFired - Notifies that a custom event has been fired.
|
|
950
247
|
* @event {CustomEvent<{ loading: boolean; hasLoadingPlaceholder: boolean; }>} auroMenu-loadingChange - Notifies when the loading attribute is changed.
|
|
951
248
|
* @event {CustomEvent<any>} auroMenu-selectValueFailure - Notifies that an attempt to select a menuoption by matching a value has failed.
|
|
952
|
-
* @event {CustomEvent<{ values: HTMLElement[] }>} auroMenu-deselectPrevented - Notifies that deselection was prevented and includes the affected options in `detail.values`.
|
|
953
249
|
* @event {CustomEvent<any>} auroMenu-selectValueReset - Notifies that the component value has been reset.
|
|
954
250
|
* @event {CustomEvent<any>} auroMenu-selectedOption - Notifies that a new menuoption selection has been made.
|
|
955
251
|
* @slot loadingText - Text to show while loading attribute is set
|
|
956
252
|
* @slot loadingIcon - Icon to show while loading attribute is set
|
|
957
253
|
* @slot - Slot for insertion of menu options.
|
|
958
254
|
*/
|
|
959
|
-
|
|
960
|
-
/* eslint-disable max-lines */
|
|
961
|
-
|
|
962
255
|
class AuroMenu extends AuroElement {
|
|
963
256
|
|
|
964
257
|
constructor() {
|
|
@@ -976,6 +269,8 @@ class AuroMenu extends AuroElement {
|
|
|
976
269
|
*/
|
|
977
270
|
this.size = "sm";
|
|
978
271
|
|
|
272
|
+
// Value of the selected options
|
|
273
|
+
this.value = undefined;
|
|
979
274
|
// Currently selected option
|
|
980
275
|
this.optionSelected = undefined;
|
|
981
276
|
// String used for highlighting/filtering
|
|
@@ -988,13 +283,24 @@ class AuroMenu extends AuroElement {
|
|
|
988
283
|
this.loading = false;
|
|
989
284
|
// Multi-select mode
|
|
990
285
|
this.multiSelect = false;
|
|
991
|
-
// Allow deselecting of menu options
|
|
992
|
-
this.allowDeselect = false;
|
|
993
|
-
// Select all matching options when setting value in multi-select mode
|
|
994
|
-
this.selectAllMatchingOptions = false;
|
|
995
286
|
|
|
996
287
|
// Event Bindings
|
|
997
288
|
|
|
289
|
+
/**
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @private
|
|
296
|
+
*/
|
|
297
|
+
this.handleMouseSelect = this.handleMouseSelect.bind(this);
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @private
|
|
301
|
+
*/
|
|
302
|
+
this.handleOptionHover = this.handleOptionHover.bind(this);
|
|
303
|
+
|
|
998
304
|
/**
|
|
999
305
|
* @private
|
|
1000
306
|
*/
|
|
@@ -1021,14 +327,6 @@ class AuroMenu extends AuroElement {
|
|
|
1021
327
|
return {
|
|
1022
328
|
...super.properties,
|
|
1023
329
|
|
|
1024
|
-
/**
|
|
1025
|
-
* Allows deselecting an already selected option when clicked again in single-select mode.
|
|
1026
|
-
*/
|
|
1027
|
-
allowDeselect: {
|
|
1028
|
-
type: Boolean,
|
|
1029
|
-
reflect: true,
|
|
1030
|
-
},
|
|
1031
|
-
|
|
1032
330
|
/**
|
|
1033
331
|
* When true, the entire menu and all options are disabled.
|
|
1034
332
|
*/
|
|
@@ -1037,20 +335,6 @@ class AuroMenu extends AuroElement {
|
|
|
1037
335
|
reflect: true
|
|
1038
336
|
},
|
|
1039
337
|
|
|
1040
|
-
/**
|
|
1041
|
-
* Indicates whether the menu has a loadingIcon or loadingText to render when in a loading state.
|
|
1042
|
-
*/
|
|
1043
|
-
hasLoadingPlaceholder: {
|
|
1044
|
-
type: Boolean
|
|
1045
|
-
},
|
|
1046
|
-
|
|
1047
|
-
/**
|
|
1048
|
-
* @private
|
|
1049
|
-
*/
|
|
1050
|
-
layout: {
|
|
1051
|
-
type: String
|
|
1052
|
-
},
|
|
1053
|
-
|
|
1054
338
|
/**
|
|
1055
339
|
* Indent level for submenus.
|
|
1056
340
|
* @private
|
|
@@ -1111,44 +395,6 @@ class AuroMenu extends AuroElement {
|
|
|
1111
395
|
type: Object
|
|
1112
396
|
},
|
|
1113
397
|
|
|
1114
|
-
/**
|
|
1115
|
-
* Available menu options.
|
|
1116
|
-
* @readonly
|
|
1117
|
-
*/
|
|
1118
|
-
options: {
|
|
1119
|
-
type: Array,
|
|
1120
|
-
reflect: false,
|
|
1121
|
-
attribute: false
|
|
1122
|
-
},
|
|
1123
|
-
|
|
1124
|
-
/**
|
|
1125
|
-
* Sets the size of the menu.
|
|
1126
|
-
* @type {'sm' | 'md'}
|
|
1127
|
-
* @default 'sm'
|
|
1128
|
-
*/
|
|
1129
|
-
size: {
|
|
1130
|
-
type: String,
|
|
1131
|
-
reflect: true
|
|
1132
|
-
},
|
|
1133
|
-
|
|
1134
|
-
/**
|
|
1135
|
-
* When true, selects all options that match the provided value/key when setting value and multiselect is enabled.
|
|
1136
|
-
*/
|
|
1137
|
-
selectAllMatchingOptions: {
|
|
1138
|
-
type: Boolean,
|
|
1139
|
-
reflect: true,
|
|
1140
|
-
},
|
|
1141
|
-
|
|
1142
|
-
/**
|
|
1143
|
-
* Sets the shape of the menu.
|
|
1144
|
-
* @type {'box' | 'round'}
|
|
1145
|
-
* @default 'box'
|
|
1146
|
-
*/
|
|
1147
|
-
shape: {
|
|
1148
|
-
type: String,
|
|
1149
|
-
reflect: true
|
|
1150
|
-
},
|
|
1151
|
-
|
|
1152
398
|
/**
|
|
1153
399
|
* The value of the selected option. In multi-select mode, this is a JSON stringified array of selected option values.
|
|
1154
400
|
*/
|
|
@@ -1164,44 +410,13 @@ class AuroMenu extends AuroElement {
|
|
|
1164
410
|
return [
|
|
1165
411
|
styleCss$1,
|
|
1166
412
|
colorCss$1,
|
|
1167
|
-
tokensCss
|
|
1168
|
-
];
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/**
|
|
1172
|
-
* @readonly
|
|
1173
|
-
* @returns {string} - Returns the label of the currently selected option(s).
|
|
1174
|
-
*/
|
|
1175
|
-
get currentLabel() {
|
|
1176
|
-
return this.menuService.currentLabel;
|
|
1177
|
-
};
|
|
1178
|
-
|
|
1179
|
-
/**
|
|
1180
|
-
* @readonly
|
|
1181
|
-
* @returns {Array<HTMLElement>} - Returns the array of available menu options.
|
|
1182
|
-
* @deprecated Use `options` property instead.
|
|
1183
|
-
*/
|
|
1184
|
-
get items() {
|
|
1185
|
-
return this.options;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/**
|
|
1189
|
-
* @returns {number} - Returns the index of the currently active option.
|
|
1190
|
-
*/
|
|
1191
|
-
get index() {
|
|
1192
|
-
return this._index;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
/**
|
|
1196
|
-
* @param {number} value - Sets the index of the currently active option.
|
|
1197
|
-
*/
|
|
1198
|
-
set index(value) {
|
|
1199
|
-
this.menuService.setHighlightedIndex(value);
|
|
413
|
+
tokensCss
|
|
414
|
+
];
|
|
1200
415
|
}
|
|
1201
416
|
|
|
1202
417
|
/**
|
|
1203
418
|
* This will register this element with the browser.
|
|
1204
|
-
* @param {string} [name="auro-menu"] - The name of
|
|
419
|
+
* @param {string} [name="auro-menu"] - The name of element that you want to register to.
|
|
1205
420
|
*
|
|
1206
421
|
* @example
|
|
1207
422
|
* AuroMenu.register("custom-menu") // this will register this element to <custom-menu/>
|
|
@@ -1212,126 +427,116 @@ class AuroMenu extends AuroElement {
|
|
|
1212
427
|
}
|
|
1213
428
|
|
|
1214
429
|
/**
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
* @private
|
|
1218
|
-
* @returns {String|Array<String>}
|
|
430
|
+
* @readonly
|
|
431
|
+
* @returns {Array<HTMLElement>} - Returns the array of available menu options.
|
|
1219
432
|
*/
|
|
1220
|
-
get
|
|
1221
|
-
return this.
|
|
433
|
+
get options() {
|
|
434
|
+
return this.items;
|
|
1222
435
|
}
|
|
1223
436
|
|
|
1224
437
|
/**
|
|
1225
|
-
*
|
|
1226
|
-
* @private
|
|
1227
|
-
* @returns {Object}
|
|
438
|
+
* @returns {number} - Returns the index of the currently active option.
|
|
1228
439
|
*/
|
|
1229
|
-
get
|
|
1230
|
-
return
|
|
1231
|
-
size: this.size,
|
|
1232
|
-
shape: this.shape,
|
|
1233
|
-
noCheckmark: this.nocheckmark,
|
|
1234
|
-
disabled: this.disabled
|
|
1235
|
-
};
|
|
440
|
+
get index() {
|
|
441
|
+
return this._index;
|
|
1236
442
|
}
|
|
1237
443
|
|
|
1238
444
|
/**
|
|
1239
|
-
*
|
|
1240
|
-
* Initializes the MenuService and subscribes to menu changes.
|
|
1241
|
-
* @protected
|
|
445
|
+
* @param {number} value - Sets the index of the currently active option.
|
|
1242
446
|
*/
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
this.rootMenu = false;
|
|
1246
|
-
this.menuService = this.parentElement.menuService;
|
|
1247
|
-
this._contextProvider = this.parentElement._contextProvider;
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
this.menuService = new MenuService({host: this});
|
|
1252
|
-
this.menuService.setProperties(this.propertyValues);
|
|
1253
|
-
this.menuService.subscribe(this.handleMenuChange.bind(this));
|
|
1254
|
-
this._contextProvider = new ContextProvider(this, {
|
|
1255
|
-
context: MenuContext,
|
|
1256
|
-
initialValue: this.menuService
|
|
1257
|
-
});
|
|
447
|
+
set index(value) {
|
|
448
|
+
this.updateActiveOption(value);
|
|
1258
449
|
}
|
|
1259
450
|
|
|
1260
451
|
/**
|
|
1261
|
-
*
|
|
1262
|
-
* @
|
|
452
|
+
* Gets the currently selected options as an array.
|
|
453
|
+
* @returns {Array<HTMLElement>}
|
|
1263
454
|
*/
|
|
1264
|
-
|
|
1265
|
-
this.
|
|
455
|
+
get selectedOptions() {
|
|
456
|
+
if (!this.optionSelected) {
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
if (Array.isArray(this.optionSelected)) {
|
|
460
|
+
return this.optionSelected;
|
|
461
|
+
}
|
|
462
|
+
return [this.optionSelected];
|
|
1266
463
|
}
|
|
1267
464
|
|
|
1268
465
|
/**
|
|
1269
|
-
*
|
|
1270
|
-
* @
|
|
1271
|
-
* @protected
|
|
466
|
+
* Gets the first selected option, or null if none.
|
|
467
|
+
* @returns {HTMLElement|null}
|
|
1272
468
|
*/
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
this.value = value;
|
|
1277
|
-
|
|
1278
|
-
setTimeout(() => {
|
|
1279
|
-
this.internalUpdateInProgress = false;
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
469
|
+
get selectedOption() {
|
|
470
|
+
const opts = this.selectedOptions;
|
|
471
|
+
return opts.length > 0 ? opts[0] : null;
|
|
1282
472
|
}
|
|
1283
473
|
|
|
1284
474
|
/**
|
|
1285
|
-
*
|
|
1286
|
-
* @
|
|
1287
|
-
* @protected
|
|
475
|
+
* @readonly
|
|
476
|
+
* @returns {string} - Returns the label of the currently selected option(s).
|
|
1288
477
|
*/
|
|
1289
|
-
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1292
|
-
// New option is array value or first option with fallback to undefined for empty array in all cases
|
|
1293
|
-
const newOption = this.multiSelect && event.options.length ? event.options : event.options[0] || undefined;
|
|
1294
|
-
const newValue = event.stringValue;
|
|
1295
|
-
|
|
1296
|
-
// Check if the option or value has actually changed
|
|
1297
|
-
if (this.optionSelected !== newOption || this.stringValue !== newValue) {
|
|
1298
|
-
this.optionSelected = newOption;
|
|
1299
|
-
this.setInternalValue(newValue);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
// Notify components of selection change
|
|
1303
|
-
this.notifySelectionChange(event);
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
if (event.type === 'highlightChange') {
|
|
1307
|
-
this.optionActive = event.option;
|
|
1308
|
-
this._index = event.index;
|
|
478
|
+
get currentLabel() {
|
|
479
|
+
if (!this.optionSelected) {
|
|
480
|
+
return '';
|
|
1309
481
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
this.options = event.options;
|
|
1313
|
-
this.dispatchEvent(new CustomEvent('auroMenu-optionsChange', {
|
|
1314
|
-
detail: {
|
|
1315
|
-
options: event.options
|
|
1316
|
-
}
|
|
1317
|
-
}));
|
|
482
|
+
if (Array.isArray(this.optionSelected)) {
|
|
483
|
+
return this.optionSelected.map((opt) => opt.textContent).join(', ');
|
|
1318
484
|
}
|
|
485
|
+
return this.optionSelected.textContent || '';
|
|
1319
486
|
}
|
|
1320
487
|
|
|
1321
488
|
/**
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
489
|
+
* Formatted value based on `multiSelect` state.
|
|
490
|
+
* Default type is `String`, changing to `Array<String>` when `multiSelect` is true.
|
|
491
|
+
* @private
|
|
492
|
+
* @returns {String|Array<String>}
|
|
1324
493
|
*/
|
|
1325
|
-
get
|
|
1326
|
-
|
|
494
|
+
get formattedValue() {
|
|
495
|
+
if (this.multiSelect) {
|
|
496
|
+
if (!this.value) {
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
// Defensive: `value` is declared as String, but consumers may assign arrays or other
|
|
500
|
+
// types programmatically. Normalize without throwing so render/update never hard-crashes.
|
|
501
|
+
if (Array.isArray(this.value)) {
|
|
502
|
+
return this.value;
|
|
503
|
+
}
|
|
504
|
+
if (typeof this.value !== 'string') {
|
|
505
|
+
return [String(this.value)];
|
|
506
|
+
}
|
|
507
|
+
if (this.value.startsWith("[")) {
|
|
508
|
+
// Malformed JSON (e.g. a literal string that happens to start with "[") falls back
|
|
509
|
+
// to a single-item array rather than throwing during render.
|
|
510
|
+
try {
|
|
511
|
+
const parsed = JSON.parse(this.value);
|
|
512
|
+
return Array.isArray(parsed) ? parsed : [this.value];
|
|
513
|
+
} catch {
|
|
514
|
+
return [this.value];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return [this.value];
|
|
518
|
+
}
|
|
519
|
+
return this.value;
|
|
1327
520
|
}
|
|
1328
521
|
|
|
1329
522
|
/**
|
|
1330
|
-
*
|
|
1331
|
-
* @
|
|
523
|
+
* Selects options by value.
|
|
524
|
+
* @param {string|string[]|undefined|null} value - The value(s) to select.
|
|
525
|
+
* @public
|
|
1332
526
|
*/
|
|
1333
|
-
|
|
1334
|
-
|
|
527
|
+
selectByValue(value) {
|
|
528
|
+
const isEmpty = value === undefined ||
|
|
529
|
+
value === null ||
|
|
530
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
531
|
+
(typeof value === 'string' && value.trim() === '');
|
|
532
|
+
|
|
533
|
+
if (isEmpty) {
|
|
534
|
+
this.clearSelection();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// `value` is a String property; stringify arrays so attribute reflection and `formattedValue` parsing stay correct.
|
|
539
|
+
this.value = Array.isArray(value) ? JSON.stringify(value) : value;
|
|
1335
540
|
}
|
|
1336
541
|
|
|
1337
542
|
// Lifecycle Methods
|
|
@@ -1339,9 +544,7 @@ class AuroMenu extends AuroElement {
|
|
|
1339
544
|
connectedCallback() {
|
|
1340
545
|
super.connectedCallback();
|
|
1341
546
|
|
|
1342
|
-
this.
|
|
1343
|
-
|
|
1344
|
-
// this.addEventListener('keydown', this.handleKeyDown);
|
|
547
|
+
this.addEventListener('keydown', this.handleKeyDown);
|
|
1345
548
|
this.addEventListener('auroMenuOption-click', this.handleMouseSelect);
|
|
1346
549
|
this.addEventListener('auroMenuOption-mouseover', this.handleOptionHover);
|
|
1347
550
|
this.addEventListener('slotchange', this.handleSlotChange);
|
|
@@ -1349,7 +552,7 @@ class AuroMenu extends AuroElement {
|
|
|
1349
552
|
}
|
|
1350
553
|
|
|
1351
554
|
disconnectedCallback() {
|
|
1352
|
-
|
|
555
|
+
this.removeEventListener('keydown', this.handleKeyDown);
|
|
1353
556
|
this.removeEventListener('auroMenuOption-click', this.handleMouseSelect);
|
|
1354
557
|
this.removeEventListener('auroMenuOption-mouseover', this.handleOptionHover);
|
|
1355
558
|
this.removeEventListener('slotchange', this.handleSlotChange);
|
|
@@ -1364,25 +567,114 @@ class AuroMenu extends AuroElement {
|
|
|
1364
567
|
this.initializeMenu();
|
|
1365
568
|
}
|
|
1366
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Sets an attribute that matches the default tag name if the tag name is not the default.
|
|
572
|
+
* @param {string} tagName - The tag name to set as an attribute.
|
|
573
|
+
* @private
|
|
574
|
+
*/
|
|
575
|
+
setTagAttribute(tagName) {
|
|
576
|
+
if (this.tagName.toLowerCase() !== tagName) {
|
|
577
|
+
this.setAttribute(tagName, true);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
1367
580
|
|
|
581
|
+
// eslint-disable-next-line complexity
|
|
1368
582
|
updated(changedProperties) {
|
|
1369
583
|
super.updated(changedProperties);
|
|
1370
584
|
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1375
|
-
//
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
this.menuService.selectByValue(this.value);
|
|
585
|
+
// Single source of truth for 'auroMenu-selectedOption'. Selection handlers
|
|
586
|
+
// mutate optionSelected and let Lit's update cycle dispatch here; the prior
|
|
587
|
+
// .value comparison missed multi-select array changes and combined with the
|
|
588
|
+
// explicit calls in handleDeselectState/makeSelection produced 2-3 duplicate
|
|
589
|
+
// events per selection.
|
|
590
|
+
if (changedProperties.has('optionSelected')) {
|
|
591
|
+
this.notifySelectionChange();
|
|
1379
592
|
}
|
|
1380
593
|
|
|
1381
|
-
//
|
|
1382
|
-
if (changedProperties.has('
|
|
1383
|
-
this.
|
|
594
|
+
// Reset selection if multiSelect mode changes
|
|
595
|
+
if (changedProperties.has('multiSelect') && !changedProperties.has("value")) {
|
|
596
|
+
this.clearSelection();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (changedProperties.has("value")) {
|
|
600
|
+
// Ensure items are populated before matching. `firstUpdated` normally initializes them,
|
|
601
|
+
// but a `value` change can arrive before slotted options are appended (e.g. parent sets
|
|
602
|
+
// value before children render). Without this guard, matching against an empty `items`
|
|
603
|
+
// would falsely dispatch `auroMenu-selectValueFailure` for valid initial values.
|
|
604
|
+
if (!this.items) {
|
|
605
|
+
this.initItems();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Handle null/undefined/empty case — empty/whitespace strings clear selection
|
|
609
|
+
// consistently with selectByValue(''), and avoid downstream `.includes('')` matches.
|
|
610
|
+
if (this.value === undefined || this.value === null || (typeof this.value === 'string' && this.value.trim() === '')) {
|
|
611
|
+
this.clearSelection();
|
|
612
|
+
} else {
|
|
613
|
+
let newSelected = null;
|
|
614
|
+
|
|
615
|
+
if (this.multiSelect) {
|
|
616
|
+
// In multiselect mode, this.value should be an array of strings.
|
|
617
|
+
// Defensive default: `formattedValue` can be undefined for unexpected value types,
|
|
618
|
+
// and calling `.includes` on undefined would throw during reconciliation.
|
|
619
|
+
const valueArray = this.formattedValue || [];
|
|
620
|
+
const matchingOptions = this.items ? this.items.filter((item) => valueArray.includes(item.value)) : [];
|
|
621
|
+
newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
|
|
622
|
+
} else {
|
|
623
|
+
// In single-select mode, this.value should be a string
|
|
624
|
+
const matchingOption = this.items ? this.items.find((item) => item.value === this.value) : undefined;
|
|
625
|
+
|
|
626
|
+
if (matchingOption) {
|
|
627
|
+
newSelected = matchingOption;
|
|
628
|
+
this._index = this.items.indexOf(matchingOption);
|
|
629
|
+
} else {
|
|
630
|
+
// If no matching option found, reset selection
|
|
631
|
+
newSelected = undefined;
|
|
632
|
+
this._index = -1;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// If no matching options were found in either mode
|
|
637
|
+
if (!newSelected || (Array.isArray(newSelected) && newSelected.length === 0)) {
|
|
638
|
+
// Defer failure when no options are loaded yet (async pattern: parent sets
|
|
639
|
+
// value before slotted options render). handleSlotChange re-runs matching
|
|
640
|
+
// once items arrive. Without this guard, a valid preselected value gets
|
|
641
|
+
// cleared by the failure listener before options ever exist to match against.
|
|
642
|
+
const hasItemsToMatch = this.items && this.items.length > 0;
|
|
643
|
+
if (hasItemsToMatch) {
|
|
644
|
+
// Clear state BEFORE dispatching so synchronous listeners (e.g. auro-select's
|
|
645
|
+
// updateDisplayedValue) read fresh `optionSelected` rather than the stale prior
|
|
646
|
+
// selection and re-render the old label.
|
|
647
|
+
if (this.optionSelected !== undefined) {
|
|
648
|
+
this.optionSelected = undefined;
|
|
649
|
+
}
|
|
650
|
+
this._index = -1;
|
|
651
|
+
dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
|
|
652
|
+
}
|
|
653
|
+
} else if (!this.selectionEquals(this.optionSelected, newSelected)) {
|
|
654
|
+
this.optionSelected = newSelected;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Update UI state
|
|
659
|
+
this.updateItemsState(new Map([
|
|
660
|
+
[
|
|
661
|
+
'optionSelected',
|
|
662
|
+
true
|
|
663
|
+
]
|
|
664
|
+
]));
|
|
665
|
+
|
|
666
|
+
// Notify of changes
|
|
667
|
+
if (this.optionSelected !== undefined) {
|
|
668
|
+
const selected = Array.isArray(this.optionSelected) ? this.optionSelected : [this.optionSelected];
|
|
669
|
+
selected.forEach((opt) => {
|
|
670
|
+
if (opt.hasAttribute('event')) {
|
|
671
|
+
this.handleCustomEvent(opt);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
1384
675
|
}
|
|
1385
676
|
|
|
677
|
+
// Process all other UI updates
|
|
1386
678
|
if (changedProperties.has('multiSelect') && this.rootMenu) {
|
|
1387
679
|
if (this.multiSelect) {
|
|
1388
680
|
this.setAttribute('aria-multiselectable', 'true');
|
|
@@ -1390,30 +682,129 @@ class AuroMenu extends AuroElement {
|
|
|
1390
682
|
this.removeAttribute('aria-multiselectable');
|
|
1391
683
|
}
|
|
1392
684
|
}
|
|
685
|
+
|
|
686
|
+
this.updateItemsState(changedProperties);
|
|
1393
687
|
}
|
|
1394
688
|
|
|
1395
689
|
/**
|
|
1396
|
-
*
|
|
1397
|
-
* @param {string} tagName - The tag name to set as an attribute.
|
|
690
|
+
* Updates the UI state and appearance of menu items based on changed properties.
|
|
1398
691
|
* @private
|
|
692
|
+
* @param {Map<string, boolean>} changedProperties - LitElement's changed properties map.
|
|
1399
693
|
*/
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
694
|
+
updateItemsState(changedProperties) {
|
|
695
|
+
// Handle loading state changes
|
|
696
|
+
if (changedProperties.has('loading')) {
|
|
697
|
+
this.setAttribute("aria-busy", this.loading);
|
|
698
|
+
dispatchMenuEvent(this, "auroMenu-loadingChange", {
|
|
699
|
+
loading: this.loading,
|
|
700
|
+
hasLoadingPlaceholder: this.hasLoadingPlaceholder
|
|
701
|
+
});
|
|
1403
702
|
}
|
|
1404
|
-
}
|
|
1405
703
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
704
|
+
if (!this.items) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Handle noCheckmark propagation to all menus and options.
|
|
709
|
+
// Propagate in both directions so toggling back to false also clears nested elements
|
|
710
|
+
// (otherwise nested menus/options would stay stuck in no-checkmark mode).
|
|
711
|
+
if (changedProperties.has('noCheckmark')) {
|
|
712
|
+
this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]').forEach((element) => {
|
|
713
|
+
element.noCheckmark = this.noCheckmark;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Handle layout propagation to all menus and options.
|
|
718
|
+
// Skip elements that had size/shape set by the author (marked in initItems);
|
|
719
|
+
// explicit per-option overrides must survive menu-level propagation.
|
|
720
|
+
const propagationTargets = this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]');
|
|
721
|
+
[
|
|
722
|
+
'size',
|
|
723
|
+
'shape'
|
|
724
|
+
].forEach((prop) => {
|
|
725
|
+
if (changedProperties.has(prop)) {
|
|
726
|
+
const explicitKey = prop === 'size' ? '_explicitSize' : '_explicitShape';
|
|
727
|
+
propagationTargets.forEach((el) => {
|
|
728
|
+
if (el[explicitKey]) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
el.setAttribute(prop, this[prop]);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Regex for matchWord if needed
|
|
737
|
+
let regexWord = null;
|
|
738
|
+
|
|
739
|
+
if (changedProperties.has('matchWord') && this.matchWord && this.matchWord.length) {
|
|
740
|
+
const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
741
|
+
regexWord = new RegExp(escapedWord, 'giu');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Handle direct item updates
|
|
745
|
+
this.items.forEach((option) => {
|
|
746
|
+
// Update selection if option or value changed
|
|
747
|
+
if (changedProperties.has('optionSelected') || changedProperties.has('value')) {
|
|
748
|
+
const isSelected = this.isOptionSelected(option);
|
|
749
|
+
option.setAttribute('aria-selected', isSelected ? 'true' : 'false');
|
|
750
|
+
|
|
751
|
+
// Add/remove selected attribute based on state
|
|
752
|
+
if (isSelected) {
|
|
753
|
+
option.setAttribute('selected', '');
|
|
754
|
+
} else {
|
|
755
|
+
option.removeAttribute('selected');
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Update text highlighting if matchWord changed
|
|
760
|
+
if (changedProperties.has('matchWord') && regexWord &&
|
|
761
|
+
isOptionInteractive(option) && !option.hasAttribute('persistent')) {
|
|
762
|
+
// Create nested spacers
|
|
763
|
+
const nested = option.querySelectorAll('.nestingSpacer');
|
|
764
|
+
|
|
765
|
+
const displayValueEl = option.querySelector('[slot="displayValue"]');
|
|
766
|
+
if (displayValueEl) {
|
|
767
|
+
option.removeChild(displayValueEl);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Build highlighted content via DOM APIs rather than innerHTML so any
|
|
771
|
+
// `<`, `>`, or `&` in the option text renders literally (prevents XSS).
|
|
772
|
+
const originalText = option.textContent;
|
|
773
|
+
option.textContent = '';
|
|
774
|
+
|
|
775
|
+
nested.forEach(() => {
|
|
776
|
+
const spacer = document.createElement('span');
|
|
777
|
+
spacer.className = 'nestingSpacer';
|
|
778
|
+
option.appendChild(spacer);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const matches = [...originalText.matchAll(regexWord)];
|
|
782
|
+
let lastIndex = 0;
|
|
783
|
+
matches.forEach((match) => {
|
|
784
|
+
const [matchText] = match;
|
|
785
|
+
if (match.index > lastIndex) {
|
|
786
|
+
option.appendChild(document.createTextNode(originalText.slice(lastIndex, match.index)));
|
|
787
|
+
}
|
|
788
|
+
const strong = document.createElement('strong');
|
|
789
|
+
strong.textContent = matchText;
|
|
790
|
+
option.appendChild(strong);
|
|
791
|
+
lastIndex = match.index + matchText.length;
|
|
792
|
+
});
|
|
793
|
+
if (lastIndex < originalText.length) {
|
|
794
|
+
option.appendChild(document.createTextNode(originalText.slice(lastIndex)));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (displayValueEl) {
|
|
798
|
+
option.append(displayValueEl);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Update disabled state
|
|
803
|
+
if (changedProperties.has('disabled')) {
|
|
804
|
+
option.disabled = this.disabled;
|
|
805
|
+
}
|
|
1416
806
|
});
|
|
807
|
+
|
|
1417
808
|
}
|
|
1418
809
|
|
|
1419
810
|
// Init Methods
|
|
@@ -1423,6 +814,7 @@ class AuroMenu extends AuroElement {
|
|
|
1423
814
|
* @private
|
|
1424
815
|
*/
|
|
1425
816
|
initializeMenu() {
|
|
817
|
+
this.initItems();
|
|
1426
818
|
if (this.rootMenu) {
|
|
1427
819
|
this.setAttribute('role', 'listbox');
|
|
1428
820
|
this.setAttribute('root', '');
|
|
@@ -1432,15 +824,117 @@ class AuroMenu extends AuroElement {
|
|
|
1432
824
|
}
|
|
1433
825
|
}
|
|
1434
826
|
|
|
827
|
+
// Must run for nested menus too — sets level, role="group", and aria-label="submenu" based on parent.
|
|
1435
828
|
this.handleNestedMenus(this);
|
|
1436
829
|
}
|
|
1437
830
|
|
|
1438
831
|
/**
|
|
1439
|
-
*
|
|
1440
|
-
* @
|
|
832
|
+
* Initializes menu items and their attributes.
|
|
833
|
+
* @private
|
|
1441
834
|
*/
|
|
1442
|
-
|
|
1443
|
-
this.
|
|
835
|
+
initItems() {
|
|
836
|
+
const found = Array.from(this.querySelectorAll('auro-menuoption, [auro-menuoption]'));
|
|
837
|
+
this.items = found.length > 0 ? found : undefined;
|
|
838
|
+
|
|
839
|
+
// Record whether each propagation target had an author-set size/shape attribute
|
|
840
|
+
// BEFORE menu has had a chance to propagate. Marker is set once per element so a
|
|
841
|
+
// later menu-driven setAttribute doesn't re-flag the element as "explicit".
|
|
842
|
+
this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]').forEach((el) => {
|
|
843
|
+
if (el._explicitSize === undefined) {
|
|
844
|
+
el._explicitSize = el.hasAttribute('size');
|
|
845
|
+
}
|
|
846
|
+
if (el._explicitShape === undefined) {
|
|
847
|
+
el._explicitShape = el.hasAttribute('shape');
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
if (this.noCheckmark) {
|
|
852
|
+
this.updateItemsState(new Map([
|
|
853
|
+
[
|
|
854
|
+
'noCheckmark',
|
|
855
|
+
true
|
|
856
|
+
]
|
|
857
|
+
]));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
this.dispatchEvent(new CustomEvent('auroMenu-optionsChange', {
|
|
861
|
+
detail: {
|
|
862
|
+
options: this.items
|
|
863
|
+
}
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Logic Methods
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Updates menu state when an option is selected.
|
|
871
|
+
* @private
|
|
872
|
+
* @param {HTMLElement} option - The option element to select.
|
|
873
|
+
*/
|
|
874
|
+
handleSelectState(option) {
|
|
875
|
+
if (this.multiSelect) {
|
|
876
|
+
const currentValue = this.formattedValue || [];
|
|
877
|
+
const currentSelected = this.optionSelected || [];
|
|
878
|
+
|
|
879
|
+
if (!currentValue.includes(option.value)) {
|
|
880
|
+
this.value = JSON.stringify([
|
|
881
|
+
...currentValue,
|
|
882
|
+
option.value
|
|
883
|
+
]);
|
|
884
|
+
}
|
|
885
|
+
if (!currentSelected.includes(option)) {
|
|
886
|
+
this.optionSelected = [
|
|
887
|
+
...currentSelected,
|
|
888
|
+
option
|
|
889
|
+
];
|
|
890
|
+
}
|
|
891
|
+
} else {
|
|
892
|
+
this.value = option.value;
|
|
893
|
+
this.optionSelected = option;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
this._index = this.items.indexOf(option);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Deselects a menu option and updates related state.
|
|
901
|
+
* @private
|
|
902
|
+
* @param {HTMLElement} option - The menuoption to be deselected.
|
|
903
|
+
*/
|
|
904
|
+
handleDeselectState(option) {
|
|
905
|
+
if (this.multiSelect) {
|
|
906
|
+
// Remove this option from array
|
|
907
|
+
const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
|
|
908
|
+
|
|
909
|
+
// If array is empty after removal, set back to undefined
|
|
910
|
+
if (newFormattedValue && newFormattedValue.length === 0) {
|
|
911
|
+
this.value = undefined;
|
|
912
|
+
} else {
|
|
913
|
+
this.value = JSON.stringify(newFormattedValue);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
this.optionSelected = this.optionSelected.filter((val) => val !== option);
|
|
917
|
+
if (this.optionSelected.length === 0) {
|
|
918
|
+
this.optionSelected = undefined;
|
|
919
|
+
}
|
|
920
|
+
} else {
|
|
921
|
+
// For single-select: Back to undefined when deselected
|
|
922
|
+
this.value = undefined;
|
|
923
|
+
this.optionSelected = undefined;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Update the index tracking
|
|
927
|
+
this._index = this.items.indexOf(option);
|
|
928
|
+
|
|
929
|
+
// Update UI to reflect changes
|
|
930
|
+
this.updateItemsState(new Map([
|
|
931
|
+
[
|
|
932
|
+
'optionSelected',
|
|
933
|
+
true
|
|
934
|
+
]
|
|
935
|
+
]));
|
|
936
|
+
|
|
937
|
+
// Notification happens via updated() when optionSelected changes above.
|
|
1444
938
|
}
|
|
1445
939
|
|
|
1446
940
|
/**
|
|
@@ -1459,7 +953,18 @@ class AuroMenu extends AuroElement {
|
|
|
1459
953
|
* @public
|
|
1460
954
|
*/
|
|
1461
955
|
reset() {
|
|
1462
|
-
|
|
956
|
+
// Reset to undefined - initial state
|
|
957
|
+
this.value = undefined;
|
|
958
|
+
this.optionSelected = undefined;
|
|
959
|
+
this._index = -1;
|
|
960
|
+
|
|
961
|
+
// Reset UI state
|
|
962
|
+
this.updateItemsState(new Map([
|
|
963
|
+
[
|
|
964
|
+
'optionSelected',
|
|
965
|
+
true
|
|
966
|
+
]
|
|
967
|
+
]));
|
|
1463
968
|
|
|
1464
969
|
// Dispatch reset event
|
|
1465
970
|
dispatchMenuEvent(this, 'auroMenu-selectValueReset');
|
|
@@ -1471,6 +976,14 @@ class AuroMenu extends AuroElement {
|
|
|
1471
976
|
* @param {HTMLElement} menu - Root menu element.
|
|
1472
977
|
*/
|
|
1473
978
|
handleNestedMenus(menu) {
|
|
979
|
+
// Slot changes can fire on a menu mid-teardown (e.g. while a parent menu
|
|
980
|
+
// is removing children to rebuild its content). In that window the menu
|
|
981
|
+
// is detached and parentElement is null. Skip — handleNestedMenus will
|
|
982
|
+
// run again when the menu is reattached.
|
|
983
|
+
if (!menu.parentElement) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
1474
987
|
menu.level = menu.parentElement.level >= 0 ? menu.parentElement.level + 1 : 0;
|
|
1475
988
|
|
|
1476
989
|
if (menu.level > 0) {
|
|
@@ -1481,34 +994,221 @@ class AuroMenu extends AuroElement {
|
|
|
1481
994
|
}
|
|
1482
995
|
}
|
|
1483
996
|
|
|
1484
|
-
const options = menu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption]');
|
|
1485
|
-
options.forEach((option) => {
|
|
1486
|
-
const regex = new RegExp(this.nestingSpacer, "gu");
|
|
1487
|
-
option.innerHTML = this.nestingSpacer.repeat(menu.level) + option.innerHTML.replace(regex, '');
|
|
1488
|
-
});
|
|
997
|
+
const options = menu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption]');
|
|
998
|
+
options.forEach((option) => {
|
|
999
|
+
const regex = new RegExp(this.nestingSpacer, "gu");
|
|
1000
|
+
option.innerHTML = this.nestingSpacer.repeat(menu.level) + option.innerHTML.replace(regex, '');
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Event Handlers
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Makes a selection based on the current index.
|
|
1008
|
+
* @private
|
|
1009
|
+
*/
|
|
1010
|
+
makeSelection() {
|
|
1011
|
+
if (!this.items) {
|
|
1012
|
+
this.initItems();
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Get currently selected menu option based on index
|
|
1016
|
+
const option = this.items ? this.items[this._index] : undefined;
|
|
1017
|
+
|
|
1018
|
+
// Return early if option is not interactive
|
|
1019
|
+
if (!option || !isOptionInteractive(option)) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Handle custom events first
|
|
1024
|
+
if (option.hasAttribute('event')) {
|
|
1025
|
+
this.handleCustomEvent(option);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (this.multiSelect) {
|
|
1030
|
+
// In multiselect, toggle individual selections
|
|
1031
|
+
this.toggleOption(option);
|
|
1032
|
+
// In single select, only handle selection of new options
|
|
1033
|
+
} else if (!this.isOptionSelected(option)) {
|
|
1034
|
+
this.clearSelection();
|
|
1035
|
+
this.handleSelectState(option);
|
|
1036
|
+
} else {
|
|
1037
|
+
// Re-selecting the already-selected option in single-select doesn't change
|
|
1038
|
+
// state, so updated() won't fire. Notify explicitly so consumers (e.g.
|
|
1039
|
+
// auro-select closing its dropdown on Enter) still get the event.
|
|
1040
|
+
this.notifySelectionChange();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Toggle the selection state of the menuoption.
|
|
1046
|
+
* @private
|
|
1047
|
+
* @param {HTMLElement} option - The menuoption to toggle.
|
|
1048
|
+
*/
|
|
1049
|
+
toggleOption(option) {
|
|
1050
|
+
const isCurrentlySelected = this.isOptionSelected(option);
|
|
1051
|
+
|
|
1052
|
+
if (isCurrentlySelected) {
|
|
1053
|
+
this.handleDeselectState(option);
|
|
1054
|
+
} else if (option.value === undefined || option.value === '') {
|
|
1055
|
+
dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
|
|
1056
|
+
} else {
|
|
1057
|
+
this.handleSelectState(option);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Handles keyboard navigation and selection.
|
|
1063
|
+
* @private
|
|
1064
|
+
* @param {KeyboardEvent} event - The keydown event.
|
|
1065
|
+
*/
|
|
1066
|
+
handleKeyDown(event) {
|
|
1067
|
+
switch (event.key) {
|
|
1068
|
+
case "ArrowDown":
|
|
1069
|
+
event.preventDefault();
|
|
1070
|
+
this.navigateOptions('down');
|
|
1071
|
+
break;
|
|
1072
|
+
case "ArrowUp":
|
|
1073
|
+
event.preventDefault();
|
|
1074
|
+
this.navigateOptions('up');
|
|
1075
|
+
break;
|
|
1076
|
+
case "Tab":
|
|
1077
|
+
// Do not preventDefault on Tab so focus can move out of the menu (a11y: avoid trapping keyboard users).
|
|
1078
|
+
this.makeSelection();
|
|
1079
|
+
break;
|
|
1080
|
+
case "Enter":
|
|
1081
|
+
event.preventDefault();
|
|
1082
|
+
this.makeSelection();
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Handles option selection via click events from menuoptions.
|
|
1089
|
+
* @private
|
|
1090
|
+
* @param {CustomEvent} event - The auroMenuOption-click event.
|
|
1091
|
+
*/
|
|
1092
|
+
handleMouseSelect(event) {
|
|
1093
|
+
if (!this.rootMenu || this.disabled) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const option = event.detail;
|
|
1098
|
+
if (option && this.items) {
|
|
1099
|
+
const idx = this.items.indexOf(option);
|
|
1100
|
+
if (idx >= 0) {
|
|
1101
|
+
this._index = idx;
|
|
1102
|
+
this.makeSelection();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Handles option hover events.
|
|
1109
|
+
* @private
|
|
1110
|
+
* @param {CustomEvent} event - Event object from the browser.
|
|
1111
|
+
*/
|
|
1112
|
+
handleOptionHover(event) {
|
|
1113
|
+
const option = event.detail;
|
|
1114
|
+
if (this.items) {
|
|
1115
|
+
const idx = this.items.indexOf(option);
|
|
1116
|
+
if (idx >= 0) {
|
|
1117
|
+
this.updateActiveOption(idx);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Handles slot change events.
|
|
1124
|
+
* @private
|
|
1125
|
+
*/
|
|
1126
|
+
handleSlotChange() {
|
|
1127
|
+
if (this.parentElement && this.parentElement.closest('auro-menu, [auro-menu]')) {
|
|
1128
|
+
this.rootMenu = false;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Nested menus must also reinitialize so items, level, role="group", and aria-label refresh on content changes.
|
|
1132
|
+
// Root-specific attributes (listbox/root/aria-multiselectable) remain gated by `rootMenu` inside initializeMenu.
|
|
1133
|
+
this.initializeMenu();
|
|
1134
|
+
|
|
1135
|
+
// When options arrive after `value` was set (async option load), re-run matching
|
|
1136
|
+
// against the now-populated items. The earlier updated('value') call deferred
|
|
1137
|
+
// the failure dispatch because items were empty; this triggers the match now.
|
|
1138
|
+
const hasPendingValue = this.value !== undefined &&
|
|
1139
|
+
this.value !== null &&
|
|
1140
|
+
!(typeof this.value === 'string' && this.value.trim() === '');
|
|
1141
|
+
if (hasPendingValue && this.items && this.items.length > 0 && this.optionSelected === undefined) {
|
|
1142
|
+
this.requestUpdate('value', undefined);
|
|
1143
|
+
}
|
|
1489
1144
|
}
|
|
1490
1145
|
|
|
1491
1146
|
/**
|
|
1492
|
-
* Navigates
|
|
1493
|
-
* @param {
|
|
1494
|
-
* @protected
|
|
1147
|
+
* Navigates through options using keyboard.
|
|
1148
|
+
* @param {string} direction - 'up' or 'down'.
|
|
1495
1149
|
*/
|
|
1496
1150
|
navigateOptions(direction) {
|
|
1497
|
-
if
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1151
|
+
// Return early if no items exist
|
|
1152
|
+
if (!this.items || !this.items.length) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
let newIndex = this._index;
|
|
1157
|
+
if (newIndex === -1 && direction === 'up') {
|
|
1158
|
+
newIndex = this.items.length;
|
|
1159
|
+
}
|
|
1160
|
+
const increment = direction === 'down' ? 1 : -1;
|
|
1161
|
+
const maxIterations = this.items.length;
|
|
1162
|
+
let iterations = 0;
|
|
1163
|
+
let foundInteractiveOption = false;
|
|
1164
|
+
|
|
1165
|
+
do {
|
|
1166
|
+
newIndex = (newIndex + increment + this.items.length) % this.items.length;
|
|
1167
|
+
iterations += 1;
|
|
1168
|
+
|
|
1169
|
+
// Check if current option is interactive
|
|
1170
|
+
const currentOption = this.items[newIndex];
|
|
1171
|
+
if (isOptionInteractive(currentOption)) {
|
|
1172
|
+
foundInteractiveOption = true;
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Break if all options were checked
|
|
1177
|
+
if (iterations >= maxIterations) {
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
} while (iterations < maxIterations);
|
|
1181
|
+
|
|
1182
|
+
// Update only if an interactive option was found
|
|
1183
|
+
if (foundInteractiveOption) {
|
|
1184
|
+
this.updateActiveOption(newIndex);
|
|
1501
1185
|
}
|
|
1502
1186
|
}
|
|
1503
1187
|
|
|
1504
1188
|
/**
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1189
|
+
* Updates the active option state and dispatches events.
|
|
1190
|
+
* Accepts either a numeric index or an HTMLElement option.
|
|
1191
|
+
* @param {number|HTMLElement} indexOrOption - Index of the option or the option element to make active.
|
|
1507
1192
|
*/
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1193
|
+
updateActiveOption(indexOrOption) {
|
|
1194
|
+
let idx = -1;
|
|
1195
|
+
|
|
1196
|
+
if (typeof indexOrOption === 'number') {
|
|
1197
|
+
idx = indexOrOption;
|
|
1198
|
+
} else {
|
|
1199
|
+
idx = this.items ? this.items.indexOf(indexOrOption) : -1;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (!this.items || !this.items[idx]) {
|
|
1203
|
+
return;
|
|
1511
1204
|
}
|
|
1205
|
+
|
|
1206
|
+
this.items.forEach((item) => item.classList.remove('active'));
|
|
1207
|
+
this.items[idx].classList.add('active');
|
|
1208
|
+
this.optionActive = this.items[idx];
|
|
1209
|
+
this._index = idx;
|
|
1210
|
+
|
|
1211
|
+
dispatchMenuEvent(this, 'auroMenu-activatedOption', this.items[idx]);
|
|
1512
1212
|
}
|
|
1513
1213
|
|
|
1514
1214
|
/**
|
|
@@ -1518,8 +1218,8 @@ class AuroMenu extends AuroElement {
|
|
|
1518
1218
|
*/
|
|
1519
1219
|
handleCustomEvent(option) {
|
|
1520
1220
|
const eventName = option.getAttribute('event');
|
|
1521
|
-
dispatchMenuEvent(this, eventName);
|
|
1522
|
-
dispatchMenuEvent(this, 'auroMenu-customEventFired');
|
|
1221
|
+
dispatchMenuEvent(this, eventName, { option });
|
|
1222
|
+
dispatchMenuEvent(this, 'auroMenu-customEventFired', { option });
|
|
1523
1223
|
}
|
|
1524
1224
|
|
|
1525
1225
|
/**
|
|
@@ -1527,14 +1227,33 @@ class AuroMenu extends AuroElement {
|
|
|
1527
1227
|
* @param {any} source - The source that triggers this event.
|
|
1528
1228
|
* @private
|
|
1529
1229
|
*/
|
|
1530
|
-
notifySelectionChange(
|
|
1531
|
-
dispatchMenuEvent(this, 'auroMenu-selectedOption', {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1230
|
+
notifySelectionChange(source = undefined) {
|
|
1231
|
+
dispatchMenuEvent(this, 'auroMenu-selectedOption', { source });
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* @private
|
|
1236
|
+
* @param {any} current - Current selection.
|
|
1237
|
+
* @param {any} next - New selection to compare.
|
|
1238
|
+
* @returns {boolean} Whether the selections are equal.
|
|
1239
|
+
*/
|
|
1240
|
+
selectionEquals(current, next) {
|
|
1241
|
+
if (current === next) {
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
if (!current || !next) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
if (Array.isArray(current) && Array.isArray(next)) {
|
|
1248
|
+
if (current.length !== next.length) {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
// Compare as sets — selection order may differ from DOM order after value-driven
|
|
1252
|
+
// reconciliation, but the selected set is what matters for change detection.
|
|
1253
|
+
const nextSet = new Set(next);
|
|
1254
|
+
return current.every((item) => nextSet.has(item));
|
|
1255
|
+
}
|
|
1256
|
+
return false;
|
|
1538
1257
|
}
|
|
1539
1258
|
|
|
1540
1259
|
/**
|
|
@@ -1549,7 +1268,6 @@ class AuroMenu extends AuroElement {
|
|
|
1549
1268
|
}
|
|
1550
1269
|
|
|
1551
1270
|
if (this.multiSelect) {
|
|
1552
|
-
// In multi-select mode, check if the option is in the selected array
|
|
1553
1271
|
return Array.isArray(this.optionSelected) && this.optionSelected.some((selectedOption) => selectedOption === option);
|
|
1554
1272
|
}
|
|
1555
1273
|
|
|
@@ -1692,37 +1410,12 @@ let menuOptionIdCounter = 0;
|
|
|
1692
1410
|
* @slot default - The default slot for the menu option text.
|
|
1693
1411
|
*
|
|
1694
1412
|
* @event auroMenuOption-mouseover - Notifies that this option has been hovered over.
|
|
1413
|
+
* @event auroMenuOption-click - Notifies that this option has been clicked.
|
|
1695
1414
|
*/
|
|
1696
1415
|
class AuroMenuOption extends AuroElement {
|
|
1697
|
-
|
|
1698
|
-
/**
|
|
1699
|
-
* This will register this element with the browser.
|
|
1700
|
-
* @param {string} [name="auro-menuoption"] - The name of the element that you want to register.
|
|
1701
|
-
*
|
|
1702
|
-
* @example
|
|
1703
|
-
* AuroMenuOption.register("custom-menuoption") // this will register this element to <custom-menuoption/>
|
|
1704
|
-
*
|
|
1705
|
-
*/
|
|
1706
|
-
static register(name = "auro-menuoption") {
|
|
1707
|
-
AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroMenuOption);
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
/**
|
|
1711
|
-
* Returns whether the menu option is currently active and selectable.
|
|
1712
|
-
* An option is considered active if it is not hidden, not disabled, and not static.
|
|
1713
|
-
* @returns {boolean} True if the option is active, false otherwise.
|
|
1714
|
-
*/
|
|
1715
|
-
get isActive() {
|
|
1716
|
-
return !this.hasAttribute('hidden') &&
|
|
1717
|
-
!this.disabled &&
|
|
1718
|
-
!this.hasAttribute('static');
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
1416
|
constructor() {
|
|
1722
1417
|
super();
|
|
1723
1418
|
|
|
1724
|
-
this.bindEvents();
|
|
1725
|
-
|
|
1726
1419
|
/**
|
|
1727
1420
|
* @private
|
|
1728
1421
|
*/
|
|
@@ -1747,109 +1440,57 @@ class AuroMenuOption extends AuroElement {
|
|
|
1747
1440
|
/**
|
|
1748
1441
|
* @private
|
|
1749
1442
|
*/
|
|
1750
|
-
this.
|
|
1751
|
-
|
|
1752
|
-
// Initialize context-related properties
|
|
1753
|
-
this.menuService = null;
|
|
1754
|
-
this.unsubscribe = null;
|
|
1443
|
+
this.tabIndex = -1;
|
|
1755
1444
|
|
|
1756
1445
|
/**
|
|
1757
1446
|
* @private
|
|
1758
1447
|
*/
|
|
1759
|
-
this.
|
|
1448
|
+
this.runtimeUtils = new AuroLibraryRuntimeUtils();
|
|
1449
|
+
|
|
1450
|
+
this.addEventListener('click', this.handleClick.bind(this));
|
|
1760
1451
|
}
|
|
1761
1452
|
|
|
1762
1453
|
static get properties() {
|
|
1763
1454
|
return {
|
|
1764
1455
|
...super.properties,
|
|
1765
|
-
|
|
1766
|
-
/**
|
|
1767
|
-
* When true, disables the menu option.
|
|
1768
|
-
*/
|
|
1769
|
-
disabled: {
|
|
1456
|
+
noCheckmark: {
|
|
1770
1457
|
type: Boolean,
|
|
1771
1458
|
reflect: true
|
|
1772
1459
|
},
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
* @private
|
|
1776
|
-
*/
|
|
1777
|
-
event: {
|
|
1778
|
-
type: String,
|
|
1460
|
+
selected: {
|
|
1461
|
+
type: Boolean,
|
|
1779
1462
|
reflect: true
|
|
1780
1463
|
},
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
*/
|
|
1785
|
-
layout: {
|
|
1786
|
-
type: String
|
|
1464
|
+
disabled: {
|
|
1465
|
+
type: Boolean,
|
|
1466
|
+
reflect: true
|
|
1787
1467
|
},
|
|
1788
|
-
|
|
1789
|
-
/**
|
|
1790
|
-
* Allows users to set a unique key for the menu option for specified option selection. If no key is provided, the value property will be used.
|
|
1791
|
-
*/
|
|
1792
|
-
key: {
|
|
1468
|
+
value: {
|
|
1793
1469
|
type: String,
|
|
1794
1470
|
reflect: true
|
|
1795
1471
|
},
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
*/
|
|
1800
|
-
menuService: {
|
|
1801
|
-
type: Object,
|
|
1802
|
-
state: true
|
|
1472
|
+
tabIndex: {
|
|
1473
|
+
type: Number,
|
|
1474
|
+
reflect: true
|
|
1803
1475
|
},
|
|
1804
1476
|
|
|
1805
1477
|
/**
|
|
1806
1478
|
* @private
|
|
1807
1479
|
*/
|
|
1808
|
-
|
|
1480
|
+
event: {
|
|
1809
1481
|
type: String,
|
|
1810
|
-
state: true
|
|
1811
|
-
},
|
|
1812
|
-
|
|
1813
|
-
/**
|
|
1814
|
-
* @private
|
|
1815
|
-
*/
|
|
1816
|
-
noCheckmark: {
|
|
1817
|
-
type: Boolean,
|
|
1818
1482
|
reflect: true
|
|
1819
1483
|
},
|
|
1820
1484
|
|
|
1821
1485
|
/**
|
|
1822
|
-
* When true, marks this option as the "no matching results" placeholder shown by combobox
|
|
1486
|
+
* When true, marks this option as the "no matching results" placeholder shown by combobox
|
|
1487
|
+
* when the user's input does not match any available options.
|
|
1823
1488
|
*/
|
|
1824
1489
|
noMatch: {
|
|
1825
1490
|
type: Boolean,
|
|
1826
1491
|
reflect: true,
|
|
1827
1492
|
attribute: 'nomatch'
|
|
1828
1493
|
},
|
|
1829
|
-
|
|
1830
|
-
/**
|
|
1831
|
-
* Specifies that an option is selected.
|
|
1832
|
-
*/
|
|
1833
|
-
selected: {
|
|
1834
|
-
type: Boolean,
|
|
1835
|
-
reflect: true
|
|
1836
|
-
},
|
|
1837
|
-
|
|
1838
|
-
/**
|
|
1839
|
-
* Specifies the tab index of the menu option.
|
|
1840
|
-
*/
|
|
1841
|
-
tabIndex: {
|
|
1842
|
-
type: Number,
|
|
1843
|
-
reflect: true
|
|
1844
|
-
},
|
|
1845
|
-
|
|
1846
|
-
/**
|
|
1847
|
-
* Specifies the value to be sent to a server.
|
|
1848
|
-
*/
|
|
1849
|
-
value: {
|
|
1850
|
-
type: String,
|
|
1851
|
-
reflect: true
|
|
1852
|
-
},
|
|
1853
1494
|
};
|
|
1854
1495
|
}
|
|
1855
1496
|
|
|
@@ -1861,41 +1502,48 @@ class AuroMenuOption extends AuroElement {
|
|
|
1861
1502
|
];
|
|
1862
1503
|
}
|
|
1863
1504
|
|
|
1505
|
+
/**
|
|
1506
|
+
* This will register this element with the browser.
|
|
1507
|
+
* @param {string} [name="auro-menuoption"] - The name of element that you want to register to.
|
|
1508
|
+
*
|
|
1509
|
+
* @example
|
|
1510
|
+
* AuroMenuOption.register("custom-menuoption") // this will register this element to <custom-menuoption/>
|
|
1511
|
+
*
|
|
1512
|
+
*/
|
|
1513
|
+
static register(name = "auro-menuoption") {
|
|
1514
|
+
AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroMenuOption);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Returns whether the menu option is currently active and selectable.
|
|
1519
|
+
* @returns {boolean}
|
|
1520
|
+
*/
|
|
1521
|
+
get isActive() {
|
|
1522
|
+
return !this.hasAttribute('hidden') &&
|
|
1523
|
+
!this.disabled &&
|
|
1524
|
+
!this.hasAttribute('static');
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1864
1527
|
connectedCallback() {
|
|
1865
1528
|
super.connectedCallback();
|
|
1866
1529
|
|
|
1867
|
-
// Add the tag name as an attribute if it is different than the component name
|
|
1868
|
-
// Add this step soon as this node gets attached to the DOM to avoid racing condition with menu's value setting logic.
|
|
1869
1530
|
this.runtimeUtils.handleComponentTagRename(this, 'auro-menuoption');
|
|
1870
|
-
|
|
1871
|
-
// Set up context consumption in connectedCallback
|
|
1872
|
-
this._contextConsumer = new ContextConsumer(this, {
|
|
1873
|
-
context: MenuContext,
|
|
1874
|
-
callback: this.attachTo.bind(this),
|
|
1875
|
-
subscribe: true
|
|
1876
|
-
});
|
|
1877
|
-
|
|
1878
|
-
// Establish the key property as early as possible.
|
|
1879
|
-
// When a framework (e.g. Svelte) inserts the element into the DOM before
|
|
1880
|
-
// setting its `value` property, both `getAttribute('value')` and
|
|
1881
|
-
// `getAttribute('key')` return null here. Setting `this.key = null`
|
|
1882
|
-
// would block the fallback in `updated()` that assigns key from the
|
|
1883
|
-
// value property (the guard checked `=== undefined`). Only assign key
|
|
1884
|
-
// if at least one source attribute is actually present so that the
|
|
1885
|
-
// `updated()` fallback can run when the value property arrives later.
|
|
1886
|
-
const valueAttr = this.getAttribute('value');
|
|
1887
|
-
const keyAttr = this.getAttribute('key');
|
|
1888
|
-
const resolvedKey = keyAttr !== null ? keyAttr : valueAttr;
|
|
1889
|
-
if (resolvedKey !== null) {
|
|
1890
|
-
this.key = resolvedKey;
|
|
1891
|
-
}
|
|
1892
1531
|
}
|
|
1893
1532
|
|
|
1894
1533
|
firstUpdated() {
|
|
1895
|
-
// Add the tag name as an attribute if it is different than the component name
|
|
1896
1534
|
this.runtimeUtils.handleComponentTagRename(this, 'auro-menuoption');
|
|
1897
1535
|
|
|
1898
|
-
//
|
|
1536
|
+
// firstUpdated can fire on an option that was detached before the first
|
|
1537
|
+
// render completed (e.g. dynamic-menu rebuilds clear and repopulate the
|
|
1538
|
+
// menu mid-cycle). parentElement is null in that case — fall back to
|
|
1539
|
+
// defaults; the next render after re-attachment will pick up real values.
|
|
1540
|
+
if (!this.hasAttribute('size')) {
|
|
1541
|
+
this.size = this.parentElement ? this.parentElement.getAttribute('size') || 'sm' : 'sm';
|
|
1542
|
+
}
|
|
1543
|
+
if (!this.hasAttribute('shape')) {
|
|
1544
|
+
this.shape = this.parentElement ? this.parentElement.getAttribute('shape') || 'box' : 'box';
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1899
1547
|
if (!this.id) {
|
|
1900
1548
|
menuOptionIdCounter += 1;
|
|
1901
1549
|
this.id = `menuoption-${menuOptionIdCounter}`;
|
|
@@ -1917,16 +1565,8 @@ class AuroMenuOption extends AuroElement {
|
|
|
1917
1565
|
updated(changedProperties) {
|
|
1918
1566
|
super.updated(changedProperties);
|
|
1919
1567
|
|
|
1920
|
-
// Update aria-selected attribute if selected changed
|
|
1921
1568
|
if (changedProperties.has('selected')) {
|
|
1922
|
-
|
|
1923
|
-
// Update aria-selected attribute
|
|
1924
1569
|
this.setAttribute('aria-selected', this.selected.toString());
|
|
1925
|
-
|
|
1926
|
-
// Update menu service selection state if this isn't an internal update
|
|
1927
|
-
if (this.internalUpdateInProgress !== true && this.menuService) {
|
|
1928
|
-
this.menuService[this.selected ? 'selectOption' : 'deselectOption'](this);
|
|
1929
|
-
}
|
|
1930
1570
|
}
|
|
1931
1571
|
|
|
1932
1572
|
if (changedProperties.has('disabled')) {
|
|
@@ -1936,231 +1576,48 @@ class AuroMenuOption extends AuroElement {
|
|
|
1936
1576
|
this.removeAttribute('aria-disabled');
|
|
1937
1577
|
}
|
|
1938
1578
|
}
|
|
1939
|
-
|
|
1940
|
-
if (changedProperties.has('active')) {
|
|
1941
|
-
this.updateActiveClasses();
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
// Update text highlight if matchWord changed
|
|
1945
|
-
if (changedProperties.has('matchWord')) {
|
|
1946
|
-
this.updateTextHighlight();
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// Set the key to be the passed value if no key is provided.
|
|
1950
|
-
// Loose equality (== null) is intentional: it catches both null AND
|
|
1951
|
-
// undefined. When a framework (e.g. Svelte, React) inserts the element
|
|
1952
|
-
// before setting its value property, connectedCallback skips key
|
|
1953
|
-
// assignment because both attributes are null at that point. The Lit
|
|
1954
|
-
// property default for `key` is undefined (not null), so strict
|
|
1955
|
-
// === null would miss the case and the fallback would never run.
|
|
1956
|
-
if (changedProperties.has('value') && this.key == null) { // eslint-disable-line eqeqeq, no-eq-null
|
|
1957
|
-
this.key = this.value;
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
disconnectedCallback() {
|
|
1962
|
-
if (this.menuService) {
|
|
1963
|
-
this.menuService.unsubscribe(this.handleMenuChange);
|
|
1964
|
-
this.menuService.removeMenuOption(this);
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
/**
|
|
1969
|
-
* Sets up event listeners for user interaction with the menu option.
|
|
1970
|
-
* This function enables click and mouse enter events to trigger selection and highlighting logic.
|
|
1971
|
-
*/
|
|
1972
|
-
bindEvents() {
|
|
1973
|
-
this.addEventListener('click', this.handleClick.bind(this));
|
|
1974
|
-
this.addEventListener('mouseenter', this.handleMouseEnter.bind(this));
|
|
1975
1579
|
}
|
|
1976
1580
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
* This method enables the option to participate in menu selection and highlighting logic.
|
|
1980
|
-
* @param {Object} service - The menu service instance to attach to.
|
|
1981
|
-
*/
|
|
1982
|
-
attachTo(service) {
|
|
1983
|
-
if (!service) {
|
|
1984
|
-
return;
|
|
1985
|
-
}
|
|
1986
|
-
this.menuService = service;
|
|
1987
|
-
this.menuService.addMenuOption(this);
|
|
1988
|
-
this.menuService.subscribe(this.handleMenuChange);
|
|
1581
|
+
handleMenuChange() {
|
|
1582
|
+
// no-op: menu owns state in the distributed architecture
|
|
1989
1583
|
}
|
|
1990
1584
|
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
* This function synchronizes the option's properties and selection/highlight state with menu events.
|
|
1994
|
-
* @param {Object} event - The event object from the menu service.
|
|
1995
|
-
*/
|
|
1996
|
-
handleMenuChange(event) {
|
|
1997
|
-
|
|
1998
|
-
// Ignore events without a type or property
|
|
1999
|
-
if (!event || (!event.type && !event.property)) {
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
// Update reactive properties based on event type
|
|
2004
|
-
if (event.property && Object.keys(AuroMenuOption.properties).includes(event.property)) {
|
|
2005
|
-
this[event.property] = event.value;
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
// Handle highlight changes
|
|
2009
|
-
if (event.type === 'highlightChange') {
|
|
2010
|
-
const isActive = event.option === this;
|
|
2011
|
-
this.active = isActive;
|
|
2012
|
-
this.updateActiveClasses();
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
if (event.type === 'stateChange') {
|
|
2016
|
-
const isSelected = event.selectedOptions.includes(this);
|
|
2017
|
-
this.setInternalSelected(isSelected);
|
|
2018
|
-
}
|
|
1585
|
+
setSelected(value) {
|
|
1586
|
+
this.selected = value;
|
|
2019
1587
|
}
|
|
2020
1588
|
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
this.internalUpdateInProgress = true;
|
|
2028
|
-
this.selected = isSelected;
|
|
2029
|
-
|
|
2030
|
-
// Fire custom event if selected
|
|
2031
|
-
if (isSelected) {
|
|
2032
|
-
this.handleCustomEvent();
|
|
1589
|
+
updateActive(active) {
|
|
1590
|
+
this.active = active;
|
|
1591
|
+
if (active) {
|
|
1592
|
+
this.classList.add('active');
|
|
1593
|
+
} else {
|
|
1594
|
+
this.classList.remove('active');
|
|
2033
1595
|
}
|
|
2034
|
-
|
|
2035
|
-
setTimeout(() => {
|
|
2036
|
-
this.internalUpdateInProgress = false;
|
|
2037
|
-
}, 0);
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
/**
|
|
2041
|
-
* Sets the selected state of the menu option.
|
|
2042
|
-
* This function updates whether the option is currently selected.
|
|
2043
|
-
* @param {boolean} isSelected - Whether the option should be marked as selected.
|
|
2044
|
-
* @deprecated Simply modify the `selected` property directly instead.
|
|
2045
|
-
*/
|
|
2046
|
-
setSelected(isSelected) {
|
|
2047
|
-
this.selected = isSelected;
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
/**
|
|
2051
|
-
* Updates the active state and visual highlighting of the menu option.
|
|
2052
|
-
* This function toggles the option's active status and applies or removes the active CSS class.
|
|
2053
|
-
* @param {boolean} isActive - Whether the option should be marked as active.
|
|
2054
|
-
* @deprecated Simply modify the `active` property directly instead.
|
|
2055
|
-
*/
|
|
2056
|
-
updateActive(isActive) {
|
|
2057
|
-
|
|
2058
|
-
// Set active state
|
|
2059
|
-
this.active = isActive;
|
|
2060
|
-
this.updateActiveClasses();
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
/**
|
|
2064
|
-
* Updates the CSS class for the menu option based on its active state.
|
|
2065
|
-
* This function adds or removes the 'active' class to visually indicate the option's active status.
|
|
2066
|
-
* @private
|
|
2067
|
-
*/
|
|
2068
|
-
updateActiveClasses() {
|
|
2069
|
-
// Update class based on active state
|
|
2070
|
-
if (this.active) this.classList.add('active');
|
|
2071
|
-
else this.classList.remove('active');
|
|
2072
1596
|
}
|
|
2073
1597
|
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
* Updates the visual highlighting of text within the menu option based on the current match word.
|
|
2077
|
-
* This function highlights matching text segments and manages nested spacers for display formatting.
|
|
2078
|
-
* @private
|
|
2079
|
-
*/
|
|
2080
|
-
updateTextHighlight() {
|
|
2081
|
-
|
|
2082
|
-
// Regex for matchWord if needed
|
|
2083
|
-
let regexWord = null;
|
|
2084
|
-
|
|
2085
|
-
if (this.matchWord && this.matchWord.length) {
|
|
2086
|
-
const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
2087
|
-
regexWord = new RegExp(escapedWord, 'giu');
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
// Update text highlighting if matchWord changed
|
|
2091
|
-
if (regexWord &&
|
|
2092
|
-
this.isActive && !this.hasAttribute('persistent')) {
|
|
2093
|
-
const nested = this.querySelectorAll('.nestingSpacer');
|
|
2094
|
-
|
|
2095
|
-
const displayValueEl = this.querySelector('[slot="displayValue"]');
|
|
2096
|
-
if (displayValueEl) {
|
|
2097
|
-
this.removeChild(displayValueEl);
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
// Create nested spacers
|
|
2101
|
-
const nestingSpacerBundle = [...nested].map(() => this.nestingSpacer).join('');
|
|
2102
|
-
|
|
2103
|
-
// Update with spacers and matchWord
|
|
2104
|
-
this.innerHTML = nestingSpacerBundle +
|
|
2105
|
-
this.textContent.replace(
|
|
2106
|
-
regexWord,
|
|
2107
|
-
(match) => `<strong>${match}</strong>`
|
|
2108
|
-
);
|
|
2109
|
-
if (displayValueEl) {
|
|
2110
|
-
this.append(displayValueEl);
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
1598
|
+
attachTo() {
|
|
1599
|
+
// no-op: menu owns state in the distributed architecture
|
|
2113
1600
|
}
|
|
2114
1601
|
|
|
2115
1602
|
/**
|
|
2116
|
-
* Handles click events on the menu option
|
|
2117
|
-
* This function dispatches a click event and updates selection if the option is not disabled.
|
|
1603
|
+
* Handles click events on the menu option.
|
|
2118
1604
|
* @private
|
|
2119
1605
|
*/
|
|
2120
1606
|
handleClick() {
|
|
2121
|
-
if (!this.disabled && !this.menuService?.disabled) {
|
|
2122
|
-
this.dispatchClickEvent();
|
|
2123
|
-
this.selected = !this.selected;
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
/**
|
|
2128
|
-
* Handles mouse enter events to highlight the menu option.
|
|
2129
|
-
* This function updates the menu service to set this option as the currently highlighted item if not disabled.
|
|
2130
|
-
* @private
|
|
2131
|
-
*/
|
|
2132
|
-
handleMouseEnter() {
|
|
2133
1607
|
if (!this.disabled) {
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
if (this.event) {
|
|
2145
|
-
dispatchMenuEvent(this, this.event, { option: this });
|
|
2146
|
-
dispatchMenuEvent(this, 'auroMenu-customEventFired', { option: this });
|
|
1608
|
+
// Pure event emitter: the parent menu owns selection state and will
|
|
1609
|
+
// update `selected` via setSelected(). Toggling here desyncs the option
|
|
1610
|
+
// UI from auro-menu.optionSelected (e.g. single-select re-click on the
|
|
1611
|
+
// already-selected option would flip the option off while the menu keeps it on).
|
|
1612
|
+
this.dispatchEvent(new CustomEvent('auroMenuOption-click', {
|
|
1613
|
+
bubbles: true,
|
|
1614
|
+
cancelable: false,
|
|
1615
|
+
composed: true,
|
|
1616
|
+
detail: this
|
|
1617
|
+
}));
|
|
2147
1618
|
}
|
|
2148
1619
|
}
|
|
2149
1620
|
|
|
2150
|
-
/**
|
|
2151
|
-
* Dispatches a click event for this menu option.
|
|
2152
|
-
* This function notifies listeners that the option has been clicked.
|
|
2153
|
-
* @private
|
|
2154
|
-
*/
|
|
2155
|
-
dispatchClickEvent() {
|
|
2156
|
-
this.dispatchEvent(new CustomEvent('auroMenuOption-click', {
|
|
2157
|
-
bubbles: true,
|
|
2158
|
-
cancelable: false,
|
|
2159
|
-
composed: true,
|
|
2160
|
-
detail: this
|
|
2161
|
-
}));
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
1621
|
/**
|
|
2165
1622
|
* Generates an HTML element containing an SVG icon based on the provided `svgContent`.
|
|
2166
1623
|
*
|
|
@@ -2200,8 +1657,8 @@ class AuroMenuOption extends AuroElement {
|
|
|
2200
1657
|
return html$1`
|
|
2201
1658
|
<div class="${classes}">
|
|
2202
1659
|
${this.selected && !this.noCheckmark
|
|
2203
|
-
|
|
2204
|
-
|
|
1660
|
+
? this.generateIconHtml(checkmarkIcon.svg)
|
|
1661
|
+
: undefined}
|
|
2205
1662
|
<slot></slot>
|
|
2206
1663
|
</div>
|
|
2207
1664
|
`;
|