@aurodesignsystem-dev/auro-formkit 0.0.0-pr1576.8 → 0.0.0-pr1577.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 (37) hide show
  1. package/components/combobox/demo/customize.md +0 -40
  2. package/components/combobox/demo/customize.min.js +62 -365
  3. package/components/combobox/demo/getting-started.min.js +62 -365
  4. package/components/combobox/demo/index.min.js +62 -365
  5. package/components/combobox/dist/index.js +35 -53
  6. package/components/combobox/dist/registered.js +35 -53
  7. package/components/counter/demo/customize.min.js +9 -0
  8. package/components/counter/demo/index.min.js +9 -0
  9. package/components/counter/dist/index.js +9 -0
  10. package/components/counter/dist/registered.js +9 -0
  11. package/components/datepicker/demo/customize.min.js +9 -0
  12. package/components/datepicker/demo/index.min.js +9 -0
  13. package/components/datepicker/dist/index.js +9 -0
  14. package/components/datepicker/dist/registered.js +9 -0
  15. package/components/dropdown/demo/customize.min.js +9 -0
  16. package/components/dropdown/demo/getting-started.min.js +9 -0
  17. package/components/dropdown/demo/index.min.js +9 -0
  18. package/components/dropdown/dist/index.js +9 -0
  19. package/components/dropdown/dist/registered.js +9 -0
  20. package/components/form/demo/customize.min.js +89 -365
  21. package/components/form/demo/getting-started.min.js +89 -365
  22. package/components/form/demo/index.min.js +89 -365
  23. package/components/form/demo/registerDemoDeps.min.js +89 -365
  24. package/components/menu/demo/customize.md +0 -50
  25. package/components/menu/demo/index.min.js +27 -312
  26. package/components/menu/dist/auro-menu-utils.d.ts +0 -30
  27. package/components/menu/dist/auro-menu.d.ts +0 -23
  28. package/components/menu/dist/index.js +27 -312
  29. package/components/menu/dist/registered.js +27 -312
  30. package/components/select/demo/customize.md +0 -72
  31. package/components/select/demo/customize.min.js +36 -312
  32. package/components/select/demo/getting-started.min.js +36 -312
  33. package/components/select/demo/index.min.js +36 -312
  34. package/components/select/dist/index.js +9 -0
  35. package/components/select/dist/registered.js +9 -0
  36. package/custom-elements.json +1510 -1613
  37. package/package.json +1 -1
@@ -29,7 +29,6 @@
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>
33
32
  </auro-nav>
34
33
  </nav>
35
34
  <div class="mainContent">
@@ -1297,45 +1296,6 @@
1297
1296
  &lt;/auro-combobox&gt;</code></pre>
1298
1297
  <!-- AURO-GENERATED-CONTENT:END -->
1299
1298
  </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>
1339
1299
  </section>
1340
1300
  </div>
1341
1301
  </div>
