@aurodesignsystem-dev/auro-formkit 0.0.0-pr1551.0 → 0.0.0-pr1552.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/components/checkbox/demo/customize.min.js +1 -1
  2. package/components/checkbox/demo/getting-started.min.js +1 -1
  3. package/components/checkbox/demo/index.min.js +1 -1
  4. package/components/checkbox/dist/index.js +1 -1
  5. package/components/checkbox/dist/registered.js +1 -1
  6. package/components/combobox/demo/customize.min.js +207 -46
  7. package/components/combobox/demo/getting-started.min.js +207 -46
  8. package/components/combobox/demo/index.min.js +207 -46
  9. package/components/combobox/dist/index.js +207 -46
  10. package/components/combobox/dist/registered.js +207 -46
  11. package/components/counter/demo/customize.min.js +206 -45
  12. package/components/counter/demo/index.min.js +206 -45
  13. package/components/counter/dist/index.js +206 -45
  14. package/components/counter/dist/registered.js +206 -45
  15. package/components/datepicker/demo/customize.min.js +207 -46
  16. package/components/datepicker/demo/index.min.js +207 -46
  17. package/components/datepicker/dist/index.js +207 -46
  18. package/components/datepicker/dist/registered.js +207 -46
  19. package/components/dropdown/demo/customize.min.js +205 -44
  20. package/components/dropdown/demo/getting-started.min.js +205 -44
  21. package/components/dropdown/demo/index.min.js +205 -44
  22. package/components/dropdown/dist/index.js +205 -44
  23. package/components/dropdown/dist/registered.js +205 -44
  24. package/components/form/demo/customize.min.js +829 -185
  25. package/components/form/demo/getting-started.min.js +829 -185
  26. package/components/form/demo/index.min.js +829 -185
  27. package/components/form/demo/registerDemoDeps.min.js +829 -185
  28. package/components/input/demo/customize.min.js +1 -1
  29. package/components/input/demo/getting-started.min.js +1 -1
  30. package/components/input/demo/index.min.js +1 -1
  31. package/components/input/dist/index.js +1 -1
  32. package/components/input/dist/registered.js +1 -1
  33. package/components/radio/demo/customize.min.js +1 -1
  34. package/components/radio/demo/getting-started.min.js +1 -1
  35. package/components/radio/demo/index.min.js +1 -1
  36. package/components/radio/dist/index.js +1 -1
  37. package/components/radio/dist/registered.js +1 -1
  38. package/components/select/demo/customize.min.js +206 -45
  39. package/components/select/demo/getting-started.min.js +206 -45
  40. package/components/select/demo/index.min.js +206 -45
  41. package/components/select/dist/index.js +206 -45
  42. package/components/select/dist/registered.js +206 -45
  43. package/package.json +2 -2
@@ -3453,40 +3453,12 @@ class AuroFloatingUI {
3453
3453
  return;
3454
3454
  }
3455
3455
 
3456
- // Chrome-specific: during popover top-layer promotion after a click on a
3457
- // slotted focusable, :focus-within can briefly evaluate false while the
3458
- // active element is still structurally inside the trigger/bib. Fall back
3459
- // to a shadow-piercing ancestry walk from the deep active element before
3460
- // treating this as a real focus loss.
3461
- try {
3462
- let active = document.activeElement;
3463
- while (active && active.shadowRoot && active.shadowRoot.activeElement) {
3464
- active = active.shadowRoot.activeElement;
3465
- }
3466
- const targets = [element, element.trigger, element.bib].filter(Boolean);
3467
- let node = active;
3468
- while (node) {
3469
- if (targets.includes(node)) {
3470
- return;
3471
- }
3472
- node =
3473
- node.parentElement ||
3474
- (node.getRootNode && node.getRootNode().host) ||
3475
- null;
3476
- }
3477
- } catch (e) {
3478
- // Defensive: fall through to the existing close path if traversal fails.
3479
- }
3480
-
3481
3456
  // if fullscreen bib is in fullscreen mode, do not close
