@aurodesignsystem-dev/auro-formkit 0.0.0-pr1574.0 → 0.0.0-pr1576.1

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 (51) hide show
  1. package/components/checkbox/demo/customize.min.js +1 -1
  2. package/components/checkbox/demo/getting-started.min.js +1 -1
  3. package/components/checkbox/demo/index.min.js +1 -1
  4. package/components/checkbox/dist/index.js +1 -1
  5. package/components/checkbox/dist/registered.js +1 -1
  6. package/components/combobox/demo/customize.md +40 -0
  7. package/components/combobox/demo/customize.min.js +256 -17
  8. package/components/combobox/demo/getting-started.min.js +256 -17
  9. package/components/combobox/demo/index.min.js +256 -17
  10. package/components/combobox/dist/index.js +3 -3
  11. package/components/combobox/dist/registered.js +3 -3
  12. package/components/counter/demo/customize.min.js +3 -3
  13. package/components/counter/demo/index.min.js +3 -3
  14. package/components/counter/dist/index.js +3 -3
  15. package/components/counter/dist/registered.js +3 -3
  16. package/components/datepicker/demo/customize.min.js +3 -3
  17. package/components/datepicker/demo/index.min.js +3 -3
  18. package/components/datepicker/dist/index.js +3 -3
  19. package/components/datepicker/dist/registered.js +3 -3
  20. package/components/dropdown/demo/customize.min.js +1 -1
  21. package/components/dropdown/demo/getting-started.min.js +1 -1
  22. package/components/dropdown/demo/index.min.js +1 -1
  23. package/components/dropdown/dist/index.js +1 -1
  24. package/components/dropdown/dist/registered.js +1 -1
  25. package/components/form/demo/customize.min.js +267 -28
  26. package/components/form/demo/getting-started.min.js +267 -28
  27. package/components/form/demo/index.min.js +267 -28
  28. package/components/form/demo/registerDemoDeps.min.js +267 -28
  29. package/components/input/demo/customize.min.js +1 -1
  30. package/components/input/demo/getting-started.min.js +1 -1
  31. package/components/input/demo/index.min.js +1 -1
  32. package/components/input/dist/index.js +1 -1
  33. package/components/input/dist/registered.js +1 -1
  34. package/components/menu/demo/index.min.js +253 -14
  35. package/components/menu/dist/auro-menu-utils.d.ts +30 -0
  36. package/components/menu/dist/auro-menu.d.ts +23 -0
  37. package/components/menu/dist/index.js +253 -14
  38. package/components/menu/dist/registered.js +253 -14
  39. package/components/radio/demo/customize.min.js +1 -1
  40. package/components/radio/demo/getting-started.min.js +1 -1
  41. package/components/radio/demo/index.min.js +1 -1
  42. package/components/radio/dist/index.js +1 -1
  43. package/components/radio/dist/registered.js +1 -1
  44. package/components/select/demo/customize.md +72 -0
  45. package/components/select/demo/customize.min.js +255 -16
  46. package/components/select/demo/getting-started.min.js +255 -16
  47. package/components/select/demo/index.min.js +255 -16
  48. package/components/select/dist/index.js +2 -2
  49. package/components/select/dist/registered.js +2 -2
  50. package/custom-elements.json +1617 -1513
  51. package/package.json +1 -1
@@ -259,6 +259,107 @@ function isSelectableByValue(option) {
259
259
  !option.hasAttribute('static');
260
260
  }
261
261
 
