@aurodesignsystem-dev/auro-formkit 0.0.0-pr1506.1 → 0.0.0-pr1507.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 (49) 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 +268 -140
  7. package/components/combobox/demo/getting-started.min.js +268 -140
  8. package/components/combobox/demo/index.min.js +268 -140
  9. package/components/combobox/dist/auro-combobox.d.ts +0 -9
  10. package/components/combobox/dist/index.js +266 -138
  11. package/components/combobox/dist/registered.js +266 -138
  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 +149 -39
  17. package/components/datepicker/demo/index.min.js +149 -39
  18. package/components/datepicker/dist/index.js +149 -39
  19. package/components/datepicker/dist/registered.js +149 -39
  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 +570 -221
  26. package/components/form/demo/getting-started.min.js +570 -221
  27. package/components/form/demo/index.min.js +570 -221
  28. package/components/form/demo/registerDemoDeps.min.js +570 -221
  29. package/components/input/demo/customize.min.js +147 -36
  30. package/components/input/demo/getting-started.min.js +147 -36
  31. package/components/input/demo/index.min.js +147 -36
  32. package/components/input/dist/base-input.d.ts +48 -0
  33. package/components/input/dist/index.js +147 -36
  34. package/components/input/dist/registered.js +147 -36
  35. package/components/menu/demo/index.min.js +2 -2
  36. package/components/menu/dist/index.js +2 -2
  37. package/components/menu/dist/registered.js +2 -2
  38. package/components/radio/demo/customize.min.js +1 -1
  39. package/components/radio/demo/getting-started.min.js +1 -1
  40. package/components/radio/demo/index.min.js +1 -1
  41. package/components/radio/dist/index.js +1 -1
  42. package/components/radio/dist/registered.js +1 -1
  43. package/components/select/demo/customize.min.js +4 -4
  44. package/components/select/demo/getting-started.min.js +4 -4
  45. package/components/select/demo/index.min.js +4 -4
  46. package/components/select/dist/index.js +2 -2
  47. package/components/select/dist/registered.js +2 -2
  48. package/custom-elements.json +264 -20
  49. package/package.json +1 -1
@@ -767,8 +767,6 @@ function navigateArrow(component, direction, options = {}) {
767
767
  }
768
768
  }
769
769
 
770
- /* eslint-disable no-underscore-dangle */
771
-
772
770
  /**
773
771
  * Returns the clear button element from the active input's shadow
774
772
  * DOM, if available.
@@ -804,9 +802,11 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
804
802
  * @param {Object} menu - The menu component.
805
803
  */
