@aurodesignsystem/auro-formkit 2.0.0-beta.10 → 2.0.0-beta.11

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 (197) hide show
  1. package/.turbo/cache/0c8124a987c1cc05-meta.json +1 -1
  2. package/.turbo/cache/0cd512cdf86242c7-meta.json +1 -0
  3. package/.turbo/cache/{ff4dbfffc29255ab.tar.zst → 0cd512cdf86242c7.tar.zst} +0 -0
  4. package/.turbo/cache/123c83cd8727dff3-meta.json +1 -0
  5. package/.turbo/cache/123c83cd8727dff3.tar.zst +0 -0
  6. package/.turbo/cache/18129dba20f51b6b-meta.json +1 -1
  7. package/.turbo/cache/253e861af7025ed4-meta.json +1 -0
  8. package/.turbo/cache/253e861af7025ed4.tar.zst +0 -0
  9. package/.turbo/cache/2787020e69f50af2-meta.json +1 -1
  10. package/.turbo/cache/2a5295c8f561ed84-meta.json +1 -0
  11. package/.turbo/cache/2c0d681132c153dd-meta.json +1 -1
  12. package/.turbo/cache/4006a206400d5c7b-meta.json +1 -1
  13. package/.turbo/cache/492dda333b8d15f1-meta.json +1 -1
  14. package/.turbo/cache/4e3619d9dfc86809-meta.json +1 -0
  15. package/.turbo/cache/4e3619d9dfc86809.tar.zst +0 -0
  16. package/.turbo/cache/50993de942ec15a9-meta.json +1 -1
  17. package/.turbo/cache/50cd7dcfc9f820c5-meta.json +1 -0
  18. package/.turbo/cache/50cd7dcfc9f820c5.tar.zst +0 -0
  19. package/.turbo/cache/51eaa58d5c167de8-meta.json +1 -1
  20. package/.turbo/cache/5a0d3e26da304c62-meta.json +1 -0
  21. package/.turbo/cache/5a0d3e26da304c62.tar.zst +0 -0
  22. package/.turbo/cache/5c960af698582835-meta.json +1 -0
  23. package/.turbo/cache/5c960af698582835.tar.zst +0 -0
  24. package/.turbo/cache/5dbbb71dffc3f542-meta.json +1 -0
  25. package/.turbo/cache/5dbbb71dffc3f542.tar.zst +0 -0
  26. package/.turbo/cache/6081837e8943b62e-meta.json +1 -1
  27. package/.turbo/cache/60ad74320c682a2b-meta.json +1 -1
  28. package/.turbo/cache/61e218aba69cff58-meta.json +1 -1
  29. package/.turbo/cache/77da375a012de9d0-meta.json +1 -1
  30. package/.turbo/cache/7964d1656e9e702a-meta.json +1 -0
  31. package/.turbo/cache/7964d1656e9e702a.tar.zst +0 -0
  32. package/.turbo/cache/7bf2b06a479d0b30-meta.json +1 -1
  33. package/.turbo/cache/7c9ca6163e61285c-meta.json +1 -1
  34. package/.turbo/cache/80aca269cd346fb4-meta.json +1 -0
  35. package/.turbo/cache/80aca269cd346fb4.tar.zst +0 -0
  36. package/.turbo/cache/8602fc2bb737a5cf-meta.json +1 -0
  37. package/.turbo/cache/89e0e7a6148e854f-meta.json +1 -0
  38. package/.turbo/cache/89e0e7a6148e854f.tar.zst +0 -0
  39. package/.turbo/cache/8bb856bd31b5b479-meta.json +1 -1
  40. package/.turbo/cache/93c887fb93a10daa-meta.json +1 -0
  41. package/.turbo/cache/93c887fb93a10daa.tar.zst +0 -0
  42. package/.turbo/cache/94dae2a64e9d8356-meta.json +1 -0
  43. package/.turbo/cache/97f6fe83b54acf09-meta.json +1 -0
  44. package/.turbo/cache/{080ca6155e637d5d.tar.zst → 97f6fe83b54acf09.tar.zst} +0 -0
  45. package/.turbo/cache/98317b0d14d94df7-meta.json +1 -0
  46. package/.turbo/cache/98317b0d14d94df7.tar.zst +0 -0
  47. package/.turbo/cache/9ae99e8e7bd83d06-meta.json +1 -1
  48. package/.turbo/cache/9cbcd13b1d031f63-meta.json +1 -0
  49. package/.turbo/cache/{8af27c076dc010c3.tar.zst → 9cbcd13b1d031f63.tar.zst} +0 -0
  50. package/.turbo/cache/afbbd49ed1a558b9-meta.json +1 -0
  51. package/.turbo/cache/b353ce8f6da43dea-meta.json +1 -0
  52. package/.turbo/cache/b5e6dc7fb9ae1a2f-meta.json +1 -1
  53. package/.turbo/cache/b6a202cc85cb61a0-meta.json +1 -1
  54. package/.turbo/cache/b8db059a9b9ccb5d-meta.json +1 -0
  55. package/.turbo/cache/bc24a38aa1b1a102-meta.json +1 -0
  56. package/.turbo/cache/be0b95293ea517cc-meta.json +1 -1
  57. package/.turbo/cache/c3a4f7a3565d6706-meta.json +1 -0
  58. package/.turbo/cache/c3a4f7a3565d6706.tar.zst +0 -0
  59. package/.turbo/cache/c44efc9e4ddd8a0e-meta.json +1 -1
  60. package/.turbo/cache/c6c6411199b68170-meta.json +1 -1
  61. package/.turbo/cache/c97b043e748e3580-meta.json +1 -0
  62. package/.turbo/cache/d5db503b2eaf239c-meta.json +1 -1
  63. package/.turbo/cache/d775555355d6b8fc-meta.json +1 -1
  64. package/.turbo/cache/d7c3007be148d2a1-meta.json +1 -1
  65. package/.turbo/cache/dad3d78b33edd9e4-meta.json +1 -1
  66. package/.turbo/cache/dc597b3ea4f61ec8-meta.json +1 -0
  67. package/.turbo/cache/dc597b3ea4f61ec8.tar.zst +0 -0
  68. package/.turbo/cache/df40b180126e5351-meta.json +1 -0
  69. package/.turbo/cache/df40b180126e5351.tar.zst +0 -0
  70. package/.turbo/cache/e5f217f77c32c93b-meta.json +1 -0
  71. package/.turbo/cache/{c2b51643f886a493.tar.zst → e5f217f77c32c93b.tar.zst} +0 -0
  72. package/.turbo/cache/e62cfee068e3ef36-meta.json +1 -1
  73. package/.turbo/cache/e9e36823f6c98f07-meta.json +1 -1
  74. package/.turbo/cache/ee1a3c1fe389da51-meta.json +1 -0
  75. package/.turbo/cache/f3c7b40f2c6a4094-meta.json +1 -0
  76. package/.turbo/cache/{b22ca87b2f7f9cc2.tar.zst → f3c7b40f2c6a4094.tar.zst} +0 -0
  77. package/.turbo/cache/f5958c3acb889631-meta.json +1 -0
  78. package/.turbo/cache/fb3809ac3f90e3b2-meta.json +1 -0
  79. package/.turbo/cache/{eb1dbe885532c1dc.tar.zst → fb3809ac3f90e3b2.tar.zst} +0 -0
  80. package/.turbo/cache/fd5ddfa43ebd8e5c-meta.json +1 -0
  81. package/.turbo/cache/fd5ddfa43ebd8e5c.tar.zst +0 -0
  82. package/CHANGELOG.md +13 -0
  83. package/components/checkbox/.turbo/turbo-build.log +3 -3
  84. package/components/checkbox/.turbo/turbo-bundler.log +3 -3
  85. package/components/checkbox/README.md +1 -1
  86. package/components/combobox/.turbo/turbo-build.log +3 -3
  87. package/components/combobox/README.md +4 -4
  88. package/components/combobox/demo/api.md +9 -9
  89. package/components/combobox/demo/api.min.js +2508 -642
  90. package/components/combobox/demo/index.min.js +2505 -639
  91. package/components/combobox/dist/auro-combobox.d.ts +9 -4
  92. package/components/combobox/dist/auro-combobox.d.ts.map +1 -1
  93. package/components/combobox/dist/index.js +1863 -312
  94. package/components/combobox/src/auro-combobox.js +70 -26
  95. package/components/counter/.turbo/turbo-build.log +3 -3
  96. package/components/counter/.turbo/turbo-bundler.log +3 -3
  97. package/components/counter/README.md +1 -1
  98. package/components/datepicker/.turbo/turbo-build.log +3 -3
  99. package/components/datepicker/README.md +4 -4
  100. package/components/dropdown/.turbo/turbo-build.log +3 -3
  101. package/components/dropdown/.turbo/turbo-bundler.log +2 -2
  102. package/components/dropdown/README.md +1 -1
  103. package/components/form/.turbo/turbo-build.log +3 -3
  104. package/components/form/.turbo/turbo-bundler.log +3 -3
  105. package/components/form/README.md +1 -1
  106. package/components/input/.turbo/turbo-build.log +3 -3
  107. package/components/input/.turbo/turbo-bundler.log +3 -3
  108. package/components/input/README.md +1 -1
  109. package/components/menu/.turbo/turbo-build.log +4 -2
  110. package/components/menu/.turbo/turbo-bundler.log +3 -3
  111. package/components/menu/README.md +1 -1
  112. package/components/menu/demo/api.md +57 -20
  113. package/components/menu/demo/api.min.js +620 -305
  114. package/components/menu/demo/index.min.js +618 -303
  115. package/components/menu/dist/auro-menu-utils.d.ts +43 -0
  116. package/components/menu/dist/auro-menu-utils.d.ts.map +1 -0
  117. package/components/menu/dist/auro-menu.d.ts +97 -81
  118. package/components/menu/dist/auro-menu.d.ts.map +1 -1
  119. package/components/menu/dist/index.d.ts +1 -0
  120. package/components/menu/dist/index.js +619 -304
  121. package/components/menu/src/auro-menu-utils.js +131 -0
  122. package/components/menu/src/auro-menu.js +493 -303
  123. package/components/menu/src/index.js +7 -0
  124. package/components/radio/.turbo/turbo-build.log +3 -3
  125. package/components/radio/.turbo/turbo-bundler.log +3 -3
  126. package/components/radio/README.md +1 -1
  127. package/components/select/.turbo/turbo-build.log +5 -3
  128. package/components/select/README.md +3 -3
  129. package/components/select/demo/api.md +46 -11
  130. package/components/select/demo/api.min.js +2336 -485
  131. package/components/select/demo/index.min.js +2337 -486
  132. package/components/select/dist/auro-select.d.ts +17 -6
  133. package/components/select/dist/auro-select.d.ts.map +1 -1
  134. package/components/select/dist/index.js +1706 -170
  135. package/components/select/src/auro-select.js +53 -24
  136. package/components/select/src/styles/style-css.js +1 -1
  137. package/components/select/src/styles/style.css +7 -0
  138. package/components/select/src/styles/style.scss +13 -0
  139. package/package.json +1 -1
  140. package/.turbo/cache/026e4d886ba97e63-meta.json +0 -1
  141. package/.turbo/cache/026e4d886ba97e63.tar.zst +0 -0
  142. package/.turbo/cache/080ca6155e637d5d-meta.json +0 -1
  143. package/.turbo/cache/0b115e30ff606299-meta.json +0 -1
  144. package/.turbo/cache/0b115e30ff606299.tar.zst +0 -0
  145. package/.turbo/cache/1c630fb3411e4a41-meta.json +0 -1
  146. package/.turbo/cache/24b19ac5895e5dd6-meta.json +0 -1
  147. package/.turbo/cache/24b19ac5895e5dd6.tar.zst +0 -0
  148. package/.turbo/cache/29b72c73cbccb53d-meta.json +0 -1
  149. package/.turbo/cache/29b72c73cbccb53d.tar.zst +0 -0
  150. package/.turbo/cache/3e647c5863d32e6f-meta.json +0 -1
  151. package/.turbo/cache/3e647c5863d32e6f.tar.zst +0 -0
  152. package/.turbo/cache/43f5206cc4e69b44-meta.json +0 -1
  153. package/.turbo/cache/4a85ec226b585fd5-meta.json +0 -1
  154. package/.turbo/cache/50a29c70b93c57dd-meta.json +0 -1
  155. package/.turbo/cache/50a29c70b93c57dd.tar.zst +0 -0
  156. package/.turbo/cache/56455145cd768755-meta.json +0 -1
  157. package/.turbo/cache/56455145cd768755.tar.zst +0 -0
  158. package/.turbo/cache/5c06332cf9f132da-meta.json +0 -1
  159. package/.turbo/cache/5e613afc6868d0e2-meta.json +0 -1
  160. package/.turbo/cache/5e613afc6868d0e2.tar.zst +0 -0
  161. package/.turbo/cache/639dac15b979bedc-meta.json +0 -1
  162. package/.turbo/cache/664c2e08614fd212-meta.json +0 -1
  163. package/.turbo/cache/6c51b0ebfc086faa-meta.json +0 -1
  164. package/.turbo/cache/6c51b0ebfc086faa.tar.zst +0 -0
  165. package/.turbo/cache/7216d994164825fb-meta.json +0 -1
  166. package/.turbo/cache/7216d994164825fb.tar.zst +0 -0
  167. package/.turbo/cache/83a167e135cb431a-meta.json +0 -1
  168. package/.turbo/cache/83a167e135cb431a.tar.zst +0 -0
  169. package/.turbo/cache/8af27c076dc010c3-meta.json +0 -1
  170. package/.turbo/cache/953c8216249d3509-meta.json +0 -1
  171. package/.turbo/cache/95a5e76ffd8c5110-meta.json +0 -1
  172. package/.turbo/cache/95a5e76ffd8c5110.tar.zst +0 -0
  173. package/.turbo/cache/a8b0fa0a9aa707c5-meta.json +0 -1
  174. package/.turbo/cache/a8b0fa0a9aa707c5.tar.zst +0 -0
  175. package/.turbo/cache/b22ca87b2f7f9cc2-meta.json +0 -1
  176. package/.turbo/cache/b7bbe2e7d44b77f0-meta.json +0 -1
  177. package/.turbo/cache/b7bbe2e7d44b77f0.tar.zst +0 -0
  178. package/.turbo/cache/c2b51643f886a493-meta.json +0 -1
  179. package/.turbo/cache/c74d369a0475b124-meta.json +0 -1
  180. package/.turbo/cache/c7f5a276ddb73cf7-meta.json +0 -1
  181. package/.turbo/cache/c96933d40404e4c8-meta.json +0 -1
  182. package/.turbo/cache/c96933d40404e4c8.tar.zst +0 -0
  183. package/.turbo/cache/eb1dbe885532c1dc-meta.json +0 -1
  184. package/.turbo/cache/f1f6744948f1b18f-meta.json +0 -1
  185. package/.turbo/cache/f1f6744948f1b18f.tar.zst +0 -0
  186. package/.turbo/cache/feefbc25d550c1cd-meta.json +0 -1
  187. package/.turbo/cache/ff4dbfffc29255ab-meta.json +0 -1
  188. /package/.turbo/cache/{639dac15b979bedc.tar.zst → 2a5295c8f561ed84.tar.zst} +0 -0
  189. /package/.turbo/cache/{1c630fb3411e4a41.tar.zst → 8602fc2bb737a5cf.tar.zst} +0 -0
  190. /package/.turbo/cache/{664c2e08614fd212.tar.zst → 94dae2a64e9d8356.tar.zst} +0 -0
  191. /package/.turbo/cache/{c7f5a276ddb73cf7.tar.zst → afbbd49ed1a558b9.tar.zst} +0 -0
  192. /package/.turbo/cache/{43f5206cc4e69b44.tar.zst → b353ce8f6da43dea.tar.zst} +0 -0
  193. /package/.turbo/cache/{c74d369a0475b124.tar.zst → b8db059a9b9ccb5d.tar.zst} +0 -0
  194. /package/.turbo/cache/{4a85ec226b585fd5.tar.zst → bc24a38aa1b1a102.tar.zst} +0 -0
  195. /package/.turbo/cache/{5c06332cf9f132da.tar.zst → c97b043e748e3580.tar.zst} +0 -0
  196. /package/.turbo/cache/{953c8216249d3509.tar.zst → ee1a3c1fe389da51.tar.zst} +0 -0
  197. /package/.turbo/cache/{feefbc25d550c1cd.tar.zst → f5958c3acb889631.tar.zst} +0 -0
