@aurodesignsystem-dev/auro-formkit 0.0.0-pr1346.2 → 0.0.0-pr1346.4

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 (40) hide show
  1. package/components/checkbox/demo/api.min.js +1 -1
  2. package/components/checkbox/demo/index.min.js +1 -1
  3. package/components/checkbox/dist/index.js +1 -1
  4. package/components/checkbox/dist/registered.js +1 -1
  5. package/components/combobox/demo/api.min.js +252 -65
  6. package/components/combobox/demo/index.min.js +252 -65
  7. package/components/combobox/dist/auro-combobox.d.ts +2 -1
  8. package/components/combobox/dist/index.js +245 -62
  9. package/components/combobox/dist/registered.js +245 -62
  10. package/components/counter/demo/api.min.js +124 -12
  11. package/components/counter/demo/index.min.js +124 -12
  12. package/components/counter/dist/index.js +124 -12
  13. package/components/counter/dist/registered.js +124 -12
  14. package/components/datepicker/demo/api.min.js +161 -40
  15. package/components/datepicker/demo/index.min.js +161 -40
  16. package/components/datepicker/dist/index.js +161 -40
  17. package/components/datepicker/dist/registered.js +161 -40
  18. package/components/dropdown/demo/api.min.js +122 -10
  19. package/components/dropdown/demo/index.min.js +122 -10
  20. package/components/dropdown/dist/auro-dropdownBib.d.ts +18 -2
  21. package/components/dropdown/dist/index.js +122 -10
  22. package/components/dropdown/dist/registered.js +122 -10
  23. package/components/input/demo/api.min.js +38 -29
  24. package/components/input/demo/index.min.js +38 -29
  25. package/components/input/dist/index.js +38 -29
  26. package/components/input/dist/registered.js +38 -29
  27. package/components/menu/demo/api.min.js +7 -3
  28. package/components/menu/demo/index.min.js +7 -3
  29. package/components/menu/dist/index.js +7 -3
  30. package/components/menu/dist/registered.js +7 -3
  31. package/components/radio/demo/api.min.js +1 -1
  32. package/components/radio/demo/index.min.js +1 -1
  33. package/components/radio/dist/index.js +1 -1
  34. package/components/radio/dist/registered.js +1 -1
  35. package/components/select/demo/api.min.js +159 -17
  36. package/components/select/demo/index.min.js +159 -17
  37. package/components/select/dist/index.js +152 -14
  38. package/components/select/dist/registered.js +152 -14
  39. package/custom-elements.json +35 -10
  40. package/package.json +1 -1
