@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.
- package/components/checkbox/demo/accessibility.md +1 -1
- package/components/checkbox/demo/customize.min.js +1 -1
- package/components/checkbox/demo/getting-started.min.js +1 -1
- package/components/checkbox/demo/index.min.js +1 -1
- package/components/checkbox/dist/index.js +1 -1
- package/components/checkbox/dist/registered.js +1 -1
- package/components/combobox/demo/accessibility.md +1 -1
- package/components/combobox/demo/api.md +2 -2
- package/components/combobox/demo/customize.md +54 -2
- package/components/combobox/demo/customize.min.js +421 -240
- package/components/combobox/demo/getting-started.min.js +421 -240
- package/components/combobox/demo/index.md +1 -0
- package/components/combobox/demo/index.min.js +421 -240
- package/components/combobox/demo/keyboard-behavior.md +1 -4
- package/components/combobox/dist/auro-combobox.d.ts +4 -1
- package/components/combobox/dist/index.js +334 -222
- package/components/combobox/dist/registered.js +334 -222
- package/components/counter/demo/customize.min.js +59 -222
- package/components/counter/demo/index.min.js +59 -222
- package/components/counter/dist/auro-counter.d.ts +0 -8
- package/components/counter/dist/index.js +59 -222
- package/components/counter/dist/registered.js +59 -222
- package/components/datepicker/demo/accessibility.md +1 -1
- package/components/datepicker/demo/customize.md +30 -1
- package/components/datepicker/demo/customize.min.js +84 -220
- package/components/datepicker/demo/getting-started.md +118 -2
- package/components/datepicker/demo/index.min.js +84 -220
- package/components/datepicker/dist/index.js +84 -220
- package/components/datepicker/dist/registered.js +84 -220
- package/components/dropdown/demo/customize.html +3 -0
- package/components/dropdown/demo/customize.min.js +55 -206
- package/components/dropdown/demo/getting-started.min.js +44 -205
- package/components/dropdown/demo/index.min.js +44 -205
- package/components/dropdown/dist/index.js +44 -205
- package/components/dropdown/dist/registered.js +44 -205
- package/components/form/demo/customize.min.js +670 -945
- package/components/form/demo/getting-started.min.js +670 -945
- package/components/form/demo/index.min.js +670 -945
- package/components/form/demo/registerDemoDeps.min.js +670 -945
- package/components/input/demo/accessibility.md +1 -1
- package/components/input/demo/customize.min.js +1 -1
- package/components/input/demo/getting-started.min.js +1 -1
- package/components/input/demo/index.min.js +1 -1
- package/components/input/dist/index.js +1 -1
- package/components/input/dist/registered.js +1 -1
- package/components/menu/demo/accessibility.md +9 -4
- package/components/menu/demo/api.md +26 -19
- package/components/menu/demo/css-only.md +26 -19
- package/components/menu/demo/customize.md +307 -47
- package/components/menu/demo/design.md +1 -1
- package/components/menu/demo/getting-started.md +144 -7
- package/components/menu/demo/index.min.js +214 -19
- package/components/menu/demo/keyboard-behavior.md +83 -4
- package/components/menu/demo/voiceover.md +21 -14
- package/components/menu/demo/why-menu.md +8 -9
- package/components/menu/dist/auro-menu-utils.d.ts +19 -0
- package/components/menu/dist/auro-menu.d.ts +4 -8
- package/components/menu/dist/auro-menuoption.d.ts +12 -0
- package/components/menu/dist/index.js +87 -18
- package/components/menu/dist/registered.js +87 -18
- package/components/radio/demo/accessibility.md +1 -1
- package/components/radio/demo/customize.min.js +1 -1
- package/components/radio/demo/getting-started.min.js +1 -1
- package/components/radio/demo/index.min.js +1 -1
- package/components/radio/dist/index.js +1 -1
- package/components/radio/dist/registered.js +1 -1
- package/components/select/demo/accessibility.md +6 -1
- package/components/select/demo/api.md +2 -2
- package/components/select/demo/customize.md +6 -3
- package/components/select/demo/customize.min.js +139 -227
- package/components/select/demo/design.md +10 -10
- package/components/select/demo/getting-started.md +1 -1
- package/components/select/demo/getting-started.min.js +139 -227
- package/components/select/demo/index.md +2 -2
- package/components/select/demo/index.min.js +139 -227
- package/components/select/demo/keyboard-behavior.md +53 -58
- package/components/select/demo/voiceover.md +28 -15
- package/components/select/dist/auro-select.d.ts +6 -2
- package/components/select/dist/index.js +52 -209
- package/components/select/dist/registered.js +52 -209
- package/custom-elements.json +1611 -1552
- 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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1245
|
+
if (evt.shiftKey) {
|
|
1246
|
+
if (selectAndExitBackward(component)) {
|
|
1247
|
+
evt.preventDefault();
|
|
1248
|
+
}
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
selectAndClose(component);
|
|
993
1253
|
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
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:
|
|
5882
|
-
//
|
|
5883
|
-
//
|
|
5884
|
-
//
|
|
5885
|
-
//
|
|
5886
|
-
|
|
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
|
-
|
|
5889
|
-
|
|
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 = '
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
22298
|
+
attribute: false
|
|
22160
22299
|
},
|
|
22161
22300
|
|
|
22162
22301
|
/**
|
|
22163
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|