@aurodesignsystem-dev/auro-formkit 0.0.0-pr1503.4 → 0.0.0-pr1503.5

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.
Files changed (190) hide show
  1. package/components/bibtemplate/dist/index.js +1 -1
  2. package/components/bibtemplate/dist/registered.js +1 -1
  3. package/components/checkbox/demo/accessibility.md +1 -1
  4. package/components/checkbox/demo/customize.html +1 -2
  5. package/components/checkbox/demo/customize.min.js +23 -8
  6. package/components/checkbox/demo/getting-started.min.js +23 -8
  7. package/components/checkbox/demo/index.min.js +23 -8
  8. package/components/checkbox/dist/index.js +23 -8
  9. package/components/checkbox/dist/registered.js +23 -8
  10. package/components/combobox/README.md +1 -1
  11. package/components/combobox/demo/accessibility.md +1 -1
  12. package/components/combobox/demo/api.md +2 -2
  13. package/components/combobox/demo/customize.html +1 -2
  14. package/components/combobox/demo/customize.md +166 -142
  15. package/components/combobox/demo/customize.min.js +2515 -2316
  16. package/components/combobox/demo/getting-started.min.js +2515 -2312
  17. package/components/combobox/demo/index.md +2 -1
  18. package/components/combobox/demo/index.min.js +2515 -2312
  19. package/components/combobox/demo/keyboard-behavior.md +3 -146
  20. package/components/combobox/demo/readme.md +1 -1
  21. package/components/combobox/demo/why-combobox.md +2 -2
  22. package/components/combobox/dist/auro-combobox.d.ts +49 -15
  23. package/components/combobox/dist/index.js +1462 -753
  24. package/components/combobox/dist/registered.js +1462 -753
  25. package/components/counter/demo/customize.min.js +188 -244
  26. package/components/counter/demo/index.min.js +188 -244
  27. package/components/counter/dist/auro-counter.d.ts +0 -8
  28. package/components/counter/dist/index.js +188 -244
  29. package/components/counter/dist/registered.js +188 -244
  30. package/components/datepicker/demo/accessibility.md +20 -10
  31. package/components/datepicker/demo/api.md +65 -62
  32. package/components/datepicker/demo/customize.md +180 -40
  33. package/components/datepicker/demo/customize.min.js +1690 -777
  34. package/components/datepicker/demo/getting-started.md +118 -2
  35. package/components/datepicker/demo/index.min.js +1690 -759
  36. package/components/datepicker/demo/keyboard-behavior.md +3 -3
  37. package/components/datepicker/demo/voiceover.md +4 -4
  38. package/components/datepicker/demo/why-datepicker.md +2 -2
  39. package/components/datepicker/dist/{src/auro-calendar-cell.d.ts → auro-calendar-cell.d.ts} +48 -3
  40. package/components/datepicker/dist/{src/auro-calendar.d.ts → auro-calendar.d.ts} +188 -10
  41. package/components/datepicker/dist/{src/auro-datepicker.d.ts → auro-datepicker.d.ts} +89 -6
  42. package/components/datepicker/dist/blackoutUtils.d.ts +43 -0
  43. package/components/datepicker/dist/index.js +1690 -759
  44. package/components/datepicker/dist/registered.js +1690 -759
  45. package/components/dropdown/demo/customize.html +3 -0
  46. package/components/dropdown/demo/customize.min.js +142 -208
  47. package/components/dropdown/demo/getting-started.min.js +131 -207
  48. package/components/dropdown/demo/index.min.js +131 -207
  49. package/components/dropdown/dist/auro-dropdown.d.ts +17 -1
  50. package/components/dropdown/dist/index.js +131 -207
  51. package/components/dropdown/dist/registered.js +131 -207
  52. package/components/form/demo/api.md +3 -3
  53. package/components/form/demo/customize.html +6 -6
  54. package/components/form/demo/customize.md +535 -135
  55. package/components/form/demo/customize.min.js +6307 -4378
  56. package/components/form/demo/getting-started.md +344 -96
  57. package/components/form/demo/getting-started.min.js +6255 -4378
  58. package/components/form/demo/index.min.js +6206 -4378
  59. package/components/form/demo/registerDemoDeps.min.js +5827 -4333
  60. package/components/form/dist/auro-form.d.ts +128 -10
  61. package/components/form/dist/index.js +377 -43
  62. package/components/form/dist/registered.js +377 -43
  63. package/components/input/demo/accessibility.md +1 -1
  64. package/components/input/demo/api.md +58 -57
  65. package/components/input/demo/customize.html +1 -2
  66. package/components/input/demo/customize.md +62 -61
  67. package/components/input/demo/customize.min.js +396 -275
  68. package/components/input/demo/getting-started.min.js +396 -275
  69. package/components/input/demo/index.min.js +396 -275
  70. package/components/input/dist/auro-input.d.ts +1 -1
  71. package/components/input/dist/base-input.d.ts +60 -81
  72. package/components/input/dist/index.js +409 -276
  73. package/components/input/dist/registered.js +396 -275
  74. package/components/input/dist/utilities.d.ts +10 -1
  75. package/components/menu/demo/accessibility.md +9 -4
  76. package/components/menu/demo/api.md +48 -45
  77. package/components/menu/demo/css-only.md +26 -19
  78. package/components/menu/demo/customize.md +307 -75
  79. package/components/menu/demo/design.md +1 -1
  80. package/components/menu/demo/getting-started.md +144 -7
  81. package/components/menu/demo/index.min.js +961 -1341
  82. package/components/menu/demo/keyboard-behavior.md +83 -4
  83. package/components/menu/demo/voiceover.md +21 -14
  84. package/components/menu/demo/why-menu.md +8 -9
  85. package/components/menu/dist/auro-menu-utils.d.ts +19 -0
  86. package/components/menu/dist/auro-menu.d.ts +97 -116
  87. package/components/menu/dist/auro-menuoption.d.ts +39 -133
  88. package/components/menu/dist/index.js +823 -1309
  89. package/components/menu/dist/registered.js +835 -1309
  90. package/components/radio/demo/accessibility.md +1 -1
  91. package/components/radio/demo/customize.min.js +23 -8
  92. package/components/radio/demo/getting-started.min.js +23 -8
  93. package/components/radio/demo/index.min.js +23 -8
  94. package/components/radio/dist/index.js +23 -8
  95. package/components/radio/dist/registered.js +23 -8
  96. package/components/select/demo/accessibility.md +6 -1
  97. package/components/select/demo/api.md +3 -2
  98. package/components/select/demo/customize.html +1 -2
  99. package/components/select/demo/customize.md +210 -5
  100. package/components/select/demo/customize.min.js +1613 -1854
  101. package/components/select/demo/design.md +10 -10
  102. package/components/select/demo/getting-started.md +1 -1
  103. package/components/select/demo/getting-started.min.js +1617 -1903
  104. package/components/select/demo/index.md +2 -2
  105. package/components/select/demo/index.min.js +1613 -1854
  106. package/components/select/demo/keyboard-behavior.md +81 -54
  107. package/components/select/demo/voiceover.md +28 -15
  108. package/components/select/dist/auro-select.d.ts +70 -12
  109. package/components/select/dist/index.js +580 -315
  110. package/components/select/dist/registered.js +580 -315
  111. package/components/select/dist/selectUtils.d.ts +24 -0
  112. package/custom-elements.json +8071 -7919
  113. package/package.json +7 -3
  114. package/components/checkbox/demo/customize.js +0 -22
  115. package/components/checkbox/demo/demo-support.js +0 -1
  116. package/components/checkbox/demo/getting-started.js +0 -22
  117. package/components/checkbox/demo/index.js +0 -8
  118. package/components/checkbox/demo/styles.scss +0 -1
  119. package/components/combobox/demo/customize.js +0 -24
  120. package/components/combobox/demo/demo-support.js +0 -1
  121. package/components/combobox/demo/getting-started.js +0 -21
  122. package/components/combobox/demo/index.js +0 -23
  123. package/components/combobox/demo/styles.scss +0 -1
  124. package/components/counter/demo/customize.js +0 -21
  125. package/components/counter/demo/demo-support.js +0 -1
  126. package/components/counter/demo/index.js +0 -21
  127. package/components/counter/demo/styles.scss +0 -1
  128. package/components/datepicker/demo/customize.js +0 -19
  129. package/components/datepicker/demo/demo-support.js +0 -1
  130. package/components/datepicker/demo/index.js +0 -23
  131. package/components/datepicker/demo/styles.scss +0 -1
  132. package/components/dropdown/demo/customize.js +0 -9
  133. package/components/dropdown/demo/demo-support.js +0 -1
  134. package/components/dropdown/demo/getting-started.js +0 -9
  135. package/components/dropdown/demo/index.js +0 -16
  136. package/components/dropdown/demo/styles.scss +0 -1
  137. package/components/form/demo/customize.js +0 -9
  138. package/components/form/demo/demo-support.js +0 -1
  139. package/components/form/demo/getting-started.js +0 -9
  140. package/components/form/demo/index.js +0 -5
  141. package/components/form/demo/registerDemoDeps.js +0 -23
  142. package/components/form/demo/styles.scss +0 -1
  143. package/components/input/demo/customize.js +0 -25
  144. package/components/input/demo/demo-support.js +0 -1
  145. package/components/input/demo/getting-started.js +0 -8
  146. package/components/input/demo/index.js +0 -8
  147. package/components/input/demo/styles.css +0 -974
  148. package/components/input/demo/styles.scss +0 -1
  149. package/components/menu/demo/demo-support.js +0 -1
  150. package/components/menu/demo/index.js +0 -25
  151. package/components/menu/demo/styles.scss +0 -1
  152. package/components/menu/dist/auro-menu.context.d.ts +0 -238
  153. package/components/radio/demo/customize.js +0 -22
  154. package/components/radio/demo/demo-support.js +0 -1
  155. package/components/radio/demo/getting-started.js +0 -24
  156. package/components/radio/demo/index.js +0 -8
  157. package/components/radio/demo/styles.scss +0 -1
  158. package/components/select/demo/customize.js +0 -11
  159. package/components/select/demo/demo-support.js +0 -1
  160. package/components/select/demo/getting-started.js +0 -26
  161. package/components/select/demo/index.js +0 -11
  162. package/components/select/demo/styles.scss +0 -1
  163. /package/components/counter/dist/styles/{counter-wrapper-color-css.d.ts → counter-group-color-css.d.ts} +0 -0
  164. /package/components/datepicker/dist/{src/auro-calendar-month.d.ts → auro-calendar-month.d.ts} +0 -0
  165. /package/components/datepicker/dist/{src/buttonVersion.d.ts → buttonVersion.d.ts} +0 -0
  166. /package/components/datepicker/dist/{src/datepickerKeyboardStrategy.d.ts → datepickerKeyboardStrategy.d.ts} +0 -0
  167. /package/components/datepicker/dist/{src/iconVersion.d.ts → iconVersion.d.ts} +0 -0
  168. /package/components/datepicker/dist/{src/index.d.ts → index.d.ts} +0 -0
  169. /package/components/datepicker/dist/{src/popoverVersion.d.ts → popoverVersion.d.ts} +0 -0
  170. /package/components/datepicker/dist/{src/styles → styles}/classic/color-css.d.ts +0 -0
  171. /package/components/datepicker/dist/{src/styles → styles}/classic/style-css.d.ts +0 -0
  172. /package/components/datepicker/dist/{src/styles → styles}/color-calendar-css.d.ts +0 -0
  173. /package/components/datepicker/dist/{src/styles → styles}/color-cell-css.d.ts +0 -0
  174. /package/components/datepicker/dist/{src/styles → styles}/color-css.d.ts +0 -0
  175. /package/components/datepicker/dist/{src/styles → styles}/color-month-css.d.ts +0 -0
  176. /package/components/datepicker/dist/{src/styles → styles}/shapeSize-css.d.ts +0 -0
  177. /package/components/datepicker/dist/{src/styles → styles}/snowflake/color-css.d.ts +0 -0
  178. /package/components/datepicker/dist/{src/styles → styles}/snowflake/style-css.d.ts +0 -0
  179. /package/components/datepicker/dist/{src/styles → styles}/style-auro-calendar-cell-css.d.ts +0 -0
  180. /package/components/datepicker/dist/{src/styles → styles}/style-auro-calendar-css.d.ts +0 -0
  181. /package/components/datepicker/dist/{src/styles → styles}/style-auro-calendar-month-css.d.ts +0 -0
  182. /package/components/datepicker/dist/{src/styles → styles}/style-css.d.ts +0 -0
  183. /package/components/datepicker/dist/{src/styles → styles}/tokens-css.d.ts +0 -0
  184. /package/components/datepicker/dist/{src/utilities.d.ts → utilities.d.ts} +0 -0
  185. /package/components/datepicker/dist/{src/utilitiesCalendar.d.ts → utilitiesCalendar.d.ts} +0 -0
  186. /package/components/datepicker/dist/{src/utilitiesCalendarRender.d.ts → utilitiesCalendarRender.d.ts} +0 -0
  187. /package/components/datepicker/dist/{src/vendor → vendor}/wc-range-datepicker/day.d.ts +0 -0
  188. /package/components/datepicker/dist/{src/vendor → vendor}/wc-range-datepicker/range-datepicker-calendar.d.ts +0 -0
  189. /package/components/datepicker/dist/{src/vendor → vendor}/wc-range-datepicker/range-datepicker-cell.d.ts +0 -0
  190. /package/components/datepicker/dist/{src/vendor → vendor}/wc-range-datepicker/range-datepicker.d.ts +0 -0