3482
3457
  if (element.bib.hasAttribute("isfullscreen")) {
3483
3458
  return;
3484
3459
  }
3485
3460
 
3486
- // eventType "focusloss" distinguishes a Tab/click-driven close from an
3487
- // Escape keydown close. Consumers use this to decide whether to restore
3488
- // focus to the trigger (Escape) or let it advance naturally (Tab).
3489
- this.hideBib("focusloss");
3461
+ this.hideBib("keydown");
3490
3462
  }
3491
3463
 
3492
3464
  setupHideHandlers() {
@@ -4156,6 +4128,199 @@ function getFocusableElements(container) {
4156
4128
  return tabIndexedUniqueElements;
4157
4129
  }
4158
4130
 
4131
+ /**
4132
+ * FocusTrap manages keyboard focus within a specified container element, ensuring that focus does not leave the container when tabbing.
4133
+ * It is commonly used for modal dialogs or overlays to improve accessibility by trapping focus within interactive UI components.
4134
+ */
4135
+ class FocusTrap {
4136
+ /**
4137
+ * Creates a new FocusTrap instance for the given container element.
4138
+ * Initializes event listeners and prepares the container for focus management.
4139
+ *
4140
+ * @param {HTMLElement} container The DOM element to trap focus within.
4141
+ * @param {boolean} [controlTabOrder=false] If true enables manual control of the tab order by the FocusTrap.
4142
+ * @throws {Error} If the provided container is not a valid HTMLElement.
4143
+ */
4144
+ constructor(container, controlTabOrder = false) {
4145
+ if (!container || !(container instanceof HTMLElement)) {
4146
+ throw new Error("FocusTrap requires a valid HTMLElement.");
4147
+ }
4148
+
4149
+ this.container = container;
4150
+ this.tabDirection = "forward"; // or 'backward';
4151
+ this.controlTabOrder = controlTabOrder;
4152
+
4153
+ this._init();
4154
+ }
4155
+
4156
+ /**
4157
+ * Initializes the focus trap by setting up event listeners and attributes on the container.
4158
+ * Prepares the container for focus management, including support for shadow DOM and inert attributes.
4159
+ *
4160
+ * @private
4161
+ */
4162
+ _init() {
4163
+ // Add inert attribute to prevent focusing programmatically as well (if supported)
4164
+ if ("inert" in HTMLElement.prototype) {
4165
+ this.container.inert = false; // Ensure the container isn't inert
4166
+ this.container.setAttribute("data-focus-trap-container", true); // Mark for identification
4167
+ }
4168
+
4169
+ // Track tab direction
4170
+ this.container.addEventListener("keydown", this._onKeydown);
4171
+ }
4172
+
4173
+ /**
4174
+ * Gets an array of currently active (focused) elements in the document and shadow DOM.
4175
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4176
+ * @private
4177
+ */
4178
+ _getActiveElements() {
4179
+ // Get the active element(s) in the document and shadow root
4180
+ // This will include the active element in the shadow DOM if it exists
4181
+ // Active element may be inside the shadow DOM depending on delegatesFocus, so we need to check both
4182
+ let { activeElement } = document;
4183
+ const actives = [activeElement];
4184
+ while (activeElement?.shadowRoot?.activeElement) {
4185
+ actives.push(activeElement.shadowRoot.activeElement);
4186
+ activeElement = activeElement.shadowRoot.activeElement;
4187
+ }
4188
+ return actives;
4189
+ }
4190
+
4191
+ /**
4192
+ * Gets the next focus index based on the current index and focusable elements.
4193
+ * @param {number} currentIndex The current index of the focused element.
4194
+ * @param {Array<HTMLElement>} focusables The array of focusable elements.
4195
+ * @returns {number|null} The next focus index or null if not determined.
4196
+ */
4197
+ _getNextFocusIndex(currentIndex, focusables, actives) {
4198
+ if (this.controlTabOrder) {
4199
+ // Calculate the new index based on the current index and tab direction
4200
+ let newFocusIndex =
4201
+ currentIndex + (this.tabDirection === "forward" ? 1 : -1);
4202
+
4203
+ // Wrap-around logic
4204
+ if (newFocusIndex < 0) newFocusIndex = focusables.length - 1;
4205
+ if (newFocusIndex >= focusables.length) newFocusIndex = 0;
4206
+
4207
+ // Early return with the new index
4208
+ return newFocusIndex;
4209
+ }
4210
+
4211
+ // Determine if we need to wrap
4212
+ const atFirst =
4213
+ actives.includes(focusables[0]) || actives.includes(this.container);
4214
+ const atLast = actives.includes(focusables[focusables.length - 1]);
4215
+
4216
+ // Only wrap if at the ends
4217
+ if (this.tabDirection === "backward" && atFirst) {
4218
+ return focusables.length - 1;
4219
+ }
4220
+
4221
+ if (this.tabDirection === "forward" && atLast) {
4222
+ return 0;
4223
+ }
4224
+
4225
+ // No wrap, so don't change focus, return early
4226
+ return null;
4227
+ }
4228
+
4229
+ /**
4230
+ * Handles the Tab key press event to manage focus within the container.
4231
+ * @param {KeyboardEvent} e The keyboard event triggered by the user.
4232
+ * @returns {void}
4233
+ */
4234
+ _handleTabKey(e) {
4235
+ // Update the focusable elements
4236
+ const focusables = this._getFocusableElements();
4237
+
4238
+ // If there are no focusable elements, exit
4239
+ if (!focusables.length) return;
4240
+
4241
+ // Set the tab direction based on the key pressed
4242
+ this.tabDirection = e.shiftKey ? "backward" : "forward";
4243
+
4244
+ // Get the active elements that are currently focused
4245
+ const actives = this._getActiveElements();
4246
+
4247
+ // If we're at either end of the focusable elements, wrap around to the other end
4248
+ let focusIndex = focusables.findIndex((el) => actives.includes(el));
4249
+
4250
+ // Fallback if we have no focused element
4251
+ if (focusIndex === -1) focusIndex = 0;
4252
+
4253
+ // Get the next focus index based on the current focus index, tab direction, and controlTabOrder setting
4254
+ // Is null if no new focus index is determined
4255
+ const newFocusIndex = this._getNextFocusIndex(
4256
+ focusIndex,
4257
+ focusables,
4258
+ actives,
4259
+ );
4260
+
4261
+ // If we have a new focus index, set focus to that element
4262
+ if (newFocusIndex !== null) {
4263
+ e.preventDefault();
4264
+ focusables[newFocusIndex].focus();
4265
+ }
4266
+ }
4267
+
4268
+ /**
4269
+ * Catches the keydown event
4270
+ * @param {KeyboardEvent} e The keyboard event triggered by user interaction.
4271
+ * @private
4272
+ */
4273
+ _onKeydown = (e) => {
4274
+ // Handle tab
4275
+ if (e.key === "Tab") this._handleTabKey(e);
4276
+ };
4277
+
4278
+ /**
4279
+ * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
4280
+ * Returns a unique, ordered array of elements that can receive focus.
4281
+ *
4282
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4283
+ * @private
4284
+ */
4285
+ _getFocusableElements() {
4286
+ // Use the imported utility function to get focusable elements
4287
+ const elements = getFocusableElements(this.container);
4288
+
4289
+ // Return the elements found
4290
+ return elements;
4291
+ }
4292
+
4293
+ /**
4294
+ * Moves focus to the first focusable element within the container.
4295
+ * Useful for setting initial focus when activating the focus trap.
4296
+ */
4297
+ focusFirstElement() {
4298
+ const focusables = this._getFocusableElements();
4299
+ if (focusables.length) focusables[0].focus();
4300
+ }
4301
+
4302
+ /**
4303
+ * Moves focus to the last focusable element within the container.
4304
+ * Useful for setting focus when deactivating or cycling focus in reverse.
4305
+ */
4306
+ focusLastElement() {
4307
+ const focusables = this._getFocusableElements();
4308
+ if (focusables.length) focusables[focusables.length - 1].focus();
4309
+ }
4310
+
4311
+ /**
4312
+ * Removes event listeners and attributes added by the focus trap.
4313
+ * Call this method to clean up when the focus trap is no longer needed.
4314
+ */
4315
+ disconnect() {
4316
+ if (this.container.hasAttribute("data-focus-trap-container")) {
4317
+ this.container.removeAttribute("data-focus-trap-container");
4318
+ }
4319
+
4320
+ this.container.removeEventListener("keydown", this._onKeydown);
4321
+ }
4322
+ }
4323
+
4159
4324
  // Copyright (c) Alaska Air. All right reserved. Licensed under the Apache-2.0 license
4160
4325
  // See LICENSE in the project root for license information.
4161
4326
 
@@ -4653,7 +4818,7 @@ class AuroDropdownBib extends LitElement {
4653
4818
  classes[`shape-${this.shape}`] = true;
4654
4819
 
4655
4820
  return html`
4656
- <dialog tabindex="-1" class="${classMap(classes)}" part="bibContainer" role="${ifDefined(this.dialogRole)}" aria-labelledby="${ifDefined(this.dialogLabel ? 'dialogLabel' : undefined)}">
4821
+ <dialog class="${classMap(classes)}" part="bibContainer" role="${ifDefined(this.dialogRole)}" aria-labelledby="${ifDefined(this.dialogLabel ? 'dialogLabel' : undefined)}">
4657
4822
  ${this.dialogLabel ? html`<span id="dialogLabel" class="util_displayHiddenVisually">${this.dialogLabel}</span>` : ''}
4658
4823
  <slot></slot>
4659
4824
  <span id="srAnnouncement" class="util_displayHiddenVisually" aria-live="polite" role="status"></span>
@@ -4904,7 +5069,7 @@ let AuroHelpText$2 = class AuroHelpText extends LitElement {
4904
5069
  }
4905
5070
  };
4906
5071
 
4907
- var formkitVersion$2 = '202607100029';
5072
+ var formkitVersion$2 = '202607092329';
4908
5073
 
4909
5074
  let AuroElement$2 = class AuroElement extends LitElement {
4910
5075
  static get properties() {
@@ -5658,10 +5823,7 @@ class AuroDropdown extends AuroElement$2 {
5658
5823
  }
5659
5824
 
5660
5825
 
5661
- // On Tab-driven close (eventType "focusloss"), let focus advance naturally
5662
- // — restoring to the trigger would trap the user on this dropdown, forcing
5663
- // an extra Tab to move on. Escape and outside-click still restore.
5664
- if (!this.isPopoverVisible && eventType !== "focusloss") {
5826
+ if (!this.isPopoverVisible) {
5665
5827
  // wait til the bib gets fully closed and rendered
5666
5828
  setTimeout(() => {
5667
5829
  // Skip if the bib re-opened, or if focus moved intentionally outside the dropdown (not to body).
@@ -5891,16 +6053,15 @@ class AuroDropdown extends AuroElement$2 {
5891
6053
  }
5892
6054
  });
5893
6055
  } else {
5894
- // Normal desktop (non-modal): move initial focus into the bib but
5895
- // don't trap Tab. Tab should exit the bib and let the floater's
5896
- // handleFocusLoss close it, matching native <select>/<details>
5897
- // behavior. Deferred one frame because Floating UI positions the
5898
- // popover asynchronously a synchronous focus() would target
5899
- // zero-dimension elements and be silently ignored.
6056
+ // Normal desktop: use FocusTrap on the bib element.
6057
+ // Defer focusFirstElement to the next frame because the popover
6058
+ // is positioned asynchronously by Floating UI — calling focus()
6059
+ // synchronously here would target elements with zero dimensions
6060
+ // and the focus call would be silently ignored.
6061
+ this.focusTrap = new FocusTrap(this.bibContent);
5900
6062
  requestAnimationFrame(() => {
5901
- const focusables = getFocusableElements(this.bibContent);
5902
- if (focusables.length) {
5903
- focusables[0].focus();
6063
+ if (this.focusTrap) {
6064
+ this.focusTrap.focusFirstElement();
5904
6065
  }
5905
6066
  });
5906
6067
  }
@@ -18526,7 +18687,7 @@ let AuroHelpText$1 = class AuroHelpText extends LitElement {
18526
18687
  }
18527
18688
  };
18528
18689
 
18529
- var formkitVersion$1 = '202607100029';
18690
+ var formkitVersion$1 = '202607092329';
18530
18691
 
18531
18692
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18532
18693
  // See LICENSE in the project root for license information.
@@ -19647,7 +19808,7 @@ class AuroBibtemplate extends LitElement {
19647
19808
  }
19648
19809
  }
19649
19810
 
19650
- var formkitVersion = '202607100029';
19811
+ var formkitVersion = '202607092329';
19651
19812
 
19652
19813
  var styleCss$1 = css`.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}`;
19653
19814
 
@@ -1114,7 +1114,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$2 {
1114
1114
  }
1115
1115
  };
1116
1116
 
1117
- var formkitVersion$1 = '202607100029';
1117
+ var formkitVersion$1 = '202607092329';
1118
1118
 
1119
1119
  // Copyright (c) 2026 Alaska Airlines. All rights reserved. Licensed under the Apache-2.0 license
1120
1120
  // See LICENSE in the project root for license information.
@@ -3844,40 +3844,12 @@ class AuroFloatingUI {
3844
3844
  return;
3845
3845
  }
3846
3846
 
3847
- // Chrome-specific: during popover top-layer promotion after a click on a
3848
- // slotted focusable, :focus-within can briefly evaluate false while the
3849
- // active element is still structurally inside the trigger/bib. Fall back
3850
- // to a shadow-piercing ancestry walk from the deep active element before
3851
- // treating this as a real focus loss.
3852
- try {
3853
- let active = document.activeElement;
3854
- while (active && active.shadowRoot && active.shadowRoot.activeElement) {
3855
- active = active.shadowRoot.activeElement;
3856
- }
3857
- const targets = [element, element.trigger, element.bib].filter(Boolean);
3858
- let node = active;
3859
- while (node) {
3860
- if (targets.includes(node)) {
3861
- return;
3862
- }
3863
- node =
3864
- node.parentElement ||
3865
- (node.getRootNode && node.getRootNode().host) ||
3866
- null;
3867
- }
3868
- } catch (e) {
3869
- // Defensive: fall through to the existing close path if traversal fails.
3870
- }
3871
-
3872
3847
  // if fullscreen bib is in fullscreen mode, do not close
3873
3848
  if (element.bib.hasAttribute("isfullscreen")) {
3874
3849
  return;
3875
3850
  }
3876
3851
 
3877
- // eventType "focusloss" distinguishes a Tab/click-driven close from an
3878
- // Escape keydown close. Consumers use this to decide whether to restore
3879
- // focus to the trigger (Escape) or let it advance naturally (Tab).
3880
- this.hideBib("focusloss");
3852
+ this.hideBib("keydown");
3881
3853
  }
3882
3854
 
3883
3855
  setupHideHandlers() {
@@ -4547,6 +4519,199 @@ function getFocusableElements(container) {
4547
4519
  return tabIndexedUniqueElements;
4548
4520
  }
4549
4521
 
4522
+ /**
4523
+ * FocusTrap manages keyboard focus within a specified container element, ensuring that focus does not leave the container when tabbing.
4524
+ * It is commonly used for modal dialogs or overlays to improve accessibility by trapping focus within interactive UI components.
4525
+ */
4526
+ class FocusTrap {
4527
+ /**
4528
+ * Creates a new FocusTrap instance for the given container element.
4529
+ * Initializes event listeners and prepares the container for focus management.
4530
+ *
4531
+ * @param {HTMLElement} container The DOM element to trap focus within.
4532
+ * @param {boolean} [controlTabOrder=false] If true enables manual control of the tab order by the FocusTrap.
4533
+ * @throws {Error} If the provided container is not a valid HTMLElement.
4534
+ */
4535
+ constructor(container, controlTabOrder = false) {
4536
+ if (!container || !(container instanceof HTMLElement)) {
4537
+ throw new Error("FocusTrap requires a valid HTMLElement.");
4538
+ }
4539
+
4540
+ this.container = container;
4541
+ this.tabDirection = "forward"; // or 'backward';
4542
+ this.controlTabOrder = controlTabOrder;
4543
+
4544
+ this._init();
4545
+ }
4546
+
4547
+ /**
4548
+ * Initializes the focus trap by setting up event listeners and attributes on the container.
4549
+ * Prepares the container for focus management, including support for shadow DOM and inert attributes.
4550
+ *
4551
+ * @private
4552
+ */
4553
+ _init() {
4554
+ // Add inert attribute to prevent focusing programmatically as well (if supported)
4555
+ if ("inert" in HTMLElement.prototype) {
4556
+ this.container.inert = false; // Ensure the container isn't inert
4557
+ this.container.setAttribute("data-focus-trap-container", true); // Mark for identification
4558
+ }
4559
+
4560
+ // Track tab direction
4561
+ this.container.addEventListener("keydown", this._onKeydown);
4562
+ }
4563
+
4564
+ /**
4565
+ * Gets an array of currently active (focused) elements in the document and shadow DOM.
4566
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4567
+ * @private
4568
+ */
4569
+ _getActiveElements() {
4570
+ // Get the active element(s) in the document and shadow root
4571
+ // This will include the active element in the shadow DOM if it exists
4572
+ // Active element may be inside the shadow DOM depending on delegatesFocus, so we need to check both
4573
+ let { activeElement } = document;
4574
+ const actives = [activeElement];
4575
+ while (activeElement?.shadowRoot?.activeElement) {
4576
+ actives.push(activeElement.shadowRoot.activeElement);
4577
+ activeElement = activeElement.shadowRoot.activeElement;
4578
+ }
4579
+ return actives;
4580
+ }
4581
+
4582
+ /**
4583
+ * Gets the next focus index based on the current index and focusable elements.
4584
+ * @param {number} currentIndex The current index of the focused element.
4585
+ * @param {Array<HTMLElement>} focusables The array of focusable elements.
4586
+ * @returns {number|null} The next focus index or null if not determined.
4587
+ */
4588
+ _getNextFocusIndex(currentIndex, focusables, actives) {
4589
+ if (this.controlTabOrder) {
4590
+ // Calculate the new index based on the current index and tab direction
4591
+ let newFocusIndex =
4592
+ currentIndex + (this.tabDirection === "forward" ? 1 : -1);
4593
+
4594
+ // Wrap-around logic
4595
+ if (newFocusIndex < 0) newFocusIndex = focusables.length - 1;
4596
+ if (newFocusIndex >= focusables.length) newFocusIndex = 0;
4597
+
4598
+ // Early return with the new index
4599
+ return newFocusIndex;
4600
+ }
4601
+
4602
+ // Determine if we need to wrap
4603
+ const atFirst =
4604
+ actives.includes(focusables[0]) || actives.includes(this.container);
4605
+ const atLast = actives.includes(focusables[focusables.length - 1]);
4606
+
4607
+ // Only wrap if at the ends
4608
+ if (this.tabDirection === "backward" && atFirst) {
4609
+ return focusables.length - 1;
4610
+ }
4611
+
4612
+ if (this.tabDirection === "forward" && atLast) {
4613
+ return 0;
4614
+ }
4615
+
4616
+ // No wrap, so don't change focus, return early
4617
+ return null;
4618
+ }
4619
+
4620
+ /**
4621
+ * Handles the Tab key press event to manage focus within the container.
4622
+ * @param {KeyboardEvent} e The keyboard event triggered by the user.
4623
+ * @returns {void}
4624
+ */
4625
+ _handleTabKey(e) {
4626
+ // Update the focusable elements
4627
+ const focusables = this._getFocusableElements();
4628
+
4629
+ // If there are no focusable elements, exit
4630
+ if (!focusables.length) return;
4631
+
4632
+ // Set the tab direction based on the key pressed
4633
+ this.tabDirection = e.shiftKey ? "backward" : "forward";
4634
+
4635
+ // Get the active elements that are currently focused
4636
+ const actives = this._getActiveElements();
4637
+
4638
+ // If we're at either end of the focusable elements, wrap around to the other end
4639
+ let focusIndex = focusables.findIndex((el) => actives.includes(el));
4640
+
4641
+ // Fallback if we have no focused element
4642
+ if (focusIndex === -1) focusIndex = 0;
4643
+
4644
+ // Get the next focus index based on the current focus index, tab direction, and controlTabOrder setting
4645
+ // Is null if no new focus index is determined
4646
+ const newFocusIndex = this._getNextFocusIndex(
4647
+ focusIndex,
4648
+ focusables,
4649
+ actives,
4650
+ );
4651
+
4652
+ // If we have a new focus index, set focus to that element
4653
+ if (newFocusIndex !== null) {
4654
+ e.preventDefault();
4655
+ focusables[newFocusIndex].focus();
4656
+ }
4657
+ }
4658
+
4659
+ /**
4660
+ * Catches the keydown event
4661
+ * @param {KeyboardEvent} e The keyboard event triggered by user interaction.
4662
+ * @private
4663
+ */
4664
+ _onKeydown = (e) => {
4665
+ // Handle tab
4666
+ if (e.key === "Tab") this._handleTabKey(e);
4667
+ };
4668
+
4669
+ /**
4670
+ * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
4671
+ * Returns a unique, ordered array of elements that can receive focus.
4672
+ *
4673
+ * @returns {Array<HTMLElement>} An array of focusable elements within the container.
4674
+ * @private
4675
+ */
4676
+ _getFocusableElements() {
4677
+ // Use the imported utility function to get focusable elements
4678
+ const elements = getFocusableElements(this.container);
4679
+
4680
+ // Return the elements found
4681
+ return elements;
4682
+ }
4683
+
4684
+ /**
4685
+ * Moves focus to the first focusable element within the container.
4686
+ * Useful for setting initial focus when activating the focus trap.
4687
+ */
4688
+ focusFirstElement() {
4689
+ const focusables = this._getFocusableElements();
4690
+ if (focusables.length) focusables[0].focus();
4691
+ }
4692
+
4693
+ /**
4694
+ * Moves focus to the last focusable element within the container.
4695
+ * Useful for setting focus when deactivating or cycling focus in reverse.
4696
+ */
4697
+ focusLastElement() {
4698
+ const focusables = this._getFocusableElements();
4699
+ if (focusables.length) focusables[focusables.length - 1].focus();
4700
+ }
4701
+
4702
+ /**
4703
+ * Removes event listeners and attributes added by the focus trap.
4704
+ * Call this method to clean up when the focus trap is no longer needed.
4705
+ */
4706
+ disconnect() {
4707
+ if (this.container.hasAttribute("data-focus-trap-container")) {
4708
+ this.container.removeAttribute("data-focus-trap-container");
4709
+ }
4710
+
4711
+ this.container.removeEventListener("keydown", this._onKeydown);
4712
+ }
4713
+ }
4714
+
4550
4715
  // Copyright (c) Alaska Air. All right reserved. Licensed under the Apache-2.0 license
4551
4716
  // See LICENSE in the project root for license information.
4552
4717
 
@@ -5044,7 +5209,7 @@ class AuroDropdownBib extends i$2 {
5044
5209
  classes[`shape-${this.shape}`] = true;
5045
5210
 
5046
5211
  return u$5`
5047
- <dialog tabindex="-1" class="${e$2(classes)}" part="bibContainer" role="${o$2(this.dialogRole)}" aria-labelledby="${o$2(this.dialogLabel ? 'dialogLabel' : undefined)}">
5212
+ <dialog class="${e$2(classes)}" part="bibContainer" role="${o$2(this.dialogRole)}" aria-labelledby="${o$2(this.dialogLabel ? 'dialogLabel' : undefined)}">
5048
5213
  ${this.dialogLabel ? u$5`<span id="dialogLabel" class="util_displayHiddenVisually">${this.dialogLabel}</span>` : ''}
5049
5214
  <slot></slot>
5050
5215
  <span id="srAnnouncement" class="util_displayHiddenVisually" aria-live="polite" role="status"></span>
@@ -5295,7 +5460,7 @@ class AuroHelpText extends i$2 {
5295
5460
  }
5296
5461
  }
