@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
@@ -1275,7 +1275,7 @@ class AuroHelpText extends i$2 {
1275
1275
  }
1276
1276
  }
1277
1277
 
1278
- var formkitVersion = '202607272111';
1278
+ var formkitVersion = '202608040326';
1279
1279
 
1280
1280
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
1281
1281
  // See LICENSE in the project root for license information.
@@ -1275,7 +1275,7 @@ class AuroHelpText extends i$2 {
1275
1275
  }
1276
1276
  }
1277
1277
 
1278
- var formkitVersion = '202607272111';
1278
+ var formkitVersion = '202608040326';
1279
1279
 
1280
1280
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
1281
1281
  // See LICENSE in the project root for license information.
@@ -1275,7 +1275,7 @@ class AuroHelpText extends i$2 {
1275
1275
  }
1276
1276
  }
1277
1277
 
1278
- var formkitVersion = '202607272111';
1278
+ var formkitVersion = '202608040326';
1279
1279
 
1280
1280
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
1281
1281
  // See LICENSE in the project root for license information.
@@ -1228,7 +1228,7 @@ class AuroHelpText extends LitElement {
1228
1228
  }
1229
1229
  }
1230
1230
 
1231
- var formkitVersion = '202607272111';
1231
+ var formkitVersion = '202608040326';
1232
1232
 
1233
1233
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
1234
1234
  // See LICENSE in the project root for license information.
@@ -1228,7 +1228,7 @@ class AuroHelpText extends LitElement {
1228
1228
  }
1229
1229
  }
1230
1230
 
1231
- var formkitVersion = '202607272111';
1231
+ var formkitVersion = '202608040326';
1232
1232
 
1233
1233
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
1234
1234
  // See LICENSE in the project root for license information.
@@ -29,6 +29,7 @@
29
29
  <auro-anchorlink fluid href="#noValidate" class="level2 body-xs">No Validation</auro-anchorlink>
30
30
  <auro-anchorlink fluid href="#dynamicMenu" class="level2 body-xs">Dynamic Menu</auro-anchorlink>
31
31
  <auro-anchorlink fluid href="#loading" class="level2 body-xs">Loading</auro-anchorlink>
32
+ <auro-anchorlink fluid href="#nonUniqueValues" class="level2 body-xs">Non-Unique Option Values</auro-anchorlink>
32
33
  </auro-nav>
33
34
  </nav>
34
35
  <div class="mainContent">
@@ -1296,6 +1297,45 @@
1296
1297
  &lt;/auro-combobox&gt;</code></pre>
1297
1298
  <!-- AURO-GENERATED-CONTENT:END -->
1298
1299
  </auro-accordion>