@@ -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
 
@@ -956,6 +241,18 @@ function arrayConverter(value) {
956
241
  throw new Error('Invalid value: Input must be an array or undefined');
957
242
  }
958
243
 
244
+ /**
245
+ * Serializes a multi-select value array back into the String `value` property.
246
+ * An empty (or missing) array collapses to `undefined` so an emptied selection
247
+ * clears `value` rather than reflecting a `"[]"` attribute.
248
+ * @private
249
+ * @param {Array<string>|undefined} values - The selected values.
250
+ * @returns {string|undefined} JSON string of the values, or undefined when empty.
251
+ */
252
+ function serializeMultiSelectValue(values) {
253
+ return values && values.length > 0 ? JSON.stringify(values) : undefined;
254
+ }
255
+
959
256
  /**
960
257
  * Validates if an option can be interacted with.
961
258
  * @private
@@ -968,6 +265,20 @@ function isOptionInteractive(option) {
968
265
  !option.hasAttribute('static');
969
266
  }
970
267
 
268
+ /**
269
+ * Validates if an option may be selected by matching a programmatic value.
270
+ * Unlike `isOptionInteractive`, `hidden` is allowed: the combobox toggles
271
+ * `hidden` as its type-ahead filter, so a filtered-out option is still a
272
+ * valid programmatic selection. Only disabled and static options — which are
273
+ * never selectable — are rejected.
274
+ * @param {HTMLElement} option - The option to check.
275
+ * @returns {boolean} True if option can be selected by value.
276
+ */
277
+ function isSelectableByValue(option) {
278
+ return !option.hasAttribute('disabled') &&
279
+ !option.hasAttribute('static');
280
+ }
281
+
971
282
  /**
972
283
  * Helper method to dispatch custom events.
973
284
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -988,7 +299,7 @@ function dispatchMenuEvent(element, eventName, detail = null) {
988
299
  element.dispatchEvent(new CustomEvent(eventName, eventConfig));
989
300
  }
990
301
 
991
- /* eslint-disable no-underscore-dangle */
302
+ /* eslint-disable no-underscore-dangle, no-magic-numbers, max-lines, no-extra-parens, max-depth */
992
303
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
993
304
  // See LICENSE in the project root for license information.
994
305
 