262
+ /* eslint-disable no-underscore-dangle */
263
+ /**
264
+ * Resolves the single selected option for a given `value`, preferring the
265
+ * option tracked by `selectedKey` (a user-initiated selection) over a
266
+ * first-by-value match. When multiple options share the same `value`, matching
267
+ * by `value` alone cannot distinguish which one the user picked; the key
268
+ * disambiguates it.
269
+ *
270
+ * The key is trusted only when it still resolves to an option whose `value`
271
+ * matches the requested `value`. If the key is stale (option removed) or the
272
+ * value was changed programmatically, resolution falls back to value matching —
273
+ * preserving backward-compatible behavior for preselection and `selectByValue`.
274
+ * @private
275
+ * @param {Array<HTMLElement>} items - The menu's flat option list.
276
+ * @param {string} value - The value to resolve.
277
+ * @param {string|undefined} selectedKey - The `_optionKey` of the user-selected option, if any.
278
+ * @returns {HTMLElement|undefined} The resolved option, or undefined when none match.
279
+ */
280
+ function resolveSelectedOption(items, value, selectedKey) {
281
+ if (!items) {
282
+ return undefined;
283
+ }
284
+
285
+ if (selectedKey !== undefined) {
286
+ const keyed = items.find((item) => item._optionKey === selectedKey);
287
+ if (keyed && isSelectableByValue(keyed) && keyed.value === value) {
288
+ return keyed;
289
+ }
290
+ // Key exists but the option is gone or its value no longer matches — fall
291
+ // through to value-based matching.
292
+ }
293
+
294
+ return items.find((item) => isSelectableByValue(item) && item.value === value);
295
+ }
296
+
297
+ /**
298
+ * Resolves the selected options for a multi-select `value` array, preferring
299
+ * options tracked by `selectedKeys` (user-initiated selections) and falling
300
+ * back to value matching for any values not resolved by key. The result is
301
+ * always sorted into DOM order regardless of selection sequence.
302
+ * @private
303
+ * @param {Array<HTMLElement>} items - The menu's flat option list.
304
+ * @param {Array<string>} valueArray - The selected values.
305
+ * @param {Array<string>|undefined} selectedKeys - The `_optionKey`s of the user-selected options, if any.
306
+ * @returns {Array<HTMLElement>} The resolved options in DOM order.
307
+ */
308
+ function resolveSelectedOptions(items, valueArray, selectedKeys) {
309
+ if (!items) {
310
+ return [];
311
+ }
312
+
313
+ const resolved = [];
314
+
315
+ // Track how many of each value are still available to resolve. A value that
316
+ // appears N times in `valueArray` may be satisfied at most N times total across
317
+ // the key pass and the value fallback below — matching by count, not presence,
318
+ // on BOTH passes. This stops a duplicate value from being over-resolved: e.g.
319
+ // two keyed options that both carry `SEA` cannot both match a single requested
320
+ // `SEA` (which happens when `value` is set directly without clearing
321
+ // `_selectedKey`, so more keys survive than the value set now asks for).
322
+ const remaining = new Map();
323
+ valueArray.forEach((val) => remaining.set(val, (remaining.get(val) || 0) + 1));
324
+
325
+ // Resolve by key first: trust a key only when its option is still selectable
326
+ // and there is still an unmatched occurrence of its value in the request set.
327
+ if (Array.isArray(selectedKeys)) {
328
+ selectedKeys.forEach((key) => {
329
+ const keyed = items.find((item) => item._optionKey === key);
330
+ if (keyed && isSelectableByValue(keyed) && (remaining.get(keyed.value) || 0) > 0 && !resolved.includes(keyed)) {
331
+ resolved.push(keyed);
332
+ remaining.set(keyed.value, remaining.get(keyed.value) - 1);
333
+ }
334
+ });
335
+ }
336
+
337
+ // Fall back to value matching for the occurrences not resolved by key. Iterate
338
+ // the leftover per-value counts so a value that appears twice but was only
339
+ // resolved once by key still matches its remaining occurrence(s).
340
+ remaining.forEach((count, val) => {
341
+ for (let occurrence = 0; occurrence < count; occurrence += 1) {
342
+ const option = items.find((item) => isSelectableByValue(item) && item.value === val && !resolved.includes(item));
343
+ if (option) {
344
+ resolved.push(option);
345
+ }
346
+ }
347
+ });
348
+
349
+ // Always return in DOM order so display is consistent regardless of the order
350
+ // keys/values were selected. Every resolved option came from `items`, so an
351
+ // O(1) index lookup mirrors `_sortSelectedByDomOrder` and avoids the O(n)
352
+ // `items.indexOf` per comparison for large combobox option sets.
353
+ const indexMap = new Map(items.map((item, index) => [
354
+ item,
355
+ index
356
+ ]));
357
+ resolved.sort((optionA, optionB) => indexMap.get(optionA) - indexMap.get(optionB));
358
+
359
+ return resolved;
360
+ }
361
+ /* eslint-enable no-underscore-dangle */
362
+
262
363
  /**
263
364
  * Helper method to dispatch custom events.
264
365
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -297,6 +398,14 @@ const t={ATTRIBUTE:1},e$1=t=>(...e)=>({_$litDirective$:t,values:e});let i$1 = cl
297
398
  // See LICENSE in the project root for license information.
298
399
 
299
400
 
401
+ /**
402
+ * Monotonically increasing counter used to give each menu instance a unique
403
+ * `_menuInstanceId` prefix. Auto-generating the id (rather than using a random
404
+ * string) keeps option keys deterministic and collision-free across menus.
405
+ * @private
406
+ */
407
+ let menuInstanceIdCounter = 0;
408
+
300
409
 
