@aurodesignsystem-dev/auro-formkit 0.0.0-pr1540.1 → 0.0.0-pr1540.2

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 (82) hide show
  1. package/components/checkbox/demo/accessibility.md +1 -1
  2. package/components/checkbox/demo/customize.min.js +1 -1
  3. package/components/checkbox/demo/getting-started.min.js +1 -1
  4. package/components/checkbox/demo/index.min.js +1 -1
  5. package/components/checkbox/dist/index.js +1 -1
  6. package/components/checkbox/dist/registered.js +1 -1
  7. package/components/combobox/demo/accessibility.md +1 -1
  8. package/components/combobox/demo/api.md +2 -2
  9. package/components/combobox/demo/customize.md +54 -2
  10. package/components/combobox/demo/customize.min.js +421 -240
  11. package/components/combobox/demo/getting-started.min.js +421 -240
  12. package/components/combobox/demo/index.md +1 -0
  13. package/components/combobox/demo/index.min.js +421 -240
  14. package/components/combobox/demo/keyboard-behavior.md +1 -4
  15. package/components/combobox/dist/auro-combobox.d.ts +4 -1
  16. package/components/combobox/dist/index.js +334 -222
  17. package/components/combobox/dist/registered.js +334 -222
  18. package/components/counter/demo/customize.min.js +59 -222
  19. package/components/counter/demo/index.min.js +59 -222
  20. package/components/counter/dist/auro-counter.d.ts +0 -8
  21. package/components/counter/dist/index.js +59 -222
  22. package/components/counter/dist/registered.js +59 -222
  23. package/components/datepicker/demo/accessibility.md +1 -1
  24. package/components/datepicker/demo/customize.md +30 -1
  25. package/components/datepicker/demo/customize.min.js +84 -220
  26. package/components/datepicker/demo/getting-started.md +118 -2
  27. package/components/datepicker/demo/index.min.js +84 -220
  28. package/components/datepicker/dist/index.js +84 -220
  29. package/components/datepicker/dist/registered.js +84 -220
  30. package/components/dropdown/demo/customize.html +3 -0
  31. package/components/dropdown/demo/customize.min.js +55 -206
  32. package/components/dropdown/demo/getting-started.min.js +44 -205
  33. package/components/dropdown/demo/index.min.js +44 -205
  34. package/components/dropdown/dist/index.js +44 -205
  35. package/components/dropdown/dist/registered.js +44 -205
  36. package/components/form/demo/customize.min.js +670 -945
  37. package/components/form/demo/getting-started.min.js +670 -945
  38. package/components/form/demo/index.min.js +670 -945
  39. package/components/form/demo/registerDemoDeps.min.js +670 -945
  40. package/components/input/demo/accessibility.md +1 -1
  41. package/components/input/demo/customize.min.js +1 -1
  42. package/components/input/demo/getting-started.min.js +1 -1
  43. package/components/input/demo/index.min.js +1 -1
  44. package/components/input/dist/index.js +1 -1
  45. package/components/input/dist/registered.js +1 -1
  46. package/components/menu/demo/accessibility.md +9 -4
  47. package/components/menu/demo/api.md +26 -19
  48. package/components/menu/demo/css-only.md +26 -19
  49. package/components/menu/demo/customize.md +307 -47
  50. package/components/menu/demo/design.md +1 -1
  51. package/components/menu/demo/getting-started.md +144 -7
  52. package/components/menu/demo/index.min.js +214 -19
  53. package/components/menu/demo/keyboard-behavior.md +83 -4
  54. package/components/menu/demo/voiceover.md +21 -14
  55. package/components/menu/demo/why-menu.md +8 -9
  56. package/components/menu/dist/auro-menu-utils.d.ts +19 -0
  57. package/components/menu/dist/auro-menu.d.ts +4 -8
  58. package/components/menu/dist/auro-menuoption.d.ts +12 -0
  59. package/components/menu/dist/index.js +87 -18
  60. package/components/menu/dist/registered.js +87 -18
  61. package/components/radio/demo/accessibility.md +1 -1
  62. package/components/radio/demo/customize.min.js +1 -1
  63. package/components/radio/demo/getting-started.min.js +1 -1
  64. package/components/radio/demo/index.min.js +1 -1
  65. package/components/radio/dist/index.js +1 -1
  66. package/components/radio/dist/registered.js +1 -1
  67. package/components/select/demo/accessibility.md +6 -1
  68. package/components/select/demo/api.md +2 -2
  69. package/components/select/demo/customize.md +6 -3
  70. package/components/select/demo/customize.min.js +139 -227
  71. package/components/select/demo/design.md +10 -10
  72. package/components/select/demo/getting-started.md +1 -1
  73. package/components/select/demo/getting-started.min.js +139 -227
  74. package/components/select/demo/index.md +2 -2
  75. package/components/select/demo/index.min.js +139 -227
  76. package/components/select/demo/keyboard-behavior.md +53 -58
  77. package/components/select/demo/voiceover.md +28 -15
  78. package/components/select/dist/auro-select.d.ts +6 -2
  79. package/components/select/dist/index.js +52 -209
  80. package/components/select/dist/registered.js +52 -209
  81. package/custom-elements.json +1611 -1552
  82. package/package.json +2 -2
