@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
@@ -840,6 +840,188 @@ function navigateArrow(component, direction, options = {}) {
840
840
  }
841
841
  }
842
842
 
843
+ // Selectors for focusable elements
844
+ const FOCUSABLE_SELECTORS$1 = [
845
+ "a[href]",
846
+ "button:not([disabled])",
847
+ "textarea:not([disabled])",
848
+ "input:not([disabled])",
849
+ "select:not([disabled])",
850
+ '[role="tab"]:not([disabled])',
851
+ '[role="link"]:not([disabled])',
852
+ '[role="button"]:not([disabled])',
853
+ '[tabindex]:not([tabindex="-1"])',
854
+ '[contenteditable]:not([contenteditable="false"])',
855
+ ];
856
+
857
+ // List of custom components that are known to be focusable
858
+ const FOCUSABLE_COMPONENTS$1 = [
859
+ "auro-checkbox",
860
+ "auro-radio",
861
+ "auro-dropdown",
862
+ "auro-button",
863
+ "auro-combobox",
864
+ "auro-input",
865
+ "auro-counter",
866
+ // 'auro-menu', // Auro menu is not focusable by default, it uses a different interaction model
867
+ "auro-select",
868
+ "auro-datepicker",
869
+ "auro-hyperlink",
870
+ "auro-accordion",
871
+ ];
872
+
873
+ /**
874
+ * Determines if a given element is a custom focusable component.
875
+ * Returns true if the element matches a known focusable component and is not disabled.
876
+ *
877
+ * @param {HTMLElement} element The element to check for focusability.
878
+ * @returns {boolean} True if the element is a focusable custom component, false otherwise.
879
+ */
880
+ function isFocusableComponent$1(element) {
881
+ const componentName = element.tagName.toLowerCase();
882
+
883
+ // Guard Clause: Element is a focusable component
884
+ if (
885
+ !FOCUSABLE_COMPONENTS$1.some(
886
+ (name) => element.hasAttribute(name) || componentName === name,
887
+ )
888
+ )
889
+ return false;
890
+
891
+ // Guard Clause: Element is not disabled
892
+ if (element.hasAttribute("disabled")) return false;
893
+
894
+ // Guard Clause: The element is a hyperlink and has no href attribute
895
+ if (componentName.match("hyperlink") && !element.hasAttribute("href"))
896
+ return false;
897
+
898
+ // If all guard clauses pass, the element is a focusable component
899
+ return true;
900
+ }
901
+
902
+ /**
903
+ * Safely get a numeric tabindex for an element.
904
+ * Returns a number if the tabindex is a valid integer, otherwise null.
905
+ *
906
+ * @param {HTMLElement} element The element whose tabindex to read.
907
+ * @returns {?number} The numeric tabindex or null if missing/invalid.
908
+ */
909
+ function getNumericTabIndex$1(element) {
910
+ const raw = element.getAttribute("tabindex");
911
+ if (raw == null) return null;
912
+
913
+ const value = Number.parseInt(raw, 10);
914
+ return Number.isNaN(value) ? null : value;
915
+ }
916
+
917
+ /**
918
+ * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
919
+ * Returns a unique, ordered array of elements that can receive focus.
920
+ * Also sorts elements with tabindex first, preserving their order.
921
+ *
922
+ * @param {HTMLElement} container The container to search within
923
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
924
+ */
925
+ function getFocusableElements$1(container) {
926
+ // Get elements in DOM order by walking the tree
927
+ const orderedFocusableElements = [];
928
+
929
+ // Define a recursive function to collect focusable elements in DOM order
930
+ const collectFocusableElements = (root) => {
931
+ // Check if current element is focusable
932
+ if (root.nodeType === Node.ELEMENT_NODE) {
933
+ // Check if this is a custom component that is focusable
934
+ const isComponentFocusable = isFocusableComponent$1(root);
935
+
936
+ if (isComponentFocusable) {
937
+ // Add the component itself as a focusable element and don't traverse its shadow DOM
938
+ orderedFocusableElements.push(root);
939
+ return; // Skip traversing inside this component
940
+ }
941
+
942
+ // Check if the element itself matches any selector
943
+ for (const selector of FOCUSABLE_SELECTORS$1) {
944
+ if (root.matches?.(selector)) {
945
+ orderedFocusableElements.push(root);
946
+ break; // Once we know it's focusable, no need to check other selectors
947
+ }
948
+ }
949
+
950
+ // Process shadow DOM only for non-Auro components
951
+ if (root.shadowRoot) {
952
+ // Process shadow DOM children in order
953
+ if (root.shadowRoot.children) {
954
+ Array.from(root.shadowRoot.children).forEach((child) => {
955
+ collectFocusableElements(child);
956
+ });
957
+ }
958
+ }
959
+
960
+ // Process slots and their assigned nodes in order
961
+ if (root.tagName === "SLOT") {
962
+ const assignedNodes = root.assignedNodes({ flatten: true });
963
+ for (const node of assignedNodes) {
964
+ collectFocusableElements(node);
965
+ }
966
+ } else {
967
+ // Process light DOM children in order
968
+ if (root.children) {
969
+ Array.from(root.children).forEach((child) => {
970
+ collectFocusableElements(child);
971
+ });
972
+ }
973
+ }
974
+ }
975
+ };
976
+
977
+ // Start the traversal from the container
978
+ collectFocusableElements(container);
979
+
980
+ // Remove duplicates that might have been collected through different paths
981
+ // while preserving order
982
+ const uniqueElements = [];
983
+ const seen = new Set();
984
+
985
+ for (const element of orderedFocusableElements) {
986
+ if (!seen.has(element)) {
987
+ seen.add(element);
988
+ uniqueElements.push(element);
989
+ }
990
+ }
991
+
992
+ // Move tab-indexed elements to the front while preserving their order
993
+ // This ensures that elements with tabindex are prioritized in the focus order
994
+
995
+ // First extract elements with valid positive tabindex
996
+ const elementsWithTabindex = uniqueElements.filter((el) => {
997
+ const tabindex = getNumericTabIndex$1(el);
998
+ return tabindex !== null && tabindex > 0;
999
+ });
1000
+
1001
+ // Sort these elements by their tabindex value
1002
+ elementsWithTabindex.sort((a, b) => {
1003
+ const aIndex = getNumericTabIndex$1(a) ?? 0;
1004
+ const bIndex = getNumericTabIndex$1(b) ?? 0;
1005
+ return aIndex - bIndex;
1006
+ });
1007
+
1008
+ // Elements without tabindex (preserving their original order)
1009
+ const elementsWithoutTabindex = uniqueElements.filter((el) => {
1010
+ const tabindex = getNumericTabIndex$1(el);
1011
+
1012
+ // Elements without tabindex or with tabindex of 0 stay in DOM order
1013
+ return tabindex === null || tabindex === 0;
1014
+ });
1015
+
1016
+ // Combine both arrays with tabindex elements first
1017
+ const tabIndexedUniqueElements = [
1018
+ ...elementsWithTabindex,
1019
+ ...elementsWithoutTabindex,
1020
+ ];
1021
+
1022
+ return tabIndexedUniqueElements;
1023
+ }
1024
+
843
1025
  /* eslint-disable no-underscore-dangle */