@@ -3810,7 +3810,7 @@ let p$3 = class p{registerComponent(t,a){customElements.get(t)||customElements.d
3810
3810
 
3811
3811
  var iconVersion$2 = '9.1.2';
3812
3812
 
3813
- var styleCss$2$1 = css`:host{position:fixed;z-index:var(--depth-tooltip, 400);display:none;isolation:isolate}:host dialog{max-width:none;max-height:none;padding:0;border:none;margin:0;outline:none;transform:translateZ(0)}:host dialog::backdrop{background:transparent}:host(:not([isfullscreen])) dialog{position:absolute;inset:unset}:host(:not([isfullscreen])) .container.shape-box{border-radius:unset}:host(:not([isfullscreen])) .container[class*=shape-pill],:host(:not([isfullscreen])) .container[class*=shape-snowflake]{border-radius:30px}:host(:not([isfullscreen])) .container[class*=shape-rounded]{border-radius:16px}:host(:not([matchWidth])) .container{min-width:fit-content}:host([isfullscreen]){position:fixed;top:0;left:0}:host([isfullscreen]) .container{width:100dvw;max-width:none;height:100dvh;max-height:none;border-radius:unset;margin-top:0;box-shadow:unset;overscroll-behavior:contain}:host([data-show]){display:flex}:host([common]:not([isfullscreen])) .container,:host([rounded]:not([isfullscreen])) .container{border-radius:var(--ds-border-radius, 0.375rem)}:host([common][isfullscreen]) .container,:host([rounded][isfullscreen]) .container{border-radius:unset;box-shadow:unset}.container{display:inline-block;overflow:auto;box-sizing:border-box;border-radius:var(--ds-border-radius, 0.375rem);margin:var(--ds-size-50, 0.25rem) 0}.util_displayHiddenVisually{position:absolute;overflow:hidden;width:1px;height:1px;padding:0;border:0;margin:-1px;clip-path:inset(50%);white-space:nowrap}`;
3813
+ var styleCss$2$1 = css`:host{position:fixed;z-index:var(--depth-tooltip, 400);display:none;isolation:isolate}:host dialog{max-width:none;max-height:none;padding:0;border:none;margin:0;outline:none;transform:translateZ(0)}:host dialog::backdrop{background:transparent}:host(:not([isfullscreen])) dialog{position:absolute;inset:unset}:host(:not([isfullscreen])) .container.shape-box{border-radius:unset}:host(:not([isfullscreen])) .container[class*=shape-pill],:host(:not([isfullscreen])) .container[class*=shape-snowflake]{border-radius:30px}:host(:not([isfullscreen])) .container[class*=shape-rounded]{border-radius:16px}:host(:not([matchWidth])) .container{min-width:fit-content}:host([isfullscreen]){position:fixed;top:0;left:0}:host([isfullscreen]) .container{width:100dvw;max-width:none;height:100dvh;max-height:none;border-radius:unset;margin-top:0;box-shadow:unset;overscroll-behavior:contain}:host([isfullscreen]) .container::backdrop{background:var(--ds-color-background-primary, #fff)}:host([data-show]){display:flex}:host([common]:not([isfullscreen])) .container,:host([rounded]:not([isfullscreen])) .container{border-radius:var(--ds-border-radius, 0.375rem)}:host([common][isfullscreen]) .container,:host([rounded][isfullscreen]) .container{border-radius:unset;box-shadow:unset}.container{display:inline-block;overflow:auto;box-sizing:border-box;border-radius:var(--ds-border-radius, 0.375rem);margin:var(--ds-size-50, 0.25rem) 0}.util_displayHiddenVisually{position:absolute;overflow:hidden;width:1px;height:1px;padding:0;border:0;margin:-1px;clip-path:inset(50%);white-space:nowrap}`;
3814
3814
 
3815
3815
  var colorCss$2$1 = css`.container{background-color:var(--ds-auro-dropdownbib-container-color);box-shadow:var(--ds-auro-dropdownbib-boxshadow-color);color:var(--ds-auro-dropdownbib-text-color)}`;
3816
3816
 
@@ -3818,6 +3818,8 @@ var tokensCss$1$1 = css`:host(:not([ondark])),:host(:not([appearance=inverse])){
3818
3818
 
3819
3819
  // Copyright (c) 2020 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
3820
3820
  // See LICENSE in the project root for license information.
3821
+ /* eslint-disable max-lines */
3822
+ // ---------------------------------------------------------------------
3821
3823
 
3822
3824
 
3823
3825
  const DESIGN_TOKEN_BREAKPOINT_PREFIX = '--ds-grid-breakpoint-';
@@ -3993,22 +3995,63 @@ class AuroDropdownBib extends LitElement {
3993
3995
  // Handle ESC key via dialog's cancel event
3994
3996
  const dialog = this.shadowRoot.querySelector('dialog');
3995
3997
  dialog.addEventListener('cancel', (event) => {
3996
- event.preventDefault(); // Let parent handle closing
3998
+ // Let parent handle closing
3999
+ event.preventDefault();
3997
4000
  this.dispatchEvent(new CustomEvent('auro-bib-cancel', {
3998
4001
  bubbles: true,
3999
4002
  composed: true
4000
4003
  }));
4001
4004
  });
4002
4005
 
4003
- // Re-dispatch navigation keyboard events so they cross the shadow DOM
4004
- // boundary and reach the combobox/select key handlers.
4005
- // Only intercept keys used for menu navigation let all other keys
4006
- // (characters, Backspace, etc.) through so typing in inputs works.
4007
- const navKeys = new Set(['ArrowUp', 'ArrowDown', 'Enter', 'Escape']);
4006
+ // showModal() creates a closed focus scope keyboard events inside
4007
+ // the dialog's shadow DOM do NOT bubble out to the combobox/select
4008
+ // keydown handlers in the parent shadow DOM. This handler bridges
4009
+ // that gap by re-dispatching navigation keys so they cross the
4010
+ // shadow boundary and reach the menu navigation logic in the parent
4011
+ // component.
4012
+ //
4013
+ // The trade-off: intercepting these keys means native keyboard
4014
+ // behaviors that would normally "just work" must be manually
4015
+ // re-implemented here:
4016
+ //
4017
+ // - Enter on buttons: Custom elements (auro-button) don't get the
4018
+ // native Enter→click that <button> provides, so we call .click()
4019
+ // directly when Enter is pressed on a button-like element.
4020
+ //
4021
+ // - Tab: NOT intercepted — left to the browser's native focus trap
4022
+ // provided by showModal(), which cycles Tab between focusable
4023
+ // elements inside the dialog (e.g. the input and close button).
4024
+ // Intercepting Tab would kill the native focus trap and break
4025
+ // focus management inside the dialog.
4026
+ //
4027
+ // - Escape: The native <dialog> fires a `cancel` event on ESC
4028
+ // (handled above), so the re-dispatched Escape is a secondary
4029
+ // path for parent components that also listen for Escape keydown.
4030
+ const navKeys = new Set([
4031
+ 'ArrowUp',
4032
+ 'ArrowDown',
4033
+ 'Enter',
4034
+ 'Escape'
4035
+ ]);
4008
4036
  dialog.addEventListener('keydown', (event) => {
4009
4037
  if (!navKeys.has(event.key)) {
4010
4038
  return;
4011
4039
  }
4040
+
4041
+ // Custom elements (auro-button) don't get the native Enter→click
4042
+ // behavior that <button> has. Find the button in the composed path
4043
+ // and click it directly.
4044
+ if (event.key === 'Enter') {
4045
+ const buttonSelector = 'button, [role="button"], auro-button, [auro-button]';
4046
+ const btn = event.composedPath().find((el) => el.matches && el.matches(buttonSelector));
4047
+ if (btn) {
4048
+ event.preventDefault();
4049
+ event.stopPropagation();
4050
+ btn.click();
4051
+ return;
4052
+ }
4053
+ }
4054
+
4012
4055
  event.preventDefault();
4013
4056
  event.stopPropagation();
4014
4057
  const newEvent = new KeyboardEvent('keydown', {
@@ -4032,9 +4075,55 @@ class AuroDropdownBib extends LitElement {
4032
4075
  }
4033
4076
 
4034
4077
  /**
4035
- * Opens the dialog using showModal() for accessibility.
4036
- * @param {boolean} modal - If true, uses showModal() (default). If false, uses show().
4078
+ * Blocks touch-driven page scroll while a fullscreen modal dialog is open.
4079
+ *
4080
+ * The showModal() function places the dialog in the browser's **top layer**,
4081
+ * which is a separate rendering layer above the normal DOM. On mobile, the
4082
+ * compositor processes visual-viewport panning before top-layer touch
4083
+ * handling. This means the entire viewport — including the top-layer dialog
4084
+ * — can be panned by a touch gesture, causing the page behind the dialog to
4085
+ * scroll into view. To prevent this, we add a touchmove listener that cancels
4086
+ * the event if the touch started outside the dialog or any scrollable child within it.
4087
+ *
4088
+ * @private
4089
+ */
4090
+ _lockTouchScroll() {
4091
+ const dialog = this.shadowRoot.querySelector('dialog');
4092
+
4093
+ this._touchMoveHandler = (event) => {
4094
+ // Walk the composed path (which crosses shadow DOM boundaries) to
4095
+ // check whether the touch started inside a scrollable element that
4096
+ // lives within the dialog. If so, allow the scroll.
4097
+ for (const el of event.composedPath()) {
4098
+ if (el === dialog) {
4099
+ // Reached the dialog boundary without finding a scrollable child.
4100
+ break;
4101
+ }
4102
+ if (el instanceof HTMLElement && el.scrollHeight > el.clientHeight) {
4103
+ const { overflowY } = getComputedStyle(el);
4104
+ if (overflowY === 'auto' || overflowY === 'scroll') {
4105
+ return;
4106
+ }
4107
+ }
4108
+ }
4109
+
4110
+ event.preventDefault();
4111
+ };
4112
+
4113
+ document.addEventListener('touchmove', this._touchMoveHandler, { passive: false });
4114
+ }
4115
+
4116
+ /**
4117
+ * Removes the touchmove listener added by _lockTouchScroll().
4118
+ * @private
4037
4119
  */
4120
+ _unlockTouchScroll() {
4121
+ if (this._touchMoveHandler) {
4122
+ document.removeEventListener('touchmove', this._touchMoveHandler);
4123
+ this._touchMoveHandler = undefined;
4124
+ }
4125
+ }
4126
+
4038
4127
  open(modal = true) {
4039
4128
  const dialog = this.shadowRoot.querySelector('dialog');
4040
4129
  if (dialog && !dialog.open) {
@@ -4051,6 +4140,8 @@ class AuroDropdownBib extends LitElement {
4051
4140
 
4052
4141
  documentElement.style.overflow = prevOverflow;
4053
4142
 
4143
+ this._lockTouchScroll();
4144
+
4054
4145
  } else {
4055
4146
  // Use setAttribute instead of dialog.show() to avoid the dialog
4056
4147
  // focusing steps which steal focus from the trigger and cause
@@ -4066,6 +4157,7 @@ class AuroDropdownBib extends LitElement {
4066
4157
  close() {
4067
4158
  const dialog = this.shadowRoot.querySelector('dialog');
4068
4159
  if (dialog && dialog.open) {
4160
+ this._unlockTouchScroll();
4069
4161
  dialog.close();
4070
4162
  }
4071
4163
  }
@@ -4331,7 +4423,7 @@ let AuroHelpText$2 = class AuroHelpText extends LitElement {
4331
4423
  }
4332
4424
  };
4333
4425
 
4334
- var formkitVersion$2 = '202602201708';
4426
+ var formkitVersion$2 = '202602260152';
4335
4427
 
4336
4428
  let AuroElement$2 = class AuroElement extends LitElement {
4337
4429
  static get properties() {
@@ -4604,6 +4696,18 @@ class AuroDropdown extends AuroElement$2 {
4604
4696
  */
4605
4697
  show() {
4606
4698
  this.floater.showBib();
4699
+
4700
+ // Open dialog synchronously so callers remain in the user gesture
4701
+ // chain. This is critical for mobile browsers (iOS Safari) to keep
4702
+ // the virtual keyboard open when transitioning from the trigger
4703
+ // input to an input inside the fullscreen dialog. Without this,
4704
+ // showModal() fires asynchronously via Lit's update cycle, which
4705
+ // falls outside the user activation window and causes iOS to
4706
+ // dismiss the keyboard.
4707
+ if (this.isBibFullscreen && this.bibElement && this.bibElement.value) {
4708
+ const useModal = !this.disableFocusTrap;
4709
+ this.bibElement.value.open(useModal);
4710
+ }
4607
4711
  }
4608
4712
 
4609
4713
  /**
@@ -4990,6 +5094,14 @@ class AuroDropdown extends AuroElement$2 {
4990
5094
  this.bibElement.value.close();
4991
5095
  }
4992
5096
  }
5097
+
5098
+ // When fullscreen strategy changes while open, re-open dialog with correct mode
5099
+ // (e.g. resizing from desktop → mobile while dropdown is open)
5100
+ if (changedProperties.has('isBibFullscreen') && this.isPopoverVisible && this.bibElement.value) {
5101
+ const useModal = this.isBibFullscreen && !this.disableFocusTrap;
5102
+ this.bibElement.value.close();
5103
+ this.bibElement.value.open(useModal);
5104
+ }
4993
5105
  }
4994
5106
 
4995
5107
  /**
@@ -10484,6 +10596,15 @@ class BaseInput extends AuroElement$1 {
10484
10596
  constructor() {
10485
10597
  super();
10486
10598
 
10599
+ // Delegate focus to the native <input> inside the shadow root so that
10600
+ // showModal()'s dialog focusing steps reach the input element.
10601
+ // This keeps the mobile virtual keyboard open when the fullscreen dialog
10602
+ // opens, because the browser sees an input-to-input focus transfer.
10603
+ this.constructor.shadowRootOptions = {
10604
+ ...AuroElement$1.shadowRootOptions,
10605
+ delegatesFocus: true,
10606
+ };
10607
+
10487
10608
  this._initializeDefaults();
10488
10609
  }
10489
10610
 
@@ -10605,14 +10726,6 @@ class BaseInput extends AuroElement$1 {
10605
10726
  reflect: true
10606
10727
  },
10607
10728
 
10608
- /**
10609
- * The value for the aria-controls attribute.
10610
- */
10611
- a11yControls: {
10612
- type: String,
10613
- reflect: true
10614
- },
10615
-
10616
10729
  /**
10617
10730
  * The value for the aria-activedescendant attribute.
10618
10731
  * Points to the ID of the currently active/highlighted option in a listbox.
@@ -11305,31 +11418,34 @@ class BaseInput extends AuroElement$1 {
11305
11418
  // Process credit card type detection and formatting during input
11306
11419
  if (this.type === 'credit-card') {
11307
11420
  this.processCreditCard();
11308
- }
11421
+ this.touched = true;
11422
+ this.validation.validate(this);
11423
+ } else {
11309
11424
 
11310
- // Sets value property to value of element value (el.value).
11311
- this.value = this.inputElement.value;
11425
+ // Sets value property to value of element value (el.value).
11426
+ this.value = this.inputElement.value;
11312
11427
 
11313
- // Determine if the value change was programmatic, including autofill.
11314
- const inputWasProgrammatic = !this.matches(":focus") || event.isProgrammatic;
11428
+ // Determine if the value change was programmatic, including autofill.
11429
+ const inputWasProgrammatic = !this.matches(":focus") || event.isProgrammatic;
11315
11430
 
11316
- // Validation on input or programmatic value change (including autofill).
11317
- if (this.validateOnInput || inputWasProgrammatic) {
11318
- this.touched = true;
11319
- this.validation.validate(this);
11320
- }
11431
+ // Validation on input or programmatic value change (including autofill).
11432
+ if (this.validateOnInput || inputWasProgrammatic) {
11433
+ this.touched = true;
11434
+ this.validation.validate(this);
11435
+ }
11321
11436
 
11322
- // Prevents cursor jumping in Safari.
11323
- const { selectionStart } = this.inputElement;
11437
+ // Prevents cursor jumping in Safari.
11438
+ const { selectionStart } = this.inputElement;
11324
11439
 
11325
- if (this.setSelectionInputTypes.includes(this.type)) {
11326
- this.updateComplete.then(() => {
11327
- try {
11328
- this.inputElement.setSelectionRange(selectionStart, selectionStart);
11329
- } catch (error) { // eslint-disable-line
11330
- // do nothing
11331
- }
11332
- });
11440
+ if (this.setSelectionInputTypes.includes(this.type)) {
11441
+ this.updateComplete.then(() => {
11442
+ try {
11443
+ this.inputElement.setSelectionRange(selectionStart, selectionStart);
11444
+ } catch (error) { // eslint-disable-line
11445
+ // do nothing
11446
+ }
11447
+ });
11448
+ }
11333
11449
  }
11334
11450
  }
11335
11451
 
@@ -11362,6 +11478,11 @@ class BaseInput extends AuroElement$1 {
11362
11478
  this.inputElement.scrollLeft = 100;
11363
11479
 
11364
11480
  if (!this.noValidate) {
11481
+ // For credit card inputs with mask, ensure value is synced from mask instance
11482
+ if (this.type === 'credit-card' && this.maskInstance) {
11483
+ this.value = this.maskInstance.value;
11484
+ }
11485
+
11365
11486
  this.validation.validate(this);
11366
11487
  }
11367
11488
  }
@@ -12024,7 +12145,7 @@ let AuroHelpText$1 = class AuroHelpText extends LitElement {
12024
12145
  }
12025
12146
  };
12026
12147
 
12027
- var formkitVersion$1 = '202602201708';
12148
+ var formkitVersion$1 = '202602260152';
12028
12149
 
12029
12150
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
12030
12151
  // See LICENSE in the project root for license information.
@@ -13051,7 +13172,7 @@ class AuroBibtemplate extends LitElement {
13051
13172
  }
13052
13173
  }
13053
13174
 
13054
- var formkitVersion = '202602201708';
13175
+ var formkitVersion = '202602260152';
13055
13176
 
13056
13177
  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}`;
13057
13178
 
@@ -13469,6 +13590,7 @@ class AuroCombobox extends AuroElement {
13469
13590
  this.dropdownOpen = false;
13470
13591
  this.triggerExpandedState = false;
13471
13592
  this._expandedTimeout = null;
13593
+ this._inFullscreenTransition = false;
13472
13594
  this.errorMessage = null;
13473
13595
  this.isHiddenWhileLoading = false;
13474
13596
  this.largeFullscreenHeadline = false;
@@ -13489,7 +13611,7 @@ class AuroCombobox extends AuroElement {
13489
13611
 
13490
13612
  /**
13491
13613
  * Defines whether the component will be on lighter or darker backgrounds.
13492
- * @property {'default' | 'inverse'}
13614
+ * @property {'default' | 'inverse'} appearance - The visual appearance of the component.
13493
13615
  * @default 'default'
13494
13616
  */
13495
13617
  appearance: {
@@ -14044,7 +14166,7 @@ class AuroCombobox extends AuroElement {
14044
14166
  * @returns {void}
14045
14167
  */
14046
14168
  showBib() {
14047
- if (!this.input.value) {
14169
+ if (!this.input.value && !this.dropdown.isBibFullscreen) {
14048
14170
  this.dropdown.hide();
14049
14171
  return;
14050
14172
  }
@@ -14113,6 +14235,18 @@ class AuroCombobox extends AuroElement {
14113
14235
 
14114
14236
  // Restore trigger accessibility when closing fullscreen
14115
14237
  this.dropdown.trigger.inert = false;
14238
+
14239
+ // Restore focus to the trigger input after closing the
14240
+ // fullscreen dialog. The browser's native dialog focus restoration
14241
+ // fails because the trigger was set to inert before showModal().
14242
+ // Use rAF to run after Lit's microtask update cycle calls dialog.close().
14243
+ if (this.dropdown.isBibFullscreen) {
14244
+ requestAnimationFrame(() => {
14245
+ if (!this.dropdown.isPopoverVisible) {
14246
+ this.input.focus();
14247
+ }
14248
+ });
14249
+ }
14116
14250
  }
14117
14251
 
14118
14252
  if (this.dropdownOpen) {
@@ -14122,6 +14256,19 @@ class AuroCombobox extends AuroElement {
14122
14256
  this.updateBibDialogRole();
14123
14257
 
14124
14258
  if (this.dropdown.isBibFullscreen) {
14259
+ // Guard against spurious validation during the focus transition
14260
+ // from trigger to bib input. Setting trigger.inert = true removes
14261
+ // focus, which fires focusout on the child auro-input before the
14262
+ // bib input receives focus. That focusout triggers the input's own
14263
+ // validate(), which dispatches a composed auroFormElement-validated
14264
+ // event. Because composed events are retargetted at each shadow DOM
14265
+ // boundary, the event appears to originate from the combobox itself
14266
+ // and its listener unconditionally sets this.validity — causing
14267
+ // premature validation. This flag suppresses all validation paths
14268
+ // (focusout handler, handleInputValueChange, validate(), and the
14269
+ // auroFormElement-validated listener) until focus settles in the bib.
14270
+ this._inFullscreenTransition = true;
14271
+
14125
14272
  // Hide the trigger from assistive technology so VoiceOver cannot reach it
14126
14273
  // behind the fullscreen dialog
14127
14274
  this.dropdown.trigger.inert = true;
@@ -14153,6 +14300,7 @@ class AuroCombobox extends AuroElement {
14153
14300
  requestAnimationFrame(() => {
14154
14301
  requestAnimationFrame(() => {
14155
14302
  this.setInputFocus();
14303
+ this._inFullscreenTransition = false;
14156
14304
  });
14157
14305
  });
14158
14306
  } else {
@@ -14247,7 +14395,7 @@ class AuroCombobox extends AuroElement {
14247
14395
  * @private
14248
14396
  */
14249
14397
  updateBibDialogRole() {
14250
- const bibEl = this.dropdown.bibElement?.value;
14398
+ const bibEl = this.dropdown.bibElement && this.dropdown.bibElement.value;
14251
14399
  if (!bibEl) {
14252
14400
  return;
14253
14401
  }
@@ -14376,9 +14524,10 @@ class AuroCombobox extends AuroElement {
14376
14524
  // Announce the selection after the dropdown closes so it isn't
14377
14525
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
14378
14526
  const selectedValue = event.detail.stringValue;
14527
+ const announcementDelay = 300;
14379
14528
  setTimeout(() => {
14380
14529
  this.announceToScreenReader(`${selectedValue}, selected`);
14381
- }, 300);
14530
+ }, announcementDelay);
14382
14531
  }
14383
14532
  });
14384
14533
 
@@ -14426,7 +14575,7 @@ class AuroCombobox extends AuroElement {
14426
14575
  * Validate every time we remove focus from the combo box.
14427
14576
  */
14428
14577
  this.addEventListener('focusout', () => {
14429
- if (!this.componentHasFocus) {
14578
+ if (!this.componentHasFocus && !this._inFullscreenTransition) {
14430
14579
  this.validate();
14431
14580
  }
14432
14581
  });
@@ -14477,8 +14626,11 @@ class AuroCombobox extends AuroElement {
14477
14626
  }
14478
14627
  this.handleMenuOptions();
14479
14628
 
14480
- // Validate only if the value was set programmatically
14481
- if (!this.componentHasFocus) {
14629
+ // Validate only if the value was set programmatically (not during user
14630
+ // interaction). In fullscreen dialog mode, componentHasFocus returns false
14631
+ // because focus is inside the top-layer dialog, so also check
14632
+ // dropdownOpen and the fullscreen transition flag.
14633
+ if (!this.componentHasFocus && !this.dropdownOpen && !this._inFullscreenTransition) {
14482
14634
  this.validate();
14483
14635
  }
14484
14636
 
@@ -14493,6 +14645,22 @@ class AuroCombobox extends AuroElement {
14493
14645
  this.hideBib();
14494
14646
  }
14495
14647
 
14648
+ // iOS virtual keyboard retention: when in fullscreen mode, ensure the
14649
+ // dialog opens and the bib input is focused synchronously within the
14650
+ // input event (user gesture) chain. Without this, Lit's async update
14651
+ // cycle delays showModal() past the user activation window, causing
14652
+ // iOS Safari to dismiss the virtual keyboard when the fullscreen
14653
+ // dialog opens — the user then has to tap the input again to resume
14654
+ // typing.
14655
+ if (this.dropdown.isBibFullscreen && this.input.value && this.input.value.length > 0) {
14656
+ if (!this.dropdown.isPopoverVisible) {
14657
+ this.showBib();
14658
+ }
14659
+ if (this.dropdown.isPopoverVisible) {
14660
+ this.setInputFocus();
14661
+ }
14662
+ }
14663
+
14496
14664
  this.dispatchEvent(new CustomEvent('inputValue', { detail: { value: this.inputValue } }));
14497
14665
  }
14498
14666
 
@@ -14518,23 +14686,20 @@ class AuroCombobox extends AuroElement {
14518
14686
  }
14519
14687
 
14520
14688
  if (evt.key === 'Tab' && this.dropdown.isPopoverVisible) {
14521
- if (this.dropdown.isBibFullscreen) {
14522
-
14523
- // when focus is on the input, move focus back to close button with Tab key
14524
- if (document.activeElement.shadowRoot.activeElement === this.inputInBib) {
14525
- evt.preventDefault();
14526
- this.dropdown.focus();
14527
- }
14528
- } else {
14689
+ // Non-fullscreen (combobox pattern per WAI-ARIA APG):
14690
+ // Tab accepts the focused option and closes the popup, moving focus
14691
+ // to the next focusable element on the page.
14692
+ // https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
14693
+ //
14694
+ // Fullscreen (dialog / modal pattern): Tab navigates between
14695
+ // focusable elements inside the modal (e.g. the close button) via
14696
+ // the native focus trap provided by showModal(). The dropdown is
14697
+ // closed by the close button or Escape instead.
14698
+ if (!this.dropdown.isBibFullscreen) {
14529
14699
  if (this.menu.optionActive && this.menu.optionActive.value) {
14530
14700
  this.menu.value = this.menu.optionActive.value;
14531
14701
  }
14532
-
14533
- setTimeout(() => {
14534
- if (!this.componentHasFocus) {
14535
- this.hideBib();
14536
- }
14537
- }, 0);
14702
+ this.hideBib();
14538
14703
  }
14539
14704
  }
14540
14705
 
@@ -14565,6 +14730,14 @@ class AuroCombobox extends AuroElement {
14565
14730
  });
14566
14731
 
14567
14732
  this.addEventListener('auroFormElement-validated', (evt) => {
14733
+ // During the fullscreen transition, child elements (auro-input) may fire
14734
+ // their own validation events when the trigger becomes inert and loses
14735
+ // focus. Those composed events bubble up through shadow DOM boundaries
14736
+ // and would incorrectly set combobox validity. Ignore them.
14737
+ if (this._inFullscreenTransition) {
14738
+ return;
14739
+ }
14740
+
14568
14741
  this.input.validity = evt.detail.validity;
14569
14742
  this.input.errorMessage = evt.detail.message;
14570
14743
  this.validity = evt.detail.validity;
@@ -14668,6 +14841,9 @@ class AuroCombobox extends AuroElement {
14668
14841
  * @param {boolean} [force=false] - Whether to force validation.
14669
14842
  */
14670
14843
  validate(force = false) {
14844
+ if (this._inFullscreenTransition) {
14845
+ return;
14846
+ }
14671
14847
  this.validation.validate(this, force);
14672
14848
  }
14673
14849
 
@@ -14721,7 +14897,13 @@ class AuroCombobox extends AuroElement {
14721
14897
  }
14722
14898
 
14723
14899
  if (changedProperties.has('availableOptions')) {
14724
- if ((this.availableOptions.length > 0 && this.componentHasFocus) || this.menu.loading || (this.availableOptions.length === 0 && this.noMatchOption)) {
14900
+ // dropdownOpen is set synchronously by the auroDropdown-toggled event
14901
+ // handler during showBib() → floater.showBib() → dispatchEventDropdownToggle(),
14902
+ // so it's already true by the time updated() runs. This prevents the else
14903
+ // branch from calling hideBib() when the dropdown was just opened but
14904
+ // :focus-within hasn't propagated through the top-layer dialog's nested
14905
+ // shadow DOM boundaries.
14906
+ if ((this.availableOptions.length > 0 && (this.componentHasFocus || this.dropdownOpen)) || this.menu.loading || (this.availableOptions.length === 0 && this.noMatchOption)) {
14725
14907
  this.showBib();
14726
14908
  } else {
14727
14909
  this.hideBib();
@@ -14896,6 +15078,7 @@ class AuroCombobox extends AuroElement {
14896
15078
  <slot @slotchange="${this.handleSlotChange}"></slot>
14897
15079
  <${this.inputTag}
14898
15080
  id="inputInBib"
15081
+ autofocus
14899
15082
  @input="${this.handleInputValueChange}"
14900
15083
  .a11yActivedescendant="${this.dropdownOpen && this.optionActive ? this.optionActive.id : undefined}"
14901
15084
  .a11yControls=${`${this.dropdownId}-floater-bib`}