@@ -5679,6 +5679,15 @@ class AuroDropdown extends AuroElement$2 {
5679
5679
 
5680
5680
  connectedCallback() {
5681
5681
  super.connectedCallback();
5682
+ // Re-register trigger event listeners removed by floater.disconnect() in
5683
+ // disconnectedCallback(). Fires when a parent element (e.g. auro-drawer)
5684
+ // moves this element to a new DOM location. firstUpdated() won't re-run,
5685
+ // so without this the trigger click/keydown handlers are permanently gone.
5686
+ // Guard on floater.element so this is a no-op on the initial connect
5687
+ // (before firstUpdated() has called configure()).
5688
+ if (this.floater?.element) {
5689
+ this.floater.configure(this, 'auroDropdown', !this.disableKeyboardHandling);
5690
+ }
5682
5691
  }
5683
5692
 
5684
5693
  disconnectedCallback() {
@@ -21628,17 +21637,7 @@ class AuroCombobox extends AuroElement {
21628
21637
  return;
21629
21638
  }
21630
21639
 
21631
- // Skip this write during a display-value sync: that path sets the input to
21632
- // the selected option's LABEL, which is not its machine value. When an
21633
- // option's label differs from its value (and especially when several
21634
- // options share a value but render distinct labels — AB#1602086), writing
21635
- // the label back into this.value clobbers the machine value the selection
21636
- // listener just set (this.value = optionSelected.value), collapsing the
21637
- // selection. _syncingBibValue still tracks (that mirrors user-typed text
21638
- // from the fullscreen bib, where value should follow the input).
21639
- if (!this._syncingDisplayValue) {
21640
- this.value = this.input.value;
21641
- }
21640
+ this.value = this.input.value;
21642
21641
 
21643
21642
  // Ignore re-entrant input events caused by programmatic value sets.
21644
21643
  if (this._syncingBibValue || this._syncingDisplayValue) {
@@ -21902,50 +21901,33 @@ class AuroCombobox extends AuroElement {
21902
21901
  }
21903
21902
 
21904
21903
  if (this.input.value !== this.value) {
21905
- // A fresh user selection leaves the input showing the option's LABEL
21906
- // while this.value holds its machine value, so input.value !== this.value
21907
- // is expected whenever label ≠ value. In that case the menu's current
21908
- // selection is authoritative and when several options share a value it
21909
- // is the ONLY thing that records WHICH same-value option the user picked
21910
- // (the menu tracks it by element identity via `_selectedKey`). Clearing
21911
- // it here drops that key; the ensuing value-only re-resolution then
21912
- // collapses the selection onto the first same-value option (AB#1602086).
21913
- // So only treat the divergence as a stale menu when the selected option
21914
- // does NOT match the new value.
21915
- const menuSelectionMatchesValue =
21916
- this.menu.optionSelected &&
21917
- !Array.isArray(this.menu.optionSelected) &&
21918
- this.menu.optionSelected.value === this.value;
21919
-
21920
- if (!menuSelectionMatchesValue) {
21921
- // Clear menu.value AND menu.optionSelected together. Clearing only
21922
- // menu.value leaves the previously-selected option element pinned
21923
- // as menu.optionSelected; a later auroMenu-selectedOption event
21924
- // would then write its stale .value back into combobox.value
21925
- // (e.g. Tab-after-Backspace re-selecting the prior option).
21926
- if (this.menu.value || this.menu.optionSelected) {
21927
- this.menu.clearSelection();
21928
- }
21904
+ // Clear menu.value AND menu.optionSelected together. Clearing only
21905
+ // menu.value leaves the previously-selected option element pinned
21906
+ // as menu.optionSelected; a later auroMenu-selectedOption event
21907
+ // would then write its stale .value back into combobox.value
21908
+ // (e.g. Tab-after-Backspace re-selecting the prior option).
21909
+ if (this.menu.value || this.menu.optionSelected) {
21910
+ this.menu.clearSelection();
21911
+ }
21929
21912
 
21930
- if (!this.persistInput) {
21931
- this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21932
- }
21913
+ if (!this.persistInput) {
21914
+ this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21915
+ }
21933
21916
 
21934
- // Programmatic value with no matching option: updateFilter will close
21935
- // the bib silently (see line 648 — no noMatchOption + 0 results
21936
- // hides). Announce so screen-reader users hear the request was
21937
- // dropped. Gated on `input.value !== this.value` so this never fires
21938
- // for user typing — that path always reconciles input.value to
21939
- // this.value before updated() runs.
21940
- if (
21941
- this.value &&
21942
- this.menu &&
21943
- this.menu.options &&
21944
- this.menu.options.length > 0 &&
21945
- !this.menu.options.some((opt) => opt.value === this.value)
21946
- ) {
21947
- announceToScreenReader(this._getAnnouncementRoot(), `No matching option for ${this.value}`);
21948
- }
21917
+ // Programmatic value with no matching option: updateFilter will close
21918
+ // the bib silently (see line 648 — no noMatchOption + 0 results
21919
+ // hides). Announce so screen-reader users hear the request was
21920
+ // dropped. Gated on `input.value !== this.value` so this never fires
21921
+ // for user typing — that path always reconciles input.value to
21922
+ // this.value before updated() runs.
21923
+ if (
21924
+ this.value &&
21925
+ this.menu &&
21926
+ this.menu.options &&
21927
+ this.menu.options.length > 0 &&
21928
+ !this.menu.options.some((opt) => opt.value === this.value)
21929
+ ) {
21930
+ announceToScreenReader(this._getAnnouncementRoot(), `No matching option for ${this.value}`);
21949
21931
  }
21950
21932
  }
21951
21933
 
@@ -22282,116 +22264,6 @@ function isSelectableByValue(option) {
22282
22264
  !option.hasAttribute('static');
22283
22265
  }
22284
22266
 
22285
- /* eslint-disable no-underscore-dangle */
22286
- /**
22287
- * Resolves the single selected option for a given `value`, preferring the
22288
- * option tracked by `selectedKey` (a user-initiated selection) over a
22289
- * first-by-value match. When multiple options share the same `value`, matching
22290
- * by `value` alone cannot distinguish which one the user picked; the key
22291
- * disambiguates it.
22292
- *
22293
- * The key is trusted only when it still resolves to an option whose `value`
22294
- * matches the requested `value`. If the key is stale (option removed) or the
22295
- * value was changed programmatically, resolution falls back to value matching —
22296
- * preserving backward-compatible behavior for preselection and `selectByValue`.
22297
- * @private
22298
- * @param {Array<HTMLElement>} items - The menu's flat option list.
22299
- * @param {string} value - The value to resolve.
22300
- * @param {string|undefined} selectedKey - The `_optionKey` of the user-selected option, if any.
22301
- * @returns {HTMLElement|undefined} The resolved option, or undefined when none match.
22302
- */
22303
- function resolveSelectedOption(items, value, selectedKey) {
22304
- if (!items) {
22305
- return undefined;
22306
- }
22307
-
22308
- if (selectedKey !== undefined) {
22309
- const keyed = items.find((item) => item._optionKey === selectedKey);
22310
- if (keyed && isSelectableByValue(keyed) && keyed.value === value) {
22311
- return keyed;
22312
- }
22313
- // Key exists but the option is gone or its value no longer matches — fall
22314
- // through to value-based matching.
22315
- }
22316
-
22317
- return items.find((item) => isSelectableByValue(item) && item.value === value);
22318
- }
22319
-
22320
- /**
22321
- * Resolves the selected options for a multi-select `value` array, preferring
22322
- * options tracked by `selectedKeys` (user-initiated selections) and falling
22323
- * back to value matching for any values not resolved by key. The result is
22324
- * always sorted into DOM order regardless of selection sequence.
22325
- * @private
22326
- * @param {Array<HTMLElement>} items - The menu's flat option list.
22327
- * @param {Array<string>} valueArray - The selected values.
22328
- * @param {Array<string>|undefined} selectedKeys - The `_optionKey`s of the user-selected options, if any.
22329
- * @returns {Array<HTMLElement>} The resolved options in DOM order.
22330
- */
22331
- function resolveSelectedOptions(items, valueArray, selectedKeys) {
22332
- if (!items) {
22333
- return [];
22334
- }
22335
-
22336
- const resolved = [];
22337
- // Mirror `resolved` as a Set for O(1) membership checks below, matching the
22338
- // indexMap optimization used for the sort rather than scanning `resolved`
22339
- // on every candidate.
22340
- const resolvedSet = new Set();
22341
-
22342
- // Track how many of each value are still available to resolve. A value that
22343
- // appears N times in `valueArray` may be satisfied at most N times total across
22344
- // the key pass and the value fallback below — matching by count, not presence,
22345
- // on BOTH passes. This stops a duplicate value from being over-resolved: e.g.
22346
- // two keyed options that both carry `SEA` cannot both match a single requested
22347
- // `SEA` (which happens when `value` is set directly without clearing
22348
- // `_selectedKey`, so more keys survive than the value set now asks for).
22349
- const remaining = new Map();
22350
- valueArray.forEach((val) => remaining.set(val, (remaining.get(val) || 0) + 1));
22351
-
22352
- // Resolve by key first: trust a key only when its option is still selectable
22353
- // and there is still an unmatched occurrence of its value in the request set.
22354
- if (Array.isArray(selectedKeys)) {
22355
- selectedKeys.forEach((key) => {
22356
- const keyed = items.find((item) => item._optionKey === key);
22357
- if (keyed && isSelectableByValue(keyed) && (remaining.get(keyed.value) || 0) > 0 && !resolvedSet.has(keyed)) {
22358
- resolved.push(keyed);
22359
- resolvedSet.add(keyed);
22360
- remaining.set(keyed.value, remaining.get(keyed.value) - 1);
22361
- }
22362
- });
22363
- }
22364
-
22365
- // Fall back to value matching for the occurrences not resolved by key. Iterate
22366
- // the leftover per-value counts so a value that appears twice but was only
22367
- // resolved once by key still matches its remaining occurrence(s).
22368
- remaining.forEach((count, val) => {
22369
- for (let occurrence = 0; occurrence < count; occurrence += 1) {
22370
- const option = items.find((item) => isSelectableByValue(item) && item.value === val && !resolvedSet.has(item));
22371
- if (option) {
22372
- resolved.push(option);
22373
- resolvedSet.add(option);
22374
- }
22375
- }
22376
- });
22377
-
22378
- // Always return in DOM order so display is consistent regardless of the order
22379
- // keys/values were selected. Every resolved option came from `items`, so an
22380
- // O(1) index lookup mirrors `_sortSelectedByDomOrder` and avoids the O(n)
22381
- // `items.indexOf` per comparison for large combobox option sets. Any element
22382
- // not in `items` (a stale snapshot from a future caller) sorts to the END via
22383
- // `?? items.length`, matching `_sortSelectedByDomOrder` and avoiding NaN
22384
- // comparisons.
22385
- const indexMap = new Map(items.map((item, index) => [
22386
- item,
22387
- index
22388
- ]));
22389
- resolved.sort((optionA, optionB) => (indexMap.get(optionA) ?? items.length) - (indexMap.get(optionB) ?? items.length));
22390
-
22391
- return resolved;
22392
- }
22393
- /* eslint-enable no-underscore-dangle */
22394
-
22395
22267
  /**
22396
22268
  * Helper method to dispatch custom events.
22397
22269
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -22417,14 +22289,6 @@ function dispatchMenuEvent(element, eventName, detail = null) {
22417
22289
  // See LICENSE in the project root for license information.
22418
22290
 
22419
22291
 
22420
- /**
22421
- * Monotonically increasing counter used to give each menu instance a unique
22422
- * `_menuInstanceId` prefix. Auto-generating the id (rather than using a random
22423
- * string) keeps option keys deterministic and collision-free across menus.
22424
- * @private
22425
- */
22426
- let menuInstanceIdCounter = 0;
22427
-
22428
22292
 
22429
22293
  /**
22430
22294
  * The `auro-menu` element provides users a way to select from a list of options.
@@ -22496,8 +22360,9 @@ class AuroMenu extends AuroElement {
22496
22360
 
22497
22361
  // Instance properties (non-reactive)
22498
22362
 
22499
- menuInstanceIdCounter += 1;
22500
-
22363
+ /**
22364
+ * @private
22365
+ */
22501
22366
  Object.assign(this, {
22502
22367
  // Root-level menu (true) or a nested submenu (false)
22503
22368
  rootMenu: true,
@@ -22507,21 +22372,6 @@ class AuroMenu extends AuroElement {
22507
22372
  nestingSpacer: '<span class="nestingSpacer"></span>',
22508
22373
  // Loading indicator for slot elements
22509
22374
  loadingSlots: null,
22510
- // Unique id for this menu instance; prefixes every auto-generated option
22511
- // key so keys never collide across menus in the same document.
22512
- _menuInstanceId: `menu-${menuInstanceIdCounter}`,
22513
- // Monotonically increasing counter for option key generation. Never
22514
- // resets, so a key is never reused within this instance's lifetime.
22515
- _optionKeyCounter: 0,
22516
- // Key(s) of the option(s) the user has actively selected. A single string
22517
- // in single-select, an array in multi-select, undefined when nothing is
22518
- // user-selected. Used to disambiguate options that share a `value`.
22519
- _selectedKey: undefined,
22520
- // True only for the one updated() cycle following a user selection, so
22521
- // reconciliation trusts `_selectedKey`. A `value` change from a consumer's
22522
- // direct property assignment leaves this false, dropping the stale key so
22523
- // reconciliation falls back to first-by-value (see updated()).
22524
- _valueChangeFromSelection: false,
22525
22375
  });
22526
22376
  }
22527
22377
 
@@ -22741,13 +22591,6 @@ class AuroMenu extends AuroElement {
22741
22591
  return;
22742
22592
  }
22743
22593
 
22744
- // A programmatic value set carries no positional intent, so drop any
22745
- // `_selectedKey` left over from a prior user click. This makes reconciliation
22746
- // in updated() fall back to first-by-value (single) / value-in-DOM-order
22747
- // (multi), matching the documented contract for programmatic selection even
22748
- // when a stale key would still resolve to a duplicate-value option.
22749
- this._selectedKey = undefined;
22750
-
22751
22594
  // `value` is a String property; stringify arrays so attribute reflection and `formattedValue` parsing stay correct.
22752
22595
  this.value = Array.isArray(value) ? JSON.stringify(value) : value;
22753
22596
  }
@@ -22795,17 +22638,6 @@ class AuroMenu extends AuroElement {
22795
22638
  updated(changedProperties) {
22796
22639
  super.updated(changedProperties);
22797
22640
 
22798
- // Consume the selection-driven flag for THIS cycle up front. Clearing it
22799
- // unconditionally — not only inside the `value` branch below — prevents it
22800
- // from lingering `true` when a selection produces a serialized `value`
22801
- // byte-identical to the current one, in which case Lit schedules no
22802
- // `value`-change cycle to consume it. A lingering flag would misclassify a
22803
- // later consumer's programmatic `value` set as selection-driven and keep a
22804
- // stale `_selectedKey`. The reconcile path below re-sets the instance flag
22805
- // after this point, so its intentional cross-cycle hand-off still works.
22806
- const valueChangeFromSelection = this._valueChangeFromSelection;
22807
- this._valueChangeFromSelection = false;
22808
-
22809
22641
  // Single source of truth for 'auroMenu-selectedOption'. Selection handlers
22810
22642
  // mutate optionSelected and let Lit's update cycle dispatch here; the prior
22811
22643
  // .value comparison missed multi-select array changes and combined with the
@@ -22829,17 +22661,6 @@ class AuroMenu extends AuroElement {
22829
22661
  this.initItems();
22830
22662
  }
22831
22663
 
22832
- // Distinguish a selection-driven `value` change (a user click, which set
22833
- // the flag in handleSelectState / _sortSelectedByDomOrder) from a
22834
- // programmatic assignment by a consumer. A programmatic set carries no
22835
- // positional intent, so drop any leftover `_selectedKey` and let
22836
- // reconciliation fall back to first-by-value (single) / value-in-DOM-order
22837
- // (multi) — the same contract selectByValue() guarantees, even when a
22838
- // stale key would otherwise still resolve to a duplicate-value option.
22839
- if (!valueChangeFromSelection) {
22840
- this._selectedKey = undefined;
22841
- }
22842
-
22843
22664
  // Set when reconciliation reassigns `value` below. That reassignment schedules a
22844
22665
  // second updated() cycle, so the `event`-attribute dispatch is deferred to that
22845
22666
  // cycle to avoid firing option custom events twice on the same selection.
@@ -22857,55 +22678,19 @@ class AuroMenu extends AuroElement {
22857
22678
  // Defensive default: `formattedValue` can be undefined for unexpected value types,
22858
22679
  // and calling `.includes` on undefined would throw during reconciliation.
22859
22680
  const valueArray = this.formattedValue || [];
22860
- // Resolve by key first (the user's exact picks), then fall back to
22861
- // value matching for any values not resolved by key — so pre-selection
22862
- // and programmatic value sets keep working. Result is DOM-ordered.
22863
- const matchingOptions = resolveSelectedOptions(this.items, valueArray, this._selectedKey);
22681
+ const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
22864
22682
  newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
22865
22683
 
22866
- // Reconcile `value` with the selectable set. An occurrence is dropped
22867
- // only when it is loaded but no selectable option can satisfy it
22868
- // every loaded item sharing that value is non-selectable, or the value
22869
- // recurs more often than it has selectable options (a duplicate value
22870
- // whose extra siblings are disabled/static). This is count-based, not
22871
- // presence-based, so an enabled option is kept even when a disabled
22872
- // sibling shares its value mirroring how `resolveSelectedOptions`
22873
- // resolves the same set. Entries with no matching item yet are
22874
- // preserved so async preselection still works, and the toggle handlers
22875
- // rebuild `value` from `formattedValue`, so a rejected entry cannot
22876
- // resurface on the next select/deselect.
22877
- const selectableByValue = new Map();
22878
- const loadedValues = new Set();
22879
- if (this.items) {
22880
- this.items.forEach((item) => {
22881
- loadedValues.add(item.value);
22882
- if (isSelectableByValue(item)) {
22883
- selectableByValue.set(item.value, (selectableByValue.get(item.value) || 0) + 1);
22884
- }
22885
- });
22886
- }
22887
-
22888
- const reconciled = valueArray.filter((val) => {
22889
- // Not loaded yet (async preselection) — keep for a later cycle.
22890
- if (!loadedValues.has(val)) {
22891
- return true;
22892
- }
22893
- // Consume one selectable option per occurrence; drop once exhausted.
22894
- const remaining = selectableByValue.get(val) || 0;
22895
- if (remaining > 0) {
22896
- selectableByValue.set(val, remaining - 1);
22897
- return true;
22898
- }
22899
- return false;
22900
- });
22901
-
22902
- if (reconciled.length !== valueArray.length) {
22903
- // This is an internal correction, not a consumer's programmatic set,
22904
- // so preserve the selection-driven flag through the re-entrant
22905
- // updated() cycle it schedules. Otherwise that cycle would treat the
22906
- // reassignment as programmatic and drop `_selectedKey` mid-cascade,
22907
- // flipping resolution and looping.
22908
- this._valueChangeFromSelection = true;
22684
+ // Reconcile `value` with the selectable set. Drop only entries whose option is
22685
+ // loaded but non-selectable (disabled/static) leaving them would desync `value`
22686
+ // from `optionSelected`, and the toggle handlers rebuild `value` from `formattedValue`,
22687
+ // so the rejected entry would resurface on the next select/deselect. Entries with no
22688
+ // matching item yet are preserved so async preselection still works once options render.
22689
+ const rejectedValues = this.items
22690
+ ? this.items.filter((item) => !isSelectableByValue(item) && valueArray.includes(item.value)).map((item) => item.value)
22691
+ : [];
22692
+ if (rejectedValues.length > 0) {
22693
+ const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
22909
22694
  this.value = serializeMultiSelectValue(reconciled);
22910
22695
  valueReconciled = true;
22911
22696
  }
@@ -22917,11 +22702,7 @@ class AuroMenu extends AuroElement {
22917
22702
  // `hidden` is intentionally NOT excluded: the combobox toggles
22918
22703
  // `hidden` as its type-ahead filter, so a filtered-out option is
22919
22704
  // still a valid programmatic selection.
22920
- // Prefer the option the user actually selected (tracked by
22921
- // `_selectedKey`) so a click on the second of two options sharing a
22922
- // `value` resolves back to that exact element instead of the first
22923
- // value match. Falls back to first-by-value for programmatic sets.
22924
- const matchingOption = resolveSelectedOption(this.items, this.value, this._selectedKey);
22705
+ const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
22925
22706
 
22926
22707
  if (matchingOption) {
22927
22708
  newSelected = matchingOption;
@@ -23149,14 +22930,6 @@ class AuroMenu extends AuroElement {
23149
22930
  }
23150
22931
  });
23151
22932
 
23152
- // Assign private keys once items are populated. Only the root menu assigns
23153
- // keys: its `items` is a deep query that already includes nested submenu
23154
- // options, so a single pass keys the entire tree. Nested menus skip this
23155
- // and inherit keys from the root.
23156
- if (this.rootMenu) {
23157
- this._assignOptionKeys();
23158
- }
23159
-
23160
22933
  if (this.noCheckmark) {
23161
22934
  this.updateItemsState(new Map([
23162
22935
  [
@@ -23173,31 +22946,6 @@ class AuroMenu extends AuroElement {
23173
22946
  }));
23174
22947
  }
23175
22948
 
23176
- /**
23177
- * Assigns a private, auto-generated unique key (`_optionKey`) to each menu
23178
- * option that does not already have one. Keys are internal state on the
23179
- * element instance — never reflected as an attribute or exposed publicly —
23180
- * and let selection tracking distinguish options that share the same `value`.
23181
- *
23182
- * The `_optionKey === undefined` guard makes this idempotent: options keep the
23183
- * key they were first assigned across re-renders and slot changes, and if a
23184
- * nested menu's lifecycle runs a pass before the root, options simply wait for
23185
- * the root to key them (or keep whatever key they already hold).
23186
- * @private
23187
- */
23188
- _assignOptionKeys() {
23189
- if (!this.items) {
23190
- return;
23191
- }
23192
-
23193
- this.items.forEach((option) => {
23194
- if (option._optionKey === undefined) {
23195
- this._optionKeyCounter += 1;
23196
- option._optionKey = `${this._menuInstanceId}-${this._optionKeyCounter}`;
23197
- }
23198
- });
23199
- }
23200
-
23201
22949
  // Logic Methods
23202
22950
 
23203
22951
  /**
@@ -23207,28 +22955,24 @@ class AuroMenu extends AuroElement {
23207
22955
  */
23208
22956
  handleSelectState(option) {
23209
22957
  if (this.multiSelect) {
22958
+ const currentValue = this.formattedValue || [];
23210
22959
  const currentSelected = this.optionSelected || [];
23211
22960
 
22961
+ if (!currentValue.includes(option.value)) {
22962
+ this.value = serializeMultiSelectValue([
22963
+ ...currentValue,
22964
+ option.value
22965
+ ]);
22966
+ }
23212
22967
  if (!currentSelected.includes(option)) {
23213
22968
  this.optionSelected = [
23214
22969
  ...currentSelected,
23215
22970
  option
23216
22971
  ];
23217
22972
  }
23218
-
23219
- // Re-sort by DOM order and rebuild `_selectedKey`/`value` from the
23220
- // selected set so display order stays consistent with the menu, not with
23221
- // click order.
23222
- this._sortSelectedByDomOrder();
23223
22973
  } else {
23224
22974
  this.value = option.value;
23225
22975
  this.optionSelected = option;
23226
- // Track the specific option the user selected so the value→option
23227
- // reconciliation in updated() resolves back to this exact element even
23228
- // when another option shares the same `value`.
23229
- this._selectedKey = option._optionKey;
23230
- // Mark this `value` change as selection-driven so updated() trusts the key.
23231
- this._valueChangeFromSelection = true;
23232
22976
  }
23233
22977
 
23234
22978
  this._index = this.items.indexOf(option);
@@ -23241,22 +22985,18 @@ class AuroMenu extends AuroElement {
23241
22985
  */
23242
22986
  handleDeselectState(option) {
23243
22987
  if (this.multiSelect) {
23244
- // Remove this exact element from the selection (identity, not value two
23245
- // options can share a `value`), then rebuild `value`/`_selectedKey` from
23246
- // the remaining set in DOM order. An empty result collapses to undefined.
23247
- this.optionSelected = this.optionSelected.filter((selected) => selected !== option);
22988
+ // Remove this option from array; an empty result collapses `value` to undefined.
22989
+ const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
22990
+ this.value = serializeMultiSelectValue(newFormattedValue);
22991
+
22992
+ this.optionSelected = this.optionSelected.filter((val) => val !== option);
23248
22993
  if (this.optionSelected.length === 0) {
23249
22994
  this.optionSelected = undefined;
23250
- this._selectedKey = undefined;
23251
- this.value = undefined;
23252
- } else {
23253
- this._sortSelectedByDomOrder();
23254
22995
  }
23255
22996
  } else {
23256
22997
  // For single-select: Back to undefined when deselected
23257
22998
  this.value = undefined;
23258
22999
  this.optionSelected = undefined;
23259
- this._selectedKey = undefined;
23260
23000
  }
23261
23001
 
23262
23002
  // Update the index tracking
@@ -23280,38 +23020,9 @@ class AuroMenu extends AuroElement {
23280
23020
  clearSelection() {
23281
23021
  this.optionSelected = undefined;
23282
23022
  this.value = undefined;
23283
- this._selectedKey = undefined;
23284
23023
  this._index = -1;
23285
23024
  }
23286
23025
 
23287
- /**
23288
- * Re-sorts the multi-select selection into DOM order and rebuilds the derived
23289
- * `_selectedKey` and `value` from `optionSelected`. Selection is always stored
23290
- * and serialized in the order options appear in the menu, never in click
23291
- * order — so selecting C then A yields `[A, C]`.
23292
- * @private
23293
- */
23294
- _sortSelectedByDomOrder() {
23295
- if (!this.multiSelect || !Array.isArray(this.optionSelected) || !this.items) {
23296
- return;
23297
- }
23298
-
23299
- const indexMap = new Map(this.items.map((item, index) => [
23300
- item,
23301
- index
23302
- ]));
23303
-
23304
- // Sort any element no longer in `items` (a stale selection left over from a
23305
- // dynamic rebuild that the consumer has not cleared) to the END rather than
23306
- // the front, so it never displaces a live option to the head of the
23307
- // serialized order. Value reconciliation drops it on the next updated() cycle.
23308
- this.optionSelected.sort((optionA, optionB) => (indexMap.get(optionA) ?? this.items.length) - (indexMap.get(optionB) ?? this.items.length));
23309
- this._selectedKey = this.optionSelected.map((option) => option._optionKey);
23310
- this.value = serializeMultiSelectValue(this.optionSelected.map((option) => option.value));
23311
- // Mark this `value` change as selection-driven so updated() trusts the keys.
23312
- this._valueChangeFromSelection = true;
23313
- }
23314
-
23315
23026
  /**
23316
23027
  * Resets the menu to its initial state.
23317
23028
  * This is the only way to return value to undefined.
@@ -23321,7 +23032,6 @@ class AuroMenu extends AuroElement {
23321
23032
  // Reset to undefined - initial state
23322
23033
  this.value = undefined;
23323
23034
  this.optionSelected = undefined;
23324
- this._selectedKey = undefined;
23325
23035
  this._index = -1;
23326
23036
 
23327
23037
  // Clear active option state so a follow-up open/navigation starts fresh
@@ -23383,19 +23093,6 @@ class AuroMenu extends AuroElement {
23383
23093
  this.initItems();
23384
23094
  }
23385
23095
 
23386
- // Recover `_index` from the highlighted option when it has been reset to -1.
23387
- // In multi-select, deselecting the last remaining option collapses the value
23388
- // to undefined, and the updated() reconciliation resets `_index = -1` even
23389
- // though `optionActive` still points at the highlighted option. Without this,
23390
- // reading `items[-1]` returns undefined and the re-select no-ops until the
23391
- // highlight is moved away and back. Mirrors auro-combobox's reconcileMenuIndex.
23392
- if (this._index < 0 && this.optionActive && this.items) {
23393
- const activeIndex = this.items.indexOf(this.optionActive);
23394
- if (activeIndex >= 0) {
23395
- this._index = activeIndex;
23396
- }
23397
- }
23398
-
23399
23096
  // Get currently selected menu option based on index
23400
23097
  const option = this.items ? this.items[this._index] : undefined;
23401
23098