301
410
  /**
302
411
  * The `auro-menu` element provides users a way to select from a list of options.
@@ -371,6 +480,8 @@ class AuroMenu extends AuroElement {
371
480
  /**
372
481
  * @private
373
482
  */
483
+ menuInstanceIdCounter += 1;
484
+
374
485
  Object.assign(this, {
375
486
  // Root-level menu (true) or a nested submenu (false)
376
487
  rootMenu: true,
@@ -380,6 +491,21 @@ class AuroMenu extends AuroElement {
380
491
  nestingSpacer: '<span class="nestingSpacer"></span>',
381
492
  // Loading indicator for slot elements
382
493
  loadingSlots: null,
494
+ // Unique id for this menu instance; prefixes every auto-generated option
495
+ // key so keys never collide across menus in the same document.
496
+ _menuInstanceId: `menu-${menuInstanceIdCounter}`,
497
+ // Monotonically increasing counter for option key generation. Never
498
+ // resets, so a key is never reused within this instance's lifetime.
499
+ _optionKeyCounter: 0,
500
+ // Key(s) of the option(s) the user has actively selected. A single string
501
+ // in single-select, an array in multi-select, undefined when nothing is
502
+ // user-selected. Used to disambiguate options that share a `value`.
503
+ _selectedKey: undefined,
504
+ // True only for the one updated() cycle following a user selection, so
505
+ // reconciliation trusts `_selectedKey`. A `value` change from a consumer's
506
+ // direct property assignment leaves this false, dropping the stale key so
507
+ // reconciliation falls back to first-by-value (see updated()).
508
+ _valueChangeFromSelection: false,
383
509
  });
384
510
  }
385
511
 
@@ -599,6 +725,13 @@ class AuroMenu extends AuroElement {
599
725
  return;
600
726
  }
601
727
 
728
+ // A programmatic value set carries no positional intent, so drop any
729
+ // `_selectedKey` left over from a prior user click. This makes reconciliation
730
+ // in updated() fall back to first-by-value (single) / value-in-DOM-order
731
+ // (multi), matching the documented contract for programmatic selection even
732
+ // when a stale key would still resolve to a duplicate-value option.
733
+ this._selectedKey = undefined;
734
+
602
735
  // `value` is a String property; stringify arrays so attribute reflection and `formattedValue` parsing stay correct.
603
736
  this.value = Array.isArray(value) ? JSON.stringify(value) : value;
604
737
  }