1300
+ <auro-header level="3" id="nonUniqueValues">Non-Unique Option Values</auro-header>
1301
+ <p>Two or more <code>auro-menuoption</code> elements may share the same <code>value</code>. This is common when the <code>value</code> represents a coarser grouping than the option label &mdash; for example, several airports that all serve the same city. When the user filters the list and selects one of several options that share a <code>value</code>, the combobox resolves to the exact option chosen, so the correct label is displayed even though the underlying <code>value</code> is duplicated.</p>
1302
+ <div class="exampleWrapper">
1303
+ <!-- AURO-GENERATED-CONTENT:START (FILE:src=./../apiExamples/duplicate-values.html) -->
1304
+ <!-- The below content is automatically added from ./../apiExamples/duplicate-values.html -->
1305
+ <auro-combobox>
1306
+ <span slot="ariaLabel.bib.close">Close combobox</span>
1307
+ <span slot="ariaLabel.input.clear">Clear All</span>
1308
+ <span slot="bib.fullscreen.headline">Choose an airport</span>
1309
+ <span slot="label">Departure airport</span>
1310
+ <auro-menu>
1311
+ <auro-menuoption value="seattle" suggest="seattle sea seatac tacoma">Seattle&ndash;Tacoma International (SEA)</auro-menuoption>
1312
+ <auro-menuoption value="seattle" suggest="seattle pae paine field everett">Seattle Paine Field (PAE)</auro-menuoption>
1313
+ <auro-menuoption value="portland" suggest="portland pdx oregon">Portland International (PDX)</auro-menuoption>
1314
+ <auro-menuoption value="spokane" suggest="spokane geg washington">Spokane International (GEG)</auro-menuoption>
1315
+ <auro-menuoption static nomatch>No matching airport</auro-menuoption>
1316
+ </auro-menu>
1317
+ </auro-combobox>
1318
+ <!-- AURO-GENERATED-CONTENT:END -->
1319
+ </div>
1320
+ <auro-accordion alignRight>
1321
+ <span slot="trigger">See code</span>
1322
+ <!-- AURO-GENERATED-CONTENT:START (CODE:src=./../apiExamples/duplicate-values.html) -->
1323
+ <!-- The below code snippet is automatically added from ./../apiExamples/duplicate-values.html -->
1324
+ <pre class="language-html"><code class="language-html">&lt;auro-combobox&gt;
1325
+ &lt;span slot="ariaLabel.bib.close"&gt;Close combobox&lt;/span&gt;
1326
+ &lt;span slot="ariaLabel.input.clear"&gt;Clear All&lt;/span&gt;
1327
+ &lt;span slot="bib.fullscreen.headline"&gt;Choose an airport&lt;/span&gt;
1328
+ &lt;span slot="label"&gt;Departure airport&lt;/span&gt;
1329
+ &lt;auro-menu&gt;
1330
+ &lt;auro-menuoption value="seattle" suggest="seattle sea seatac tacoma"&gt;Seattle&amp;ndash;Tacoma International (SEA)&lt;/auro-menuoption&gt;
1331
+ &lt;auro-menuoption value="seattle" suggest="seattle pae paine field everett"&gt;Seattle Paine Field (PAE)&lt;/auro-menuoption&gt;
1332
+ &lt;auro-menuoption value="portland" suggest="portland pdx oregon"&gt;Portland International (PDX)&lt;/auro-menuoption&gt;
1333
+ &lt;auro-menuoption value="spokane" suggest="spokane geg washington"&gt;Spokane International (GEG)&lt;/auro-menuoption&gt;
1334
+ &lt;auro-menuoption static nomatch&gt;No matching airport&lt;/auro-menuoption&gt;
1335
+ &lt;/auro-menu&gt;
1336
+ &lt;/auro-combobox&gt;</code></pre>
1337
+ <!-- AURO-GENERATED-CONTENT:END -->
1338
+ </auro-accordion>
1299
1339
  </section>
1300
1340
  </div>
1301
1341
  </div>
@@ -4977,7 +4977,7 @@ let AuroHelpText$2 = class AuroHelpText extends i$3 {
4977
4977
  }
4978
4978
  };
4979
4979
 
4980
- var formkitVersion$2 = '202607272111';
4980
+ var formkitVersion$2 = '202608040326';
4981
4981
 
4982
4982
  let AuroElement$2 = class AuroElement extends i$3 {
4983
4983
  static get properties() {
@@ -18666,7 +18666,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18666
18666
  }
18667
18667
  };
18668
18668
 
18669
- var formkitVersion$1 = '202607272111';
18669
+ var formkitVersion$1 = '202608040326';
18670
18670
 
18671
18671
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18672
18672
  // See LICENSE in the project root for license information.
@@ -19822,7 +19822,7 @@ class AuroBibtemplate extends i$3 {
19822
19822
  }
19823
19823
  }
19824
19824
 
19825
- var formkitVersion = '202607272111';
19825
+ var formkitVersion = '202608040326';
19826
19826
 
19827
19827
  var styleCss$3 = i$6`.util_displayInline{display:inline}.util_displayInlineBlock{display:inline-block}.util_displayBlock{display:block}.util_displayFlex{display:flex}.util_displayHidden{display:none}.util_displayHiddenVisually{position:absolute;overflow:hidden;clip:rect(1px, 1px, 1px, 1px);width:1px;height:1px;padding:0;border:0}:host{display:block;text-align:left}:host [auro-dropdown]{--ds-auro-dropdown-trigger-background-color: transparent}:host #inputInBib::part(wrapper){box-shadow:none}:host #inputInBib::part(accent-left){display:none}:host([layout*=classic]) [auro-input]{width:100%}:host([layout*=classic]) [auro-input]::part(helpText){display:none}:host([layout*=classic]) #slotHolder{display:none}`;