844
1026
 
845
1027
  /**
@@ -885,6 +1067,82 @@ function reconcileMenuIndex(menu) {
885
1067
  }
886
1068
  }
887
1069
 
1070
+ /**
1071
+ * Commit the highlighted option and close the bib.
1072
+ * @param {Element} component - The auro-combobox host element.
1073
+ */
1074
+ function selectAndClose(component) {
1075
+ reconcileMenuIndex(component.menu);
1076
+ component.menu.makeSelection();
1077
+ component.hideBib();
1078
+ }
1079
+
1080
+ /**
1081
+ * Whether `el` is currently visible enough to receive focus. Prefers the
1082
+ * browser's `checkVisibility()` (Chrome/Safari; Firefox behind a flag until
1083
+ * recently); otherwise combines a display/visibility check with an
1084
+ * ancestor-hidden probe that keeps position:fixed elements visible.
1085
+ * @param {Element} el - The element to test.
1086
+ * @returns {boolean}
1087
+ */
1088
+ function isVisibleForFocus(el) {
1089
+ if (typeof el.checkVisibility === 'function') {
1090
+ return el.checkVisibility();
1091
+ }
1092
+ const style = el.ownerDocument.defaultView.getComputedStyle(el);
1093
+ if (style.display === 'none' || style.visibility === 'hidden') {
1094
+ return false;
1095
+ }
1096
+ return el.offsetParent !== null || style.position === 'fixed';
1097
+ }
1098
+
1099
+ /**
1100
+ * Return the tab stop that comes before `component` in page tab order,
1101
+ * skipping any hidden entries the walker doesn't filter itself.
1102
+ * @param {Element} component - The auro-combobox host element.
1103
+ * @returns {Element|null}
1104
+ */
1105
+ function getPreviousTabStop(component) {
1106
+ const tabStops = getFocusableElements$1(component.ownerDocument.body);
1107
+ const componentIndex = tabStops.indexOf(component);
1108
+ for (let index = componentIndex - 1; index >= 0; index -= 1) {
1109
+ if (isVisibleForFocus(tabStops[index])) {
1110
+ return tabStops[index];
1111
+ }
1112
+ }
1113
+ return null;
1114
+ }
1115
+
1116
+ /**
1117
+ * Commit the highlighted option, close the bib, and move focus to the tab
1118
+ * stop before the combobox — the Shift+Tab exit path (AB#1592239).
1119
+ * Returns true when this function moved focus and the caller should
1120
+ * preventDefault; false when no previous tab stop was found and the browser's
1121
+ * native traversal should run.
1122
+ * @param {Element} component - The auro-combobox host element.
1123
+ * @returns {boolean}
1124
+ */
1125
+ function selectAndExitBackward(component) {
1126
+ const previousTabStop = getPreviousTabStop(component);
1127
+
1128
+ // Opts this selection out of setClearBtnFocus so the focus move below
1129
+ // isn't clobbered. Consumed by auro-combobox's selection listener; also
1130
+ // cleared via microtask in case makeSelection is a no-op and the listener
1131
+ // never fires (microtasks run before the next user event, so this can't
1132
+ // leak into an unrelated selection).
1133
+ component._suppressClearBtnFocusOnSelection = true;
1134
+ selectAndClose(component);
1135
+ queueMicrotask(() => {
1136
+ component._suppressClearBtnFocusOnSelection = false;
1137
+ });
1138
+
1139
+ if (!previousTabStop) {
1140
+ return false;
1141
+ }
1142
+ doubleRaf(() => previousTabStop.focus());
1143
+ return true;
1144
+ }
1145
+
888
1146
  const comboboxKeyboardStrategy = {
889
1147
  ArrowDown(component, evt, ctx) {
890
1148
  // If the clear button has focus, let the browser handle ArrowDown normally.
@@ -984,18 +1242,17 @@ const comboboxKeyboardStrategy = {
984
1242
 
985
1243
  Tab(component, evt, ctx) {
986
1244
  if (ctx.isExpanded && !isClearBtnFocused(ctx)) {
987
- // When the clear button is focused, Tab events do not bubble out of
988
- // its shadow DOM, so this handler only fires when the clear button
989
- // is NOT focused. In that case, select the active option and close.
990
- reconcileMenuIndex(component.menu);
991
- component.menu.makeSelection();
992
- component.hideBib();
1245
+ if (evt.shiftKey) {
1246
+ if (selectAndExitBackward(component)) {
1247
+ evt.preventDefault();
1248
+ }
1249
+ return;
1250
+ }
1251
+
1252
+ selectAndClose(component);
993
1253
 
994
- // In fullscreen modal mode, closing the dialog does not
995
- // automatically restores focus to the input. In the tab case,
996
- // Explicitly move focus to the trigger's clear button so the
997
- // user can continues tabbing through the page normally.
998
- if (ctx.isModal && !evt.shiftKey) {
1254
+ if (ctx.isModal) {
1255
+ // Fullscreen close does not automatically restore focus to the input.
999
1256
  component.setClearBtnFocus();
1000
1257
  }
1001
1258
  }
@@ -3278,12 +3535,40 @@ class AuroFloatingUI {
3278
3535
  return;
3279
3536
  }
3280
3537
 
3538
+ // Chrome-specific: during popover top-layer promotion after a click on a
3539
+ // slotted focusable, :focus-within can briefly evaluate false while the
3540
+ // active element is still structurally inside the trigger/bib. Fall back
3541
+ // to a shadow-piercing ancestry walk from the deep active element before
3542
+ // treating this as a real focus loss.
3543
+ try {
3544
+ let active = document.activeElement;
3545
+ while (active && active.shadowRoot && active.shadowRoot.activeElement) {
3546
+ active = active.shadowRoot.activeElement;
3547
+ }
3548
+ const targets = [element, element.trigger, element.bib].filter(Boolean);
3549
+ let node = active;
3550
+ while (node) {
3551
+ if (targets.includes(node)) {
3552
+ return;
3553
+ }
3554
+ node =
3555
+ node.parentElement ||
3556
+ (node.getRootNode && node.getRootNode().host) ||
3557
+ null;
3558
+ }
3559
+ } catch (e) {
3560
+ // Defensive: fall through to the existing close path if traversal fails.
3561
+ }
3562
+
3281
3563
  // if fullscreen bib is in fullscreen mode, do not close
3282
3564
  if (element.bib.hasAttribute("isfullscreen")) {
3283
3565
  return;
3284
3566
  }
3285
3567
 
3286
- this.hideBib("keydown");
3568
+ // eventType "focusloss" distinguishes a Tab/click-driven close from an
3569
+ // Escape keydown close. Consumers use this to decide whether to restore
3570
+ // focus to the trigger (Escape) or let it advance naturally (Tab).
3571
+ this.hideBib("focusloss");
3287
3572
  }
3288
3573
 
3289
3574
  setupHideHandlers() {
@@ -3953,199 +4238,6 @@ function getFocusableElements(container) {
3953
4238
  return tabIndexedUniqueElements;
3954
4239
  }
3955
4240
 
3956
- /**
3957
- * FocusTrap manages keyboard focus within a specified container element, ensuring that focus does not leave the container when tabbing.
3958
- * It is commonly used for modal dialogs or overlays to improve accessibility by trapping focus within interactive UI components.
3959
- */
3960
- class FocusTrap {
3961
- /**
3962
- * Creates a new FocusTrap instance for the given container element.
3963
- * Initializes event listeners and prepares the container for focus management.
3964
- *
3965
- * @param {HTMLElement} container The DOM element to trap focus within.
3966
- * @param {boolean} [controlTabOrder=false] If true enables manual control of the tab order by the FocusTrap.
3967
- * @throws {Error} If the provided container is not a valid HTMLElement.
3968
- */
3969
- constructor(container, controlTabOrder = false) {
3970
- if (!container || !(container instanceof HTMLElement)) {
3971
- throw new Error("FocusTrap requires a valid HTMLElement.");
3972
- }
3973
-
3974
- this.container = container;
3975
- this.tabDirection = "forward"; // or 'backward';
3976
- this.controlTabOrder = controlTabOrder;
3977
-
3978
- this._init();
3979
- }
3980
-
3981
- /**
3982
- * Initializes the focus trap by setting up event listeners and attributes on the container.
3983
- * Prepares the container for focus management, including support for shadow DOM and inert attributes.
3984
- *
3985
- * @private
3986
- */
3987
- _init() {
3988
- // Add inert attribute to prevent focusing programmatically as well (if supported)
3989
- if ("inert" in HTMLElement.prototype) {
3990
- this.container.inert = false; // Ensure the container isn't inert
3991
- this.container.setAttribute("data-focus-trap-container", true); // Mark for identification
3992
- }
3993
-
3994
- // Track tab direction
3995
- this.container.addEventListener("keydown", this._onKeydown);
3996
- }
3997
-
3998
- /**
3999
- * Gets an array of currently active (focused) elements in the document and shadow DOM.
4000
- * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4001
- * @private
4002
- */
4003
- _getActiveElements() {
4004
- // Get the active element(s) in the document and shadow root
4005
- // This will include the active element in the shadow DOM if it exists
4006
- // Active element may be inside the shadow DOM depending on delegatesFocus, so we need to check both
4007
- let { activeElement } = document;
4008
- const actives = [activeElement];
4009
- while (activeElement?.shadowRoot?.activeElement) {
4010
- actives.push(activeElement.shadowRoot.activeElement);
4011
- activeElement = activeElement.shadowRoot.activeElement;
4012
- }
4013
- return actives;
4014
- }
4015
-
4016
- /**
4017
- * Gets the next focus index based on the current index and focusable elements.
4018
- * @param {number} currentIndex The current index of the focused element.
4019
- * @param {Array<HTMLElement>} focusables The array of focusable elements.
4020
- * @returns {number|null} The next focus index or null if not determined.
4021
- */
4022
- _getNextFocusIndex(currentIndex, focusables, actives) {
4023
- if (this.controlTabOrder) {
4024
- // Calculate the new index based on the current index and tab direction
4025
- let newFocusIndex =
4026
- currentIndex + (this.tabDirection === "forward" ? 1 : -1);
4027
-
4028
- // Wrap-around logic
4029
- if (newFocusIndex < 0) newFocusIndex = focusables.length - 1;
4030
- if (newFocusIndex >= focusables.length) newFocusIndex = 0;
4031
-
4032
- // Early return with the new index
4033
- return newFocusIndex;
4034
- }
4035
-
4036
- // Determine if we need to wrap
4037
- const atFirst =
4038
- actives.includes(focusables[0]) || actives.includes(this.container);
4039
- const atLast = actives.includes(focusables[focusables.length - 1]);
4040
-
4041
- // Only wrap if at the ends
4042
- if (this.tabDirection === "backward" && atFirst) {
4043
- return focusables.length - 1;
4044
- }
4045
-
4046
- if (this.tabDirection === "forward" && atLast) {
4047
- return 0;
4048
- }
4049
-
4050
- // No wrap, so don't change focus, return early
4051
- return null;
4052
- }
4053
-
4054
- /**
4055
- * Handles the Tab key press event to manage focus within the container.
4056
- * @param {KeyboardEvent} e The keyboard event triggered by the user.
4057
- * @returns {void}
4058
- */
4059
- _handleTabKey(e) {
4060
- // Update the focusable elements
4061
- const focusables = this._getFocusableElements();
4062
-
4063
- // If there are no focusable elements, exit
4064
- if (!focusables.length) return;
4065
-
4066
- // Set the tab direction based on the key pressed
4067
- this.tabDirection = e.shiftKey ? "backward" : "forward";
4068
-
4069
- // Get the active elements that are currently focused
4070
- const actives = this._getActiveElements();
4071
-
4072
- // If we're at either end of the focusable elements, wrap around to the other end
4073
- let focusIndex = focusables.findIndex((el) => actives.includes(el));
4074
-
4075
- // Fallback if we have no focused element
4076
- if (focusIndex === -1) focusIndex = 0;
4077
-
4078
- // Get the next focus index based on the current focus index, tab direction, and controlTabOrder setting
4079
- // Is null if no new focus index is determined
4080
- const newFocusIndex = this._getNextFocusIndex(
4081
- focusIndex,
4082
- focusables,
4083
- actives,
4084
- );
4085
-
4086
- // If we have a new focus index, set focus to that element
4087
- if (newFocusIndex !== null) {
4088
- e.preventDefault();
4089
- focusables[newFocusIndex].focus();
4090
- }
4091
- }
4092
-
4093
- /**
4094
- * Catches the keydown event
4095
- * @param {KeyboardEvent} e The keyboard event triggered by user interaction.
4096
- * @private
4097
- */
4098
- _onKeydown = (e) => {
4099
- // Handle tab
4100
- if (e.key === "Tab") this._handleTabKey(e);
4101
- };
4102
-
4103
- /**
4104
- * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
4105
- * Returns a unique, ordered array of elements that can receive focus.
4106
- *
4107
- * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4108
- * @private
4109
- */
4110
- _getFocusableElements() {
4111
- // Use the imported utility function to get focusable elements
4112
- const elements = getFocusableElements(this.container);
4113
-
4114
- // Return the elements found
4115
- return elements;
4116
- }
4117
-
4118
- /**
4119
- * Moves focus to the first focusable element within the container.
4120
- * Useful for setting initial focus when activating the focus trap.
4121
- */
4122
- focusFirstElement() {
4123
- const focusables = this._getFocusableElements();
4124
- if (focusables.length) focusables[0].focus();
4125
- }
4126
-
4127
- /**
4128
- * Moves focus to the last focusable element within the container.
4129
- * Useful for setting focus when deactivating or cycling focus in reverse.
4130
- */
4131
- focusLastElement() {
4132
- const focusables = this._getFocusableElements();
4133
- if (focusables.length) focusables[focusables.length - 1].focus();
4134
- }
4135
-
4136
- /**
4137
- * Removes event listeners and attributes added by the focus trap.
4138
- * Call this method to clean up when the focus trap is no longer needed.
4139
- */
4140
- disconnect() {
4141
- if (this.container.hasAttribute("data-focus-trap-container")) {
4142
- this.container.removeAttribute("data-focus-trap-container");
4143
- }
4144
-
4145
- this.container.removeEventListener("keydown", this._onKeydown);
4146
- }
4147
- }
4148
-
4149
4241
  // Copyright (c) Alaska Air. All right reserved. Licensed under the Apache-2.0 license
4150
4242
  // See LICENSE in the project root for license information.
4151
4243
 
@@ -4643,7 +4735,7 @@ class AuroDropdownBib extends i$3 {
4643
4735
  classes[`shape-${this.shape}`] = true;
4644
4736
 
4645
4737
  return u$7`
4646
- <dialog class="${e$2(classes)}" part="bibContainer" role="${o(this.dialogRole)}" aria-labelledby="${o(this.dialogLabel ? 'dialogLabel' : undefined)}">
4738
+ <dialog tabindex="-1" class="${e$2(classes)}" part="bibContainer" role="${o(this.dialogRole)}" aria-labelledby="${o(this.dialogLabel ? 'dialogLabel' : undefined)}">
4647
4739
  ${this.dialogLabel ? u$7`<span id="dialogLabel" class="util_displayHiddenVisually">${this.dialogLabel}</span>` : ''}
4648
4740
  <slot></slot>
4649
4741
  <span id="srAnnouncement" class="util_displayHiddenVisually" aria-live="polite" role="status"></span>
@@ -4894,7 +4986,7 @@ let AuroHelpText$2 = class AuroHelpText extends i$3 {
4894
4986
  }
4895
4987
  };
4896
4988
 
4897
- var formkitVersion$2 = '202607072039';
4989
+ var formkitVersion$2 = '202607102214';
4898
4990
 
4899
4991
  let AuroElement$2 = class AuroElement extends i$3 {
4900
4992
  static get properties() {
@@ -5648,7 +5740,10 @@ class AuroDropdown extends AuroElement$2 {
5648
5740
  }
5649
5741
 
5650
5742
 
5651
- if (!this.isPopoverVisible) {
5743
+ // On Tab-driven close (eventType "focusloss"), let focus advance naturally
5744
+ // — restoring to the trigger would trap the user on this dropdown, forcing
5745
+ // an extra Tab to move on. Escape and outside-click still restore.
5746
+ if (!this.isPopoverVisible && eventType !== "focusloss") {
5652
5747
  // wait til the bib gets fully closed and rendered
5653
5748
  setTimeout(() => {
5654
5749
  // Skip if the bib re-opened, or if focus moved intentionally outside the dropdown (not to body).
@@ -5878,15 +5973,16 @@ class AuroDropdown extends AuroElement$2 {
5878
5973
  }
5879
5974
  });
5880
5975
  } else {
5881
- // Normal desktop: use FocusTrap on the bib element.
5882
- // Defer focusFirstElement to the next frame because the popover
5883
- // is positioned asynchronously by Floating UI — calling focus()
5884
- // synchronously here would target elements with zero dimensions
5885
- // and the focus call would be silently ignored.
5886
- this.focusTrap = new FocusTrap(this.bibContent);
5976
+ // Normal desktop (non-modal): move initial focus into the bib but
5977
+ // don't trap Tab. Tab should exit the bib and let the floater's
5978
+ // handleFocusLoss close it, matching native <select>/<details>
5979
+ // behavior. Deferred one frame because Floating UI positions the
5980
+ // popover asynchronously a synchronous focus() would target
5981
+ // zero-dimension elements and be silently ignored.
5887
5982
  requestAnimationFrame(() => {
5888
- if (this.focusTrap) {
5889
- this.focusTrap.focusFirstElement();
5983
+ const focusables = getFocusableElements(this.bibContent);
5984
+ if (focusables.length) {
5985
+ focusables[0].focus();
5890
5986
  }
5891
5987
  });
5892
5988
  }
@@ -18519,7 +18615,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18519
18615
  }
18520
18616
  };
18521
18617
 
18522
- var formkitVersion$1 = '202607072039';
18618
+ var formkitVersion$1 = '202607102214';
18523
18619
 
18524
18620
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18525
18621
  // See LICENSE in the project root for license information.
@@ -19640,7 +19736,7 @@ class AuroBibtemplate extends i$3 {
19640
19736
  }
19641
19737
  }
19642
19738
 
19643
- var formkitVersion = '202607072039';
19739
+ var formkitVersion = '202607102214';
19644
19740
 
19645
19741
  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}`;
19646
19742
 
@@ -20423,6 +20519,8 @@ class AuroCombobox extends AuroElement {
20423
20519
 
20424
20520
  /**
20425
20521
  * If set, the `icon` attribute will be applied to the trigger `auro-input` element.
20522
+ * Icon rendering is currently limited to `type="credit-card"`. Setting `triggerIcon`
20523
+ * without a supported `type` propagates the attribute but does not render a visible icon.
20426
20524
  */
20427
20525
  triggerIcon: {
20428
20526
  type: Boolean,
@@ -20454,7 +20552,7 @@ class AuroCombobox extends AuroElement {
20454
20552
  },
20455
20553
 
20456
20554
  /**
20457
- * Value selected for the dropdown menu.
20555
+ * Value selected for the dropdown menu. When set programmatically or as a preset attribute, the value must match a selectable option. If it matches an option marked `disabled` or `static`, the selection is rejected: `value` and `optionSelected` are cleared to `undefined`. `hidden` options (including those filtered out by type-ahead) remain selectable by value.
20458
20556
  */
20459
20557
  value: {
20460
20558
  type: String
@@ -20950,7 +21048,12 @@ class AuroCombobox extends AuroElement {
20950
21048
  this.dropdown.bibDialogLabel = labelElement.textContent.trim() || undefined;
20951
21049
  }
20952
21050
 
20953
- // Exposes the CSS parts from the bibtemplate for styling
21051
+ if (this.customBibWidth) {
21052
+ this.dropdown.dropdownWidth = this.customBibWidth;
21053
+ }
21054
+
21055
+ // Exposes the CSS parts from the dropdown and bibtemplate for styling
21056
+ this.dropdown.exposeCssParts();
20954
21057
  this.bibtemplate.exposeCssParts();
20955
21058
 
20956
21059
  this.hideBib = this.hideBib.bind(this);
@@ -21170,8 +21273,11 @@ class AuroCombobox extends AuroElement {
21170
21273
  // do not close while typing in suggestion mode with no value selected, to allow freeform input
21171
21274
  this.hideBib();
21172
21275
 
21173
- // Move focus to the clear button when the user makes a selection.
21174
- if (!isEcho && this.menu.value !== undefined) {
21276
+ // Move focus to the clear button when the user makes a selection,
21277
+ // unless the Shift+Tab handler opted this selection out.
21278
+ if (this._suppressClearBtnFocusOnSelection) {
21279
+ this._suppressClearBtnFocusOnSelection = false;
21280
+ } else if (!isEcho && this.menu.value !== undefined) {
21175
21281
  this.setClearBtnFocus();
21176
21282
  }
21177
21283
 
@@ -21577,6 +21683,12 @@ class AuroCombobox extends AuroElement {
21577
21683
  this.value = undefined;
21578
21684
  this.typedValue = undefined;
21579
21685
  this.input.value = undefined;
21686
+ // Fullscreen bib mounts a second auro-input that holds the user-facing
21687
+ // native <input> on mobile; clearing only the trigger leaves stale text
21688
+ // there when the bib is reopened after reset.
21689
+ if (this.inputInBib) {
21690
+ this.inputInBib.value = undefined;
21691
+ }
21580
21692
  this.menu.value = undefined;
21581
21693
  this.validation.reset(this);
21582
21694
  this.touched = false;
@@ -21972,6 +22084,18 @@ var tokensCss = i$6`:host{--ds-auro-menu-divider-color: var(--ds-basic-color-bor
21972
22084
  // See LICENSE in the project root for license information.
21973
22085
 
21974
22086
 
22087
+ /**
22088
+ * Serializes a multi-select value array back into the String `value` property.
22089
+ * An empty (or missing) array collapses to `undefined` so an emptied selection
22090
+ * clears `value` rather than reflecting a `"[]"` attribute.
22091
+ * @private
22092
+ * @param {Array<string>|undefined} values - The selected values.
22093
+ * @returns {string|undefined} JSON string of the values, or undefined when empty.
22094
+ */
22095
+ function serializeMultiSelectValue(values) {
22096
+ return values && values.length > 0 ? JSON.stringify(values) : undefined;
22097
+ }
22098
+
21975
22099
  /**
21976
22100
  * Validates if an option can be interacted with.
21977
22101
  * @private
@@ -21984,6 +22108,20 @@ function isOptionInteractive(option) {
21984
22108
  !option.hasAttribute('static');
21985
22109
  }
21986
22110
 
22111
+ /**
22112
+ * Validates if an option may be selected by matching a programmatic value.
22113
+ * Unlike `isOptionInteractive`, `hidden` is allowed: the combobox toggles
22114
+ * `hidden` as its type-ahead filter, so a filtered-out option is still a
22115
+ * valid programmatic selection. Only disabled and static options — which are
22116
+ * never selectable — are rejected.
22117
+ * @param {HTMLElement} option - The option to check.
22118
+ * @returns {boolean} True if option can be selected by value.
22119
+ */
22120
+ function isSelectableByValue(option) {
22121
+ return !option.hasAttribute('disabled') &&
22122
+ !option.hasAttribute('static');
22123
+ }
22124
+
21987
22125
  /**
21988
22126
  * Helper method to dispatch custom events.
21989
22127
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -22153,22 +22291,26 @@ class AuroMenu extends AuroElement {
22153
22291
 
22154
22292
  /**
22155
22293
  * Specifies the current active menuOption.
22294
+ * @readonly
22156
22295
  */
22157
22296
  optionActive: {
22158
22297
  type: Object,
22159
- attribute: 'optionactive'
22298
+ attribute: false
22160
22299
  },
22161
22300
 
22162
22301
  /**
22163
- * An array of currently selected menu options, type `HTMLElement` by default. In multi-select mode, `optionSelected` is an array of HTML elements.
22302
+ * 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.
22303
+ * @readonly
22164
22304
  */
22165
22305
  optionSelected: {
22166
22306
  // Allow HTMLElement, HTMLElement[] arrays and undefined
22167
- type: Object
22307
+ type: Object,
22308
+ attribute: false
22168
22309
  },
22169
22310
 
22170
22311
  /**
22171
22312
  * The value of the selected option. In multi-select mode, this is a JSON stringified array of selected option values.
22313
+ * 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.
22172
22314
  */
22173
22315
  value: {
22174
22316
  type: String,
@@ -22292,7 +22434,7 @@ class AuroMenu extends AuroElement {
22292
22434
  }
22293
22435
 
22294
22436
  /**
22295
- * Selects options by value.
22437
+ * 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.
22296
22438
  * @param {string|string[]|undefined|null} value - The value(s) to select.
22297
22439
  * @public
22298
22440
  */
@@ -22377,6 +22519,11 @@ class AuroMenu extends AuroElement {
22377
22519
  this.initItems();
22378
22520
  }
22379
22521
 
22522
+ // Set when reconciliation reassigns `value` below. That reassignment schedules a
22523
+ // second updated() cycle, so the `event`-attribute dispatch is deferred to that
22524
+ // cycle to avoid firing option custom events twice on the same selection.
22525
+ let valueReconciled = false;
22526
+
22380
22527
  // Handle null/undefined/empty case — empty/whitespace strings clear selection
22381
22528
  // consistently with selectByValue(''), and avoid downstream `.includes('')` matches.
22382
22529
  if (this.value === undefined || this.value === null || (typeof this.value === 'string' && this.value.trim() === '')) {
@@ -22389,11 +22536,31 @@ class AuroMenu extends AuroElement {
22389
22536
  // Defensive default: `formattedValue` can be undefined for unexpected value types,
22390
22537
  // and calling `.includes` on undefined would throw during reconciliation.
22391
22538
  const valueArray = this.formattedValue || [];
22392
- const matchingOptions = this.items ? this.items.filter((item) => valueArray.includes(item.value)) : [];
22539
+ const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
22393
22540
  newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
22541
+
22542
+ // Reconcile `value` with the selectable set. Drop only entries whose option is
22543
+ // loaded but non-selectable (disabled/static) — leaving them would desync `value`
22544
+ // from `optionSelected`, and the toggle handlers rebuild `value` from `formattedValue`,
22545
+ // so the rejected entry would resurface on the next select/deselect. Entries with no
22546
+ // matching item yet are preserved so async preselection still works once options render.
22547
+ const rejectedValues = this.items
22548
+ ? this.items.filter((item) => !isSelectableByValue(item) && valueArray.includes(item.value)).map((item) => item.value)
22549
+ : [];
22550
+ if (rejectedValues.length > 0) {
22551
+ const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
22552
+ this.value = serializeMultiSelectValue(reconciled);
22553
+ valueReconciled = true;
22554
+ }
22394
22555
  } else {
22395
- // In single-select mode, this.value should be a string
22396
- const matchingOption = this.items ? this.items.find((item) => item.value === this.value) : undefined;
22556
+ // In single-select mode, this.value should be a string. Reject
22557
+ // disabled/static options so a programmatic value pointing at a
22558
+ // non-selectable option falls through to the no-match path below
22559
+ // (dispatching auroMenu-selectValueFailure) instead of pinning it.
22560
+ // `hidden` is intentionally NOT excluded: the combobox toggles
22561
+ // `hidden` as its type-ahead filter, so a filtered-out option is
22562
+ // still a valid programmatic selection.
22563
+ const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
22397
22564
 
22398
22565
  if (matchingOption) {
22399
22566
  newSelected = matchingOption;
@@ -22435,8 +22602,9 @@ class AuroMenu extends AuroElement {
22435
22602
  ]
22436
22603
  ]));
22437
22604
 
22438
- // Notify of changes
22439
- if (this.optionSelected !== undefined) {
22605
+ // Notify of changes. Skip when reconciliation just reassigned `value`: the
22606
+ // follow-on update cycle re-runs this branch and fires the events exactly once.
22607
+ if (this.optionSelected !== undefined && !valueReconciled) {
22440
22608
  const selected = Array.isArray(this.optionSelected) ? this.optionSelected : [this.optionSelected];
22441
22609
  selected.forEach((opt) => {
22442
22610
  if (opt.hasAttribute('event')) {
@@ -22649,7 +22817,7 @@ class AuroMenu extends AuroElement {
22649
22817
  const currentSelected = this.optionSelected || [];
22650
22818
 
22651
22819
  if (!currentValue.includes(option.value)) {
22652
- this.value = JSON.stringify([
22820
+ this.value = serializeMultiSelectValue([
22653
22821
  ...currentValue,
22654
22822
  option.value
22655
22823
  ]);
@@ -22675,15 +22843,9 @@ class AuroMenu extends AuroElement {
22675
22843
  */
22676
22844
  handleDeselectState(option) {
22677
22845
  if (this.multiSelect) {
22678
- // Remove this option from array
22846
+ // Remove this option from array; an empty result collapses `value` to undefined.
22679
22847
  const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
22680
-
22681
- // If array is empty after removal, set back to undefined
22682
- if (newFormattedValue && newFormattedValue.length === 0) {
22683
- this.value = undefined;
22684
- } else {
22685
- this.value = JSON.stringify(newFormattedValue);
22686
- }
22848
+ this.value = serializeMultiSelectValue(newFormattedValue);
22687
22849
 
22688
22850
  this.optionSelected = this.optionSelected.filter((val) => val !== option);
22689
22851
  if (this.optionSelected.length === 0) {
@@ -22730,6 +22892,11 @@ class AuroMenu extends AuroElement {
22730
22892
  this.optionSelected = undefined;
22731
22893
  this._index = -1;
22732
22894
 
22895
+ // Clear active option state so a follow-up open/navigation starts fresh
22896
+ // rather than reusing a stale reference from before the reset.
22897
+ this.items?.forEach((item) => item.classList.remove('active'));
22898
+ this.optionActive = undefined;
22899
+
22733
22900
  // Reset UI state
22734
22901
  this.updateItemsState(new Map([
22735
22902
  [
@@ -23139,6 +23306,7 @@ let menuOptionIdCounter = 0;
23139
23306
  * The `auro-menuoption` element provides users a way to define a menu option.
23140
23307
  * @customElement auro-menuoption
23141
23308
  *
23309
+ * @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.
23142
23310
  * @slot default - The default slot for the menu option text.
23143
23311
  *
23144
23312
  * @event auroMenuOption-mouseover - Notifies that this option has been hovered over.
@@ -23168,6 +23336,7 @@ class AuroMenuOption extends AuroElement {
23168
23336
  this.noCheckmark = false;
23169
23337
  this.disabled = false;
23170
23338
  this.noMatch = false;
23339
+ this.persistent = false;
23171
23340
 
23172
23341
  /**
23173
23342
  * @private
@@ -23189,6 +23358,12 @@ class AuroMenuOption extends AuroElement {
23189
23358
  type: Boolean,
23190
23359
  reflect: true
23191
23360
  },
23361
+
23362
+ /**
23363
+ * **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.
23364
+ *
23365
+ * @deprecated Use the `value` attribute on `auro-menu` instead.
23366
+ */
23192
23367
  selected: {
23193
23368
  type: Boolean,
23194
23369
  reflect: true
@@ -23223,6 +23398,12 @@ class AuroMenuOption extends AuroElement {
23223
23398
  reflect: true,
23224
23399
  attribute: 'nomatch'
23225
23400
  },
23401
+
23402
+ /** 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. */
23403
+ persistent: {
23404
+ type: Boolean,
23405
+ reflect: true
23406
+ },
23226
23407
  };
23227
23408
  }
23228
23409