@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
@@ -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 = '202606192004';
4876
+ var formkitVersion$2 = '202606192121';
4877
4877
 
4878
4878
  let AuroElement$2 = class AuroElement extends i$3 {
4879
4879
  static get properties() {
@@ -16594,7 +16594,6 @@ class AuroInputUtilities {
16594
16594
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16595
16595
 
16596
16596
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16597
- console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16598
16597
  return undefined;
16599
16598
  }
16600
16599
 
@@ -16746,8 +16745,10 @@ class BaseInput extends AuroElement$1 {
16746
16745
  this.layout = 'classic';
16747
16746
  this.locale = 'en-US';
16748
16747
  this.max = undefined;
16748
+ this._maxObject = undefined;
16749
16749
  this.maxLength = undefined;
16750
16750
  this.min = undefined;
16751
+ this._minObject = undefined;
16751
16752
  this.minLength = undefined;
16752
16753
  this.required = false;
16753
16754
  this.onDark = false;
@@ -16755,6 +16756,7 @@ class BaseInput extends AuroElement$1 {
16755
16756
  this.size = 'lg';
16756
16757
  this.shape = 'classic';
16757
16758
  this.value = undefined;
16759
+ this._valueObject = undefined;
16758
16760
 
16759
16761
  this._initializePrivateDefaults();
16760
16762
  }
@@ -17294,7 +17296,7 @@ class BaseInput extends AuroElement$1 {
17294
17296
  * @returns {Date|undefined}
17295
17297
  */
17296
17298
  get valueObject() {
17297
- return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17299
+ return this._valueObject || this._computeDateObjectFallback(this.value);
17298
17300
  }
17299
17301
 
17300
17302
  /**
@@ -17302,7 +17304,7 @@ class BaseInput extends AuroElement$1 {
17302
17304
  * @returns {Date|undefined}
17303
17305
  */
17304
17306
  get minObject() {
17305
- return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17307
+ return this._minObject || this._computeDateObjectFallback(this.min);
17306
17308
  }
17307
17309
 
17308
17310
  /**
@@ -17310,7 +17312,50 @@ class BaseInput extends AuroElement$1 {
17310
17312
  * @returns {Date|undefined}
17311
17313
  */
17312
17314
  get maxObject() {
17313
- return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
17315
+ return this._maxObject || this._computeDateObjectFallback(this.max);
17316
+ }
17317
+
17318
+ /**
17319
+ * Parses a date string into a Date object when the corresponding `_*Object`
17320
+ * field hasn't been synced yet by `updated()`. Returns undefined when the
17321
+ * input type/format isn't a full date or the string is not a valid date.
17322
+ *
17323
+ * Why this exists: a parent (datepicker) can call `inputN.validate()` from
17324
+ * inside its own `updated()` before this input's `updated()` has run
17325
+ * `syncDateValues()` — so `_valueObject`/`_maxObject` are still `undefined`
17326
+ * and range checks would otherwise silently no-op (flipping the result to
17327
+ * `valid` or `patternMismatch`).
17328
+ * @private
17329
+ * @param {string|undefined} dateStr - ISO date string from `value`/`min`/`max`.
17330
+ * @returns {Date|undefined}
17331
+ */
17332
+ _computeDateObjectFallback(dateStr) {
17333
+ if (!dateStr || !this.util || !this.util.isFullDateFormat(this.type, this.format)) {
17334
+ return undefined;
17335
+ }
17336
+ if (!dateFormatter.isValidDate(dateStr)) {
17337
+ return undefined;
17338
+ }
17339
+ return dateFormatter.stringToDateInstance(dateStr);
17340
+ }
17341
+
17342
+ /**
17343
+ * Internal setter for readonly date object properties.
17344
+ * @private
17345
+ * @param {'valueObject'|'minObject'|'maxObject'} propertyName - Public object property name.
17346
+ * @param {Date|undefined} propertyValue - Value to assign.
17347
+ * @returns {void}
17348
+ */
17349
+ setDateObjectProperty(propertyName, propertyValue) {
17350
+ const internalPropertyName = `_${propertyName}`;
17351
+ const previousValue = this[internalPropertyName];
17352
+
17353
+ if (previousValue === propertyValue) {
17354
+ return;
17355
+ }
17356
+
17357
+ this[internalPropertyName] = propertyValue;
17358
+ this.requestUpdate(propertyName, previousValue);
17314
17359
  }
17315
17360
 
17316
17361
  connectedCallback() {
@@ -17339,6 +17384,7 @@ class BaseInput extends AuroElement$1 {
17339
17384
  format: this.format
17340
17385
  });
17341
17386
  this.configureDataForType();
17387
+ this.syncDateValues();
17342
17388
  }
17343
17389
 
17344
17390
  disconnectedCallback() {
@@ -17358,6 +17404,10 @@ class BaseInput extends AuroElement$1 {
17358
17404
  this.wrapperElement.addEventListener('click', this.handleClick);
17359
17405
  }
17360
17406
 
17407
+ // add attribute for query selectors when auro-input is registered under a custom name
17408
+ if (this.tagName.toLowerCase() !== 'auro-input' && !this.hasAttribute('auro-input')) {
17409
+ this.setAttribute('auro-input', '');
17410
+ }
17361
17411
  this.inputId = this.id ? `${this.id}-input` : window.crypto.randomUUID();
17362
17412
 
17363
17413
  // use validity message override if declared when initializing the component
@@ -17368,6 +17418,7 @@ class BaseInput extends AuroElement$1 {
17368
17418
  this.setCustomHelpTextMessage();
17369
17419
  this.configureAutoFormatting();
17370
17420
  this.configureDataForType();
17421
+ this.syncDateValues();
17371
17422
  }
17372
17423
 
17373
17424
  /**
@@ -17504,6 +17555,8 @@ class BaseInput extends AuroElement$1 {
17504
17555
  this.configureDataForType();
17505
17556
  }
17506
17557
 
17558
+ this.syncDateValues(changedProperties);
17559
+
17507
17560
  if (changedProperties.has('value')) {
17508
17561
  if (this.value && this.value.length > 0) {
17509
17562
  this.hasValue = true;
@@ -17525,14 +17578,14 @@ class BaseInput extends AuroElement$1 {
17525
17578
 
17526
17579
  if (formattedValue !== this.inputElement.value) {
17527
17580
  this.skipNextProgrammaticInputEvent = true;
17528
- if (this.maskInstance && this.type !== 'date') {
17581
+ if (this.maskInstance && this.type === 'credit-card') {
17529
17582
  // Route through the mask so its _value and el.value stay in lock-step
17530
17583
  // (set value calls updateControl which writes el.value = displayValue).
17531
17584
  // Writing el.value directly leaves the mask thinking displayValue is
17532
- // stale; _saveSelection on the next focus/click then warns. Date is
17533
- // excluded because its formattedValue can be raw ISO when the calendar
17534
- // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17535
- // flip validity from patternMismatch to tooShort.
17585
+ // stale; _saveSelection on the next focus/click then warns. Scoped to
17586
+ // credit-card so date's own formattedValue (raw ISO when calendar-invalid)
17587
+ // isn't re-masked through the mm/dd/yyyy mask, which would truncate it
17588
+ // and flip validity from patternMismatch to tooShort.
17536
17589
  this.maskInstance.value = formattedValue || '';
17537
17590
  } else if (formattedValue) {
17538
17591
  this.inputElement.value = formattedValue;
@@ -17574,60 +17627,120 @@ class BaseInput extends AuroElement$1 {
17574
17627
  }));
17575
17628
  }
17576
17629
 
17630
+
17631
+ /**
17632
+ * Synchronizes the ISO string values and Date object representations for date-related properties.
17633
+ * This keeps the model and display values aligned when either side changes.
17634
+ *
17635
+ * When a full date format is in use, this method updates `value`, `min`, and `max` from their corresponding
17636
+ * Date objects or vice versa, based on which properties have changed. It only runs when the current configuration
17637
+ * represents a full year/month/day date format.
17638
+ *
17639
+ * @param {Map<string, unknown>|undefined} [changedProperties=undefined] - Optional map of changed properties used to limit which values are synchronized.
17640
+ * @returns {void}
17641
+ * @private
17642
+ */
17643
+ syncDateValues(changedProperties = undefined) {
17644
+ if (!this.util.isFullDateFormat(this.type, this.format)) {
17645
+ return;
17646
+ }
17647
+
17648
+ this.syncSingleDateValue(changedProperties, 'valueObject', 'value');
17649
+ this.syncSingleDateValue(changedProperties, 'minObject', 'min');
17650
+ this.syncSingleDateValue(changedProperties, 'maxObject', 'max');
17651
+ }
17652
+
17653
+ /**
17654
+ * Synchronizes one date object/string property pair.
17655
+ * @private
17656
+ * @param {Map<string, unknown>|undefined} changedProperties - Map of changed properties from Lit.
17657
+ * @param {string} objectProperty - Date object property name.
17658
+ * @param {string} valueProperty - ISO string property name.
17659
+ * @returns {void}
17660
+ */
17661
+ syncSingleDateValue(changedProperties, objectProperty, valueProperty) {
17662
+ const objectPropertyChanged = !changedProperties || changedProperties.has(objectProperty);
17663
+
17664
+ // objectProperty wins over valueProperty when both changed
17665
+ if (objectPropertyChanged && this[objectProperty]) {
17666
+ this[valueProperty] = dateFormatter.toISOFormatString(this[objectProperty]);
17667
+ return;
17668
+ }
17669
+
17670
+ const valuePropertyChanged = !changedProperties || changedProperties.has(valueProperty);
17671
+ if (!valuePropertyChanged) {
17672
+ return;
17673
+ }
17674
+
17675
+ // 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)
17676
+ if (
17677
+ changedProperties &&
17678
+ valueProperty === 'value' &&
17679
+ changedProperties.get('value') === undefined &&
17680
+ this[objectProperty] instanceof Date &&
17681
+ this[valueProperty] === dateFormatter.toISOFormatString(this[objectProperty])
17682
+ ) {
17683
+ return;
17684
+ }
17685
+
17686
+ if (dateFormatter.isValidDate(this[valueProperty])) {
17687
+ this.setDateObjectProperty(objectProperty, dateFormatter.stringToDateInstance(this[valueProperty]));
17688
+ } else {
17689
+ this.setDateObjectProperty(objectProperty, undefined);
17690
+ }
17691
+ }
17692
+
17577
17693
  /**
17578
17694
  * Sets up IMasks and logic based on auto-formatting requirements.
17579
17695
  * @private
17580
17696
  * @returns {void}
17581
17697
  */
17582
17698
  configureAutoFormatting() {
17583
- // _configuringMask gates two things: external re-entry into this method
17584
- // while setup is mid-flight (from property changes that call back here),
17585
- // and the accept/complete listeners below both need to ignore the mask
17586
- // events fired by our own value-restore step.
17699
+ // Re-entrancy guard. The patched-setter's synthetic input event (suppressed
17700
+ // by _configuringMask above) could otherwise trigger handleInput
17701
+ // processCreditCard configureAutoFormatting before the outer call's
17702
+ // set value has finished its alignCursor pass.
17587
17703
  if (this._configuringMask) return;
17588
17704
  this._configuringMask = true;
17589
17705
  try {
17590
- // Destroy any prior mask so IMask can attach fresh under the new format.
17591
17706
  if (this.maskInstance) {
17592
17707
  this.maskInstance.destroy();
17593
17708
  }
17594
17709
 
17710
+ // Pass new format to util
17595
17711
  this.util.updateFormat(this.format);
17596
17712
 
17597
17713
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17598
17714
 
17599
17715
  if (this.inputElement && maskOptions.mask) {
17600
- // Capture the current display so it can be re-applied after IMask
17601
- // attaches. The restore at the bottom goes through maskInstance.value
17602
- // (not inputElement.value directly) so the mask's internal state and
17603
- // the input's displayed text stay in lock-step.
17604
- let existingValue = this.inputElement.value;
17605
17716
 
17606
- // Format-change case (e.g. locale switch): existingValue is the OLD
17607
- // mask's display string and may not parse under the new mask. When
17608
- // we have a valid date model, rebuild the display from valueObject
17609
- // (the canonical source) using the new mask's format function.
17717
+ // Stash and clear any existing value before IMask init.
17718
+ // IMask's constructor processes the current input value which requires
17719
+ // selection state clearing first avoids that scenario entirely.
17720
+ // When the format changes (e.g. locale switch) and we have a valid ISO
17721
+ // model value, compute the display string for the NEW format instead of
17722
+ // re-using the old display string, which may be invalid in the new mask.
17723
+ let existingValue = this.inputElement.value;
17610
17724
  if (
17611
17725
  this.util.isFullDateFormat(this.type, this.format) &&
17612
17726
  this.value &&
17613
- this.valueObject &&
17727
+ dateFormatter.isValidDate(this.value) &&
17728
+ this.valueObject instanceof Date &&
17729
+ !Number.isNaN(this.valueObject.getTime()) &&
17614
17730
  typeof maskOptions.format === 'function'
17615
17731
  ) {
17616
17732
  existingValue = maskOptions.format(this.valueObject);
17617
17733
  }
17618
17734
 
17619
- // Clear before IMask attaches so the constructor seeds an empty
17620
- // internal value. Otherwise IMask reads the stale unmasked string
17621
- // and emits a spurious 'accept' before the restore below runs.
17622
17735
  this.skipNextProgrammaticInputEvent = true;
17623
17736
  this.inputElement.value = '';
17624
17737
 
17625
17738
  this.maskInstance = IMask(this.inputElement, maskOptions);
17626
17739
 
17627
- // Mask fires 'accept' on every value change, including the restore
17628
- // step below. Skip events fired during configureAutoFormatting so
17629
- // we don't overwrite a value the parent just pushed.
17630
17740
  this.maskInstance.on('accept', () => {
17741
+ // Suppress propagation during configureAutoFormatting's own value-restoration
17742
+ // (line below) — the mask emits 'accept' on every value-set, including ours,
17743
+ // and we don't want to overwrite a value the parent just pushed.
17631
17744
  if (this._configuringMask) return;
17632
17745
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17633
17746
  if (this.type === "date") {
@@ -17635,8 +17748,6 @@ class BaseInput extends AuroElement$1 {
17635
17748
  }
17636
17749
  });
17637
17750
 
17638
- // Mask fires 'complete' on the restore step below for any value that
17639
- // happens to be a complete match. Same setup-suppression as 'accept'.
17640
17751
  this.maskInstance.on('complete', () => {
17641
17752
  if (this._configuringMask) return;
17642
17753
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17645,9 +17756,7 @@ class BaseInput extends AuroElement$1 {
17645
17756
  }
17646
17757
  });
17647
17758
 
17648
- // Write existingValue through the mask (not the input directly) so
17649
- // the mask reformats it under the new rules and keeps its internal
17650
- // _value aligned with the input's displayed text.
17759
+ // Restore the stashed value through IMask so it's properly masked
17651
17760
  if (existingValue) {
17652
17761
  this.maskInstance.value = existingValue;
17653
17762
  }
@@ -17815,6 +17924,7 @@ class BaseInput extends AuroElement$1 {
17815
17924
  */
17816
17925
  reset() {
17817
17926
  this.value = undefined;
17927
+ this.setDateObjectProperty('valueObject', undefined);
17818
17928
  this.validation.reset(this);
17819
17929
  }
17820
17930
 
@@ -17823,6 +17933,7 @@ class BaseInput extends AuroElement$1 {
17823
17933
  */
17824
17934
  clear() {
17825
17935
  this.value = undefined;
17936
+ this.setDateObjectProperty('valueObject', undefined);
17826
17937
  }
17827
17938
 
17828
17939
  /**
@@ -18351,7 +18462,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18351
18462
  }
18352
18463
  };
18353
18464
 
18354
- var formkitVersion$1 = '202606192004';
18465
+ var formkitVersion$1 = '202606192121';
18355
18466
 
18356
18467
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18357
18468
  // See LICENSE in the project root for license information.
@@ -19472,7 +19583,7 @@ class AuroBibtemplate extends i$3 {
19472
19583
  }
19473
19584
  }
19474
19585
 
19475
- var formkitVersion = '202606192004';
19586
+ var formkitVersion = '202606192121';
19476
19587
 
19477
19588
  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}`;
19478
19589
 
@@ -20459,7 +20570,7 @@ class AuroCombobox extends AuroElement {
20459
20570
  if (this.menu) {
20460
20571
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20461
20572
  }
20462
- const label = getOptionLabel(this.menu.optionSelected);
20573
+ const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20463
20574
  this.updateTriggerTextDisplay(label || this.value);
20464
20575
  }
20465
20576
 
@@ -20473,9 +20584,40 @@ class AuroCombobox extends AuroElement {
20473
20584
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20474
20585
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20475
20586
 
20476
- const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20477
- if (!suppressed) {
20478
- this.syncInputValuesAcrossTriggerAndBib(label || this.value);
20587
+ if (!this.persistInput && !(this.behavior === 'suggestion' && isInputFocusedWithNoSelection)) {
20588
+ const nextValue = label || this.value;
20589
+ // Only set the flag when there's an actual write to suppress —
20590
+ // syncValuesAndStates re-enters here during typing when both inputs
20591
+ // already match, and a no-op flag flip would make the bib branch's
20592
+ // bail eat legitimate user-input events.
20593
+ const triggerNeedsSync = this.input.value !== nextValue;
20594
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20595
+ if (triggerNeedsSync || bibNeedsSync) {
20596
+ this._syncingDisplayValue = true;
20597
+ if (triggerNeedsSync) {
20598
+ this.input.value = nextValue;
20599
+ }
20600
+ if (bibNeedsSync) {
20601
+ this.inputInBib.value = nextValue;
20602
+ }
20603
+ const pending = [];
20604
+ if (triggerNeedsSync) {
20605
+ pending.push(this.input.updateComplete);
20606
+ }
20607
+ if (bibNeedsSync) {
20608
+ pending.push(this.inputInBib.updateComplete);
20609
+ }
20610
+ Promise.all(pending).then(() => {
20611
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20612
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20613
+ this.input.maskInstance.updateValue();
20614
+ }
20615
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20616
+ this.inputInBib.maskInstance.updateValue();
20617
+ }
20618
+ this._syncingDisplayValue = false;
20619
+ });
20620
+ }
20479
20621
  }
20480
20622
 
20481
20623
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20503,52 +20645,6 @@ class AuroCombobox extends AuroElement {
20503
20645
  this.requestUpdate();
20504
20646
  }
20505
20647
 
20506
- /**
20507
- * Writes nextValue to the trigger input and the bib input when their current
20508
- * value differs, then re-asserts imask after Lit's update flushes.
20509
- * @param {string} nextValue - The value to write to both inputs.
20510
- * @returns {Promise<void> | null} Promise that resolves after the inputs flush,
20511
- * or null when no sync was needed (so callers can skip post-flush work).
20512
- * @private
20513
- */
20514
- syncInputValuesAcrossTriggerAndBib(nextValue) {
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
- return null;
20523
- }
20524
-
20525
- this._syncingDisplayValue = true;
20526
- if (triggerNeedsSync) {
20527
- this.input.value = nextValue;
20528
- }
20529
- if (bibNeedsSync) {
20530
- this.inputInBib.value = nextValue;
20531
- }
20532
-
20533
- const pending = [];
20534
- if (triggerNeedsSync) {
20535
- pending.push(this.input.updateComplete);
20536
- }
20537
- if (bibNeedsSync) {
20538
- pending.push(this.inputInBib.updateComplete);
20539
- }
20540
- return Promise.all(pending).then(() => {
20541
- // imask reasserts otherwise, and handleBlur reverts to its stale value.
20542
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20543
- this.input.maskInstance.updateValue();
20544
- }
20545
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20546
- this.inputInBib.maskInstance.updateValue();
20547
- }
20548
- this._syncingDisplayValue = false;
20549
- });
20550
- }
20551
-
20552
20648
  /**
20553
20649
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20554
20650
  * @private
@@ -20576,9 +20672,9 @@ class AuroCombobox extends AuroElement {
20576
20672
  this.syncValuesAndStates();
20577
20673
  }
20578
20674
 
20579
- // Re-activate when optionActive is not in the current scrollable viewport,
20580
- // or when _index has been reset (e.g. by clearSelection in an async update)
20581
- // so that makeSelection() can find the correct option.
20675
+ // Re-activate when optionActive is no longer visible, or when _index has
20676
+ // been reset (e.g. by clearSelection in an async update) so that
20677
+ // makeSelection() can find the correct option.
20582
20678
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20583
20679
  this.activateFirstEnabledAvailableOption();
20584
20680
  }
@@ -20712,25 +20808,28 @@ class AuroCombobox extends AuroElement {
20712
20808
 
20713
20809
  guardTouchPassthrough(this.menu);
20714
20810
 
20715
- // showModal() takes focus away from the trigger. Early focus once
20716
- // the dialog has painted; the shared doubleRaf below is the fallback
20717
- // for when the dialog needs an extra frame and also clears the
20718
- // validation guard.
20811
+ // The dialog's showModal() steals focus from the trigger.
20812
+ // A single rAF is enough for the dialog DOM to be painted and
20813
+ // focusable, then doubleRaf finalizes the transition.
20719
20814
  requestAnimationFrame(() => {
20720
20815
  this.setInputFocus();
20721
20816
  });
20722
- }
20723
- // else (desktop popover-open): Chrome resets the trigger caret to
20724
- // [0, 0] when its floating <label for="…"> overlay receives focus.
20725
- // The shared doubleRaf below parks the caret back at end-of-text
20726
- // after the dropdown layout settles.
20727
-
20728
- doubleRaf(() => {
20729
- this.setInputFocus();
20730
- if (this._inFullscreenTransition) {
20817
+
20818
+ doubleRaf(() => {
20819
+ this.setInputFocus();
20731
20820
  this._inFullscreenTransition = false;
20732
- }
20733
- });
20821
+ });
20822
+ } else {
20823
+ // Desktop popover-open: restore the trigger caret to end-of-text.
20824
+ // Clicking the trigger lands on auro-input's floating <label for="…">
20825
+ // overlay; Chrome resets the native input's selection to [0, 0] on
20826
+ // label-focus before any JS runs. setInputFocus()'s non-fullscreen
20827
+ // branch parks the caret at end. doubleRaf lets the dropdown layout
20828
+ // settle first, matching the fullscreen branch timing.
20829
+ doubleRaf(() => {
20830
+ this.setInputFocus();
20831
+ });
20832
+ }
20734
20833
  }
20735
20834
  });
20736
20835
 
@@ -20837,9 +20936,9 @@ class AuroCombobox extends AuroElement {
20837
20936
  } else {
20838
20937
  // Safety net for the strategy-change setTimeout: a viewport-crossing
20839
20938
  // fullscreen→floating switch can close the dialog and leave focus on
20840
- // body. Every other call into this branch already has focus on the
20841
- // input via the combobox-level focusin → this.focus() redirect, so
20842
- // this is a no-op in the common case.
20939
+ // body. Every other Branch 2 callsite already has focus on the input
20940
+ // via the focusin → this.focus() redirect (L1448-1452), so this is a
20941
+ // no-op in the common case.
20843
20942
  if (!this.input.componentHasFocus) {
20844
20943
  this.input.focus();
20845
20944
  }
@@ -20958,7 +21057,7 @@ class AuroCombobox extends AuroElement {
20958
21057
  if (this.menu.optionSelected) {
20959
21058
  const selected = this.menu.optionSelected;
20960
21059
 
20961
- if (this.optionSelected !== selected) {
21060
+ if (!this.optionSelected || this.optionSelected !== selected) {
20962
21061
  this.optionSelected = selected;
20963
21062
  }
20964
21063
 
@@ -20971,7 +21070,7 @@ class AuroCombobox extends AuroElement {
20971
21070
  }
20972
21071
 
20973
21072
  // Update display
20974
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
21073
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20975
21074
 
20976
21075
  // Update match word for filtering
20977
21076
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -20989,15 +21088,11 @@ class AuroCombobox extends AuroElement {
20989
21088
 
20990
21089
  // Announce the selection after the dropdown closes so it isn't
20991
21090
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
20992
- // Skip when there's no selected value (e.g. menu.clearSelection() from
20993
- // the unmatched-value path), otherwise VoiceOver reads "undefined".
20994
21091
  const selectedValue = this.menu.value;
20995
- if (selectedValue) {
20996
- const announcementDelay = 300;
20997
- setTimeout(() => {
20998
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
20999
- }, announcementDelay);
21000
- }
21092
+ const announcementDelay = 300;
21093
+ setTimeout(() => {
21094
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21095
+ }, announcementDelay);
21001
21096
  }
21002
21097
 
21003
21098
  // Programmatic value syncs leave availableOptions stale because
@@ -21357,7 +21452,15 @@ class AuroCombobox extends AuroElement {
21357
21452
  this.menu.value = undefined;
21358
21453
  this.validation.reset(this);
21359
21454
  this.touched = false;
21455
+ // Force validity back to the cleared state. validation.reset() calls
21456
+ // validate() at the end, and (post-credit-card-fix) the trigger input's
21457
+ // residual validity may still be copied into the combobox by the
21458
+ // auroInputElements loop. Explicitly clear here so reset() actually
21459
+ // leaves the component in a "no validity" state.
21360
21460
  this.validity = undefined;
21461
+ this.errorMessage = '';
21462
+ this.input.validity = undefined;
21463
+ this.input.errorMessage = '';
21361
21464
  }
21362
21465
 
21363
21466
  /**
@@ -21410,27 +21513,52 @@ class AuroCombobox extends AuroElement {
21410
21513
  this.input.value = this.value;
21411
21514
  }
21412
21515
 
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.
21516
+ // Sync menu.value only when an option actually matches; otherwise clear it.
21417
21517
  if (this.menu.options && this.menu.options.length > 0) {
21418
- if (this.menu.options.some((opt) => opt.value === this.value) || this.behavior === 'filter') {
21518
+ if (this.menu.options.some((opt) => opt.value === this.value)) {
21519
+ this.setMenuValue(this.value);
21520
+ } else if (this.behavior === 'filter') {
21521
+ // In filter mode, freeform values aren't allowed. Push the unmatched
21522
+ // value through setMenuValue so menu dispatches auroMenu-selectValueFailure
21523
+ // and the listener at line 1206 clears combobox.value (mirrors select's
21524
+ // pattern). Bypassing setMenuValue here would skip the failure cascade.
21419
21525
  this.setMenuValue(this.value);
21420
21526
  } 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();
21527
+ if (this.menu.value) {
21528
+ this.menu.value = undefined;
21427
21529
  }
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(() => {
21530
+ // Sync the display for programmatic freeform value changes (e.g. the
21531
+ // swap-values pattern). Guard with _syncingDisplayValue so that the
21532
+ // re-entrant input event from base-input.notifyValueChanged() is
21533
+ // ignored by handleInputValueChange. Skip the sync when input already
21534
+ // matches to avoid spurious events during the normal typing path.
21535
+ const nextValue = this.value || '';
21536
+ const triggerNeedsSync = this.input.value !== nextValue;
21537
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
21538
+ if (triggerNeedsSync || bibNeedsSync) {
21539
+ this._syncingDisplayValue = true;
21540
+ if (triggerNeedsSync) {
21541
+ this.input.value = nextValue;
21542
+ }
21543
+ if (bibNeedsSync) {
21544
+ this.inputInBib.value = nextValue;
21545
+ }
21546
+ const pending = [];
21547
+ if (triggerNeedsSync) {
21548
+ pending.push(this.input.updateComplete);
21549
+ }
21550
+ if (bibNeedsSync) {
21551
+ pending.push(this.inputInBib.updateComplete);
21552
+ }
21553
+ Promise.all(pending).then(() => {
21554
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
21555
+ this.input.maskInstance.updateValue();
21556
+ }
21557
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
21558
+ this.inputInBib.maskInstance.updateValue();
21559
+ }
21560
+ this._syncingDisplayValue = false;
21561
+ // handleInputValueChange bailed on the flag — refresh the filter.
21434
21562
  this._programmaticFilterRefresh = true;
21435
21563
  this.handleMenuOptions();
21436
21564
  setTimeout(() => {
@@ -22036,8 +22164,8 @@ class AuroMenu extends AuroElement {
22036
22164
  // Malformed JSON (e.g. a literal string that happens to start with "[") falls back
22037
22165
  // to a single-item array rather than throwing during render.
22038
22166
  try {
22039
- const parsed = JSON.parse(this.value);
22040
- return Array.isArray(parsed) ? parsed : [this.value];
22167
+ // any valid JSON starting with `[` ALWAYS parses to an array
22168
+ return JSON.parse(this.value);
22041
22169
  } catch {
22042
22170
  return [this.value];
22043
22171
  }