5297
5462
 
5298
- var formkitVersion = '202607100029';
5463
+ var formkitVersion = '202607092329';
5299
5464
 
5300
5465
  let AuroElement$1 = class AuroElement extends i$2 {
5301
5466
  static get properties() {
@@ -6049,10 +6214,7 @@ class AuroDropdown extends AuroElement$1 {
6049
6214
  }
6050
6215
 
6051
6216
 
6052
- // On Tab-driven close (eventType "focusloss"), let focus advance naturally
6053
- // — restoring to the trigger would trap the user on this dropdown, forcing
6054
- // an extra Tab to move on. Escape and outside-click still restore.
6055
- if (!this.isPopoverVisible && eventType !== "focusloss") {
6217
+ if (!this.isPopoverVisible) {
6056
6218
  // wait til the bib gets fully closed and rendered
6057
6219
  setTimeout(() => {
6058
6220
  // Skip if the bib re-opened, or if focus moved intentionally outside the dropdown (not to body).
@@ -6282,16 +6444,15 @@ class AuroDropdown extends AuroElement$1 {
6282
6444
  }
6283
6445
  });
6284
6446
  } else {
6285
- // Normal desktop (non-modal): move initial focus into the bib but
6286
- // don't trap Tab. Tab should exit the bib and let the floater's
6287
- // handleFocusLoss close it, matching native <select>/<details>
6288
- // behavior. Deferred one frame because Floating UI positions the
6289
- // popover asynchronously a synchronous focus() would target
6290
- // zero-dimension elements and be silently ignored.
6447
+ // Normal desktop: use FocusTrap on the bib element.
6448
+ // Defer focusFirstElement to the next frame because the popover
6449
+ // is positioned asynchronously by Floating UI — calling focus()
6450
+ // synchronously here would target elements with zero dimensions
6451
+ // and the focus call would be silently ignored.
6452
+ this.focusTrap = new FocusTrap(this.bibContent);
6291
6453
  requestAnimationFrame(() => {
6292
- const focusables = getFocusableElements(this.bibContent);
6293
- if (focusables.length) {
6294
- focusables[0].focus();
6454
+ if (this.focusTrap) {
6455
+ this.focusTrap.focusFirstElement();
6295
6456
  }
6296
6457
  });
6297
6458
  }