@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
@@ -810,6 +810,8 @@ function navigateArrow(component, direction, options = {}) {
810
810
  }
811
811
  }
812
812
 
813
+ /* eslint-disable no-underscore-dangle */
814
+
813
815
  /**
814
816
  * Returns the clear button element from the active input's shadow
815
817
  * DOM, if available.
@@ -845,11 +847,9 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
845
847
  * @param {Object} menu - The menu component.
846
848
  */
847
849
  function reconcileMenuIndex(menu) {
848
- // eslint-disable-next-line no-underscore-dangle
849
850
  if (menu._index < 0 && menu.optionActive && menu.items) {
850
851
  const idx = menu.items.indexOf(menu.optionActive);
851
852
  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 = '202606192121';
4876
+ var formkitVersion$2 = '202606231948';
4877
4877
 
4878
4878
  let AuroElement$2 = class AuroElement extends i$3 {
4879
4879
  static get properties() {
@@ -5824,6 +5824,14 @@ class AuroDropdown extends AuroElement$2 {
5824
5824
  };
5825
5825
  this.addEventListener('keydown', this._bibTabHandler);
5826
5826
 
5827
+ // Suppress AuroFloatingUI's auto-hide-on-focus-loss while the
5828
+ // desktopModal trap owns focus management. Without this, the very
5829
+ // first focus move into the bib (from the RAF below) triggers
5830
+ // handleFocusLoss → hideBib, tearing down the trap before the
5831
+ // user can press Tab.
5832
+ this._priorNoHideOnFocusLoss = this.noHideOnThisFocusLoss;
5833
+ this.noHideOnThisFocusLoss = true;
5834
+
5827
5835
  // Move initial focus into the bib content, matching FocusTrap behavior
5828
5836
  requestAnimationFrame(() => {
5829
5837
  const focusables = getFocusableElements(this.bibContent);
@@ -16594,6 +16602,7 @@ class AuroInputUtilities {
16594
16602
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16595
16603
 
16596
16604
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16605
+ console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16597
16606
  return undefined;
16598
16607
  }
16599
16608
 
@@ -16745,10 +16754,8 @@ class BaseInput extends AuroElement$1 {
16745
16754
  this.layout = 'classic';
16746
16755
  this.locale = 'en-US';
16747
16756
  this.max = undefined;
16748
- this._maxObject = undefined;
16749
16757
  this.maxLength = undefined;
16750
16758
  this.min = undefined;
16751
- this._minObject = undefined;
16752
16759
  this.minLength = undefined;
16753
16760
  this.required = false;
16754
16761
  this.onDark = false;
@@ -16756,7 +16763,6 @@ class BaseInput extends AuroElement$1 {
16756
16763
  this.size = 'lg';
16757
16764
  this.shape = 'classic';
16758
16765
  this.value = undefined;
16759
- this._valueObject = undefined;
16760
16766
 
16761
16767
  this._initializePrivateDefaults();
16762
16768
  }
@@ -17296,7 +17302,7 @@ class BaseInput extends AuroElement$1 {
17296
17302
  * @returns {Date|undefined}
17297
17303
  */
17298
17304
  get valueObject() {
17299
- return this._valueObject || this._computeDateObjectFallback(this.value);
17305
+ return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17300
17306
  }
17301
17307
 
17302
17308
  /**
@@ -17304,7 +17310,7 @@ class BaseInput extends AuroElement$1 {
17304
17310
  * @returns {Date|undefined}
17305
17311
  */
17306
17312
  get minObject() {
17307
- return this._minObject || this._computeDateObjectFallback(this.min);
17313
+ return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17308
17314
  }
17309
17315
 
17310
17316
  /**
@@ -17312,50 +17318,7 @@ class BaseInput extends AuroElement$1 {
17312
17318
  * @returns {Date|undefined}
17313
17319
  */
17314
17320
  get maxObject() {
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);
17321
+ return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
17359
17322
  }
17360
17323
 
17361
17324
  connectedCallback() {
@@ -17384,7 +17347,6 @@ class BaseInput extends AuroElement$1 {
17384
17347
  format: this.format
17385
17348
  });
17386
17349
  this.configureDataForType();
17387
- this.syncDateValues();
17388
17350
  }
17389
17351
 
17390
17352
  disconnectedCallback() {
@@ -17404,10 +17366,6 @@ class BaseInput extends AuroElement$1 {
17404
17366
  this.wrapperElement.addEventListener('click', this.handleClick);
17405
17367
  }
17406
17368
 
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
- }
17411
17369
  this.inputId = this.id ? `${this.id}-input` : window.crypto.randomUUID();
17412
17370
 
17413
17371
  // use validity message override if declared when initializing the component
@@ -17418,7 +17376,6 @@ class BaseInput extends AuroElement$1 {
17418
17376
  this.setCustomHelpTextMessage();
17419
17377
  this.configureAutoFormatting();
17420
17378
  this.configureDataForType();
17421
- this.syncDateValues();
17422
17379
  }
17423
17380
 
17424
17381
  /**
@@ -17555,8 +17512,6 @@ class BaseInput extends AuroElement$1 {
17555
17512
  this.configureDataForType();
17556
17513
  }
17557
17514
 
17558
- this.syncDateValues(changedProperties);
17559
-
17560
17515
  if (changedProperties.has('value')) {
17561
17516
  if (this.value && this.value.length > 0) {
17562
17517
  this.hasValue = true;
@@ -17578,14 +17533,14 @@ class BaseInput extends AuroElement$1 {
17578
17533
 
17579
17534
  if (formattedValue !== this.inputElement.value) {
17580
17535
  this.skipNextProgrammaticInputEvent = true;
17581
- if (this.maskInstance && this.type === 'credit-card') {
17536
+ if (this.maskInstance && this.type !== 'date') {
17582
17537
  // Route through the mask so its _value and el.value stay in lock-step
17583
17538
  // (set value calls updateControl which writes el.value = displayValue).
17584
17539
  // Writing el.value directly leaves the mask thinking displayValue is
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.
17540
+ // stale; _saveSelection on the next focus/click then warns. Date is
17541
+ // excluded because its formattedValue can be raw ISO when the calendar
17542
+ // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17543
+ // flip validity from patternMismatch to tooShort.
17589
17544
  this.maskInstance.value = formattedValue || '';
17590
17545
  } else if (formattedValue) {
17591
17546
  this.inputElement.value = formattedValue;
@@ -17627,120 +17582,65 @@ class BaseInput extends AuroElement$1 {
17627
17582
  }));
17628
17583
  }
17629
17584
 
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
-
17693
17585
  /**
17694
17586
  * Sets up IMasks and logic based on auto-formatting requirements.
17695
17587
  * @private
17696
17588
  * @returns {void}
17697
17589
  */
17698
17590
  configureAutoFormatting() {
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.
17591
+ // _configuringMask gates two things: external re-entry into this method
17592
+ // while setup is mid-flight (from property changes that call back here),
17593
+ // and the accept/complete listeners below — both need to ignore the mask
17594
+ // events fired by our own value-restore step.
17703
17595
  if (this._configuringMask) return;
17704
17596
  this._configuringMask = true;
17705
17597
  try {
17598
+ // Destroy any prior mask so IMask can attach fresh under the new format.
17599
+ // Null the reference too — if the new maskOptions.mask is falsy (e.g.
17600
+ // type switched from credit-card to text) the IMask() reassignment
17601
+ // below is skipped, and downstream writes at line ~823 would otherwise
17602
+ // route through the destroyed instance.
17706
17603
  if (this.maskInstance) {
17707
17604
  this.maskInstance.destroy();
17605
+ this.maskInstance = null;
17708
17606
  }
17709
17607
 
17710
- // Pass new format to util
17711
17608
  this.util.updateFormat(this.format);
17712
17609
 
17713
17610
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17714
17611
 
17715
17612
  if (this.inputElement && maskOptions.mask) {
17716
-
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.
17613
+ // Capture the current display so it can be re-applied after IMask
17614
+ // attaches. The restore at the bottom goes through maskInstance.value
17615
+ // (not inputElement.value directly) so the mask's internal state and
17616
+ // the input's displayed text stay in lock-step.
17723
17617
  let existingValue = this.inputElement.value;
17618
+
17619
+ // Format-change case (e.g. locale switch): existingValue is the OLD
17620
+ // mask's display string and may not parse under the new mask. When
17621
+ // we have a valid date model, rebuild the display from valueObject
17622
+ // (the canonical source) using the new mask's format function.
17724
17623
  if (
17725
17624
  this.util.isFullDateFormat(this.type, this.format) &&
17726
17625
  this.value &&
17727
- dateFormatter.isValidDate(this.value) &&
17728
- this.valueObject instanceof Date &&
17729
- !Number.isNaN(this.valueObject.getTime()) &&
17626
+ this.valueObject &&
17730
17627
  typeof maskOptions.format === 'function'
17731
17628
  ) {
17732
17629
  existingValue = maskOptions.format(this.valueObject);
17733
17630
  }
17734
17631
 
17632
+ // Clear before IMask attaches so the constructor seeds an empty
17633
+ // internal value. Otherwise IMask reads the stale unmasked string
17634
+ // and emits a spurious 'accept' before the restore below runs.
17735
17635
  this.skipNextProgrammaticInputEvent = true;
17736
17636
  this.inputElement.value = '';
17737
17637
 
17738
17638
  this.maskInstance = IMask(this.inputElement, maskOptions);
17739
17639
 
17640
+ // Mask fires 'accept' on every value change, including the restore
17641
+ // step below. Skip events fired during configureAutoFormatting so
17642
+ // we don't overwrite a value the parent just pushed.
17740
17643
  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.
17744
17644
  if (this._configuringMask) return;
17745
17645
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17746
17646
  if (this.type === "date") {
@@ -17748,6 +17648,8 @@ class BaseInput extends AuroElement$1 {
17748
17648
  }
17749
17649
  });
17750
17650
 
17651
+ // Mask fires 'complete' on the restore step below for any value that
17652
+ // happens to be a complete match. Same setup-suppression as 'accept'.
17751
17653
  this.maskInstance.on('complete', () => {
17752
17654
  if (this._configuringMask) return;
17753
17655
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17756,7 +17658,9 @@ class BaseInput extends AuroElement$1 {
17756
17658
  }
17757
17659
  });
17758
17660
 
17759
- // Restore the stashed value through IMask so it's properly masked
17661
+ // Write existingValue through the mask (not the input directly) so
17662
+ // the mask reformats it under the new rules and keeps its internal
17663
+ // _value aligned with the input's displayed text.
17760
17664
  if (existingValue) {
17761
17665
  this.maskInstance.value = existingValue;
17762
17666
  }
@@ -17924,7 +17828,6 @@ class BaseInput extends AuroElement$1 {
17924
17828
  */
17925
17829
  reset() {
17926
17830
  this.value = undefined;
17927
- this.setDateObjectProperty('valueObject', undefined);
17928
17831
  this.validation.reset(this);
17929
17832
  }
17930
17833
 
@@ -17933,7 +17836,6 @@ class BaseInput extends AuroElement$1 {
17933
17836
  */
17934
17837
  clear() {
17935
17838
  this.value = undefined;
17936
- this.setDateObjectProperty('valueObject', undefined);
17937
17839
  }
17938
17840
 
17939
17841
  /**
@@ -18462,7 +18364,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18462
18364
  }
18463
18365
  };
18464
18366
 
18465
- var formkitVersion$1 = '202606192121';
18367
+ var formkitVersion$1 = '202606231948';
18466
18368
 
18467
18369
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18468
18370
  // See LICENSE in the project root for license information.
@@ -19583,7 +19485,7 @@ class AuroBibtemplate extends i$3 {
19583
19485
  }
19584
19486
  }
19585
19487
 
19586
- var formkitVersion = '202606192121';
19488
+ var formkitVersion = '202606231948';
19587
19489
 
19588
19490
  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}`;
19589
19491
 
@@ -20570,7 +20472,7 @@ class AuroCombobox extends AuroElement {
20570
20472
  if (this.menu) {
20571
20473
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20572
20474
  }
20573
- const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20475
+ const label = getOptionLabel(this.menu.optionSelected);
20574
20476
  this.updateTriggerTextDisplay(label || this.value);
20575
20477
  }
20576
20478
 
@@ -20584,40 +20486,9 @@ class AuroCombobox extends AuroElement {
20584
20486
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20585
20487
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20586
20488
 
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
- }
20489
+ const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20490
+ if (!suppressed) {
20491
+ this.syncInputValuesAcrossTriggerAndBib(label || this.value);
20621
20492
  }
20622
20493
 
20623
20494
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20645,6 +20516,48 @@ class AuroCombobox extends AuroElement {
20645
20516
  this.requestUpdate();
20646
20517
  }
20647
20518
 
20519
+ /**
20520
+ * Writes nextValue to the trigger input and the bib input when their current
20521
+ * value differs, then re-asserts imask after Lit's update flushes.
20522
+ * @param {string} nextValue - The value to write to both inputs.
20523
+ * @returns {Promise<void> | null} Promise that resolves after the inputs flush,
20524
+ * or null when no sync was needed (so callers can skip post-flush work).
20525
+ * @private
20526
+ */
20527
+ syncInputValuesAcrossTriggerAndBib(nextValue) {
20528
+ // Only set the flag when there's an actual write to suppress —
20529
+ // syncValuesAndStates re-enters here during typing when both inputs
20530
+ // already match, and a no-op flag flip would make the bib branch's
20531
+ // bail eat legitimate user-input events.
20532
+ const triggerNeedsSync = this.input.value !== nextValue;
20533
+ const bibNeedsSync = this.inputInBib.value !== nextValue;
20534
+ if (!triggerNeedsSync && !bibNeedsSync) {
20535
+ return null;
20536
+ }
20537
+
20538
+ this._syncingDisplayValue = true;
20539
+
20540
+ const pending = [];
20541
+ if (triggerNeedsSync) {
20542
+ this.input.value = nextValue;
20543
+ pending.push(this.input.updateComplete);
20544
+ }
20545
+ if (bibNeedsSync) {
20546
+ this.inputInBib.value = nextValue;
20547
+ pending.push(this.inputInBib.updateComplete);
20548
+ }
20549
+ return Promise.all(pending).then(() => {
20550
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20551
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20552
+ this.input.maskInstance.updateValue();
20553
+ }
20554
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20555
+ this.inputInBib.maskInstance.updateValue();
20556
+ }
20557
+ this._syncingDisplayValue = false;
20558
+ });
20559
+ }
20560
+
20648
20561
  /**
20649
20562
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20650
20563
  * @private
@@ -20672,9 +20585,9 @@ class AuroCombobox extends AuroElement {
20672
20585
  this.syncValuesAndStates();
20673
20586
  }
20674
20587
 
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.
20588
+ // Re-activate when optionActive is not in the current scrollable viewport,
20589
+ // or when _index has been reset (e.g. by clearSelection in an async update)
20590
+ // so that makeSelection() can find the correct option.
20678
20591
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20679
20592
  this.activateFirstEnabledAvailableOption();
20680
20593
  }
@@ -20808,28 +20721,25 @@ class AuroCombobox extends AuroElement {
20808
20721
 
20809
20722
  guardTouchPassthrough(this.menu);
20810
20723
 
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.
20724
+ // showModal() takes focus away from the trigger. Early focus once
20725
+ // the dialog has painted; the shared doubleRaf below is the fallback
20726
+ // for when the dialog needs an extra frame and also clears the
20727
+ // validation guard.
20814
20728
  requestAnimationFrame(() => {
20815
20729
  this.setInputFocus();
20816
20730
  });
20817
-
20818
- doubleRaf(() => {
20819
- this.setInputFocus();
20820
- this._inFullscreenTransition = false;
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
20731
  }
20732
+ // else (desktop popover-open): Chrome resets the trigger caret to
20733
+ // [0, 0] when its floating <label for="…"> overlay receives focus.
20734
+ // The shared doubleRaf below parks the caret back at end-of-text
20735
+ // after the dropdown layout settles.
20736
+
20737
+ doubleRaf(() => {
20738
+ this.setInputFocus();
20739
+ if (this._inFullscreenTransition) {
20740
+ this._inFullscreenTransition = false;
20741
+ }
20742
+ });
20833
20743
  }
20834
20744
  });
20835
20745
 
@@ -20934,24 +20844,7 @@ class AuroCombobox extends AuroElement {
20934
20844
  nativeInput.setSelectionRange(len, len);
20935
20845
  }
20936
20846
  } else {
20937
- // Safety net for the strategy-change setTimeout: a viewport-crossing
20938
- // fullscreen→floating switch can close the dialog and leave focus on
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.
20942
- if (!this.input.componentHasFocus) {
20943
- this.input.focus();
20944
- }
20945
-
20946
- // Park the trigger native input's caret at end-of-text every time the
20947
- // popover opens. Chrome focuses the native via the floating <label for="…">
20948
- // overlay on click and resets selection to [0, 0]; no JS fires between
20949
- // mousedown and selectionchange, so we restore the caret here.
20950
- const triggerNativeInput = this.input.inputElement;
20951
- if (triggerNativeInput && triggerNativeInput.value) {
20952
- const len = triggerNativeInput.value.length;
20953
- triggerNativeInput.setSelectionRange(len, len);
20954
- }
20847
+ this.input.focus();
20955
20848
  }
20956
20849
  }
20957
20850
 
@@ -21057,7 +20950,7 @@ class AuroCombobox extends AuroElement {
21057
20950
  if (this.menu.optionSelected) {
21058
20951
  const selected = this.menu.optionSelected;
21059
20952
 
21060
- if (!this.optionSelected || this.optionSelected !== selected) {
20953
+ if (this.optionSelected !== selected) {
21061
20954
  this.optionSelected = selected;
21062
20955
  }
21063
20956
 
@@ -21070,7 +20963,7 @@ class AuroCombobox extends AuroElement {
21070
20963
  }
21071
20964
 
21072
20965
  // Update display
21073
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20966
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
21074
20967
 
21075
20968
  // Update match word for filtering
21076
20969
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -21086,13 +20979,22 @@ class AuroCombobox extends AuroElement {
21086
20979
  this.hideBib();
21087
20980
  }
21088
20981
 
20982
+ // Move focus to the clear button when the user makes a selection.
20983
+ if (!isEcho && this.menu.value !== undefined) {
20984
+ this.setClearBtnFocus();
20985
+ }
20986
+
21089
20987
  // Announce the selection after the dropdown closes so it isn't
21090
20988
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
20989
+ // Skip when there's no selected value (e.g. menu.clearSelection() from
20990
+ // the unmatched-value path), otherwise VoiceOver reads "undefined".
21091
20991
  const selectedValue = this.menu.value;
21092
- const announcementDelay = 300;
21093
- setTimeout(() => {
21094
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21095
- }, announcementDelay);
20992
+ if (selectedValue) {
20993
+ const announcementDelay = 300;
20994
+ setTimeout(() => {
20995
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
20996
+ }, announcementDelay);
20997
+ }
21096
20998
  }
21097
20999
 
21098
21000
  // Programmatic value syncs leave availableOptions stale because
@@ -21106,18 +21008,6 @@ class AuroCombobox extends AuroElement {
21106
21008
  this._programmaticFilterRefresh = false;
21107
21009
  }, 0);
21108
21010
  }
21109
-
21110
- // base-input skips auto-validate when focus is inside its own shadow,
21111
- // which it is after setTriggerInputFocus — re-run so prior tooShort
21112
- // clears. processCreditCard reasserts errorMessage on every render.
21113
- if (!isEcho && this.menu.optionSelected && this.input.validate) {
21114
- this.input.updateComplete.then(() => {
21115
- this.input.validate(true);
21116
- if (this.input.validity === 'valid') {
21117
- this.input.errorMessage = '';
21118
- }
21119
- });
21120
- }
21121
21011
  });
21122
21012
 
21123
21013
  this.menu.addEventListener('auroMenu-customEventFired', () => {
@@ -21452,15 +21342,7 @@ class AuroCombobox extends AuroElement {
21452
21342
  this.menu.value = undefined;
21453
21343
  this.validation.reset(this);
21454
21344
  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.
21460
21345
  this.validity = undefined;
21461
- this.errorMessage = '';
21462
- this.input.validity = undefined;
21463
- this.input.errorMessage = '';
21464
21346
  }
21465
21347
 
21466
21348
  /**
@@ -21513,52 +21395,27 @@ class AuroCombobox extends AuroElement {
21513
21395
  this.input.value = this.value;
21514
21396
  }
21515
21397
 
21516
- // Sync menu.value only when an option actually matches; otherwise clear it.
21398
+ // Sync menu.value only when an option actually matches, or when filter
21399
+ // mode needs setMenuValue to dispatch auroMenu-selectValueFailure (the
21400
+ // listener at line 1206 clears combobox.value, mirroring select's
21401
+ // pattern). Otherwise sync the input display for freeform values.
21517
21402
  if (this.menu.options && this.menu.options.length > 0) {
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.
21403
+ if (this.menu.options.some((opt) => opt.value === this.value) || this.behavior === 'filter') {
21525
21404
  this.setMenuValue(this.value);
21526
21405
  } else {
21527
- if (this.menu.value) {
21528
- this.menu.value = undefined;
21406
+ // Clear menu.value AND menu.optionSelected together. Clearing only
21407
+ // menu.value leaves the previously-selected option element pinned
21408
+ // as menu.optionSelected; a later auroMenu-selectedOption event
21409
+ // would then write its stale .value back into combobox.value.
21410
+ if (this.menu.value || this.menu.optionSelected) {
21411
+ this.menu.clearSelection();
21529
21412
  }
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.
21413
+ // Suggestion-mode freeform value: sync the trigger + bib to show it,
21414
+ // then refresh the filter once the inputs flush (handleInputValueChange
21415
+ // bailed on _syncingDisplayValue).
21416
+ const syncPromise = this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21417
+ if (syncPromise) {
21418
+ syncPromise.then(() => {
21562
21419
  this._programmaticFilterRefresh = true;
21563
21420
  this.handleMenuOptions();
21564
21421
  setTimeout(() => {