@aurodesignsystem-dev/auro-formkit 0.0.0-pr1608.0 → 0.0.0-pr1608.1
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 +15 -4
- package/components/checkbox/demo/customize.min.js +25 -10
- package/components/checkbox/demo/getting-started.min.js +25 -10
- package/components/checkbox/demo/index.min.js +25 -10
- package/components/checkbox/dist/auro-checkbox-group.d.ts +9 -0
- package/components/checkbox/dist/index.js +25 -10
- package/components/checkbox/dist/registered.js +25 -10
- package/components/combobox/demo/customize.min.js +46 -4
- package/components/combobox/demo/getting-started.min.js +46 -4
- package/components/combobox/demo/index.min.js +46 -4
- package/components/combobox/dist/index.js +46 -4
- package/components/combobox/dist/registered.js +46 -4
- package/components/counter/demo/customize.min.js +339 -23
- package/components/counter/demo/getting-started.min.js +339 -23
- package/components/counter/demo/index.min.js +339 -23
- package/components/counter/dist/counterGroupKeyboardStrategy.d.ts +1 -0
- package/components/counter/dist/index.js +339 -23
- package/components/counter/dist/registered.js +339 -23
- package/components/datepicker/demo/customize.min.js +46 -4
- package/components/datepicker/demo/getting-started.min.js +46 -4
- package/components/datepicker/demo/index.min.js +46 -4
- package/components/datepicker/dist/index.js +46 -4
- package/components/datepicker/dist/registered.js +46 -4
- package/components/dropdown/demo/customize.min.js +44 -2
- package/components/dropdown/demo/getting-started.min.js +45 -3
- package/components/dropdown/demo/index.min.js +44 -2
- package/components/dropdown/dist/index.js +44 -2
- package/components/dropdown/dist/registered.js +44 -2
- package/components/form/demo/customize.min.js +527 -57
- package/components/form/demo/getting-started.min.js +527 -57
- package/components/form/demo/index.min.js +527 -57
- package/components/form/demo/registerDemoDeps.min.js +527 -57
- 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/radio/demo/accessibility.md +13 -4
- package/components/radio/demo/customize.min.js +25 -12
- package/components/radio/demo/getting-started.min.js +25 -12
- package/components/radio/demo/index.min.js +25 -12
- package/components/radio/dist/auro-radio-group.d.ts +9 -0
- package/components/radio/dist/index.js +25 -12
- package/components/radio/dist/registered.js +25 -12
- package/components/select/demo/customize.min.js +45 -3
- package/components/select/demo/getting-started.min.js +45 -3
- package/components/select/demo/index.min.js +45 -3
- package/components/select/dist/index.js +45 -3
- package/components/select/dist/registered.js +45 -3
- package/custom-elements.json +1507 -1485
- package/package.json +3 -3
|
@@ -1127,7 +1127,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$2 {
|
|
|
1127
1127
|
}
|
|
1128
1128
|
};
|
|
1129
1129
|
|
|
1130
|
-
var formkitVersion$1 = '
|
|
1130
|
+
var formkitVersion$1 = '202609022217';
|
|
1131
1131
|
|
|
1132
1132
|
// Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
|
|
1133
1133
|
// See LICENSE in the project root for license information.
|
|
@@ -3382,6 +3382,48 @@ const computePosition = (reference, floating, options) => {
|
|
|
3382
3382
|
});
|
|
3383
3383
|
};
|
|
3384
3384
|
|
|
3385
|
+
/**
|
|
3386
|
+
* Generates a v4 UUID with a three-tier fallback for insecure contexts.
|
|
3387
|
+
*
|
|
3388
|
+
* Tier 1 (secure contexts): crypto.randomUUID() — native, fully random.
|
|
3389
|
+
* Tier 2 (insecure contexts): crypto.getRandomValues() — available on all
|
|
3390
|
+
* modern browsers regardless of secure-context status.
|
|
3391
|
+
* Tier 3 (last resort, no crypto API): new Date().getTime() — lower entropy,
|
|
3392
|
+
* but sufficient for element IDs where collision risk is negligible.
|
|
3393
|
+
*
|
|
3394
|
+
* See: https://w3c.github.io/webcrypto/#dom-crypto-randomuuid (secure-context restriction)
|
|
3395
|
+
*/
|
|
3396
|
+
function generateUUID() {
|
|
3397
|
+
let uuid;
|
|
3398
|
+
|
|
3399
|
+
if (
|
|
3400
|
+
typeof crypto !== "undefined" &&
|
|
3401
|
+
typeof crypto.randomUUID === "function"
|
|
3402
|
+
) {
|
|
3403
|
+
uuid = crypto.randomUUID();
|
|
3404
|
+
} else if (
|
|
3405
|
+
typeof crypto !== "undefined" &&
|
|
3406
|
+
typeof crypto.getRandomValues === "function"
|
|
3407
|
+
) {
|
|
3408
|
+
const bytes = new Uint8Array(16);
|
|
3409
|
+
crypto.getRandomValues(bytes);
|
|
3410
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
3411
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
3412
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0"));
|
|
3413
|
+
uuid = `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
|
3414
|
+
} else {
|
|
3415
|
+
let d = new Date().getTime();
|
|
3416
|
+
uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
3417
|
+
const r = (d % 16) | 0;
|
|
3418
|
+
d = (d - r) / 16 || new Date().getTime();
|
|
3419
|
+
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
// CSS selectors require IDs starting with a letter; replace a leading digit with 'a'.
|
|
3424
|
+
return /^[0-9]/.test(uuid) ? `a${uuid.slice(1)}` : uuid;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3385
3427
|
/* eslint-disable line-comment-position, no-inline-comments */
|
|
3386
3428
|
|
|
3387
3429
|
|
|
@@ -4400,7 +4442,7 @@ class AuroFloatingUI {
|
|
|
4400
4442
|
|
|
4401
4443
|
this.id = element.getAttribute("id");
|
|
4402
4444
|
if (!this.id) {
|
|
4403
|
-
this.id =
|
|
4445
|
+
this.id = generateUUID();
|
|
4404
4446
|
element.setAttribute("id", this.id);
|
|
4405
4447
|
}
|
|
4406
4448
|
|
|
@@ -4484,7 +4526,7 @@ class AuroFloatingUI {
|
|
|
4484
4526
|
}
|
|
4485
4527
|
|
|
4486
4528
|
// Selectors for focusable elements
|
|
4487
|
-
const FOCUSABLE_SELECTORS = [
|
|
4529
|
+
const FOCUSABLE_SELECTORS$1 = [
|
|
4488
4530
|
"a[href]",
|
|
4489
4531
|
"button:not([disabled])",
|
|
4490
4532
|
"textarea:not([disabled])",
|
|
@@ -4498,7 +4540,7 @@ const FOCUSABLE_SELECTORS = [
|
|
|
4498
4540
|
];
|
|
4499
4541
|
|
|
4500
4542
|
// List of custom components that are known to be focusable
|
|
4501
|
-
const FOCUSABLE_COMPONENTS = [
|
|
4543
|
+
const FOCUSABLE_COMPONENTS$1 = [
|
|
4502
4544
|
"auro-checkbox",
|
|
4503
4545
|
"auro-radio",
|
|
4504
4546
|
"auro-dropdown",
|
|
@@ -4520,12 +4562,12 @@ const FOCUSABLE_COMPONENTS = [
|
|
|
4520
4562
|
* @param {HTMLElement} element The element to check for focusability.
|
|
4521
4563
|
* @returns {boolean} True if the element is a focusable custom component, false otherwise.
|
|
4522
4564
|
*/
|
|
4523
|
-
function isFocusableComponent(element) {
|
|
4565
|
+
function isFocusableComponent$1(element) {
|
|
4524
4566
|
const componentName = element.tagName.toLowerCase();
|
|
4525
4567
|
|
|
4526
4568
|
// Guard Clause: Element is a focusable component
|
|
4527
4569
|
if (
|
|
4528
|
-
!FOCUSABLE_COMPONENTS.some(
|
|
4570
|
+
!FOCUSABLE_COMPONENTS$1.some(
|
|
4529
4571
|
(name) => element.hasAttribute(name) || componentName === name,
|
|
4530
4572
|
)
|
|
4531
4573
|
)
|
|
@@ -4549,7 +4591,7 @@ function isFocusableComponent(element) {
|
|
|
4549
4591
|
* @param {HTMLElement} element The element whose tabindex to read.
|
|
4550
4592
|
* @returns {?number} The numeric tabindex or null if missing/invalid.
|
|
4551
4593
|
*/
|
|
4552
|
-
function getNumericTabIndex(element) {
|
|
4594
|
+
function getNumericTabIndex$1(element) {
|
|
4553
4595
|
const raw = element.getAttribute("tabindex");
|
|
4554
4596
|
if (raw == null) return null;
|
|
4555
4597
|
|
|
@@ -4565,7 +4607,7 @@ function getNumericTabIndex(element) {
|
|
|
4565
4607
|
* @param {HTMLElement} container The container to search within
|
|
4566
4608
|
* @returns {Array<HTMLElement>} An array of focusable elements within the container.
|
|
4567
4609
|
*/
|
|
4568
|
-
function getFocusableElements(container) {
|
|
4610
|
+
function getFocusableElements$1(container) {
|
|
4569
4611
|
// Get elements in DOM order by walking the tree
|
|
4570
4612
|
const orderedFocusableElements = [];
|
|
4571
4613
|
|
|
@@ -4574,7 +4616,7 @@ function getFocusableElements(container) {
|
|
|
4574
4616
|
// Check if current element is focusable
|
|
4575
4617
|
if (root.nodeType === Node.ELEMENT_NODE) {
|
|
4576
4618
|
// Check if this is a custom component that is focusable
|
|
4577
|
-
const isComponentFocusable = isFocusableComponent(root);
|
|
4619
|
+
const isComponentFocusable = isFocusableComponent$1(root);
|
|
4578
4620
|
|
|
4579
4621
|
if (isComponentFocusable) {
|
|
4580
4622
|
// Add the component itself as a focusable element and don't traverse its shadow DOM
|
|
@@ -4583,7 +4625,7 @@ function getFocusableElements(container) {
|
|
|
4583
4625
|
}
|
|
4584
4626
|
|
|
4585
4627
|
// Check if the element itself matches any selector
|
|
4586
|
-
for (const selector of FOCUSABLE_SELECTORS) {
|
|
4628
|
+
for (const selector of FOCUSABLE_SELECTORS$1) {
|
|
4587
4629
|
if (root.matches?.(selector)) {
|
|
4588
4630
|
orderedFocusableElements.push(root);
|
|
4589
4631
|
break; // Once we know it's focusable, no need to check other selectors
|
|
@@ -4637,20 +4679,20 @@ function getFocusableElements(container) {
|
|
|
4637
4679
|
|
|
4638
4680
|
// First extract elements with valid positive tabindex
|
|
4639
4681
|
const elementsWithTabindex = uniqueElements.filter((el) => {
|
|
4640
|
-
const tabindex = getNumericTabIndex(el);
|
|
4682
|
+
const tabindex = getNumericTabIndex$1(el);
|
|
4641
4683
|
return tabindex !== null && tabindex > 0;
|
|
4642
4684
|
});
|
|
4643
4685
|
|
|
4644
4686
|
// Sort these elements by their tabindex value
|
|
4645
4687
|
elementsWithTabindex.sort((a, b) => {
|
|
4646
|
-
const aIndex = getNumericTabIndex(a) ?? 0;
|
|
4647
|
-
const bIndex = getNumericTabIndex(b) ?? 0;
|
|
4688
|
+
const aIndex = getNumericTabIndex$1(a) ?? 0;
|
|
4689
|
+
const bIndex = getNumericTabIndex$1(b) ?? 0;
|
|
4648
4690
|
return aIndex - bIndex;
|
|
4649
4691
|
});
|
|
4650
4692
|
|
|
4651
4693
|
// Elements without tabindex (preserving their original order)
|
|
4652
4694
|
const elementsWithoutTabindex = uniqueElements.filter((el) => {
|
|
4653
|
-
const tabindex = getNumericTabIndex(el);
|
|
4695
|
+
const tabindex = getNumericTabIndex$1(el);
|
|
4654
4696
|
|
|
4655
4697
|
// Elements without tabindex or with tabindex of 0 stay in DOM order
|
|
4656
4698
|
return tabindex === null || tabindex === 0;
|
|
@@ -5445,7 +5487,7 @@ class AuroHelpText extends i$2 {
|
|
|
5445
5487
|
}
|
|
5446
5488
|
}
|
|
5447
5489
|
|
|
5448
|
-
var formkitVersion = '
|
|
5490
|
+
var formkitVersion = '202609022217';
|
|
5449
5491
|
|
|
5450
5492
|
/**
|
|
5451
5493
|
* AuroElement is the shared base class for layout-aware Auro form components.
|
|
@@ -5745,7 +5787,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
5745
5787
|
*/
|
|
5746
5788
|
focus() {
|
|
5747
5789
|
if (this.isPopoverVisible && this.bibContent) {
|
|
5748
|
-
const focusables = getFocusableElements(this.bibContent);
|
|
5790
|
+
const focusables = getFocusableElements$1(this.bibContent);
|
|
5749
5791
|
if (focusables.length > 0) {
|
|
5750
5792
|
focusables[0].focus();
|
|
5751
5793
|
}
|
|
@@ -5777,7 +5819,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
5777
5819
|
// wrappers like <div> containing a <button>). If none found, try
|
|
5778
5820
|
// focusing the element directly (works for custom elements like
|
|
5779
5821
|
// auro-input that have delegatesFocus or a custom focus() method).
|
|
5780
|
-
const descendants = getFocusableElements(el);
|
|
5822
|
+
const descendants = getFocusableElements$1(el);
|
|
5781
5823
|
if (descendants.length > 0) {
|
|
5782
5824
|
descendants[0].focus();
|
|
5783
5825
|
return;
|
|
@@ -5789,7 +5831,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
5789
5831
|
}
|
|
5790
5832
|
}
|
|
5791
5833
|
// Fallback: try DOM children (non-slotted content)
|
|
5792
|
-
const focusables = getFocusableElements(this.trigger);
|
|
5834
|
+
const focusables = getFocusableElements$1(this.trigger);
|
|
5793
5835
|
if (focusables.length > 0) {
|
|
5794
5836
|
focusables[0].focus();
|
|
5795
5837
|
}
|
|
@@ -6468,7 +6510,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
6468
6510
|
}
|
|
6469
6511
|
|
|
6470
6512
|
// Collect focusable elements from the bib content.
|
|
6471
|
-
const focusables = getFocusableElements(this.bibContent);
|
|
6513
|
+
const focusables = getFocusableElements$1(this.bibContent);
|
|
6472
6514
|
|
|
6473
6515
|
// Fallback: try from slotted content directly
|
|
6474
6516
|
if (!focusables.length) {
|
|
@@ -6477,7 +6519,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
6477
6519
|
|
|
6478
6520
|
for (const node of assignedNodes) {
|
|
6479
6521
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
6480
|
-
focusables.push(...getFocusableElements(node));
|
|
6522
|
+
focusables.push(...getFocusableElements$1(node));
|
|
6481
6523
|
}
|
|
6482
6524
|
}
|
|
6483
6525
|
}
|
|
@@ -6537,7 +6579,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
6537
6579
|
|
|
6538
6580
|
// Move initial focus into the bib content, matching FocusTrap behavior
|
|
6539
6581
|
requestAnimationFrame(() => {
|
|
6540
|
-
const focusables = getFocusableElements(this.bibContent);
|
|
6582
|
+
const focusables = getFocusableElements$1(this.bibContent);
|
|
6541
6583
|
if (focusables.length) {
|
|
6542
6584
|
focusables[0].focus();
|
|
6543
6585
|
}
|
|
@@ -6550,7 +6592,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
6550
6592
|
// popover asynchronously — a synchronous focus() would target
|
|
6551
6593
|
// zero-dimension elements and be silently ignored.
|
|
6552
6594
|
requestAnimationFrame(() => {
|
|
6553
|
-
const focusables = getFocusableElements(this.bibContent);
|
|
6595
|
+
const focusables = getFocusableElements$1(this.bibContent);
|
|
6554
6596
|
if (focusables.length) {
|
|
6555
6597
|
focusables[0].focus();
|
|
6556
6598
|
}
|
|
@@ -6768,7 +6810,7 @@ class AuroDropdown extends AuroElement$1 {
|
|
|
6768
6810
|
if (triggerContentNodes) {
|
|
6769
6811
|
|
|
6770
6812
|
// See if any of them are focusable elements
|
|
6771
|
-
this.triggerContentFocusable = triggerContentNodes.some((node) => getFocusableElements(node).length > 0);
|
|
6813
|
+
this.triggerContentFocusable = triggerContentNodes.some((node) => getFocusableElements$1(node).length > 0);
|
|
6772
6814
|
|
|
6773
6815
|
// If any of them are focusable elements
|
|
6774
6816
|
if (this.triggerContentFocusable) {
|
|
@@ -7530,9 +7572,241 @@ class AuroElement extends i$2 {
|
|
|
7530
7572
|
}
|
|
7531
7573
|
}
|
|
7532
7574
|
|
|
7575
|
+
// Selectors for focusable elements
|
|
7576
|
+
const FOCUSABLE_SELECTORS = [
|
|
7577
|
+
"a[href]",
|
|
7578
|
+
"button:not([disabled])",
|
|
7579
|
+
"textarea:not([disabled])",
|
|
7580
|
+
"input:not([disabled])",
|
|
7581
|
+
"select:not([disabled])",
|
|
7582
|
+
'[role="tab"]:not([disabled])',
|
|
7583
|
+
'[role="link"]:not([disabled])',
|
|
7584
|
+
'[role="button"]:not([disabled])',
|
|
7585
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
7586
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
7587
|
+
];
|
|
7588
|
+
|
|
7589
|
+
// List of custom components that are known to be focusable
|
|
7590
|
+
const FOCUSABLE_COMPONENTS = [
|
|
7591
|
+
"auro-checkbox",
|
|
7592
|
+
"auro-radio",
|
|
7593
|
+
"auro-dropdown",
|
|
7594
|
+
"auro-button",
|
|
7595
|
+
"auro-combobox",
|
|
7596
|
+
"auro-input",
|
|
7597
|
+
"auro-counter",
|
|
7598
|
+
// 'auro-menu', // Auro menu is not focusable by default, it uses a different interaction model
|
|
7599
|
+
"auro-select",
|
|
7600
|
+
"auro-datepicker",
|
|
7601
|
+
"auro-hyperlink",
|
|
7602
|
+
"auro-accordion",
|
|
7603
|
+
];
|
|
7604
|
+
|
|
7605
|
+
/**
|
|
7606
|
+
* Determines if a given element is a custom focusable component.
|
|
7607
|
+
* Returns true if the element matches a known focusable component and is not disabled.
|
|
7608
|
+
*
|
|
7609
|
+
* @param {HTMLElement} element The element to check for focusability.
|
|
7610
|
+
* @returns {boolean} True if the element is a focusable custom component, false otherwise.
|
|
7611
|
+
*/
|
|
7612
|
+
function isFocusableComponent(element) {
|
|
7613
|
+
const componentName = element.tagName.toLowerCase();
|
|
7614
|
+
|
|
7615
|
+
// Guard Clause: Element is a focusable component
|
|
7616
|
+
if (
|
|
7617
|
+
!FOCUSABLE_COMPONENTS.some(
|
|
7618
|
+
(name) => element.hasAttribute(name) || componentName === name,
|
|
7619
|
+
)
|
|
7620
|
+
)
|
|
7621
|
+
return false;
|
|
7622
|
+
|
|
7623
|
+
// Guard Clause: Element is not disabled
|
|
7624
|
+
if (element.hasAttribute("disabled")) return false;
|
|
7625
|
+
|
|
7626
|
+
// Guard Clause: The element is a hyperlink and has no href attribute
|
|
7627
|
+
if (componentName.match("hyperlink") && !element.hasAttribute("href"))
|
|
7628
|
+
return false;
|
|
7629
|
+
|
|
7630
|
+
// If all guard clauses pass, the element is a focusable component
|
|
7631
|
+
return true;
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
/**
|
|
7635
|
+
* Safely get a numeric tabindex for an element.
|
|
7636
|
+
* Returns a number if the tabindex is a valid integer, otherwise null.
|
|
7637
|
+
*
|
|
7638
|
+
* @param {HTMLElement} element The element whose tabindex to read.
|
|
7639
|
+
* @returns {?number} The numeric tabindex or null if missing/invalid.
|
|
7640
|
+
*/
|
|
7641
|
+
function getNumericTabIndex(element) {
|
|
7642
|
+
const raw = element.getAttribute("tabindex");
|
|
7643
|
+
if (raw == null) return null;
|
|
7644
|
+
|
|
7645
|
+
const value = Number.parseInt(raw, 10);
|
|
7646
|
+
return Number.isNaN(value) ? null : value;
|
|
7647
|
+
}
|
|
7648
|
+
|
|
7649
|
+
/**
|
|
7650
|
+
* Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
|
|
7651
|
+
* Returns a unique, ordered array of elements that can receive focus.
|
|
7652
|
+
* Also sorts elements with tabindex first, preserving their order.
|
|
7653
|
+
*
|
|
7654
|
+
* @param {HTMLElement} container The container to search within
|
|
7655
|
+
* @returns {Array<HTMLElement>} An array of focusable elements within the container.
|
|
7656
|
+
*/
|
|
7657
|
+
function getFocusableElements(container) {
|
|
7658
|
+
// Get elements in DOM order by walking the tree
|
|
7659
|
+
const orderedFocusableElements = [];
|
|
7660
|
+
|
|
7661
|
+
// Define a recursive function to collect focusable elements in DOM order
|
|
7662
|
+
const collectFocusableElements = (root) => {
|
|
7663
|
+
// Check if current element is focusable
|
|
7664
|
+
if (root.nodeType === Node.ELEMENT_NODE) {
|
|
7665
|
+
// Check if this is a custom component that is focusable
|
|
7666
|
+
const isComponentFocusable = isFocusableComponent(root);
|
|
7667
|
+
|
|
7668
|
+
if (isComponentFocusable) {
|
|
7669
|
+
// Add the component itself as a focusable element and don't traverse its shadow DOM
|
|
7670
|
+
orderedFocusableElements.push(root);
|
|
7671
|
+
return; // Skip traversing inside this component
|
|
7672
|
+
}
|
|
7673
|
+
|
|
7674
|
+
// Check if the element itself matches any selector
|
|
7675
|
+
for (const selector of FOCUSABLE_SELECTORS) {
|
|
7676
|
+
if (root.matches?.(selector)) {
|
|
7677
|
+
orderedFocusableElements.push(root);
|
|
7678
|
+
break; // Once we know it's focusable, no need to check other selectors
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
|
|
7682
|
+
// Process shadow DOM only for non-Auro components
|
|
7683
|
+
if (root.shadowRoot) {
|
|
7684
|
+
// Process shadow DOM children in order
|
|
7685
|
+
if (root.shadowRoot.children) {
|
|
7686
|
+
Array.from(root.shadowRoot.children).forEach((child) => {
|
|
7687
|
+
collectFocusableElements(child);
|
|
7688
|
+
});
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
|
|
7692
|
+
// Process slots and their assigned nodes in order
|
|
7693
|
+
if (root.tagName === "SLOT") {
|
|
7694
|
+
const assignedNodes = root.assignedNodes({ flatten: true });
|
|
7695
|
+
for (const node of assignedNodes) {
|
|
7696
|
+
collectFocusableElements(node);
|
|
7697
|
+
}
|
|
7698
|
+
} else {
|
|
7699
|
+
// Process light DOM children in order
|
|
7700
|
+
if (root.children) {
|
|
7701
|
+
Array.from(root.children).forEach((child) => {
|
|
7702
|
+
collectFocusableElements(child);
|
|
7703
|
+
});
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
};
|
|
7708
|
+
|
|
7709
|
+
// Start the traversal from the container
|
|
7710
|
+
collectFocusableElements(container);
|
|
7711
|
+
|
|
7712
|
+
// Remove duplicates that might have been collected through different paths
|
|
7713
|
+
// while preserving order
|
|
7714
|
+
const uniqueElements = [];
|
|
7715
|
+
const seen = new Set();
|
|
7716
|
+
|
|
7717
|
+
for (const element of orderedFocusableElements) {
|
|
7718
|
+
if (!seen.has(element)) {
|
|
7719
|
+
seen.add(element);
|
|
7720
|
+
uniqueElements.push(element);
|
|
7721
|
+
}
|
|
7722
|
+
}
|
|
7723
|
+
|
|
7724
|
+
// Move tab-indexed elements to the front while preserving their order
|
|
7725
|
+
// This ensures that elements with tabindex are prioritized in the focus order
|
|
7726
|
+
|
|
7727
|
+
// First extract elements with valid positive tabindex
|
|
7728
|
+
const elementsWithTabindex = uniqueElements.filter((el) => {
|
|
7729
|
+
const tabindex = getNumericTabIndex(el);
|
|
7730
|
+
return tabindex !== null && tabindex > 0;
|
|
7731
|
+
});
|
|
7732
|
+
|
|
7733
|
+
// Sort these elements by their tabindex value
|
|
7734
|
+
elementsWithTabindex.sort((a, b) => {
|
|
7735
|
+
const aIndex = getNumericTabIndex(a) ?? 0;
|
|
7736
|
+
const bIndex = getNumericTabIndex(b) ?? 0;
|
|
7737
|
+
return aIndex - bIndex;
|
|
7738
|
+
});
|
|
7739
|
+
|
|
7740
|
+
// Elements without tabindex (preserving their original order)
|
|
7741
|
+
const elementsWithoutTabindex = uniqueElements.filter((el) => {
|
|
7742
|
+
const tabindex = getNumericTabIndex(el);
|
|
7743
|
+
|
|
7744
|
+
// Elements without tabindex or with tabindex of 0 stay in DOM order
|
|
7745
|
+
return tabindex === null || tabindex === 0;
|
|
7746
|
+
});
|
|
7747
|
+
|
|
7748
|
+
// Combine both arrays with tabindex elements first
|
|
7749
|
+
const tabIndexedUniqueElements = [
|
|
7750
|
+
...elementsWithTabindex,
|
|
7751
|
+
...elementsWithoutTabindex,
|
|
7752
|
+
];
|
|
7753
|
+
|
|
7754
|
+
return tabIndexedUniqueElements;
|
|
7755
|
+
}
|
|
7756
|
+
|
|
7533
7757
|
// Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
|
|
7534
7758
|
// See LICENSE in the project root for license information.
|
|
7535
7759
|
|
|
7760
|
+
|
|
7761
|
+
/**
|
|
7762
|
+
* Whether `el` is currently visible enough to receive focus. Prefers the
|
|
7763
|
+
* browser's `checkVisibility()`; otherwise combines a display/visibility check
|
|
7764
|
+
* with an ancestor-hidden probe that keeps position:fixed elements visible.
|
|
7765
|
+
* @param {Element} el - The element to test.
|
|
7766
|
+
* @returns {boolean}
|
|
7767
|
+
*/
|
|
7768
|
+
function isVisibleForFocus(el) {
|
|
7769
|
+
if (typeof el.checkVisibility === 'function') {
|
|
7770
|
+
return el.checkVisibility();
|
|
7771
|
+
}
|
|
7772
|
+
const style = el.ownerDocument.defaultView.getComputedStyle(el);
|
|
7773
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
7774
|
+
return false;
|
|
7775
|
+
}
|
|
7776
|
+
return el.offsetParent !== null || style.position === 'fixed';
|
|
7777
|
+
}
|
|
7778
|
+
|
|
7779
|
+
/**
|
|
7780
|
+
* Return the tab stop that comes before the counter-group in page tab order,
|
|
7781
|
+
* skipping any hidden entries the walker doesn't filter itself. The group host
|
|
7782
|
+
* itself is not a tab stop; its focusables (the internal dropdown and the
|
|
7783
|
+
* counters) are, so anchor the search on the group's earliest focusable.
|
|
7784
|
+
* @param {Element} component - The auro-counter-group host element.
|
|
7785
|
+
* @returns {Element|null}
|
|
7786
|
+
*/
|
|
7787
|
+
function getPreviousTabStop(component) {
|
|
7788
|
+
const tabStops = getFocusableElements(component.ownerDocument.body);
|
|
7789
|
+
const counters = component.counters || [];
|
|
7790
|
+
const owned = [
|
|
7791
|
+
component,
|
|
7792
|
+
component.dropdown,
|
|
7793
|
+
...counters
|
|
7794
|
+
].filter(Boolean);
|
|
7795
|
+
let groupStart = tabStops.length;
|
|
7796
|
+
owned.forEach((node) => {
|
|
7797
|
+
const index = tabStops.indexOf(node);
|
|
7798
|
+
if (index >= 0 && index < groupStart) {
|
|
7799
|
+
groupStart = index;
|
|
7800
|
+
}
|
|
7801
|
+
});
|
|
7802
|
+
for (let index = groupStart - 1; index >= 0; index -= 1) {
|
|
7803
|
+
if (isVisibleForFocus(tabStops[index])) {
|
|
7804
|
+
return tabStops[index];
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
return null;
|
|
7808
|
+
}
|
|
7809
|
+
|
|
7536
7810
|
const counterGroupKeyboardStrategy = {
|
|
7537
7811
|
Escape(component, evt) {
|
|
7538
7812
|
if (!component.dropdown || !component.dropdown.isPopoverVisible) {
|
|
@@ -7546,6 +7820,48 @@ const counterGroupKeyboardStrategy = {
|
|
|
7546
7820
|
|
|
7547
7821
|
component.dropdown.hide();
|
|
7548
7822
|
},
|
|
7823
|
+
|
|
7824
|
+
Tab(component, evt, ctx) {
|
|
7825
|
+
// Forward Tab already exits past the bib content and the focusout handler
|
|
7826
|
+
// closes the bib. Fullscreen relies on the dialog's native focus trapping.
|
|
7827
|
+
if (!ctx.isExpanded || ctx.isModal || !evt.shiftKey) {
|
|
7828
|
+
return;
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7831
|
+
// The trigger precedes the bib content in the flattened tab order, so a
|
|
7832
|
+
// native Shift+Tab off the first counter lands back on the trigger and
|
|
7833
|
+
// leaves the bib open. Only intercept when focus is on the first focusable
|
|
7834
|
+
// in the bib; otherwise let the browser move to the previous counter.
|
|
7835
|
+
// `composedPath()` reports the real event origin through every shadow
|
|
7836
|
+
// boundary, unlike `document.activeElement`, which retargets to the first
|
|
7837
|
+
// slotted counter and cannot tell the counters apart.
|
|
7838
|
+
const focusables = getFocusableElements(component.dropdown.bibContent);
|
|
7839
|
+
if (!focusables.length) {
|
|
7840
|
+
return;
|
|
7841
|
+
}
|
|
7842
|
+
|
|
7843
|
+
const path = evt.composedPath();
|
|
7844
|
+
const activeIndex = focusables.findIndex((el) => path.includes(el));
|
|
7845
|
+
if (activeIndex !== 0) {
|
|
7846
|
+
return;
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7849
|
+
const previousTabStop = getPreviousTabStop(component);
|
|
7850
|
+
|
|
7851
|
+
// Always suppress the native Shift+Tab. Otherwise it fires against the bib
|
|
7852
|
+
// as it is torn down and lands focus on the trigger (which precedes the
|
|
7853
|
+
// first counter), re-opening the exact loop this handler prevents. When
|
|
7854
|
+
// nothing precedes the group, focus falls to the body instead of the
|
|
7855
|
+
// trigger, which reads as exiting the group backward.
|
|
7856
|
+
evt.preventDefault();
|
|
7857
|
+
component.hideBib();
|
|
7858
|
+
|
|
7859
|
+
if (!previousTabStop) {
|
|
7860
|
+
return;
|
|
7861
|
+
}
|
|
7862
|
+
|
|
7863
|
+
doubleRaf(() => previousTabStop.focus());
|
|
7864
|
+
},
|
|
7549
7865
|
};
|
|
7550
7866
|
|
|
7551
7867
|
/* eslint-disable lit/no-invalid-html, lit/binding-positions, max-lines, no-underscore-dangle, arrow-parens, no-confusing-arrow, curly, dot-location, no-inline-comments, line-comment-position,
|