@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
|
@@ -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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1230
|
+
if (evt.shiftKey) {
|
|
1231
|
+
if (selectAndExitBackward(component)) {
|
|
1232
|
+
evt.preventDefault();
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
selectAndClose(component);
|
|
978
1238
|
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
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:
|
|
5867
|
-
//
|
|
5868
|
-
//
|
|
5869
|
-
//
|
|
5870
|
-
//
|
|
5871
|
-
|
|
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
|
-
|
|
5874
|
-
|
|
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 = '
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
22283
|
+
attribute: false
|
|
22145
22284
|
},
|
|
22146
22285
|
|
|
22147
22286
|
/**
|
|
22148
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|