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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/components/checkbox/demo/customize.html +1 -2
  2. package/components/checkbox/demo/customize.min.js +1 -1
  3. package/components/checkbox/demo/getting-started.min.js +1 -1
  4. package/components/checkbox/demo/index.min.js +1 -1
  5. package/components/checkbox/dist/index.js +1 -1
  6. package/components/checkbox/dist/registered.js +1 -1
  7. package/components/combobox/demo/customize.html +1 -2
  8. package/components/combobox/demo/customize.md +108 -132
  9. package/components/combobox/demo/customize.min.js +150 -293
  10. package/components/combobox/demo/getting-started.min.js +150 -293
  11. package/components/combobox/demo/index.min.js +150 -293
  12. package/components/combobox/dist/auro-combobox.d.ts +9 -0
  13. package/components/combobox/dist/index.js +150 -293
  14. package/components/combobox/dist/registered.js +150 -293
  15. package/components/counter/demo/customize.min.js +10 -2
  16. package/components/counter/demo/index.min.js +10 -2
  17. package/components/counter/dist/index.js +10 -2
  18. package/components/counter/dist/registered.js +10 -2
  19. package/components/datepicker/demo/api.md +2 -0
  20. package/components/datepicker/demo/customize.js +0 -2
  21. package/components/datepicker/demo/customize.md +138 -38
  22. package/components/datepicker/demo/customize.min.js +59 -168
  23. package/components/datepicker/demo/index.min.js +59 -150
  24. package/components/datepicker/dist/index.js +59 -150
  25. package/components/datepicker/dist/registered.js +59 -150
  26. package/components/datepicker/dist/src/auro-datepicker.d.ts +2 -0
  27. package/components/dropdown/demo/customize.min.js +9 -1
  28. package/components/dropdown/demo/getting-started.min.js +9 -1
  29. package/components/dropdown/demo/index.min.js +9 -1
  30. package/components/dropdown/dist/auro-dropdown.d.ts +1 -0
  31. package/components/dropdown/dist/index.js +9 -1
  32. package/components/dropdown/dist/registered.js +9 -1
  33. package/components/form/demo/customize.html +6 -6
  34. package/components/form/demo/customize.js +19 -0
  35. package/components/form/demo/customize.md +203 -51
  36. package/components/form/demo/customize.min.js +707 -633
  37. package/components/form/demo/getting-started.min.js +635 -633
  38. package/components/form/demo/index.min.js +635 -633
  39. package/components/form/demo/registerDemoDeps.min.js +272 -596
  40. package/components/form/dist/auro-form.d.ts +122 -4
  41. package/components/form/dist/index.js +363 -37
  42. package/components/form/dist/registered.js +363 -37
  43. package/components/input/demo/customize.html +1 -2
  44. package/components/input/demo/customize.md +54 -53
  45. package/components/input/demo/customize.min.js +41 -147
  46. package/components/input/demo/getting-started.min.js +41 -147
  47. package/components/input/demo/index.min.js +41 -147
  48. package/components/input/dist/base-input.d.ts +1 -49
  49. package/components/input/dist/index.js +41 -147
  50. package/components/input/dist/registered.js +41 -147
  51. package/components/radio/demo/customize.min.js +1 -1
  52. package/components/radio/demo/getting-started.min.js +1 -1
  53. package/components/radio/demo/index.min.js +1 -1
  54. package/components/radio/dist/index.js +1 -1
  55. package/components/radio/dist/registered.js +1 -1
  56. package/components/select/demo/customize.html +1 -2
  57. package/components/select/demo/customize.min.js +10 -2
  58. package/components/select/demo/getting-started.min.js +10 -2
  59. package/components/select/demo/index.min.js +10 -2
  60. package/components/select/dist/index.js +10 -2
  61. package/components/select/dist/registered.js +10 -2
  62. package/custom-elements.json +1592 -1707
  63. package/package.json +1 -1
@@ -825,6 +825,8 @@ function navigateArrow(component, direction, options = {}) {
825
825
  }
826
826
  }
827
827
 
828
+ /* eslint-disable no-underscore-dangle */
829
+
828
830
  /**
829
831
  * Returns the clear button element from the active input's shadow
830
832
  * DOM, if available.
@@ -860,11 +862,9 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
860
862
  * @param {Object} menu - The menu component.
861
863
  */
862
864
  function reconcileMenuIndex(menu) {
863
- // eslint-disable-next-line no-underscore-dangle
864
865
  if (menu._index < 0 && menu.optionActive && menu.items) {
865
866
  const idx = menu.items.indexOf(menu.optionActive);
866
867
  if (idx >= 0) {
867
- // eslint-disable-next-line no-underscore-dangle
868
868
  menu._index = idx;
869
869
  }
870
870
  }
@@ -4888,7 +4888,7 @@ let AuroHelpText$2 = class AuroHelpText extends i$3 {
4888
4888
  }
4889
4889
  };
