@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
@@ -810,8 +810,6 @@ function navigateArrow(component, direction, options = {}) {
810
810
  }
811
811
  }
812
812
 
813
- /* eslint-disable no-underscore-dangle */
814
-
815
813
  /**
816
814
  * Returns the clear button element from the active input's shadow
817
815
  * DOM, if available.
@@ -847,9 +845,11 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
847
845
  * @param {Object} menu - The menu component.
848
846
  */
849
847
  function reconcileMenuIndex(menu) {
848
+ // eslint-disable-next-line no-underscore-dangle
850
849
  if (menu._index < 0 && menu.optionActive && menu.items) {
851
850
  const idx = menu.items.indexOf(menu.optionActive);
852
851
  if (idx >= 0) {
852
+ // eslint-disable-next-line no-underscore-dangle
853
853
  menu._index = idx;
854
854
  }
855
855
  }
@@ -4873,7 +4873,7 @@ let AuroHelpText$2 = class AuroHelpText extends i$3 {
4873
4873
  }
4874
4874
  };
4875
4875
 
4876
- var formkitVersion$2 = '202606231948';
4876
+ var formkitVersion$2 = '202606241801';
4877
4877
 
4878
4878
  let AuroElement$2 = class AuroElement extends i$3 {
4879
4879
  static get properties() {
@@ -16602,7 +16602,6 @@ class AuroInputUtilities {
16602
16602
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16603
16603
 
16604
16604
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16605
- console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16606
16605
  return undefined;
16607
16606
  }
16608
16607
 
@@ -16754,8 +16753,10 @@ class BaseInput extends AuroElement$1 {
16754
16753
  this.layout = 'classic';
16755
16754
  this.locale = 'en-US';
16756
16755
  this.max = undefined;
16756
+ this._maxObject = undefined;
16757
16757
  this.maxLength = undefined;
16758
16758
  this.min = undefined;
16759
+ this._minObject = undefined;
16759
16760
  this.minLength = undefined;
16760
16761
  this.required = false;
16761
16762
  this.onDark = false;
@@ -16763,6 +16764,7 @@ class BaseInput extends AuroElement$1 {
16763
16764
  this.size = 'lg';
16764
16765
  this.shape = 'classic';
16765
16766
  this.value = undefined;
16767
+ this._valueObject = undefined;
16766
16768
 
16767
16769
  this._initializePrivateDefaults();
16768
16770
  }
@@ -17302,7 +17304,7 @@ class BaseInput extends AuroElement$1 {
17302
17304
  * @returns {Date|undefined}
17303
17305
  */
17304
17306
  get valueObject() {
17305
- return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17307
+ return this._valueObject || this._computeDateObjectFallback(this.value);
17306
17308
  }
17307
17309
 
17308
17310
  /**
@@ -17310,7 +17312,7 @@ class BaseInput extends AuroElement$1 {
17310
17312
  * @returns {Date|undefined}
17311
17313
  */
17312
17314
  get minObject() {
17313
- return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17315
+ return this._minObject || this._computeDateObjectFallback(this.min);
17314
17316
  }
17315
17317
 
17316
17318
  /**
@@ -17318,7 +17320,50 @@ class BaseInput extends AuroElement$1 {
17318
17320
  * @returns {Date|undefined}
17319
17321
  */
17320
17322
  get maxObject() {
17321
- return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
17323
+ return this._maxObject || this._computeDateObjectFallback(this.max);
17324
+ }
17325
+
17326
+ /**
17327
+ * Parses a date string into a Date object when the corresponding `_*Object`
17328
+ * field hasn't been synced yet by `updated()`. Returns undefined when the
17329
+ * input type/format isn't a full date or the string is not a valid date.
17330
+ *
17331
+ * Why this exists: a parent (datepicker) can call `inputN.validate()` from
17332
+ * inside its own `updated()` before this input's `updated()` has run
17333
+ * `syncDateValues()` — so `_valueObject`/`_maxObject` are still `undefined`
17334
+ * and range checks would otherwise silently no-op (flipping the result to
17335
+ * `valid` or `patternMismatch`).
17336
+ * @private
17337
+ * @param {string|undefined} dateStr - ISO date string from `value`/`min`/`max`.
17338
+ * @returns {Date|undefined}
17339
+ */
17340
+ _computeDateObjectFallback(dateStr) {
17341
+ if (!dateStr || !this.util || !this.util.isFullDateFormat(this.type, this.format)) {
17342
+ return undefined;
17343
+ }
17344
+ if (!dateFormatter.isValidDate(dateStr)) {
17345
+ return undefined;
17346
+ }
17347
+ return dateFormatter.stringToDateInstance(dateStr);
17348
+ }
17349
+
17350
+ /**
17351
+ * Internal setter for readonly date object properties.
17352
+ * @private
17353
+ * @param {'valueObject'|'minObject'|'maxObject'} propertyName - Public object property name.
17354
+ * @param {Date|undefined} propertyValue - Value to assign.
17355
+ * @returns {void}
17356
+ */
17357
+ setDateObjectProperty(propertyName, propertyValue) {
17358
+ const internalPropertyName = `_${propertyName}`;
17359
+ const previousValue = this[internalPropertyName];
17360
+
17361
+ if (previousValue === propertyValue) {
17362
+ return;
17363
+ }
17364
+
17365
+ this[internalPropertyName] = propertyValue;
17366
+ this.requestUpdate(propertyName, previousValue);
17322
17367
  }
17323
17368
 
17324
17369
  connectedCallback() {
@@ -17347,6 +17392,7 @@ class BaseInput extends AuroElement$1 {
17347
17392
  format: this.format
17348
17393
  });
17349
17394
  this.configureDataForType();
17395
+ this.syncDateValues();
17350
17396
  }
17351
17397
 
17352
17398
  disconnectedCallback() {
@@ -17376,6 +17422,7 @@ class BaseInput extends AuroElement$1 {
17376
17422
  this.setCustomHelpTextMessage();
17377
17423
  this.configureAutoFormatting();
17378
17424
  this.configureDataForType();
17425
+ this.syncDateValues();
17379
17426
  }
17380
17427
 
17381
17428
  /**
@@ -17451,6 +17498,13 @@ class BaseInput extends AuroElement$1 {
17451
17498
 
17452
17499
  if (typeToI18n.includes(this.type)) {
17453
17500
  this.setCustomValidityForType = i18n(this.lang, this.type);
17501
+ // COVERAGE: this `else if` branch is unreachable in WTR. connectedCallback
17502
+ // (L682) calls configureDataForType, which at L1266-1268 assigns
17503
+ // `this.format = this.util.getDateMaskFromLocale().toLowerCase()` for any
17504
+ // type=date input whose `format` attribute is unset. That runs before
17505
+ // firstUpdated invokes setCustomHelpTextMessage, so `!this.format` is
17506
+ // always false here. Retained as a defensive fallback; the whole function
17507
+ // is @deprecated per AB#1557296 and slated for removal.
17454
17508
  } else if (!this.format && this.type === 'date') {
17455
17509
  this.setCustomValidityForType = i18n(this.lang, 'dateMMDDYYYY');
17456
17510
  } else if (this.dateFormatMap[this.format]) {
@@ -17512,6 +17566,8 @@ class BaseInput extends AuroElement$1 {
17512
17566
  this.configureDataForType();
17513
17567
  }
17514
17568
 
17569
+ this.syncDateValues(changedProperties);
17570
+
17515
17571
  if (changedProperties.has('value')) {
17516
17572
  if (this.value && this.value.length > 0) {
17517
17573
  this.hasValue = true;
@@ -17533,14 +17589,14 @@ class BaseInput extends AuroElement$1 {
17533
17589
 
17534
17590
  if (formattedValue !== this.inputElement.value) {
17535
17591
  this.skipNextProgrammaticInputEvent = true;
17536
- if (this.maskInstance && this.type !== 'date') {
17592
+ if (this.maskInstance && this.type === 'credit-card') {
17537
17593
  // Route through the mask so its _value and el.value stay in lock-step
17538
17594
  // (set value calls updateControl which writes el.value = displayValue).
17539
17595
  // Writing el.value directly leaves the mask thinking displayValue is
17540
- // stale; _saveSelection on the next focus/click then warns. Date is
17541
- // excluded because its formattedValue can be raw ISO when the calendar
17542
- // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17543
- // flip validity from patternMismatch to tooShort.
17596
+ // stale; _saveSelection on the next focus/click then warns. Scoped to
17597
+ // credit-card so date's own formattedValue (raw ISO when calendar-invalid)
17598
+ // isn't re-masked through the mm/dd/yyyy mask, which would truncate it
17599
+ // and flip validity from patternMismatch to tooShort.
17544
17600
  this.maskInstance.value = formattedValue || '';
17545
17601
  } else if (formattedValue) {
17546
17602
  this.inputElement.value = formattedValue;
@@ -17582,65 +17638,120 @@ class BaseInput extends AuroElement$1 {
17582
17638
  }));
17583
17639
  }
17584
17640
 
17641
+
17642
+ /**
17643
+ * Synchronizes the ISO string values and Date object representations for date-related properties.
17644
+ * This keeps the model and display values aligned when either side changes.
17645
+ *
17646
+ * When a full date format is in use, this method updates `value`, `min`, and `max` from their corresponding
17647
+ * Date objects or vice versa, based on which properties have changed. It only runs when the current configuration
17648
+ * represents a full year/month/day date format.
17649
+ *
17650
+ * @param {Map<string, unknown>|undefined} [changedProperties=undefined] - Optional map of changed properties used to limit which values are synchronized.
17651
+ * @returns {void}
17652
+ * @private
17653
+ */
17654
+ syncDateValues(changedProperties = undefined) {
17655
+ if (!this.util.isFullDateFormat(this.type, this.format)) {
17656
+ return;
17657
+ }
17658
+
17659
+ this.syncSingleDateValue(changedProperties, 'valueObject', 'value');
17660
+ this.syncSingleDateValue(changedProperties, 'minObject', 'min');
17661
+ this.syncSingleDateValue(changedProperties, 'maxObject', 'max');
17662
+ }
17663
+
17664
+ /**
17665
+ * Synchronizes one date object/string property pair.
17666
+ * @private
17667
+ * @param {Map<string, unknown>|undefined} changedProperties - Map of changed properties from Lit.
17668
+ * @param {string} objectProperty - Date object property name.
17669
+ * @param {string} valueProperty - ISO string property name.
17670
+ * @returns {void}
17671
+ */
17672
+ syncSingleDateValue(changedProperties, objectProperty, valueProperty) {
17673
+ const objectPropertyChanged = !changedProperties || changedProperties.has(objectProperty);
17674
+
17675
+ // objectProperty wins over valueProperty when both changed
17676
+ if (objectPropertyChanged && this[objectProperty]) {
17677
+ this[valueProperty] = dateFormatter.toISOFormatString(this[objectProperty]);
17678
+ return;
17679
+ }
17680
+
17681
+ const valuePropertyChanged = !changedProperties || changedProperties.has(valueProperty);
17682
+ if (!valuePropertyChanged) {
17683
+ return;
17684
+ }
17685
+
17686
+ // 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)
17687
+ if (
17688
+ changedProperties &&
17689
+ valueProperty === 'value' &&
17690
+ changedProperties.get('value') === undefined &&
17691
+ this[objectProperty] instanceof Date &&
17692
+ this[valueProperty] === dateFormatter.toISOFormatString(this[objectProperty])
17693
+ ) {
17694
+ return;
17695
+ }
17696
+
17697
+ if (dateFormatter.isValidDate(this[valueProperty])) {
17698
+ this.setDateObjectProperty(objectProperty, dateFormatter.stringToDateInstance(this[valueProperty]));
17699
+ } else {
17700
+ this.setDateObjectProperty(objectProperty, undefined);
17701
+ }
17702
+ }
17703
+
17585
17704
  /**
17586
17705
  * Sets up IMasks and logic based on auto-formatting requirements.
17587
17706
  * @private
17588
17707
  * @returns {void}
17589
17708
  */
17590
17709
  configureAutoFormatting() {
17591
- // _configuringMask gates two things: external re-entry into this method
17592
- // while setup is mid-flight (from property changes that call back here),
17593
- // and the accept/complete listeners below both need to ignore the mask
17594
- // events fired by our own value-restore step.
17710
+ // Re-entrancy guard. The patched-setter's synthetic input event (suppressed
17711
+ // by _configuringMask above) could otherwise trigger handleInput
17712
+ // processCreditCard configureAutoFormatting before the outer call's
17713
+ // set value has finished its alignCursor pass.
17595
17714
  if (this._configuringMask) return;
17596
17715
  this._configuringMask = true;
17597
17716
  try {
17598
- // Destroy any prior mask so IMask can attach fresh under the new format.
17599
- // Null the reference too — if the new maskOptions.mask is falsy (e.g.
17600
- // type switched from credit-card to text) the IMask() reassignment
17601
- // below is skipped, and downstream writes at line ~823 would otherwise
17602
- // route through the destroyed instance.
17603
17717
  if (this.maskInstance) {
17604
17718
  this.maskInstance.destroy();
17605
- this.maskInstance = null;
17606
17719
  }
17607
17720
 
17721
+ // Pass new format to util
17608
17722
  this.util.updateFormat(this.format);
17609
17723
 
17610
17724
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17611
17725
 
17612
17726
  if (this.inputElement && maskOptions.mask) {
17613
- // Capture the current display so it can be re-applied after IMask
17614
- // attaches. The restore at the bottom goes through maskInstance.value
17615
- // (not inputElement.value directly) so the mask's internal state and
17616
- // the input's displayed text stay in lock-step.
17617
- let existingValue = this.inputElement.value;
17618
17727
 
17619
- // Format-change case (e.g. locale switch): existingValue is the OLD
17620
- // mask's display string and may not parse under the new mask. When
17621
- // we have a valid date model, rebuild the display from valueObject
17622
- // (the canonical source) using the new mask's format function.
17728
+ // Stash and clear any existing value before IMask init.
17729
+ // IMask's constructor processes the current input value which requires
17730
+ // selection state clearing first avoids that scenario entirely.
17731
+ // When the format changes (e.g. locale switch) and we have a valid ISO
17732
+ // model value, compute the display string for the NEW format instead of
17733
+ // re-using the old display string, which may be invalid in the new mask.
17734
+ let existingValue = this.inputElement.value;
17623
17735
  if (
17624
17736
  this.util.isFullDateFormat(this.type, this.format) &&
17625
17737
  this.value &&
17626
- this.valueObject &&
17738
+ dateFormatter.isValidDate(this.value) &&
17739
+ this.valueObject instanceof Date &&
17740
+ !Number.isNaN(this.valueObject.getTime()) &&
17627
17741
  typeof maskOptions.format === 'function'
17628
17742
  ) {
17629
17743
  existingValue = maskOptions.format(this.valueObject);
17630
17744
  }
17631
17745
 
17632
- // Clear before IMask attaches so the constructor seeds an empty
17633
- // internal value. Otherwise IMask reads the stale unmasked string
17634
- // and emits a spurious 'accept' before the restore below runs.
17635
17746
  this.skipNextProgrammaticInputEvent = true;
17636
17747
  this.inputElement.value = '';
17637
17748
 
17638
17749
  this.maskInstance = IMask(this.inputElement, maskOptions);
17639
17750
 
17640
- // Mask fires 'accept' on every value change, including the restore
17641
- // step below. Skip events fired during configureAutoFormatting so
17642
- // we don't overwrite a value the parent just pushed.
17643
17751
  this.maskInstance.on('accept', () => {
17752
+ // Suppress propagation during configureAutoFormatting's own value-restoration
17753
+ // (line below) — the mask emits 'accept' on every value-set, including ours,
17754
+ // and we don't want to overwrite a value the parent just pushed.
17644
17755
  if (this._configuringMask) return;
17645
17756
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17646
17757
  if (this.type === "date") {
@@ -17648,8 +17759,6 @@ class BaseInput extends AuroElement$1 {
17648
17759
  }
17649
17760
  });
17650
17761
 
17651
- // Mask fires 'complete' on the restore step below for any value that
17652
- // happens to be a complete match. Same setup-suppression as 'accept'.
17653
17762
  this.maskInstance.on('complete', () => {
17654
17763
  if (this._configuringMask) return;
17655
17764
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17658,9 +17767,7 @@ class BaseInput extends AuroElement$1 {
17658
17767
  }
17659
17768
  });
17660
17769
 
17661
- // Write existingValue through the mask (not the input directly) so
17662
- // the mask reformats it under the new rules and keeps its internal
17663
- // _value aligned with the input's displayed text.
17770
+ // Restore the stashed value through IMask so it's properly masked
17664
17771
  if (existingValue) {
17665
17772
  this.maskInstance.value = existingValue;
17666
17773
  }
@@ -17828,6 +17935,7 @@ class BaseInput extends AuroElement$1 {
17828
17935
  */
17829
17936
  reset() {
17830
17937
  this.value = undefined;
17938
+ this.setDateObjectProperty('valueObject', undefined);
17831
17939
  this.validation.reset(this);
17832
17940
  }
17833
17941
 
@@ -17836,6 +17944,7 @@ class BaseInput extends AuroElement$1 {
17836
17944
  */
17837
17945
  clear() {
17838
17946
  this.value = undefined;
17947
+ this.setDateObjectProperty('valueObject', undefined);
17839
17948
  }
17840
17949
 
17841
17950
  /**
@@ -17873,7 +17982,7 @@ class BaseInput extends AuroElement$1 {
17873
17982
  */
17874
17983
  get placeholderStr() {
17875
17984
  if (!this.placeholder && this.type === 'date') {
17876
- return this.format ? this.format.toUpperCase() : 'MM/DD/YYYY';
17985
+ return this.format.toUpperCase();
17877
17986
  }
17878
17987
  return this.placeholder || "";
17879
17988
  }
@@ -18364,7 +18473,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18364
18473
  }
18365
18474
  };
18366
18475
 
18367
- var formkitVersion$1 = '202606231948';
18476
+ var formkitVersion$1 = '202606241801';
18368
18477
 
18369
18478
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18370
18479
  // See LICENSE in the project root for license information.
@@ -19485,7 +19594,7 @@ class AuroBibtemplate extends i$3 {
19485
19594
  }
19486
19595
  }
19487
19596
 
19488
- var formkitVersion = '202606231948';
19597
+ var formkitVersion = '202606241801';
19489
19598
 
19490
19599
  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}`;
19491
19600
 
@@ -20472,7 +20581,7 @@ class AuroCombobox extends AuroElement {
20472
20581
  if (this.menu) {
20473
20582
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20474
20583
  }
20475
- const label = getOptionLabel(this.menu.optionSelected);
20584
+ const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20476
20585
  this.updateTriggerTextDisplay(label || this.value);
20477
20586
  }
20478
20587
 
@@ -20486,9 +20595,40 @@ class AuroCombobox extends AuroElement {
20486
20595
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20487
20596
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20488
20597
 
20489
- const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20490
- if (!suppressed) {
20491
- this.syncInputValuesAcrossTriggerAndBib(label || this.value);
20598
+ if (!this.persistInput && !(this.behavior === 'suggestion' && isInputFocusedWithNoSelection)) {
20599
+ const nextValue = label || this.value;
20600
+ // Only set the flag when there's an actual write to suppress —
20601
+ // syncValuesAndStates re-enters here during typing when both inputs
20602
+ // already match, and a no-op flag flip would make the bib branch's
20603
+ // bail eat legitimate user-input events.
20604
+ const triggerNeedsSync = this.input.value !== nextValue;
20605
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20606
+ if (triggerNeedsSync || bibNeedsSync) {
20607
+ this._syncingDisplayValue = true;
20608
+ if (triggerNeedsSync) {
20609
+ this.input.value = nextValue;
20610
+ }
20611
+ if (bibNeedsSync) {
20612
+ this.inputInBib.value = nextValue;
20613
+ }
20614
+ const pending = [];
20615
+ if (triggerNeedsSync) {
20616
+ pending.push(this.input.updateComplete);
20617
+ }
20618
+ if (bibNeedsSync) {
20619
+ pending.push(this.inputInBib.updateComplete);
20620
+ }
20621
+ Promise.all(pending).then(() => {
20622
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20623
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20624
+ this.input.maskInstance.updateValue();
20625
+ }
20626
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20627
+ this.inputInBib.maskInstance.updateValue();
20628
+ }
20629
+ this._syncingDisplayValue = false;
20630
+ });
20631
+ }
20492
20632
  }
20493
20633
 
20494
20634
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20516,48 +20656,6 @@ class AuroCombobox extends AuroElement {
20516
20656
  this.requestUpdate();
20517
20657
  }
20518
20658
 
20519
- /**
20520
- * Writes nextValue to the trigger input and the bib input when their current
20521
- * value differs, then re-asserts imask after Lit's update flushes.
20522
- * @param {string} nextValue - The value to write to both inputs.
20523
- * @returns {Promise<void> | null} Promise that resolves after the inputs flush,
20524
- * or null when no sync was needed (so callers can skip post-flush work).
20525
- * @private
20526
- */
20527
- syncInputValuesAcrossTriggerAndBib(nextValue) {
20528
- // Only set the flag when there's an actual write to suppress —
20529
- // syncValuesAndStates re-enters here during typing when both inputs
20530
- // already match, and a no-op flag flip would make the bib branch's
20531
- // bail eat legitimate user-input events.
20532
- const triggerNeedsSync = this.input.value !== nextValue;
20533
- const bibNeedsSync = this.inputInBib.value !== nextValue;
20534
- if (!triggerNeedsSync && !bibNeedsSync) {
20535
- return null;
20536
- }
20537
-
20538
- this._syncingDisplayValue = true;
20539
-
20540
- const pending = [];
20541
- if (triggerNeedsSync) {
20542
- this.input.value = nextValue;
20543
- pending.push(this.input.updateComplete);
20544
- }
20545
- if (bibNeedsSync) {
20546
- this.inputInBib.value = nextValue;
20547
- pending.push(this.inputInBib.updateComplete);
20548
- }
20549
- return Promise.all(pending).then(() => {
20550
- // imask reasserts otherwise, and handleBlur reverts to its stale value.
20551
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20552
- this.input.maskInstance.updateValue();
20553
- }
20554
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20555
- this.inputInBib.maskInstance.updateValue();
20556
- }
20557
- this._syncingDisplayValue = false;
20558
- });
20559
- }
20560
-
20561
20659
  /**
20562
20660
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20563
20661
  * @private
@@ -20585,9 +20683,9 @@ class AuroCombobox extends AuroElement {
20585
20683
  this.syncValuesAndStates();
20586
20684
  }
20587
20685
 
20588
- // Re-activate when optionActive is not in the current scrollable viewport,
20589
- // or when _index has been reset (e.g. by clearSelection in an async update)
20590
- // so that makeSelection() can find the correct option.
20686
+ // Re-activate when optionActive is no longer visible, or when _index has
20687
+ // been reset (e.g. by clearSelection in an async update) so that
20688
+ // makeSelection() can find the correct option.
20591
20689
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20592
20690
  this.activateFirstEnabledAvailableOption();
20593
20691
  }
@@ -20721,25 +20819,28 @@ class AuroCombobox extends AuroElement {
20721
20819
 
20722
20820
  guardTouchPassthrough(this.menu);
20723
20821
 
20724
- // showModal() takes focus away from the trigger. Early focus once
20725
- // the dialog has painted; the shared doubleRaf below is the fallback
20726
- // for when the dialog needs an extra frame and also clears the
20727
- // validation guard.
20822
+ // The dialog's showModal() steals focus from the trigger.
20823
+ // A single rAF is enough for the dialog DOM to be painted and
20824
+ // focusable, then doubleRaf finalizes the transition.
20728
20825
  requestAnimationFrame(() => {
20729
20826
  this.setInputFocus();
20730
20827
  });
20731
- }
20732
- // else (desktop popover-open): Chrome resets the trigger caret to
20733
- // [0, 0] when its floating <label for="…"> overlay receives focus.
20734
- // The shared doubleRaf below parks the caret back at end-of-text
20735
- // after the dropdown layout settles.
20736
-
20737
- doubleRaf(() => {
20738
- this.setInputFocus();
20739
- if (this._inFullscreenTransition) {
20828
+
20829
+ doubleRaf(() => {
20830
+ this.setInputFocus();
20740
20831
  this._inFullscreenTransition = false;
20741
- }
20742
- });
20832
+ });
20833
+ } else {
20834
+ // Desktop popover-open: restore the trigger caret to end-of-text.
20835
+ // Clicking the trigger lands on auro-input's floating <label for="…">
20836
+ // overlay; Chrome resets the native input's selection to [0, 0] on
20837
+ // label-focus before any JS runs. setInputFocus()'s non-fullscreen
20838
+ // branch parks the caret at end. doubleRaf lets the dropdown layout
20839
+ // settle first, matching the fullscreen branch timing.
20840
+ doubleRaf(() => {
20841
+ this.setInputFocus();
20842
+ });
20843
+ }
20743
20844
  }
20744
20845
  });
20745
20846
 
@@ -20950,7 +21051,7 @@ class AuroCombobox extends AuroElement {
20950
21051
  if (this.menu.optionSelected) {
20951
21052
  const selected = this.menu.optionSelected;
20952
21053
 
20953
- if (this.optionSelected !== selected) {
21054
+ if (!this.optionSelected || this.optionSelected !== selected) {
20954
21055
  this.optionSelected = selected;
20955
21056
  }
20956
21057
 
@@ -20963,7 +21064,7 @@ class AuroCombobox extends AuroElement {
20963
21064
  }
20964
21065
 
20965
21066
  // Update display
20966
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
21067
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20967
21068
 
20968
21069
  // Update match word for filtering
20969
21070
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -20979,22 +21080,13 @@ class AuroCombobox extends AuroElement {
20979
21080
  this.hideBib();
20980
21081
  }
20981
21082
 
20982
- // Move focus to the clear button when the user makes a selection.
20983
- if (!isEcho && this.menu.value !== undefined) {
20984
- this.setClearBtnFocus();
20985
- }
20986
-
20987
21083
  // Announce the selection after the dropdown closes so it isn't
20988
21084
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
20989
- // Skip when there's no selected value (e.g. menu.clearSelection() from
20990
- // the unmatched-value path), otherwise VoiceOver reads "undefined".
20991
21085
  const selectedValue = this.menu.value;
20992
- if (selectedValue) {
20993
- const announcementDelay = 300;
20994
- setTimeout(() => {
20995
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
20996
- }, announcementDelay);
20997
- }
21086
+ const announcementDelay = 300;
21087
+ setTimeout(() => {
21088
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21089
+ }, announcementDelay);
20998
21090
  }
20999
21091
 
21000
21092
  // Programmatic value syncs leave availableOptions stale because
@@ -21008,6 +21100,18 @@ class AuroCombobox extends AuroElement {
21008
21100
  this._programmaticFilterRefresh = false;
21009
21101
  }, 0);
21010
21102
  }
21103
+
21104
+ // base-input skips auto-validate when focus is inside its own shadow,
21105
+ // which it is after setTriggerInputFocus — re-run so prior tooShort
21106
+ // clears. processCreditCard reasserts errorMessage on every render.
21107
+ if (!isEcho && this.menu.optionSelected && this.input.validate) {
21108
+ this.input.updateComplete.then(() => {
21109
+ this.input.validate(true);
21110
+ if (this.input.validity === 'valid') {
21111
+ this.input.errorMessage = '';
21112
+ }
21113
+ });
21114
+ }
21011
21115
  });
21012
21116
 
21013
21117
  this.menu.addEventListener('auroMenu-customEventFired', () => {
@@ -21342,7 +21446,15 @@ class AuroCombobox extends AuroElement {
21342
21446
  this.menu.value = undefined;
21343
21447
  this.validation.reset(this);
21344
21448
  this.touched = false;
21449
+ // Force validity back to the cleared state. validation.reset() calls
21450
+ // validate() at the end, and (post-credit-card-fix) the trigger input's
21451
+ // residual validity may still be copied into the combobox by the
21452
+ // auroInputElements loop. Explicitly clear here so reset() actually
21453
+ // leaves the component in a "no validity" state.
21345
21454
  this.validity = undefined;
21455
+ this.errorMessage = '';
21456
+ this.input.validity = undefined;
21457
+ this.input.errorMessage = '';
21346
21458
  }
21347
21459
 
21348
21460
  /**
@@ -21395,27 +21507,52 @@ class AuroCombobox extends AuroElement {
21395
21507
  this.input.value = this.value;
21396
21508
  }
21397
21509
 
21398
- // Sync menu.value only when an option actually matches, or when filter
21399
- // mode needs setMenuValue to dispatch auroMenu-selectValueFailure (the
21400
- // listener at line 1206 clears combobox.value, mirroring select's
21401
- // pattern). Otherwise sync the input display for freeform values.
21510
+ // Sync menu.value only when an option actually matches; otherwise clear it.
21402
21511
  if (this.menu.options && this.menu.options.length > 0) {
21403
- if (this.menu.options.some((opt) => opt.value === this.value) || this.behavior === 'filter') {
21512
+ if (this.menu.options.some((opt) => opt.value === this.value)) {
21513
+ this.setMenuValue(this.value);
21514
+ } else if (this.behavior === 'filter') {
21515
+ // In filter mode, freeform values aren't allowed. Push the unmatched
21516
+ // value through setMenuValue so menu dispatches auroMenu-selectValueFailure
21517
+ // and the listener at line 1206 clears combobox.value (mirrors select's
21518
+ // pattern). Bypassing setMenuValue here would skip the failure cascade.
21404
21519
  this.setMenuValue(this.value);
21405
21520
  } else {
21406
- // Clear menu.value AND menu.optionSelected together. Clearing only
21407
- // menu.value leaves the previously-selected option element pinned
21408
- // as menu.optionSelected; a later auroMenu-selectedOption event
21409
- // would then write its stale .value back into combobox.value.
21410
- if (this.menu.value || this.menu.optionSelected) {
21411
- this.menu.clearSelection();
21521
+ if (this.menu.value) {
21522
+ this.menu.value = undefined;
21412
21523
  }
21413
- // Suggestion-mode freeform value: sync the trigger + bib to show it,
21414
- // then refresh the filter once the inputs flush (handleInputValueChange
21415
- // bailed on _syncingDisplayValue).
21416
- const syncPromise = this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21417
- if (syncPromise) {
21418
- syncPromise.then(() => {
21524
+ // Sync the display for programmatic freeform value changes (e.g. the
21525
+ // swap-values pattern). Guard with _syncingDisplayValue so that the
21526
+ // re-entrant input event from base-input.notifyValueChanged() is
21527
+ // ignored by handleInputValueChange. Skip the sync when input already
21528
+ // matches to avoid spurious events during the normal typing path.
21529
+ const nextValue = this.value || '';
21530
+ const triggerNeedsSync = this.input.value !== nextValue;
21531
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
21532
+ if (triggerNeedsSync || bibNeedsSync) {
21533
+ this._syncingDisplayValue = true;
21534
+ if (triggerNeedsSync) {
21535
+ this.input.value = nextValue;
21536
+ }
21537
+ if (bibNeedsSync) {
21538
+ this.inputInBib.value = nextValue;
21539
+ }
21540
+ const pending = [];
21541
+ if (triggerNeedsSync) {
21542
+ pending.push(this.input.updateComplete);
21543
+ }
21544
+ if (bibNeedsSync) {
21545
+ pending.push(this.inputInBib.updateComplete);
21546
+ }
21547
+ Promise.all(pending).then(() => {
21548
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
21549
+ this.input.maskInstance.updateValue();
21550
+ }
21551
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
21552
+ this.inputInBib.maskInstance.updateValue();
21553
+ }
21554
+ this._syncingDisplayValue = false;
21555
+ // handleInputValueChange bailed on the flag — refresh the filter.
21419
21556
  this._programmaticFilterRefresh = true;
21420
21557
  this.handleMenuOptions();
21421
21558
  setTimeout(() => {