806
804
  function reconcileMenuIndex(menu) {
805
+ // eslint-disable-next-line no-underscore-dangle
807
806
  if (menu._index < 0 && menu.optionActive && menu.items) {
808
807
  const idx = menu.items.indexOf(menu.optionActive);
809
808
  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 = '202606192004';
4809
+ var formkitVersion$2 = '202606192121';
4810
4810
 
4811
4811
  let AuroElement$2 = class AuroElement extends LitElement {
4812
4812
  static get properties() {
@@ -16520,7 +16520,6 @@ class AuroInputUtilities {
16520
16520
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16521
16521
 
16522
16522
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16523
- console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16524
16523
  return undefined;
16525
16524
  }
16526
16525
 
@@ -16672,8 +16671,10 @@ class BaseInput extends AuroElement$1 {
16672
16671
  this.layout = 'classic';
16673
16672
  this.locale = 'en-US';
16674
16673
  this.max = undefined;
16674
+ this._maxObject = undefined;
16675
16675
  this.maxLength = undefined;
16676
16676
  this.min = undefined;
16677
+ this._minObject = undefined;
16677
16678
  this.minLength = undefined;
16678
16679
  this.required = false;
16679
16680
  this.onDark = false;
@@ -16681,6 +16682,7 @@ class BaseInput extends AuroElement$1 {
16681
16682
  this.size = 'lg';
16682
16683
  this.shape = 'classic';
16683
16684
  this.value = undefined;
16685
+ this._valueObject = undefined;
16684
16686
 
16685
16687
  this._initializePrivateDefaults();
16686
16688
  }
@@ -17220,7 +17222,7 @@ class BaseInput extends AuroElement$1 {
17220
17222
  * @returns {Date|undefined}
17221
17223
  */
17222
17224
  get valueObject() {
17223
- return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17225
+ return this._valueObject || this._computeDateObjectFallback(this.value);
17224
17226
  }
17225
17227
 
17226
17228
  /**
@@ -17228,7 +17230,7 @@ class BaseInput extends AuroElement$1 {
17228
17230
  * @returns {Date|undefined}
17229
17231
  */
17230
17232
  get minObject() {
17231
- return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17233
+ return this._minObject || this._computeDateObjectFallback(this.min);
17232
17234
  }
17233
17235
 
17234
17236
  /**
@@ -17236,7 +17238,50 @@ class BaseInput extends AuroElement$1 {
17236
17238
  * @returns {Date|undefined}
17237
17239
  */
17238
17240
  get maxObject() {
17239
- return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
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);
17240
17285
  }
17241
17286
 
17242
17287
  connectedCallback() {
@@ -17265,6 +17310,7 @@ class BaseInput extends AuroElement$1 {
17265
17310
  format: this.format
17266
17311
  });
17267
17312
  this.configureDataForType();
17313
+ this.syncDateValues();
17268
17314
  }
17269
17315
 
17270
17316
  disconnectedCallback() {
@@ -17284,6 +17330,10 @@ class BaseInput extends AuroElement$1 {
17284
17330
  this.wrapperElement.addEventListener('click', this.handleClick);
17285
17331
  }
17286
17332
 
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
+ }
17287
17337
  this.inputId = this.id ? `${this.id}-input` : window.crypto.randomUUID();
17288
17338
 
17289
17339
  // use validity message override if declared when initializing the component
@@ -17294,6 +17344,7 @@ class BaseInput extends AuroElement$1 {
17294
17344
  this.setCustomHelpTextMessage();
17295
17345
  this.configureAutoFormatting();
17296
17346
  this.configureDataForType();
17347
+ this.syncDateValues();
17297
17348
  }
17298
17349
 
17299
17350
  /**
@@ -17430,6 +17481,8 @@ class BaseInput extends AuroElement$1 {
17430
17481
  this.configureDataForType();
17431
17482
  }
17432
17483
 
17484
+ this.syncDateValues(changedProperties);
17485
+
17433
17486
  if (changedProperties.has('value')) {
17434
17487
  if (this.value && this.value.length > 0) {
17435
17488
  this.hasValue = true;
@@ -17451,14 +17504,14 @@ class BaseInput extends AuroElement$1 {
17451
17504
 
17452
17505
  if (formattedValue !== this.inputElement.value) {
17453
17506
  this.skipNextProgrammaticInputEvent = true;
17454
- if (this.maskInstance && this.type !== 'date') {
17507
+ if (this.maskInstance && this.type === 'credit-card') {
17455
17508
  // Route through the mask so its _value and el.value stay in lock-step
17456
17509
  // (set value calls updateControl which writes el.value = displayValue).
17457
17510
  // Writing el.value directly leaves the mask thinking displayValue is
17458
- // stale; _saveSelection on the next focus/click then warns. Date is
17459
- // excluded because its formattedValue can be raw ISO when the calendar
17460
- // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17461
- // flip validity from patternMismatch to tooShort.
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.
17462
17515
  this.maskInstance.value = formattedValue || '';
17463
17516
  } else if (formattedValue) {
17464
17517
  this.inputElement.value = formattedValue;
@@ -17500,60 +17553,120 @@ class BaseInput extends AuroElement$1 {
17500
17553
  }));
17501
17554
  }
17502
17555
 
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
+
17503
17619
  /**
17504
17620
  * Sets up IMasks and logic based on auto-formatting requirements.
17505
17621
  * @private
17506
17622
  * @returns {void}
17507
17623
  */
17508
17624
  configureAutoFormatting() {
17509
- // _configuringMask gates two things: external re-entry into this method
17510
- // while setup is mid-flight (from property changes that call back here),
17511
- // and the accept/complete listeners below both need to ignore the mask
17512
- // events fired by our own value-restore step.
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.
17513
17629
  if (this._configuringMask) return;
17514
17630
  this._configuringMask = true;
17515
17631
  try {
17516
- // Destroy any prior mask so IMask can attach fresh under the new format.
17517
17632
  if (this.maskInstance) {
17518
17633
  this.maskInstance.destroy();
17519
17634
  }
17520
17635
 
17636
+ // Pass new format to util
17521
17637
  this.util.updateFormat(this.format);
17522
17638
 
17523
17639
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17524
17640
 
17525
17641
  if (this.inputElement && maskOptions.mask) {
17526
- // Capture the current display so it can be re-applied after IMask
17527
- // attaches. The restore at the bottom goes through maskInstance.value
17528
- // (not inputElement.value directly) so the mask's internal state and
17529
- // the input's displayed text stay in lock-step.
17530
- let existingValue = this.inputElement.value;
17531
17642
 
17532
- // Format-change case (e.g. locale switch): existingValue is the OLD
17533
- // mask's display string and may not parse under the new mask. When
17534
- // we have a valid date model, rebuild the display from valueObject
17535
- // (the canonical source) using the new mask's format function.
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.
17649
+ let existingValue = this.inputElement.value;
17536
17650
  if (
17537
17651
  this.util.isFullDateFormat(this.type, this.format) &&
17538
17652
  this.value &&
17539
- this.valueObject &&
17653
+ dateFormatter.isValidDate(this.value) &&
17654
+ this.valueObject instanceof Date &&
17655
+ !Number.isNaN(this.valueObject.getTime()) &&
17540
17656
  typeof maskOptions.format === 'function'
17541
17657
  ) {
17542
17658
  existingValue = maskOptions.format(this.valueObject);
17543
17659
  }
17544
17660
 
17545
- // Clear before IMask attaches so the constructor seeds an empty
17546
- // internal value. Otherwise IMask reads the stale unmasked string
17547
- // and emits a spurious 'accept' before the restore below runs.
17548
17661
  this.skipNextProgrammaticInputEvent = true;
17549
17662
  this.inputElement.value = '';
17550
17663
 
17551
17664
  this.maskInstance = IMask(this.inputElement, maskOptions);
17552
17665
 
17553
- // Mask fires 'accept' on every value change, including the restore
17554
- // step below. Skip events fired during configureAutoFormatting so
17555
- // we don't overwrite a value the parent just pushed.
17556
17666
  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.
17557
17670
  if (this._configuringMask) return;
17558
17671
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17559
17672
  if (this.type === "date") {
@@ -17561,8 +17674,6 @@ class BaseInput extends AuroElement$1 {
17561
17674
  }
17562
17675
  });
17563
17676
 
17564
- // Mask fires 'complete' on the restore step below for any value that
17565
- // happens to be a complete match. Same setup-suppression as 'accept'.
17566
17677
  this.maskInstance.on('complete', () => {
17567
17678
  if (this._configuringMask) return;
17568
17679
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17571,9 +17682,7 @@ class BaseInput extends AuroElement$1 {
17571
17682
  }
17572
17683
  });
17573
17684
 
17574
- // Write existingValue through the mask (not the input directly) so
17575
- // the mask reformats it under the new rules and keeps its internal
17576
- // _value aligned with the input's displayed text.
17685
+ // Restore the stashed value through IMask so it's properly masked
17577
17686
  if (existingValue) {
17578
17687
  this.maskInstance.value = existingValue;
17579
17688
  }
@@ -17741,6 +17850,7 @@ class BaseInput extends AuroElement$1 {
17741
17850
  */
17742
17851
  reset() {
17743
17852
  this.value = undefined;
17853
+ this.setDateObjectProperty('valueObject', undefined);
17744
17854
  this.validation.reset(this);
17745
17855
  }
17746
17856
 
@@ -17749,6 +17859,7 @@ class BaseInput extends AuroElement$1 {
17749
17859
  */
17750
17860
  clear() {
17751
17861
  this.value = undefined;
17862
+ this.setDateObjectProperty('valueObject', undefined);
17752
17863
  }
17753
17864
 
17754
17865
  /**
@@ -18277,7 +18388,7 @@ let AuroHelpText$1 = class AuroHelpText extends LitElement {
18277
18388
  }
18278
18389
  };
18279
18390
 
18280
- var formkitVersion$1 = '202606192004';
18391
+ var formkitVersion$1 = '202606192121';
18281
18392
 
18282
18393
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18283
18394
  // See LICENSE in the project root for license information.
@@ -19398,7 +19509,7 @@ class AuroBibtemplate extends LitElement {
19398
19509
  }
19399
19510
  }
19400
19511
 
19401
- var formkitVersion = '202606192004';
19512
+ var formkitVersion = '202606192121';
19402
19513
 
19403
19514
  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}`;
19404
19515
 
@@ -20385,7 +20496,7 @@ class AuroCombobox extends AuroElement {
20385
20496
  if (this.menu) {
20386
20497
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20387
20498
  }
20388
- const label = getOptionLabel(this.menu.optionSelected);
20499
+ const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20389
20500
  this.updateTriggerTextDisplay(label || this.value);
20390
20501
  }
20391
20502
 
@@ -20399,9 +20510,40 @@ class AuroCombobox extends AuroElement {
20399
20510
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20400
20511
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20401
20512
 
20402
- const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20403
- if (!suppressed) {
20404
- this.syncInputValuesAcrossTriggerAndBib(label || this.value);
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
+ }
20405
20547
  }
20406
20548
 
20407
20549
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20429,52 +20571,6 @@ class AuroCombobox extends AuroElement {
20429
20571
  this.requestUpdate();
20430
20572
  }
20431
20573
 
20432
- /**
20433
- * Writes nextValue to the trigger input and the bib input when their current
20434
- * value differs, then re-asserts imask after Lit's update flushes.
20435
- * @param {string} nextValue - The value to write to both inputs.
20436
- * @returns {Promise<void> | null} Promise that resolves after the inputs flush,
20437
- * or null when no sync was needed (so callers can skip post-flush work).
20438
- * @private
20439
- */
20440
- syncInputValuesAcrossTriggerAndBib(nextValue) {
20441
- // Only set the flag when there's an actual write to suppress —
20442
- // syncValuesAndStates re-enters here during typing when both inputs
20443
- // already match, and a no-op flag flip would make the bib branch's
20444
- // bail eat legitimate user-input events.
20445
- const triggerNeedsSync = this.input.value !== nextValue;
20446
- const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20447
- if (!triggerNeedsSync && !bibNeedsSync) {
20448
- return null;
20449
- }
20450
-
20451
- this._syncingDisplayValue = true;
20452
- if (triggerNeedsSync) {
20453
- this.input.value = nextValue;
20454
- }
20455
- if (bibNeedsSync) {
20456
- this.inputInBib.value = nextValue;
20457
- }
20458
-
20459
- const pending = [];
20460
- if (triggerNeedsSync) {
20461
- pending.push(this.input.updateComplete);
20462
- }
20463
- if (bibNeedsSync) {
20464
- pending.push(this.inputInBib.updateComplete);
20465
- }
20466
- return Promise.all(pending).then(() => {
20467
- // imask reasserts otherwise, and handleBlur reverts to its stale value.
20468
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20469
- this.input.maskInstance.updateValue();
20470
- }
20471
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20472
- this.inputInBib.maskInstance.updateValue();
20473
- }
20474
- this._syncingDisplayValue = false;
20475
- });
20476
- }
20477
-
20478
20574
  /**
20479
20575
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20480
20576
  * @private
@@ -20502,9 +20598,9 @@ class AuroCombobox extends AuroElement {
20502
20598
  this.syncValuesAndStates();
20503
20599
  }
20504
20600
 
20505
- // Re-activate when optionActive is not in the current scrollable viewport,
20506
- // or when _index has been reset (e.g. by clearSelection in an async update)
20507
- // so that makeSelection() can find the correct option.
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.
20508
20604
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20509
20605
  this.activateFirstEnabledAvailableOption();
20510
20606
  }
@@ -20638,25 +20734,28 @@ class AuroCombobox extends AuroElement {
20638
20734
 
20639
20735
  guardTouchPassthrough(this.menu);
20640
20736
 
20641
- // showModal() takes focus away from the trigger. Early focus once
20642
- // the dialog has painted; the shared doubleRaf below is the fallback
20643
- // for when the dialog needs an extra frame and also clears the
20644
- // validation guard.
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.
20645
20740
  requestAnimationFrame(() => {
20646
20741
  this.setInputFocus();
20647
20742
  });
20648
- }
20649
- // else (desktop popover-open): Chrome resets the trigger caret to
20650
- // [0, 0] when its floating <label for="…"> overlay receives focus.
20651
- // The shared doubleRaf below parks the caret back at end-of-text
20652
- // after the dropdown layout settles.
20653
-
20654
- doubleRaf(() => {
20655
- this.setInputFocus();
20656
- if (this._inFullscreenTransition) {
20743
+
20744
+ doubleRaf(() => {
20745
+ this.setInputFocus();
20657
20746
  this._inFullscreenTransition = false;
20658
- }
20659
- });
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
+ }
20660
20759
  }
20661
20760
  });
20662
20761
 
@@ -20763,9 +20862,9 @@ class AuroCombobox extends AuroElement {
20763
20862
  } else {
20764
20863
  // Safety net for the strategy-change setTimeout: a viewport-crossing
20765
20864
  // fullscreen→floating switch can close the dialog and leave focus on
20766
- // body. Every other call into this branch already has focus on the
20767
- // input via the combobox-level focusin → this.focus() redirect, so
20768
- // this is a no-op in the common case.
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.
20769
20868
  if (!this.input.componentHasFocus) {
20770
20869
  this.input.focus();
20771
20870
  }
@@ -20884,7 +20983,7 @@ class AuroCombobox extends AuroElement {
20884
20983
  if (this.menu.optionSelected) {
20885
20984
  const selected = this.menu.optionSelected;
20886
20985
 
20887
- if (this.optionSelected !== selected) {
20986
+ if (!this.optionSelected || this.optionSelected !== selected) {
20888
20987
  this.optionSelected = selected;
20889
20988
  }
20890
20989
 
@@ -20897,7 +20996,7 @@ class AuroCombobox extends AuroElement {
20897
20996
  }
20898
20997
 
20899
20998
  // Update display
20900
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
20999
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20901
21000
 
20902
21001
  // Update match word for filtering
20903
21002
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -20915,15 +21014,11 @@ class AuroCombobox extends AuroElement {
20915
21014
 
20916
21015
  // Announce the selection after the dropdown closes so it isn't
20917
21016
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
20918
- // Skip when there's no selected value (e.g. menu.clearSelection() from
20919
- // the unmatched-value path), otherwise VoiceOver reads "undefined".
20920
21017
  const selectedValue = this.menu.value;
20921
- if (selectedValue) {
20922
- const announcementDelay = 300;
20923
- setTimeout(() => {
20924
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
20925
- }, announcementDelay);
20926
- }
21018
+ const announcementDelay = 300;
21019
+ setTimeout(() => {
21020
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21021
+ }, announcementDelay);
20927
21022
  }
20928
21023
 
20929
21024
  // Programmatic value syncs leave availableOptions stale because
@@ -21283,7 +21378,15 @@ class AuroCombobox extends AuroElement {
21283
21378
  this.menu.value = undefined;
21284
21379
  this.validation.reset(this);
21285
21380
  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.
21286
21386
  this.validity = undefined;
21387
+ this.errorMessage = '';
21388
+ this.input.validity = undefined;
21389
+ this.input.errorMessage = '';
21287
21390
  }
21288
21391
 
21289
21392
  /**
@@ -21336,27 +21439,52 @@ class AuroCombobox extends AuroElement {
21336
21439
  this.input.value = this.value;
21337
21440
  }
21338
21441
 
21339
- // Sync menu.value only when an option actually matches, or when filter
21340
- // mode needs setMenuValue to dispatch auroMenu-selectValueFailure (the
21341
- // listener at line 1206 clears combobox.value, mirroring select's
21342
- // pattern). Otherwise sync the input display for freeform values.
21442
+ // Sync menu.value only when an option actually matches; otherwise clear it.
21343
21443
  if (this.menu.options && this.menu.options.length > 0) {
21344
- if (this.menu.options.some((opt) => opt.value === this.value) || this.behavior === 'filter') {
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.
21345
21451
  this.setMenuValue(this.value);
21346
21452
  } else {
21347
- // Clear menu.value AND menu.optionSelected together. Clearing only
21348
- // menu.value leaves the previously-selected option element pinned
21349
- // as menu.optionSelected; a later auroMenu-selectedOption event
21350
- // would then write its stale .value back into combobox.value.
21351
- if (this.menu.value || this.menu.optionSelected) {
21352
- this.menu.clearSelection();
21453
+ if (this.menu.value) {
21454
+ this.menu.value = undefined;
21353
21455
  }
21354
- // Suggestion-mode freeform value: sync the trigger + bib to show it,
21355
- // then refresh the filter once the inputs flush (handleInputValueChange
21356
- // bailed on _syncingDisplayValue).
21357
- const syncPromise = this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21358
- if (syncPromise) {
21359
- syncPromise.then(() => {
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.
21360
21488
  this._programmaticFilterRefresh = true;
21361
21489
  this.handleMenuOptions();
21362
21490
  setTimeout(() => {