@@ -646,6 +779,17 @@ class AuroMenu extends AuroElement {
646
779
  updated(changedProperties) {
647
780
  super.updated(changedProperties);
648
781
 
782
+ // Consume the selection-driven flag for THIS cycle up front. Clearing it
783
+ // unconditionally — not only inside the `value` branch below — prevents it
784
+ // from lingering `true` when a selection produces a serialized `value`
785
+ // byte-identical to the current one, in which case Lit schedules no
786
+ // `value`-change cycle to consume it. A lingering flag would misclassify a
787
+ // later consumer's programmatic `value` set as selection-driven and keep a
788
+ // stale `_selectedKey`. The reconcile path below re-sets the instance flag
789
+ // after this point, so its intentional cross-cycle hand-off still works.
790
+ const valueChangeFromSelection = this._valueChangeFromSelection;
791
+ this._valueChangeFromSelection = false;
792
+
649
793
  // Single source of truth for 'auroMenu-selectedOption'. Selection handlers
650
794
  // mutate optionSelected and let Lit's update cycle dispatch here; the prior
651
795
  // .value comparison missed multi-select array changes and combined with the
@@ -669,6 +813,17 @@ class AuroMenu extends AuroElement {
669
813
  this.initItems();
670
814
  }
671
815
 
816
+ // Distinguish a selection-driven `value` change (a user click, which set
817
+ // the flag in handleSelectState / _sortSelectedByDomOrder) from a
818
+ // programmatic assignment by a consumer. A programmatic set carries no
819
+ // positional intent, so drop any leftover `_selectedKey` and let
820
+ // reconciliation fall back to first-by-value (single) / value-in-DOM-order
821
+ // (multi) — the same contract selectByValue() guarantees, even when a
822
+ // stale key would otherwise still resolve to a duplicate-value option.
823
+ if (!valueChangeFromSelection) {
824
+ this._selectedKey = undefined;
825
+ }
826
+
672
827
  // Set when reconciliation reassigns `value` below. That reassignment schedules a
673
828
  // second updated() cycle, so the `event`-attribute dispatch is deferred to that
674
829
  // cycle to avoid firing option custom events twice on the same selection.
@@ -686,7 +841,10 @@ class AuroMenu extends AuroElement {
686
841
  // Defensive default: `formattedValue` can be undefined for unexpected value types,
687
842
  // and calling `.includes` on undefined would throw during reconciliation.
688
843
  const valueArray = this.formattedValue || [];
689
- const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
844
+ // Resolve by key first (the user's exact picks), then fall back to
845
+ // value matching for any values not resolved by key — so pre-selection
846
+ // and programmatic value sets keep working. Result is DOM-ordered.
847
+ const matchingOptions = resolveSelectedOptions(this.items, valueArray, this._selectedKey);
690
848
  newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
691
849
 
692
850
  // Reconcile `value` with the selectable set. Drop only entries whose option is
@@ -699,6 +857,12 @@ class AuroMenu extends AuroElement {
699
857
  : [];
700
858
  if (rejectedValues.length > 0) {
701
859
  const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
860
+ // This is an internal correction, not a consumer's programmatic set,
861
+ // so preserve the selection-driven flag through the re-entrant
862
+ // updated() cycle it schedules. Otherwise that cycle would treat the
863
+ // reassignment as programmatic and drop `_selectedKey` mid-cascade,
864
+ // flipping resolution and looping.
865
+ this._valueChangeFromSelection = true;
702
866
  this.value = serializeMultiSelectValue(reconciled);
703
867
  valueReconciled = true;
704
868
  }
@@ -710,7 +874,11 @@ class AuroMenu extends AuroElement {
710
874
  // `hidden` is intentionally NOT excluded: the combobox toggles
711
875
  // `hidden` as its type-ahead filter, so a filtered-out option is
712
876
  // still a valid programmatic selection.
713
- const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
877
+ // Prefer the option the user actually selected (tracked by
878
+ // `_selectedKey`) so a click on the second of two options sharing a
879
+ // `value` resolves back to that exact element instead of the first
880
+ // value match. Falls back to first-by-value for programmatic sets.
881
+ const matchingOption = resolveSelectedOption(this.items, this.value, this._selectedKey);
714
882
 
715
883
  if (matchingOption) {
716
884
  newSelected = matchingOption;
@@ -938,6 +1106,14 @@ class AuroMenu extends AuroElement {
938
1106
  }
939
1107
  });
940
1108
 
1109
+ // Assign private keys once items are populated. Only the root menu assigns
1110
+ // keys: its `items` is a deep query that already includes nested submenu
1111
+ // options, so a single pass keys the entire tree. Nested menus skip this
1112
+ // and inherit keys from the root.
1113
+ if (this.rootMenu) {
1114
+ this._assignOptionKeys();
1115
+ }
1116
+
941
1117
  if (this.noCheckmark) {
942
1118
  this.updateItemsState(new Map([
943
1119
  [
@@ -954,6 +1130,31 @@ class AuroMenu extends AuroElement {
954
1130
  }));
955
1131
  }
956
1132
 
1133
+ /**
1134
+ * Assigns a private, auto-generated unique key (`_optionKey`) to each menu
1135
+ * option that does not already have one. Keys are internal state on the
1136
+ * element instance — never reflected as an attribute or exposed publicly —
1137
+ * and let selection tracking distinguish options that share the same `value`.
1138
+ *
1139
+ * The `_optionKey === undefined` guard makes this idempotent: options keep the
1140
+ * key they were first assigned across re-renders and slot changes, and if a
1141
+ * nested menu's lifecycle runs a pass before the root, options simply wait for
1142
+ * the root to key them (or keep whatever key they already hold).
1143
+ * @private
1144
+ */
1145
+ _assignOptionKeys() {
1146
+ if (!this.items) {
1147
+ return;
1148
+ }
1149
+
1150
+ this.items.forEach((option) => {
1151
+ if (option._optionKey === undefined) {
1152
+ this._optionKeyCounter += 1;
1153
+ option._optionKey = `${this._menuInstanceId}-${this._optionKeyCounter}`;
1154
+ }
1155
+ });
1156
+ }
1157
+
957
1158
  // Logic Methods
958
1159
 
959
1160
  /**
@@ -963,24 +1164,28 @@ class AuroMenu extends AuroElement {
963
1164
  */
964
1165
  handleSelectState(option) {
965
1166
  if (this.multiSelect) {
966
- const currentValue = this.formattedValue || [];
967
1167
  const currentSelected = this.optionSelected || [];
968
1168
 
969
- if (!currentValue.includes(option.value)) {
970
- this.value = serializeMultiSelectValue([
971
- ...currentValue,
972
- option.value
973
- ]);
974
- }
975
1169
  if (!currentSelected.includes(option)) {
976
1170
  this.optionSelected = [
977
1171
  ...currentSelected,
978
1172
  option
979
1173
  ];
980
1174
  }
1175
+
1176
+ // Re-sort by DOM order and rebuild `_selectedKey`/`value` from the
1177
+ // selected set so display order stays consistent with the menu, not with
1178
+ // click order.
1179
+ this._sortSelectedByDomOrder();
981
1180
  } else {
982
1181
  this.value = option.value;
983
1182
  this.optionSelected = option;
1183
+ // Track the specific option the user selected so the value→option
1184
+ // reconciliation in updated() resolves back to this exact element even
1185
+ // when another option shares the same `value`.
1186
+ this._selectedKey = option._optionKey;
1187
+ // Mark this `value` change as selection-driven so updated() trusts the key.
1188
+ this._valueChangeFromSelection = true;
984
1189
  }
985
1190
 
986
1191
  this._index = this.items.indexOf(option);
@@ -993,18 +1198,22 @@ class AuroMenu extends AuroElement {
993
1198
  */
994
1199
  handleDeselectState(option) {
995
1200
  if (this.multiSelect) {
996
- // Remove this option from array; an empty result collapses `value` to undefined.
997
- const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
998
- this.value = serializeMultiSelectValue(newFormattedValue);
999
-
1000
- this.optionSelected = this.optionSelected.filter((val) => val !== option);
1201
+ // Remove this exact element from the selection (identity, not value — two
1202
+ // options can share a `value`), then rebuild `value`/`_selectedKey` from
1203
+ // the remaining set in DOM order. An empty result collapses to undefined.
1204
+ this.optionSelected = this.optionSelected.filter((selected) => selected !== option);
1001
1205
  if (this.optionSelected.length === 0) {
1002
1206
  this.optionSelected = undefined;
1207
+ this._selectedKey = undefined;
1208
+ this.value = undefined;
1209
+ } else {
1210
+ this._sortSelectedByDomOrder();
1003
1211
  }
1004
1212
  } else {
1005
1213
  // For single-select: Back to undefined when deselected
1006
1214
  this.value = undefined;
1007
1215
  this.optionSelected = undefined;
1216
+ this._selectedKey = undefined;
1008
1217
  }
1009
1218
 
1010
1219
  // Update the index tracking
@@ -1028,9 +1237,38 @@ class AuroMenu extends AuroElement {
1028
1237
  clearSelection() {
1029
1238
  this.optionSelected = undefined;
1030
1239
  this.value = undefined;
1240
+ this._selectedKey = undefined;
1031
1241
  this._index = -1;
1032
1242
  }
1033
1243
 
1244
+ /**
1245
+ * Re-sorts the multi-select selection into DOM order and rebuilds the derived
1246
+ * `_selectedKey` and `value` from `optionSelected`. Selection is always stored
1247
+ * and serialized in the order options appear in the menu, never in click
1248
+ * order — so selecting C then A yields `[A, C]`.
1249
+ * @private
1250
+ */
1251
+ _sortSelectedByDomOrder() {
1252
+ if (!this.multiSelect || !Array.isArray(this.optionSelected) || !this.items) {
1253
+ return;
1254
+ }
1255
+
1256
+ const indexMap = new Map(this.items.map((item, index) => [
1257
+ item,
1258
+ index
1259
+ ]));
1260
+
1261
+ // Sort any element no longer in `items` (a stale selection left over from a
1262
+ // dynamic rebuild that the consumer has not cleared) to the END rather than
1263
+ // the front, so it never displaces a live option to the head of the
1264
+ // serialized order. Value reconciliation drops it on the next updated() cycle.
1265
+ this.optionSelected.sort((optionA, optionB) => (indexMap.get(optionA) ?? this.items.length) - (indexMap.get(optionB) ?? this.items.length));
1266
+ this._selectedKey = this.optionSelected.map((option) => option._optionKey);
1267
+ this.value = serializeMultiSelectValue(this.optionSelected.map((option) => option.value));
1268
+ // Mark this `value` change as selection-driven so updated() trusts the keys.
1269
+ this._valueChangeFromSelection = true;
1270
+ }
1271
+
1034
1272
  /**
1035
1273
  * Resets the menu to its initial state.
1036
1274
  * This is the only way to return value to undefined.
@@ -1040,6 +1278,7 @@ class AuroMenu extends AuroElement {
1040
1278
  // Reset to undefined - initial state
1041
1279
  this.value = undefined;
1042
1280
  this.optionSelected = undefined;
1281
+ this._selectedKey = undefined;
1043
1282
  this._index = -1;
1044
1283
 
1045
1284
  // Clear active option state so a follow-up open/navigation starts fresh
@@ -44,6 +44,36 @@ export function isOptionInteractive(option: HTMLElement): boolean;
44
44
  * @returns {boolean} True if option can be selected by value.
45
45
  */
46
46
  export function isSelectableByValue(option: HTMLElement): boolean;
47
+ /**
48
+ * Resolves the single selected option for a given `value`, preferring the
49
+ * option tracked by `selectedKey` (a user-initiated selection) over a
50
+ * first-by-value match. When multiple options share the same `value`, matching
51
+ * by `value` alone cannot distinguish which one the user picked; the key
52
+ * disambiguates it.
53
+ *
54
+ * The key is trusted only when it still resolves to an option whose `value`
55
+ * matches the requested `value`. If the key is stale (option removed) or the
56
+ * value was changed programmatically, resolution falls back to value matching —
57
+ * preserving backward-compatible behavior for preselection and `selectByValue`.
58
+ * @private
59
+ * @param {Array<HTMLElement>} items - The menu's flat option list.
60
+ * @param {string} value - The value to resolve.
61
+ * @param {string|undefined} selectedKey - The `_optionKey` of the user-selected option, if any.
62
+ * @returns {HTMLElement|undefined} The resolved option, or undefined when none match.
63
+ */
64
+ export function resolveSelectedOption(items: Array<HTMLElement>, value: string, selectedKey: string | undefined): HTMLElement | undefined;
65
+ /**
66
+ * Resolves the selected options for a multi-select `value` array, preferring
67
+ * options tracked by `selectedKeys` (user-initiated selections) and falling
68
+ * back to value matching for any values not resolved by key. The result is
69
+ * always sorted into DOM order regardless of selection sequence.
70
+ * @private
71
+ * @param {Array<HTMLElement>} items - The menu's flat option list.
72
+ * @param {Array<string>} valueArray - The selected values.
73
+ * @param {Array<string>|undefined} selectedKeys - The `_optionKey`s of the user-selected options, if any.
74
+ * @returns {Array<HTMLElement>} The resolved options in DOM order.
75
+ */
76
+ export function resolveSelectedOptions(items: Array<HTMLElement>, valueArray: Array<string>, selectedKeys: Array<string> | undefined): Array<HTMLElement>;
47
77
  /**
48
78
  * Helper method to dispatch custom events.
49
79
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -172,6 +172,7 @@ export class AuroMenu extends AuroElement {
172
172
  * @public
173
173
  */
174
174
  public selectByValue(value: string | string[] | undefined | null): void;
175
+ _selectedKey: any;
175
176
  firstUpdated(): void;
176
177
  loadingSlots: NodeListOf<Element> | undefined;
177
178
  /**
@@ -181,6 +182,7 @@ export class AuroMenu extends AuroElement {
181
182
  */
182
183
  private setTagAttribute;
183
184
  updated(changedProperties: any): void;
185
+ _valueChangeFromSelection: boolean | undefined;
184
186
  _index: number | undefined;
185
187
  /**
186
188
  * Updates the UI state and appearance of menu items based on changed properties.
@@ -199,6 +201,19 @@ export class AuroMenu extends AuroElement {
199
201
  */
200
202
  private initItems;
201
203
  items: Element[] | undefined;
204
+ /**
205
+ * Assigns a private, auto-generated unique key (`_optionKey`) to each menu
206
+ * option that does not already have one. Keys are internal state on the
207
+ * element instance — never reflected as an attribute or exposed publicly —
208
+ * and let selection tracking distinguish options that share the same `value`.
209
+ *
210
+ * The `_optionKey === undefined` guard makes this idempotent: options keep the
211
+ * key they were first assigned across re-renders and slot changes, and if a
212
+ * nested menu's lifecycle runs a pass before the root, options simply wait for
213
+ * the root to key them (or keep whatever key they already hold).
214
+ * @private
215
+ */
216
+ private _assignOptionKeys;
202
217
  /**
203
218
  * Updates menu state when an option is selected.
204
219
  * @private
@@ -216,6 +231,14 @@ export class AuroMenu extends AuroElement {
216
231
  * @private
217
232
  */
218
233
  private clearSelection;
234
+ /**
235
+ * Re-sorts the multi-select selection into DOM order and rebuilds the derived
236
+ * `_selectedKey` and `value` from `optionSelected`. Selection is always stored
237
+ * and serialized in the order options appear in the menu, never in click
238
+ * order — so selecting C then A yields `[A, C]`.
239
+ * @private
240
+ */
241
+ private _sortSelectedByDomOrder;
219
242
  /**
220
243
  * Resets the menu to its initial state.
221
244
  * This is the only way to return value to undefined.