@@ -221,6 +221,18 @@ class AuroLibraryRuntimeUtils {
221
221
  // See LICENSE in the project root for license information.
222
222
 
223
223
 
224
+ /**
225
+ * Serializes a multi-select value array back into the String `value` property.
226
+ * An empty (or missing) array collapses to `undefined` so an emptied selection
227
+ * clears `value` rather than reflecting a `"[]"` attribute.
228
+ * @private
229
+ * @param {Array<string>|undefined} values - The selected values.
230
+ * @returns {string|undefined} JSON string of the values, or undefined when empty.
231
+ */
232
+ function serializeMultiSelectValue(values) {
233
+ return values && values.length > 0 ? JSON.stringify(values) : undefined;
234
+ }
235
+
224
236
  /**
225
237
  * Validates if an option can be interacted with.
226
238
  * @private
@@ -233,6 +245,20 @@ function isOptionInteractive(option) {
233
245
  !option.hasAttribute('static');
234
246
  }
235
247
 
248
+ /**
249
+ * Validates if an option may be selected by matching a programmatic value.
250
+ * Unlike `isOptionInteractive`, `hidden` is allowed: the combobox toggles
251
+ * `hidden` as its type-ahead filter, so a filtered-out option is still a
252
+ * valid programmatic selection. Only disabled and static options — which are
253
+ * never selectable — are rejected.
254
+ * @param {HTMLElement} option - The option to check.
255
+ * @returns {boolean} True if option can be selected by value.
256
+ */
257
+ function isSelectableByValue(option) {
258
+ return !option.hasAttribute('disabled') &&
259
+ !option.hasAttribute('static');
260
+ }
261
+
236
262
  /**
237
263
  * Helper method to dispatch custom events.
238
264
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -415,22 +441,26 @@ class AuroMenu extends AuroElement {
415
441
 
416
442
  /**
417
443
  * Specifies the current active menuOption.
444
+ * @readonly
418
445
  */
419
446
  optionActive: {
420
447
  type: Object,
421
- attribute: 'optionactive'
448
+ attribute: false
422
449
  },
423
450
 
424
451
  /**
425
- * An array of currently selected menu options, type `HTMLElement` by default. In multi-select mode, `optionSelected` is an array of HTML elements.
452
+ * The currently selected menu option(s). In single-select mode this is a single `HTMLElement` (or `undefined` when nothing is selected). In multi-select mode this is an array of `HTMLElement`s.
453
+ * @readonly
426
454
  */
427
455
  optionSelected: {
428
456
  // Allow HTMLElement, HTMLElement[] arrays and undefined
429
- type: Object
457
+ type: Object,
458
+ attribute: false
430
459
  },
431
460
 
432
461
  /**
433
462
  * The value of the selected option. In multi-select mode, this is a JSON stringified array of selected option values.
463
+ * Options marked `disabled` or `static` are not selectable by value; `hidden` options remain selectable. In single-select mode, if the value matches a non-selectable option the selection is cleared (`optionSelected` becomes `undefined`) and `auroMenu-selectValueFailure` is dispatched. In multi-select mode, non-selectable entries are dropped from the value and the remaining selectable entries are selected; `auroMenu-selectValueFailure` is dispatched only when none of the entries match a selectable option.
434
464
  */
435
465
  value: {
436
466
  type: String,
@@ -554,7 +584,7 @@ class AuroMenu extends AuroElement {
554
584
  }
555
585
 
556
586
  /**
557
- * Selects options by value.
587
+ * Selects options by value. Options marked `disabled` or `static` are not selectable; `hidden` options remain selectable. In single-select mode, if the value matches a non-selectable option the selection is cleared and `auroMenu-selectValueFailure` is dispatched. In multi-select mode, non-selectable entries are dropped and the remaining selectable entries are selected; `auroMenu-selectValueFailure` is dispatched only when none of the entries match a selectable option. Passing `undefined`, `null`, an empty string, or an empty array clears the selection without dispatching a failure.
558
588
  * @param {string|string[]|undefined|null} value - The value(s) to select.
559
589
  * @public
560
590
  */
@@ -639,6 +669,11 @@ class AuroMenu extends AuroElement {
639
669
  this.initItems();
640
670
  }
641
671
 
672
+ // Set when reconciliation reassigns `value` below. That reassignment schedules a
673
+ // second updated() cycle, so the `event`-attribute dispatch is deferred to that
674
+ // cycle to avoid firing option custom events twice on the same selection.
675
+ let valueReconciled = false;
676
+
642
677
  // Handle null/undefined/empty case — empty/whitespace strings clear selection
643
678
  // consistently with selectByValue(''), and avoid downstream `.includes('')` matches.
644
679
  if (this.value === undefined || this.value === null || (typeof this.value === 'string' && this.value.trim() === '')) {
@@ -651,11 +686,31 @@ class AuroMenu extends AuroElement {
651
686
  // Defensive default: `formattedValue` can be undefined for unexpected value types,
652
687
  // and calling `.includes` on undefined would throw during reconciliation.
653
688
  const valueArray = this.formattedValue || [];
654
- const matchingOptions = this.items ? this.items.filter((item) => valueArray.includes(item.value)) : [];
689
+ const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
655
690
  newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
691
+
692
+ // Reconcile `value` with the selectable set. Drop only entries whose option is
693
+ // loaded but non-selectable (disabled/static) — leaving them would desync `value`
694
+ // from `optionSelected`, and the toggle handlers rebuild `value` from `formattedValue`,
695
+ // so the rejected entry would resurface on the next select/deselect. Entries with no
696
+ // matching item yet are preserved so async preselection still works once options render.
697
+ const rejectedValues = this.items
698
+ ? this.items.filter((item) => !isSelectableByValue(item) && valueArray.includes(item.value)).map((item) => item.value)
699
+ : [];
700
+ if (rejectedValues.length > 0) {
701
+ const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
702
+ this.value = serializeMultiSelectValue(reconciled);
703
+ valueReconciled = true;
704
+ }
656
705
  } else {
657
- // In single-select mode, this.value should be a string
658
- const matchingOption = this.items ? this.items.find((item) => item.value === this.value) : undefined;
706
+ // In single-select mode, this.value should be a string. Reject
707
+ // disabled/static options so a programmatic value pointing at a
708
+ // non-selectable option falls through to the no-match path below
709
+ // (dispatching auroMenu-selectValueFailure) instead of pinning it.
710
+ // `hidden` is intentionally NOT excluded: the combobox toggles
711
+ // `hidden` as its type-ahead filter, so a filtered-out option is
712
+ // still a valid programmatic selection.
713
+ const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
659
714
 
660
715
  if (matchingOption) {
661
716
  newSelected = matchingOption;
@@ -697,8 +752,9 @@ class AuroMenu extends AuroElement {
697
752
  ]
698
753
  ]));
699
754
 
700
- // Notify of changes
701
- if (this.optionSelected !== undefined) {
755
+ // Notify of changes. Skip when reconciliation just reassigned `value`: the
756
+ // follow-on update cycle re-runs this branch and fires the events exactly once.
757
+ if (this.optionSelected !== undefined && !valueReconciled) {
702
758
  const selected = Array.isArray(this.optionSelected) ? this.optionSelected : [this.optionSelected];
703
759
  selected.forEach((opt) => {
704
760
  if (opt.hasAttribute('event')) {
@@ -911,7 +967,7 @@ class AuroMenu extends AuroElement {
911
967
  const currentSelected = this.optionSelected || [];
912
968
 
913
969
  if (!currentValue.includes(option.value)) {
914
- this.value = JSON.stringify([
970
+ this.value = serializeMultiSelectValue([
915
971
  ...currentValue,
916
972
  option.value
917
973
  ]);
@@ -937,15 +993,9 @@ class AuroMenu extends AuroElement {
937
993
  */
938
994
  handleDeselectState(option) {
939
995
  if (this.multiSelect) {
940
- // Remove this option from array
996
+ // Remove this option from array; an empty result collapses `value` to undefined.
941
997
  const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
942
-
943
- // If array is empty after removal, set back to undefined
944
- if (newFormattedValue && newFormattedValue.length === 0) {
945
- this.value = undefined;
946
- } else {
947
- this.value = JSON.stringify(newFormattedValue);
948
- }
998
+ this.value = serializeMultiSelectValue(newFormattedValue);
949
999
 
950
1000
  this.optionSelected = this.optionSelected.filter((val) => val !== option);
951
1001
  if (this.optionSelected.length === 0) {
@@ -992,6 +1042,11 @@ class AuroMenu extends AuroElement {
992
1042
  this.optionSelected = undefined;
993
1043
  this._index = -1;
994
1044
 
1045
+ // Clear active option state so a follow-up open/navigation starts fresh
1046
+ // rather than reusing a stale reference from before the reset.
1047
+ this.items?.forEach((item) => item.classList.remove('active'));
1048
+ this.optionActive = undefined;
1049
+
995
1050
  // Reset UI state
996
1051
  this.updateItemsState(new Map([
997
1052
  [
@@ -1454,6 +1509,7 @@ let menuOptionIdCounter = 0;
1454
1509
  * The `auro-menuoption` element provides users a way to define a menu option.
1455
1510
  * @customElement auro-menuoption
1456
1511
  *
1512
+ * @attr {Boolean} static - When present, marks the option as non-interactive — it renders but is skipped during keyboard navigation and cannot be selected. Useful for section headers, informational rows inside a menu, or attaching event listeners.
1457
1513
  * @slot default - The default slot for the menu option text.
1458
1514
  *
1459
1515
  * @event auroMenuOption-mouseover - Notifies that this option has been hovered over.
@@ -1483,6 +1539,7 @@ class AuroMenuOption extends AuroElement {
1483
1539
  this.noCheckmark = false;
1484
1540
  this.disabled = false;
1485
1541
  this.noMatch = false;
1542
+ this.persistent = false;
1486
1543
 
1487
1544
  /**
1488
1545
  * @private
@@ -1504,6 +1561,12 @@ class AuroMenuOption extends AuroElement {
1504
1561
  type: Boolean,
1505
1562
  reflect: true
1506
1563
  },
1564
+
1565
+ /**
1566
+ * **Deprecated.** Use the `value` attribute on `auro-menu` to set the selected option when the menu renders (or call `menu.selectByValue(value)` programmatically). Support for the child-level `selected` attribute will be removed in a future major release.
1567
+ *
1568
+ * @deprecated Use the `value` attribute on `auro-menu` instead.
1569
+ */
1507
1570
  selected: {
1508
1571
  type: Boolean,
1509
1572
  reflect: true
@@ -1538,6 +1601,12 @@ class AuroMenuOption extends AuroElement {
1538
1601
  reflect: true,
1539
1602
  attribute: 'nomatch'
1540
1603
  },
1604
+
1605
+ /** When true, this option is excluded from `matchWord` DOM rewriting — useful for utility rows (e.g., "Add new…") that must render identically regardless of the current filter. */
1606
+ persistent: {
1607
+ type: Boolean,
1608
+ reflect: true
1609
+ },
1541
1610
  };
1542
1611
  }
1543
1612
 
@@ -1712,6 +1781,103 @@ class AuroMenuOption extends AuroElement {
1712
1781
  }
1713
1782
  }
1714
1783
 
1784
+ function auroMenuLoadingExample() {
1785
+ document.querySelector("#loadingExampleToggleButton").addEventListener("click", () => {
1786
+ document.querySelectorAll("#loadingExampleTable auro-menu").forEach(menu => menu.toggleAttribute("loading"));
1787
+ });
1788
+ }
1789
+
1790
+ function updateMatch() {
1791
+ let matchWordMenu = document.querySelector('#matchWordMenu');
1792
+
1793
+ matchWordMenu.matchWord = matchWordInput.value;
1794
+ }
1795
+
1796
+ function auroMenuMatchWordExample() {
1797
+ let matchWordInput = document.querySelector('#matchWordInput');
1798
+
1799
+ matchWordInput.addEventListener('keyup', updateMatch);
1800
+ }
1801
+
1802
+ function auroMenuResetExample() {
1803
+ const resetExampleBtnElem = document.querySelector('#resetExampleBtn');
1804
+ const resetExampleElem = document.querySelector('#resetExample');
1805
+
1806
+ if (resetExampleElem && resetExampleBtnElem) {
1807
+ resetExampleBtnElem.addEventListener('click', () => {
1808
+ resetExampleElem.reset();
1809
+ });
1810
+ }
1811
+ }
1812
+
1813
+ function auroMenuEventAttributeExample() {
1814
+ const menu = document.querySelector('#eventAttrExample');
1815
+ const output = document.querySelector('#eventAttrOutput');
1816
+
1817
+ menu.addEventListener('auroMenu-customEventFired', (evt) => {
1818
+ const eventName = evt.detail.option.getAttribute('event');
1819
+ output.textContent = `${eventName} (from "${evt.detail.option.textContent}")`;
1820
+ });
1821
+ }
1822
+
1823
+ function auroMenuEventsExample() {
1824
+ const menu = document.querySelector('#eventsExampleMenu');
1825
+ const log = document.querySelector('#eventsExampleLog');
1826
+ const resetBtn = document.querySelector('#eventsExampleReset');
1827
+ const badValueBtn = document.querySelector('#eventsExampleBadValue');
1828
+ const clearBtn = document.querySelector('#eventsExampleClearLog');
1829
+
1830
+ let entries = [];
1831
+ const write = (name, detail) => {
1832
+ const label = detail && detail.tagName ? detail.tagName.toLowerCase() : JSON.stringify(detail);
1833
+ entries.unshift(`${name}: ${label}`);
1834
+ log.textContent = entries.slice(0, 10).join('\n');
1835
+ };
1836
+
1837
+ menu.addEventListener('auroMenu-selectedOption', () => {
1838
+ write('auroMenu-selectedOption', menu.optionSelected);
1839
+ });
1840
+ menu.addEventListener('auroMenu-activatedOption', (evt) => {
1841
+ write('auroMenu-activatedOption', evt.detail);
1842
+ });
1843
+ menu.addEventListener('auroMenu-optionsChange', (evt) => {
1844
+ write('auroMenu-optionsChange', { count: (evt.detail.options || []).length });
1845
+ });
1846
+ menu.addEventListener('auroMenu-selectValueReset', () => {
1847
+ write('auroMenu-selectValueReset', { value: menu.value });
1848
+ });
1849
+ menu.addEventListener('auroMenu-selectValueFailure', (evt) => {
1850
+ write('auroMenu-selectValueFailure', evt.detail);
1851
+ });
1852
+
1853
+ resetBtn.addEventListener('click', () => menu.reset());
1854
+ badValueBtn.addEventListener('click', () => menu.selectByValue('nonexistent'));
1855
+ clearBtn.addEventListener('click', () => {
1856
+ entries = [];
1857
+ log.textContent = '(interact with the menu above)';
1858
+ });
1859
+ }
1860
+
1861
+ function auroMenuPersistentExample() {
1862
+ const input = document.querySelector('#persistentExampleInput');
1863
+ const menu = document.querySelector('#persistentExampleMenu');
1864
+
1865
+ input.addEventListener('keyup', () => {
1866
+ menu.matchWord = input.value;
1867
+ });
1868
+ }
1869
+
1870
+ function auroMenuSelectByValueExample() {
1871
+ const menu = document.querySelector('#selectByValueMenu');
1872
+ const stopsBtn = document.querySelector('#selectByValueStops');
1873
+ const durationBtn = document.querySelector('#selectByValueDuration');
1874
+ const clearBtn = document.querySelector('#selectByValueClear');
1875
+
1876
+ stopsBtn.addEventListener('click', () => menu.selectByValue('stops'));
1877
+ durationBtn.addEventListener('click', () => menu.selectByValue('duration'));
1878
+ clearBtn.addEventListener('click', () => menu.selectByValue(undefined));
1879
+ }
1880
+
1715
1881
  /* eslint-disable jsdoc/require-jsdoc, no-magic-numbers, no-param-reassign */
1716
1882
 
1717
1883
 
@@ -1720,7 +1886,36 @@ AuroMenuOption.register();
1720
1886
  AuroMenu.register('custom-menu');
1721
1887
  AuroMenuOption.register('custom-menuoption');
1722
1888
 
1723
- function initExamples(initCount) {
1889
+ const examples = [
1890
+ auroMenuLoadingExample,
1891
+ auroMenuMatchWordExample,
1892
+ auroMenuResetExample,
1893
+ auroMenuEventAttributeExample,
1894
+ auroMenuEventsExample,
1895
+ auroMenuPersistentExample,
1896
+ auroMenuSelectByValueExample
1897
+ ];
1898
+
1899
+ function initExamples(pending, initCount) {
1900
+ pending = pending || examples.slice();
1901
+ initCount = initCount || 0;
1902
+ const stillPending = [];
1903
+
1904
+ for (const fn of pending) {
1905
+ try {
1906
+ fn();
1907
+ } catch {
1908
+ stillPending.push(fn);
1909
+ }
1910
+ }
1911
+
1912
+ if (stillPending.length && initCount <= 20) {
1913
+ setTimeout(() => {
1914
+ initExamples(stillPending, initCount + 1);
1915
+ }, 100);
1916
+ }
1724
1917
  }
1725
1918
 
1919
+ initExamples();
1920
+
1726
1921
  export { initExamples };
@@ -7,11 +7,90 @@
7
7
  <auro-header level="2" id="keyEvents">Key Events</auro-header>
8
8
  <!-- AURO-GENERATED-CONTENT:START (FILE:src=./../docs/partials/keyEvents.md) -->
9
9
  <!-- The below content is automatically added from ./../docs/partials/keyEvents.md -->
10
- <!-- auro-menu does not implement direct keyboard event handling. -->
11
- <!-- Keyboard navigation is delegated to parent components (auro-select, auro-combobox) via their keyboard strategy files. -->
12
10
  <div class="note">
13
- <p><strong>Note:</strong> The <code>&lt;auro-menu&gt;</code> component does not handle keyboard events directly. Keyboard navigation is managed by the parent component (e.g., <code>&lt;auro-select&gt;</code>, <code>&lt;auro-combobox&gt;</code>) through their keyboard strategy classes, which call menu methods such as <code>navigateOptions()</code>, <code>makeSelection()</code>, and <code>updateActiveOption()</code>.</p>
14
- </div>
11
+ <p><strong>Note:</strong> <code>&lt;auro-menu&gt;</code> registers a <code>keydown</code> listener on its host. By default the menu is not focusable no <code>tabindex</code> on the host, and menuoptions are <code>tabindex="-1"</code> so the listener does not fire on its own. It fires whenever a <code>keydown</code> reaches the host, which happens in any of the following ways:</p>
12
+ <ul>
13
+ <li>A wrapping component focuses something inside the menu subtree and the event bubbles up to the host. In the standard integrations (<code>&lt;auro-select&gt;</code>, <code>&lt;auro-combobox&gt;</code>), keyboard behavior is primarily parent-managed rather than bubble-driven: focus stays on the parent's trigger/input, and the parent captures keys there and drives the menu directly via its public <code>navigateOptions()</code> / <code>updateActiveOption()</code> methods and the menu's internal <code>makeSelection()</code> routine (<code>@private</code>, not part of the public menu API).</li>
14
+ <li>A wrapping component dispatches or reroutes a <code>KeyboardEvent</code> at the menu programmatically (for example, <code>menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))</code>).</li>
15
+ <li>The author makes the host focusable (for example, by adding <code>tabindex="0"</code> to the <code>&lt;auro-menu&gt;</code> element) and focuses it. The host already has <code>role="listbox"</code>, so this yields a bare, focusable listbox that responds to the keys in the table below. For full combobox semantics use <code>&lt;auro-select&gt;</code> or <code>&lt;auro-combobox&gt;</code>, or replicate their <code>aria-activedescendant</code> wiring from a separate trigger.</li>
16
+ </ul>
17
+ <p>Keys not listed below (<code>Home</code>, <code>End</code>, <code>Escape</code>, type-ahead) are handled by the parent — see the <code>&lt;auro-select&gt;</code> and <code>&lt;auro-combobox&gt;</code> keyboard behavior docs. An "interactive option" is one that is not <code>disabled</code>, <code>hidden</code>, or <code>static</code> — these are always skipped during navigation and selection, along with <code>&lt;hr&gt;</code> dividers.</p>
18
+ </div>
19
+ <table>
20
+ <thead>
21
+ <tr>
22
+ <th>Key</th>
23
+ <th>Current State</th>
24
+ <th>Behavior</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <tr>
29
+ <td rowspan="5">ArrowDown</td>
30
+ <td>No option is active</td>
31
+ <td>The first interactive option becomes active.</td>
32
+ </tr>
33
+ <tr>
34
+ <td>An option is active, followed by interactive options</td>
35
+ <td>Advances the active option to the next interactive option.</td>
36
+ </tr>
37
+ <tr>
38
+ <td>The last interactive option is active</td>
39
+ <td>Wraps to the first interactive option.</td>
40
+ </tr>
41
+ <tr>
42
+ <td>Menu has no options or all options are non-interactive</td>
43
+ <td>No option becomes active; no <code>auroMenu-activatedOption</code> event fires.</td>
44
+ </tr>
45
+ <tr>
46
+ <td>Menu is in <code>loading</code> state</td>
47
+ <td>The rendered loading placeholder is inert (marked <code>disabled</code>) and is not part of <code>items</code>, so it cannot become active.</td>
48
+ </tr>
49
+ <tr>
50
+ <td rowspan="4">ArrowUp</td>
51
+ <td>No option is active</td>
52
+ <td>The last interactive option becomes active.</td>
53
+ </tr>
54
+ <tr>
55
+ <td>An option is active, preceded by interactive options</td>
56
+ <td>Advances the active option to the previous interactive option.</td>
57
+ </tr>
58
+ <tr>
59
+ <td>The first interactive option is active</td>
60
+ <td>Wraps to the last interactive option.</td>
61
+ </tr>
62
+ <tr>
63
+ <td>Menu has no options or all options are non-interactive</td>
64
+ <td>No option becomes active.</td>
65
+ </tr>
66
+ <tr>
67
+ <td rowspan="4">Enter</td>
68
+ <td>Single-select, active option is not currently selected</td>
69
+ <td>The active option is selected. Any previous selection is cleared. <code>auroMenu-selectedOption</code> fires.</td>
70
+ </tr>
71
+ <tr>
72
+ <td>Single-select, active option is already selected</td>
73
+ <td>No selection change, but <code>auroMenu-selectedOption</code> still fires so the parent can react (for example, close the dropdown).</td>
74
+ </tr>
75
+ <tr>
76
+ <td>Multi-select</td>
77
+ <td>The active option toggles selected. Other selections are preserved.</td>
78
+ </tr>
79
+ <tr>
80
+ <td>Active option is disabled, hidden, or static</td>
81
+ <td>Selection is a no-op.</td>
82
+ </tr>
83
+ <tr>
84
+ <td rowspan="2">Tab</td>
85
+ <td>An option is active</td>
86
+ <td>The active option is selected (same behavior as <code>Enter</code>).<div class="note"><strong>Note:</strong> <code>Tab</code> does not <code>preventDefault</code>, so focus continues to the next element in the tabindex sequence after selection.</div></td>
87
+ </tr>
88
+ <tr>
89
+ <td>No option is active</td>
90
+ <td>Focus moves to the next tabbable element; no selection change.</td>
91
+ </tr>
92
+ </tbody>
93
+ </table>
15
94
  <!-- AURO-GENERATED-CONTENT:END -->
16
95
  </div>
17
96
  </div>
@@ -1,33 +1,40 @@
1
1
  <auro-header level="1" id="overview">Menu - VoiceOver Behavior</auro-header>
2
2
  <p>This page documents the VoiceOver experience when using the <code>&lt;auro-menu&gt;</code> component. It covers announcements and interactions for menu navigation and option selection.</p>
3
+ <div class="note"><strong>Note:</strong> In the standard integrations (<code>&lt;auro-select&gt;</code>, <code>&lt;auro-combobox&gt;</code>), keyboard focus stays on the parent trigger/input — it does <strong>not</strong> move into the menu. The parent captures keys and drives the menu, and the active option is announced via <code>aria-activedescendant</code> on the trigger rather than by moving DOM focus between options. The focus-on-the-menu behavior described alongside the standalone case below only applies when an author makes <code>&lt;auro-menu&gt;</code> itself focusable (for example, <code>tabindex="0"</code>).</p>
3
4
  <auro-header level="2" id="voiceOverInteractiveAnnouncements">Interactive Announcements</auro-header>
4
5
  <auro-header level="3" id="voiceOverMenuContainer">Menu Container</auro-header>
5
- <p>When focus enters the menu, VoiceOver announces:</p>
6
+ <p>When the listbox is exposed — the parent's dropdown opens, or a standalone focusable menu receives focus — VoiceOver announces:</p>
6
7
  <ol>
7
- <li><strong>Role:</strong> <em>"list box"</em></li>
8
+ <li><strong>Role:</strong> <em>"list box"</em> — set on the root <code>&lt;auro-menu&gt;</code>; nested menus use <code>role="group"</code> with <code>aria-label="submenu"</code>.</li>
8
9
  <li><strong>Item count:</strong> The number of visible options (e.g., <em>"5 items"</em>)</li>
10
+ <li><strong>Multi-select:</strong> When <code>multiSelect</code> is set on the root menu, <code>aria-multiselectable="true"</code> is exposed so VoiceOver announces the listbox as multi-selectable.</li>
11
+ <li><strong>Loading:</strong> While <code>loading</code> is set, <code>aria-busy="true"</code> is exposed so VoiceOver signals that the option set is not yet stable.</li>
9
12
  </ol>
10
13
  <auro-header level="3" id="voiceOverMenuOptions">Menu Options</auro-header>
11
- <p>Each <code>auro-menuoption</code> announces the following when focused:</p>
14
+ <p>Each <code>auro-menuoption</code> announces the following when the parent field points at it (via <code>aria-activedescendant</code>) or when the user reads through the listbox:</p>
12
15
  <ol>
13
16
  <li><strong>Label:</strong> The option's text content</li>
14
17
  <li><strong>Role:</strong> <em>"option"</em></li>
15
- <li><strong>Position:</strong> Position in the list (e.g., <em>"3 of 5"</em>)</li>
16
- <li><strong>Selected state:</strong> <em>"selected"</em> if the option is currently selected</li>
17
- <li><strong>Disabled state:</strong> <em>"dimmed"</em> if the option is disabled</li>
18
- </ol>
18
+ <li><strong>Position:</strong> Position in the list (e.g., <em>"3 of 5"</em>) — when the menu is used inside <code>&lt;auro-select&gt;</code> (or another host that stamps them), each option receives <code>aria-setsize</code> and <code>aria-posinset</code>. Standalone <code>&lt;auro-menu&gt;</code> does not set these attributes itself; positional announcements outside a select/combobox host rely on VoiceOver's own listbox item count.</li>
19
+ <li><strong>Selected state:</strong> <em>"selected"</em> if <code>aria-selected="true"</code> is set on the option</li>
20
+ <li><strong>Disabled state:</strong> <em>"dimmed"</em> if <code>aria-disabled="true"</code> is set on the option</li>
21
+ </ol>
22
+ <div class="note"><strong>Note:</strong> <code>&lt;auro-menuoption&gt;</code> is not directly focusable — <code>tabindex="-1"</code> keeps it out of the tab order. In a select or combobox context, focus stays on the parent trigger and the active option is pointed at via <code>aria-activedescendant</code> (using <code>ariaActiveDescendantElement</code> to cross the shadow-root boundary).</div>
19
23
  <auro-header level="3" id="voiceOverSelection">Selection</auro-header>
20
- <p>When an option is selected via Enter or Space:</p>
24
+ <p>In a select or combobox context, the parent captures Enter (and Tab) on its trigger/input and commits the active option through the menu; the menu itself does not receive the key. When the selection is committed:</p>
21
25
  <ul>
22
26
  <li>VoiceOver announces the option label followed by <em>"selected"</em></li>
23
27
  <li>In multi-select mode, previously selected options remain selected and their state is preserved</li>
24
- </ul>
28
+ </ul>
29
+ <p>A standalone focusable menu (see the note at the top of this page) handles Enter and Tab on its own host, producing the same selection announcement.</p>
25
30
  <auro-header level="2" id="voiceOverWorkflow">Navigation Workflow</auro-header>
31
+ <p>In the standard select/combobox integrations, focus remains on the parent trigger/input throughout — the menu is a visually presented listbox the parent points into, not a place focus travels to:</p>
26
32
  <ol>
27
- <li><strong>Enter the menu:</strong> Focus moves into the listbox; VoiceOver announces role and item count</li>
28
- <li><strong>Navigate options:</strong> Use Arrow Down/Up keys; VoiceOver announces each option's label, role, and position</li>
29
- <li><strong>Select an option:</strong> Press Enter or Space; VoiceOver confirms the selection</li>
30
- <li><strong>Exit:</strong> Tab out of the menu or press Escape (when used within a dropdown context)</li>
31
- </ol>
33
+ <li><strong>Open the listbox:</strong> Activating the trigger (or typing in a combobox) exposes the menu; VoiceOver announces the listbox role and item count</li>
34
+ <li><strong>Navigate options:</strong> Arrow Down/Up keys pressed on the trigger move the active option; <code>aria-activedescendant</code> updates and VoiceOver announces each option's label, role, and position</li>
35
+ <li><strong>Select an option:</strong> Enter (captured by the parent) commits the active option; VoiceOver confirms the selection</li>
36
+ <li><strong>Exit:</strong> Escape and Tab are handled by the parent Escape closes the listbox, and Tab commits the active option before moving focus to the next element in the tab sequence</li>
37
+ </ol>
38
+ <p>A standalone focusable menu follows the same active-option model, except focus is on the menu host and the menu handles Arrow, Enter, and Tab directly. See the menu's key events documentation for the full standalone key table.</p>
32
39
  <auro-header level="2" id="voiceOverNestedMenus">Nested Menus</auro-header>
33
40
  <p>When a nested <code>auro-menu</code> is encountered, VoiceOver announces a new listbox context. Each nested menu maintains its own option count and position tracking.</p>
@@ -4,7 +4,7 @@
4
4
  <p>Custom list implementations often fail to provide correct ARIA semantics, keyboard navigation, or screen reader announcements.</p>
5
5
  <p><code>auro-menu</code> provides:</p>
6
6
  <ul>
7
- <li><strong>Keyboard navigation</strong> — Arrow keys move between options, Enter selects, Escape cancels, Home/End jump to the first and last options.</li>
7
+ <li><strong>Keyboard navigation</strong> — Arrow Up/Down move the active option; Enter selects it; Tab selects and moves focus out of the menu.</li>
8
8
  <li><strong>Active option tracking</strong> — The currently highlighted option is tracked programmatically, enabling <code>aria-activedescendant</code> patterns in parent components like <code>auro-select</code> and <code>auro-combobox</code>.</li>
9
9
  <li><strong>Multi-select semantics</strong> — When <code>multiSelect</code> is enabled, <code>aria-multiselectable</code> is set on the menu.</li>
10
10
  <li><strong>Loading state</strong> — <code>aria-busy</code> is set during loading, with customizable loading icon and text slots.</li>
@@ -24,15 +24,14 @@
24
24
  <ul>
25
25
  <li>Checkmark indicators for each selected option</li>
26
26
  <li>A <code>value</code> that returns a JSON-stringified array of selections</li>
27
- <li><code>allowDeselect</code> to toggle selections off</li>
28
- <li><code>selectAllMatchingOptions</code> for bulk selection operations</li>
29
- <li><code>clearSelection()</code> and <code>reset()</code> methods for programmatic control</li>
27
+ <li>Click-to-toggle behavior that deselects an already-selected option</li>
28
+ <li><code>reset()</code> method for programmatic control</li>
30
29
  </ul>
31
30
  <auro-header level="2" id="searchAndFiltering">Search and filtering</auro-header>
32
31
  <p>Native menus have no concept of filtering or highlighting matched text.</p>
33
32
  <p><code>auro-menu</code> supports:</p>
34
33
  <ul>
35
- <li><code>matchWord</code> to highlight matching portions of option text as the user types (used by <code>auro-combobox</code> for real-time filtering)</li>
34
+ <li><auro-hyperlink href="customize#matchWord"><code>matchWord</code></auro-hyperlink> to highlight matching portions of option text as the user types (used by <code>auro-combobox</code> for real-time filtering)</li>
36
35
  <li>Programmatic <code>navigateOptions(direction)</code> for keyboard-driven traversal</li>
37
36
  </ul>
38
37
  <auro-header level="2" id="loadingState">Loading state</auro-header>
@@ -88,7 +87,7 @@
88
87
  <tr>
89
88
  <td>Keyboard navigation</td>
90
89
  <td>Browser-dependent</td>
91
- <td>Full arrow/Enter/Escape/Home/End</td>
90
+ <td>Arrow Up/Down, Enter, Tab</td>
92
91
  </tr>
93
92
  <tr>
94
93
  <td>Composable primitive</td>
@@ -96,9 +95,9 @@
96
95
  <td>Foundation for select and combobox</td>
97
96
  </tr>
98
97
  <tr>
99
- <td>Deselect support</td>
100
- <td>No</td>
101
- <td><code>allowDeselect</code> attribute</td>
98
+ <td>Multi-select deselect</td>
99
+ <td>Ctrl+click list box</td>
100
+ <td>Click-to-toggle in multi-select</td>
102
101
  </tr>
103
102
  </tbody>
104
103
  </table>
@@ -10,6 +10,15 @@
10
10
  * or if the value cannot be parsed into an array from a JSON string.
11
11
  */
12
12
  export function arrayConverter(value: any): any[] | undefined;
13
+ /**
14
+ * Serializes a multi-select value array back into the String `value` property.
15
+ * An empty (or missing) array collapses to `undefined` so an emptied selection
16
+ * clears `value` rather than reflecting a `"[]"` attribute.
17
+ * @private
18
+ * @param {Array<string>|undefined} values - The selected values.
19
+ * @returns {string|undefined} JSON string of the values, or undefined when empty.
20
+ */
21
+ export function serializeMultiSelectValue(values: Array<string> | undefined): string | undefined;
13
22
  /**
14
23
  * Compare two arrays for equality.
15
24
  * @private
@@ -25,6 +34,16 @@ export function arraysAreEqual(arr1: any[], arr2: any[]): boolean;
25
34
  * @returns {boolean} True if option is interactive.
26
35
  */
27
36
  export function isOptionInteractive(option: HTMLElement): boolean;
37
+ /**
38
+ * Validates if an option may be selected by matching a programmatic value.
39
+ * Unlike `isOptionInteractive`, `hidden` is allowed: the combobox toggles
40
+ * `hidden` as its type-ahead filter, so a filtered-out option is still a
41
+ * valid programmatic selection. Only disabled and static options — which are
42
+ * never selectable — are rejected.
43
+ * @param {HTMLElement} option - The option to check.
44
+ * @returns {boolean} True if option can be selected by value.
45
+ */
46
+ export function isSelectableByValue(option: HTMLElement): boolean;
28
47
  /**
29
48
  * Helper method to dispatch custom events.
30
49
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -60,21 +60,17 @@ export class AuroMenu extends AuroElement {
60
60
  reflect: boolean;
61
61
  attribute: string;
62
62
  };
63
- /**
64
- * Specifies the current active menuOption.
65
- */
66
63
  optionActive: {
67
64
  type: ObjectConstructor;
68
- attribute: string;
65
+ attribute: boolean;
69
66
  };
70
- /**
71
- * An array of currently selected menu options, type `HTMLElement` by default. In multi-select mode, `optionSelected` is an array of HTML elements.
72
- */
73
67
  optionSelected: {
74
68
  type: ObjectConstructor;
69
+ attribute: boolean;
75
70
  };
76
71
  /**
77
72
  * The value of the selected option. In multi-select mode, this is a JSON stringified array of selected option values.
73
+ * Options marked `disabled` or `static` are not selectable by value; `hidden` options remain selectable. In single-select mode, if the value matches a non-selectable option the selection is cleared (`optionSelected` becomes `undefined`) and `auroMenu-selectValueFailure` is dispatched. In multi-select mode, non-selectable entries are dropped from the value and the remaining selectable entries are selected; `auroMenu-selectValueFailure` is dispatched only when none of the entries match a selectable option.
78
74
  */
79
75
  value: {
80
76
  type: StringConstructor;
@@ -171,7 +167,7 @@ export class AuroMenu extends AuroElement {
171
167
  */
172
168
  private get formattedValue();
173
169
  /**
174
- * Selects options by value.
170
+ * Selects options by value. Options marked `disabled` or `static` are not selectable; `hidden` options remain selectable. In single-select mode, if the value matches a non-selectable option the selection is cleared and `auroMenu-selectValueFailure` is dispatched. In multi-select mode, non-selectable entries are dropped and the remaining selectable entries are selected; `auroMenu-selectValueFailure` is dispatched only when none of the entries match a selectable option. Passing `undefined`, `null`, an empty string, or an empty array clears the selection without dispatching a failure.
175
171
  * @param {string|string[]|undefined|null} value - The value(s) to select.
176
172
  * @public
177
173
  */