19828
19828
 
@@ -22255,6 +22255,107 @@ function isSelectableByValue(option) {
22255
22255
  !option.hasAttribute('static');
22256
22256
  }
22257
22257
 
22258
+ /* eslint-disable no-underscore-dangle */
22259
+ /**
22260
+ * Resolves the single selected option for a given `value`, preferring the
22261
+ * option tracked by `selectedKey` (a user-initiated selection) over a
22262
+ * first-by-value match. When multiple options share the same `value`, matching
22263
+ * by `value` alone cannot distinguish which one the user picked; the key
22264
+ * disambiguates it.
22265
+ *
22266
+ * The key is trusted only when it still resolves to an option whose `value`
22267
+ * matches the requested `value`. If the key is stale (option removed) or the
22268
+ * value was changed programmatically, resolution falls back to value matching —
22269
+ * preserving backward-compatible behavior for preselection and `selectByValue`.
22270
+ * @private
22271
+ * @param {Array<HTMLElement>} items - The menu's flat option list.
22272
+ * @param {string} value - The value to resolve.
22273
+ * @param {string|undefined} selectedKey - The `_optionKey` of the user-selected option, if any.
22274
+ * @returns {HTMLElement|undefined} The resolved option, or undefined when none match.
22275
+ */
22276
+ function resolveSelectedOption(items, value, selectedKey) {
22277
+ if (!items) {
22278
+ return undefined;
22279
+ }
22280
+
22281
+ if (selectedKey !== undefined) {
22282
+ const keyed = items.find((item) => item._optionKey === selectedKey);
22283
+ if (keyed && isSelectableByValue(keyed) && keyed.value === value) {
22284
+ return keyed;
22285
+ }
22286
+ // Key exists but the option is gone or its value no longer matches — fall
22287
+ // through to value-based matching.
22288
+ }
22289
+
22290
+ return items.find((item) => isSelectableByValue(item) && item.value === value);
22291
+ }
22292
+
22293
+ /**
22294
+ * Resolves the selected options for a multi-select `value` array, preferring
22295
+ * options tracked by `selectedKeys` (user-initiated selections) and falling
22296
+ * back to value matching for any values not resolved by key. The result is
22297
+ * always sorted into DOM order regardless of selection sequence.
22298
+ * @private
22299
+ * @param {Array<HTMLElement>} items - The menu's flat option list.
22300
+ * @param {Array<string>} valueArray - The selected values.
22301
+ * @param {Array<string>|undefined} selectedKeys - The `_optionKey`s of the user-selected options, if any.
22302
+ * @returns {Array<HTMLElement>} The resolved options in DOM order.
22303
+ */
22304
+ function resolveSelectedOptions(items, valueArray, selectedKeys) {
22305
+ if (!items) {
22306
+ return [];
22307
+ }
22308
+
22309
+ const resolved = [];
22310
+
22311
+ // Track how many of each value are still available to resolve. A value that
22312
+ // appears N times in `valueArray` may be satisfied at most N times total across
22313
+ // the key pass and the value fallback below — matching by count, not presence,
22314
+ // on BOTH passes. This stops a duplicate value from being over-resolved: e.g.
22315
+ // two keyed options that both carry `SEA` cannot both match a single requested
22316
+ // `SEA` (which happens when `value` is set directly without clearing
22317
+ // `_selectedKey`, so more keys survive than the value set now asks for).
22318
+ const remaining = new Map();
22319
+ valueArray.forEach((val) => remaining.set(val, (remaining.get(val) || 0) + 1));
22320
+
22321
+ // Resolve by key first: trust a key only when its option is still selectable
22322
+ // and there is still an unmatched occurrence of its value in the request set.
22323
+ if (Array.isArray(selectedKeys)) {
22324
+ selectedKeys.forEach((key) => {
22325
+ const keyed = items.find((item) => item._optionKey === key);
22326
+ if (keyed && isSelectableByValue(keyed) && (remaining.get(keyed.value) || 0) > 0 && !resolved.includes(keyed)) {
22327
+ resolved.push(keyed);
22328
+ remaining.set(keyed.value, remaining.get(keyed.value) - 1);
22329
+ }
22330
+ });
22331
+ }
22332
+
22333
+ // Fall back to value matching for the occurrences not resolved by key. Iterate
22334
+ // the leftover per-value counts so a value that appears twice but was only
22335
+ // resolved once by key still matches its remaining occurrence(s).
22336
+ remaining.forEach((count, val) => {
22337
+ for (let occurrence = 0; occurrence < count; occurrence += 1) {
22338
+ const option = items.find((item) => isSelectableByValue(item) && item.value === val && !resolved.includes(item));
22339
+ if (option) {
22340
+ resolved.push(option);
22341
+ }
22342
+ }
22343
+ });
22344
+
22345
+ // Always return in DOM order so display is consistent regardless of the order
22346
+ // keys/values were selected. Every resolved option came from `items`, so an
22347
+ // O(1) index lookup mirrors `_sortSelectedByDomOrder` and avoids the O(n)
22348
+ // `items.indexOf` per comparison for large combobox option sets.
22349
+ const indexMap = new Map(items.map((item, index) => [
22350
+ item,
22351
+ index
22352
+ ]));
22353
+ resolved.sort((optionA, optionB) => indexMap.get(optionA) - indexMap.get(optionB));
22354
+
22355
+ return resolved;
22356
+ }
22357
+ /* eslint-enable no-underscore-dangle */
22358
+
22258
22359
  /**
22259
22360
  * Helper method to dispatch custom events.
22260
22361
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -22280,6 +22381,14 @@ function dispatchMenuEvent(element, eventName, detail = null) {
22280
22381
  // See LICENSE in the project root for license information.
22281
22382
 
22282
22383
 
22384
+ /**
22385
+ * Monotonically increasing counter used to give each menu instance a unique
22386
+ * `_menuInstanceId` prefix. Auto-generating the id (rather than using a random
22387
+ * string) keeps option keys deterministic and collision-free across menus.
22388
+ * @private
22389
+ */
22390
+ let menuInstanceIdCounter = 0;
22391
+
22283
22392
 
