@aurodesignsystem-dev/auro-formkit 0.0.0-pr1503.3 → 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
 
@@ -915,6 +200,44 @@ class AuroLibraryRuntimeUtils {
915
200
  // See LICENSE in the project root for license information.
916
201
 
917
202
 
203
+ /**
204
+ * Serializes a multi-select value array back into the String `value` property.
205
+ * An empty (or missing) array collapses to `undefined` so an emptied selection
206
+ * clears `value` rather than reflecting a `"[]"` attribute.
207
+ * @private
208
+ * @param {Array<string>|undefined} values - The selected values.
209
+ * @returns {string|undefined} JSON string of the values, or undefined when empty.
210
+ */
211
+ function serializeMultiSelectValue(values) {
212
+ return values && values.length > 0 ? JSON.stringify(values) : undefined;
213
+ }
214
+
215
+ /**
216
+ * Validates if an option can be interacted with.
217
+ * @private
218
+ * @param {HTMLElement} option - The option to check.
219
+ * @returns {boolean} True if option is interactive.
220
+ */
221
+ function isOptionInteractive(option) {
222
+ return !option.hasAttribute('hidden') &&
223
+ !option.hasAttribute('disabled') &&
224
+ !option.hasAttribute('static');
225
+ }
226
+
227
+ /**
228
+ * Validates if an option may be selected by matching a programmatic value.
229
+ * Unlike `isOptionInteractive`, `hidden` is allowed: the combobox toggles
230
+ * `hidden` as its type-ahead filter, so a filtered-out option is still a
231
+ * valid programmatic selection. Only disabled and static options — which are
232
+ * never selectable — are rejected.
233
+ * @param {HTMLElement} option - The option to check.
234
+ * @returns {boolean} True if option can be selected by value.
235
+ */
236
+ function isSelectableByValue(option) {
237
+ return !option.hasAttribute('disabled') &&
238
+ !option.hasAttribute('static');
239
+ }
240
+
918
241
  /**
919
242
  * Helper method to dispatch custom events.
920
243
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -935,7 +258,7 @@ function dispatchMenuEvent(element, eventName, detail = null) {
935
258
  element.dispatchEvent(new CustomEvent(eventName, eventConfig));
936
259
  }
937
260
 
938
- /* eslint-disable no-underscore-dangle */
261
+ /* eslint-disable no-underscore-dangle, no-magic-numbers, max-lines, no-extra-parens, max-depth */
939
262
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
940
263
  // See LICENSE in the project root for license information.
941
264
 
@@ -949,16 +272,12 @@ function dispatchMenuEvent(element, eventName, detail = null) {
949
272
  * @event {CustomEvent<any>} auroMenu-customEventFired - Notifies that a custom event has been fired.
950
273
  * @event {CustomEvent<{ loading: boolean; hasLoadingPlaceholder: boolean; }>} auroMenu-loadingChange - Notifies when the loading attribute is changed.
951
274
  * @event {CustomEvent<any>} auroMenu-selectValueFailure - Notifies that an attempt to select a menuoption by matching a value has failed.
952
- * @event {CustomEvent<{ values: HTMLElement[] }>} auroMenu-deselectPrevented - Notifies that deselection was prevented and includes the affected options in `detail.values`.
953
275
  * @event {CustomEvent<any>} auroMenu-selectValueReset - Notifies that the component value has been reset.
954
276
  * @event {CustomEvent<any>} auroMenu-selectedOption - Notifies that a new menuoption selection has been made.
955
277
  * @slot loadingText - Text to show while loading attribute is set
956
278
  * @slot loadingIcon - Icon to show while loading attribute is set
957
279
  * @slot - Slot for insertion of menu options.
958
280
  */
959
-
960
- /* eslint-disable max-lines */
961
-
962
281
  class AuroMenu extends AuroElement {
963
282
 
964
283
  constructor() {
@@ -976,6 +295,8 @@ class AuroMenu extends AuroElement {
976
295
  */
977
296
  this.size = "sm";
978
297
 
298
+ // Value of the selected options
299
+ this.value = undefined;
979
300
  // Currently selected option
980
301
  this.optionSelected = undefined;
981
302
  // String used for highlighting/filtering
@@ -988,13 +309,24 @@ class AuroMenu extends AuroElement {
988
309
  this.loading = false;
989
310
  // Multi-select mode
990
311
  this.multiSelect = false;
991
- // Allow deselecting of menu options
992
- this.allowDeselect = false;
993
- // Select all matching options when setting value in multi-select mode
994
- this.selectAllMatchingOptions = false;
995
312
 
996
313
  // Event Bindings
997
314
 
315
+ /**
316
+ * @private
317
+ */
318
+ this.handleKeyDown = this.handleKeyDown.bind(this);
319
+
320
+ /**
321
+ * @private
322
+ */
323
+ this.handleMouseSelect = this.handleMouseSelect.bind(this);
324
+
325
+ /**
326
+ * @private
327
+ */
328
+ this.handleOptionHover = this.handleOptionHover.bind(this);
329
+
998
330
  /**
999
331
  * @private
1000
332
  */
@@ -1021,14 +353,6 @@ class AuroMenu extends AuroElement {
1021
353
  return {
1022
354
  ...super.properties,
1023
355
 
1024
- /**
1025
- * Allows deselecting an already selected option when clicked again in single-select mode.
1026
- */
1027
- allowDeselect: {
1028
- type: Boolean,
1029
- reflect: true,
1030
- },
1031
-
1032
356
  /**
1033
357
  * When true, the entire menu and all options are disabled.
1034
358
  */
@@ -1037,20 +361,6 @@ class AuroMenu extends AuroElement {
1037
361
  reflect: true
1038
362
  },
1039
363
 
1040
- /**
1041
- * Indicates whether the menu has a loadingIcon or loadingText to render when in a loading state.
1042
- */
1043
- hasLoadingPlaceholder: {
1044
- type: Boolean
1045
- },
1046
-
1047
- /**
1048
- * @private
1049
- */
1050
- layout: {
1051
- type: String
1052
- },
1053
-
1054
364
  /**
1055
365
  * Indent level for submenus.
1056
366
  * @private
@@ -1097,60 +407,26 @@ class AuroMenu extends AuroElement {
1097
407
 
1098
408
  /**
1099
409
  * Specifies the current active menuOption.
410
+ * @readonly
1100
411
  */
1101
412
  optionActive: {
1102
413
  type: Object,
1103
- attribute: 'optionactive'
414
+ attribute: false
1104
415
  },
1105
416
 
1106
417
  /**
1107
- * An array of currently selected menu options, type `HTMLElement` by default. In multi-select mode, `optionSelected` is an array of HTML elements.
418
+ * 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.
419
+ * @readonly
1108
420
  */
1109
421
  optionSelected: {
1110
422
  // Allow HTMLElement, HTMLElement[] arrays and undefined
1111
- type: Object
1112
- },
1113
-
1114
- /**
1115
- * Available menu options.
1116
- * @readonly
1117
- */
1118
- options: {
1119
- type: Array,
1120
- reflect: false,
423
+ type: Object,
1121
424
  attribute: false
1122
425
  },
1123
426
 
1124
- /**
1125
- * Sets the size of the menu.
1126
- * @type {'sm' | 'md'}
1127
- * @default 'sm'
1128
- */
1129
- size: {
1130
- type: String,
1131
- reflect: true
1132
- },
1133
-
1134
- /**
1135
- * When true, selects all options that match the provided value/key when setting value and multiselect is enabled.
1136
- */
1137
- selectAllMatchingOptions: {
1138
- type: Boolean,
1139
- reflect: true,
1140
- },
1141
-
1142
- /**
1143
- * Sets the shape of the menu.
1144
- * @type {'box' | 'round'}
1145
- * @default 'box'
1146
- */
1147
- shape: {
1148
- type: String,
1149
- reflect: true
1150
- },
1151
-
1152
427
  /**
1153
428
  * The value of the selected option. In multi-select mode, this is a JSON stringified array of selected option values.
429
+ * 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.
1154
430
  */
1155
431
  value: {
1156
432
  type: String,
@@ -1160,48 +436,17 @@ class AuroMenu extends AuroElement {
1160
436
  };
1161
437
  }
1162
438
 
1163
- static get styles() {
1164
- return [
1165
- styleCss$1,
1166
- colorCss$1,
1167
- tokensCss
1168
- ];
1169
- }
1170
-
1171
- /**
1172
- * @readonly
1173
- * @returns {string} - Returns the label of the currently selected option(s).
1174
- */
1175
- get currentLabel() {
1176
- return this.menuService.currentLabel;
1177
- };
1178
-
1179
- /**
1180
- * @readonly
1181
- * @returns {Array<HTMLElement>} - Returns the array of available menu options.
1182
- * @deprecated Use `options` property instead.
1183
- */
1184
- get items() {
1185
- return this.options;
1186
- }
1187
-
1188
- /**
1189
- * @returns {number} - Returns the index of the currently active option.
1190
- */
1191
- get index() {
1192
- return this._index;
1193
- }
1194
-
1195
- /**
1196
- * @param {number} value - Sets the index of the currently active option.
1197
- */
1198
- set index(value) {
1199
- this.menuService.setHighlightedIndex(value);
1200
- }
1201
-
439
+ static get styles() {
440
+ return [
441
+ styleCss$1,
442
+ colorCss$1,
443
+ tokensCss
444
+ ];
445
+ }
446
+
1202
447
  /**
1203
448
  * This will register this element with the browser.
1204
- * @param {string} [name="auro-menu"] - The name of the element that you want to register.
449
+ * @param {string} [name="auro-menu"] - The name of element that you want to register to.
1205
450
  *
1206
451
  * @example
1207
452
  * AuroMenu.register("custom-menu") // this will register this element to <custom-menu/>
@@ -1212,126 +457,116 @@ class AuroMenu extends AuroElement {
1212
457
  }
1213
458
 
1214
459
  /**
1215
- * Formatted value based on `multiSelect` state.
1216
- * Default type is `String`, changing to `Array<String>` when `multiSelect` is true.
1217
- * @private
1218
- * @returns {String|Array<String>}
460
+ * @readonly
461
+ * @returns {Array<HTMLElement>} - Returns the array of available menu options.
1219
462
  */
1220
- get formattedValue() {
1221
- return this.menuService.currentValue;
463
+ get options() {
464
+ return this.items;
1222
465
  }
1223
466
 
1224
467
  /**
1225
- * Gets the current property values for the menu service.
1226
- * @private
1227
- * @returns {Object}
468
+ * @returns {number} - Returns the index of the currently active option.
1228
469
  */
1229
- get propertyValues() {
1230
- return {
1231
- size: this.size,
1232
- shape: this.shape,
1233
- noCheckmark: this.nocheckmark,
1234
- disabled: this.disabled
1235
- };
470
+ get index() {
471
+ return this._index;
1236
472
  }
1237
473
 
1238
474
  /**
1239
- * Provides the menu context to child components.
1240
- * Initializes the MenuService and subscribes to menu changes.
1241
- * @protected
475
+ * @param {number} value - Sets the index of the currently active option.
1242
476
  */
1243
- provideContext() {
1244
- if (this.parentElement && this.parentElement.closest('auro-menu, [auro-menu]')) {
1245
- this.rootMenu = false;
1246
- this.menuService = this.parentElement.menuService;
1247
- this._contextProvider = this.parentElement._contextProvider;
1248
- return;
1249
- }
1250
-
1251
- this.menuService = new MenuService({host: this});
1252
- this.menuService.setProperties(this.propertyValues);
1253
- this.menuService.subscribe(this.handleMenuChange.bind(this));
1254
- this._contextProvider = new ContextProvider(this, {
1255
- context: MenuContext,
1256
- initialValue: this.menuService
1257
- });
477
+ set index(value) {
478
+ this.updateActiveOption(value);
1258
479
  }
1259
480
 
1260
481
  /**
1261
- * Updates the currently active option in the menu.
1262
- * @param {HTMLElement} option - The option to set as active.
482
+ * Gets the currently selected options as an array.
483
+ * @returns {Array<HTMLElement>}
1263
484
  */
1264
- updateActiveOption(option) {
1265
- this.menuService.setHighlightedOption(option);
485
+ get selectedOptions() {
486
+ if (!this.optionSelected) {
487
+ return [];
488
+ }
489
+ if (Array.isArray(this.optionSelected)) {
490
+ return this.optionSelected;
491
+ }
492
+ return [this.optionSelected];
1266
493
  }
1267
494
 
1268
495
  /**
1269
- * Sets the internal value and manages update state.
1270
- * @param {String|Array<String>} value - The value to set.
1271
- * @protected
496
+ * Gets the first selected option, or null if none.
497
+ * @returns {HTMLElement|null}
1272
498
  */
1273
- setInternalValue(value) {
1274
- if (this.value !== value) {
1275
- this.internalUpdateInProgress = true;
1276
- this.value = value;
1277
-
1278
- setTimeout(() => {
1279
- this.internalUpdateInProgress = false;
1280
- });
1281
- }
499
+ get selectedOption() {
500
+ const opts = this.selectedOptions;
501
+ return opts.length > 0 ? opts[0] : null;
1282
502
  }
1283
503
 
1284
504
  /**
1285
- * Handles changes from the menu service and updates component state.
1286
- * @param {Object} event - The event object from the menu service.
1287
- * @protected
505
+ * @readonly
506
+ * @returns {string} - Returns the label of the currently selected option(s).
1288
507
  */
1289
- handleMenuChange(event) {
1290
- if (event.type === 'valueChange') {
1291
-
1292
- // New option is array value or first option with fallback to undefined for empty array in all cases
1293
- const newOption = this.multiSelect && event.options.length ? event.options : event.options[0] || undefined;
1294
- const newValue = event.stringValue;
1295
-
1296
- // Check if the option or value has actually changed
1297
- if (this.optionSelected !== newOption || this.stringValue !== newValue) {
1298
- this.optionSelected = newOption;
1299
- this.setInternalValue(newValue);
1300
- }
1301
-
1302
- // Notify components of selection change
1303
- this.notifySelectionChange(event);
1304
- }
1305
-
1306
- if (event.type === 'highlightChange') {
1307
- this.optionActive = event.option;
1308
- this._index = event.index;
508
+ get currentLabel() {
509
+ if (!this.optionSelected) {
510
+ return '';
1309
511
  }
1310
-
1311
- if (event.type === 'optionsChange') {
1312
- this.options = event.options;
1313
- this.dispatchEvent(new CustomEvent('auroMenu-optionsChange', {
1314
- detail: {
1315
- options: event.options
1316
- }
1317
- }));
512
+ if (Array.isArray(this.optionSelected)) {
513
+ return this.optionSelected.map((opt) => opt.textContent).join(', ');
1318
514
  }
515
+ return this.optionSelected.textContent || '';
1319
516
  }
1320
517
 
1321
518
  /**
1322
- * Gets the currently selected options.
1323
- * @returns {Array<HTMLElement>}
519
+ * Formatted value based on `multiSelect` state.
520
+ * Default type is `String`, changing to `Array<String>` when `multiSelect` is true.
521
+ * @private
522
+ * @returns {String|Array<String>}
1324
523
  */
1325
- get selectedOptions() {
1326
- return this.menuService ? this.menuService.selectedOptions : [];
524
+ get formattedValue() {
525
+ if (this.multiSelect) {
526
+ if (!this.value) {
527
+ return undefined;
528
+ }
529
+ // Defensive: `value` is declared as String, but consumers may assign arrays or other
530
+ // types programmatically. Normalize without throwing so render/update never hard-crashes.
531
+ if (Array.isArray(this.value)) {
532
+ return this.value;
533
+ }
534
+ if (typeof this.value !== 'string') {
535
+ return [String(this.value)];
536
+ }
537
+ if (this.value.startsWith("[")) {
538
+ // Malformed JSON (e.g. a literal string that happens to start with "[") falls back
539
+ // to a single-item array rather than throwing during render.
540
+ try {
541
+ // any valid JSON starting with `[` ALWAYS parses to an array
542
+ return JSON.parse(this.value);
543
+ } catch {
544
+ return [this.value];
545
+ }
546
+ }
547
+ return [this.value];
548
+ }
549
+ return this.value;
1327
550
  }
1328
551
 
1329
552
  /**
1330
- * Gets the first selected option, or null if none.
1331
- * @returns {HTMLElement|null}
553
+ * 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.
554
+ * @param {string|string[]|undefined|null} value - The value(s) to select.
555
+ * @public
1332
556
  */
1333
- get selectedOption() {
1334
- return this.menuService ? this.menuService.selectedOptions[0] : null;
557
+ selectByValue(value) {
558
+ const isEmpty = value === undefined ||
559
+ value === null ||
560
+ (Array.isArray(value) && value.length === 0) ||
561
+ (typeof value === 'string' && value.trim() === '');
562
+
563
+ if (isEmpty) {
564
+ this.clearSelection();
565
+ return;
566
+ }
567
+
568
+ // `value` is a String property; stringify arrays so attribute reflection and `formattedValue` parsing stay correct.
569
+ this.value = Array.isArray(value) ? JSON.stringify(value) : value;
1335
570
  }
1336
571
 
1337
572
  // Lifecycle Methods
@@ -1339,9 +574,7 @@ class AuroMenu extends AuroElement {
1339
574
  connectedCallback() {
1340
575
  super.connectedCallback();
1341
576
 
1342
- this.provideContext();
1343
-
1344
- // this.addEventListener('keydown', this.handleKeyDown);
577
+ this.addEventListener('keydown', this.handleKeyDown);
1345
578
  this.addEventListener('auroMenuOption-click', this.handleMouseSelect);
1346
579
  this.addEventListener('auroMenuOption-mouseover', this.handleOptionHover);
1347
580
  this.addEventListener('slotchange', this.handleSlotChange);
@@ -1349,7 +582,7 @@ class AuroMenu extends AuroElement {
1349
582
  }
1350
583
 
1351
584
  disconnectedCallback() {
1352
- // this.removeEventListener('keydown', this.handleKeyDown);
585
+ this.removeEventListener('keydown', this.handleKeyDown);
1353
586
  this.removeEventListener('auroMenuOption-click', this.handleMouseSelect);
1354
587
  this.removeEventListener('auroMenuOption-mouseover', this.handleOptionHover);
1355
588
  this.removeEventListener('slotchange', this.handleSlotChange);
@@ -1364,25 +597,140 @@ class AuroMenu extends AuroElement {
1364
597
  this.initializeMenu();
1365
598
  }
1366
599
 
600
+ /**
601
+ * Sets an attribute that matches the default tag name if the tag name is not the default.
602
+ * @param {string} tagName - The tag name to set as an attribute.
603
+ * @private
604
+ */
605
+ setTagAttribute(tagName) {
606
+ if (this.tagName.toLowerCase() !== tagName) {
607
+ this.setAttribute(tagName, true);
608
+ }
609
+ }
1367
610
 
611
+ // eslint-disable-next-line complexity
1368
612
  updated(changedProperties) {
1369
613
  super.updated(changedProperties);
1370
614
 
1371
- // Apply value selection synchronously so that static-HTML fixtures
1372
- // resolve within a single update cycle. The refactored selectByValue
1373
- // no longer calls reset() first, so the destructive intermediate-event
1374
- // cascade that originally required deferral is eliminated. If option
1375
- // keys are not yet resolved (framework mount-order race), selectByValue
1376
- // queues a bounded retry automatically via queuePendingValue.
1377
- if (changedProperties.has('value') && !this.internalUpdateInProgress) {
1378
- this.menuService.selectByValue(this.value);
615
+ // Single source of truth for 'auroMenu-selectedOption'. Selection handlers
616
+ // mutate optionSelected and let Lit's update cycle dispatch here; the prior
617
+ // .value comparison missed multi-select array changes and combined with the
618
+ // explicit calls in handleDeselectState/makeSelection produced 2-3 duplicate
619
+ // events per selection.
620
+ if (changedProperties.has('optionSelected')) {
621
+ this.notifySelectionChange();
1379
622
  }
1380
623
 
1381
- // Handle loading state changes
1382
- if (changedProperties.has('loading')) {
1383
- this.setLoadingState(this.loading);
624
+ // Reset selection if multiSelect mode changes
625
+ if (changedProperties.has('multiSelect') && !changedProperties.has("value")) {
626
+ this.clearSelection();
627
+ }
628
+
629
+ if (changedProperties.has("value")) {
630
+ // Ensure items are populated before matching. `firstUpdated` normally initializes them,
631
+ // but a `value` change can arrive before slotted options are appended (e.g. parent sets
632
+ // value before children render). Without this guard, matching against an empty `items`
633
+ // would falsely dispatch `auroMenu-selectValueFailure` for valid initial values.
634
+ if (!this.items) {
635
+ this.initItems();
636
+ }
637
+
638
+ // Set when reconciliation reassigns `value` below. That reassignment schedules a
639
+ // second updated() cycle, so the `event`-attribute dispatch is deferred to that
640
+ // cycle to avoid firing option custom events twice on the same selection.
641
+ let valueReconciled = false;
642
+
643
+ // Handle null/undefined/empty case — empty/whitespace strings clear selection
644
+ // consistently with selectByValue(''), and avoid downstream `.includes('')` matches.
645
+ if (this.value === undefined || this.value === null || (typeof this.value === 'string' && this.value.trim() === '')) {
646
+ this.clearSelection();
647
+ } else {
648
+ let newSelected = null;
649
+
650
+ if (this.multiSelect) {
651
+ // In multiselect mode, this.value should be an array of strings.
652
+ // Defensive default: `formattedValue` can be undefined for unexpected value types,
653
+ // and calling `.includes` on undefined would throw during reconciliation.
654
+ const valueArray = this.formattedValue || [];
655
+ const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
656
+ newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
657
+
658
+ // Reconcile `value` with the selectable set. Drop only entries whose option is
659
+ // loaded but non-selectable (disabled/static) — leaving them would desync `value`
660
+ // from `optionSelected`, and the toggle handlers rebuild `value` from `formattedValue`,
661
+ // so the rejected entry would resurface on the next select/deselect. Entries with no
662
+ // matching item yet are preserved so async preselection still works once options render.
663
+ const rejectedValues = this.items
664
+ ? this.items.filter((item) => !isSelectableByValue(item) && valueArray.includes(item.value)).map((item) => item.value)
665
+ : [];
666
+ if (rejectedValues.length > 0) {
667
+ const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
668
+ this.value = serializeMultiSelectValue(reconciled);
669
+ valueReconciled = true;
670
+ }
671
+ } else {
672
+ // In single-select mode, this.value should be a string. Reject
673
+ // disabled/static options so a programmatic value pointing at a
674
+ // non-selectable option falls through to the no-match path below
675
+ // (dispatching auroMenu-selectValueFailure) instead of pinning it.
676
+ // `hidden` is intentionally NOT excluded: the combobox toggles
677
+ // `hidden` as its type-ahead filter, so a filtered-out option is
678
+ // still a valid programmatic selection.
679
+ const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
680
+
681
+ if (matchingOption) {
682
+ newSelected = matchingOption;
683
+ this._index = this.items.indexOf(matchingOption);
684
+ } else {
685
+ // If no matching option found, reset selection
686
+ newSelected = undefined;
687
+ this._index = -1;
688
+ }
689
+ }
690
+
691
+ // If no matching options were found in either mode
692
+ if (!newSelected || (Array.isArray(newSelected) && newSelected.length === 0)) {
693
+ // Defer failure when no options are loaded yet (async pattern: parent sets
694
+ // value before slotted options render). handleSlotChange re-runs matching
695
+ // once items arrive. Without this guard, a valid preselected value gets
696
+ // cleared by the failure listener before options ever exist to match against.
697
+ const hasItemsToMatch = this.items && this.items.length > 0;
698
+ if (hasItemsToMatch) {
699
+ // Clear state BEFORE dispatching so synchronous listeners (e.g. auro-select's
700
+ // updateDisplayedValue) read fresh `optionSelected` rather than the stale prior
701
+ // selection and re-render the old label.
702
+ if (this.optionSelected !== undefined) {
703
+ this.optionSelected = undefined;
704
+ }
705
+ this._index = -1;
706
+ dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
707
+ }
708
+ } else if (!this.selectionEquals(this.optionSelected, newSelected)) {
709
+ this.optionSelected = newSelected;
710
+ }
711
+ }
712
+
713
+ // Update UI state
714
+ this.updateItemsState(new Map([
715
+ [
716
+ 'optionSelected',
717
+ true
718
+ ]
719
+ ]));
720
+
721
+ // Notify of changes. Skip when reconciliation just reassigned `value`: the
722
+ // follow-on update cycle re-runs this branch and fires the events exactly once.
723
+ if (this.optionSelected !== undefined && !valueReconciled) {
724
+ const selected = Array.isArray(this.optionSelected) ? this.optionSelected : [this.optionSelected];
725
+ selected.forEach((opt) => {
726
+ if (opt.hasAttribute('event')) {
727
+ this.handleCustomEvent(opt);
728
+ }
729
+ });
730
+ }
1384
731
  }
1385
732
 
733
+ // Process all other UI updates
1386
734
  if (changedProperties.has('multiSelect') && this.rootMenu) {
1387
735
  if (this.multiSelect) {
1388
736
  this.setAttribute('aria-multiselectable', 'true');
@@ -1390,30 +738,129 @@ class AuroMenu extends AuroElement {
1390
738
  this.removeAttribute('aria-multiselectable');
1391
739
  }
1392
740
  }
741
+
742
+ this.updateItemsState(changedProperties);
1393
743
  }
1394
744
 
1395
745
  /**
1396
- * Sets an attribute that matches the default tag name if the tag name is not the default.
1397
- * @param {string} tagName - The tag name to set as an attribute.
746
+ * Updates the UI state and appearance of menu items based on changed properties.
1398
747
  * @private
748
+ * @param {Map<string, boolean>} changedProperties - LitElement's changed properties map.
1399
749
  */
1400
- setTagAttribute(tagName) {
1401
- if (this.tagName.toLowerCase() !== tagName) {
1402
- this.setAttribute(tagName, true);
750
+ updateItemsState(changedProperties) {
751
+ // Handle loading state changes
752
+ if (changedProperties.has('loading')) {
753
+ this.setAttribute("aria-busy", this.loading);
754
+ dispatchMenuEvent(this, "auroMenu-loadingChange", {
755
+ loading: this.loading,
756
+ hasLoadingPlaceholder: this.hasLoadingPlaceholder
757
+ });
1403
758
  }
1404
- }
1405
759
 
1406
- /**
1407
- * Sets the loading state and dispatches a loading change event.
1408
- * @param {boolean} isLoading - Whether the menu is loading.
1409
- * @protected
1410
- */
1411
- setLoadingState(isLoading) {
1412
- this.setAttribute("aria-busy", isLoading);
1413
- dispatchMenuEvent(this, "auroMenu-loadingChange", {
1414
- loading: isLoading,
1415
- hasLoadingPlaceholder: this.hasLoadingPlaceholder
760
+ if (!this.items) {
761
+ return;
762
+ }
763
+
764
+ // Handle noCheckmark propagation to all menus and options.
765
+ // Propagate in both directions so toggling back to false also clears nested elements
766
+ // (otherwise nested menus/options would stay stuck in no-checkmark mode).
767
+ if (changedProperties.has('noCheckmark')) {
768
+ this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]').forEach((element) => {
769
+ element.noCheckmark = this.noCheckmark;
770
+ });
771
+ }
772
+
773
+ // Handle layout propagation to all menus and options.
774
+ // Skip elements that had size/shape set by the author (marked in initItems);
775
+ // explicit per-option overrides must survive menu-level propagation.
776
+ const propagationTargets = this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]');
777
+ [
778
+ 'size',
779
+ 'shape'
780
+ ].forEach((prop) => {
781
+ if (changedProperties.has(prop)) {
782
+ const explicitKey = prop === 'size' ? '_explicitSize' : '_explicitShape';
783
+ propagationTargets.forEach((el) => {
784
+ if (el[explicitKey]) {
785
+ return;
786
+ }
787
+ el.setAttribute(prop, this[prop]);
788
+ });
789
+ }
790
+ });
791
+
792
+ // Regex for matchWord if needed
793
+ let regexWord = null;
794
+
795
+ if (changedProperties.has('matchWord') && this.matchWord && this.matchWord.length) {
796
+ const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
797
+ regexWord = new RegExp(escapedWord, 'giu');
798
+ }
799
+
800
+ // Handle direct item updates
801
+ this.items.forEach((option) => {
802
+ // Update selection if option or value changed
803
+ if (changedProperties.has('optionSelected') || changedProperties.has('value')) {
804
+ const isSelected = this.isOptionSelected(option);
805
+ option.setAttribute('aria-selected', isSelected ? 'true' : 'false');
806
+
807
+ // Add/remove selected attribute based on state
808
+ if (isSelected) {
809
+ option.setAttribute('selected', '');
810
+ } else {
811
+ option.removeAttribute('selected');
812
+ }
813
+ }
814
+
815
+ // Update text highlighting if matchWord changed
816
+ if (changedProperties.has('matchWord') && regexWord &&
817
+ isOptionInteractive(option) && !option.hasAttribute('persistent')) {
818
+ // Create nested spacers
819
+ const nested = option.querySelectorAll('.nestingSpacer');
820
+
821
+ const displayValueEl = option.querySelector('[slot="displayValue"]');
822
+ if (displayValueEl) {
823
+ option.removeChild(displayValueEl);
824
+ }
825
+
826
+ // Build highlighted content via DOM APIs rather than innerHTML so any
827
+ // `<`, `>`, or `&` in the option text renders literally (prevents XSS).
828
+ const originalText = option.textContent;
829
+ option.textContent = '';
830
+
831
+ nested.forEach(() => {
832
+ const spacer = document.createElement('span');
833
+ spacer.className = 'nestingSpacer';
834
+ option.appendChild(spacer);
835
+ });
836
+
837
+ const matches = [...originalText.matchAll(regexWord)];
838
+ let lastIndex = 0;
839
+ matches.forEach((match) => {
840
+ const [matchText] = match;
841
+ if (match.index > lastIndex) {
842
+ option.appendChild(document.createTextNode(originalText.slice(lastIndex, match.index)));
843
+ }
844
+ const strong = document.createElement('strong');
845
+ strong.textContent = matchText;
846
+ option.appendChild(strong);
847
+ lastIndex = match.index + matchText.length;
848
+ });
849
+ if (lastIndex < originalText.length) {
850
+ option.appendChild(document.createTextNode(originalText.slice(lastIndex)));
851
+ }
852
+
853
+ if (displayValueEl) {
854
+ option.append(displayValueEl);
855
+ }
856
+ }
857
+
858
+ // Update disabled state
859
+ if (changedProperties.has('disabled')) {
860
+ option.disabled = this.disabled;
861
+ }
1416
862
  });
863
+
1417
864
  }
1418
865
 
1419
866
  // Init Methods
@@ -1423,6 +870,7 @@ class AuroMenu extends AuroElement {
1423
870
  * @private
1424
871
  */
1425
872
  initializeMenu() {
873
+ this.initItems();
1426
874
  if (this.rootMenu) {
1427
875
  this.setAttribute('role', 'listbox');
1428
876
  this.setAttribute('root', '');
@@ -1432,15 +880,111 @@ class AuroMenu extends AuroElement {
1432
880
  }
1433
881
  }
1434
882
 
883
+ // Must run for nested menus too — sets level, role="group", and aria-label="submenu" based on parent.
1435
884
  this.handleNestedMenus(this);
1436
885
  }
1437
886
 
1438
887
  /**
1439
- * Selects the currently highlighted option.
1440
- * @protected
888
+ * Initializes menu items and their attributes.
889
+ * @private
1441
890
  */
1442
- makeSelection() {
1443
- this.menuService.selectHighlightedOption();
891
+ initItems() {
892
+ const found = Array.from(this.querySelectorAll('auro-menuoption, [auro-menuoption]'));
893
+ this.items = found.length > 0 ? found : undefined;
894
+
895
+ // Record whether each propagation target had an author-set size/shape attribute
896
+ // BEFORE menu has had a chance to propagate. Marker is set once per element so a
897
+ // later menu-driven setAttribute doesn't re-flag the element as "explicit".
898
+ this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]').forEach((el) => {
899
+ if (el._explicitSize === undefined) {
900
+ el._explicitSize = el.hasAttribute('size');
901
+ }
902
+ if (el._explicitShape === undefined) {
903
+ el._explicitShape = el.hasAttribute('shape');
904
+ }
905
+ });
906
+
907
+ if (this.noCheckmark) {
908
+ this.updateItemsState(new Map([
909
+ [
910
+ 'noCheckmark',
911
+ true
912
+ ]
913
+ ]));
914
+ }
915
+
916
+ this.dispatchEvent(new CustomEvent('auroMenu-optionsChange', {
917
+ detail: {
918
+ options: this.items
919
+ }
920
+ }));
921
+ }
922
+
923
+ // Logic Methods
924
+
925
+ /**
926
+ * Updates menu state when an option is selected.
927
+ * @private
928
+ * @param {HTMLElement} option - The option element to select.
929
+ */
930
+ handleSelectState(option) {
931
+ if (this.multiSelect) {
932
+ const currentValue = this.formattedValue || [];
933
+ const currentSelected = this.optionSelected || [];
934
+
935
+ if (!currentValue.includes(option.value)) {
936
+ this.value = serializeMultiSelectValue([
937
+ ...currentValue,
938
+ option.value
939
+ ]);
940
+ }
941
+ if (!currentSelected.includes(option)) {
942
+ this.optionSelected = [
943
+ ...currentSelected,
944
+ option
945
+ ];
946
+ }
947
+ } else {
948
+ this.value = option.value;
949
+ this.optionSelected = option;
950
+ }
951
+
952
+ this._index = this.items.indexOf(option);
953
+ }
954
+
955
+ /**
956
+ * Deselects a menu option and updates related state.
957
+ * @private
958
+ * @param {HTMLElement} option - The menuoption to be deselected.
959
+ */
960
+ handleDeselectState(option) {
961
+ if (this.multiSelect) {
962
+ // Remove this option from array; an empty result collapses `value` to undefined.
963
+ const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
964
+ this.value = serializeMultiSelectValue(newFormattedValue);
965
+
966
+ this.optionSelected = this.optionSelected.filter((val) => val !== option);
967
+ if (this.optionSelected.length === 0) {
968
+ this.optionSelected = undefined;
969
+ }
970
+ } else {
971
+ // For single-select: Back to undefined when deselected
972
+ this.value = undefined;
973
+ this.optionSelected = undefined;
974
+ }
975
+
976
+ // Update the index tracking
977
+ this._index = this.items.indexOf(option);
978
+
979
+ // Update UI to reflect changes
980
+ this.updateItemsState(new Map([
981
+ [
982
+ 'optionSelected',
983
+ true
984
+ ]
985
+ ]));
986
+
987
+ // Notification happens via updated() when optionSelected changes above.
1444
988
  }
1445
989
 
1446
990
  /**
@@ -1459,7 +1003,23 @@ class AuroMenu extends AuroElement {
1459
1003
  * @public
1460
1004
  */
1461
1005
  reset() {
1462
- this.menuService.reset();
1006
+ // Reset to undefined - initial state
1007
+ this.value = undefined;
1008
+ this.optionSelected = undefined;
1009
+ this._index = -1;
1010
+
1011
+ // Clear active option state so a follow-up open/navigation starts fresh
1012
+ // rather than reusing a stale reference from before the reset.
1013
+ this.items?.forEach((item) => item.classList.remove('active'));
1014
+ this.optionActive = undefined;
1015
+
1016
+ // Reset UI state
1017
+ this.updateItemsState(new Map([
1018
+ [
1019
+ 'optionSelected',
1020
+ true
1021
+ ]
1022
+ ]));
1463
1023
 
1464
1024
  // Dispatch reset event
1465
1025
  dispatchMenuEvent(this, 'auroMenu-selectValueReset');
@@ -1471,6 +1031,14 @@ class AuroMenu extends AuroElement {
1471
1031
  * @param {HTMLElement} menu - Root menu element.
1472
1032
  */
1473
1033
  handleNestedMenus(menu) {
1034
+ // Slot changes can fire on a menu mid-teardown (e.g. while a parent menu
1035
+ // is removing children to rebuild its content). In that window the menu
1036
+ // is detached and parentElement is null. Skip — handleNestedMenus will
1037
+ // run again when the menu is reattached.
1038
+ if (!menu.parentElement) {
1039
+ return;
1040
+ }
1041
+
1474
1042
  menu.level = menu.parentElement.level >= 0 ? menu.parentElement.level + 1 : 0;
1475
1043
 
1476
1044
  if (menu.level > 0) {
@@ -1481,34 +1049,221 @@ class AuroMenu extends AuroElement {
1481
1049
  }
1482
1050
  }
1483
1051
 
1484
- const options = menu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption]');
1485
- options.forEach((option) => {
1486
- const regex = new RegExp(this.nestingSpacer, "gu");
1487
- option.innerHTML = this.nestingSpacer.repeat(menu.level) + option.innerHTML.replace(regex, '');
1488
- });
1052
+ const options = menu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption]');
1053
+ options.forEach((option) => {
1054
+ const regex = new RegExp(this.nestingSpacer, "gu");
1055
+ option.innerHTML = this.nestingSpacer.repeat(menu.level) + option.innerHTML.replace(regex, '');
1056
+ });
1057
+ }
1058
+
1059
+ // Event Handlers
1060
+
1061
+ /**
1062
+ * Makes a selection based on the current index.
1063
+ * @private
1064
+ */
1065
+ makeSelection() {
1066
+ if (!this.items) {
1067
+ this.initItems();
1068
+ }
1069
+
1070
+ // Get currently selected menu option based on index
1071
+ const option = this.items ? this.items[this._index] : undefined;
1072
+
1073
+ // Return early if option is not interactive
1074
+ if (!option || !isOptionInteractive(option)) {
1075
+ return;
1076
+ }
1077
+
1078
+ // Handle custom events first
1079
+ if (option.hasAttribute('event')) {
1080
+ this.handleCustomEvent(option);
1081
+ return;
1082
+ }
1083
+
1084
+ if (this.multiSelect) {
1085
+ // In multiselect, toggle individual selections
1086
+ this.toggleOption(option);
1087
+ // In single select, only handle selection of new options
1088
+ } else if (!this.isOptionSelected(option)) {
1089
+ this.clearSelection();
1090
+ this.handleSelectState(option);
1091
+ } else {
1092
+ // Re-selecting the already-selected option in single-select doesn't change
1093
+ // state, so updated() won't fire. Notify explicitly so consumers (e.g.
1094
+ // auro-select closing its dropdown on Enter) still get the event.
1095
+ this.notifySelectionChange();
1096
+ }
1097
+ }
1098
+
1099
+ /**
1100
+ * Toggle the selection state of the menuoption.
1101
+ * @private
1102
+ * @param {HTMLElement} option - The menuoption to toggle.
1103
+ */
1104
+ toggleOption(option) {
1105
+ const isCurrentlySelected = this.isOptionSelected(option);
1106
+
1107
+ if (isCurrentlySelected) {
1108
+ this.handleDeselectState(option);
1109
+ } else if (option.value === undefined || option.value === '') {
1110
+ dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
1111
+ } else {
1112
+ this.handleSelectState(option);
1113
+ }
1114
+ }
1115
+
1116
+ /**
1117
+ * Handles keyboard navigation and selection.
1118
+ * @private
1119
+ * @param {KeyboardEvent} event - The keydown event.
1120
+ */
1121
+ handleKeyDown(event) {
1122
+ switch (event.key) {
1123
+ case "ArrowDown":
1124
+ event.preventDefault();
1125
+ this.navigateOptions('down');
1126
+ break;
1127
+ case "ArrowUp":
1128
+ event.preventDefault();
1129
+ this.navigateOptions('up');
1130
+ break;
1131
+ case "Tab":
1132
+ // Do not preventDefault on Tab so focus can move out of the menu (a11y: avoid trapping keyboard users).
1133
+ this.makeSelection();
1134
+ break;
1135
+ case "Enter":
1136
+ event.preventDefault();
1137
+ this.makeSelection();
1138
+ break;
1139
+ }
1140
+ }
1141
+
1142
+ /**
1143
+ * Handles option selection via click events from menuoptions.
1144
+ * @private
1145
+ * @param {CustomEvent} event - The auroMenuOption-click event.
1146
+ */
1147
+ handleMouseSelect(event) {
1148
+ if (!this.rootMenu || this.disabled) {
1149
+ return;
1150
+ }
1151
+
1152
+ const option = event.detail;
1153
+ if (option && this.items) {
1154
+ const idx = this.items.indexOf(option);
1155
+ if (idx >= 0) {
1156
+ this._index = idx;
1157
+ this.makeSelection();
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Handles option hover events.
1164
+ * @private
1165
+ * @param {CustomEvent} event - Event object from the browser.
1166
+ */
1167
+ handleOptionHover(event) {
1168
+ const option = event.detail;
1169
+ if (this.items) {
1170
+ const idx = this.items.indexOf(option);
1171
+ if (idx >= 0) {
1172
+ this.updateActiveOption(idx);
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ /**
1178
+ * Handles slot change events.
1179
+ * @private
1180
+ */
1181
+ handleSlotChange() {
1182
+ if (this.parentElement && this.parentElement.closest('auro-menu, [auro-menu]')) {
1183
+ this.rootMenu = false;
1184
+ }
1185
+
1186
+ // Nested menus must also reinitialize so items, level, role="group", and aria-label refresh on content changes.
1187
+ // Root-specific attributes (listbox/root/aria-multiselectable) remain gated by `rootMenu` inside initializeMenu.
1188
+ this.initializeMenu();
1189
+
1190
+ // When options arrive after `value` was set (async option load), re-run matching
1191
+ // against the now-populated items. The earlier updated('value') call deferred
1192
+ // the failure dispatch because items were empty; this triggers the match now.
1193
+ const hasPendingValue = this.value !== undefined &&
1194
+ this.value !== null &&
1195
+ !(typeof this.value === 'string' && this.value.trim() === '');
1196
+ if (hasPendingValue && this.items && this.items.length > 0 && this.optionSelected === undefined) {
1197
+ this.requestUpdate('value', undefined);
1198
+ }
1489
1199
  }
1490
1200
 
1491
1201
  /**
1492
- * Navigates the menu options in the specified direction.
1493
- * @param {'up'|'down'} direction - The direction to navigate.
1494
- * @protected
1202
+ * Navigates through options using keyboard.
1203
+ * @param {string} direction - 'up' or 'down'.
1495
1204
  */
1496
1205
  navigateOptions(direction) {
1497
- if (direction === 'up') {
1498
- this.menuService.highlightPrevious();
1499
- } else if (direction === 'down') {
1500
- this.menuService.highlightNext();
1206
+ // Return early if no items exist
1207
+ if (!this.items || !this.items.length) {
1208
+ return;
1209
+ }
1210
+
1211
+ let newIndex = this._index;
1212
+ if (newIndex === -1 && direction === 'up') {
1213
+ newIndex = this.items.length;
1214
+ }
1215
+ const increment = direction === 'down' ? 1 : -1;
1216
+ const maxIterations = this.items.length;
1217
+ let iterations = 0;
1218
+ let foundInteractiveOption = false;
1219
+
1220
+ do {
1221
+ newIndex = (newIndex + increment + this.items.length) % this.items.length;
1222
+ iterations += 1;
1223
+
1224
+ // Check if current option is interactive
1225
+ const currentOption = this.items[newIndex];
1226
+ if (isOptionInteractive(currentOption)) {
1227
+ foundInteractiveOption = true;
1228
+ break;
1229
+ }
1230
+
1231
+ // Break if all options were checked
1232
+ if (iterations >= maxIterations) {
1233
+ break;
1234
+ }
1235
+ } while (iterations < maxIterations);
1236
+
1237
+ // Update only if an interactive option was found
1238
+ if (foundInteractiveOption) {
1239
+ this.updateActiveOption(newIndex);
1501
1240
  }
1502
1241
  }
1503
1242
 
1504
1243
  /**
1505
- * Handles slot change events.
1506
- * @private
1244
+ * Updates the active option state and dispatches events.
1245
+ * Accepts either a numeric index or an HTMLElement option.
1246
+ * @param {number|HTMLElement} indexOrOption - Index of the option or the option element to make active.
1507
1247
  */
1508
- handleSlotChange() {
1509
- if (this.rootMenu) {
1510
- this.initializeMenu();
1248
+ updateActiveOption(indexOrOption) {
1249
+ let idx = -1;
1250
+
1251
+ if (typeof indexOrOption === 'number') {
1252
+ idx = indexOrOption;
1253
+ } else {
1254
+ idx = this.items ? this.items.indexOf(indexOrOption) : -1;
1511
1255
  }
1256
+
1257
+ if (!this.items || !this.items[idx]) {
1258
+ return;
1259
+ }
1260
+
1261
+ this.items.forEach((item) => item.classList.remove('active'));
1262
+ this.items[idx].classList.add('active');
1263
+ this.optionActive = this.items[idx];
1264
+ this._index = idx;
1265
+
1266
+ dispatchMenuEvent(this, 'auroMenu-activatedOption', this.items[idx]);
1512
1267
  }
1513
1268
 
1514
1269
  /**
@@ -1518,8 +1273,8 @@ class AuroMenu extends AuroElement {
1518
1273
  */
1519
1274
  handleCustomEvent(option) {
1520
1275
  const eventName = option.getAttribute('event');
1521
- dispatchMenuEvent(this, eventName);
1522
- dispatchMenuEvent(this, 'auroMenu-customEventFired');
1276
+ dispatchMenuEvent(this, eventName, { option });
1277
+ dispatchMenuEvent(this, 'auroMenu-customEventFired', { option });
1523
1278
  }
1524
1279
 
1525
1280
  /**
@@ -1527,14 +1282,33 @@ class AuroMenu extends AuroElement {
1527
1282
  * @param {any} source - The source that triggers this event.
1528
1283
  * @private
1529
1284
  */
1530
- notifySelectionChange({value, stringValue, keys, options, reason} = {}) {
1531
- dispatchMenuEvent(this, 'auroMenu-selectedOption', {
1532
- value,
1533
- stringValue,
1534
- keys,
1535
- options,
1536
- reason
1537
- });
1285
+ notifySelectionChange(source = undefined) {
1286
+ dispatchMenuEvent(this, 'auroMenu-selectedOption', { source });
1287
+ }
1288
+
1289
+ /**
1290
+ * @private
1291
+ * @param {any} current - Current selection.
1292
+ * @param {any} next - New selection to compare.
1293
+ * @returns {boolean} Whether the selections are equal.
1294
+ */
1295
+ selectionEquals(current, next) {
1296
+ if (current === next) {
1297
+ return true;
1298
+ }
1299
+ if (!current || !next) {
1300
+ return false;
1301
+ }
1302
+ if (Array.isArray(current) && Array.isArray(next)) {
1303
+ if (current.length !== next.length) {
1304
+ return false;
1305
+ }
1306
+ // Compare as sets — selection order may differ from DOM order after value-driven
1307
+ // reconciliation, but the selected set is what matters for change detection.
1308
+ const nextSet = new Set(next);
1309
+ return current.every((item) => nextSet.has(item));
1310
+ }
1311
+ return false;
1538
1312
  }
1539
1313
 
1540
1314
  /**
@@ -1549,7 +1323,6 @@ class AuroMenu extends AuroElement {
1549
1323
  }
1550
1324
 
1551
1325
  if (this.multiSelect) {
1552
- // In multi-select mode, check if the option is in the selected array
1553
1326
  return Array.isArray(this.optionSelected) && this.optionSelected.some((selectedOption) => selectedOption === option);
1554
1327
  }
1555
1328
 
@@ -1689,40 +1462,16 @@ let menuOptionIdCounter = 0;
1689
1462
  * The `auro-menuoption` element provides users a way to define a menu option.
1690
1463
  * @customElement auro-menuoption
1691
1464
  *
1465
+ * @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.
1692
1466
  * @slot default - The default slot for the menu option text.
1693
1467
  *
1694
1468
  * @event auroMenuOption-mouseover - Notifies that this option has been hovered over.
1469
+ * @event auroMenuOption-click - Notifies that this option has been clicked.
1695
1470
  */
1696
1471
  class AuroMenuOption extends AuroElement {
1697
-
1698
- /**
1699
- * This will register this element with the browser.
1700
- * @param {string} [name="auro-menuoption"] - The name of the element that you want to register.
1701
- *
1702
- * @example
1703
- * AuroMenuOption.register("custom-menuoption") // this will register this element to <custom-menuoption/>
1704
- *
1705
- */
1706
- static register(name = "auro-menuoption") {
1707
- AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroMenuOption);
1708
- }
1709
-
1710
- /**
1711
- * Returns whether the menu option is currently active and selectable.
1712
- * An option is considered active if it is not hidden, not disabled, and not static.
1713
- * @returns {boolean} True if the option is active, false otherwise.
1714
- */
1715
- get isActive() {
1716
- return !this.hasAttribute('hidden') &&
1717
- !this.disabled &&
1718
- !this.hasAttribute('static');
1719
- }
1720
-
1721
1472
  constructor() {
1722
1473
  super();
1723
1474
 
1724
- this.bindEvents();
1725
-
1726
1475
  /**
1727
1476
  * @private
1728
1477
  */
@@ -1743,83 +1492,62 @@ class AuroMenuOption extends AuroElement {
1743
1492
  this.noCheckmark = false;
1744
1493
  this.disabled = false;
1745
1494
  this.noMatch = false;
1495
+ this.persistent = false;
1746
1496
 
1747
1497
  /**
1748
1498
  * @private
1749
1499
  */
1750
- this.runtimeUtils = new AuroLibraryRuntimeUtils();
1751
-
1752
- // Initialize context-related properties
1753
- this.menuService = null;
1754
- this.unsubscribe = null;
1500
+ this.tabIndex = -1;
1755
1501
 
1756
1502
  /**
1757
1503
  * @private
1758
1504
  */
1759
- this.handleMenuChange = this.handleMenuChange.bind(this);
1505
+ this.runtimeUtils = new AuroLibraryRuntimeUtils();
1506
+
1507
+ this.addEventListener('click', this.handleClick.bind(this));
1760
1508
  }
1761
1509
 
1762
1510
  static get properties() {
1763
1511
  return {
1764
1512
  ...super.properties,
1765
-
1766
- /**
1767
- * When true, disables the menu option.
1768
- */
1769
- disabled: {
1513
+ noCheckmark: {
1770
1514
  type: Boolean,
1771
1515
  reflect: true
1772
1516
  },
1773
1517
 
1774
1518
  /**
1775
- * @private
1519
+ * **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.
1520
+ *
1521
+ * @deprecated Use the `value` attribute on `auro-menu` instead.
1776
1522
  */
1777
- event: {
1778
- type: String,
1523
+ selected: {
1524
+ type: Boolean,
1779
1525
  reflect: true
1780
1526
  },
1781
-
1782
- /**
1783
- * @private
1784
- */
1785
- layout: {
1786
- type: String
1527
+ disabled: {
1528
+ type: Boolean,
1529
+ reflect: true
1787
1530
  },
1788
-
1789
- /**
1790
- * Allows users to set a unique key for the menu option for specified option selection. If no key is provided, the value property will be used.
1791
- */
1792
- key: {
1531
+ value: {
1793
1532
  type: String,
1794
1533
  reflect: true
1795
1534
  },
1796
-
1797
- /**
1798
- * @private
1799
- */
1800
- menuService: {
1801
- type: Object,
1802
- state: true
1535
+ tabIndex: {
1536
+ type: Number,
1537
+ reflect: true
1803
1538
  },
1804
1539
 
1805
1540
  /**
1806
1541
  * @private
1807
1542
  */
1808
- matchWord: {
1543
+ event: {
1809
1544
  type: String,
1810
- state: true
1811
- },
1812
-
1813
- /**
1814
- * @private
1815
- */
1816
- noCheckmark: {
1817
- type: Boolean,
1818
1545
  reflect: true
1819
1546
  },
1820
1547
 
1821
1548
  /**
1822
- * 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.
1549
+ * When true, marks this option as the "no matching results" placeholder shown by combobox
1550
+ * when the user's input does not match any available options.
1823
1551
  */
1824
1552
  noMatch: {
1825
1553
  type: Boolean,
@@ -1827,29 +1555,11 @@ class AuroMenuOption extends AuroElement {
1827
1555
  attribute: 'nomatch'
1828
1556
  },
1829
1557
 
1830
- /**
1831
- * Specifies that an option is selected.
1832
- */
1833
- selected: {
1558
+ /** 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. */
1559
+ persistent: {
1834
1560
  type: Boolean,
1835
1561
  reflect: true
1836
1562
  },
1837
-
1838
- /**
1839
- * Specifies the tab index of the menu option.
1840
- */
1841
- tabIndex: {
1842
- type: Number,
1843
- reflect: true
1844
- },
1845
-
1846
- /**
1847
- * Specifies the value to be sent to a server.
1848
- */
1849
- value: {
1850
- type: String,
1851
- reflect: true
1852
- },
1853
1563
  };
1854
1564
  }
1855
1565
 
@@ -1861,41 +1571,48 @@ class AuroMenuOption extends AuroElement {
1861
1571
  ];
1862
1572
  }
1863
1573
 
1574
+ /**
1575
+ * This will register this element with the browser.
1576
+ * @param {string} [name="auro-menuoption"] - The name of element that you want to register to.
1577
+ *
1578
+ * @example
1579
+ * AuroMenuOption.register("custom-menuoption") // this will register this element to <custom-menuoption/>
1580
+ *
1581
+ */
1582
+ static register(name = "auro-menuoption") {
1583
+ AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroMenuOption);
1584
+ }
1585
+
1586
+ /**
1587
+ * Returns whether the menu option is currently active and selectable.
1588
+ * @returns {boolean}
1589
+ */
1590
+ get isActive() {
1591
+ return !this.hasAttribute('hidden') &&
1592
+ !this.disabled &&
1593
+ !this.hasAttribute('static');
1594
+ }
1595
+
1864
1596
  connectedCallback() {
1865
1597
  super.connectedCallback();
1866
1598
 
1867
- // Add the tag name as an attribute if it is different than the component name
1868
- // Add this step soon as this node gets attached to the DOM to avoid racing condition with menu's value setting logic.
1869
1599
  this.runtimeUtils.handleComponentTagRename(this, 'auro-menuoption');
1870
-
1871
- // Set up context consumption in connectedCallback
1872
- this._contextConsumer = new ContextConsumer(this, {
1873
- context: MenuContext,
1874
- callback: this.attachTo.bind(this),
1875
- subscribe: true
1876
- });
1877
-
1878
- // Establish the key property as early as possible.
1879
- // When a framework (e.g. Svelte) inserts the element into the DOM before
1880
- // setting its `value` property, both `getAttribute('value')` and
1881
- // `getAttribute('key')` return null here. Setting `this.key = null`
1882
- // would block the fallback in `updated()` that assigns key from the
1883
- // value property (the guard checked `=== undefined`). Only assign key
1884
- // if at least one source attribute is actually present so that the
1885
- // `updated()` fallback can run when the value property arrives later.
1886
- const valueAttr = this.getAttribute('value');
1887
- const keyAttr = this.getAttribute('key');
1888
- const resolvedKey = keyAttr !== null ? keyAttr : valueAttr;
1889
- if (resolvedKey !== null) {
1890
- this.key = resolvedKey;
1891
- }
1892
1600
  }
1893
1601
 
1894
1602
  firstUpdated() {
1895
- // Add the tag name as an attribute if it is different than the component name
1896
1603
  this.runtimeUtils.handleComponentTagRename(this, 'auro-menuoption');
1897
1604
 
1898
- // Generate unique ID if not already set (required for aria-activedescendant)
1605
+ // firstUpdated can fire on an option that was detached before the first
1606
+ // render completed (e.g. dynamic-menu rebuilds clear and repopulate the
1607
+ // menu mid-cycle). parentElement is null in that case — fall back to
1608
+ // defaults; the next render after re-attachment will pick up real values.
1609
+ if (!this.hasAttribute('size')) {
1610
+ this.size = this.parentElement ? this.parentElement.getAttribute('size') || 'sm' : 'sm';
1611
+ }
1612
+ if (!this.hasAttribute('shape')) {
1613
+ this.shape = this.parentElement ? this.parentElement.getAttribute('shape') || 'box' : 'box';
1614
+ }
1615
+
1899
1616
  if (!this.id) {
1900
1617
  menuOptionIdCounter += 1;
1901
1618
  this.id = `menuoption-${menuOptionIdCounter}`;
@@ -1917,16 +1634,8 @@ class AuroMenuOption extends AuroElement {
1917
1634
  updated(changedProperties) {
1918
1635
  super.updated(changedProperties);
1919
1636
 
1920
- // Update aria-selected attribute if selected changed
1921
1637
  if (changedProperties.has('selected')) {
1922
-
1923
- // Update aria-selected attribute
1924
1638
  this.setAttribute('aria-selected', this.selected.toString());
1925
-
1926
- // Update menu service selection state if this isn't an internal update
1927
- if (this.internalUpdateInProgress !== true && this.menuService) {
1928
- this.menuService[this.selected ? 'selectOption' : 'deselectOption'](this);
1929
- }
1930
1639
  }
1931
1640
 
1932
1641
  if (changedProperties.has('disabled')) {
@@ -1936,231 +1645,48 @@ class AuroMenuOption extends AuroElement {
1936
1645
  this.removeAttribute('aria-disabled');
1937
1646
  }
1938
1647
  }
1939
-
1940
- if (changedProperties.has('active')) {
1941
- this.updateActiveClasses();
1942
- }
1943
-
1944
- // Update text highlight if matchWord changed
1945
- if (changedProperties.has('matchWord')) {
1946
- this.updateTextHighlight();
1947
- }
1948
-
1949
- // Set the key to be the passed value if no key is provided.
1950
- // Loose equality (== null) is intentional: it catches both null AND
1951
- // undefined. When a framework (e.g. Svelte, React) inserts the element
1952
- // before setting its value property, connectedCallback skips key
1953
- // assignment because both attributes are null at that point. The Lit
1954
- // property default for `key` is undefined (not null), so strict
1955
- // === null would miss the case and the fallback would never run.
1956
- if (changedProperties.has('value') && this.key == null) { // eslint-disable-line eqeqeq, no-eq-null
1957
- this.key = this.value;
1958
- }
1959
- }
1960
-
1961
- disconnectedCallback() {
1962
- if (this.menuService) {
1963
- this.menuService.unsubscribe(this.handleMenuChange);
1964
- this.menuService.removeMenuOption(this);
1965
- }
1966
- }
1967
-
1968
- /**
1969
- * Sets up event listeners for user interaction with the menu option.
1970
- * This function enables click and mouse enter events to trigger selection and highlighting logic.
1971
- */
1972
- bindEvents() {
1973
- this.addEventListener('click', this.handleClick.bind(this));
1974
- this.addEventListener('mouseenter', this.handleMouseEnter.bind(this));
1975
1648
  }
1976
1649
 
1977
- /**
1978
- * Attaches this menu option to a menu service and subscribes to its events.
1979
- * This method enables the option to participate in menu selection and highlighting logic.
1980
- * @param {Object} service - The menu service instance to attach to.
1981
- */
1982
- attachTo(service) {
1983
- if (!service) {
1984
- return;
1985
- }
1986
- this.menuService = service;
1987
- this.menuService.addMenuOption(this);
1988
- this.menuService.subscribe(this.handleMenuChange);
1650
+ handleMenuChange() {
1651
+ // no-op: menu owns state in the distributed architecture
1989
1652
  }
1990
1653
 
1991
- /**
1992
- * Handles changes from the menu service and updates the option's state.
1993
- * This function synchronizes the option's properties and selection/highlight state with menu events.
1994
- * @param {Object} event - The event object from the menu service.
1995
- */
1996
- handleMenuChange(event) {
1997
-
1998
- // Ignore events without a type or property
1999
- if (!event || (!event.type && !event.property)) {
2000
- return;
2001
- }
2002
-
2003
- // Update reactive properties based on event type
2004
- if (event.property && Object.keys(AuroMenuOption.properties).includes(event.property)) {
2005
- this[event.property] = event.value;
2006
- }
2007
-
2008
- // Handle highlight changes
2009
- if (event.type === 'highlightChange') {
2010
- const isActive = event.option === this;
2011
- this.active = isActive;
2012
- this.updateActiveClasses();
2013
- }
2014
-
2015
- if (event.type === 'stateChange') {
2016
- const isSelected = event.selectedOptions.includes(this);
2017
- this.setInternalSelected(isSelected);
2018
- }
1654
+ setSelected(value) {
1655
+ this.selected = value;
2019
1656
  }
2020
1657
 
2021
- /**
2022
- * Updates the internal selected state of the menu option bypassing 'updated' and triggers custom events if selected.
2023
- * This function ensures the option's selection state is synchronized with menu logic and notifies listeners.
2024
- * @param {boolean} isSelected - Whether the option should be marked as selected.
2025
- */
2026
- setInternalSelected(isSelected) {
2027
- this.internalUpdateInProgress = true;
2028
- this.selected = isSelected;
2029
-
2030
- // Fire custom event if selected
2031
- if (isSelected) {
2032
- this.handleCustomEvent();
1658
+ updateActive(active) {
1659
+ this.active = active;
1660
+ if (active) {
1661
+ this.classList.add('active');
1662
+ } else {
1663
+ this.classList.remove('active');
2033
1664
  }
2034
-
2035
- setTimeout(() => {
2036
- this.internalUpdateInProgress = false;
2037
- }, 0);
2038
- }
2039
-
2040
- /**
2041
- * Sets the selected state of the menu option.
2042
- * This function updates whether the option is currently selected.
2043
- * @param {boolean} isSelected - Whether the option should be marked as selected.
2044
- * @deprecated Simply modify the `selected` property directly instead.
2045
- */
2046
- setSelected(isSelected) {
2047
- this.selected = isSelected;
2048
- }
2049
-
2050
- /**
2051
- * Updates the active state and visual highlighting of the menu option.
2052
- * This function toggles the option's active status and applies or removes the active CSS class.
2053
- * @param {boolean} isActive - Whether the option should be marked as active.
2054
- * @deprecated Simply modify the `active` property directly instead.
2055
- */
2056
- updateActive(isActive) {
2057
-
2058
- // Set active state
2059
- this.active = isActive;
2060
- this.updateActiveClasses();
2061
- }
2062
-
2063
- /**
2064
- * Updates the CSS class for the menu option based on its active state.
2065
- * This function adds or removes the 'active' class to visually indicate the option's active status.
2066
- * @private
2067
- */
2068
- updateActiveClasses() {
2069
- // Update class based on active state
2070
- if (this.active) this.classList.add('active');
2071
- else this.classList.remove('active');
2072
1665
  }
2073
1666
 
2074
-
2075
- /**
2076
- * Updates the visual highlighting of text within the menu option based on the current match word.
2077
- * This function highlights matching text segments and manages nested spacers for display formatting.
2078
- * @private
2079
- */
2080
- updateTextHighlight() {
2081
-
2082
- // Regex for matchWord if needed
2083
- let regexWord = null;
2084
-
2085
- if (this.matchWord && this.matchWord.length) {
2086
- const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
2087
- regexWord = new RegExp(escapedWord, 'giu');
2088
- }
2089
-
2090
- // Update text highlighting if matchWord changed
2091
- if (regexWord &&
2092
- this.isActive && !this.hasAttribute('persistent')) {
2093
- const nested = this.querySelectorAll('.nestingSpacer');
2094
-
2095
- const displayValueEl = this.querySelector('[slot="displayValue"]');
2096
- if (displayValueEl) {
2097
- this.removeChild(displayValueEl);
2098
- }
2099
-
2100
- // Create nested spacers
2101
- const nestingSpacerBundle = [...nested].map(() => this.nestingSpacer).join('');
2102
-
2103
- // Update with spacers and matchWord
2104
- this.innerHTML = nestingSpacerBundle +
2105
- this.textContent.replace(
2106
- regexWord,
2107
- (match) => `<strong>${match}</strong>`
2108
- );
2109
- if (displayValueEl) {
2110
- this.append(displayValueEl);
2111
- }
2112
- }
1667
+ attachTo() {
1668
+ // no-op: menu owns state in the distributed architecture
2113
1669
  }
2114
1670
 
2115
1671
  /**
2116
- * Handles click events on the menu option, toggling its selected state.
2117
- * This function dispatches a click event and updates selection if the option is not disabled.
1672
+ * Handles click events on the menu option.
2118
1673
  * @private
2119
1674
  */
2120
1675
  handleClick() {
2121
- if (!this.disabled && !this.menuService?.disabled) {
2122
- this.dispatchClickEvent();
2123
- this.selected = !this.selected;
2124
- }
2125
- }
2126
-
2127
- /**
2128
- * Handles mouse enter events to highlight the menu option.
2129
- * This function updates the menu service to set this option as the currently highlighted item if not disabled.
2130
- * @private
2131
- */
2132
- handleMouseEnter() {
2133
1676
  if (!this.disabled) {
2134
- this.menuService.setHighlightedOption(this);
2135
- }
2136
- }
2137
-
2138
- /**
2139
- * Dispatches custom events defined for this menu option.
2140
- * This function notifies listeners when a custom event is triggered by the option.
2141
- * @private
2142
- */
2143
- handleCustomEvent() {
2144
- if (this.event) {
2145
- dispatchMenuEvent(this, this.event, { option: this });
2146
- dispatchMenuEvent(this, 'auroMenu-customEventFired', { option: this });
1677
+ // Pure event emitter: the parent menu owns selection state and will
1678
+ // update `selected` via setSelected(). Toggling here desyncs the option
1679
+ // UI from auro-menu.optionSelected (e.g. single-select re-click on the
1680
+ // already-selected option would flip the option off while the menu keeps it on).
1681
+ this.dispatchEvent(new CustomEvent('auroMenuOption-click', {
1682
+ bubbles: true,
1683
+ cancelable: false,
1684
+ composed: true,
1685
+ detail: this
1686
+ }));
2147
1687
  }
2148
1688
  }
2149
1689
 
2150
- /**
2151
- * Dispatches a click event for this menu option.
2152
- * This function notifies listeners that the option has been clicked.
2153
- * @private
2154
- */
2155
- dispatchClickEvent() {
2156
- this.dispatchEvent(new CustomEvent('auroMenuOption-click', {
2157
- bubbles: true,
2158
- cancelable: false,
2159
- composed: true,
2160
- detail: this
2161
- }));
2162
- }
2163
-
2164
1690
  /**
2165
1691
  * Generates an HTML element containing an SVG icon based on the provided `svgContent`.
2166
1692
  *
@@ -2200,8 +1726,8 @@ class AuroMenuOption extends AuroElement {
2200
1726
  return html$1`
2201
1727
  <div class="${classes}">
2202
1728
  ${this.selected && !this.noCheckmark
2203
- ? this.generateIconHtml(checkmarkIcon.svg)
2204
- : undefined}
1729
+ ? this.generateIconHtml(checkmarkIcon.svg)
1730
+ : undefined}
2205
1731
  <slot></slot>
2206
1732
  </div>
2207
1733
  `;