@aurodesignsystem-dev/auro-formkit 0.0.0-pr1508.0 → 0.0.0-pr1509.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) 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 +279 -142
  7. package/components/combobox/demo/getting-started.min.js +279 -142
  8. package/components/combobox/demo/index.min.js +279 -142
  9. package/components/combobox/dist/auro-combobox.d.ts +0 -9
  10. package/components/combobox/dist/index.js +279 -142
  11. package/components/combobox/dist/registered.js +279 -142
  12. package/components/counter/demo/customize.min.js +2 -2
  13. package/components/counter/demo/index.min.js +2 -2
  14. package/components/counter/dist/index.js +2 -2
  15. package/components/counter/dist/registered.js +2 -2
  16. package/components/datepicker/demo/customize.min.js +153 -45
  17. package/components/datepicker/demo/index.min.js +153 -45
  18. package/components/datepicker/dist/index.js +153 -45
  19. package/components/datepicker/dist/registered.js +153 -45
  20. package/components/dropdown/demo/customize.min.js +1 -1
  21. package/components/dropdown/demo/getting-started.min.js +1 -1
  22. package/components/dropdown/demo/index.min.js +1 -1
  23. package/components/dropdown/dist/index.js +1 -1
  24. package/components/dropdown/dist/registered.js +1 -1
  25. package/components/form/demo/customize.min.js +589 -235
  26. package/components/form/demo/getting-started.min.js +589 -235
  27. package/components/form/demo/index.min.js +589 -235
  28. package/components/form/demo/registerDemoDeps.min.js +589 -235
  29. package/components/input/demo/customize.min.js +151 -42
  30. package/components/input/demo/getting-started.min.js +151 -42
  31. package/components/input/demo/index.min.js +151 -42
  32. package/components/input/dist/base-input.d.ts +49 -1
  33. package/components/input/dist/index.js +151 -42
  34. package/components/input/dist/registered.js +151 -42
  35. package/components/radio/demo/customize.min.js +1 -1
  36. package/components/radio/demo/getting-started.min.js +1 -1
  37. package/components/radio/demo/index.min.js +1 -1
  38. package/components/radio/dist/index.js +1 -1
  39. package/components/radio/dist/registered.js +1 -1
  40. package/components/select/demo/customize.min.js +2 -2
  41. package/components/select/demo/getting-started.min.js +2 -2
  42. package/components/select/demo/index.min.js +2 -2
  43. package/components/select/dist/index.js +2 -2
  44. package/components/select/dist/registered.js +2 -2
  45. package/custom-elements.json +264 -20
  46. package/package.json +1 -1
@@ -825,8 +825,6 @@ function navigateArrow(component, direction, options = {}) {
825
825
  }
826
826
  }
827
827
 
828
- /* eslint-disable no-underscore-dangle */
829
-
830
828
  /**
831
829
  * Returns the clear button element from the active input's shadow
832
830
  * DOM, if available.
@@ -862,9 +860,11 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
862
860
  * @param {Object} menu - The menu component.
863
861
  */
864
862
  function reconcileMenuIndex(menu) {
863
+ // eslint-disable-next-line no-underscore-dangle
865
864
  if (menu._index < 0 && menu.optionActive && menu.items) {
866
865
  const idx = menu.items.indexOf(menu.optionActive);
867
866
  if (idx >= 0) {
867
+ // eslint-disable-next-line no-underscore-dangle
868
868
  menu._index = idx;
869
869
  }
870
870
  }
@@ -4888,7 +4888,7 @@ let AuroHelpText$2 = class AuroHelpText extends i$3 {
4888
4888
  }
4889
4889
  };
4890
4890
 
4891
- var formkitVersion$2 = '202606231948';
4891
+ var formkitVersion$2 = '202606241801';
4892
4892
 