@@ -1,10 +1,10 @@
1
1
  function auroMenuResetExample() {
2
2
  const resetExampleBtnElem = document.querySelector('#resetExampleBtn');
3
3
  const resetExampleElem = document.querySelector('#resetExample');
4
-
4
+
5
5
  if (resetExampleElem && resetExampleBtnElem) {
6
6
  resetExampleBtnElem.addEventListener('click', () => {
7
- resetExampleElem.value = undefined;
7
+ resetExampleElem.reset();
8
8
  });
9
9
  }
10
10
  }
@@ -143,23 +143,158 @@ class AuroLibraryRuntimeUtils {
143
143
  // Copyright (c) 2021 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
144
144
  // See LICENSE in the project root for license information.
145
145
 
146
+ // ---------------------------------------------------------------------
147
+
148
+ /**
149
+ * Converts value to an array.
150
+ * If the value is a JSON string representing an array, it will be parsed.
151
+ * If the value is already an array, it is returned.
152
+ * If the value is undefined, it returns undefined.
153
+ * @private
154
+ * @param {any} value - The value to be converted. Can be a string, array, or undefined.
155
+ * @returns {Array|undefined} - The converted array or undefined.
156
+ * @throws {Error} - Throws an error if the value is not an array, undefined,
157
+ * or if the value cannot be parsed into an array from a JSON string.
158
+ */
159
+ function arrayConverter(value) {
160
+ // Allow undefined
161
+ if (value === undefined) {
162
+ return undefined;
163
+ }
164
+
165
+ // Return the value if it is already an array
166
+ if (Array.isArray(value)) {
167
+ return value;
168
+ }
169
+
170
+ try {
171
+ // If value is a JSON string, parse it
172
+ const parsed = typeof value === 'string' ? JSON.parse(value) : value;
173
+
174
+ // Check if the parsed value is an array
175
+ if (Array.isArray(parsed)) {
176
+ return parsed;
177
+ }
178
+ } catch (error) {
179
+ // If JSON parsing fails, continue to throw an error below
180
+ /* eslint-disable no-console */
181
+ console.error('JSON parsing failed:', error);
182
+ }
183
+
184
+ // Throw error if the input is not an array or undefined
185
+ throw new Error('Invalid value: Input must be an array or undefined');
186
+ }
187
+
188
+ /**
189
+ * Compare two arrays for equality.
190
+ * @private
191
+ * @param {Array} arr1 - First array to compare.
192
+ * @param {Array} arr2 - Second array to compare.
193
+ * @returns {boolean} True if arrays are equal.
194
+ */
195
+ function arraysAreEqual(arr1, arr2) {
196
+ // If both arrays undefined, they are equal (true)
197
+ if (arr1 === undefined || arr2 === undefined) {
198
+ return arr1 === arr2;
199
+ }
200
+
201
+ // If arrays have different lengths, they are not equal
202
+ if (arr1.length !== arr2.length) {
203
+ return false;
204
+ }
205
+
206
+ // If every item at each index is the same, return true
207
+ for (let index = 0; index < arr1.length; index += 1) {
208
+ if (arr1[index] !== arr2[index]) {
209
+ return false;
210
+ }
211
+ }
212
+ return true;
213
+ }
214
+
215
+ /**
216
+ * Compares array for changes.
217
+ * @private
218
+ * @param {Array|any} newVal - New value to compare.
219
+ * @param {Array|any} oldVal - Old value to compare.
220
+ * @returns {boolean} True if arrays have changed.
221
+ */
222
+ function arrayOrUndefinedHasChanged(newVal, oldVal) {
223
+ try {
224
+ // Check if values are undefined or arrays
225
+ const isArrayOrUndefined = (val) => val === undefined || Array.isArray(val);
226
+
227
+ // If non-array or non-undefined, throw error
228
+ if (!isArrayOrUndefined(newVal) || !isArrayOrUndefined(oldVal)) {
229
+ const invalidValue = isArrayOrUndefined(newVal) ? oldVal : newVal;
230
+ throw new Error(`Value must be an array or undefined, received ${typeof invalidValue}`);
231
+ }
232
+
233
+ // Return true if arrays have changed, false if they are the same
234
+ return !arraysAreEqual(newVal, oldVal);
235
+ } catch (error) {
236
+ /* eslint-disable no-console */
237
+ console.error(error);
238
+ // If validation fails, it has changed
239
+ return true;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Validates if an option can be interacted with.
245
+ * @private
246
+ * @param {HTMLElement} option - The option to check.
247
+ * @returns {boolean} True if option is interactive.
248
+ */
249
+ function isOptionInteractive(option) {
250
+ return !option.hasAttribute('hidden') &&
251
+ !option.hasAttribute('disabled') &&
252
+ !option.hasAttribute('static');
253
+ }
254
+
255
+ /**
256
+ * Helper method to dispatch custom events.
257
+ * @param {HTMLElement} element - Element to dispatch event from.
258
+ * @param {string} eventName - Name of the event to dispatch.
259
+ * @param {Object} [detail] - Optional detail object to include with the event.
260
+ */
261
+ function dispatchMenuEvent(element, eventName, detail = null) {
262
+ const eventConfig = {
263
+ bubbles: true,
264
+ cancelable: false,
265
+ composed: true
266
+ };
267
+
268
+ if (detail !== null) {
269
+ eventConfig.detail = detail;
270
+ }
271
+
272
+ element.dispatchEvent(new CustomEvent(eventName, eventConfig));
273
+ }
274
+
275
+ // Copyright (c) 2021 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
276
+ // See LICENSE in the project root for license information.
277
+
278
+
146
279
 
147
280
  // See https://git.io/JJ6SJ for "How to document your components using JSDoc"
148
281
  /**
149
282
  * The auro-menu element provides users a way to select from a list of options.
150
- * @attr {Object} optionSelected - Specifies the current selected menuOption.
151
- * @attr {String} matchWord - Specifies a string used to highlight matched string parts in options.
152
- * @attr {Boolean} disabled - When true, the entire menu and all options are disabled;
153
- * @attr {Boolean} noCheckmark - When true, selected option will not show the checkmark.
154
- * @attr {Boolean} loading - When true, displays a loading state using the loadingIcon and loadingText slots if provided.
155
- * @attr {String} value - Value selected for the menu.
156
- * @prop {Boolean} hasLoadingPlaceholder - Indicates whether the menu has a loadingIcon or loadingText to render when in a loading state.
157
- * @event auroMenu-selectedOption - Notifies that a new menuoption selection has been made.
158
- * @event auroMenu-activatedOption - Notifies that a menuoption has been made `active`.
159
- * @event auroMenu-selectValueFailure - Notifies that a an attempt to select a menuoption by matching a value has failed.
160
- * @event auroMenu-selectValueReset - Notifies that the component value has been reset.
161
- * @event auroMenu-customEventFired - Notifies that a custom event has been fired.
162
- * @event auroMenu-loadingChange - Notifies when the loading attribute is changed.
283
+ * @attr {Array<HTMLElement>|undefined} optionselected - An array of currently selected menu options. In single-select mode, the array will contain only one HTMLElement. `undefined` when no options are selected.
284
+ * @attr {object} optionactive - Specifies the current active menuOption.
285
+ * @attr {string} matchword - Specifies a string used to highlight matched string parts in options.
286
+ * @attr {boolean} disabled - When true, the entire menu and all options are disabled;
287
+ * @attr {boolean} nocheckmark - When true, selected option will not show the checkmark.
288
+ * @attr {boolean} loading - When true, displays a loading state using the loadingIcon and loadingText slots if provided.
289
+ * @attr {boolean} multiselect - When true, the selected option can be multiple options.
290
+ * @attr {Array<string>|undefined} value - Value selected for the menu. `undefined` when no selection has been made, otherwise an array of strings. In single-select mode, the array will contain only one value.
291
+ * @prop {boolean} hasLoadingPlaceholder - Indicates whether the menu has a loadingIcon or loadingText to render when in a loading state.
292
+ * @event {CustomEvent<Element>} auroMenu-activatedOption - Notifies that a menuoption has been made `active`.
293
+ * @event {CustomEvent<any>} auroMenu-customEventFired - Notifies that a custom event has been fired.
294
+ * @event {CustomEvent<{ loading: boolean; hasLoadingPlaceholder: boolean; }>} auroMenu-loadingChange - Notifies when the loading attribute is changed.
295
+ * @event {CustomEvent<any>} auroMenu-selectValueFailure - Notifies that an attempt to select a menuoption by matching a value has failed.
296
+ * @event {CustomEvent<any>} auroMenu-selectValueReset - Notifies that the component value has been reset.
297
+ * @event {CustomEvent<any>} auroMenu-selectedOption - Notifies that a new menuoption selection has been made.
163
298
  * @slot loadingText - Text to show while loading attribute is set
164
299
  * @slot loadingIcon - Icon to show while loading attribute is set
165
300
  * @slot - Slot for insertion of menu options.
@@ -170,52 +305,104 @@ class AuroLibraryRuntimeUtils {
170
305
  class AuroMenu extends r {
171
306
  constructor() {
172
307
  super();
308
+
309
+ // State properties (reactive)
310
+
311
+ // Value of the selected options
173
312
  this.value = undefined;
313
+ // Currently selected option
174
314
  this.optionSelected = undefined;
315
+ // String used for highlighting/filtering
175
316
  this.matchWord = undefined;
317
+ // Hide the checkmark icon on selected options
176
318
  this.noCheckmark = false;
319
+ // Currently active option
177
320
  this.optionActive = undefined;
321
+ // Loading state
178
322
  this.loading = false;
323
+ // Multi-select mode
324
+ this.multiSelect = false;
325
+
326
+ // Event Bindings
179
327
 
180
328
  /**
181
329
  * @private
182
330
  */
183
- this.rootMenu = true;
331
+ this.handleKeyDown = this.handleKeyDown.bind(this);
184
332
 
185
333
  /**
186
334
  * @private
187
335
  */
188
- this.runtimeUtils = new AuroLibraryRuntimeUtils();
336
+ this.handleMouseSelect = this.handleMouseSelect.bind(this);
189
337
 
190
338
  /**
191
339
  * @private
192
340
  */
193
- this.nestingSpacer = '<span class="nestingSpacer"></span>';
341
+ this.handleOptionHover = this.handleOptionHover.bind(this);
194
342
 
195
343
  /**
196
344
  * @private
197
345
  */
198
- this.loadingSlots = null;
346
+ this.handleSlotChange = this.handleSlotChange.bind(this);
347
+
348
+ // Instance properties (non-reactive)
349
+
350
+ /**
351
+ * @private
352
+ */
353
+ Object.assign(this, {
354
+ // Root-level menu (true) or a nested submenu (false)
355
+ rootMenu: true,
356
+ // Currently focused/active menu item index
357
+ index: -1,
358
+ // Nested menu spacer
359
+ nestingSpacer: '<span class="nestingSpacer"></span>',
360
+ // Loading indicator for slot elements
361
+ loadingSlots: null,
362
+ // Store for menu items
363
+ items: [],
364
+ });
199
365
  }
200
366
 
201
367
  static get properties() {
202
368
  return {
203
- noCheckmark: {
369
+ noCheckmark: {
204
370
  type: Boolean,
205
- reflect: true
371
+ reflect: true,
372
+ attribute: 'nocheckmark'
206
373
  },
207
- disabled: {
374
+ disabled: {
208
375
  type: Boolean,
209
376
  reflect: true
210
377
  },
211
- loading: {
378
+ loading: {
212
379
  type: Boolean,
213
380
  reflect: true
214
381
  },
215
- optionSelected: { type: Object },
216
- optionActive: { type: Object },
217
- matchWord: { type: String },
218
- value: { type: String }
382
+ optionSelected: {
383
+ // Allow HTMLElement[] arrays and undefined
384
+ converter: arrayConverter,
385
+ hasChanged: arrayOrUndefinedHasChanged
386
+ },
387
+ optionActive: {
388
+ type: Object,
389
+ attribute: 'optionactive'
390
+ },
391
+ matchWord: {
392
+ type: String,
393
+ attribute: 'matchword'
394
+ },
395
+ multiSelect: {
396
+ type: Boolean,
397
+ reflect: true,
398
+ attribute: 'multiselect'
399
+ },
400
+ value: {
401
+ // Allow string[] arrays and undefined
402
+ type: Object,
403
+ converter: arrayConverter,
404
+ hasChanged: arrayOrUndefinedHasChanged
405
+ }
219
406
  };
220
407
  }
221
408
 
@@ -239,198 +426,329 @@ class AuroMenu extends r {
239
426
  AuroLibraryRuntimeUtils.prototype.registerComponent(name, AuroMenu);
240
427
  }
241
428
 
242
- /**
243
- * Passes the noCheckmark attribute to all nested auro-menuoptions.
244
- * @private
245
- * @returns {void}
246
- */
247
- handleNoCheckmarkAttr() {
248
- if (this.noCheckmark) {
249
- const menus = this.querySelectorAll('auro-menu, [auro-menu]');
429
+ // Lifecycle Methods
250
430
 
251
- menus.forEach((menu) => {
252
- menu.setAttribute('noCheckmark', '');
253
- });
431
+ connectedCallback() {
432
+ super.connectedCallback();
254
433
 
255
- const options = this.querySelectorAll('auro-menuoption, [auro-menuoption]');
434
+ this.addEventListener('keydown', this.handleKeyDown);
435
+ this.addEventListener('mousedown', this.handleMouseSelect);
436
+ this.addEventListener('auroMenuOption-mouseover', this.handleOptionHover);
437
+ this.addEventListener('slotchange', this.handleSlotChange);
438
+ }
256
439
 
257
- options.forEach((option) => {
258
- option.setAttribute('noCheckmark', '');
259
- });
260
- }
440
+ disconnectedCallback() {
441
+ this.removeEventListener('keydown', this.handleKeyDown);
442
+ this.removeEventListener('mousedown', this.handleMouseSelect);
443
+ this.removeEventListener('auroMenuOption-mouseover', this.handleOptionHover);
444
+ this.removeEventListener('slotchange', this.handleSlotChange);
445
+
446
+ super.disconnectedCallback();
261
447
  }
262
448
 
263
449
  firstUpdated() {
264
- // Add the tag name as an attribute if it is different than the component name
265
- this.runtimeUtils.handleComponentTagRename(this, 'auro-menu');
266
-
267
- this.addEventListener('keydown', this.handleKeyDown);
450
+ AuroLibraryRuntimeUtils.prototype.handleComponentTagRename(this, 'auro-menu');
268
451
 
269
452
  this.loadingSlots = this.querySelectorAll("[slot='loadingText'], [slot='loadingIcon']");
453
+ this.initializeMenu();
270
454
  }
271
455
 
272
456
  updated(changedProperties) {
273
- if (changedProperties.has('matchWord')) {
274
- this.markOptions();
457
+ if (changedProperties.has('value')) {
458
+ // Handle null/undefined case
459
+ if (this.value === undefined || this.value === null) {
460
+ this.optionSelected = undefined;
461
+ // Reset index tracking
462
+ this.index = -1;
463
+ } else {
464
+ // Convert single values to arrays
465
+ const valueArray = Array.isArray(this.value) ? this.value : [this.value];
466
+
467
+ // Find all matching options
468
+ const matchingOptions = this.items.filter((item) => valueArray.includes(item.value));
469
+
470
+ if (matchingOptions.length > 0) {
471
+ if (this.multiSelect) {
472
+ // For multiselect, keep all matching options
473
+ this.optionSelected = matchingOptions;
474
+ } else {
475
+ // For single select, only use the first match
476
+ this.optionSelected = [matchingOptions[0]];
477
+ this.index = this.items.indexOf(matchingOptions[0]);
478
+ }
479
+ } else {
480
+ // No matches found - trigger failure event
481
+ dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
482
+ this.optionSelected = undefined;
483
+ this.index = -1;
484
+ }
485
+ }
486
+
487
+ // Update UI state
488
+ this.updateItemsState(new Map([
489
+ [
490
+ 'optionSelected',
491
+ true
492
+ ]
493
+ ]));
494
+
495
+ // Notify of changes
496
+ if (this.optionSelected !== undefined) {
497
+ this.notifySelectionChange();
498
+ }
275
499
  }
276
500
 
277
- if (changedProperties.has('value')) {
278
- this.selectByValue(this.value);
501
+ // Process all other UI updates
502
+ this.updateItemsState(changedProperties);
503
+ }
504
+
505
+ /**
506
+ * Updates the UI state and appearance of menu items based on changed properties.
507
+ * @private
508
+ * @param {Map<string, boolean>} changedProperties - LitElement's changed properties map.
509
+ */
510
+ updateItemsState(changedProperties) {
511
+ if (!this.items) {
512
+ return;
279
513
  }
280
514
 
281
- if (changedProperties.has('disabled')) {
282
- const options = Array.from(this.querySelectorAll('auro-menuoption, [auro-menuoption]'));
515
+ // Handle noCheckmark propagation to all menus and options
516
+ if (changedProperties.has('noCheckmark') && this.noCheckmark) {
517
+ // Update both menus and options
518
+ this.querySelectorAll('auro-menu, [auro-menu], auro-menuoption, [auro-menuoption]').forEach((element) => element.setAttribute('noCheckmark', ''));
519
+ }
283
520
 
284
- for (const element of options) {
285
- element.disabled = this.disabled;
286
- }
521
+ // Regex for matchWord if needed
522
+ let regexWord = null;
523
+
524
+ if (changedProperties.has('matchWord') && this.matchWord && this.matchWord.length) {
525
+ const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
526
+ regexWord = new RegExp(escapedWord, 'giu');
287
527
  }
288
528
 
289
- if (changedProperties.has('loading')) {
290
- const event = new CustomEvent("auroMenu-loadingChange", {
291
- detail: {
292
- loading: this.loading,
293
- hasLoadingPlaceholder:
294
- this.hasLoadingPlaceholder
529
+ // Handle direct item updates
530
+ this.items.forEach((option) => {
531
+ // Update selection if option or value changed
532
+ if (changedProperties.has('optionSelected') || changedProperties.has('value')) {
533
+ const isSelected = this.isOptionSelected(option);
534
+ option.classList.toggle('active', isSelected);
535
+ option.setAttribute('aria-selected', isSelected ? 'true' : 'false');
536
+
537
+ // Add/remove selected attribute based on state
538
+ if (isSelected) {
539
+ option.setAttribute('selected', '');
540
+ } else {
541
+ option.removeAttribute('selected');
295
542
  }
543
+ }
544
+
545
+ // Update text highlighting if matchWord changed
546
+ if (changedProperties.has('matchWord') && regexWord &&
547
+ isOptionInteractive(option) && !option.hasAttribute('persistent')) {
548
+ const nested = option.querySelectorAll('.nestingSpacer');
549
+ // Create nested spacers
550
+ const nestingSpacerBundle = [...nested].map(() => this.nestingSpacer).join('');
551
+
552
+ // Update with spacers and matchWord
553
+ option.innerHTML = nestingSpacerBundle +
554
+ option.textContent.replace(
555
+ regexWord,
556
+ (match) => `<strong>${match}</strong>`
557
+ );
558
+ }
559
+
560
+ // Update disabled state
561
+ if (changedProperties.has('disabled')) {
562
+ option.disabled = this.disabled;
563
+ }
564
+ });
565
+
566
+ // Handle loading state changes
567
+ if (changedProperties.has('loading')) {
568
+ this.setAttribute("aria-busy", this.loading);
569
+ dispatchMenuEvent(this, "auroMenu-loadingChange", {
570
+ loading: this.loading,
571
+ hasLoadingPlaceholder: this.hasLoadingPlaceholder
296
572
  });
297
- this.setAttribute("aria-busy", this.hasAttribute("loading"));
298
- this.dispatchEvent(event);
299
573
  }
300
574
  }
301
575
 
576
+ // Init Methods
577
+
302
578
  /**
579
+ * Initializes the menu's state and structure.
303
580
  * @private
304
- * @param {Object} option - The menuoption to check for interactive state.
305
- * @returns {Boolean} Returns true if the option is interactive.
306
581
  */
307
- optionInteractive(option) {
308
- return !option.hasAttribute('hidden') && !option.hasAttribute('disabled') && !option.hasAttribute('static');
582
+ initializeMenu() {
583
+ this.initItems();
584
+ if (this.rootMenu) {
585
+ this.setAttribute('role', 'listbox');
586
+ this.setAttribute('root', '');
587
+ this.handleNestedMenus(this);
588
+ }
309
589
  }
310
590
 
311
591
  /**
592
+ * Initializes menu items and their attributes.
312
593
  * @private
313
- * @returns {void} When called will update the DOM with visible suggest text matches.
314
594
  */
315
- markOptions() {
316
- if (this.items && this.items.length > 0 && (this.matchWord && this.matchWord.length > 0)) {
317
-
318
- // Escape special regex characters
319
- const escapedWord = this.matchWord.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
320
-
321
- // Global, case-insensitive, unicode matching regex pattern
322
- const regexWord = new RegExp(escapedWord, 'giu');
323
-
324
- this.items.forEach((item) => {
325
- if (this.optionInteractive(item) && !item.hasAttribute('persistent')) {
326
- const nested = item.querySelectorAll('.nestingSpacer');
327
- const nestingSpacerBundle = [...nested].map(() => this.nestingSpacer).join('');
328
-
329
- item.innerHTML = nestingSpacerBundle + item.textContent.replace(regexWord, (match) => `<strong>${match}</strong>`);
330
- }
331
- });
595
+ initItems() {
596
+ this.items = Array.from(this.querySelectorAll('auro-menuoption, [auro-menuoption]'));
597
+ if (this.noCheckmark) {
598
+ this.updateItemsState(new Map([
599
+ [
600
+ 'noCheckmark',
601
+ true
602
+ ]
603
+ ]));
332
604
  }
333
605
  }
334
606
 
607
+ // Logic Methods
608
+
335
609
  /**
336
- * Reset the menu and all options.
610
+ * Updates menu state when an option is selected.
611
+ * @private
612
+ * @param {HTMLElement} option - The option element to select.
337
613
  */
338
- resetOptionsStates() {
339
- this.optionSelected = undefined;
340
- if (this.items) {
341
- this.items.forEach((item) => {
342
- item.classList.remove('active');
343
- item.removeAttribute('selected');
344
- });
614
+ handleSelectState(option) {
615
+ if (this.multiSelect) {
616
+ const currentValue = this.value || [];
617
+ const currentSelected = this.optionSelected || [];
618
+
619
+ if (!currentValue.includes(option.value)) {
620
+ this.value = [
621
+ ...currentValue,
622
+ option.value
623
+ ];
624
+ }
625
+ if (!currentSelected.includes(option)) {
626
+ this.optionSelected = [
627
+ ...currentSelected,
628
+ option
629
+ ];
630
+ }
631
+ } else {
632
+ // Single select - use arrays with single values
633
+ this.value = [option.value];
634
+ this.optionSelected = [option];
345
635
  }
636
+
637
+ this.index = this.items.indexOf(option);
346
638
  }
347
639
 
348
640
  /**
349
- * Set the attributes on the selected menuoption, the menu value and stored option.
350
- * @param {Object} option - The menuoption to be selected.
641
+ * Deselects a menu option and updates related state.
351
642
  * @private
643
+ * @param {HTMLElement} option - The menuoption to be deselected.
352
644
  */
353
- handleLocalSelectState(option) {
354
- option.setAttribute('selected', '');
355
- option.classList.add('active');
356
- option.ariaSelected = true;
645
+ handleDeselectState(option) {
646
+ if (this.multiSelect && Array.isArray(this.value)) {
647
+ // Remove this option from array
648
+ this.value = this.value.filter((val) => val !== option.value);
649
+
650
+ // If array is empty after removal, set back to undefined
651
+ if (this.value.length === 0) {
652
+ this.value = undefined;
653
+ }
357
654
 
358
- this.value = option.value;
359
- this.optionSelected = option;
655
+ this.optionSelected = this.optionSelected.filter((val) => val !== option);
656
+ if (this.optionSelected.length === 0) {
657
+ this.optionSelected = undefined;
658
+ }
659
+ } else {
660
+ // For single-select: Back to undefined when deselected
661
+ this.value = undefined;
662
+ this.optionSelected = undefined;
663
+ }
664
+
665
+ // Update the index tracking
360
666
  this.index = this.items.indexOf(option);
667
+
668
+ // Update UI to reflect changes
669
+ this.updateItemsState(new Map([
670
+ [
671
+ 'optionSelected',
672
+ true
673
+ ]
674
+ ]));
675
+
676
+ // Notify of selection change
677
+ this.notifySelectionChange();
361
678
  }
362
679
 
363
680
  /**
364
- * Notify selection change.
681
+ * Resets all options to their default state.
365
682
  * @private
366
- * @return {void}
367
683
  */
368
- notifySelectionChange() {
369
- this.dispatchEvent(new CustomEvent('auroMenu-selectedOption', {
370
- bubbles: true,
371
- cancelable: false,
372
- composed: true,
373
- }));
684
+ clearSelection() {
685
+ this.optionSelected = undefined;
686
+ this.value = undefined;
374
687
  }
375
688
 
376
689
  /**
377
- * Process actions for making making a menuoption selection.
690
+ * Resets the menu to its initial state.
691
+ * This is the only way to return value to undefined.
692
+ * @public
378
693
  */
379
- makeSelection() {
380
- if (!this.items) {
381
- this.initItems();
382
- }
694
+ reset() {
695
+ // Reset to undefined - initial state
696
+ this.value = undefined;
697
+ this.optionSelected = undefined;
698
+ this.index = -1;
699
+
700
+ // Reset UI state
701
+ this.updateItemsState(new Map([
702
+ [
703
+ 'optionSelected',
704
+ true
705
+ ]
706
+ ]));
707
+
708
+ // Dispatch reset event
709
+ dispatchMenuEvent(this, 'auroMenu-selectValueReset');
710
+ }
383
711
 
384
- if (this.items[this.index] && !this.items[this.index].hasAttribute('disabled')) {
385
- this.resetOptionsStates();
386
-
387
- if (this.index >= 0) {
388
- const option = this.items[this.index];
389
-
390
- // only handle options that are not disabled, hidden or static
391
- if (option && this.optionInteractive(option)) {
392
- // fire custom event if defined otherwise make selection
393
- if (option.hasAttribute('event')) {
394
- this.dispatchEvent(new CustomEvent(option.getAttribute('event'), {
395
- bubbles: true,
396
- cancelable: false,
397
- composed: true,
398
- }));
399
-
400
- this.dispatchEvent(new CustomEvent('auroMenu-customEventFired', {
401
- bubbles: true,
402
- cancelable: false,
403
- composed: true,
404
- }));
405
- } else {
406
- this.handleLocalSelectState(option);
407
- }
408
- }
712
+ /**
713
+ * Handles nested menu structure.
714
+ * @private
715
+ * @param {HTMLElement} menu - Root menu element.
716
+ */
717
+ handleNestedMenus(menu) {
718
+ const nestedMenus = menu.querySelectorAll('auro-menu, [auro-menu]');
719
+
720
+ nestedMenus.forEach((nestedMenu) => {
721
+ // role="listbox" only allows "role=group" for children.
722
+ nestedMenu.setAttribute('role', 'group');
723
+ if (!nestedMenu.hasAttribute('aria-label')) {
724
+ nestedMenu.setAttribute('aria-label', 'submenu');
409
725
  }
410
- }
411
726
 
412
- this.notifySelectionChange();
727
+ const options = nestedMenu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption]');
728
+ options.forEach((option) => {
729
+ option.innerHTML = this.nestingSpacer + option.innerHTML;
730
+ });
731
+
732
+ this.handleNestedMenus(nestedMenu);
733
+ });
413
734
  }
414
735
 
736
+ // Event Handlers
737
+
415
738
  /**
416
- * Manage ArrowDown, ArrowUp and Enter keyboard events.
739
+ * Handles keyboard navigation.
417
740
  * @private
418
- * @param {Object} event - Event object from the browser.
741
+ * @param {KeyboardEvent} event - Event object from the browser.
419
742
  */
420
743
  handleKeyDown(event) {
421
744
  event.preventDefault();
422
-
423
- // With ArrowDown/ArrowUp events, pass new value to selectNextItem()
424
- // With Enter event, set value and apply attrs
425
745
  switch (event.key) {
426
746
  case "ArrowDown":
427
- this.selectNextItem('down');
747
+ this.navigateOptions('down');
428
748
  break;
429
-
430
749
  case "ArrowUp":
431
- this.selectNextItem('up');
750
+ this.navigateOptions('up');
432
751
  break;
433
-
434
752
  case "Enter":
435
753
  this.makeSelection();
436
754
  break;
@@ -438,222 +756,218 @@ class AuroMenu extends r {
438
756
  }
439
757
 
440
758
  /**
441
- * Initializes all menu options in the DOM. This must be re-run every time the options are changed.
759
+ * Makes a selection based on the current index or clicked option.
442
760
  * @private
443
761
  */
444
- initItems() {
445
- this.items = Array.from(this.querySelectorAll('auro-menuoption, [auro-menuoption]'));
446
- this.handleNoCheckmarkAttr();
762
+ makeSelection() {
763
+ if (!this.items) {
764
+ this.initItems();
765
+ }
766
+
767
+ // Get currently selected menu option based on index
768
+ const option = this.items[this.index];
769
+
770
+ // Return early if option is not interactive
771
+ if (!option || !isOptionInteractive(option)) {
772
+ return;
773
+ }
774
+
775
+ // Handle custom events first
776
+ if (option.hasAttribute('event')) {
777
+ this.handleCustomEvent(option);
778
+ return;
779
+ }
780
+
781
+ if (this.multiSelect) {
782
+ // In multiselect, toggle individual selections
783
+ this.toggleOption(option);
784
+ // In single select, only handle selection of new options
785
+ } else if (!this.isOptionSelected(option)) {
786
+ this.clearSelection();
787
+ this.handleSelectState(option);
788
+ }
789
+
790
+ this.notifySelectionChange();
447
791
  }
448
792
 
449
793
  /**
450
- * Sets the index value of the selected item or first non-disabled menuoption.
794
+ * Toggle the selection state of the menuoption.
451
795
  * @private
796
+ * @param {HTMLElement} option - The menuoption to toggle.
452
797
  */
453
- getSelectedIndex() {
454
- // find the first `selected` and not `disabled`, `hidden` or `static` option
455
- const index = this.items.findIndex((option) => option.hasAttribute('selected') && this.optionInteractive(option));
798
+ toggleOption(option) {
799
+ const isCurrentlySelected = this.isOptionSelected(option);
456
800
 
457
- if (index >= 0) {
458
- this.index = index;
459
- this.makeSelection();
801
+ if (isCurrentlySelected) {
802
+ this.handleDeselectState(option);
803
+ } else if (option.value === undefined || option.value === '') {
804
+ dispatchMenuEvent(this, 'auroMenu-selectValueFailure');
805
+ } else {
806
+ this.handleSelectState(option);
460
807
  }
461
808
  }
462
809
 
463
810
  /**
464
- * Using value of current this.index evaluates index
465
- * of next :focus to set based on array of this.items ignoring items
466
- * with disabled attr.
467
- *
468
- * The event.target is not used as the function needs to know where to go,
469
- * versus knowing where it is.
470
- * @param {String} moveDirection - Up or Down based on keyboard event.
811
+ * Handles option selection via mouse.
812
+ * @private
813
+ * @param {MouseEvent} event - Event object from the browser.
471
814
  */
472
- selectNextItem(moveDirection) {
473
- if (this.index >= 0) {
474
- this.items[this.index].classList.remove('active');
475
-
476
- // calculate which is the selection we should focus next
477
- let increment = 0;
478
-
479
- if (moveDirection === 'down') {
480
- increment = 1;
481
- } else if (moveDirection === 'up') {
482
- increment = -1;
483
- }
484
-
485
- this.index += increment;
486
-
487
- // keep looping inside the array of options
488
- if (this.index > this.items.length - 1) {
489
- this.index = 0;
490
- } else if (this.index < 0) {
491
- this.index = this.items.length - 1;
492
- }
493
-
494
- // check if new index is disabled, static or hidden, if so, execute again
495
- if (!this.optionInteractive(this.items[this.index])) {
496
- this.selectNextItem(moveDirection);
497
- } else {
498
- // apply focus to new index
499
- this.updateActiveOption(this.index);
500
- }
501
- } else {
502
- this.index = 0;
815
+ handleMouseSelect(event) {
816
+ if (event.target === this) {
817
+ return;
818
+ }
503
819
 
504
- if (this.items[this.index].hasAttribute('hidden') || this.items[this.index].hasAttribute('disabled')) {
505
- this.selectNextItem(moveDirection);
506
- } else {
507
- this.updateActiveOption(this.index);
508
- }
820
+ const option = event.target.closest('auro-menuoption, [auro-menuoption]');
821
+ if (option) {
822
+ this.index = this.items.indexOf(option);
823
+ this.makeSelection();
509
824
  }
510
825
  }
511
826
 
512
827
  /**
513
- * Used for applying indentation to each level of nested menu.
828
+ * Handles option hover events.
514
829
  * @private
515
- * @param {String} menu - Root level menu object.
830
+ * @param {CustomEvent} event - Event object from the browser.
516
831
  */
517
- handleNestedMenus(menu) {
518
- const nestedMenus = menu.querySelectorAll('auro-menu, [auro-menu');
832
+ handleOptionHover(event) {
833
+ const option = event.target;
834
+ this.index = this.items.indexOf(option);
835
+ this.updateActiveOption(this.index);
836
+ }
519
837
 
520
- if (nestedMenus.length === 0) {
521
- return;
838
+ /**
839
+ * Handles slot change events.
840
+ * @private
841
+ */
842
+ handleSlotChange() {
843
+ if (this.parentElement && this.parentElement.closest('auro-menu, [auro-menu]')) {
844
+ this.rootMenu = false;
522
845
  }
523
846
 
524
- nestedMenus.forEach((nestedMenu) => {
525
- const options = nestedMenu.querySelectorAll(':scope > auro-menuoption, :scope > [auro-menuoption');
526
-
527
- options.forEach((option) => {
528
- option.innerHTML = this.nestingSpacer + option.innerHTML;
529
- });
530
-
531
- this.handleNestedMenus(nestedMenu);
532
- });
847
+ if (this.rootMenu) {
848
+ this.initializeMenu();
849
+ } else if (this.noCheckmark) {
850
+ this.updateItemsState(new Map([
851
+ [
852
+ 'noCheckmark',
853
+ true
854
+ ]
855
+ ]));
856
+ }
533
857
  }
534
858
 
535
859
  /**
536
- * Method to apply `selected` attribute to `menuoption` via `value`.
860
+ * Navigates through options using keyboard.
537
861
  * @private
538
- * @param {String} value - Must match a unique `menuoption` value.
862
+ * @param {string} direction - 'up' or 'down'.
539
863
  */
540
- selectByValue(value) {
541
- let valueMatch = false;
542
- if (!this.items) {
543
- this.initItems();
864
+ navigateOptions(direction) {
865
+ // Return early if no items exist
866
+ if (!this.items || !this.items.length) {
867
+ return;
544
868
  }
545
869
 
546
- this.index = undefined;
870
+ let newIndex = this.index;
871
+ const increment = direction === 'down' ? 1 : -1;
872
+ const maxIterations = this.items.length;
873
+ let iterations = 0;
874
+ let foundInteractiveOption = false;
547
875
 
548
- if (this.value && this.value.length > 0) {
549
- for (let index = 0; index < this.items.length; index += 1) {
550
- if (this.items[index].value === value) {
551
- valueMatch = true;
552
- this.index = index;
553
- }
554
- }
876
+ do {
877
+ newIndex = (newIndex + increment + this.items.length) % this.items.length;
878
+ iterations += 1;
555
879
 
556
- if (!valueMatch) {
557
- // reset the menu to no selection
558
- this.index = undefined;
880
+ // Check if current option is interactive
881
+ const currentOption = this.items[newIndex];
882
+ if (isOptionInteractive(currentOption)) {
883
+ foundInteractiveOption = true;
884
+ break;
885
+ }
559
886
 
560
- this.dispatchEvent(new CustomEvent('auroMenu-selectValueFailure', {
561
- bubbles: true,
562
- cancelable: false,
563
- composed: true,
564
- }));
565
- } else {
566
- this.makeSelection();
887
+ // Break if all options were checked
888
+ if (iterations >= maxIterations) {
889
+ break;
567
890
  }
568
- } else {
569
- this.resetOptionsStates();
891
+ } while (iterations < maxIterations);
570
892
 
571
- this.dispatchEvent(new CustomEvent('auroMenu-selectValueReset', {
572
- bubbles: true,
573
- cancelable: false,
574
- composed: true,
575
- }));
893
+ // Handle the results of the search
894
+ if (foundInteractiveOption) {
895
+ // Update only if an interactive option was found
896
+ this.index = newIndex;
897
+ this.updateActiveOption(this.index);
898
+ } else {
899
+ // All options are disabled or non-interactive
900
+ // Keep the current index unchanged
901
+ dispatchMenuEvent(this, 'auroMenu-navigateFailure', {
902
+ reason: 'No interactive options available',
903
+ direction,
904
+ currentIndex: this.index
905
+ });
576
906
  }
577
907
  }
578
908
 
579
909
  /**
580
- * Used to make the active state for options follow mouseover.
581
- * @param {Number} index - Index of the menuoption that will be made active.
910
+ * Updates the active option state and dispatches events.
582
911
  * @private
912
+ * @param {number} index - Index of the option to make active.
583
913
  */
584
914
  updateActiveOption(index) {
585
- this.items.forEach((item) => {
586
- item.classList.remove('active');
587
- });
915
+ if (!this.items || !this.items[index]) {
916
+ return;
917
+ }
918
+
919
+ this.items.forEach((item) => item.classList.remove('active'));
588
920
  this.items[index].classList.add('active');
589
921
  this.optionActive = this.items[index];
590
922
 
591
- this.dispatchEvent(new CustomEvent('auroMenu-activatedOption', {
592
- bubbles: true,
593
- cancelable: false,
594
- composed: true,
595
- detail: this.items[index]
596
- }));
923
+ dispatchMenuEvent(this, 'auroMenu-activatedOption', this.items[index]);
597
924
  }
598
925
 
599
926
  /**
600
- * Used to only make a selection when a menuoption is receiving a mousedown event.
601
- * @param {Event} evt - Mousedown event.
927
+ * Handles custom events defined on options.
602
928
  * @private
929
+ * @param {HTMLElement} option - Option with custom event.
603
930
  */
604
- handleMenuMouseDown(evt) {
605
- if (evt.target !== this) {
606
- this.makeSelection();
607
- }
931
+ handleCustomEvent(option) {
932
+ const eventName = option.getAttribute('event');
933
+ dispatchMenuEvent(this, eventName);
934
+ dispatchMenuEvent(this, 'auroMenu-customEventFired');
608
935
  }
609
936
 
610
937
  /**
611
- * Checks if there are any loading placeholders in the component.
612
- *
613
- * This getter evaluates the `loadingSlots` collection to determine if it contains any items.
614
- * If the size of the collection is greater than zero, it indicates the presence of loading
615
- * placeholders, returning true; otherwise, it returns false.
616
- *
617
- * @getter hasLoadingPlaceholder
618
- * @type {boolean}
619
- * @returns {boolean} Returns true if loading placeholders exist; false otherwise.
938
+ * Notifies selection change to parent components.
939
+ * @private
620
940
  */
621
- get hasLoadingPlaceholder() {
622
- return this.loadingSlots.length > 0;
941
+ notifySelectionChange() {
942
+ dispatchMenuEvent(this, 'auroMenu-selectedOption');
623
943
  }
624
944
 
625
945
  /**
626
- * Used for @slotchange event on slotted element.
946
+ * Checks if an option is currently selected.
627
947
  * @private
948
+ * @param {HTMLElement} option - The option to check.
949
+ * @returns {boolean}
628
950
  */
629
- handleSlotItems() {
630
- // Determine if this is the root of the menu/submenu layout.
631
- if (this.parentElement && this.parentElement.closest('auro-menu, [auro-menu]')) {
632
- this.rootMenu = false;
951
+ isOptionSelected(option) {
952
+ if (!this.optionSelected) {
953
+ return false;
633
954
  }
955
+ // Always treat as array for both single and multi-select
956
+ return Array.isArray(this.optionSelected) && this.optionSelected.includes(option);
957
+ }
634
958
 
635
- // If this is the root menu (not a nested menu) handle events, states and styling.
636
- if (this.rootMenu) {
637
- this.initItems();
638
- this.setAttribute('role', 'listbox');
639
- this.setAttribute('root', '');
640
- this.handleNestedMenus(this);
641
- this.markOptions();
642
- this.index = -1;
643
- this.getSelectedIndex();
644
-
645
- this.addEventListener('keydown', this.handleKeyDown);
646
- this.addEventListener('mousedown', this.handleMenuMouseDown);
647
- this.addEventListener('auroMenuOption-mouseover', (evt) => {
648
- this.index = this.items.indexOf(evt.target);
649
- this.updateActiveOption(this.index);
650
- });
651
- } else {
652
- // make sure to update all menuoption noCheckmark attributes when the menu is dynamically changed
653
- this.handleNoCheckmarkAttr();
654
- }
959
+ /**
960
+ * Getter for loading placeholder state.
961
+ * @returns {boolean} - True if loading slots are present and non-empty.
962
+ */
963
+ get hasLoadingPlaceholder() {
964
+ return this.loadingSlots && this.loadingSlots.length > 0;
655
965
  }
656
966
 
967
+ /**
968
+ * Renders the component.
969
+ * @returns {boolean} - True if loading slots are present and non-empty.
970
+ */
657
971
  render() {
658
972
  if (this.loading) {
659
973
  return x`
@@ -665,7 +979,8 @@ class AuroMenu extends r {
665
979
  </auro-menuoption>
666
980
  `;
667
981
  }
668
- return x`<slot @slotchange=${this.handleSlotItems}></slot>`;
982
+
983
+ return x`<slot @slotchange=${this.handleSlotChange}></slot>`;
669
984
  }
670
985
  }
671
986