@aurodesignsystem-dev/auro-formkit 0.0.0-pr1507.0 → 0.0.0-pr1508.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 (63) hide show
  1. package/components/checkbox/demo/customize.html +1 -2
  2. package/components/checkbox/demo/customize.min.js +1 -1
  3. package/components/checkbox/demo/getting-started.min.js +1 -1
  4. package/components/checkbox/demo/index.min.js +1 -1
  5. package/components/checkbox/dist/index.js +1 -1
  6. package/components/checkbox/dist/registered.js +1 -1
  7. package/components/combobox/demo/customize.html +1 -2
  8. package/components/combobox/demo/customize.md +108 -132
  9. package/components/combobox/demo/customize.min.js +150 -293
  10. package/components/combobox/demo/getting-started.min.js +150 -293
  11. package/components/combobox/demo/index.min.js +150 -293
  12. package/components/combobox/dist/auro-combobox.d.ts +9 -0
  13. package/components/combobox/dist/index.js +150 -293
  14. package/components/combobox/dist/registered.js +150 -293
  15. package/components/counter/demo/customize.min.js +10 -2
  16. package/components/counter/demo/index.min.js +10 -2
  17. package/components/counter/dist/index.js +10 -2
  18. package/components/counter/dist/registered.js +10 -2
  19. package/components/datepicker/demo/api.md +2 -0
  20. package/components/datepicker/demo/customize.js +0 -2
  21. package/components/datepicker/demo/customize.md +138 -38
  22. package/components/datepicker/demo/customize.min.js +59 -168
  23. package/components/datepicker/demo/index.min.js +59 -150
  24. package/components/datepicker/dist/index.js +59 -150
  25. package/components/datepicker/dist/registered.js +59 -150
  26. package/components/datepicker/dist/src/auro-datepicker.d.ts +2 -0
  27. package/components/dropdown/demo/customize.min.js +9 -1
  28. package/components/dropdown/demo/getting-started.min.js +9 -1
  29. package/components/dropdown/demo/index.min.js +9 -1
  30. package/components/dropdown/dist/auro-dropdown.d.ts +1 -0
  31. package/components/dropdown/dist/index.js +9 -1
  32. package/components/dropdown/dist/registered.js +9 -1
  33. package/components/form/demo/customize.html +6 -6
  34. package/components/form/demo/customize.js +19 -0
  35. package/components/form/demo/customize.md +203 -51
  36. package/components/form/demo/customize.min.js +707 -633
  37. package/components/form/demo/getting-started.min.js +635 -633
  38. package/components/form/demo/index.min.js +635 -633
  39. package/components/form/demo/registerDemoDeps.min.js +272 -596
  40. package/components/form/dist/auro-form.d.ts +122 -4
  41. package/components/form/dist/index.js +363 -37
  42. package/components/form/dist/registered.js +363 -37
  43. package/components/input/demo/customize.html +1 -2
  44. package/components/input/demo/customize.md +54 -53
  45. package/components/input/demo/customize.min.js +41 -147
  46. package/components/input/demo/getting-started.min.js +41 -147
  47. package/components/input/demo/index.min.js +41 -147
  48. package/components/input/dist/base-input.d.ts +1 -49
  49. package/components/input/dist/index.js +41 -147
  50. package/components/input/dist/registered.js +41 -147
  51. package/components/radio/demo/customize.min.js +1 -1
  52. package/components/radio/demo/getting-started.min.js +1 -1
  53. package/components/radio/demo/index.min.js +1 -1
  54. package/components/radio/dist/index.js +1 -1
  55. package/components/radio/dist/registered.js +1 -1
  56. package/components/select/demo/customize.html +1 -2
  57. package/components/select/demo/customize.min.js +10 -2
  58. package/components/select/demo/getting-started.min.js +10 -2
  59. package/components/select/demo/index.min.js +10 -2
  60. package/components/select/dist/index.js +10 -2
  61. package/components/select/dist/registered.js +10 -2
  62. package/custom-elements.json +1592 -1707
  63. package/package.json +1 -1
@@ -767,6 +767,8 @@ function navigateArrow(component, direction, options = {}) {
767
767
  }
768
768
  }
769
769
 
770
+ /* eslint-disable no-underscore-dangle */
771
+
770
772
  /**
771
773
  * Returns the clear button element from the active input's shadow
772
774
  * DOM, if available.
@@ -802,11 +804,9 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
802
804
  * @param {Object} menu - The menu component.
803
805
  */
804
806
  function reconcileMenuIndex(menu) {
805
- // eslint-disable-next-line no-underscore-dangle
806
807
  if (menu._index < 0 && menu.optionActive && menu.items) {
807
808
  const idx = menu.items.indexOf(menu.optionActive);
808
809
  if (idx >= 0) {
809
- // eslint-disable-next-line no-underscore-dangle
810
810
  menu._index = idx;
811
811
  }
812
812
  }
@@ -4806,7 +4806,7 @@ let AuroHelpText$2 = class AuroHelpText extends LitElement {
4806
4806
  }
4807
4807
  };