@@ -1002,16 +313,12 @@ function dispatchMenuEvent(element, eventName, detail = null) {
1002
313
  * @event {CustomEvent<any>} auroMenu-customEventFired - Notifies that a custom event has been fired.
1003
314
  * @event {CustomEvent<{ loading: boolean; hasLoadingPlaceholder: boolean; }>} auroMenu-loadingChange - Notifies when the loading attribute is changed.
1004
315
  * @event {CustomEvent<any>} auroMenu-selectValueFailure - Notifies that an attempt to select a menuoption by matching a value has failed.
1005
- * @event {CustomEvent<{ values: HTMLElement[] }>} auroMenu-deselectPrevented - Notifies that deselection was prevented and includes the affected options in `detail.values`.
1006
316
  * @event {CustomEvent<any>} auroMenu-selectValueReset - Notifies that the component value has been reset.
1007
317
  * @event {CustomEvent<any>} auroMenu-selectedOption - Notifies that a new menuoption selection has been made.
1008
318
  * @slot loadingText - Text to show while loading attribute is set
1009
319
  * @slot loadingIcon - Icon to show while loading attribute is set
1010
320
  * @slot - Slot for insertion of menu options.
1011
321
  */
1012
-
1013
- /* eslint-disable max-lines */
1014
-
1015
322
  class AuroMenu extends AuroElement {
1016
323
 
1017
324
  constructor() {
@@ -1029,6 +336,8 @@ class AuroMenu extends AuroElement {
1029
336
  */
1030
337
  this.size = "sm";
1031
338
 
339
+ // Value of the selected options
340
+ this.value = undefined;
1032
341
  // Currently selected option
1033
342
  this.optionSelected = undefined;
1034
343
  // String used for highlighting/filtering
@@ -1041,13 +350,24 @@ class AuroMenu extends AuroElement {
1041
350
  this.loading = false;
1042
351
  // Multi-select mode
1043
352
  this.multiSelect = false;
1044
- // Allow deselecting of menu options
1045
- this.allowDeselect = false;
1046
- // Select all matching options when setting value in multi-select mode
1047
- this.selectAllMatchingOptions = false;
1048
353
 
1049
354
  // Event Bindings
1050
355
 
356
+ /**
357
+ * @private
358
+ */
359
+ this.handleKeyDown = this.handleKeyDown.bind(this);
360
+
361
+ /**
362
+ * @private
363
+ */
364
+ this.handleMouseSelect = this.handleMouseSelect.bind(this);
365
+
366
+ /**
367
+ * @private
368
+ */
369
+ this.handleOptionHover = this.handleOptionHover.bind(this);
370
+
1051
371
  /**
1052
372
  * @private
1053
373
  */
@@ -1074,14 +394,6 @@ class AuroMenu extends AuroElement {
1074
394
  return {
1075
395
  ...super.properties,
1076
396
 
1077
- /**
1078
- * Allows deselecting an already selected option when clicked again in single-select mode.
1079
- */
1080
- allowDeselect: {
1081
- type: Boolean,
1082
- reflect: true,
1083
- },
1084
-
1085
397
  /**
1086
398
  * When true, the entire menu and all options are disabled.
1087
399
  */
@@ -1090,20 +402,6 @@ class AuroMenu extends AuroElement {
1090
402
  reflect: true
1091
403
  },
1092
404
 
1093
- /**
1094
- * Indicates whether the menu has a loadingIcon or loadingText to render when in a loading state.
1095
- */
1096
- hasLoadingPlaceholder: {
1097
- type: Boolean
1098
- },
1099
-
1100
- /**
1101
- * @private
1102
- */
1103
- layout: {
1104
- type: String
1105
- },
1106
-
1107
405
  /**
1108
406
  * Indent level for submenus.
1109
407
  * @private
@@ -1150,60 +448,26 @@ class AuroMenu extends AuroElement {
1150
448
 
1151
449
  /**
1152
450
  * Specifies the current active menuOption.
451
+ * @readonly
1153
452
  */
1154
453
  optionActive: {
1155
454
  type: Object,
1156
- attribute: 'optionactive'
455
+ attribute: false
1157
456
  },
1158
457
 
1159
458
  /**
1160
- * An array of currently selected menu options, type `HTMLElement` by default. In multi-select mode, `optionSelected` is an array of HTML elements.
459
+ * The currently selected menu option(s). In single-select mode this is a single `HTMLElement` (or `undefined` when nothing is selected). In multi-select mode this is an array of `HTMLElement`s.
460
+ * @readonly
1161
461
  */
1162
462
  optionSelected: {
1163
463
  // Allow HTMLElement, HTMLElement[] arrays and undefined
1164
- type: Object
1165
- },
1166
-
1167
- /**
1168
- * Available menu options.
1169
- * @readonly
1170
- */
1171
- options: {
1172
- type: Array,
1173
- reflect: false,
464
+ type: Object,
1174
465
  attribute: false
1175
466
  },
1176
467
 
1177
- /**
1178
- * Sets the size of the menu.
1179
- * @type {'sm' | 'md'}
1180
- * @default 'sm'
1181
- */
1182
- size: {
1183
- type: String,
1184
- reflect: true
1185
- },
1186
-
1187
- /**
1188
- * When true, selects all options that match the provided value/key when setting value and multiselect is enabled.
1189
- */
1190
- selectAllMatchingOptions: {
1191
- type: Boolean,
1192
- reflect: true,
1193
- },
1194
-
1195
- /**
1196
- * Sets the shape of the menu.
1197
- * @type {'box' | 'round'}
1198
- * @default 'box'
1199
- */
1200
- shape: {
1201
- type: String,
1202
- reflect: true
1203
- },
1204
-
1205
468
  /**
1206
469
  * The value of the selected option. In multi-select mode, this is a JSON stringified array of selected option values.
470
+ * Options marked `disabled` or `static` are not selectable by value; `hidden` options remain selectable. In single-select mode, if the value matches a non-selectable option the selection is cleared (`optionSelected` becomes `undefined`) and `auroMenu-selectValueFailure` is dispatched. In multi-select mode, non-selectable entries are dropped from the value and the remaining selectable entries are selected; `auroMenu-selectValueFailure` is dispatched only when none of the entries match a selectable option.
1207
471
  */
1208
472
  value: {
1209
473
  type: String,
@@ -1213,48 +477,17 @@ class AuroMenu extends AuroElement {
1213
477
  };
1214
478
  }
1215
479
 
1216
- static get styles() {
1217
- return [
1218
- styleCss$1,
1219
- colorCss$1,
1220
- tokensCss
1221
- ];
1222
- }
1223
-
1224
- /**
1225
- * @readonly
1226
- * @returns {string} - Returns the label of the currently selected option(s).
1227
- */
1228
- get currentLabel() {
1229
- return this.menuService.currentLabel;
1230
- };
1231
-
1232
- /**
1233
- * @readonly
1234
- * @returns {Array<HTMLElement>} - Returns the array of available menu options.
1235
- * @deprecated Use `options` property instead.
1236
- */
1237
- get items() {
1238
- return this.options;
1239
- }
1240
-
1241
- /**
1242
- * @returns {number} - Returns the index of the currently active option.
1243
- */
1244
- get index() {
1245
- return this._index;
1246
- }
1247
-
1248
- /**
1249
- * @param {number} value - Sets the index of the currently active option.
1250
- */
1251
- set index(value) {
1252
- this.menuService.setHighlightedIndex(value);
1253
- }
1254
-
480
+ static get styles() {
481
+ return [
482
+ styleCss$1,
483
+ colorCss$1,
484
+ tokensCss
485
+ ];
486
+ }
487
+
1255
488
  /**
1256
489
  * This will register this element with the browser.
1257
- * @param {string} [name="auro-menu"] - The name of the element that you want to register.
490
+ * @param {string} [name="auro-menu"] - The name of element that you want to register to.
1258
491
  *
1259
492
  * @example
1260
493
  * AuroMenu.register("custom-menu") // this will register this element to <custom-menu/>
@@ -1265,126 +498,116 @@ class AuroMenu extends AuroElement {
1265
498
  }
1266
499
 
1267
500
  /**
1268
- * Formatted value based on `multiSelect` state.
1269
- * Default type is `String`, changing to `Array<String>` when `multiSelect` is true.
1270
- * @private
1271
- * @returns {String|Array<String>}
501
+ * @readonly
502
+ * @returns {Array<HTMLElement>} - Returns the array of available menu options.
1272
503
  */
1273
- get formattedValue() {
1274
- return this.menuService.currentValue;
504
+ get options() {
505
+ return this.items;
1275
506
  }
1276
507
 
1277
508
  /**
1278
- * Gets the current property values for the menu service.
1279
- * @private
1280
- * @returns {Object}
509
+ * @returns {number} - Returns the index of the currently active option.
1281
510
  */
1282
- get propertyValues() {
1283
- return {
1284
- size: this.size,
1285
- shape: this.shape,
1286
- noCheckmark: this.nocheckmark,
1287
- disabled: this.disabled
1288
- };
511
+ get index() {
512
+ return this._index;
1289
513
  }
1290
514
 
1291
515
  /**
1292
- * Provides the menu context to child components.
1293
- * Initializes the MenuService and subscribes to menu changes.
1294
- * @protected
516
+ * @param {number} value - Sets the index of the currently active option.
1295
517
  */
1296
- provideContext() {
1297
- if (this.parentElement && this.parentElement.closest('auro-menu, [auro-menu]')) {
1298
- this.rootMenu = false;
1299
- this.menuService = this.parentElement.menuService;
1300
- this._contextProvider = this.parentElement._contextProvider;
1301
- return;
1302
- }
1303
-
1304
- this.menuService = new MenuService({host: this});
1305
- this.menuService.setProperties(this.propertyValues);
1306
- this.menuService.subscribe(this.handleMenuChange.bind(this));
1307
- this._contextProvider = new ContextProvider(this, {
1308
- context: MenuContext,
1309
- initialValue: this.menuService
1310
- });
518
+ set index(value) {
519
+ this.updateActiveOption(value);
1311
520
  }
1312
521
 
1313
522
  /**
1314
- * Updates the currently active option in the menu.
1315
- * @param {HTMLElement} option - The option to set as active.
523
+ * Gets the currently selected options as an array.
524
+ * @returns {Array<HTMLElement>}
1316
525
  */
1317
- updateActiveOption(option) {
1318
- this.menuService.setHighlightedOption(option);
526
+ get selectedOptions() {
527
+ if (!this.optionSelected) {
528
+ return [];
529
+ }
530
+ if (Array.isArray(this.optionSelected)) {
531
+ return this.optionSelected;
532
+ }
533
+ return [this.optionSelected];
1319
534
  }
1320
535
 
1321
536
  /**
1322
- * Sets the internal value and manages update state.
1323
- * @param {String|Array<String>} value - The value to set.
1324
- * @protected
537
+ * Gets the first selected option, or null if none.
538
+ * @returns {HTMLElement|null}
1325
539
  */
1326
- setInternalValue(value) {
1327
- if (this.value !== value) {
1328
- this.internalUpdateInProgress = true;
1329
- this.value = value;
1330
-
1331
- setTimeout(() => {
1332
- this.internalUpdateInProgress = false;
1333
- });
1334
- }
540
+ get selectedOption() {
541
+ const opts = this.selectedOptions;
542
+ return opts.length > 0 ? opts[0] : null;
1335
543
  }
1336
544
 
1337
545
  /**
1338
- * Handles changes from the menu service and updates component state.
1339
- * @param {Object} event - The event object from the menu service.
1340
- * @protected
546
+ * @readonly
547
+ * @returns {string} - Returns the label of the currently selected option(s).
1341
548
  */
1342
- handleMenuChange(event) {
1343
- if (event.type === 'valueChange') {
1344
-
1345
- // New option is array value or first option with fallback to undefined for empty array in all cases
1346
- const newOption = this.multiSelect && event.options.length ? event.options : event.options[0] || undefined;
1347
- const newValue = event.stringValue;
1348
-
1349
- // Check if the option or value has actually changed
1350
- if (this.optionSelected !== newOption || this.stringValue !== newValue) {
1351
- this.optionSelected = newOption;
1352
- this.setInternalValue(newValue);
1353
- }
1354
-
1355
- // Notify components of selection change
1356
- this.notifySelectionChange(event);
1357
- }
1358
-
1359
- if (event.type === 'highlightChange') {
1360
- this.optionActive = event.option;
1361
- this._index = event.index;
549
+ get currentLabel() {
550
+ if (!this.optionSelected) {
551
+ return '';
1362
552
  }
1363
-
1364
- if (event.type === 'optionsChange') {
1365
- this.options = event.options;
1366
- this.dispatchEvent(new CustomEvent('auroMenu-optionsChange', {
1367
- detail: {
1368
- options: event.options
1369
- }
1370
- }));
553
+ if (Array.isArray(this.optionSelected)) {
554
+ return this.optionSelected.map((opt) => opt.textContent).join(', ');
1371
555
  }
556
+ return this.optionSelected.textContent || '';
1372
557
  }
1373
558
 
1374
559
  /**
1375
- * Gets the currently selected options.
1376
- * @returns {Array<HTMLElement>}
560
+ * Formatted value based on `multiSelect` state.
561
+ * Default type is `String`, changing to `Array<String>` when `multiSelect` is true.
562
+ * @private
563
+ * @returns {String|Array<String>}
1377
564
  */
1378
- get selectedOptions() {
1379
- return this.menuService ? this.menuService.selectedOptions : [];
565
+ get formattedValue() {
566
+ if (this.multiSelect) {
567
+ if (!this.value) {
568
+ return undefined;
569
+ }
570
+ // Defensive: `value` is declared as String, but consumers may assign arrays or other
571
+ // types programmatically. Normalize without throwing so render/update never hard-crashes.
572
+ if (Array.isArray(this.value)) {
573
+ return this.value;
574
+ }
575
+ if (typeof this.value !== 'string') {
576
+ return [String(this.value)];
577
+ }
578
+ if (this.value.startsWith("[")) {
579
+ // Malformed JSON (e.g. a literal string that happens to start with "[") falls back
580
+ // to a single-item array rather than throwing during render.
581
+ try {
582
+ // any valid JSON starting with `[` ALWAYS parses to an array
583
+ return JSON.parse(this.value);
584
+ } catch {
585
+ return [this.value];
586
+ }
587
+ }
588
+ return [this.value];
589
+ }
590
+ return this.value;
1380
591
  }
1381
592
 
1382
593
  /**
1383
- * Gets the first selected option, or null if none.
1384
- * @returns {HTMLElement|null}
594
+ * Selects options by value. Options marked `disabled` or `static` are not selectable; `hidden` options remain selectable. In single-select mode, if the value matches a non-selectable option the selection is cleared and `auroMenu-selectValueFailure` is dispatched. In multi-select mode, non-selectable entries are dropped and the remaining selectable entries are selected; `auroMenu-selectValueFailure` is dispatched only when none of the entries match a selectable option. Passing `undefined`, `null`, an empty string, or an empty array clears the selection without dispatching a failure.
595
+ * @param {string|string[]|undefined|null} value - The value(s) to select.
596
+ * @public
1385
597
  */
1386
- get selectedOption() {
1387
- return this.menuService ? this.menuService.selectedOptions[0] : null;
598
+ selectByValue(value) {
599
+ const isEmpty = value === undefined ||
600
+ value === null ||
601
+ (Array.isArray(value) && value.length === 0) ||
602
+ (typeof value === 'string' && value.trim() === '');
603
+
604
+ if (isEmpty) {
605
+ this.clearSelection();
606
+ return;
607
+ }
608
+
609
+ // `value` is a String property; stringify arrays so attribute reflection and `formattedValue` parsing stay correct.
610
+ this.value = Array.isArray(value) ? JSON.stringify(value) : value;
1388
611
  }
1389
612
 
1390
613
  // Lifecycle Methods
@@ -1392,9 +615,7 @@ class AuroMenu extends AuroElement {
1392
615
  connectedCallback() {
1393
616
  super.connectedCallback();
1394
617
 
1395
- this.provideContext();
1396
-
1397
- // this.addEventListener('keydown', this.handleKeyDown);
618
+ this.addEventListener('keydown', this.handleKeyDown);
1398
619
  this.addEventListener('auroMenuOption-click', this.handleMouseSelect);
1399
620
  this.addEventListener('auroMenuOption-mouseover', this.handleOptionHover);
1400
621
  this.addEventListener('slotchange', this.handleSlotChange);
@@ -1402,7 +623,7 @@ class AuroMenu extends AuroElement {
1402
623
  }
1403
624
 
1404
625
  disconnectedCallback() {
1405
- // this.removeEventListener('keydown', this.handleKeyDown);
626
+ this.removeEventListener('keydown', this.handleKeyDown);
1406
627
  this.removeEventListener('auroMenuOption-click', this.handleMouseSelect);
1407
628
  this.removeEventListener('auroMenuOption-mouseover', this.handleOptionHover);
1408
629
  this.removeEventListener('slotchange', this.handleSlotChange);
@@ -1417,25 +638,140 @@ class AuroMenu extends AuroElement {
1417
638
  this.initializeMenu();
1418
639
  }
1419
640
 
641
+ /**
642
+ * Sets an attribute that matches the default tag name if the tag name is not the default.
643
+ * @param {string} tagName - The tag name to set as an attribute.
644
+ * @private
645
+ */
646
+ setTagAttribute(tagName) {
647
+ if (this.tagName.toLowerCase() !== tagName) {
648
+ this.setAttribute(tagName, true);
649
+ }
650
+ }
1420
651
 
652
+ // eslint-disable-next-line complexity
1421
653
  updated(changedProperties) {
1422
654
  super.updated(changedProperties);
1423
655
 
1424
- // Apply value selection synchronously so that static-HTML fixtures
1425
- // resolve within a single update cycle. The refactored selectByValue
1426
- // no longer calls reset() first, so the destructive intermediate-event
1427
- // cascade that originally required deferral is eliminated. If option
1428
- // keys are not yet resolved (framework mount-order race), selectByValue
1429
- // queues a bounded retry automatically via queuePendingValue.
1430
- if (changedProperties.has('value') && !this.internalUpdateInProgress) {
1431
- this.menuService.selectByValue(this.value);
656
+ // Single source of truth for 'auroMenu-selectedOption'. Selection handlers
657
+ // mutate optionSelected and let Lit's update cycle dispatch here; the prior
658
+ // .value comparison missed multi-select array changes and combined with the
659
+ // explicit calls in handleDeselectState/makeSelection produced 2-3 duplicate
660
+ // events per selection.
661
+ if (changedProperties.has('optionSelected')) {
662
+ this.notifySelectionChange();
1432
663
  }
1433
664
 
1434
- // Handle loading state changes
1435
- if (changedProperties.has('loading')) {
1436
- this.setLoadingState(this.loading);
665
+ // Reset selection if multiSelect mode changes
666
+ if (changedProperties.has('multiSelect') && !changedProperties.has("value")) {
667
+ this.clearSelection();
668
+ }
669
+
670
+ if (changedProperties.has("value")) {
671
+ // Ensure items are populated before matching. `firstUpdated` normally initializes them,
672
+ // but a `value` change can arrive before slotted options are appended (e.g. parent sets
673
+ // value before children render). Without this guard, matching against an empty `items`
674
+ // would falsely dispatch `auroMenu-selectValueFailure` for valid initial values.
675
+ if (!this.items) {
676
+ this.initItems();
677
+ }
678
+
679
+ // Set when reconciliation reassigns `value` below. That reassignment schedules a
680
+ // second updated() cycle, so the `event`-attribute dispatch is deferred to that
681
+ // cycle to avoid firing option custom events twice on the same selection.
682
+ let valueReconciled = false;
683
+
684
+ // Handle null/undefined/empty case — empty/whitespace strings clear selection
685
+ // consistently with selectByValue(''), and avoid downstream `.includes('')` matches.
686
+ if (this.value === undefined || this.value === null || (typeof this.value === 'string' && this.value.trim() === '')) {
687
+ this.clearSelection();
688
+ } else {
689
+ let newSelected = null;
690
+
691
+ if (this.multiSelect) {
692
+ // In multiselect mode, this.value should be an array of strings.
693
+ // Defensive default: `formattedValue` can be undefined for unexpected value types,
694
+ // and calling `.includes` on undefined would throw during reconciliation.
695
+ const valueArray = this.formattedValue || [];
696
+ const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
697
+ newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
698
+
699
+ // Reconcile `value` with the selectable set. Drop only entries whose option is
700
+ // loaded but non-selectable (disabled/static) — leaving them would desync `value`
701
+ // from `optionSelected`, and the toggle handlers rebuild `value` from `formattedValue`,
702
+ // so the rejected entry would resurface on the next select/deselect. Entries with no
703
+ // matching item yet are preserved so async preselection still works once options render.
704
+ const rejectedValues = this.items
705
+ ? this.items.filter((item) => !isSelectableByValue(item) && valueArray.includes(item.value)).map((item) => item.value)
706
+ : [];
707
+ if (rejectedValues.length > 0) {
708
+ const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
709
+ this.value = serializeMultiSelectValue(reconciled);
710
+ valueReconciled = true;
711
+ }
712
+ } else {
713
+ // In single-select mode, this.value should be a string. Reject
714
+ // disabled/static options so a programmatic value pointing at a
715
+ // non-selectable option falls through to the no-match path below
716
+ // (dispatching auroMenu-selectValueFailure) instead of pinning it.
717
+ // `hidden` is intentionally NOT excluded: the combobox toggles
718
+ // `hidden` as its type-ahead filter, so a filtered-out option is
719
+ // still a valid programmatic selection.
720
+ const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
721
+
722
+ if (matchingOption) {
723
+ newSelected = matchingOption;
724
+ this._index = this.items.indexOf(matchingOption);
725
+ } else {
726
+ // If no matching option found, reset selection
727
+ newSelected = undefined;
728
+ this._index = -1;
729
+ }
730
+ }
731
+
732
+ // If no matching options were found in either mode
733
+ if (!newSelected || (Array.isArray(newSelected) && newSelected.length === 0)) {
734
+ // Defer failure when no options are loaded yet (async pattern: parent sets
735
+ // value before slotted options render). handleSlotChange re-runs matching
736
+ // once items arrive. Without this guard, a valid preselected value gets
737
+ // cleared by the failure listener before options ever exist to match against.
738
+ const hasItemsToMatch = this.items && this.items.length > 0;
739
+ if (hasItemsToMatch) {
740
+ // Clear state BEFORE dispatching so synchronous listeners (e.g. auro-select's
741
+ // updateDisplayedValue) read fresh `optionSelected` rather than the stale prior
742
+ // selection and re-render the old label.
743
+ if (this.optionSelected !== undefined) {
744
+ this.optionSelected = undefined;
745
+ }
746
+ this._index = -1;
747
+ dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
748
+ }
749
+ } else if (!this.selectionEquals(this.optionSelected, newSelected)) {
750
+ this.optionSelected = newSelected;
751
+ }
752
+ }
753
+
754
+ // Update UI state
755
+ this.updateItemsState(new Map([
756
+ [
757
+ 'optionSelected',
758
+ true
759
+ ]
760
+ ]));
761
+
762
+ // Notify of changes. Skip when reconciliation just reassigned `value`: the
763
+ // follow-on update cycle re-runs this branch and fires the events exactly once.
764
+ if (this.optionSelected !== undefined && !valueReconciled) {
765
+ const selected = Array.isArray(this.optionSelected) ? this.optionSelected : [this.optionSelected];
766
+ selected.forEach((opt) => {
767
+ if (opt.hasAttribute('event')) {
768
+ this.handleCustomEvent(opt);
769
+ }
770
+ });
771
+ }
1437
772
  }
1438
773
 
774
+ // Process all other UI updates
1439
775
  if (changedProperties.has('multiSelect') && this.rootMenu) {
1440
776
  if (this.multiSelect) {
1441
777
  this.setAttribute('aria-multiselectable', 'true');
@@ -1443,30 +779,129 @@ class AuroMenu extends AuroElement {
1443
779
  this.removeAttribute('aria-multiselectable');
1444
780
  }
1445
781
  }
782
+
783
+ this.updateItemsState(changedProperties);
1446
784
  }
1447
785
 
1448
786
  /**
1449
- * Sets an attribute that matches the default tag name if the tag name is not the default.
1450
- * @param {string} tagName - The tag name to set as an attribute.
787
+ * Updates the UI state and appearance of menu items based on changed properties.
1451
788
  * @private
789
+ * @param {Map<string, boolean>} changedProperties - LitElement's changed properties map.
1452
790
  */
1453
- setTagAttribute(tagName) {
1454
- if (this.tagName.toLowerCase() !== tagName) {
1455
- this.setAttribute(tagName, true);
791
+ updateItemsState(changedProperties) {
792
+ // Handle loading state changes
793
+ if (changedProperties.has('loading')) {
794
+ this.setAttribute("aria-busy", this.loading);
795
+ dispatchMenuEvent(this, "auroMenu-loadingChange", {
796
+ loading: this.loading,
797
+ hasLoadingPlaceholder: this.hasLoadingPlaceholder
798
+ });
1456
799
  }
1457
- }
1458
800
 
1459
- /**
1460
- * Sets the loading state and dispatches a loading change event.
1461
- * @param {boolean} isLoading - Whether the menu is loading.
1462
- * @protected
1463
- */
1464
- setLoadingState(isLoading) {
1465
- this.setAttribute("aria-busy", isLoading);
1466
- dispatchMenuEvent(this, "auroMenu-loadingChange", {
1467
- loading: isLoading,
1468
- hasLoadingPlaceholder: this.hasLoadingPlaceholder
801
+ if (!this.items) {
802
+ return;
803
+ }
804
+
805
+ // Handle noCheckmark propagation to all menus and options.
806
+ // Propagate in both directions so toggling back to false also clears nested elements
807
+ // (otherwise nested menus/options would stay stuck in no-checkmark mode).
808
+ if (changedProperties.has('noCheckmark')) {
809
+ this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]').forEach((element) => {
810
+ element.noCheckmark = this.noCheckmark;
811
+ });
812
+ }
813
+
814
+ // Handle layout propagation to all menus and options.
815
+ // Skip elements that had size/shape set by the author (marked in initItems);
816
+ // explicit per-option overrides must survive menu-level propagation.
817
+ const propagationTargets = this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]');
818
+ [
819
+ 'size',
820
+ 'shape'
821
+ ].forEach((prop) => {
822
+ if (changedProperties.has(prop)) {
823
+ const explicitKey = prop === 'size' ? '_explicitSize' : '_explicitShape';
824
+ propagationTargets.forEach((el) => {
825
+ if (el[explicitKey]) {
826
+ return;
827
+ }
828
+ el.setAttribute(prop, this[prop]);
829
+ });
830
+ }
831
+ });
832
+
833
+ // Regex for matchWord if needed
834
+ let regexWord = null;
835
+
836
+ if (changedProperties.has('matchWord') && this.matchWord && this.matchWord.length) {
837
+ const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
838
+ regexWord = new RegExp(escapedWord, 'giu');
839
+ }
840
+
841
+ // Handle direct item updates
842
+ this.items.forEach((option) => {
843
+ // Update selection if option or value changed
844
+ if (changedProperties.has('optionSelected') || changedProperties.has('value')) {
845
+ const isSelected = this.isOptionSelected(option);
846
+ option.setAttribute('aria-selected', isSelected ? 'true' : 'false');
847
+
848
+ // Add/remove selected attribute based on state
849
+ if (isSelected) {
850
+ option.setAttribute('selected', '');
851
+ } else {
852
+ option.removeAttribute('selected');
853
+ }
854
+ }
855
+
856
+ // Update text highlighting if matchWord changed
857
+ if (changedProperties.has('matchWord') && regexWord &&
858
+ isOptionInteractive(option) && !option.hasAttribute('persistent')) {
859
+ // Create nested spacers
860
+ const nested = option.querySelectorAll('.nestingSpacer');
861
+
862
+ const displayValueEl = option.querySelector('[slot="displayValue"]');
863
+ if (displayValueEl) {
864
+ option.removeChild(displayValueEl);
865
+ }
866
+
867
+ // Build highlighted content via DOM APIs rather than innerHTML so any
868
+ // `<`, `>`, or `&` in the option text renders literally (prevents XSS).
869
+ const originalText = option.textContent;
870
+ option.textContent = '';
871
+
872
+ nested.forEach(() => {
873
+ const spacer = document.createElement('span');
874
+ spacer.className = 'nestingSpacer';
875
+ option.appendChild(spacer);
876
+ });
877
+
878
+ const matches = [...originalText.matchAll(regexWord)];
879
+ let lastIndex = 0;
880
+ matches.forEach((match) => {
881
+ const [matchText] = match;
882
+ if (match.index > lastIndex) {
883
+ option.appendChild(document.createTextNode(originalText.slice(lastIndex, match.index)));
884
+ }
885
+ const strong = document.createElement('strong');
886
+ strong.textContent = matchText;
887
+ option.appendChild(strong);
888
+ lastIndex = match.index + matchText.length;
889
+ });
890
+ if (lastIndex < originalText.length) {
891
+ option.appendChild(document.createTextNode(originalText.slice(lastIndex)));
892
+ }
893
+
894
+ if (displayValueEl) {
895
+ option.append(displayValueEl);
896
+ }
897
+ }
898
+
899
+ // Update disabled state
900
+ if (changedProperties.has('disabled')) {
901
+ option.disabled = this.disabled;
902
+ }
1469
903
  });
904
+
1470
905
  }
1471
906
 
1472
907
  // Init Methods
@@ -1476,6 +911,7 @@ class AuroMenu extends AuroElement {
1476
911
  * @private
1477
912
  */
1478
913
  initializeMenu() {
914
+ this.initItems();
1479
915
  if (this.rootMenu) {
1480
916
  this.setAttribute('role', 'listbox');
1481
917
  this.setAttribute('root', '');
@@ -1485,15 +921,111 @@ class AuroMenu extends AuroElement {
1485
921
  }
1486
922
  }
1487
923
 
924
+ // Must run for nested menus too — sets level, role="group", and aria-label="submenu" based on parent.
1488
925
  this.handleNestedMenus(this);
1489
926
  }
1490
927
 
1491
928
  /**
1492
- * Selects the currently highlighted option.
1493
- * @protected
929
+ * Initializes menu items and their attributes.
930
+ * @private
1494
931
  */
1495
- makeSelection() {
1496
- this.menuService.selectHighlightedOption();
932
+ initItems() {
933
+ const found = Array.from(this.querySelectorAll('auro-menuoption, [auro-menuoption]'));
934
+ this.items = found.length > 0 ? found : undefined;
935
+
936
+ // Record whether each propagation target had an author-set size/shape attribute
937
+ // BEFORE menu has had a chance to propagate. Marker is set once per element so a
938
+ // later menu-driven setAttribute doesn't re-flag the element as "explicit".
939
+ this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]').forEach((el) => {
940
+ if (el._explicitSize === undefined) {
941
+ el._explicitSize = el.hasAttribute('size');
942
+ }
943
+ if (el._explicitShape === undefined) {
944
+ el._explicitShape = el.hasAttribute('shape');
945
+ }
946
+ });
947
+
948
+ if (this.noCheckmark) {
949
+ this.updateItemsState(new Map([
950
+ [
951
+ 'noCheckmark',
952
+ true
953
+ ]
954
+ ]));
955
+ }
956
+
957
+ this.dispatchEvent(new CustomEvent('auroMenu-optionsChange', {
958
+ detail: {
959
+ options: this.items
960
+ }
961
+ }));
962
+ }
963
+
964
+ // Logic Methods
965
+
966
+ /**
967
+ * Updates menu state when an option is selected.
968
+ * @private
969
+ * @param {HTMLElement} option - The option element to select.
970
+ */
971
+ handleSelectState(option) {
972
+ if (this.multiSelect) {
973
+ const currentValue = this.formattedValue || [];
974
+ const currentSelected = this.optionSelected || [];
975
+
976
+ if (!currentValue.includes(option.value)) {
977
+ this.value = serializeMultiSelectValue([
978
+ ...currentValue,
979
+ option.value
980
+ ]);
981
+ }
982
+ if (!currentSelected.includes(option)) {
983
+ this.optionSelected = [
984
+ ...currentSelected,
985
+ option
986
+ ];
987
+ }
988
+ } else {
989
+ this.value = option.value;
990
+ this.optionSelected = option;
991
+ }
992
+
993
+ this._index = this.items.indexOf(option);
994
+ }
995
+
996
+ /**
997
+ * Deselects a menu option and updates related state.
998
+ * @private
999
+ * @param {HTMLElement} option - The menuoption to be deselected.
1000
+ */
1001
+ handleDeselectState(option) {
1002
+ if (this.multiSelect) {
1003
+ // Remove this option from array; an empty result collapses `value` to undefined.
1004
+ const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
1005
+ this.value = serializeMultiSelectValue(newFormattedValue);
1006
+
1007
+ this.optionSelected = this.optionSelected.filter((val) => val !== option);
1008
+ if (this.optionSelected.length === 0) {
1009
+ this.optionSelected = undefined;
1010
+ }
1011
+ } else {
1012
+ // For single-select: Back to undefined when deselected
1013
+ this.value = undefined;
1014
+ this.optionSelected = undefined;
1015
+ }
1016
+
1017
+ // Update the index tracking
1018
+ this._index = this.items.indexOf(option);
1019
+
1020
+ // Update UI to reflect changes
1021
+ this.updateItemsState(new Map([
1022
+ [
1023
+ 'optionSelected',
1024
+ true
1025
+ ]
1026
+ ]));
1027
+
1028
+ // Notification happens via updated() when optionSelected changes above.
1497
1029
  }
1498
1030
 
1499
1031
  /**
@@ -1512,7 +1044,23 @@ class AuroMenu extends AuroElement {
1512
1044
  * @public
1513
1045
  */
1514
1046
  reset() {
1515
- this.menuService.reset();
1047
+ // Reset to undefined - initial state
1048
+ this.value = undefined;
1049
+ this.optionSelected = undefined;
1050
+ this._index = -1;
1051
+
1052
+ // Clear active option state so a follow-up open/navigation starts fresh
1053
+ // rather than reusing a stale reference from before the reset.
1054
+ this.items?.forEach((item) => item.classList.remove('active'));
1055
+ this.optionActive = undefined;
1056
+
1057
+ // Reset UI state
1058
+ this.updateItemsState(new Map([
1059
+ [
1060
+ 'optionSelected',
1061
+ true
1062
+ ]
1063
+ ]));
1516
1064
 
1517
1065
  // Dispatch reset event
1518
1066
  dispatchMenuEvent(this, 'auroMenu-selectValueReset');
@@ -1524,6 +1072,14 @@ class AuroMenu extends AuroElement {
1524
1072
  * @param {HTMLElement} menu - Root menu element.
1525
1073
  */
1526
1074
  handleNestedMenus(menu) {
1075
+ // Slot changes can fire on a menu mid-teardown (e.g. while a parent menu
1076
+ // is removing children to rebuild its content). In that window the menu
1077
+ // is detached and parentElement is null. Skip — handleNestedMenus will
1078
+ // run again when the menu is reattached.
1079
+ if (!menu.parentElement) {
1080
+ return;
1081
+ }
1082
+
1527
1083
  menu.level = menu.parentElement.level >= 0 ? menu.parentElement.level + 1 : 0;
1528
1084
 
1529
1085
  if (menu.level > 0) {
@@ -1534,34 +1090,221 @@ class AuroMenu extends AuroElement {
1534
1090
  }
1535
1091
  }
1536
1092
 
1537
- const options = menu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption]');
1538
- options.forEach((option) => {
1539
- const regex = new RegExp(this.nestingSpacer, "gu");
1540
- option.innerHTML = this.nestingSpacer.repeat(menu.level) + option.innerHTML.replace(regex, '');
1541
- });
1093
+ const options = menu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption]');
1094
+ options.forEach((option) => {
1095
+ const regex = new RegExp(this.nestingSpacer, "gu");
1096
+ option.innerHTML = this.nestingSpacer.repeat(menu.level) + option.innerHTML.replace(regex, '');
1097
+ });
1098
+ }
1099
+
1100
+ // Event Handlers
1101
+
1102
+ /**
1103
+ * Makes a selection based on the current index.
1104
+ * @private
1105
+ */
1106
+ makeSelection() {
1107
+ if (!this.items) {
1108
+ this.initItems();
1109
+ }
1110
+
1111
+ // Get currently selected menu option based on index
1112
+ const option = this.items ? this.items[this._index] : undefined;
1113
+
1114
+ // Return early if option is not interactive
1115
+ if (!option || !isOptionInteractive(option)) {
1116
+ return;
1117
+ }
1118
+
1119
+ // Handle custom events first
1120
+ if (option.hasAttribute('event')) {
1121
+ this.handleCustomEvent(option);
1122
+ return;
1123
+ }
1124
+
1125
+ if (this.multiSelect) {
1126
+ // In multiselect, toggle individual selections
1127
+ this.toggleOption(option);
1128
+ // In single select, only handle selection of new options
1129
+ } else if (!this.isOptionSelected(option)) {
1130
+ this.clearSelection();
1131
+ this.handleSelectState(option);
1132
+ } else {
1133
+ // Re-selecting the already-selected option in single-select doesn't change
1134
+ // state, so updated() won't fire. Notify explicitly so consumers (e.g.
1135
+ // auro-select closing its dropdown on Enter) still get the event.
1136
+ this.notifySelectionChange();
1137
+ }
1138
+ }
1139
+
1140
+ /**
1141
+ * Toggle the selection state of the menuoption.
1142
+ * @private
1143
+ * @param {HTMLElement} option - The menuoption to toggle.
1144
+ */
1145
+ toggleOption(option) {
1146
+ const isCurrentlySelected = this.isOptionSelected(option);
1147
+
1148
+ if (isCurrentlySelected) {
1149
+ this.handleDeselectState(option);
1150
+ } else if (option.value === undefined || option.value === '') {
1151
+ dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
1152
+ } else {
1153
+ this.handleSelectState(option);
1154
+ }
1155
+ }
1156
+
1157
+ /**
1158
+ * Handles keyboard navigation and selection.
1159
+ * @private
1160
+ * @param {KeyboardEvent} event - The keydown event.
1161
+ */
1162
+ handleKeyDown(event) {
1163
+ switch (event.key) {
1164
+ case "ArrowDown":
1165
+ event.preventDefault();
1166
+ this.navigateOptions('down');
1167
+ break;
1168
+ case "ArrowUp":
1169
+ event.preventDefault();
1170
+ this.navigateOptions('up');
1171
+ break;
1172
+ case "Tab":
1173
+ // Do not preventDefault on Tab so focus can move out of the menu (a11y: avoid trapping keyboard users).
1174
+ this.makeSelection();
1175
+ break;
1176
+ case "Enter":
1177
+ event.preventDefault();
1178
+ this.makeSelection();
1179
+ break;
1180
+ }
1181
+ }
1182
+
1183
+ /**
1184
+ * Handles option selection via click events from menuoptions.
1185
+ * @private
1186
+ * @param {CustomEvent} event - The auroMenuOption-click event.
1187
+ */
1188
+ handleMouseSelect(event) {
1189
+ if (!this.rootMenu || this.disabled) {
1190
+ return;
1191
+ }
1192
+
1193
+ const option = event.detail;
1194
+ if (option && this.items) {
1195
+ const idx = this.items.indexOf(option);
1196
+ if (idx >= 0) {
1197
+ this._index = idx;
1198
+ this.makeSelection();
1199
+ }
1200
+ }
1201
+ }
1202
+
1203
+ /**
1204
+ * Handles option hover events.
1205
+ * @private
1206
+ * @param {CustomEvent} event - Event object from the browser.
1207
+ */
1208
+ handleOptionHover(event) {
1209
+ const option = event.detail;
1210
+ if (this.items) {
1211
+ const idx = this.items.indexOf(option);
1212
+ if (idx >= 0) {
1213
+ this.updateActiveOption(idx);
1214
+ }
1215
+ }
1216
+ }
1217
+
1218
+ /**
1219
+ * Handles slot change events.
1220
+ * @private
1221
+ */
1222
+ handleSlotChange() {
1223
+ if (this.parentElement && this.parentElement.closest('auro-menu, [auro-menu]')) {
1224
+ this.rootMenu = false;
1225
+ }
1226
+
1227
+ // Nested menus must also reinitialize so items, level, role="group", and aria-label refresh on content changes.
1228
+ // Root-specific attributes (listbox/root/aria-multiselectable) remain gated by `rootMenu` inside initializeMenu.
1229
+ this.initializeMenu();
1230
+
1231
+ // When options arrive after `value` was set (async option load), re-run matching
1232
+ // against the now-populated items. The earlier updated('value') call deferred
1233
+ // the failure dispatch because items were empty; this triggers the match now.
1234
+ const hasPendingValue = this.value !== undefined &&
1235
+ this.value !== null &&
1236
+ !(typeof this.value === 'string' && this.value.trim() === '');
1237
+ if (hasPendingValue && this.items && this.items.length > 0 && this.optionSelected === undefined) {
1238
+ this.requestUpdate('value', undefined);
1239
+ }
1542
1240
  }
1543
1241
 
1544
1242
  /**
1545
- * Navigates the menu options in the specified direction.
1546
- * @param {'up'|'down'} direction - The direction to navigate.
1547
- * @protected
1243
+ * Navigates through options using keyboard.
1244
+ * @param {string} direction - 'up' or 'down'.
1548
1245
  */
1549
1246
  navigateOptions(direction) {
1550
- if (direction === 'up') {
1551
- this.menuService.highlightPrevious();
1552
- } else if (direction === 'down') {
1553
- this.menuService.highlightNext();
1247
+ // Return early if no items exist
1248
+ if (!this.items || !this.items.length) {
1249
+ return;
1250
+ }
1251
+
1252
+ let newIndex = this._index;
1253
+ if (newIndex === -1 && direction === 'up') {
1254
+ newIndex = this.items.length;
1255
+ }
1256
+ const increment = direction === 'down' ? 1 : -1;
1257
+ const maxIterations = this.items.length;
1258
+ let iterations = 0;
1259
+ let foundInteractiveOption = false;
1260
+
1261
+ do {
1262
+ newIndex = (newIndex + increment + this.items.length) % this.items.length;
1263
+ iterations += 1;
1264
+
1265
+ // Check if current option is interactive
1266
+ const currentOption = this.items[newIndex];
1267
+ if (isOptionInteractive(currentOption)) {
1268
+ foundInteractiveOption = true;
1269
+ break;
1270
+ }
1271
+
1272
+ // Break if all options were checked
1273
+ if (iterations >= maxIterations) {
1274
+ break;
1275
+ }
1276
+ } while (iterations < maxIterations);
1277
+
1278
+ // Update only if an interactive option was found
1279
+ if (foundInteractiveOption) {
1280
+ this.updateActiveOption(newIndex);
1554
1281
  }
1555
1282
  }
1556
1283
 
1557
1284
  /**
1558
- * Handles slot change events.
1559
- * @private
1285
+ * Updates the active option state and dispatches events.
1286
+ * Accepts either a numeric index or an HTMLElement option.
1287
+ * @param {number|HTMLElement} indexOrOption - Index of the option or the option element to make active.
1560
1288
  */
1561
- handleSlotChange() {
1562
- if (this.rootMenu) {
1563
- this.initializeMenu();
1289
+ updateActiveOption(indexOrOption) {
1290
+ let idx = -1;
1291
+
1292
+ if (typeof indexOrOption === 'number') {
1293
+ idx = indexOrOption;
1294
+ } else {
1295
+ idx = this.items ? this.items.indexOf(indexOrOption) : -1;
1564
1296
  }
1297
+
1298
+ if (!this.items || !this.items[idx]) {
1299
+ return;
1300
+ }
1301
+
1302
+ this.items.forEach((item) => item.classList.remove('active'));
1303
+ this.items[idx].classList.add('active');
1304
+ this.optionActive = this.items[idx];
1305
+ this._index = idx;
1306
+
1307
+ dispatchMenuEvent(this, 'auroMenu-activatedOption', this.items[idx]);
1565
1308
  }
1566
1309
 
1567
1310
  /**
@@ -1571,8 +1314,8 @@ class AuroMenu extends AuroElement {
1571
1314
  */
1572
1315
  handleCustomEvent(option) {
1573
1316
  const eventName = option.getAttribute('event');
1574
- dispatchMenuEvent(this, eventName);
1575
- dispatchMenuEvent(this, 'auroMenu-customEventFired');
1317
+ dispatchMenuEvent(this, eventName, { option });
1318
+ dispatchMenuEvent(this, 'auroMenu-customEventFired', { option });
1576
1319
  }
1577
1320
 
1578
1321
  /**
@@ -1580,14 +1323,33 @@ class AuroMenu extends AuroElement {
1580
1323
  * @param {any} source - The source that triggers this event.
1581
1324
  * @private
1582
1325
  */
1583
- notifySelectionChange({value, stringValue, keys, options, reason} = {}) {
1584
- dispatchMenuEvent(this, 'auroMenu-selectedOption', {
1585
- value,
1586
- stringValue,
1587
- keys,
1588
- options,
1589
- reason
1590
- });
1326
+ notifySelectionChange(source = undefined) {
1327
+ dispatchMenuEvent(this, 'auroMenu-selectedOption', { source });
1328
+ }
1329
+
1330
+ /**
1331
+ * @private
1332
+ * @param {any} current - Current selection.
1333
+ * @param {any} next - New selection to compare.
1334
+ * @returns {boolean} Whether the selections are equal.
1335
+ */
1336
+ selectionEquals(current, next) {
1337
+ if (current === next) {
1338
+ return true;
1339
+ }
1340
+ if (!current || !next) {
1341
+ return false;
1342
+ }
1343
+ if (Array.isArray(current) && Array.isArray(next)) {
1344
+ if (current.length !== next.length) {
1345
+ return false;
1346
+ }
1347
+ // Compare as sets — selection order may differ from DOM order after value-driven
1348
+ // reconciliation, but the selected set is what matters for change detection.
1349
+ const nextSet = new Set(next);
1350
+ return current.every((item) => nextSet.has(item));
1351
+ }
1352
+ return false;
1591
1353
  }
1592
1354
 
1593
1355
  /**
@@ -1602,7 +1364,6 @@ class AuroMenu extends AuroElement {
1602
1364
  }
1603
1365
 
1604
1366
  if (this.multiSelect) {
1605
- // In multi-select mode, check if the option is in the selected array
1606
1367
  return Array.isArray(this.optionSelected) && this.optionSelected.some((selectedOption) => selectedOption === option);
1607
1368
  }
1608
1369
 
@@ -1742,40 +1503,16 @@ let menuOptionIdCounter = 0;
1742
1503
  * The `auro-menuoption` element provides users a way to define a menu option.
1743
1504
  * @customElement auro-menuoption
1744
1505
  *
1506
+ * @attr {Boolean} static - When present, marks the option as non-interactive — it renders but is skipped during keyboard navigation and cannot be selected. Useful for section headers, informational rows inside a menu, or attaching event listeners.
1745
1507
  * @slot default - The default slot for the menu option text.
1746
1508
  *
1747
1509
  * @event auroMenuOption-mouseover - Notifies that this option has been hovered over.
1510
+ * @event auroMenuOption-click - Notifies that this option has been clicked.
1748
1511
  */
1749
1512
  class AuroMenuOption extends AuroElement {
1750
-
1751
- /**
1752
- * This will register this element with the browser.
1753
- * @param {string} [name="auro-menuoption"] - The name of the element that you want to register.
1754
- *
1755
- * @example
1756
- * AuroMenuOption.register("custom-menuoption") // this will register this element to <custom-menuoption/>
1757
- *
1758
- */
1759
- static register(name = "auro-menuoption") {
1760
- AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroMenuOption);
1761
- }
1762
-
1763
- /**
1764
- * Returns whether the menu option is currently active and selectable.
1765
- * An option is considered active if it is not hidden, not disabled, and not static.
1766
- * @returns {boolean} True if the option is active, false otherwise.
1767
- */
1768
- get isActive() {
1769
- return !this.hasAttribute('hidden') &&
1770
- !this.disabled &&
1771
- !this.hasAttribute('static');
1772
- }
1773
-
1774
1513
  constructor() {
1775
1514
  super();
1776
1515
 
1777
- this.bindEvents();
1778
-
1779
1516
  /**
1780
1517
  * @private
1781
1518
  */
@@ -1796,83 +1533,62 @@ class AuroMenuOption extends AuroElement {
1796
1533
  this.noCheckmark = false;
1797
1534
  this.disabled = false;
1798
1535
  this.noMatch = false;
1536
+ this.persistent = false;
1799
1537
 
1800
1538
  /**
1801
1539
  * @private
1802
1540
  */
1803
- this.runtimeUtils = new AuroLibraryRuntimeUtils();
1804
-
1805
- // Initialize context-related properties
1806
- this.menuService = null;
1807
- this.unsubscribe = null;
1541
+ this.tabIndex = -1;
1808
1542
 
1809
1543
  /**
1810
1544
  * @private
1811
1545
  */
1812
- this.handleMenuChange = this.handleMenuChange.bind(this);
1546
+ this.runtimeUtils = new AuroLibraryRuntimeUtils();
1547
+
1548
+ this.addEventListener('click', this.handleClick.bind(this));
1813
1549
  }
1814
1550
 
1815
1551
  static get properties() {
1816
1552
  return {
1817
1553
  ...super.properties,
1818
-
1819
- /**
1820
- * When true, disables the menu option.
1821
- */
1822
- disabled: {
1554
+ noCheckmark: {
1823
1555
  type: Boolean,
1824
1556
  reflect: true
1825
1557
  },
1826
1558
 
1827
1559
  /**
1828
- * @private
1560
+ * **Deprecated.** Use the `value` attribute on `auro-menu` to set the selected option when the menu renders (or call `menu.selectByValue(value)` programmatically). Support for the child-level `selected` attribute will be removed in a future major release.
1561
+ *
1562
+ * @deprecated Use the `value` attribute on `auro-menu` instead.
1829
1563
  */
1830
- event: {
1831
- type: String,
1564
+ selected: {
1565
+ type: Boolean,
1832
1566
  reflect: true
1833
1567
  },
1834
-
1835
- /**
1836
- * @private
1837
- */
1838
- layout: {
1839
- type: String
1568
+ disabled: {
1569
+ type: Boolean,
1570
+ reflect: true
1840
1571
  },
1841
-
1842
- /**
1843
- * 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.
1844
- */
1845
- key: {
1572
+ value: {
1846
1573
  type: String,
1847
1574
  reflect: true
1848
1575
  },
1849
-
1850
- /**
1851
- * @private
1852
- */
1853
- menuService: {
1854
- type: Object,
1855
- state: true
1576
+ tabIndex: {
1577
+ type: Number,
1578
+ reflect: true
1856
1579
  },
1857
1580
 
1858
1581
  /**
1859
1582
  * @private
1860
1583
  */
1861
- matchWord: {
1584
+ event: {
1862
1585
  type: String,
1863
- state: true
1864
- },
1865
-
1866
- /**
1867
- * @private
1868
- */
1869
- noCheckmark: {
1870
- type: Boolean,
1871
1586
  reflect: true
1872
1587
  },
1873
1588
 
1874
1589
  /**
1875
- * When true, marks this option as the "no matching results" placeholder shown by combobox when the user's input does not match any available options. Enables distinct styling and prevents the option from being treated as a selectable match.
1590
+ * When true, marks this option as the "no matching results" placeholder shown by combobox
1591
+ * when the user's input does not match any available options.
1876
1592
  */
1877
1593
  noMatch: {
1878
1594
  type: Boolean,
@@ -1880,29 +1596,11 @@ class AuroMenuOption extends AuroElement {
1880
1596
  attribute: 'nomatch'
1881
1597
  },
1882
1598
 
1883
- /**
1884
- * Specifies that an option is selected.
1885
- */
1886
- selected: {
1599
+ /** When true, this option is excluded from `matchWord` DOM rewriting — useful for utility rows (e.g., "Add new…") that must render identically regardless of the current filter. */
1600
+ persistent: {
1887
1601
  type: Boolean,
1888
1602
  reflect: true
1889
1603
  },
1890
-
1891
- /**
1892
- * Specifies the tab index of the menu option.
1893
- */
1894
- tabIndex: {
1895
- type: Number,
1896
- reflect: true
1897
- },
1898
-
1899
- /**
1900
- * Specifies the value to be sent to a server.
1901
- */
1902
- value: {
1903
- type: String,
1904
- reflect: true
1905
- },
1906
1604
  };
1907
1605
  }
1908
1606
 
@@ -1914,41 +1612,48 @@ class AuroMenuOption extends AuroElement {
1914
1612
  ];
1915
1613
  }
1916
1614
 
1615
+ /**
1616
+ * This will register this element with the browser.
1617
+ * @param {string} [name="auro-menuoption"] - The name of element that you want to register to.
1618
+ *
1619
+ * @example
1620
+ * AuroMenuOption.register("custom-menuoption") // this will register this element to <custom-menuoption/>
1621
+ *
1622
+ */
1623
+ static register(name = "auro-menuoption") {
1624
+ AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroMenuOption);
1625
+ }
1626
+
1627
+ /**
1628
+ * Returns whether the menu option is currently active and selectable.
1629
+ * @returns {boolean}
1630
+ */
1631
+ get isActive() {
1632
+ return !this.hasAttribute('hidden') &&
1633
+ !this.disabled &&
1634
+ !this.hasAttribute('static');
1635
+ }
1636
+
1917
1637
  connectedCallback() {
1918
1638
  super.connectedCallback();
1919
1639
 
1920
- // Add the tag name as an attribute if it is different than the component name
1921
- // Add this step soon as this node gets attached to the DOM to avoid racing condition with menu's value setting logic.
1922
1640
  this.runtimeUtils.handleComponentTagRename(this, 'auro-menuoption');
1923
-
1924
- // Set up context consumption in connectedCallback
1925
- this._contextConsumer = new ContextConsumer(this, {
1926
- context: MenuContext,
1927
- callback: this.attachTo.bind(this),
1928
- subscribe: true
1929
- });
1930
-
1931
- // Establish the key property as early as possible.
1932
- // When a framework (e.g. Svelte) inserts the element into the DOM before
1933
- // setting its `value` property, both `getAttribute('value')` and
1934
- // `getAttribute('key')` return null here. Setting `this.key = null`
1935
- // would block the fallback in `updated()` that assigns key from the
1936
- // value property (the guard checked `=== undefined`). Only assign key
1937
- // if at least one source attribute is actually present so that the
1938
- // `updated()` fallback can run when the value property arrives later.
1939
- const valueAttr = this.getAttribute('value');
1940
- const keyAttr = this.getAttribute('key');
1941
- const resolvedKey = keyAttr !== null ? keyAttr : valueAttr;
1942
- if (resolvedKey !== null) {
1943
- this.key = resolvedKey;
1944
- }
1945
1641
  }
1946
1642
 
1947
1643
  firstUpdated() {
1948
- // Add the tag name as an attribute if it is different than the component name
1949
1644
  this.runtimeUtils.handleComponentTagRename(this, 'auro-menuoption');
1950
1645
 
1951
- // Generate unique ID if not already set (required for aria-activedescendant)
1646
+ // firstUpdated can fire on an option that was detached before the first
1647
+ // render completed (e.g. dynamic-menu rebuilds clear and repopulate the
1648
+ // menu mid-cycle). parentElement is null in that case — fall back to
1649
+ // defaults; the next render after re-attachment will pick up real values.
1650
+ if (!this.hasAttribute('size')) {
1651
+ this.size = this.parentElement ? this.parentElement.getAttribute('size') || 'sm' : 'sm';
1652
+ }
1653
+ if (!this.hasAttribute('shape')) {
1654
+ this.shape = this.parentElement ? this.parentElement.getAttribute('shape') || 'box' : 'box';
1655
+ }
1656
+
1952
1657
  if (!this.id) {
1953
1658
  menuOptionIdCounter += 1;
1954
1659
  this.id = `menuoption-${menuOptionIdCounter}`;
@@ -1970,16 +1675,8 @@ class AuroMenuOption extends AuroElement {
1970
1675
  updated(changedProperties) {
1971
1676
  super.updated(changedProperties);
1972
1677
 
1973
- // Update aria-selected attribute if selected changed
1974
1678
  if (changedProperties.has('selected')) {
1975
-
1976
- // Update aria-selected attribute
1977
1679
  this.setAttribute('aria-selected', this.selected.toString());
1978
-
1979
- // Update menu service selection state if this isn't an internal update
1980
- if (this.internalUpdateInProgress !== true && this.menuService) {
1981
- this.menuService[this.selected ? 'selectOption' : 'deselectOption'](this);
1982
- }
1983
1680
  }
1984
1681
 
1985
1682
  if (changedProperties.has('disabled')) {
@@ -1989,231 +1686,48 @@ class AuroMenuOption extends AuroElement {
1989
1686
  this.removeAttribute('aria-disabled');
1990
1687
  }
1991
1688
  }
1992
-
1993
- if (changedProperties.has('active')) {
1994
- this.updateActiveClasses();
1995
- }
1996
-
1997
- // Update text highlight if matchWord changed
1998
- if (changedProperties.has('matchWord')) {
1999
- this.updateTextHighlight();
2000
- }
2001
-
2002
- // Set the key to be the passed value if no key is provided.
2003
- // Loose equality (== null) is intentional: it catches both null AND
2004
- // undefined. When a framework (e.g. Svelte, React) inserts the element
2005
- // before setting its value property, connectedCallback skips key
2006
- // assignment because both attributes are null at that point. The Lit
2007
- // property default for `key` is undefined (not null), so strict
2008
- // === null would miss the case and the fallback would never run.
2009
- if (changedProperties.has('value') && this.key == null) { // eslint-disable-line eqeqeq, no-eq-null
2010
- this.key = this.value;
2011
- }
2012
- }
2013
-
2014
- disconnectedCallback() {
2015
- if (this.menuService) {
2016
- this.menuService.unsubscribe(this.handleMenuChange);
2017
- this.menuService.removeMenuOption(this);
2018
- }
2019
- }
2020
-
2021
- /**
2022
- * Sets up event listeners for user interaction with the menu option.
2023
- * This function enables click and mouse enter events to trigger selection and highlighting logic.
2024
- */
2025
- bindEvents() {
2026
- this.addEventListener('click', this.handleClick.bind(this));
2027
- this.addEventListener('mouseenter', this.handleMouseEnter.bind(this));
2028
1689
  }
2029
1690
 
2030
- /**
2031
- * Attaches this menu option to a menu service and subscribes to its events.
2032
- * This method enables the option to participate in menu selection and highlighting logic.
2033
- * @param {Object} service - The menu service instance to attach to.
2034
- */
2035
- attachTo(service) {
2036
- if (!service) {
2037
- return;
2038
- }
2039
- this.menuService = service;
2040
- this.menuService.addMenuOption(this);
2041
- this.menuService.subscribe(this.handleMenuChange);
1691
+ handleMenuChange() {
1692
+ // no-op: menu owns state in the distributed architecture
2042
1693
  }
2043
1694
 
2044
- /**
2045
- * Handles changes from the menu service and updates the option's state.
2046
- * This function synchronizes the option's properties and selection/highlight state with menu events.
2047
- * @param {Object} event - The event object from the menu service.
2048
- */
2049
- handleMenuChange(event) {
2050
-
2051
- // Ignore events without a type or property
2052
- if (!event || (!event.type && !event.property)) {
2053
- return;
2054
- }
2055
-
2056
- // Update reactive properties based on event type
2057
- if (event.property && Object.keys(AuroMenuOption.properties).includes(event.property)) {
2058
- this[event.property] = event.value;
2059
- }
2060
-
2061
- // Handle highlight changes
2062
- if (event.type === 'highlightChange') {
2063
- const isActive = event.option === this;
2064
- this.active = isActive;
2065
- this.updateActiveClasses();
2066
- }
2067
-
2068
- if (event.type === 'stateChange') {
2069
- const isSelected = event.selectedOptions.includes(this);
2070
- this.setInternalSelected(isSelected);
2071
- }
1695
+ setSelected(value) {
1696
+ this.selected = value;
2072
1697
  }
2073
1698
 
2074
- /**
2075
- * Updates the internal selected state of the menu option bypassing 'updated' and triggers custom events if selected.
2076
- * This function ensures the option's selection state is synchronized with menu logic and notifies listeners.
2077
- * @param {boolean} isSelected - Whether the option should be marked as selected.
2078
- */
2079
- setInternalSelected(isSelected) {
2080
- this.internalUpdateInProgress = true;
2081
- this.selected = isSelected;
2082
-
2083
- // Fire custom event if selected
2084
- if (isSelected) {
2085
- this.handleCustomEvent();
1699
+ updateActive(active) {
1700
+ this.active = active;
1701
+ if (active) {
1702
+ this.classList.add('active');
1703
+ } else {
1704
+ this.classList.remove('active');
2086
1705
  }
2087
-
2088
- setTimeout(() => {
2089
- this.internalUpdateInProgress = false;
2090
- }, 0);
2091
- }
2092
-
2093
- /**
2094
- * Sets the selected state of the menu option.
2095
- * This function updates whether the option is currently selected.
2096
- * @param {boolean} isSelected - Whether the option should be marked as selected.
2097
- * @deprecated Simply modify the `selected` property directly instead.
2098
- */
2099
- setSelected(isSelected) {
2100
- this.selected = isSelected;
2101
- }
2102
-
2103
- /**
2104
- * Updates the active state and visual highlighting of the menu option.
2105
- * This function toggles the option's active status and applies or removes the active CSS class.
2106
- * @param {boolean} isActive - Whether the option should be marked as active.
2107
- * @deprecated Simply modify the `active` property directly instead.
2108
- */
2109
- updateActive(isActive) {
2110
-
2111
- // Set active state
2112
- this.active = isActive;
2113
- this.updateActiveClasses();
2114
- }
2115
-
2116
- /**
2117
- * Updates the CSS class for the menu option based on its active state.
2118
- * This function adds or removes the 'active' class to visually indicate the option's active status.
2119
- * @private
2120
- */
2121
- updateActiveClasses() {
2122
- // Update class based on active state
2123
- if (this.active) this.classList.add('active');
2124
- else this.classList.remove('active');
2125
1706
  }
2126
1707
 
2127
-
2128
- /**
2129
- * Updates the visual highlighting of text within the menu option based on the current match word.
2130
- * This function highlights matching text segments and manages nested spacers for display formatting.
2131
- * @private
2132
- */
2133
- updateTextHighlight() {
2134
-
2135
- // Regex for matchWord if needed
2136
- let regexWord = null;
2137
-
2138
- if (this.matchWord && this.matchWord.length) {
2139
- const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
2140
- regexWord = new RegExp(escapedWord, 'giu');
2141
- }
2142
-
2143
- // Update text highlighting if matchWord changed
2144
- if (regexWord &&
2145
- this.isActive && !this.hasAttribute('persistent')) {
2146
- const nested = this.querySelectorAll('.nestingSpacer');
2147
-
2148
- const displayValueEl = this.querySelector('[slot="displayValue"]');
2149
- if (displayValueEl) {
2150
- this.removeChild(displayValueEl);
2151
- }
2152
-
2153
- // Create nested spacers
2154
- const nestingSpacerBundle = [...nested].map(() => this.nestingSpacer).join('');
2155
-
2156
- // Update with spacers and matchWord
2157
- this.innerHTML = nestingSpacerBundle +
2158
- this.textContent.replace(
2159
- regexWord,
2160
- (match) => `<strong>${match}</strong>`
2161
- );
2162
- if (displayValueEl) {
2163
- this.append(displayValueEl);
2164
- }
2165
- }
1708
+ attachTo() {
1709
+ // no-op: menu owns state in the distributed architecture
2166
1710
  }
2167
1711
 
2168
1712
  /**
2169
- * Handles click events on the menu option, toggling its selected state.
2170
- * This function dispatches a click event and updates selection if the option is not disabled.
1713
+ * Handles click events on the menu option.
2171
1714
  * @private
2172
1715
  */
2173
1716
  handleClick() {
2174
- if (!this.disabled && !this.menuService?.disabled) {
2175
- this.dispatchClickEvent();
2176
- this.selected = !this.selected;
2177
- }
2178
- }
2179
-
2180
- /**
2181
- * Handles mouse enter events to highlight the menu option.
2182
- * This function updates the menu service to set this option as the currently highlighted item if not disabled.
2183
- * @private
2184
- */
2185
- handleMouseEnter() {
2186
1717
  if (!this.disabled) {
2187
- this.menuService.setHighlightedOption(this);
2188
- }
2189
- }
2190
-
2191
- /**
2192
- * Dispatches custom events defined for this menu option.
2193
- * This function notifies listeners when a custom event is triggered by the option.
2194
- * @private
2195
- */
2196
- handleCustomEvent() {
2197
- if (this.event) {
2198
- dispatchMenuEvent(this, this.event, { option: this });
2199
- dispatchMenuEvent(this, 'auroMenu-customEventFired', { option: this });
1718
+ // Pure event emitter: the parent menu owns selection state and will
1719
+ // update `selected` via setSelected(). Toggling here desyncs the option
1720
+ // UI from auro-menu.optionSelected (e.g. single-select re-click on the
1721
+ // already-selected option would flip the option off while the menu keeps it on).
1722
+ this.dispatchEvent(new CustomEvent('auroMenuOption-click', {
1723
+ bubbles: true,
1724
+ cancelable: false,
1725
+ composed: true,
1726
+ detail: this
1727
+ }));
2200
1728
  }
2201
1729
  }
2202
1730
 
2203
- /**
2204
- * Dispatches a click event for this menu option.
2205
- * This function notifies listeners that the option has been clicked.
2206
- * @private
2207
- */
2208
- dispatchClickEvent() {
2209
- this.dispatchEvent(new CustomEvent('auroMenuOption-click', {
2210
- bubbles: true,
2211
- cancelable: false,
2212
- composed: true,
2213
- detail: this
2214
- }));
2215
- }
2216
-
2217
1731
  /**
2218
1732
  * Generates an HTML element containing an SVG icon based on the provided `svgContent`.
2219
1733
  *
@@ -2253,8 +1767,8 @@ class AuroMenuOption extends AuroElement {
2253
1767
  return html$1`
2254
1768
  <div class="${classes}">
2255
1769
  ${this.selected && !this.noCheckmark
2256
- ? this.generateIconHtml(checkmarkIcon.svg)
2257
- : undefined}
1770
+ ? this.generateIconHtml(checkmarkIcon.svg)
1771
+ : undefined}
2258
1772
  <slot></slot>
2259
1773
  </div>
2260
1774
  `;