22284
22393
  /**
22285
22394
  * The `auro-menu` element provides users a way to select from a list of options.
@@ -22354,6 +22463,8 @@ class AuroMenu extends AuroElement {
22354
22463
  /**
22355
22464
  * @private
22356
22465
  */
22466
+ menuInstanceIdCounter += 1;
22467
+
22357
22468
  Object.assign(this, {
22358
22469
  // Root-level menu (true) or a nested submenu (false)
22359
22470
  rootMenu: true,
@@ -22363,6 +22474,21 @@ class AuroMenu extends AuroElement {
22363
22474
  nestingSpacer: '<span class="nestingSpacer"></span>',
22364
22475
  // Loading indicator for slot elements
22365
22476
  loadingSlots: null,
22477
+ // Unique id for this menu instance; prefixes every auto-generated option
22478
+ // key so keys never collide across menus in the same document.
22479
+ _menuInstanceId: `menu-${menuInstanceIdCounter}`,
22480
+ // Monotonically increasing counter for option key generation. Never
22481
+ // resets, so a key is never reused within this instance's lifetime.
22482
+ _optionKeyCounter: 0,
22483
+ // Key(s) of the option(s) the user has actively selected. A single string
22484
+ // in single-select, an array in multi-select, undefined when nothing is
22485
+ // user-selected. Used to disambiguate options that share a `value`.
22486
+ _selectedKey: undefined,
22487
+ // True only for the one updated() cycle following a user selection, so
22488
+ // reconciliation trusts `_selectedKey`. A `value` change from a consumer's
22489
+ // direct property assignment leaves this false, dropping the stale key so
22490
+ // reconciliation falls back to first-by-value (see updated()).
22491
+ _valueChangeFromSelection: false,
22366
22492
  });
22367
22493
  }
22368
22494
 
@@ -22582,6 +22708,13 @@ class AuroMenu extends AuroElement {
22582
22708
  return;
22583
22709
  }
22584
22710
 