4893
4893
  let AuroElement$2 = class AuroElement extends i$3 {
4894
4894
  static get properties() {
@@ -16617,7 +16617,6 @@ class AuroInputUtilities {
16617
16617
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16618
16618
 
16619
16619
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16620
- console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16621
16620
  return undefined;
16622
16621
  }
16623
16622
 
@@ -16769,8 +16768,10 @@ class BaseInput extends AuroElement$1 {
16769
16768
  this.layout = 'classic';
16770
16769
  this.locale = 'en-US';
16771
16770
  this.max = undefined;
16771
+ this._maxObject = undefined;
16772
16772
  this.maxLength = undefined;
16773
16773
  this.min = undefined;
16774
+ this._minObject = undefined;
16774
16775
  this.minLength = undefined;
16775
16776
  this.required = false;
16776
16777
  this.onDark = false;
@@ -16778,6 +16779,7 @@ class BaseInput extends AuroElement$1 {
16778
16779
  this.size = 'lg';
16779
16780
  this.shape = 'classic';
16780
16781
  this.value = undefined;
16782
+ this._valueObject = undefined;
16781
16783
 
16782
16784
  this._initializePrivateDefaults();
16783
16785
  }
@@ -17317,7 +17319,7 @@ class BaseInput extends AuroElement$1 {
17317
17319
  * @returns {Date|undefined}
17318
17320
  */
17319
17321
  get valueObject() {
17320
- return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17322
+ return this._valueObject || this._computeDateObjectFallback(this.value);
17321
17323
  }
17322
17324
 
17323
17325
  /**
@@ -17325,7 +17327,7 @@ class BaseInput extends AuroElement$1 {
17325
17327
  * @returns {Date|undefined}
17326
17328
  */
17327
17329
  get minObject() {
17328
- return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17330
+ return this._minObject || this._computeDateObjectFallback(this.min);
17329
17331
  }
17330
17332
 
17331
17333
  /**
@@ -17333,7 +17335,50 @@ class BaseInput extends AuroElement$1 {
17333
17335
  * @returns {Date|undefined}
17334
17336
  */
17335
17337
  get maxObject() {
17336
- return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
17338
+ return this._maxObject || this._computeDateObjectFallback(this.max);
17339
+ }
17340
+
17341
+ /**
17342
+ * Parses a date string into a Date object when the corresponding `_*Object`
17343
+ * field hasn't been synced yet by `updated()`. Returns undefined when the
17344
+ * input type/format isn't a full date or the string is not a valid date.
17345
+ *
17346
+ * Why this exists: a parent (datepicker) can call `inputN.validate()` from
17347
+ * inside its own `updated()` before this input's `updated()` has run
17348
+ * `syncDateValues()` — so `_valueObject`/`_maxObject` are still `undefined`
17349
+ * and range checks would otherwise silently no-op (flipping the result to
17350
+ * `valid` or `patternMismatch`).
17351
+ * @private
17352
+ * @param {string|undefined} dateStr - ISO date string from `value`/`min`/`max`.
17353
+ * @returns {Date|undefined}
17354
+ */
17355
+ _computeDateObjectFallback(dateStr) {
17356
+ if (!dateStr || !this.util || !this.util.isFullDateFormat(this.type, this.format)) {
17357
+ return undefined;
17358
+ }
17359
+ if (!dateFormatter.isValidDate(dateStr)) {
17360
+ return undefined;
17361
+ }
17362
+ return dateFormatter.stringToDateInstance(dateStr);
17363
+ }
17364
+
17365
+ /**
17366
+ * Internal setter for readonly date object properties.
17367
+ * @private
17368
+ * @param {'valueObject'|'minObject'|'maxObject'} propertyName - Public object property name.
17369
+ * @param {Date|undefined} propertyValue - Value to assign.
17370
+ * @returns {void}
17371
+ */
17372
+ setDateObjectProperty(propertyName, propertyValue) {
17373
+ const internalPropertyName = `_${propertyName}`;
17374
+ const previousValue = this[internalPropertyName];
17375
+
17376
+ if (previousValue === propertyValue) {
17377
+ return;
17378
+ }
17379
+
17380
+ this[internalPropertyName] = propertyValue;
17381
+ this.requestUpdate(propertyName, previousValue);
17337
17382
  }
17338
17383
 
17339
17384
  connectedCallback() {
@@ -17362,6 +17407,7 @@ class BaseInput extends AuroElement$1 {
17362
17407
  format: this.format
17363
17408
  });
17364
17409
  this.configureDataForType();
17410
+ this.syncDateValues();
17365
17411
  }
17366
17412
 
17367
17413
  disconnectedCallback() {
@@ -17391,6 +17437,7 @@ class BaseInput extends AuroElement$1 {
17391
17437
  this.setCustomHelpTextMessage();
17392
17438
  this.configureAutoFormatting();
17393
17439
  this.configureDataForType();
17440
+ this.syncDateValues();
17394
17441
  }
17395
17442
 
17396
17443
  /**
@@ -17466,6 +17513,13 @@ class BaseInput extends AuroElement$1 {
17466
17513
 
17467
17514
  if (typeToI18n.includes(this.type)) {
17468
17515
  this.setCustomValidityForType = i18n(this.lang, this.type);
17516
+ // COVERAGE: this `else if` branch is unreachable in WTR. connectedCallback
17517
+ // (L682) calls configureDataForType, which at L1266-1268 assigns
17518
+ // `this.format = this.util.getDateMaskFromLocale().toLowerCase()` for any
17519
+ // type=date input whose `format` attribute is unset. That runs before
17520
+ // firstUpdated invokes setCustomHelpTextMessage, so `!this.format` is
17521
+ // always false here. Retained as a defensive fallback; the whole function
17522
+ // is @deprecated per AB#1557296 and slated for removal.
17469
17523
  } else if (!this.format && this.type === 'date') {
17470
17524
  this.setCustomValidityForType = i18n(this.lang, 'dateMMDDYYYY');
17471
17525
  } else if (this.dateFormatMap[this.format]) {
@@ -17527,6 +17581,8 @@ class BaseInput extends AuroElement$1 {
17527
17581
  this.configureDataForType();
17528
17582
  }
17529
17583
 
17584
+ this.syncDateValues(changedProperties);
17585
+
17530
17586
  if (changedProperties.has('value')) {
17531
17587
  if (this.value && this.value.length > 0) {
17532
17588
  this.hasValue = true;
@@ -17548,14 +17604,14 @@ class BaseInput extends AuroElement$1 {
17548
17604
 
17549
17605
  if (formattedValue !== this.inputElement.value) {
17550
17606
  this.skipNextProgrammaticInputEvent = true;
17551
- if (this.maskInstance && this.type !== 'date') {
17607
+ if (this.maskInstance && this.type === 'credit-card') {
17552
17608
  // Route through the mask so its _value and el.value stay in lock-step
17553
17609
  // (set value calls updateControl which writes el.value = displayValue).
17554
17610
  // Writing el.value directly leaves the mask thinking displayValue is
17555
- // stale; _saveSelection on the next focus/click then warns. Date is
17556
- // excluded because its formattedValue can be raw ISO when the calendar
17557
- // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17558
- // flip validity from patternMismatch to tooShort.
17611
+ // stale; _saveSelection on the next focus/click then warns. Scoped to
17612
+ // credit-card so date's own formattedValue (raw ISO when calendar-invalid)
17613
+ // isn't re-masked through the mm/dd/yyyy mask, which would truncate it
17614
+ // and flip validity from patternMismatch to tooShort.
17559
17615
  this.maskInstance.value = formattedValue || '';
17560
17616
  } else if (formattedValue) {
17561
17617
  this.inputElement.value = formattedValue;
@@ -17597,65 +17653,120 @@ class BaseInput extends AuroElement$1 {
17597
17653
  }));
17598
17654
  }
17599
17655
 
17656
+
17657
+ /**
17658
+ * Synchronizes the ISO string values and Date object representations for date-related properties.
17659
+ * This keeps the model and display values aligned when either side changes.
17660
+ *
17661
+ * When a full date format is in use, this method updates `value`, `min`, and `max` from their corresponding
17662
+ * Date objects or vice versa, based on which properties have changed. It only runs when the current configuration
17663
+ * represents a full year/month/day date format.
17664
+ *
17665
+ * @param {Map<string, unknown>|undefined} [changedProperties=undefined] - Optional map of changed properties used to limit which values are synchronized.
17666
+ * @returns {void}
17667
+ * @private
17668
+ */
17669
+ syncDateValues(changedProperties = undefined) {
17670
+ if (!this.util.isFullDateFormat(this.type, this.format)) {
17671
+ return;
17672
+ }
17673
+
17674
+ this.syncSingleDateValue(changedProperties, 'valueObject', 'value');
17675
+ this.syncSingleDateValue(changedProperties, 'minObject', 'min');
17676
+ this.syncSingleDateValue(changedProperties, 'maxObject', 'max');
17677
+ }
17678
+
17679
+ /**
17680
+ * Synchronizes one date object/string property pair.
17681
+ * @private
17682
+ * @param {Map<string, unknown>|undefined} changedProperties - Map of changed properties from Lit.
17683
+ * @param {string} objectProperty - Date object property name.
17684
+ * @param {string} valueProperty - ISO string property name.
17685
+ * @returns {void}
17686
+ */
17687
+ syncSingleDateValue(changedProperties, objectProperty, valueProperty) {
17688
+ const objectPropertyChanged = !changedProperties || changedProperties.has(objectProperty);
17689
+
17690
+ // objectProperty wins over valueProperty when both changed
17691
+ if (objectPropertyChanged && this[objectProperty]) {
17692
+ this[valueProperty] = dateFormatter.toISOFormatString(this[objectProperty]);
17693
+ return;
17694
+ }
17695
+
17696
+ const valuePropertyChanged = !changedProperties || changedProperties.has(valueProperty);
17697
+ if (!valuePropertyChanged) {
17698
+ return;
17699
+ }
17700
+
17701
+ // 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)
17702
+ if (
17703
+ changedProperties &&
17704
+ valueProperty === 'value' &&
17705
+ changedProperties.get('value') === undefined &&
17706
+ this[objectProperty] instanceof Date &&
17707
+ this[valueProperty] === dateFormatter.toISOFormatString(this[objectProperty])
17708
+ ) {
17709
+ return;
17710
+ }
17711
+
17712
+ if (dateFormatter.isValidDate(this[valueProperty])) {
17713
+ this.setDateObjectProperty(objectProperty, dateFormatter.stringToDateInstance(this[valueProperty]));
17714
+ } else {
17715
+ this.setDateObjectProperty(objectProperty, undefined);
17716
+ }
17717
+ }
17718
+
17600
17719
  /**
17601
17720
  * Sets up IMasks and logic based on auto-formatting requirements.
17602
17721
  * @private
17603
17722
  * @returns {void}
17604
17723
  */
17605
17724
  configureAutoFormatting() {
17606
- // _configuringMask gates two things: external re-entry into this method
17607
- // while setup is mid-flight (from property changes that call back here),
17608
- // and the accept/complete listeners below both need to ignore the mask
17609
- // events fired by our own value-restore step.
17725
+ // Re-entrancy guard. The patched-setter's synthetic input event (suppressed
17726
+ // by _configuringMask above) could otherwise trigger handleInput
17727
+ // processCreditCard configureAutoFormatting before the outer call's
17728
+ // set value has finished its alignCursor pass.
17610
17729
  if (this._configuringMask) return;
17611
17730
  this._configuringMask = true;
17612
17731
  try {
17613
- // Destroy any prior mask so IMask can attach fresh under the new format.
17614
- // Null the reference too — if the new maskOptions.mask is falsy (e.g.
17615
- // type switched from credit-card to text) the IMask() reassignment
17616
- // below is skipped, and downstream writes at line ~823 would otherwise
17617
- // route through the destroyed instance.
17618
17732
  if (this.maskInstance) {
17619
17733
  this.maskInstance.destroy();
17620
- this.maskInstance = null;
17621
17734
  }
17622
17735
 
17736
+ // Pass new format to util
17623
17737
  this.util.updateFormat(this.format);
17624
17738
 
17625
17739
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17626
17740
 
17627
17741
  if (this.inputElement && maskOptions.mask) {
17628
- // Capture the current display so it can be re-applied after IMask
17629
- // attaches. The restore at the bottom goes through maskInstance.value
17630
- // (not inputElement.value directly) so the mask's internal state and
17631
- // the input's displayed text stay in lock-step.
17632
- let existingValue = this.inputElement.value;
17633
17742
 
17634
- // Format-change case (e.g. locale switch): existingValue is the OLD
17635
- // mask's display string and may not parse under the new mask. When
17636
- // we have a valid date model, rebuild the display from valueObject
17637
- // (the canonical source) using the new mask's format function.
17743
+ // Stash and clear any existing value before IMask init.
17744
+ // IMask's constructor processes the current input value which requires
17745
+ // selection state clearing first avoids that scenario entirely.
17746
+ // When the format changes (e.g. locale switch) and we have a valid ISO
17747
+ // model value, compute the display string for the NEW format instead of
17748
+ // re-using the old display string, which may be invalid in the new mask.
17749
+ let existingValue = this.inputElement.value;
17638
17750
  if (
17639
17751
  this.util.isFullDateFormat(this.type, this.format) &&
17640
17752
  this.value &&
17641
- this.valueObject &&
17753
+ dateFormatter.isValidDate(this.value) &&
17754
+ this.valueObject instanceof Date &&
17755
+ !Number.isNaN(this.valueObject.getTime()) &&
17642
17756
  typeof maskOptions.format === 'function'
17643
17757
  ) {
17644
17758
  existingValue = maskOptions.format(this.valueObject);
17645
17759
  }
17646
17760
 
17647
- // Clear before IMask attaches so the constructor seeds an empty
17648
- // internal value. Otherwise IMask reads the stale unmasked string
17649
- // and emits a spurious 'accept' before the restore below runs.
17650
17761
  this.skipNextProgrammaticInputEvent = true;
17651
17762
  this.inputElement.value = '';
17652
17763
 
17653
17764
  this.maskInstance = IMask(this.inputElement, maskOptions);
17654
17765
 
17655
- // Mask fires 'accept' on every value change, including the restore
17656
- // step below. Skip events fired during configureAutoFormatting so
17657
- // we don't overwrite a value the parent just pushed.
17658
17766
  this.maskInstance.on('accept', () => {
17767
+ // Suppress propagation during configureAutoFormatting's own value-restoration
17768
+ // (line below) — the mask emits 'accept' on every value-set, including ours,
17769
+ // and we don't want to overwrite a value the parent just pushed.
17659
17770
  if (this._configuringMask) return;
17660
17771
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17661
17772
  if (this.type === "date") {
@@ -17663,8 +17774,6 @@ class BaseInput extends AuroElement$1 {
17663
17774
  }
17664
17775
  });
17665
17776
 
17666
- // Mask fires 'complete' on the restore step below for any value that
17667
- // happens to be a complete match. Same setup-suppression as 'accept'.
17668
17777
  this.maskInstance.on('complete', () => {
17669
17778
  if (this._configuringMask) return;
17670
17779
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17673,9 +17782,7 @@ class BaseInput extends AuroElement$1 {
17673
17782
  }
17674
17783
  });
17675
17784
 
17676
- // Write existingValue through the mask (not the input directly) so
17677
- // the mask reformats it under the new rules and keeps its internal
17678
- // _value aligned with the input's displayed text.
17785
+ // Restore the stashed value through IMask so it's properly masked
17679
17786
  if (existingValue) {
17680
17787
  this.maskInstance.value = existingValue;
17681
17788
  }
@@ -17843,6 +17950,7 @@ class BaseInput extends AuroElement$1 {
17843
17950
  */
17844
17951
  reset() {
17845
17952
  this.value = undefined;
17953
+ this.setDateObjectProperty('valueObject', undefined);
17846
17954
  this.validation.reset(this);
17847
17955
  }
17848
17956
 
@@ -17851,6 +17959,7 @@ class BaseInput extends AuroElement$1 {
17851
17959
  */
17852
17960
  clear() {
17853
17961
  this.value = undefined;
17962
+ this.setDateObjectProperty('valueObject', undefined);
17854
17963
  }
17855
17964
 
17856
17965
  /**
@@ -17888,7 +17997,7 @@ class BaseInput extends AuroElement$1 {
17888
17997
  */
17889
17998
  get placeholderStr() {
17890
17999
  if (!this.placeholder && this.type === 'date') {
17891
- return this.format ? this.format.toUpperCase() : 'MM/DD/YYYY';
18000
+ return this.format.toUpperCase();
17892
18001
  }
17893
18002
  return this.placeholder || "";
17894
18003
  }
@@ -18379,7 +18488,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18379
18488
  }
18380
18489
  };
18381
18490
 
18382
- var formkitVersion$1 = '202606231948';
18491
+ var formkitVersion$1 = '202606241801';
18383
18492
 
18384
18493
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18385
18494
  // See LICENSE in the project root for license information.
@@ -19500,7 +19609,7 @@ class AuroBibtemplate extends i$3 {
19500
19609
  }
19501
19610
  }
19502
19611
 
19503
- var formkitVersion = '202606231948';
19612
+ var formkitVersion = '202606241801';
19504
19613
 
19505
19614
  var styleCss$3 = i$6`.util_displayInline{display:inline}.util_displayInlineBlock{display:inline-block}.util_displayBlock{display:block}.util_displayFlex{display:flex}.util_displayHidden{display:none}.util_displayHiddenVisually{position:absolute;overflow:hidden;clip:rect(1px, 1px, 1px, 1px);width:1px;height:1px;padding:0;border:0}:host{display:block;text-align:left}:host [auro-dropdown]{--ds-auro-dropdown-trigger-background-color: transparent}:host #inputInBib::part(wrapper){box-shadow:none}:host #inputInBib::part(accent-left){display:none}:host([layout*=classic]) [auro-input]{width:100%}:host([layout*=classic]) [auro-input]::part(helpText){display:none}:host([layout*=classic]) #slotHolder{display:none}`;
19506
19615
 
@@ -20487,7 +20596,7 @@ class AuroCombobox extends AuroElement {
20487
20596
  if (this.menu) {
20488
20597
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20489
20598
  }
20490
- const label = getOptionLabel(this.menu.optionSelected);
20599
+ const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20491
20600
  this.updateTriggerTextDisplay(label || this.value);
20492
20601
  }
20493
20602
 
@@ -20501,9 +20610,40 @@ class AuroCombobox extends AuroElement {
20501
20610
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20502
20611
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20503
20612
 
20504
- const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20505
- if (!suppressed) {
20506
- this.syncInputValuesAcrossTriggerAndBib(label || this.value);
20613
+ if (!this.persistInput && !(this.behavior === 'suggestion' && isInputFocusedWithNoSelection)) {
20614
+ const nextValue = label || this.value;
20615
+ // Only set the flag when there's an actual write to suppress —
20616
+ // syncValuesAndStates re-enters here during typing when both inputs
20617
+ // already match, and a no-op flag flip would make the bib branch's
20618
+ // bail eat legitimate user-input events.
20619
+ const triggerNeedsSync = this.input.value !== nextValue;
20620
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20621
+ if (triggerNeedsSync || bibNeedsSync) {
20622
+ this._syncingDisplayValue = true;
20623
+ if (triggerNeedsSync) {
20624
+ this.input.value = nextValue;
20625
+ }
20626
+ if (bibNeedsSync) {
20627
+ this.inputInBib.value = nextValue;
20628
+ }
20629
+ const pending = [];
20630
+ if (triggerNeedsSync) {
20631
+ pending.push(this.input.updateComplete);
20632
+ }
20633
+ if (bibNeedsSync) {
20634
+ pending.push(this.inputInBib.updateComplete);
20635
+ }
20636
+ Promise.all(pending).then(() => {
20637
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20638
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20639
+ this.input.maskInstance.updateValue();
20640
+ }
20641
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20642
+ this.inputInBib.maskInstance.updateValue();
20643
+ }
20644
+ this._syncingDisplayValue = false;
20645
+ });
20646
+ }
20507
20647
  }
20508
20648
 
20509
20649
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20531,48 +20671,6 @@ class AuroCombobox extends AuroElement {
20531
20671
  this.requestUpdate();
20532
20672
  }
20533
20673
 
20534
- /**
20535
- * Writes nextValue to the trigger input and the bib input when their current
20536
- * value differs, then re-asserts imask after Lit's update flushes.
20537
- * @param {string} nextValue - The value to write to both inputs.
20538
- * @returns {Promise<void> | null} Promise that resolves after the inputs flush,
20539
- * or null when no sync was needed (so callers can skip post-flush work).
20540
- * @private
20541
- */
20542
- syncInputValuesAcrossTriggerAndBib(nextValue) {
20543
- // Only set the flag when there's an actual write to suppress —
20544
- // syncValuesAndStates re-enters here during typing when both inputs
20545
- // already match, and a no-op flag flip would make the bib branch's
20546
- // bail eat legitimate user-input events.
20547
- const triggerNeedsSync = this.input.value !== nextValue;
20548
- const bibNeedsSync = this.inputInBib.value !== nextValue;
20549
- if (!triggerNeedsSync && !bibNeedsSync) {
20550
- return null;
20551
- }
20552
-
20553
- this._syncingDisplayValue = true;
20554
-
20555
- const pending = [];
20556
- if (triggerNeedsSync) {
20557
- this.input.value = nextValue;
20558
- pending.push(this.input.updateComplete);
20559
- }
20560
- if (bibNeedsSync) {
20561
- this.inputInBib.value = nextValue;
20562
- pending.push(this.inputInBib.updateComplete);
20563
- }
20564
- return Promise.all(pending).then(() => {
20565
- // imask reasserts otherwise, and handleBlur reverts to its stale value.
20566
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20567
- this.input.maskInstance.updateValue();
20568
- }
20569
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20570
- this.inputInBib.maskInstance.updateValue();
20571
- }
20572
- this._syncingDisplayValue = false;
20573
- });
20574
- }
20575
-
20576
20674
  /**
20577
20675
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20578
20676
  * @private
@@ -20600,9 +20698,9 @@ class AuroCombobox extends AuroElement {
20600
20698
  this.syncValuesAndStates();
20601
20699
  }
20602
20700
 
20603
- // Re-activate when optionActive is not in the current scrollable viewport,
20604
- // or when _index has been reset (e.g. by clearSelection in an async update)
20605
- // so that makeSelection() can find the correct option.
20701
+ // Re-activate when optionActive is no longer visible, or when _index has
20702
+ // been reset (e.g. by clearSelection in an async update) so that
20703
+ // makeSelection() can find the correct option.
20606
20704
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20607
20705
  this.activateFirstEnabledAvailableOption();
20608
20706
  }
@@ -20736,25 +20834,28 @@ class AuroCombobox extends AuroElement {
20736
20834
 
20737
20835
  guardTouchPassthrough(this.menu);
20738
20836
 
20739
- // showModal() takes focus away from the trigger. Early focus once
20740
- // the dialog has painted; the shared doubleRaf below is the fallback
20741
- // for when the dialog needs an extra frame and also clears the
20742
- // validation guard.
20837
+ // The dialog's showModal() steals focus from the trigger.
20838
+ // A single rAF is enough for the dialog DOM to be painted and
20839
+ // focusable, then doubleRaf finalizes the transition.
20743
20840
  requestAnimationFrame(() => {
20744
20841
  this.setInputFocus();
20745
20842
  });
20746
- }
20747
- // else (desktop popover-open): Chrome resets the trigger caret to
20748
- // [0, 0] when its floating <label for="…"> overlay receives focus.
20749
- // The shared doubleRaf below parks the caret back at end-of-text
20750
- // after the dropdown layout settles.
20751
-
20752
- doubleRaf(() => {
20753
- this.setInputFocus();
20754
- if (this._inFullscreenTransition) {
20843
+
20844
+ doubleRaf(() => {
20845
+ this.setInputFocus();
20755
20846
  this._inFullscreenTransition = false;
20756
- }
20757
- });
20847
+ });
20848
+ } else {
20849
+ // Desktop popover-open: restore the trigger caret to end-of-text.
20850
+ // Clicking the trigger lands on auro-input's floating <label for="…">
20851
+ // overlay; Chrome resets the native input's selection to [0, 0] on
20852
+ // label-focus before any JS runs. setInputFocus()'s non-fullscreen
20853
+ // branch parks the caret at end. doubleRaf lets the dropdown layout
20854
+ // settle first, matching the fullscreen branch timing.
20855
+ doubleRaf(() => {
20856
+ this.setInputFocus();
20857
+ });
20858
+ }
20758
20859
  }
20759
20860
  });
20760
20861
 
@@ -20965,7 +21066,7 @@ class AuroCombobox extends AuroElement {
20965
21066
  if (this.menu.optionSelected) {
20966
21067
  const selected = this.menu.optionSelected;
20967
21068
 
20968
- if (this.optionSelected !== selected) {
21069
+ if (!this.optionSelected || this.optionSelected !== selected) {
20969
21070
  this.optionSelected = selected;
20970
21071
  }
20971
21072
 
@@ -20978,7 +21079,7 @@ class AuroCombobox extends AuroElement {
20978
21079
  }
20979
21080
 
20980
21081
  // Update display
20981
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
21082
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20982
21083
 
20983
21084
  // Update match word for filtering
20984
21085
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -20994,22 +21095,13 @@ class AuroCombobox extends AuroElement {
20994
21095
  this.hideBib();
20995
21096
  }
20996
21097
 
20997
- // Move focus to the clear button when the user makes a selection.
20998
- if (!isEcho && this.menu.value !== undefined) {
20999
- this.setClearBtnFocus();
21000
- }
21001
-
21002
21098
  // Announce the selection after the dropdown closes so it isn't
21003
21099
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
21004
- // Skip when there's no selected value (e.g. menu.clearSelection() from
21005
- // the unmatched-value path), otherwise VoiceOver reads "undefined".
21006
21100
  const selectedValue = this.menu.value;
21007
- if (selectedValue) {
21008
- const announcementDelay = 300;
21009
- setTimeout(() => {
21010
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21011
- }, announcementDelay);
21012
- }
21101
+ const announcementDelay = 300;
21102
+ setTimeout(() => {
21103
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21104
+ }, announcementDelay);
21013
21105
  }
21014
21106
 
21015
21107
  // Programmatic value syncs leave availableOptions stale because
@@ -21023,6 +21115,18 @@ class AuroCombobox extends AuroElement {
21023
21115
  this._programmaticFilterRefresh = false;
21024
21116
  }, 0);
21025
21117
  }
21118
+
21119
+ // base-input skips auto-validate when focus is inside its own shadow,
21120
+ // which it is after setTriggerInputFocus — re-run so prior tooShort
21121
+ // clears. processCreditCard reasserts errorMessage on every render.
21122
+ if (!isEcho && this.menu.optionSelected && this.input.validate) {
21123
+ this.input.updateComplete.then(() => {
21124
+ this.input.validate(true);
21125
+ if (this.input.validity === 'valid') {
21126
+ this.input.errorMessage = '';
21127
+ }
21128
+ });
21129
+ }
21026
21130
  });
21027
21131
 
21028
21132
  this.menu.addEventListener('auroMenu-customEventFired', () => {
@@ -21357,7 +21461,15 @@ class AuroCombobox extends AuroElement {
21357
21461
  this.menu.value = undefined;
21358
21462
  this.validation.reset(this);
21359
21463
  this.touched = false;
21464
+ // Force validity back to the cleared state. validation.reset() calls
21465
+ // validate() at the end, and (post-credit-card-fix) the trigger input's
21466
+ // residual validity may still be copied into the combobox by the
21467
+ // auroInputElements loop. Explicitly clear here so reset() actually
21468
+ // leaves the component in a "no validity" state.
21360
21469
  this.validity = undefined;
21470
+ this.errorMessage = '';
21471
+ this.input.validity = undefined;
21472
+ this.input.errorMessage = '';
21361
21473
  }
21362
21474
 
21363
21475
  /**
@@ -21410,27 +21522,52 @@ class AuroCombobox extends AuroElement {
21410
21522
  this.input.value = this.value;
21411
21523
  }
21412
21524
 
21413
- // Sync menu.value only when an option actually matches, or when filter
21414
- // mode needs setMenuValue to dispatch auroMenu-selectValueFailure (the
21415
- // listener at line 1206 clears combobox.value, mirroring select's
21416
- // pattern). Otherwise sync the input display for freeform values.
21525
+ // Sync menu.value only when an option actually matches; otherwise clear it.
21417
21526
  if (this.menu.options && this.menu.options.length > 0) {
21418
- if (this.menu.options.some((opt) => opt.value === this.value) || this.behavior === 'filter') {
21527
+ if (this.menu.options.some((opt) => opt.value === this.value)) {
21528
+ this.setMenuValue(this.value);
21529
+ } else if (this.behavior === 'filter') {
21530
+ // In filter mode, freeform values aren't allowed. Push the unmatched
21531
+ // value through setMenuValue so menu dispatches auroMenu-selectValueFailure
21532
+ // and the listener at line 1206 clears combobox.value (mirrors select's
21533
+ // pattern). Bypassing setMenuValue here would skip the failure cascade.
21419
21534
  this.setMenuValue(this.value);
21420
21535
  } else {
21421
- // Clear menu.value AND menu.optionSelected together. Clearing only
21422
- // menu.value leaves the previously-selected option element pinned
21423
- // as menu.optionSelected; a later auroMenu-selectedOption event
21424
- // would then write its stale .value back into combobox.value.
21425
- if (this.menu.value || this.menu.optionSelected) {
21426
- this.menu.clearSelection();
21536
+ if (this.menu.value) {
21537
+ this.menu.value = undefined;
21427
21538
  }
21428
- // Suggestion-mode freeform value: sync the trigger + bib to show it,
21429
- // then refresh the filter once the inputs flush (handleInputValueChange
21430
- // bailed on _syncingDisplayValue).
21431
- const syncPromise = this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21432
- if (syncPromise) {
21433
- syncPromise.then(() => {
21539
+ // Sync the display for programmatic freeform value changes (e.g. the
21540
+ // swap-values pattern). Guard with _syncingDisplayValue so that the
21541
+ // re-entrant input event from base-input.notifyValueChanged() is
21542
+ // ignored by handleInputValueChange. Skip the sync when input already
21543
+ // matches to avoid spurious events during the normal typing path.
21544
+ const nextValue = this.value || '';
21545
+ const triggerNeedsSync = this.input.value !== nextValue;
21546
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
21547
+ if (triggerNeedsSync || bibNeedsSync) {
21548
+ this._syncingDisplayValue = true;
21549
+ if (triggerNeedsSync) {
21550
+ this.input.value = nextValue;
21551
+ }
21552
+ if (bibNeedsSync) {
21553
+ this.inputInBib.value = nextValue;
21554
+ }
21555
+ const pending = [];
21556
+ if (triggerNeedsSync) {
21557
+ pending.push(this.input.updateComplete);
21558
+ }
21559
+ if (bibNeedsSync) {
21560
+ pending.push(this.inputInBib.updateComplete);
21561
+ }
21562
+ Promise.all(pending).then(() => {
21563
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
21564
+ this.input.maskInstance.updateValue();
21565
+ }
21566
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
21567
+ this.inputInBib.maskInstance.updateValue();
21568
+ }
21569
+ this._syncingDisplayValue = false;
21570
+ // handleInputValueChange bailed on the flag — refresh the filter.
21434
21571
  this._programmaticFilterRefresh = true;
21435
21572
  this.handleMenuOptions();
21436
21573
  setTimeout(() => {