@aurodesignsystem-dev/auro-formkit 0.0.0-pr1540.0 → 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
@@ -825,6 +825,188 @@ function navigateArrow(component, direction, options = {}) {
825
825
  }
826
826
  }
827
827
 
828
+ // Selectors for focusable elements
829
+ const FOCUSABLE_SELECTORS$1 = [
830
+ "a[href]",
831
+ "button:not([disabled])",
832
+ "textarea:not([disabled])",
833
+ "input:not([disabled])",
834
+ "select:not([disabled])",
835
+ '[role="tab"]:not([disabled])',
836
+ '[role="link"]:not([disabled])',
837
+ '[role="button"]:not([disabled])',
838
+ '[tabindex]:not([tabindex="-1"])',
839
+ '[contenteditable]:not([contenteditable="false"])',
840
+ ];
841
+
842
+ // List of custom components that are known to be focusable
843
+ const FOCUSABLE_COMPONENTS$1 = [
844
+ "auro-checkbox",
845
+ "auro-radio",
846
+ "auro-dropdown",
847
+ "auro-button",
848
+ "auro-combobox",
849
+ "auro-input",
850
+ "auro-counter",
851
+ // 'auro-menu', // Auro menu is not focusable by default, it uses a different interaction model
852
+ "auro-select",
853
+ "auro-datepicker",
854
+ "auro-hyperlink",
855
+ "auro-accordion",
856
+ ];
857
+
858
+ /**
859
+ * Determines if a given element is a custom focusable component.
860
+ * Returns true if the element matches a known focusable component and is not disabled.
861
+ *
862
+ * @param {HTMLElement} element The element to check for focusability.
863
+ * @returns {boolean} True if the element is a focusable custom component, false otherwise.
864
+ */
865
+ function isFocusableComponent$1(element) {
866
+ const componentName = element.tagName.toLowerCase();
867
+
868
+ // Guard Clause: Element is a focusable component
869
+ if (
870
+ !FOCUSABLE_COMPONENTS$1.some(
871
+ (name) => element.hasAttribute(name) || componentName === name,
872
+ )
873
+ )
874
+ return false;
875
+
876
+ // Guard Clause: Element is not disabled
877
+ if (element.hasAttribute("disabled")) return false;
878
+
879
+ // Guard Clause: The element is a hyperlink and has no href attribute
880
+ if (componentName.match("hyperlink") && !element.hasAttribute("href"))
881
+ return false;
882
+
883
+ // If all guard clauses pass, the element is a focusable component
884
+ return true;
885
+ }
886
+
887
+ /**
888
+ * Safely get a numeric tabindex for an element.
889
+ * Returns a number if the tabindex is a valid integer, otherwise null.
890
+ *
891
+ * @param {HTMLElement} element The element whose tabindex to read.
892
+ * @returns {?number} The numeric tabindex or null if missing/invalid.
893
+ */
894
+ function getNumericTabIndex$1(element) {
895
+ const raw = element.getAttribute("tabindex");
896
+ if (raw == null) return null;
897
+
898
+ const value = Number.parseInt(raw, 10);
899
+ return Number.isNaN(value) ? null : value;
900
+ }
901
+
902
+ /**
903
+ * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
904
+ * Returns a unique, ordered array of elements that can receive focus.
905
+ * Also sorts elements with tabindex first, preserving their order.
906
+ *
907
+ * @param {HTMLElement} container The container to search within
908
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
909
+ */
910
+ function getFocusableElements$1(container) {
911
+ // Get elements in DOM order by walking the tree
912
+ const orderedFocusableElements = [];
913
+
914
+ // Define a recursive function to collect focusable elements in DOM order
915
+ const collectFocusableElements = (root) => {
916
+ // Check if current element is focusable
917
+ if (root.nodeType === Node.ELEMENT_NODE) {
918
+ // Check if this is a custom component that is focusable
919
+ const isComponentFocusable = isFocusableComponent$1(root);
920
+
921
+ if (isComponentFocusable) {
922
+ // Add the component itself as a focusable element and don't traverse its shadow DOM
923
+ orderedFocusableElements.push(root);
924
+ return; // Skip traversing inside this component
925
+ }
926
+
927
+ // Check if the element itself matches any selector
928
+ for (const selector of FOCUSABLE_SELECTORS$1) {
929
+ if (root.matches?.(selector)) {
930
+ orderedFocusableElements.push(root);
931
+ break; // Once we know it's focusable, no need to check other selectors
932
+ }
933
+ }
934
+
935
+ // Process shadow DOM only for non-Auro components
936
+ if (root.shadowRoot) {
937
+ // Process shadow DOM children in order
938
+ if (root.shadowRoot.children) {
939
+ Array.from(root.shadowRoot.children).forEach((child) => {
940
+ collectFocusableElements(child);
941
+ });
942
+ }
943
+ }
944
+
945
+ // Process slots and their assigned nodes in order
946
+ if (root.tagName === "SLOT") {
947
+ const assignedNodes = root.assignedNodes({ flatten: true });
948
+ for (const node of assignedNodes) {
949
+ collectFocusableElements(node);
950
+ }
951
+ } else {
952
+ // Process light DOM children in order
953
+ if (root.children) {
954
+ Array.from(root.children).forEach((child) => {
955
+ collectFocusableElements(child);
956
+ });
957
+ }
958
+ }
959
+ }
960
+ };
961
+
962
+ // Start the traversal from the container
963
+ collectFocusableElements(container);
964
+
965
+ // Remove duplicates that might have been collected through different paths
966
+ // while preserving order
967
+ const uniqueElements = [];
968
+ const seen = new Set();
969
+
970
+ for (const element of orderedFocusableElements) {
971
+ if (!seen.has(element)) {
972
+ seen.add(element);
973
+ uniqueElements.push(element);
974
+ }
975
+ }
976
+
977
+ // Move tab-indexed elements to the front while preserving their order
978
+ // This ensures that elements with tabindex are prioritized in the focus order
979
+
980
+ // First extract elements with valid positive tabindex
981
+ const elementsWithTabindex = uniqueElements.filter((el) => {
982
+ const tabindex = getNumericTabIndex$1(el);
983
+ return tabindex !== null && tabindex > 0;
984
+ });
985
+
986
+ // Sort these elements by their tabindex value
987
+ elementsWithTabindex.sort((a, b) => {
988
+ const aIndex = getNumericTabIndex$1(a) ?? 0;
989
+ const bIndex = getNumericTabIndex$1(b) ?? 0;
990
+ return aIndex - bIndex;
991
+ });
992
+
993
+ // Elements without tabindex (preserving their original order)
994
+ const elementsWithoutTabindex = uniqueElements.filter((el) => {
995
+ const tabindex = getNumericTabIndex$1(el);
996
+
997
+ // Elements without tabindex or with tabindex of 0 stay in DOM order
998
+ return tabindex === null || tabindex === 0;
999
+ });
1000
+
1001
+ // Combine both arrays with tabindex elements first
1002
+ const tabIndexedUniqueElements = [
1003
+ ...elementsWithTabindex,
1004
+ ...elementsWithoutTabindex,
1005
+ ];
1006
+
1007
+ return tabIndexedUniqueElements;
1008
+ }
1009
+
828
1010
  /* eslint-disable no-underscore-dangle */