22711
+ // A programmatic value set carries no positional intent, so drop any
22712
+ // `_selectedKey` left over from a prior user click. This makes reconciliation
22713
+ // in updated() fall back to first-by-value (single) / value-in-DOM-order
22714
+ // (multi), matching the documented contract for programmatic selection even
22715
+ // when a stale key would still resolve to a duplicate-value option.
22716
+ this._selectedKey = undefined;
22717
+
22585
22718
  // `value` is a String property; stringify arrays so attribute reflection and `formattedValue` parsing stay correct.
22586
22719
  this.value = Array.isArray(value) ? JSON.stringify(value) : value;
22587
22720
  }
@@ -22629,6 +22762,17 @@ class AuroMenu extends AuroElement {
22629
22762
  updated(changedProperties) {
22630
22763
  super.updated(changedProperties);
22631
22764
 
22765
+ // Consume the selection-driven flag for THIS cycle up front. Clearing it
22766
+ // unconditionally — not only inside the `value` branch below — prevents it
22767
+ // from lingering `true` when a selection produces a serialized `value`
22768
+ // byte-identical to the current one, in which case Lit schedules no
22769
+ // `value`-change cycle to consume it. A lingering flag would misclassify a
22770
+ // later consumer's programmatic `value` set as selection-driven and keep a
22771
+ // stale `_selectedKey`. The reconcile path below re-sets the instance flag
22772
+ // after this point, so its intentional cross-cycle hand-off still works.
22773
+ const valueChangeFromSelection = this._valueChangeFromSelection;
22774
+ this._valueChangeFromSelection = false;
22775
+
22632
22776
  // Single source of truth for 'auroMenu-selectedOption'. Selection handlers
22633
22777
  // mutate optionSelected and let Lit's update cycle dispatch here; the prior
22634
22778
  // .value comparison missed multi-select array changes and combined with the
@@ -22652,6 +22796,17 @@ class AuroMenu extends AuroElement {
22652
22796
  this.initItems();
22653
22797
  }
22654
22798
 
22799
+ // Distinguish a selection-driven `value` change (a user click, which set
22800
+ // the flag in handleSelectState / _sortSelectedByDomOrder) from a
22801
+ // programmatic assignment by a consumer. A programmatic set carries no
22802
+ // positional intent, so drop any leftover `_selectedKey` and let
22803
+ // reconciliation fall back to first-by-value (single) / value-in-DOM-order
22804
+ // (multi) — the same contract selectByValue() guarantees, even when a
22805
+ // stale key would otherwise still resolve to a duplicate-value option.
22806
+ if (!valueChangeFromSelection) {
22807
+ this._selectedKey = undefined;
22808
+ }
22809
+
22655
22810
  // Set when reconciliation reassigns `value` below. That reassignment schedules a
22656
22811
  // second updated() cycle, so the `event`-attribute dispatch is deferred to that
22657
22812
  // cycle to avoid firing option custom events twice on the same selection.
@@ -22669,7 +22824,10 @@ class AuroMenu extends AuroElement {
22669
22824
  // Defensive default: `formattedValue` can be undefined for unexpected value types,
22670
22825
  // and calling `.includes` on undefined would throw during reconciliation.
22671
22826
  const valueArray = this.formattedValue || [];
22672
- const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
22827
+ // Resolve by key first (the user's exact picks), then fall back to
22828
+ // value matching for any values not resolved by key — so pre-selection
22829
+ // and programmatic value sets keep working. Result is DOM-ordered.
22830
+ const matchingOptions = resolveSelectedOptions(this.items, valueArray, this._selectedKey);
22673
22831
  newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
22674
22832
 
22675
22833
  // Reconcile `value` with the selectable set. Drop only entries whose option is
@@ -22682,6 +22840,12 @@ class AuroMenu extends AuroElement {
22682
22840
  : [];
22683
22841
  if (rejectedValues.length > 0) {
22684
22842
  const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
22843
+ // This is an internal correction, not a consumer's programmatic set,
22844
+ // so preserve the selection-driven flag through the re-entrant
22845
+ // updated() cycle it schedules. Otherwise that cycle would treat the
22846
+ // reassignment as programmatic and drop `_selectedKey` mid-cascade,
22847
+ // flipping resolution and looping.
22848
+ this._valueChangeFromSelection = true;
22685
22849
  this.value = serializeMultiSelectValue(reconciled);
22686
22850
  valueReconciled = true;
22687
22851
  }
@@ -22693,7 +22857,11 @@ class AuroMenu extends AuroElement {
22693
22857
  // `hidden` is intentionally NOT excluded: the combobox toggles
22694
22858
  // `hidden` as its type-ahead filter, so a filtered-out option is
22695
22859
  // still a valid programmatic selection.
22696
- const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
22860
+ // Prefer the option the user actually selected (tracked by
22861
+ // `_selectedKey`) so a click on the second of two options sharing a
22862
+ // `value` resolves back to that exact element instead of the first
22863
+ // value match. Falls back to first-by-value for programmatic sets.
22864
+ const matchingOption = resolveSelectedOption(this.items, this.value, this._selectedKey);
22697
22865
 
22698
22866
  if (matchingOption) {
22699
22867
  newSelected = matchingOption;
@@ -22921,6 +23089,14 @@ class AuroMenu extends AuroElement {
22921
23089
  }
22922
23090
  });
22923
23091
 
23092
+ // Assign private keys once items are populated. Only the root menu assigns
23093
+ // keys: its `items` is a deep query that already includes nested submenu
23094
+ // options, so a single pass keys the entire tree. Nested menus skip this
23095
+ // and inherit keys from the root.
23096
+ if (this.rootMenu) {
23097
+ this._assignOptionKeys();
23098
+ }
23099
+
22924
23100
  if (this.noCheckmark) {
22925
23101
  this.updateItemsState(new Map([
22926
23102
  [
@@ -22937,6 +23113,31 @@ class AuroMenu extends AuroElement {
22937
23113
  }));
22938
23114
  }
22939
23115
 
23116
+ /**
23117
+ * Assigns a private, auto-generated unique key (`_optionKey`) to each menu
23118
+ * option that does not already have one. Keys are internal state on the
23119
+ * element instance — never reflected as an attribute or exposed publicly —
23120
+ * and let selection tracking distinguish options that share the same `value`.
23121
+ *
23122
+ * The `_optionKey === undefined` guard makes this idempotent: options keep the
23123
+ * key they were first assigned across re-renders and slot changes, and if a
23124
+ * nested menu's lifecycle runs a pass before the root, options simply wait for
23125
+ * the root to key them (or keep whatever key they already hold).
23126
+ * @private
23127
+ */
23128
+ _assignOptionKeys() {
23129
+ if (!this.items) {
23130
+ return;
23131
+ }
23132
+
23133
+ this.items.forEach((option) => {
23134
+ if (option._optionKey === undefined) {
23135
+ this._optionKeyCounter += 1;
23136
+ option._optionKey = `${this._menuInstanceId}-${this._optionKeyCounter}`;
23137
+ }
23138
+ });
23139
+ }
23140
+
22940
23141
  // Logic Methods
22941
23142
 
22942
23143
  /**
@@ -22946,24 +23147,28 @@ class AuroMenu extends AuroElement {
22946
23147
  */
22947
23148
  handleSelectState(option) {
22948
23149
  if (this.multiSelect) {
22949
- const currentValue = this.formattedValue || [];
22950
23150
  const currentSelected = this.optionSelected || [];
22951
23151
 
22952
- if (!currentValue.includes(option.value)) {
22953
- this.value = serializeMultiSelectValue([
22954
- ...currentValue,
22955
- option.value
22956
- ]);
22957
- }
22958
23152
  if (!currentSelected.includes(option)) {
22959
23153
  this.optionSelected = [
22960
23154
  ...currentSelected,
22961
23155
  option
22962
23156
  ];
22963
23157
  }
23158
+
23159
+ // Re-sort by DOM order and rebuild `_selectedKey`/`value` from the
23160
+ // selected set so display order stays consistent with the menu, not with
23161
+ // click order.
23162
+ this._sortSelectedByDomOrder();
22964
23163
  } else {
22965
23164
  this.value = option.value;
22966
23165
  this.optionSelected = option;
23166
+ // Track the specific option the user selected so the value→option
23167
+ // reconciliation in updated() resolves back to this exact element even
23168
+ // when another option shares the same `value`.
23169
+ this._selectedKey = option._optionKey;
23170
+ // Mark this `value` change as selection-driven so updated() trusts the key.
23171
+ this._valueChangeFromSelection = true;
22967
23172
  }
22968
23173
 
22969
23174
  this._index = this.items.indexOf(option);
@@ -22976,18 +23181,22 @@ class AuroMenu extends AuroElement {
22976
23181
  */
22977
23182
  handleDeselectState(option) {
22978
23183
  if (this.multiSelect) {
22979
- // Remove this option from array; an empty result collapses `value` to undefined.
22980
- const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
22981
- this.value = serializeMultiSelectValue(newFormattedValue);
22982
-
22983
- this.optionSelected = this.optionSelected.filter((val) => val !== option);
23184
+ // Remove this exact element from the selection (identity, not value two
23185
+ // options can share a `value`), then rebuild `value`/`_selectedKey` from
23186
+ // the remaining set in DOM order. An empty result collapses to undefined.
23187
+ this.optionSelected = this.optionSelected.filter((selected) => selected !== option);
22984
23188
  if (this.optionSelected.length === 0) {
22985
23189
  this.optionSelected = undefined;
23190
+ this._selectedKey = undefined;
23191
+ this.value = undefined;
23192
+ } else {
23193
+ this._sortSelectedByDomOrder();
22986
23194
  }
22987
23195
  } else {
22988
23196
  // For single-select: Back to undefined when deselected
22989
23197
  this.value = undefined;
22990
23198
  this.optionSelected = undefined;
23199
+ this._selectedKey = undefined;
22991
23200
  }
22992
23201
 
22993
23202
  // Update the index tracking
@@ -23011,9 +23220,38 @@ class AuroMenu extends AuroElement {
23011
23220
  clearSelection() {
23012
23221
  this.optionSelected = undefined;
23013
23222
  this.value = undefined;
23223
+ this._selectedKey = undefined;
23014
23224
  this._index = -1;
23015
23225
  }
23016
23226
 
23227
+ /**
23228
+ * Re-sorts the multi-select selection into DOM order and rebuilds the derived
23229
+ * `_selectedKey` and `value` from `optionSelected`. Selection is always stored
23230
+ * and serialized in the order options appear in the menu, never in click
23231
+ * order — so selecting C then A yields `[A, C]`.
23232
+ * @private
23233
+ */
23234
+ _sortSelectedByDomOrder() {
23235
+ if (!this.multiSelect || !Array.isArray(this.optionSelected) || !this.items) {
23236
+ return;
23237
+ }
23238
+
23239
+ const indexMap = new Map(this.items.map((item, index) => [
23240
+ item,
23241
+ index
23242
+ ]));
23243
+
23244
+ // Sort any element no longer in `items` (a stale selection left over from a
23245
+ // dynamic rebuild that the consumer has not cleared) to the END rather than
23246
+ // the front, so it never displaces a live option to the head of the
23247
+ // serialized order. Value reconciliation drops it on the next updated() cycle.
23248
+ this.optionSelected.sort((optionA, optionB) => (indexMap.get(optionA) ?? this.items.length) - (indexMap.get(optionB) ?? this.items.length));
23249
+ this._selectedKey = this.optionSelected.map((option) => option._optionKey);
23250
+ this.value = serializeMultiSelectValue(this.optionSelected.map((option) => option.value));
23251
+ // Mark this `value` change as selection-driven so updated() trusts the keys.
23252
+ this._valueChangeFromSelection = true;
23253
+ }
23254
+
23017
23255
  /**
23018
23256
  * Resets the menu to its initial state.
23019
23257
  * This is the only way to return value to undefined.
@@ -23023,6 +23261,7 @@ class AuroMenu extends AuroElement {
23023
23261
  // Reset to undefined - initial state
23024
23262
  this.value = undefined;
23025
23263
  this.optionSelected = undefined;
23264
+ this._selectedKey = undefined;
23026
23265
  this._index = -1;
23027
23266
 
23028
23267
  // Clear active option state so a follow-up open/navigation starts fresh