4808
4808
 
4809
- var formkitVersion$2 = '202606192121';
4809
+ var formkitVersion$2 = '202606231948';
4810
4810
 
4811
4811
  let AuroElement$2 = class AuroElement extends LitElement {
4812
4812
  static get properties() {
@@ -5757,6 +5757,14 @@ class AuroDropdown extends AuroElement$2 {
5757
5757
  };
5758
5758
  this.addEventListener('keydown', this._bibTabHandler);
5759
5759
 
5760
+ // Suppress AuroFloatingUI's auto-hide-on-focus-loss while the
5761
+ // desktopModal trap owns focus management. Without this, the very
5762
+ // first focus move into the bib (from the RAF below) triggers
5763
+ // handleFocusLoss → hideBib, tearing down the trap before the
5764
+ // user can press Tab.
5765
+ this._priorNoHideOnFocusLoss = this.noHideOnThisFocusLoss;
5766
+ this.noHideOnThisFocusLoss = true;
5767
+
5760
5768
  // Move initial focus into the bib content, matching FocusTrap behavior
5761
5769
  requestAnimationFrame(() => {
5762
5770
  const focusables = getFocusableElements(this.bibContent);
@@ -16520,6 +16528,7 @@ class AuroInputUtilities {
16520
16528
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16521
16529
 
16522
16530
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16531
+ console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16523
16532
  return undefined;
16524
16533
  }
16525
16534
 
@@ -16671,10 +16680,8 @@ class BaseInput extends AuroElement$1 {
16671
16680
  this.layout = 'classic';
16672
16681
  this.locale = 'en-US';
16673
16682
  this.max = undefined;
16674
- this._maxObject = undefined;
16675
16683
  this.maxLength = undefined;
16676
16684
  this.min = undefined;
16677
- this._minObject = undefined;
16678
16685
  this.minLength = undefined;
16679
16686
  this.required = false;
16680
16687
  this.onDark = false;
@@ -16682,7 +16689,6 @@ class BaseInput extends AuroElement$1 {
16682
16689
  this.size = 'lg';
16683
16690
  this.shape = 'classic';
16684
16691
  this.value = undefined;
16685
- this._valueObject = undefined;
16686
16692
 
16687
16693
  this._initializePrivateDefaults();
16688
16694
  }
@@ -17222,7 +17228,7 @@ class BaseInput extends AuroElement$1 {
17222
17228
  * @returns {Date|undefined}
17223
17229
  */
17224
17230
  get valueObject() {
17225
- return this._valueObject || this._computeDateObjectFallback(this.value);
17231
+ return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17226
17232
  }
17227
17233
 
17228
17234
  /**
@@ -17230,7 +17236,7 @@ class BaseInput extends AuroElement$1 {
17230
17236
  * @returns {Date|undefined}
17231
17237
  */
17232
17238
  get minObject() {
17233
- return this._minObject || this._computeDateObjectFallback(this.min);
17239
+ return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17234
17240
  }
17235
17241
 
17236
17242
  /**
@@ -17238,50 +17244,7 @@ class BaseInput extends AuroElement$1 {
17238
17244
  * @returns {Date|undefined}
17239
17245
  */
17240
17246
  get maxObject() {
17241
- return this._maxObject || this._computeDateObjectFallback(this.max);
17242
- }
17243
-
17244
- /**
17245
- * Parses a date string into a Date object when the corresponding `_*Object`
17246
- * field hasn't been synced yet by `updated()`. Returns undefined when the
17247
- * input type/format isn't a full date or the string is not a valid date.
17248
- *
17249
- * Why this exists: a parent (datepicker) can call `inputN.validate()` from
17250
- * inside its own `updated()` before this input's `updated()` has run
17251
- * `syncDateValues()` — so `_valueObject`/`_maxObject` are still `undefined`
17252
- * and range checks would otherwise silently no-op (flipping the result to
17253
- * `valid` or `patternMismatch`).
17254
- * @private
17255
- * @param {string|undefined} dateStr - ISO date string from `value`/`min`/`max`.
17256
- * @returns {Date|undefined}
17257
- */
17258
- _computeDateObjectFallback(dateStr) {
17259
- if (!dateStr || !this.util || !this.util.isFullDateFormat(this.type, this.format)) {
17260
- return undefined;
17261
- }
17262
- if (!dateFormatter.isValidDate(dateStr)) {
17263
- return undefined;
17264
- }
17265
- return dateFormatter.stringToDateInstance(dateStr);
17266
- }
17267
-
17268
- /**
17269
- * Internal setter for readonly date object properties.
17270
- * @private
17271
- * @param {'valueObject'|'minObject'|'maxObject'} propertyName - Public object property name.
17272
- * @param {Date|undefined} propertyValue - Value to assign.
17273
- * @returns {void}
17274
- */
17275
- setDateObjectProperty(propertyName, propertyValue) {
17276
- const internalPropertyName = `_${propertyName}`;
17277
- const previousValue = this[internalPropertyName];
17278
-
17279
- if (previousValue === propertyValue) {
17280
- return;
17281
- }
17282
-
17283
- this[internalPropertyName] = propertyValue;
17284
- this.requestUpdate(propertyName, previousValue);
17247
+ return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
17285
17248
  }
17286
17249
 
17287
17250
  connectedCallback() {
@@ -17310,7 +17273,6 @@ class BaseInput extends AuroElement$1 {
17310
17273
  format: this.format
17311
17274
  });
17312
17275
  this.configureDataForType();
17313
- this.syncDateValues();
17314
17276
  }
17315
17277
 
17316
17278
  disconnectedCallback() {
@@ -17330,10 +17292,6 @@ class BaseInput extends AuroElement$1 {
17330
17292
  this.wrapperElement.addEventListener('click', this.handleClick);
17331
17293
  }
17332
17294
 
17333
- // add attribute for query selectors when auro-input is registered under a custom name
17334
- if (this.tagName.toLowerCase() !== 'auro-input' && !this.hasAttribute('auro-input')) {
17335
- this.setAttribute('auro-input', '');
17336
- }
17337
17295
  this.inputId = this.id ? `${this.id}-input` : window.crypto.randomUUID();
17338
17296
 
17339
17297
  // use validity message override if declared when initializing the component
@@ -17344,7 +17302,6 @@ class BaseInput extends AuroElement$1 {
17344
17302
  this.setCustomHelpTextMessage();
17345
17303
  this.configureAutoFormatting();
17346
17304
  this.configureDataForType();
17347
- this.syncDateValues();
17348
17305
  }
17349
17306
 
17350
17307
  /**
@@ -17481,8 +17438,6 @@ class BaseInput extends AuroElement$1 {
17481
17438
  this.configureDataForType();
17482
17439
  }
17483
17440
 
17484
- this.syncDateValues(changedProperties);
17485
-
17486
17441
  if (changedProperties.has('value')) {
17487
17442
  if (this.value && this.value.length > 0) {
17488
17443
  this.hasValue = true;
@@ -17504,14 +17459,14 @@ class BaseInput extends AuroElement$1 {
17504
17459
 
17505
17460
  if (formattedValue !== this.inputElement.value) {
17506
17461
  this.skipNextProgrammaticInputEvent = true;
17507
- if (this.maskInstance && this.type === 'credit-card') {
17462
+ if (this.maskInstance && this.type !== 'date') {
17508
17463
  // Route through the mask so its _value and el.value stay in lock-step
17509
17464
  // (set value calls updateControl which writes el.value = displayValue).
17510
17465
  // Writing el.value directly leaves the mask thinking displayValue is
17511
- // stale; _saveSelection on the next focus/click then warns. Scoped to
17512
- // credit-card so date's own formattedValue (raw ISO when calendar-invalid)
17513
- // isn't re-masked through the mm/dd/yyyy mask, which would truncate it
17514
- // and flip validity from patternMismatch to tooShort.
17466
+ // stale; _saveSelection on the next focus/click then warns. Date is
17467
+ // excluded because its formattedValue can be raw ISO when the calendar
17468
+ // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17469
+ // flip validity from patternMismatch to tooShort.
17515
17470
  this.maskInstance.value = formattedValue || '';
17516
17471
  } else if (formattedValue) {
17517
17472
  this.inputElement.value = formattedValue;
@@ -17553,120 +17508,65 @@ class BaseInput extends AuroElement$1 {
17553
17508
  }));
17554
17509
  }
17555
17510
 
17556
-
17557
- /**
17558
- * Synchronizes the ISO string values and Date object representations for date-related properties.
17559
- * This keeps the model and display values aligned when either side changes.
17560
- *
17561
- * When a full date format is in use, this method updates `value`, `min`, and `max` from their corresponding
17562
- * Date objects or vice versa, based on which properties have changed. It only runs when the current configuration
17563
- * represents a full year/month/day date format.
17564
- *
17565
- * @param {Map<string, unknown>|undefined} [changedProperties=undefined] - Optional map of changed properties used to limit which values are synchronized.
17566
- * @returns {void}
17567
- * @private
17568
- */
17569
- syncDateValues(changedProperties = undefined) {
17570
- if (!this.util.isFullDateFormat(this.type, this.format)) {
17571
- return;
17572
- }
17573
-
17574
- this.syncSingleDateValue(changedProperties, 'valueObject', 'value');
17575
- this.syncSingleDateValue(changedProperties, 'minObject', 'min');
17576
- this.syncSingleDateValue(changedProperties, 'maxObject', 'max');
17577
- }
17578
-
17579
- /**
17580
- * Synchronizes one date object/string property pair.
17581
- * @private
17582
- * @param {Map<string, unknown>|undefined} changedProperties - Map of changed properties from Lit.
17583
- * @param {string} objectProperty - Date object property name.
17584
- * @param {string} valueProperty - ISO string property name.
17585
- * @returns {void}
17586
- */
17587
- syncSingleDateValue(changedProperties, objectProperty, valueProperty) {
17588
- const objectPropertyChanged = !changedProperties || changedProperties.has(objectProperty);
17589
-
17590
- // objectProperty wins over valueProperty when both changed
17591
- if (objectPropertyChanged && this[objectProperty]) {
17592
- this[valueProperty] = dateFormatter.toISOFormatString(this[objectProperty]);
17593
- return;
17594
- }
17595
-
17596
- const valuePropertyChanged = !changedProperties || changedProperties.has(valueProperty);
17597
- if (!valuePropertyChanged) {
17598
- return;
17599
- }
17600
-
17601
- // when value is newly set to the same ISO string that corresponds to the existing Date object, do not clear the Date object (avoid unnecessary updates)
17602
- if (
17603
- changedProperties &&
17604
- valueProperty === 'value' &&
17605
- changedProperties.get('value') === undefined &&
17606
- this[objectProperty] instanceof Date &&
17607
- this[valueProperty] === dateFormatter.toISOFormatString(this[objectProperty])
17608
- ) {
17609
- return;
17610
- }
17611
-
17612
- if (dateFormatter.isValidDate(this[valueProperty])) {
17613
- this.setDateObjectProperty(objectProperty, dateFormatter.stringToDateInstance(this[valueProperty]));
17614
- } else {
17615
- this.setDateObjectProperty(objectProperty, undefined);
17616
- }
17617
- }
17618
-
17619
17511
  /**
17620
17512
  * Sets up IMasks and logic based on auto-formatting requirements.
17621
17513
  * @private
17622
17514
  * @returns {void}
17623
17515
  */
17624
17516
  configureAutoFormatting() {
17625
- // Re-entrancy guard. The patched-setter's synthetic input event (suppressed
17626
- // by _configuringMask above) could otherwise trigger handleInput
17627
- // processCreditCard configureAutoFormatting before the outer call's
17628
- // set value has finished its alignCursor pass.
17517
+ // _configuringMask gates two things: external re-entry into this method
17518
+ // while setup is mid-flight (from property changes that call back here),
17519
+ // and the accept/complete listeners below — both need to ignore the mask
17520
+ // events fired by our own value-restore step.
17629
17521
  if (this._configuringMask) return;
17630
17522
  this._configuringMask = true;
17631
17523
  try {
17524
+ // Destroy any prior mask so IMask can attach fresh under the new format.
17525
+ // Null the reference too — if the new maskOptions.mask is falsy (e.g.
17526
+ // type switched from credit-card to text) the IMask() reassignment
17527
+ // below is skipped, and downstream writes at line ~823 would otherwise
17528
+ // route through the destroyed instance.
17632
17529
  if (this.maskInstance) {
17633
17530
  this.maskInstance.destroy();
17531
+ this.maskInstance = null;
17634
17532
  }
17635
17533
 
17636
- // Pass new format to util
17637
17534
  this.util.updateFormat(this.format);
17638
17535
 
17639
17536
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17640
17537
 
17641
17538
  if (this.inputElement && maskOptions.mask) {
17642
-
17643
- // Stash and clear any existing value before IMask init.
17644
- // IMask's constructor processes the current input value which requires
17645
- // selection state clearing first avoids that scenario entirely.
17646
- // When the format changes (e.g. locale switch) and we have a valid ISO
17647
- // model value, compute the display string for the NEW format instead of
17648
- // re-using the old display string, which may be invalid in the new mask.
17539
+ // Capture the current display so it can be re-applied after IMask
17540
+ // attaches. The restore at the bottom goes through maskInstance.value
17541
+ // (not inputElement.value directly) so the mask's internal state and
17542
+ // the input's displayed text stay in lock-step.
17649
17543
  let existingValue = this.inputElement.value;
17544
+
17545
+ // Format-change case (e.g. locale switch): existingValue is the OLD
17546
+ // mask's display string and may not parse under the new mask. When
17547
+ // we have a valid date model, rebuild the display from valueObject
17548
+ // (the canonical source) using the new mask's format function.
17650
17549
  if (
17651
17550
  this.util.isFullDateFormat(this.type, this.format) &&
17652
17551
  this.value &&
17653
- dateFormatter.isValidDate(this.value) &&
17654
- this.valueObject instanceof Date &&
17655
- !Number.isNaN(this.valueObject.getTime()) &&
17552
+ this.valueObject &&
17656
17553
  typeof maskOptions.format === 'function'
17657
17554
  ) {
17658
17555
  existingValue = maskOptions.format(this.valueObject);
17659
17556
  }
17660
17557
 
17558
+ // Clear before IMask attaches so the constructor seeds an empty
17559
+ // internal value. Otherwise IMask reads the stale unmasked string
17560
+ // and emits a spurious 'accept' before the restore below runs.
17661
17561
  this.skipNextProgrammaticInputEvent = true;
17662
17562
  this.inputElement.value = '';
17663
17563
 
17664
17564
  this.maskInstance = IMask(this.inputElement, maskOptions);
17665
17565
 
17566
+ // Mask fires 'accept' on every value change, including the restore
17567
+ // step below. Skip events fired during configureAutoFormatting so
17568
+ // we don't overwrite a value the parent just pushed.
17666
17569
  this.maskInstance.on('accept', () => {
17667
- // Suppress propagation during configureAutoFormatting's own value-restoration
17668
- // (line below) — the mask emits 'accept' on every value-set, including ours,
17669
- // and we don't want to overwrite a value the parent just pushed.
17670
17570
  if (this._configuringMask) return;
17671
17571
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17672
17572
  if (this.type === "date") {
@@ -17674,6 +17574,8 @@ class BaseInput extends AuroElement$1 {
17674
17574
  }
17675
17575
  });
17676
17576
 
17577
+ // Mask fires 'complete' on the restore step below for any value that
17578
+ // happens to be a complete match. Same setup-suppression as 'accept'.
17677
17579
  this.maskInstance.on('complete', () => {
17678
17580
  if (this._configuringMask) return;
17679
17581
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17682,7 +17584,9 @@ class BaseInput extends AuroElement$1 {
17682
17584
  }
17683
17585
  });
17684
17586
 
17685
- // Restore the stashed value through IMask so it's properly masked
17587
+ // Write existingValue through the mask (not the input directly) so
17588
+ // the mask reformats it under the new rules and keeps its internal
17589
+ // _value aligned with the input's displayed text.
17686
17590
  if (existingValue) {
17687
17591
  this.maskInstance.value = existingValue;
17688
17592
  }
@@ -17850,7 +17754,6 @@ class BaseInput extends AuroElement$1 {
17850
17754
  */
17851
17755
  reset() {
17852
17756
  this.value = undefined;
17853
- this.setDateObjectProperty('valueObject', undefined);
17854
17757
  this.validation.reset(this);
17855
17758
  }
17856
17759
 
@@ -17859,7 +17762,6 @@ class BaseInput extends AuroElement$1 {
17859
17762
  */
17860
17763
  clear() {
17861
17764
  this.value = undefined;
17862
- this.setDateObjectProperty('valueObject', undefined);
17863
17765
  }
17864
17766
 
17865
17767
  /**
@@ -18388,7 +18290,7 @@ let AuroHelpText$1 = class AuroHelpText extends LitElement {
18388
18290
  }
18389
18291
  };
18390
18292
 
18391
- var formkitVersion$1 = '202606192121';
18293
+ var formkitVersion$1 = '202606231948';
18392
18294
 
18393
18295
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18394
18296
  // See LICENSE in the project root for license information.
@@ -19509,7 +19411,7 @@ class AuroBibtemplate extends LitElement {
19509
19411
  }
19510
19412
  }
19511
19413
 
19512
- var formkitVersion = '202606192121';
19414
+ var formkitVersion = '202606231948';
19513
19415
 
19514
19416
  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}`;
19515
19417
 
@@ -20496,7 +20398,7 @@ class AuroCombobox extends AuroElement {
20496
20398
  if (this.menu) {
20497
20399
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20498
20400
  }
20499
- const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20401
+ const label = getOptionLabel(this.menu.optionSelected);
20500
20402
  this.updateTriggerTextDisplay(label || this.value);
20501
20403
  }
20502
20404
 
@@ -20510,40 +20412,9 @@ class AuroCombobox extends AuroElement {
20510
20412
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20511
20413
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20512
20414
 
20513
- if (!this.persistInput && !(this.behavior === 'suggestion' && isInputFocusedWithNoSelection)) {
20514
- const nextValue = label || this.value;
20515
- // Only set the flag when there's an actual write to suppress —
20516
- // syncValuesAndStates re-enters here during typing when both inputs
20517
- // already match, and a no-op flag flip would make the bib branch's
20518
- // bail eat legitimate user-input events.
20519
- const triggerNeedsSync = this.input.value !== nextValue;
20520
- const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20521
- if (triggerNeedsSync || bibNeedsSync) {
20522
- this._syncingDisplayValue = true;
20523
- if (triggerNeedsSync) {
20524
- this.input.value = nextValue;
20525
- }
20526
- if (bibNeedsSync) {
20527
- this.inputInBib.value = nextValue;
20528
- }
20529
- const pending = [];
20530
- if (triggerNeedsSync) {
20531
- pending.push(this.input.updateComplete);
20532
- }
20533
- if (bibNeedsSync) {
20534
- pending.push(this.inputInBib.updateComplete);
20535
- }
20536
- Promise.all(pending).then(() => {
20537
- // imask reasserts otherwise, and handleBlur reverts to its stale value.
20538
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20539
- this.input.maskInstance.updateValue();
20540
- }
20541
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20542
- this.inputInBib.maskInstance.updateValue();
20543
- }
20544
- this._syncingDisplayValue = false;
20545
- });
20546
- }
20415
+ const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20416
+ if (!suppressed) {
20417
+ this.syncInputValuesAcrossTriggerAndBib(label || this.value);
20547
20418
  }
20548
20419
 
20549
20420
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20571,6 +20442,48 @@ class AuroCombobox extends AuroElement {
20571
20442
  this.requestUpdate();
20572
20443
  }
20573
20444
 
20445
+ /**
20446
+ * Writes nextValue to the trigger input and the bib input when their current
20447
+ * value differs, then re-asserts imask after Lit's update flushes.
20448
+ * @param {string} nextValue - The value to write to both inputs.
20449
+ * @returns {Promise<void> | null} Promise that resolves after the inputs flush,
20450
+ * or null when no sync was needed (so callers can skip post-flush work).
20451
+ * @private
20452
+ */
20453
+ syncInputValuesAcrossTriggerAndBib(nextValue) {
20454
+ // Only set the flag when there's an actual write to suppress —
20455
+ // syncValuesAndStates re-enters here during typing when both inputs
20456
+ // already match, and a no-op flag flip would make the bib branch's
20457
+ // bail eat legitimate user-input events.
20458
+ const triggerNeedsSync = this.input.value !== nextValue;
20459
+ const bibNeedsSync = this.inputInBib.value !== nextValue;
20460
+ if (!triggerNeedsSync && !bibNeedsSync) {
20461
+ return null;
20462
+ }
20463
+
20464
+ this._syncingDisplayValue = true;
20465
+
20466
+ const pending = [];
20467
+ if (triggerNeedsSync) {
20468
+ this.input.value = nextValue;
20469
+ pending.push(this.input.updateComplete);
20470
+ }
20471
+ if (bibNeedsSync) {
20472
+ this.inputInBib.value = nextValue;
20473
+ pending.push(this.inputInBib.updateComplete);
20474
+ }
20475
+ return Promise.all(pending).then(() => {
20476
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20477
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20478
+ this.input.maskInstance.updateValue();
20479
+ }
20480
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20481
+ this.inputInBib.maskInstance.updateValue();
20482
+ }
20483
+ this._syncingDisplayValue = false;
20484
+ });
20485
+ }
20486
+
20574
20487
  /**
20575
20488
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20576
20489
  * @private
@@ -20598,9 +20511,9 @@ class AuroCombobox extends AuroElement {
20598
20511
  this.syncValuesAndStates();
20599
20512
  }
20600
20513
 
20601
- // Re-activate when optionActive is no longer visible, or when _index has
20602
- // been reset (e.g. by clearSelection in an async update) so that
20603
- // makeSelection() can find the correct option.
20514
+ // Re-activate when optionActive is not in the current scrollable viewport,
20515
+ // or when _index has been reset (e.g. by clearSelection in an async update)
20516
+ // so that makeSelection() can find the correct option.
20604
20517
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20605
20518
  this.activateFirstEnabledAvailableOption();
20606
20519
  }
@@ -20734,28 +20647,25 @@ class AuroCombobox extends AuroElement {
20734
20647
 
20735
20648
  guardTouchPassthrough(this.menu);
20736
20649
 
20737
- // The dialog's showModal() steals focus from the trigger.
20738
- // A single rAF is enough for the dialog DOM to be painted and
20739
- // focusable, then doubleRaf finalizes the transition.
20650
+ // showModal() takes focus away from the trigger. Early focus once
20651
+ // the dialog has painted; the shared doubleRaf below is the fallback
20652
+ // for when the dialog needs an extra frame and also clears the
20653
+ // validation guard.
20740
20654
  requestAnimationFrame(() => {
20741
20655
  this.setInputFocus();
20742
20656
  });
20743
-
20744
- doubleRaf(() => {
20745
- this.setInputFocus();
20746
- this._inFullscreenTransition = false;
20747
- });
20748
- } else {
20749
- // Desktop popover-open: restore the trigger caret to end-of-text.
20750
- // Clicking the trigger lands on auro-input's floating <label for="…">
20751
- // overlay; Chrome resets the native input's selection to [0, 0] on
20752
- // label-focus before any JS runs. setInputFocus()'s non-fullscreen
20753
- // branch parks the caret at end. doubleRaf lets the dropdown layout
20754
- // settle first, matching the fullscreen branch timing.
20755
- doubleRaf(() => {
20756
- this.setInputFocus();
20757
- });
20758
20657
  }
20658
+ // else (desktop popover-open): Chrome resets the trigger caret to
20659
+ // [0, 0] when its floating <label for="…"> overlay receives focus.
20660
+ // The shared doubleRaf below parks the caret back at end-of-text
20661
+ // after the dropdown layout settles.
20662
+
20663
+ doubleRaf(() => {
20664
+ this.setInputFocus();
20665
+ if (this._inFullscreenTransition) {
20666
+ this._inFullscreenTransition = false;
20667
+ }
20668
+ });
20759
20669
  }
20760
20670
  });
20761
20671
 
@@ -20860,24 +20770,7 @@ class AuroCombobox extends AuroElement {
20860
20770
  nativeInput.setSelectionRange(len, len);
20861
20771
  }
20862
20772
  } else {
20863
- // Safety net for the strategy-change setTimeout: a viewport-crossing
20864
- // fullscreen→floating switch can close the dialog and leave focus on
20865
- // body. Every other Branch 2 callsite already has focus on the input
20866
- // via the focusin → this.focus() redirect (L1448-1452), so this is a
20867
- // no-op in the common case.
20868
- if (!this.input.componentHasFocus) {
20869
- this.input.focus();
20870
- }
20871
-
20872
- // Park the trigger native input's caret at end-of-text every time the
20873
- // popover opens. Chrome focuses the native via the floating <label for="…">
20874
- // overlay on click and resets selection to [0, 0]; no JS fires between
20875
- // mousedown and selectionchange, so we restore the caret here.
20876
- const triggerNativeInput = this.input.inputElement;
20877
- if (triggerNativeInput && triggerNativeInput.value) {
20878
- const len = triggerNativeInput.value.length;
20879
- triggerNativeInput.setSelectionRange(len, len);
20880
- }
20773
+ this.input.focus();
20881
20774
  }
20882
20775
  }
20883
20776
 
@@ -20983,7 +20876,7 @@ class AuroCombobox extends AuroElement {
20983
20876
  if (this.menu.optionSelected) {
20984
20877
  const selected = this.menu.optionSelected;
20985
20878
 
20986
- if (!this.optionSelected || this.optionSelected !== selected) {
20879
+ if (this.optionSelected !== selected) {
20987
20880
  this.optionSelected = selected;
20988
20881
  }
20989
20882
 
@@ -20996,7 +20889,7 @@ class AuroCombobox extends AuroElement {
20996
20889
  }
20997
20890
 
20998
20891
  // Update display
20999
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20892
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
21000
20893
 
21001
20894
  // Update match word for filtering
21002
20895
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -21012,13 +20905,22 @@ class AuroCombobox extends AuroElement {
21012
20905
  this.hideBib();
21013
20906
  }
21014
20907
 
20908
+ // Move focus to the clear button when the user makes a selection.
20909
+ if (!isEcho && this.menu.value !== undefined) {
20910
+ this.setClearBtnFocus();
20911
+ }
20912
+
21015
20913
  // Announce the selection after the dropdown closes so it isn't
21016
20914
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
20915
+ // Skip when there's no selected value (e.g. menu.clearSelection() from
20916
+ // the unmatched-value path), otherwise VoiceOver reads "undefined".
21017
20917
  const selectedValue = this.menu.value;
21018
- const announcementDelay = 300;
21019
- setTimeout(() => {
21020
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21021
- }, announcementDelay);
20918
+ if (selectedValue) {
20919
+ const announcementDelay = 300;
20920
+ setTimeout(() => {
20921
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
20922
+ }, announcementDelay);
20923
+ }
21022
20924
  }
21023
20925
 
21024
20926
  // Programmatic value syncs leave availableOptions stale because
@@ -21032,18 +20934,6 @@ class AuroCombobox extends AuroElement {
21032
20934
  this._programmaticFilterRefresh = false;
21033
20935
  }, 0);
21034
20936
  }
21035
-
21036
- // base-input skips auto-validate when focus is inside its own shadow,
21037
- // which it is after setTriggerInputFocus — re-run so prior tooShort
21038
- // clears. processCreditCard reasserts errorMessage on every render.
21039
- if (!isEcho && this.menu.optionSelected && this.input.validate) {
21040
- this.input.updateComplete.then(() => {
21041
- this.input.validate(true);
21042
- if (this.input.validity === 'valid') {
21043
- this.input.errorMessage = '';
21044
- }
21045
- });
21046
- }
21047
20937
  });
21048
20938
 
21049
20939
  this.menu.addEventListener('auroMenu-customEventFired', () => {
@@ -21378,15 +21268,7 @@ class AuroCombobox extends AuroElement {
21378
21268
  this.menu.value = undefined;
21379
21269
  this.validation.reset(this);
21380
21270
  this.touched = false;
21381
- // Force validity back to the cleared state. validation.reset() calls
21382
- // validate() at the end, and (post-credit-card-fix) the trigger input's
21383
- // residual validity may still be copied into the combobox by the
21384
- // auroInputElements loop. Explicitly clear here so reset() actually
21385
- // leaves the component in a "no validity" state.
21386
21271
  this.validity = undefined;
21387
- this.errorMessage = '';
21388
- this.input.validity = undefined;
21389
- this.input.errorMessage = '';
21390
21272
  }
21391
21273
 
21392
21274
  /**
@@ -21439,52 +21321,27 @@ class AuroCombobox extends AuroElement {
21439
21321
  this.input.value = this.value;
21440
21322
  }
21441
21323
 
21442
- // Sync menu.value only when an option actually matches; otherwise clear it.
21324
+ // Sync menu.value only when an option actually matches, or when filter
21325
+ // mode needs setMenuValue to dispatch auroMenu-selectValueFailure (the
21326
+ // listener at line 1206 clears combobox.value, mirroring select's
21327
+ // pattern). Otherwise sync the input display for freeform values.
21443
21328
  if (this.menu.options && this.menu.options.length > 0) {
21444
- if (this.menu.options.some((opt) => opt.value === this.value)) {
21445
- this.setMenuValue(this.value);
21446
- } else if (this.behavior === 'filter') {
21447
- // In filter mode, freeform values aren't allowed. Push the unmatched
21448
- // value through setMenuValue so menu dispatches auroMenu-selectValueFailure
21449
- // and the listener at line 1206 clears combobox.value (mirrors select's
21450
- // pattern). Bypassing setMenuValue here would skip the failure cascade.
21329
+ if (this.menu.options.some((opt) => opt.value === this.value) || this.behavior === 'filter') {
21451
21330
  this.setMenuValue(this.value);
21452
21331
  } else {
21453
- if (this.menu.value) {
21454
- this.menu.value = undefined;
21332
+ // Clear menu.value AND menu.optionSelected together. Clearing only
21333
+ // menu.value leaves the previously-selected option element pinned
21334
+ // as menu.optionSelected; a later auroMenu-selectedOption event
21335
+ // would then write its stale .value back into combobox.value.
21336
+ if (this.menu.value || this.menu.optionSelected) {
21337
+ this.menu.clearSelection();
21455
21338
  }
21456
- // Sync the display for programmatic freeform value changes (e.g. the
21457
- // swap-values pattern). Guard with _syncingDisplayValue so that the
21458
- // re-entrant input event from base-input.notifyValueChanged() is
21459
- // ignored by handleInputValueChange. Skip the sync when input already
21460
- // matches to avoid spurious events during the normal typing path.
21461
- const nextValue = this.value || '';
21462
- const triggerNeedsSync = this.input.value !== nextValue;
21463
- const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
21464
- if (triggerNeedsSync || bibNeedsSync) {
21465
- this._syncingDisplayValue = true;
21466
- if (triggerNeedsSync) {
21467
- this.input.value = nextValue;
21468
- }
21469
- if (bibNeedsSync) {
21470
- this.inputInBib.value = nextValue;
21471
- }
21472
- const pending = [];
21473
- if (triggerNeedsSync) {
21474
- pending.push(this.input.updateComplete);
21475
- }
21476
- if (bibNeedsSync) {
21477
- pending.push(this.inputInBib.updateComplete);
21478
- }
21479
- Promise.all(pending).then(() => {
21480
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
21481
- this.input.maskInstance.updateValue();
21482
- }
21483
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
21484
- this.inputInBib.maskInstance.updateValue();
21485
- }
21486
- this._syncingDisplayValue = false;
21487
- // handleInputValueChange bailed on the flag — refresh the filter.
21339
+ // Suggestion-mode freeform value: sync the trigger + bib to show it,
21340
+ // then refresh the filter once the inputs flush (handleInputValueChange
21341
+ // bailed on _syncingDisplayValue).
21342
+ const syncPromise = this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21343
+ if (syncPromise) {
21344
+ syncPromise.then(() => {
21488
21345
  this._programmaticFilterRefresh = true;
21489
21346
  this.handleMenuOptions();
21490
21347
  setTimeout(() => {