829
1011
 
830
1012
  /**
@@ -870,6 +1052,82 @@ function reconcileMenuIndex(menu) {
870
1052
  }
871
1053
  }
872
1054
 
1055
+ /**
1056
+ * Commit the highlighted option and close the bib.
1057
+ * @param {Element} component - The auro-combobox host element.
1058
+ */
1059
+ function selectAndClose(component) {
1060
+ reconcileMenuIndex(component.menu);
1061
+ component.menu.makeSelection();
1062
+ component.hideBib();
1063
+ }
1064
+
1065
+ /**
1066
+ * Whether `el` is currently visible enough to receive focus. Prefers the
1067
+ * browser's `checkVisibility()` (Chrome/Safari; Firefox behind a flag until
1068
+ * recently); otherwise combines a display/visibility check with an
1069
+ * ancestor-hidden probe that keeps position:fixed elements visible.
1070
+ * @param {Element} el - The element to test.
1071
+ * @returns {boolean}
1072
+ */
1073
+ function isVisibleForFocus(el) {
1074
+ if (typeof el.checkVisibility === 'function') {
1075
+ return el.checkVisibility();
1076
+ }
1077
+ const style = el.ownerDocument.defaultView.getComputedStyle(el);
1078
+ if (style.display === 'none' || style.visibility === 'hidden') {
1079
+ return false;
1080
+ }
1081
+ return el.offsetParent !== null || style.position === 'fixed';
1082
+ }
1083
+
1084
+ /**
1085
+ * Return the tab stop that comes before `component` in page tab order,
1086
+ * skipping any hidden entries the walker doesn't filter itself.
1087
+ * @param {Element} component - The auro-combobox host element.
1088
+ * @returns {Element|null}
1089
+ */
1090
+ function getPreviousTabStop(component) {
1091
+ const tabStops = getFocusableElements$1(component.ownerDocument.body);
1092
+ const componentIndex = tabStops.indexOf(component);
1093
+ for (let index = componentIndex - 1; index >= 0; index -= 1) {
1094
+ if (isVisibleForFocus(tabStops[index])) {
1095
+ return tabStops[index];
1096
+ }
1097
+ }
1098
+ return null;
1099
+ }
1100
+
1101
+ /**
1102
+ * Commit the highlighted option, close the bib, and move focus to the tab
1103
+ * stop before the combobox — the Shift+Tab exit path (AB#1592239).
1104
+ * Returns true when this function moved focus and the caller should
1105
+ * preventDefault; false when no previous tab stop was found and the browser's
1106
+ * native traversal should run.
1107
+ * @param {Element} component - The auro-combobox host element.
1108
+ * @returns {boolean}
1109
+ */
1110
+ function selectAndExitBackward(component) {
1111
+ const previousTabStop = getPreviousTabStop(component);
1112
+
1113
+ // Opts this selection out of setClearBtnFocus so the focus move below
1114
+ // isn't clobbered. Consumed by auro-combobox's selection listener; also
1115
+ // cleared via microtask in case makeSelection is a no-op and the listener
1116
+ // never fires (microtasks run before the next user event, so this can't
1117
+ // leak into an unrelated selection).
1118
+ component._suppressClearBtnFocusOnSelection = true;
1119
+ selectAndClose(component);
1120
+ queueMicrotask(() => {
1121
+ component._suppressClearBtnFocusOnSelection = false;
1122
+ });
1123
+
1124
+ if (!previousTabStop) {
1125
+ return false;
1126
+ }
1127
+ doubleRaf(() => previousTabStop.focus());
1128
+ return true;
1129
+ }
1130
+
873
1131
  const comboboxKeyboardStrategy = {
874
1132
  ArrowDown(component, evt, ctx) {
875
1133
  // If the clear button has focus, let the browser handle ArrowDown normally.
@@ -969,18 +1227,17 @@ const comboboxKeyboardStrategy = {
969
1227
 
970
1228
  Tab(component, evt, ctx) {
971
1229
  if (ctx.isExpanded && !isClearBtnFocused(ctx)) {
972
- // When the clear button is focused, Tab events do not bubble out of
973
- // its shadow DOM, so this handler only fires when the clear button
974
- // is NOT focused. In that case, select the active option and close.
975
- reconcileMenuIndex(component.menu);
976
- component.menu.makeSelection();
977
- component.hideBib();
1230
+ if (evt.shiftKey) {
1231
+ if (selectAndExitBackward(component)) {
1232
+ evt.preventDefault();
1233
+ }
1234
+ return;
1235
+ }
1236
+
1237
+ selectAndClose(component);
978
1238
 
979
- // In fullscreen modal mode, closing the dialog does not
980
- // automatically restores focus to the input. In the tab case,
981
- // Explicitly move focus to the trigger's clear button so the
982
- // user can continues tabbing through the page normally.
983
- if (ctx.isModal && !evt.shiftKey) {
1239
+ if (ctx.isModal) {
1240
+ // Fullscreen close does not automatically restore focus to the input.
984
1241
  component.setClearBtnFocus();
985
1242
  }
986
1243
  }
@@ -3263,12 +3520,40 @@ class AuroFloatingUI {
3263
3520
  return;
3264
3521
  }
3265
3522
 
3523
+ // Chrome-specific: during popover top-layer promotion after a click on a
3524
+ // slotted focusable, :focus-within can briefly evaluate false while the
3525
+ // active element is still structurally inside the trigger/bib. Fall back
3526
+ // to a shadow-piercing ancestry walk from the deep active element before
3527
+ // treating this as a real focus loss.
3528
+ try {
3529
+ let active = document.activeElement;
3530
+ while (active && active.shadowRoot && active.shadowRoot.activeElement) {
3531
+ active = active.shadowRoot.activeElement;
3532
+ }
3533
+ const targets = [element, element.trigger, element.bib].filter(Boolean);
3534
+ let node = active;
3535
+ while (node) {
3536
+ if (targets.includes(node)) {
3537
+ return;
3538
+ }
3539
+ node =
3540
+ node.parentElement ||
3541
+ (node.getRootNode && node.getRootNode().host) ||
3542
+ null;
3543
+ }
3544
+ } catch (e) {
3545
+ // Defensive: fall through to the existing close path if traversal fails.
3546
+ }
3547
+
3266
3548
  // if fullscreen bib is in fullscreen mode, do not close
3267
3549
  if (element.bib.hasAttribute("isfullscreen")) {
3268
3550
  return;
3269
3551
  }
3270
3552
 
3271
- this.hideBib("keydown");
3553
+ // eventType "focusloss" distinguishes a Tab/click-driven close from an
3554
+ // Escape keydown close. Consumers use this to decide whether to restore
3555
+ // focus to the trigger (Escape) or let it advance naturally (Tab).
3556
+ this.hideBib("focusloss");
3272
3557
  }
3273
3558
 
3274
3559
  setupHideHandlers() {
@@ -3938,199 +4223,6 @@ function getFocusableElements(container) {
3938
4223
  return tabIndexedUniqueElements;
3939
4224
  }
3940
4225
 
3941
- /**
3942
- * FocusTrap manages keyboard focus within a specified container element, ensuring that focus does not leave the container when tabbing.
3943
- * It is commonly used for modal dialogs or overlays to improve accessibility by trapping focus within interactive UI components.
3944
- */
3945
- class FocusTrap {
3946
- /**
3947
- * Creates a new FocusTrap instance for the given container element.
3948
- * Initializes event listeners and prepares the container for focus management.
3949
- *
3950
- * @param {HTMLElement} container The DOM element to trap focus within.
3951
- * @param {boolean} [controlTabOrder=false] If true enables manual control of the tab order by the FocusTrap.
3952
- * @throws {Error} If the provided container is not a valid HTMLElement.
3953
- */
3954
- constructor(container, controlTabOrder = false) {
3955
- if (!container || !(container instanceof HTMLElement)) {
3956
- throw new Error("FocusTrap requires a valid HTMLElement.");
3957
- }
3958
-
3959
- this.container = container;
3960
- this.tabDirection = "forward"; // or 'backward';
3961
- this.controlTabOrder = controlTabOrder;
3962
-
3963
- this._init();
3964
- }
3965
-
3966
- /**
3967
- * Initializes the focus trap by setting up event listeners and attributes on the container.
3968
- * Prepares the container for focus management, including support for shadow DOM and inert attributes.
3969
- *
3970
- * @private
3971
- */
3972
- _init() {
3973
- // Add inert attribute to prevent focusing programmatically as well (if supported)
3974
- if ("inert" in HTMLElement.prototype) {
3975
- this.container.inert = false; // Ensure the container isn't inert
3976
- this.container.setAttribute("data-focus-trap-container", true); // Mark for identification
3977
- }
3978
-
3979
- // Track tab direction
3980
- this.container.addEventListener("keydown", this._onKeydown);
3981
- }
3982
-
3983
- /**
3984
- * Gets an array of currently active (focused) elements in the document and shadow DOM.
3985
- * @returns {Array<HTMLElement>} An array of focusable elements within the container.
3986
- * @private
3987
- */
3988
- _getActiveElements() {
3989
- // Get the active element(s) in the document and shadow root
3990
- // This will include the active element in the shadow DOM if it exists
3991
- // Active element may be inside the shadow DOM depending on delegatesFocus, so we need to check both
3992
- let { activeElement } = document;
3993
- const actives = [activeElement];
3994
- while (activeElement?.shadowRoot?.activeElement) {
3995
- actives.push(activeElement.shadowRoot.activeElement);
3996
- activeElement = activeElement.shadowRoot.activeElement;
3997
- }
3998
- return actives;
3999
- }
4000
-
4001
- /**
4002
- * Gets the next focus index based on the current index and focusable elements.
4003
- * @param {number} currentIndex The current index of the focused element.
4004
- * @param {Array<HTMLElement>} focusables The array of focusable elements.
4005
- * @returns {number|null} The next focus index or null if not determined.
4006
- */
4007
- _getNextFocusIndex(currentIndex, focusables, actives) {
4008
- if (this.controlTabOrder) {
4009
- // Calculate the new index based on the current index and tab direction
4010
- let newFocusIndex =
4011
- currentIndex + (this.tabDirection === "forward" ? 1 : -1);
4012
-
4013
- // Wrap-around logic
4014
- if (newFocusIndex < 0) newFocusIndex = focusables.length - 1;
4015
- if (newFocusIndex >= focusables.length) newFocusIndex = 0;
4016
-
4017
- // Early return with the new index
4018
- return newFocusIndex;
4019
- }
4020
-
4021
- // Determine if we need to wrap
4022
- const atFirst =
4023
- actives.includes(focusables[0]) || actives.includes(this.container);
4024
- const atLast = actives.includes(focusables[focusables.length - 1]);
4025
-
4026
- // Only wrap if at the ends
4027
- if (this.tabDirection === "backward" && atFirst) {
4028
- return focusables.length - 1;
4029
- }
4030
-
4031
- if (this.tabDirection === "forward" && atLast) {
4032
- return 0;
4033
- }
4034
-
4035
- // No wrap, so don't change focus, return early
4036
- return null;
4037
- }
4038
-
4039
- /**
4040
- * Handles the Tab key press event to manage focus within the container.
4041
- * @param {KeyboardEvent} e The keyboard event triggered by the user.
4042
- * @returns {void}
4043
- */
4044
- _handleTabKey(e) {
4045
- // Update the focusable elements
4046
- const focusables = this._getFocusableElements();
4047
-
4048
- // If there are no focusable elements, exit
4049
- if (!focusables.length) return;
4050
-
4051
- // Set the tab direction based on the key pressed
4052
- this.tabDirection = e.shiftKey ? "backward" : "forward";
4053
-
4054
- // Get the active elements that are currently focused
4055
- const actives = this._getActiveElements();
4056
-
4057
- // If we're at either end of the focusable elements, wrap around to the other end
4058
- let focusIndex = focusables.findIndex((el) => actives.includes(el));
4059
-
4060
- // Fallback if we have no focused element
4061
- if (focusIndex === -1) focusIndex = 0;
4062
-
4063
- // Get the next focus index based on the current focus index, tab direction, and controlTabOrder setting
4064
- // Is null if no new focus index is determined
4065
- const newFocusIndex = this._getNextFocusIndex(
4066
- focusIndex,
4067
- focusables,
4068
- actives,
4069
- );
4070
-
4071
- // If we have a new focus index, set focus to that element
4072
- if (newFocusIndex !== null) {
4073
- e.preventDefault();
4074
- focusables[newFocusIndex].focus();
4075
- }
4076
- }
4077
-
4078
- /**
4079
- * Catches the keydown event
4080
- * @param {KeyboardEvent} e The keyboard event triggered by user interaction.
4081
- * @private
4082
- */
4083
- _onKeydown = (e) => {
4084
- // Handle tab
4085
- if (e.key === "Tab") this._handleTabKey(e);
4086
- };
4087
-
4088
- /**
4089
- * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
4090
- * Returns a unique, ordered array of elements that can receive focus.
4091
- *
4092
- * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4093
- * @private
4094
- */
4095
- _getFocusableElements() {
4096
- // Use the imported utility function to get focusable elements
4097
- const elements = getFocusableElements(this.container);
4098
-
4099
- // Return the elements found
4100
- return elements;
4101
- }
4102
-
4103
- /**
4104
- * Moves focus to the first focusable element within the container.
4105
- * Useful for setting initial focus when activating the focus trap.
4106
- */
4107
- focusFirstElement() {
4108
- const focusables = this._getFocusableElements();
4109
- if (focusables.length) focusables[0].focus();
4110
- }
4111
-
4112
- /**
4113
- * Moves focus to the last focusable element within the container.
4114
- * Useful for setting focus when deactivating or cycling focus in reverse.
4115
- */
4116
- focusLastElement() {
4117
- const focusables = this._getFocusableElements();
4118
- if (focusables.length) focusables[focusables.length - 1].focus();
4119
- }
4120
-
4121
- /**
4122
- * Removes event listeners and attributes added by the focus trap.
4123
- * Call this method to clean up when the focus trap is no longer needed.
4124
- */
4125
- disconnect() {
4126
- if (this.container.hasAttribute("data-focus-trap-container")) {
4127
- this.container.removeAttribute("data-focus-trap-container");
4128
- }
4129
-
4130
- this.container.removeEventListener("keydown", this._onKeydown);
4131
- }
4132
- }
4133
-
4134
4226
  // Copyright (c) Alaska Air. All right reserved. Licensed under the Apache-2.0 license
4135
4227
  // See LICENSE in the project root for license information.
4136
4228
 
@@ -4628,7 +4720,7 @@ class AuroDropdownBib extends i$3 {
4628
4720
  classes[`shape-${this.shape}`] = true;
4629
4721
 
4630
4722
  return u$7`
4631
- <dialog class="${e$2(classes)}" part="bibContainer" role="${o(this.dialogRole)}" aria-labelledby="${o(this.dialogLabel ? 'dialogLabel' : undefined)}">
4723
+ <dialog tabindex="-1" class="${e$2(classes)}" part="bibContainer" role="${o(this.dialogRole)}" aria-labelledby="${o(this.dialogLabel ? 'dialogLabel' : undefined)}">
4632
4724
  ${this.dialogLabel ? u$7`<span id="dialogLabel" class="util_displayHiddenVisually">${this.dialogLabel}</span>` : ''}
4633
4725
  <slot></slot>
4634
4726
  <span id="srAnnouncement" class="util_displayHiddenVisually" aria-live="polite" role="status"></span>
@@ -4879,7 +4971,7 @@ let AuroHelpText$2 = class AuroHelpText extends i$3 {
4879
4971
  }
4880
4972
  };
4881
4973
 
4882
- var formkitVersion$2 = '202607072023';
4974
+ var formkitVersion$2 = '202607102214';
4883
4975
 
4884
4976
  let AuroElement$2 = class AuroElement extends i$3 {
4885
4977
  static get properties() {
@@ -5633,7 +5725,10 @@ class AuroDropdown extends AuroElement$2 {
5633
5725
  }
5634
5726
 
5635
5727
 
5636
- if (!this.isPopoverVisible) {
5728
+ // On Tab-driven close (eventType "focusloss"), let focus advance naturally
5729
+ // — restoring to the trigger would trap the user on this dropdown, forcing
5730
+ // an extra Tab to move on. Escape and outside-click still restore.
5731
+ if (!this.isPopoverVisible && eventType !== "focusloss") {
5637
5732
  // wait til the bib gets fully closed and rendered
5638
5733
  setTimeout(() => {
5639
5734
  // Skip if the bib re-opened, or if focus moved intentionally outside the dropdown (not to body).
@@ -5863,15 +5958,16 @@ class AuroDropdown extends AuroElement$2 {
5863
5958
  }
5864
5959
  });
5865
5960
  } else {
5866
- // Normal desktop: use FocusTrap on the bib element.
5867
- // Defer focusFirstElement to the next frame because the popover
5868
- // is positioned asynchronously by Floating UI — calling focus()
5869
- // synchronously here would target elements with zero dimensions
5870
- // and the focus call would be silently ignored.
5871
- this.focusTrap = new FocusTrap(this.bibContent);
5961
+ // Normal desktop (non-modal): move initial focus into the bib but
5962
+ // don't trap Tab. Tab should exit the bib and let the floater's
5963
+ // handleFocusLoss close it, matching native <select>/<details>
5964
+ // behavior. Deferred one frame because Floating UI positions the
5965
+ // popover asynchronously a synchronous focus() would target
5966
+ // zero-dimension elements and be silently ignored.
5872
5967
  requestAnimationFrame(() => {
5873
- if (this.focusTrap) {
5874
- this.focusTrap.focusFirstElement();
5968
+ const focusables = getFocusableElements(this.bibContent);
5969
+ if (focusables.length) {
5970
+ focusables[0].focus();
5875
5971
  }
5876
5972
  });
5877
5973
  }
@@ -18504,7 +18600,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18504
18600
  }
18505
18601
  };
18506
18602
 
18507
- var formkitVersion$1 = '202607072023';
18603
+ var formkitVersion$1 = '202607102214';
18508
18604
 
18509
18605
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18510
18606
  // See LICENSE in the project root for license information.
@@ -19625,7 +19721,7 @@ class AuroBibtemplate extends i$3 {
19625
19721
  }
19626
19722
  }
19627
19723
 
19628
- var formkitVersion = '202607072023';
19724
+ var formkitVersion = '202607102214';
19629
19725
 
19630
19726
  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}`;
19631
19727
 
@@ -20408,6 +20504,8 @@ class AuroCombobox extends AuroElement {
20408
20504
 
20409
20505
  /**
20410
20506
  * If set, the `icon` attribute will be applied to the trigger `auro-input` element.
20507
+ * Icon rendering is currently limited to `type="credit-card"`. Setting `triggerIcon`
20508
+ * without a supported `type` propagates the attribute but does not render a visible icon.
20411
20509
  */
20412
20510
  triggerIcon: {
20413
20511
  type: Boolean,
@@ -20439,7 +20537,7 @@ class AuroCombobox extends AuroElement {
20439
20537
  },
20440
20538
 
20441
20539
  /**
20442
- * Value selected for the dropdown menu.
20540
+ * 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.
20443
20541
  */
20444
20542
  value: {
20445
20543
  type: String
@@ -20935,7 +21033,12 @@ class AuroCombobox extends AuroElement {
20935
21033
  this.dropdown.bibDialogLabel = labelElement.textContent.trim() || undefined;
20936
21034
  }
20937
21035
 
20938
- // Exposes the CSS parts from the bibtemplate for styling
21036
+ if (this.customBibWidth) {
21037
+ this.dropdown.dropdownWidth = this.customBibWidth;
21038
+ }
21039
+
21040
+ // Exposes the CSS parts from the dropdown and bibtemplate for styling
21041
+ this.dropdown.exposeCssParts();
20939
21042
  this.bibtemplate.exposeCssParts();
20940
21043
 
20941
21044
  this.hideBib = this.hideBib.bind(this);
@@ -21155,8 +21258,11 @@ class AuroCombobox extends AuroElement {
21155
21258
  // do not close while typing in suggestion mode with no value selected, to allow freeform input
21156
21259
  this.hideBib();
21157
21260
 
21158
- // Move focus to the clear button when the user makes a selection.
21159
- if (!isEcho && this.menu.value !== undefined) {
21261
+ // Move focus to the clear button when the user makes a selection,
21262
+ // unless the Shift+Tab handler opted this selection out.
21263
+ if (this._suppressClearBtnFocusOnSelection) {
21264
+ this._suppressClearBtnFocusOnSelection = false;
21265
+ } else if (!isEcho && this.menu.value !== undefined) {
21160
21266
  this.setClearBtnFocus();
21161
21267
  }
21162
21268
 
@@ -21562,6 +21668,12 @@ class AuroCombobox extends AuroElement {
21562
21668
  this.value = undefined;
21563
21669
  this.typedValue = undefined;
21564
21670
  this.input.value = undefined;
21671
+ // Fullscreen bib mounts a second auro-input that holds the user-facing
21672
+ // native <input> on mobile; clearing only the trigger leaves stale text
21673
+ // there when the bib is reopened after reset.
21674
+ if (this.inputInBib) {
21675
+ this.inputInBib.value = undefined;
21676
+ }
21565
21677
  this.menu.value = undefined;
21566
21678
  this.validation.reset(this);
21567
21679
  this.touched = false;
@@ -21957,6 +22069,18 @@ var tokensCss = i$6`:host{--ds-auro-menu-divider-color: var(--ds-basic-color-bor
21957
22069
  // See LICENSE in the project root for license information.
21958
22070
 
21959
22071
 
22072
+ /**
22073
+ * Serializes a multi-select value array back into the String `value` property.
22074
+ * An empty (or missing) array collapses to `undefined` so an emptied selection
22075
+ * clears `value` rather than reflecting a `"[]"` attribute.
22076
+ * @private
22077
+ * @param {Array<string>|undefined} values - The selected values.
22078
+ * @returns {string|undefined} JSON string of the values, or undefined when empty.
22079
+ */
22080
+ function serializeMultiSelectValue(values) {
22081
+ return values && values.length > 0 ? JSON.stringify(values) : undefined;
22082
+ }
22083
+
21960
22084
  /**
21961
22085
  * Validates if an option can be interacted with.
21962
22086
  * @private
@@ -21969,6 +22093,20 @@ function isOptionInteractive(option) {
21969
22093
  !option.hasAttribute('static');
21970
22094
  }
21971
22095
 
22096
+ /**
22097
+ * Validates if an option may be selected by matching a programmatic value.
22098
+ * Unlike `isOptionInteractive`, `hidden` is allowed: the combobox toggles
22099
+ * `hidden` as its type-ahead filter, so a filtered-out option is still a
22100
+ * valid programmatic selection. Only disabled and static options — which are
22101
+ * never selectable — are rejected.
22102
+ * @param {HTMLElement} option - The option to check.
22103
+ * @returns {boolean} True if option can be selected by value.
22104
+ */
22105
+ function isSelectableByValue(option) {
22106
+ return !option.hasAttribute('disabled') &&
22107
+ !option.hasAttribute('static');
22108
+ }
22109
+
21972
22110
  /**
21973
22111
  * Helper method to dispatch custom events.
21974
22112
  * @param {HTMLElement} element - Element to dispatch event from.
@@ -22138,22 +22276,26 @@ class AuroMenu extends AuroElement {
22138
22276
 
22139
22277
  /**
22140
22278
  * Specifies the current active menuOption.
22279
+ * @readonly
22141
22280
  */
22142
22281
  optionActive: {
22143
22282
  type: Object,
22144
- attribute: 'optionactive'
22283
+ attribute: false
22145
22284
  },
22146
22285
 
22147
22286
  /**
22148
- * An array of currently selected menu options, type `HTMLElement` by default. In multi-select mode, `optionSelected` is an array of HTML elements.
22287
+ * 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.
22288
+ * @readonly
22149
22289
  */
22150
22290
  optionSelected: {
22151
22291
  // Allow HTMLElement, HTMLElement[] arrays and undefined
22152
- type: Object
22292
+ type: Object,
22293
+ attribute: false
22153
22294
  },
22154
22295
 
22155
22296
  /**
22156
22297
  * The value of the selected option. In multi-select mode, this is a JSON stringified array of selected option values.
22298
+ * 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.
22157
22299
  */
22158
22300
  value: {
22159
22301
  type: String,
@@ -22277,7 +22419,7 @@ class AuroMenu extends AuroElement {
22277
22419
  }
22278
22420
 
22279
22421
  /**
22280
- * Selects options by value.
22422
+ * 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.
22281
22423
  * @param {string|string[]|undefined|null} value - The value(s) to select.
22282
22424
  * @public
22283
22425
  */
@@ -22362,6 +22504,11 @@ class AuroMenu extends AuroElement {
22362
22504
  this.initItems();
22363
22505
  }
22364
22506
 
22507
+ // Set when reconciliation reassigns `value` below. That reassignment schedules a
22508
+ // second updated() cycle, so the `event`-attribute dispatch is deferred to that
22509
+ // cycle to avoid firing option custom events twice on the same selection.
22510
+ let valueReconciled = false;
22511
+
22365
22512
  // Handle null/undefined/empty case — empty/whitespace strings clear selection
22366
22513
  // consistently with selectByValue(''), and avoid downstream `.includes('')` matches.
22367
22514
  if (this.value === undefined || this.value === null || (typeof this.value === 'string' && this.value.trim() === '')) {
@@ -22374,11 +22521,31 @@ class AuroMenu extends AuroElement {
22374
22521
  // Defensive default: `formattedValue` can be undefined for unexpected value types,
22375
22522
  // and calling `.includes` on undefined would throw during reconciliation.
22376
22523
  const valueArray = this.formattedValue || [];
22377
- const matchingOptions = this.items ? this.items.filter((item) => valueArray.includes(item.value)) : [];
22524
+ const matchingOptions = this.items ? this.items.filter((item) => isSelectableByValue(item) && valueArray.includes(item.value)) : [];
22378
22525
  newSelected = matchingOptions.length > 0 ? matchingOptions : undefined;
22526
+
22527
+ // Reconcile `value` with the selectable set. Drop only entries whose option is
22528
+ // loaded but non-selectable (disabled/static) — leaving them would desync `value`
22529
+ // from `optionSelected`, and the toggle handlers rebuild `value` from `formattedValue`,
22530
+ // so the rejected entry would resurface on the next select/deselect. Entries with no
22531
+ // matching item yet are preserved so async preselection still works once options render.
22532
+ const rejectedValues = this.items
22533
+ ? this.items.filter((item) => !isSelectableByValue(item) && valueArray.includes(item.value)).map((item) => item.value)
22534
+ : [];
22535
+ if (rejectedValues.length > 0) {
22536
+ const reconciled = valueArray.filter((val) => !rejectedValues.includes(val));
22537
+ this.value = serializeMultiSelectValue(reconciled);
22538
+ valueReconciled = true;
22539
+ }
22379
22540
  } else {
22380
- // In single-select mode, this.value should be a string
22381
- const matchingOption = this.items ? this.items.find((item) => item.value === this.value) : undefined;
22541
+ // In single-select mode, this.value should be a string. Reject
22542
+ // disabled/static options so a programmatic value pointing at a
22543
+ // non-selectable option falls through to the no-match path below
22544
+ // (dispatching auroMenu-selectValueFailure) instead of pinning it.
22545
+ // `hidden` is intentionally NOT excluded: the combobox toggles
22546
+ // `hidden` as its type-ahead filter, so a filtered-out option is
22547
+ // still a valid programmatic selection.
22548
+ const matchingOption = this.items ? this.items.find((item) => isSelectableByValue(item) && item.value === this.value) : undefined;
22382
22549
 
22383
22550
  if (matchingOption) {
22384
22551
  newSelected = matchingOption;
@@ -22420,8 +22587,9 @@ class AuroMenu extends AuroElement {
22420
22587
  ]
22421
22588
  ]));
22422
22589
 
22423
- // Notify of changes
22424
- if (this.optionSelected !== undefined) {
22590
+ // Notify of changes. Skip when reconciliation just reassigned `value`: the
22591
+ // follow-on update cycle re-runs this branch and fires the events exactly once.
22592
+ if (this.optionSelected !== undefined && !valueReconciled) {
22425
22593
  const selected = Array.isArray(this.optionSelected) ? this.optionSelected : [this.optionSelected];
22426
22594
  selected.forEach((opt) => {
22427
22595
  if (opt.hasAttribute('event')) {
@@ -22634,7 +22802,7 @@ class AuroMenu extends AuroElement {
22634
22802
  const currentSelected = this.optionSelected || [];
22635
22803
 
22636
22804
  if (!currentValue.includes(option.value)) {
22637
- this.value = JSON.stringify([
22805
+ this.value = serializeMultiSelectValue([
22638
22806
  ...currentValue,
22639
22807
  option.value
22640
22808
  ]);
@@ -22660,15 +22828,9 @@ class AuroMenu extends AuroElement {
22660
22828
  */
22661
22829
  handleDeselectState(option) {
22662
22830
  if (this.multiSelect) {
22663
- // Remove this option from array
22831
+ // Remove this option from array; an empty result collapses `value` to undefined.
22664
22832
  const newFormattedValue = (this.formattedValue || []).filter((val) => val !== option.value);
22665
-
22666
- // If array is empty after removal, set back to undefined
22667
- if (newFormattedValue && newFormattedValue.length === 0) {
22668
- this.value = undefined;
22669
- } else {
22670
- this.value = JSON.stringify(newFormattedValue);
22671
- }
22833
+ this.value = serializeMultiSelectValue(newFormattedValue);
22672
22834
 
22673
22835
  this.optionSelected = this.optionSelected.filter((val) => val !== option);
22674
22836
  if (this.optionSelected.length === 0) {
@@ -22715,6 +22877,11 @@ class AuroMenu extends AuroElement {
22715
22877
  this.optionSelected = undefined;
22716
22878
  this._index = -1;
22717
22879
 
22880
+ // Clear active option state so a follow-up open/navigation starts fresh
22881
+ // rather than reusing a stale reference from before the reset.
22882
+ this.items?.forEach((item) => item.classList.remove('active'));
22883
+ this.optionActive = undefined;
22884
+
22718
22885
  // Reset UI state
22719
22886
  this.updateItemsState(new Map([
22720
22887
  [
@@ -23124,6 +23291,7 @@ let menuOptionIdCounter = 0;
23124
23291
  * The `auro-menuoption` element provides users a way to define a menu option.
23125
23292
  * @customElement auro-menuoption
23126
23293
  *
23294
+ * @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.
23127
23295
  * @slot default - The default slot for the menu option text.
23128
23296
  *
23129
23297
  * @event auroMenuOption-mouseover - Notifies that this option has been hovered over.
@@ -23153,6 +23321,7 @@ class AuroMenuOption extends AuroElement {
23153
23321
  this.noCheckmark = false;
23154
23322
  this.disabled = false;
23155
23323
  this.noMatch = false;
23324
+ this.persistent = false;
23156
23325
 
23157
23326
  /**
23158
23327
  * @private
@@ -23174,6 +23343,12 @@ class AuroMenuOption extends AuroElement {
23174
23343
  type: Boolean,
23175
23344
  reflect: true
23176
23345
  },
23346
+
23347
+ /**
23348
+ * **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.
23349
+ *
23350
+ * @deprecated Use the `value` attribute on `auro-menu` instead.
23351
+ */
23177
23352
  selected: {
23178
23353
  type: Boolean,
23179
23354
  reflect: true
@@ -23208,6 +23383,12 @@ class AuroMenuOption extends AuroElement {
23208
23383
  reflect: true,
23209
23384
  attribute: 'nomatch'
23210
23385
  },
23386
+
23387
+ /** 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. */
23388
+ persistent: {
23389
+ type: Boolean,
23390
+ reflect: true
23391
+ },
23211
23392
  };
23212
23393
  }
23213
23394