4890
4890
 
4891
- var formkitVersion$2 = '202606192121';
4891
+ var formkitVersion$2 = '202606231948';
4892
4892
 
4893
4893
  let AuroElement$2 = class AuroElement extends i$3 {
4894
4894
  static get properties() {
@@ -5839,6 +5839,14 @@ class AuroDropdown extends AuroElement$2 {
5839
5839
  };
5840
5840
  this.addEventListener('keydown', this._bibTabHandler);
5841
5841
 
5842
+ // Suppress AuroFloatingUI's auto-hide-on-focus-loss while the
5843
+ // desktopModal trap owns focus management. Without this, the very
5844
+ // first focus move into the bib (from the RAF below) triggers
5845
+ // handleFocusLoss → hideBib, tearing down the trap before the
5846
+ // user can press Tab.
5847
+ this._priorNoHideOnFocusLoss = this.noHideOnThisFocusLoss;
5848
+ this.noHideOnThisFocusLoss = true;
5849
+
5842
5850
  // Move initial focus into the bib content, matching FocusTrap behavior
5843
5851
  requestAnimationFrame(() => {
5844
5852
  const focusables = getFocusableElements(this.bibContent);
@@ -16609,6 +16617,7 @@ class AuroInputUtilities {
16609
16617
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16610
16618
 
16611
16619
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16620
+ console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16612
16621
  return undefined;
16613
16622
  }
16614
16623
 
@@ -16760,10 +16769,8 @@ class BaseInput extends AuroElement$1 {
16760
16769
  this.layout = 'classic';
16761
16770
  this.locale = 'en-US';
16762
16771
  this.max = undefined;
16763
- this._maxObject = undefined;
16764
16772
  this.maxLength = undefined;
16765
16773
  this.min = undefined;
16766
- this._minObject = undefined;
16767
16774
  this.minLength = undefined;
16768
16775
  this.required = false;
16769
16776
  this.onDark = false;
@@ -16771,7 +16778,6 @@ class BaseInput extends AuroElement$1 {
16771
16778
  this.size = 'lg';
16772
16779
  this.shape = 'classic';
16773
16780
  this.value = undefined;
16774
- this._valueObject = undefined;
16775
16781
 
16776
16782
  this._initializePrivateDefaults();
16777
16783
  }
@@ -17311,7 +17317,7 @@ class BaseInput extends AuroElement$1 {
17311
17317
  * @returns {Date|undefined}
17312
17318
  */
17313
17319
  get valueObject() {
17314
- return this._valueObject || this._computeDateObjectFallback(this.value);
17320
+ return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17315
17321
  }
17316
17322
 
17317
17323
  /**
@@ -17319,7 +17325,7 @@ class BaseInput extends AuroElement$1 {
17319
17325
  * @returns {Date|undefined}
17320
17326
  */
17321
17327
  get minObject() {
17322
- return this._minObject || this._computeDateObjectFallback(this.min);
17328
+ return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17323
17329
  }
17324
17330
 
17325
17331
  /**
@@ -17327,50 +17333,7 @@ class BaseInput extends AuroElement$1 {
17327
17333
  * @returns {Date|undefined}
17328
17334
  */
17329
17335
  get maxObject() {
17330
- return this._maxObject || this._computeDateObjectFallback(this.max);
17331
- }
17332
-
17333
- /**
17334
- * Parses a date string into a Date object when the corresponding `_*Object`
17335
- * field hasn't been synced yet by `updated()`. Returns undefined when the
17336
- * input type/format isn't a full date or the string is not a valid date.
17337
- *
17338
- * Why this exists: a parent (datepicker) can call `inputN.validate()` from
17339
- * inside its own `updated()` before this input's `updated()` has run
17340
- * `syncDateValues()` — so `_valueObject`/`_maxObject` are still `undefined`
17341
- * and range checks would otherwise silently no-op (flipping the result to
17342
- * `valid` or `patternMismatch`).
17343
- * @private
17344
- * @param {string|undefined} dateStr - ISO date string from `value`/`min`/`max`.
17345
- * @returns {Date|undefined}
17346
- */
17347
- _computeDateObjectFallback(dateStr) {
17348
- if (!dateStr || !this.util || !this.util.isFullDateFormat(this.type, this.format)) {
17349
- return undefined;
17350
- }
17351
- if (!dateFormatter.isValidDate(dateStr)) {
17352
- return undefined;
17353
- }
17354
- return dateFormatter.stringToDateInstance(dateStr);
17355
- }
17356
-
17357
- /**
17358
- * Internal setter for readonly date object properties.
17359
- * @private
17360
- * @param {'valueObject'|'minObject'|'maxObject'} propertyName - Public object property name.
17361
- * @param {Date|undefined} propertyValue - Value to assign.
17362
- * @returns {void}
17363
- */
17364
- setDateObjectProperty(propertyName, propertyValue) {
17365
- const internalPropertyName = `_${propertyName}`;
17366
- const previousValue = this[internalPropertyName];
17367
-
17368
- if (previousValue === propertyValue) {
17369
- return;
17370
- }
17371
-
17372
- this[internalPropertyName] = propertyValue;
17373
- this.requestUpdate(propertyName, previousValue);
17336
+ return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
17374
17337
  }
17375
17338
 
17376
17339
  connectedCallback() {
@@ -17399,7 +17362,6 @@ class BaseInput extends AuroElement$1 {
17399
17362
  format: this.format
17400
17363
  });
17401
17364
  this.configureDataForType();
17402
- this.syncDateValues();
17403
17365
  }
17404
17366
 
17405
17367
  disconnectedCallback() {
@@ -17419,10 +17381,6 @@ class BaseInput extends AuroElement$1 {
17419
17381
  this.wrapperElement.addEventListener('click', this.handleClick);
17420
17382
  }
17421
17383
 
17422
- // add attribute for query selectors when auro-input is registered under a custom name
17423
- if (this.tagName.toLowerCase() !== 'auro-input' && !this.hasAttribute('auro-input')) {
17424
- this.setAttribute('auro-input', '');
17425
- }
17426
17384
  this.inputId = this.id ? `${this.id}-input` : window.crypto.randomUUID();
17427
17385
 
17428
17386
  // use validity message override if declared when initializing the component
@@ -17433,7 +17391,6 @@ class BaseInput extends AuroElement$1 {
17433
17391
  this.setCustomHelpTextMessage();
17434
17392
  this.configureAutoFormatting();
17435
17393
  this.configureDataForType();
17436
- this.syncDateValues();
17437
17394
  }
17438
17395
 
17439
17396
  /**
@@ -17570,8 +17527,6 @@ class BaseInput extends AuroElement$1 {
17570
17527
  this.configureDataForType();
17571
17528
  }
17572
17529
 
17573
- this.syncDateValues(changedProperties);
17574
-
17575
17530
  if (changedProperties.has('value')) {
17576
17531
  if (this.value && this.value.length > 0) {
17577
17532
  this.hasValue = true;
@@ -17593,14 +17548,14 @@ class BaseInput extends AuroElement$1 {
17593
17548
 
17594
17549
  if (formattedValue !== this.inputElement.value) {
17595
17550
  this.skipNextProgrammaticInputEvent = true;
17596
- if (this.maskInstance && this.type === 'credit-card') {
17551
+ if (this.maskInstance && this.type !== 'date') {
17597
17552
  // Route through the mask so its _value and el.value stay in lock-step
17598
17553
  // (set value calls updateControl which writes el.value = displayValue).
17599
17554
  // Writing el.value directly leaves the mask thinking displayValue is
17600
- // stale; _saveSelection on the next focus/click then warns. Scoped to
17601
- // credit-card so date's own formattedValue (raw ISO when calendar-invalid)
17602
- // isn't re-masked through the mm/dd/yyyy mask, which would truncate it
17603
- // and flip validity from patternMismatch to tooShort.
17555
+ // stale; _saveSelection on the next focus/click then warns. Date is
17556
+ // excluded because its formattedValue can be raw ISO when the calendar
17557
+ // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17558
+ // flip validity from patternMismatch to tooShort.
17604
17559
  this.maskInstance.value = formattedValue || '';
17605
17560
  } else if (formattedValue) {
17606
17561
  this.inputElement.value = formattedValue;
@@ -17642,120 +17597,65 @@ class BaseInput extends AuroElement$1 {
17642
17597
  }));
17643
17598
  }
17644
17599
 
17645
-
17646
- /**
17647
- * Synchronizes the ISO string values and Date object representations for date-related properties.
17648
- * This keeps the model and display values aligned when either side changes.
17649
- *
17650
- * When a full date format is in use, this method updates `value`, `min`, and `max` from their corresponding
17651
- * Date objects or vice versa, based on which properties have changed. It only runs when the current configuration
17652
- * represents a full year/month/day date format.
17653
- *
17654
- * @param {Map<string, unknown>|undefined} [changedProperties=undefined] - Optional map of changed properties used to limit which values are synchronized.
17655
- * @returns {void}
17656
- * @private
17657
- */
17658
- syncDateValues(changedProperties = undefined) {
17659
- if (!this.util.isFullDateFormat(this.type, this.format)) {
17660
- return;
17661
- }
17662
-
17663
- this.syncSingleDateValue(changedProperties, 'valueObject', 'value');
17664
- this.syncSingleDateValue(changedProperties, 'minObject', 'min');
17665
- this.syncSingleDateValue(changedProperties, 'maxObject', 'max');
17666
- }
17667
-
17668
- /**
17669
- * Synchronizes one date object/string property pair.
17670
- * @private
17671
- * @param {Map<string, unknown>|undefined} changedProperties - Map of changed properties from Lit.
17672
- * @param {string} objectProperty - Date object property name.
17673
- * @param {string} valueProperty - ISO string property name.
17674
- * @returns {void}
17675
- */
17676
- syncSingleDateValue(changedProperties, objectProperty, valueProperty) {
17677
- const objectPropertyChanged = !changedProperties || changedProperties.has(objectProperty);
17678
-
17679
- // objectProperty wins over valueProperty when both changed
17680
- if (objectPropertyChanged && this[objectProperty]) {
17681
- this[valueProperty] = dateFormatter.toISOFormatString(this[objectProperty]);
17682
- return;
17683
- }
17684
-
17685
- const valuePropertyChanged = !changedProperties || changedProperties.has(valueProperty);
17686
- if (!valuePropertyChanged) {
17687
- return;
17688
- }
17689
-
17690
- // 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)
17691
- if (
17692
- changedProperties &&
17693
- valueProperty === 'value' &&
17694
- changedProperties.get('value') === undefined &&
17695
- this[objectProperty] instanceof Date &&
17696
- this[valueProperty] === dateFormatter.toISOFormatString(this[objectProperty])
17697
- ) {
17698
- return;
17699
- }
17700
-
17701
- if (dateFormatter.isValidDate(this[valueProperty])) {
17702
- this.setDateObjectProperty(objectProperty, dateFormatter.stringToDateInstance(this[valueProperty]));
17703
- } else {
17704
- this.setDateObjectProperty(objectProperty, undefined);
17705
- }
17706
- }
17707
-
17708
17600
  /**
17709
17601
  * Sets up IMasks and logic based on auto-formatting requirements.
17710
17602
  * @private
17711
17603
  * @returns {void}
17712
17604
  */
17713
17605
  configureAutoFormatting() {
17714
- // Re-entrancy guard. The patched-setter's synthetic input event (suppressed
17715
- // by _configuringMask above) could otherwise trigger handleInput
17716
- // processCreditCard configureAutoFormatting before the outer call's
17717
- // set value has finished its alignCursor pass.
17606
+ // _configuringMask gates two things: external re-entry into this method
17607
+ // while setup is mid-flight (from property changes that call back here),
17608
+ // and the accept/complete listeners below — both need to ignore the mask
17609
+ // events fired by our own value-restore step.
17718
17610
  if (this._configuringMask) return;
17719
17611
  this._configuringMask = true;
17720
17612
  try {
17613
+ // Destroy any prior mask so IMask can attach fresh under the new format.
17614
+ // Null the reference too — if the new maskOptions.mask is falsy (e.g.
17615
+ // type switched from credit-card to text) the IMask() reassignment
17616
+ // below is skipped, and downstream writes at line ~823 would otherwise
17617
+ // route through the destroyed instance.
17721
17618
  if (this.maskInstance) {
17722
17619
  this.maskInstance.destroy();
17620
+ this.maskInstance = null;
17723
17621
  }
17724
17622
 
17725
- // Pass new format to util
17726
17623
  this.util.updateFormat(this.format);
17727
17624
 
17728
17625
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17729
17626
 
17730
17627
  if (this.inputElement && maskOptions.mask) {
17731
-
17732
- // Stash and clear any existing value before IMask init.
17733
- // IMask's constructor processes the current input value which requires
17734
- // selection state clearing first avoids that scenario entirely.
17735
- // When the format changes (e.g. locale switch) and we have a valid ISO
17736
- // model value, compute the display string for the NEW format instead of
17737
- // re-using the old display string, which may be invalid in the new mask.
17628
+ // Capture the current display so it can be re-applied after IMask
17629
+ // attaches. The restore at the bottom goes through maskInstance.value
17630
+ // (not inputElement.value directly) so the mask's internal state and
17631
+ // the input's displayed text stay in lock-step.
17738
17632
  let existingValue = this.inputElement.value;
17633
+
17634
+ // Format-change case (e.g. locale switch): existingValue is the OLD
17635
+ // mask's display string and may not parse under the new mask. When
17636
+ // we have a valid date model, rebuild the display from valueObject
17637
+ // (the canonical source) using the new mask's format function.
17739
17638
  if (
17740
17639
  this.util.isFullDateFormat(this.type, this.format) &&
17741
17640
  this.value &&
17742
- dateFormatter.isValidDate(this.value) &&
17743
- this.valueObject instanceof Date &&
17744
- !Number.isNaN(this.valueObject.getTime()) &&
17641
+ this.valueObject &&
17745
17642
  typeof maskOptions.format === 'function'
17746
17643
  ) {
17747
17644
  existingValue = maskOptions.format(this.valueObject);
17748
17645
  }
17749
17646
 
17647
+ // Clear before IMask attaches so the constructor seeds an empty
17648
+ // internal value. Otherwise IMask reads the stale unmasked string
17649
+ // and emits a spurious 'accept' before the restore below runs.
17750
17650
  this.skipNextProgrammaticInputEvent = true;
17751
17651
  this.inputElement.value = '';
17752
17652
 
17753
17653
  this.maskInstance = IMask(this.inputElement, maskOptions);
17754
17654
 
17655
+ // Mask fires 'accept' on every value change, including the restore
17656
+ // step below. Skip events fired during configureAutoFormatting so
17657
+ // we don't overwrite a value the parent just pushed.
17755
17658
  this.maskInstance.on('accept', () => {
17756
- // Suppress propagation during configureAutoFormatting's own value-restoration
17757
- // (line below) — the mask emits 'accept' on every value-set, including ours,
17758
- // and we don't want to overwrite a value the parent just pushed.
17759
17659
  if (this._configuringMask) return;
17760
17660
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17761
17661
  if (this.type === "date") {
@@ -17763,6 +17663,8 @@ class BaseInput extends AuroElement$1 {
17763
17663
  }
17764
17664
  });
17765
17665
 
17666
+ // Mask fires 'complete' on the restore step below for any value that
17667
+ // happens to be a complete match. Same setup-suppression as 'accept'.
17766
17668
  this.maskInstance.on('complete', () => {
17767
17669
  if (this._configuringMask) return;
17768
17670
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17771,7 +17673,9 @@ class BaseInput extends AuroElement$1 {
17771
17673
  }
17772
17674
  });
17773
17675
 
17774
- // Restore the stashed value through IMask so it's properly masked
17676
+ // Write existingValue through the mask (not the input directly) so
17677
+ // the mask reformats it under the new rules and keeps its internal
17678
+ // _value aligned with the input's displayed text.
17775
17679
  if (existingValue) {
17776
17680
  this.maskInstance.value = existingValue;
17777
17681
  }
@@ -17939,7 +17843,6 @@ class BaseInput extends AuroElement$1 {
17939
17843
  */
17940
17844
  reset() {
17941
17845
  this.value = undefined;
17942
- this.setDateObjectProperty('valueObject', undefined);
17943
17846
  this.validation.reset(this);
17944
17847
  }
17945
17848
 
@@ -17948,7 +17851,6 @@ class BaseInput extends AuroElement$1 {
17948
17851
  */
17949
17852
  clear() {
17950
17853
  this.value = undefined;
17951
- this.setDateObjectProperty('valueObject', undefined);
17952
17854
  }
17953
17855
 
17954
17856
  /**
@@ -18477,7 +18379,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18477
18379
  }
18478
18380
  };
18479
18381
 
18480
- var formkitVersion$1 = '202606192121';
18382
+ var formkitVersion$1 = '202606231948';
18481
18383
 
18482
18384
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18483
18385
  // See LICENSE in the project root for license information.
@@ -19598,7 +19500,7 @@ class AuroBibtemplate extends i$3 {
19598
19500
  }
19599
19501
  }
19600
19502
 
19601
- var formkitVersion = '202606192121';
19503
+ var formkitVersion = '202606231948';
19602
19504
 
19603
19505
  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}`;
19604
19506
 
@@ -20585,7 +20487,7 @@ class AuroCombobox extends AuroElement {
20585
20487
  if (this.menu) {
20586
20488
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20587
20489
  }
20588
- const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20490
+ const label = getOptionLabel(this.menu.optionSelected);
20589
20491
  this.updateTriggerTextDisplay(label || this.value);
20590
20492
  }
20591
20493
 
@@ -20599,40 +20501,9 @@ class AuroCombobox extends AuroElement {
20599
20501
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20600
20502
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20601
20503
 
20602
- if (!this.persistInput && !(this.behavior === 'suggestion' && isInputFocusedWithNoSelection)) {
20603
- const nextValue = label || this.value;
20604
- // Only set the flag when there's an actual write to suppress —
20605
- // syncValuesAndStates re-enters here during typing when both inputs
20606
- // already match, and a no-op flag flip would make the bib branch's
20607
- // bail eat legitimate user-input events.
20608
- const triggerNeedsSync = this.input.value !== nextValue;
20609
- const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20610
- if (triggerNeedsSync || bibNeedsSync) {
20611
- this._syncingDisplayValue = true;
20612
- if (triggerNeedsSync) {
20613
- this.input.value = nextValue;
20614
- }
20615
- if (bibNeedsSync) {
20616
- this.inputInBib.value = nextValue;
20617
- }
20618
- const pending = [];
20619
- if (triggerNeedsSync) {
20620
- pending.push(this.input.updateComplete);
20621
- }
20622
- if (bibNeedsSync) {
20623
- pending.push(this.inputInBib.updateComplete);
20624
- }
20625
- Promise.all(pending).then(() => {
20626
- // imask reasserts otherwise, and handleBlur reverts to its stale value.
20627
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20628
- this.input.maskInstance.updateValue();
20629
- }
20630
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20631
- this.inputInBib.maskInstance.updateValue();
20632
- }
20633
- this._syncingDisplayValue = false;
20634
- });
20635
- }
20504
+ const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20505
+ if (!suppressed) {
20506
+ this.syncInputValuesAcrossTriggerAndBib(label || this.value);
20636
20507
  }
20637
20508
 
20638
20509
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20660,6 +20531,48 @@ class AuroCombobox extends AuroElement {
20660
20531
  this.requestUpdate();
20661
20532
  }
20662
20533
 
20534
+ /**
20535
+ * Writes nextValue to the trigger input and the bib input when their current
20536
+ * value differs, then re-asserts imask after Lit's update flushes.
20537
+ * @param {string} nextValue - The value to write to both inputs.
20538
+ * @returns {Promise<void> | null} Promise that resolves after the inputs flush,
20539
+ * or null when no sync was needed (so callers can skip post-flush work).
20540
+ * @private
20541
+ */
20542
+ syncInputValuesAcrossTriggerAndBib(nextValue) {
20543
+ // Only set the flag when there's an actual write to suppress —
20544
+ // syncValuesAndStates re-enters here during typing when both inputs
20545
+ // already match, and a no-op flag flip would make the bib branch's
20546
+ // bail eat legitimate user-input events.
20547
+ const triggerNeedsSync = this.input.value !== nextValue;
20548
+ const bibNeedsSync = this.inputInBib.value !== nextValue;
20549
+ if (!triggerNeedsSync && !bibNeedsSync) {
20550
+ return null;
20551
+ }
20552
+
20553
+ this._syncingDisplayValue = true;
20554
+
20555
+ const pending = [];
20556
+ if (triggerNeedsSync) {
20557
+ this.input.value = nextValue;
20558
+ pending.push(this.input.updateComplete);
20559
+ }
20560
+ if (bibNeedsSync) {
20561
+ this.inputInBib.value = nextValue;
20562
+ pending.push(this.inputInBib.updateComplete);
20563
+ }
20564
+ return Promise.all(pending).then(() => {
20565
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20566
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20567
+ this.input.maskInstance.updateValue();
20568
+ }
20569
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20570
+ this.inputInBib.maskInstance.updateValue();
20571
+ }
20572
+ this._syncingDisplayValue = false;
20573
+ });
20574
+ }
20575
+
20663
20576
  /**
20664
20577
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20665
20578
  * @private
@@ -20687,9 +20600,9 @@ class AuroCombobox extends AuroElement {
20687
20600
  this.syncValuesAndStates();
20688
20601
  }
20689
20602
 
20690
- // Re-activate when optionActive is no longer visible, or when _index has
20691
- // been reset (e.g. by clearSelection in an async update) so that
20692
- // makeSelection() can find the correct option.
20603
+ // Re-activate when optionActive is not in the current scrollable viewport,
20604
+ // or when _index has been reset (e.g. by clearSelection in an async update)
20605
+ // so that makeSelection() can find the correct option.
20693
20606
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20694
20607
  this.activateFirstEnabledAvailableOption();
20695
20608
  }
@@ -20823,28 +20736,25 @@ class AuroCombobox extends AuroElement {
20823
20736
 
20824
20737
  guardTouchPassthrough(this.menu);
20825
20738
 
20826
- // The dialog's showModal() steals focus from the trigger.
20827
- // A single rAF is enough for the dialog DOM to be painted and
20828
- // focusable, then doubleRaf finalizes the transition.
20739
+ // showModal() takes focus away from the trigger. Early focus once
20740
+ // the dialog has painted; the shared doubleRaf below is the fallback
20741
+ // for when the dialog needs an extra frame and also clears the
20742
+ // validation guard.
20829
20743
  requestAnimationFrame(() => {
20830
20744
  this.setInputFocus();
20831
20745
  });
20832
-
20833
- doubleRaf(() => {
20834
- this.setInputFocus();
20835
- this._inFullscreenTransition = false;
20836
- });
20837
- } else {
20838
- // Desktop popover-open: restore the trigger caret to end-of-text.
20839
- // Clicking the trigger lands on auro-input's floating <label for="…">
20840
- // overlay; Chrome resets the native input's selection to [0, 0] on
20841
- // label-focus before any JS runs. setInputFocus()'s non-fullscreen
20842
- // branch parks the caret at end. doubleRaf lets the dropdown layout
20843
- // settle first, matching the fullscreen branch timing.
20844
- doubleRaf(() => {
20845
- this.setInputFocus();
20846
- });
20847
20746
  }
20747
+ // else (desktop popover-open): Chrome resets the trigger caret to
20748
+ // [0, 0] when its floating <label for="…"> overlay receives focus.
20749
+ // The shared doubleRaf below parks the caret back at end-of-text
20750
+ // after the dropdown layout settles.
20751
+
20752
+ doubleRaf(() => {
20753
+ this.setInputFocus();
20754
+ if (this._inFullscreenTransition) {
20755
+ this._inFullscreenTransition = false;
20756
+ }
20757
+ });
20848
20758
  }
20849
20759
  });
20850
20760
 
@@ -20949,24 +20859,7 @@ class AuroCombobox extends AuroElement {
20949
20859
  nativeInput.setSelectionRange(len, len);
20950
20860
  }
20951
20861
  } else {
20952
- // Safety net for the strategy-change setTimeout: a viewport-crossing
20953
- // fullscreen→floating switch can close the dialog and leave focus on
20954
- // body. Every other Branch 2 callsite already has focus on the input
20955
- // via the focusin → this.focus() redirect (L1448-1452), so this is a
20956
- // no-op in the common case.
20957
- if (!this.input.componentHasFocus) {
20958
- this.input.focus();
20959
- }
20960
-
20961
- // Park the trigger native input's caret at end-of-text every time the
20962
- // popover opens. Chrome focuses the native via the floating <label for="…">
20963
- // overlay on click and resets selection to [0, 0]; no JS fires between
20964
- // mousedown and selectionchange, so we restore the caret here.
20965
- const triggerNativeInput = this.input.inputElement;
20966
- if (triggerNativeInput && triggerNativeInput.value) {
20967
- const len = triggerNativeInput.value.length;
20968
- triggerNativeInput.setSelectionRange(len, len);
20969
- }
20862
+ this.input.focus();
20970
20863
  }
20971
20864
  }
20972
20865
 
@@ -21072,7 +20965,7 @@ class AuroCombobox extends AuroElement {
21072
20965
  if (this.menu.optionSelected) {
21073
20966
  const selected = this.menu.optionSelected;
21074
20967
 
21075
- if (!this.optionSelected || this.optionSelected !== selected) {
20968
+ if (this.optionSelected !== selected) {
21076
20969
  this.optionSelected = selected;
21077
20970
  }
21078
20971
 
@@ -21085,7 +20978,7 @@ class AuroCombobox extends AuroElement {
21085
20978
  }
21086
20979
 
21087
20980
  // Update display
21088
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20981
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
21089
20982
 
21090
20983
  // Update match word for filtering
21091
20984
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -21101,13 +20994,22 @@ class AuroCombobox extends AuroElement {
21101
20994
  this.hideBib();
21102
20995
  }
21103
20996
 
20997
+ // Move focus to the clear button when the user makes a selection.
20998
+ if (!isEcho && this.menu.value !== undefined) {
20999
+ this.setClearBtnFocus();
21000
+ }
21001
+
21104
21002
  // Announce the selection after the dropdown closes so it isn't
21105
21003
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
21004
+ // Skip when there's no selected value (e.g. menu.clearSelection() from
21005
+ // the unmatched-value path), otherwise VoiceOver reads "undefined".
21106
21006
  const selectedValue = this.menu.value;
21107
- const announcementDelay = 300;
21108
- setTimeout(() => {
21109
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21110
- }, announcementDelay);
21007
+ if (selectedValue) {
21008
+ const announcementDelay = 300;
21009
+ setTimeout(() => {
21010
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21011
+ }, announcementDelay);
21012
+ }
21111
21013
  }
21112
21014
 
21113
21015
  // Programmatic value syncs leave availableOptions stale because
@@ -21121,18 +21023,6 @@ class AuroCombobox extends AuroElement {
21121
21023
  this._programmaticFilterRefresh = false;
21122
21024
  }, 0);
21123
21025
  }
21124
-
21125
- // base-input skips auto-validate when focus is inside its own shadow,
21126
- // which it is after setTriggerInputFocus — re-run so prior tooShort
21127
- // clears. processCreditCard reasserts errorMessage on every render.
21128
- if (!isEcho && this.menu.optionSelected && this.input.validate) {
21129
- this.input.updateComplete.then(() => {
21130
- this.input.validate(true);
21131
- if (this.input.validity === 'valid') {
21132
- this.input.errorMessage = '';
21133
- }
21134
- });
21135
- }
21136
21026
  });
21137
21027
 
21138
21028
  this.menu.addEventListener('auroMenu-customEventFired', () => {
@@ -21467,15 +21357,7 @@ class AuroCombobox extends AuroElement {
21467
21357
  this.menu.value = undefined;
21468
21358
  this.validation.reset(this);
21469
21359
  this.touched = false;
21470
- // Force validity back to the cleared state. validation.reset() calls
21471
- // validate() at the end, and (post-credit-card-fix) the trigger input's
21472
- // residual validity may still be copied into the combobox by the
21473
- // auroInputElements loop. Explicitly clear here so reset() actually
21474
- // leaves the component in a "no validity" state.
21475
21360
  this.validity = undefined;
21476
- this.errorMessage = '';
21477
- this.input.validity = undefined;
21478
- this.input.errorMessage = '';
21479
21361
  }
21480
21362
 
21481
21363
  /**
@@ -21528,52 +21410,27 @@ class AuroCombobox extends AuroElement {
21528
21410
  this.input.value = this.value;
21529
21411
  }
21530
21412
 
21531
- // Sync menu.value only when an option actually matches; otherwise clear it.
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.
21532
21417
  if (this.menu.options && this.menu.options.length > 0) {
21533
- if (this.menu.options.some((opt) => opt.value === this.value)) {
21534
- this.setMenuValue(this.value);
21535
- } else if (this.behavior === 'filter') {
21536
- // In filter mode, freeform values aren't allowed. Push the unmatched
21537
- // value through setMenuValue so menu dispatches auroMenu-selectValueFailure
21538
- // and the listener at line 1206 clears combobox.value (mirrors select's
21539
- // pattern). Bypassing setMenuValue here would skip the failure cascade.
21418
+ if (this.menu.options.some((opt) => opt.value === this.value) || this.behavior === 'filter') {
21540
21419
  this.setMenuValue(this.value);
21541
21420
  } else {
21542
- if (this.menu.value) {
21543
- this.menu.value = undefined;
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();
21544
21427
  }
21545
- // Sync the display for programmatic freeform value changes (e.g. the
21546
- // swap-values pattern). Guard with _syncingDisplayValue so that the
21547
- // re-entrant input event from base-input.notifyValueChanged() is
21548
- // ignored by handleInputValueChange. Skip the sync when input already
21549
- // matches to avoid spurious events during the normal typing path.
21550
- const nextValue = this.value || '';
21551
- const triggerNeedsSync = this.input.value !== nextValue;
21552
- const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
21553
- if (triggerNeedsSync || bibNeedsSync) {
21554
- this._syncingDisplayValue = true;
21555
- if (triggerNeedsSync) {
21556
- this.input.value = nextValue;
21557
- }
21558
- if (bibNeedsSync) {
21559
- this.inputInBib.value = nextValue;
21560
- }
21561
- const pending = [];
21562
- if (triggerNeedsSync) {
21563
- pending.push(this.input.updateComplete);
21564
- }
21565
- if (bibNeedsSync) {
21566
- pending.push(this.inputInBib.updateComplete);
21567
- }
21568
- Promise.all(pending).then(() => {
21569
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
21570
- this.input.maskInstance.updateValue();
21571
- }
21572
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
21573
- this.inputInBib.maskInstance.updateValue();
21574
- }
21575
- this._syncingDisplayValue = false;
21576
- // handleInputValueChange bailed on the flag — refresh the filter.
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(() => {
21577
21434
  this._programmaticFilterRefresh = true;
21578
21435
  this.handleMenuOptions();
21579
21436
  setTimeout(() => {