@aurodesignsystem-dev/auro-formkit 0.0.0-pr1503.2 → 0.0.0-pr1505.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 (62) hide show
  1. package/components/checkbox/demo/customize.min.js +2 -2
  2. package/components/checkbox/demo/getting-started.min.js +2 -2
  3. package/components/checkbox/demo/index.min.js +2 -2
  4. package/components/checkbox/dist/index.js +2 -2
  5. package/components/checkbox/dist/registered.js +2 -2
  6. package/components/combobox/demo/customize.md +7 -3
  7. package/components/combobox/demo/customize.min.js +1450 -1662
  8. package/components/combobox/demo/getting-started.min.js +1450 -1662
  9. package/components/combobox/demo/index.min.js +1450 -1662
  10. package/components/combobox/dist/auro-combobox.d.ts +5 -0
  11. package/components/combobox/dist/index.js +504 -141
  12. package/components/combobox/dist/registered.js +504 -141
  13. package/components/counter/demo/customize.min.js +3 -3
  14. package/components/counter/demo/index.min.js +3 -3
  15. package/components/counter/dist/index.js +3 -3
  16. package/components/counter/dist/registered.js +3 -3
  17. package/components/datepicker/demo/api.md +2 -0
  18. package/components/datepicker/demo/customize.md +195 -33
  19. package/components/datepicker/demo/customize.min.js +145 -60
  20. package/components/datepicker/demo/index.min.js +137 -59
  21. package/components/datepicker/dist/index.js +137 -59
  22. package/components/datepicker/dist/registered.js +137 -59
  23. package/components/datepicker/dist/src/auro-datepicker.d.ts +2 -0
  24. package/components/dropdown/demo/customize.min.js +1 -1
  25. package/components/dropdown/demo/getting-started.min.js +1 -1
  26. package/components/dropdown/demo/index.min.js +1 -1
  27. package/components/dropdown/dist/index.js +1 -1
  28. package/components/dropdown/dist/registered.js +1 -1
  29. package/components/form/demo/customize.min.js +2209 -2237
  30. package/components/form/demo/getting-started.min.js +2209 -2237
  31. package/components/form/demo/index.min.js +2209 -2237
  32. package/components/form/demo/registerDemoDeps.min.js +2207 -2235
  33. package/components/input/demo/customize.md +56 -55
  34. package/components/input/demo/customize.min.js +131 -56
  35. package/components/input/demo/getting-started.min.js +131 -56
  36. package/components/input/demo/index.md +2 -2
  37. package/components/input/demo/index.min.js +131 -56
  38. package/components/input/dist/base-input.d.ts +16 -0
  39. package/components/input/dist/index.js +131 -56
  40. package/components/input/dist/registered.js +131 -56
  41. package/components/menu/demo/api.md +41 -45
  42. package/components/menu/demo/customize.md +0 -28
  43. package/components/menu/demo/index.min.js +779 -1354
  44. package/components/menu/dist/auro-menu.d.ts +95 -110
  45. package/components/menu/dist/auro-menuoption.d.ts +32 -138
  46. package/components/menu/dist/index.js +753 -1308
  47. package/components/menu/dist/registered.js +765 -1308
  48. package/components/radio/demo/customize.min.js +2 -2
  49. package/components/radio/demo/getting-started.min.js +2 -2
  50. package/components/radio/demo/index.min.js +2 -2
  51. package/components/radio/dist/index.js +2 -2
  52. package/components/radio/dist/registered.js +2 -2
  53. package/components/select/demo/customize.md +7 -3
  54. package/components/select/demo/customize.min.js +1072 -1616
  55. package/components/select/demo/getting-started.min.js +1072 -1616
  56. package/components/select/demo/index.min.js +1072 -1616
  57. package/components/select/dist/index.js +46 -15
  58. package/components/select/dist/registered.js +46 -15
  59. package/components/select/dist/selectUtils.d.ts +12 -0
  60. package/custom-elements.json +384 -1022
  61. package/package.json +1 -3
  62. package/components/menu/dist/auro-menu.context.d.ts +0 -238
@@ -555,7 +555,7 @@ let AuroFormValidation$1 = class AuroFormValidation {
555
555
  } else if (this.runtimeUtils.elementMatch(elem, 'auro-input') && elem.errorMessage === '') {
556
556
  const input = elem.renderRoot.querySelector('input');
557
557
 
558
- if (input.validationMessage.length > 0) {
558
+ if (input && input.validationMessage.length > 0) {
559
559
  elem.errorMessage = input.validationMessage;
560
560
  }
561
561
  } else if (this.runtimeUtils.elementMatch(elem, 'auro-combobox') && elem.errorMessage === '') {
@@ -797,6 +797,21 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
797
797
  return isFocused;
798
798
  }
799
799
 
800
+ /**
801
+ * Reconcile the menu `_index` from `optionActive` so subsequent `makeSelection` calls find the correct option after async clearSelection has reset it.
802
+ * @param {Object} menu - The menu component.
803
+ */
804
+ function reconcileMenuIndex(menu) {
805
+ // eslint-disable-next-line no-underscore-dangle
806
+ if (menu._index < 0 && menu.optionActive && menu.items) {
807
+ const idx = menu.items.indexOf(menu.optionActive);
808
+ if (idx >= 0) {
809
+ // eslint-disable-next-line no-underscore-dangle
810
+ menu._index = idx;
811
+ }
812
+ }
813
+ }
814
+
800
815
  const comboboxKeyboardStrategy = {
801
816
  ArrowDown(component, evt, ctx) {
802
817
  // If the clear button has focus, let the browser handle ArrowDown normally.
@@ -860,6 +875,7 @@ const comboboxKeyboardStrategy = {
860
875
  // block the browser's built-in "Enter activates focused button" behavior.
861
876
  evt.stopPropagation();
862
877
  } else if (ctx.isExpanded && component.menu.optionActive) {
878
+ reconcileMenuIndex(component.menu);
863
879
  component.menu.makeSelection();
864
880
 
865
881
  if (ctx.isModal) {
@@ -907,6 +923,7 @@ const comboboxKeyboardStrategy = {
907
923
  // When the clear button is focused, Tab events do not bubble out of
908
924
  // its shadow DOM, so this handler only fires when the clear button
909
925
  // is NOT focused. In that case, select the active option and close.
926
+ reconcileMenuIndex(component.menu);
910
927
  component.menu.makeSelection();
911
928
  component.hideBib();
912
929
 
@@ -4789,7 +4806,7 @@ let AuroHelpText$2 = class AuroHelpText extends LitElement {
4789
4806
  }
4790
4807
  };
4791
4808
 
4792
- var formkitVersion$2 = '202606151650';
4809
+ var formkitVersion$2 = '202606171946';
4793
4810
 
4794
4811
  let AuroElement$2 = class AuroElement extends LitElement {
4795
4812
  static get properties() {
@@ -10662,7 +10679,7 @@ class AuroFormValidation {
10662
10679
  } else if (this.runtimeUtils.elementMatch(elem, 'auro-input') && elem.errorMessage === '') {
10663
10680
  const input = elem.renderRoot.querySelector('input');
10664
10681
 
10665
- if (input.validationMessage.length > 0) {
10682
+ if (input && input.validationMessage.length > 0) {
10666
10683
  elem.errorMessage = input.validationMessage;
10667
10684
  }
10668
10685
  } else if (this.runtimeUtils.elementMatch(elem, 'auro-combobox') && elem.errorMessage === '') {
@@ -16503,7 +16520,6 @@ class AuroInputUtilities {
16503
16520
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16504
16521
 
16505
16522
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16506
- console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16507
16523
  return undefined;
16508
16524
  }
16509
16525
 
@@ -17206,7 +17222,7 @@ class BaseInput extends AuroElement$1 {
17206
17222
  * @returns {Date|undefined}
17207
17223
  */
17208
17224
  get valueObject() {
17209
- return this._valueObject;
17225
+ return this._valueObject || this._computeDateObjectFallback(this.value);
17210
17226
  }
17211
17227
 
17212
17228
  /**
@@ -17214,7 +17230,7 @@ class BaseInput extends AuroElement$1 {
17214
17230
  * @returns {Date|undefined}
17215
17231
  */
17216
17232
  get minObject() {
17217
- return this._minObject;
17233
+ return this._minObject || this._computeDateObjectFallback(this.min);
17218
17234
  }
17219
17235
 
17220
17236
  /**
@@ -17222,7 +17238,31 @@ class BaseInput extends AuroElement$1 {
17222
17238
  * @returns {Date|undefined}
17223
17239
  */
17224
17240
  get maxObject() {
17225
- return this._maxObject;
17241
+ return this._maxObject || this._computeDateObjectFallback(this.max);
17242
+ }
17243
+
17244
+ /**
17245
+ * Parses a date string into a Date object when the corresponding `_*Object`
17246
+ * field hasn't been synced yet by `updated()`. Returns undefined when the
17247
+ * input type/format isn't a full date or the string is not a valid date.
17248
+ *
17249
+ * Why this exists: a parent (datepicker) can call `inputN.validate()` from
17250
+ * inside its own `updated()` before this input's `updated()` has run
17251
+ * `syncDateValues()` — so `_valueObject`/`_maxObject` are still `undefined`
17252
+ * and range checks would otherwise silently no-op (flipping the result to
17253
+ * `valid` or `patternMismatch`).
17254
+ * @private
17255
+ * @param {string|undefined} dateStr - ISO date string from `value`/`min`/`max`.
17256
+ * @returns {Date|undefined}
17257
+ */
17258
+ _computeDateObjectFallback(dateStr) {
17259
+ if (!dateStr || !this.util || !this.util.isFullDateFormat(this.type, this.format)) {
17260
+ return undefined;
17261
+ }
17262
+ if (!dateFormatter.isValidDate(dateStr)) {
17263
+ return undefined;
17264
+ }
17265
+ return dateFormatter.stringToDateInstance(dateStr);
17226
17266
  }
17227
17267
 
17228
17268
  /**
@@ -17247,8 +17287,30 @@ class BaseInput extends AuroElement$1 {
17247
17287
  connectedCallback() {
17248
17288
  super.connectedCallback();
17249
17289
 
17290
+ // Mark for query selectors when registered under a versioned tag (e.g. inside
17291
+ // a datepicker/combobox). Must run before parent components call validate() on
17292
+ // their inner inputs — the validation framework matches via tag-or-attribute,
17293
+ // and parents trigger validation during their own updated() cycle, which can
17294
+ // precede this input's firstUpdated().
17295
+ if (this.tagName.toLowerCase() !== 'auro-input' && !this.hasAttribute('auro-input')) {
17296
+ this.setAttribute('auro-input', '');
17297
+ }
17298
+
17250
17299
  this.locale = this.domHandler.getLocale(this);
17251
17300
  notifyOnLangChange(this);
17301
+
17302
+ // Pre-compute lengthForType and date-object fields so a parent (e.g. datepicker)
17303
+ // calling our validate() synchronously during its own updated() cycle sees
17304
+ // populated values. Without this, range validation silently no-ops because
17305
+ // `max.length === lengthForType` fails when lengthForType is still undefined.
17306
+ // Rebuild util here (constructor seeded with en-US default) so configureDataForType's
17307
+ // locale-derived format lookup uses the actual locale just resolved above.
17308
+ this.util = new AuroInputUtilities({
17309
+ locale: this.locale,
17310
+ format: this.format
17311
+ });
17312
+ this.configureDataForType();
17313
+ this.syncDateValues();
17252
17314
  }
17253
17315
 
17254
17316
  disconnectedCallback() {
@@ -17269,8 +17331,8 @@ class BaseInput extends AuroElement$1 {
17269
17331
  }
17270
17332
 
17271
17333
  // add attribute for query selectors when auro-input is registered under a custom name
17272
- if (this.tagName.toLowerCase() !== 'auro-input') {
17273
- this.setAttribute('auro-input', true);
17334
+ if (this.tagName.toLowerCase() !== 'auro-input' && !this.hasAttribute('auro-input')) {
17335
+ this.setAttribute('auro-input', '');
17274
17336
  }
17275
17337
  this.inputId = this.id ? `${this.id}-input` : window.crypto.randomUUID();
17276
17338
 
@@ -17318,6 +17380,12 @@ class BaseInput extends AuroElement$1 {
17318
17380
  return;
17319
17381
  }
17320
17382
 
17383
+ // While configureAutoFormatting is running, imask's internal updateControl
17384
+ // writes el.value to align display with the masked value. A synthetic input
17385
+ // event from that write would re-enter handleInput → processCreditCard mid-setup
17386
+ // and clobber a Lit value that was just pushed by the parent combobox.
17387
+ if (component._configuringMask) return;
17388
+
17321
17389
  // If all guard clauses are passed, dispatch the event
17322
17390
  const inputEvent = new InputEvent('input', {
17323
17391
  bubbles: true,
@@ -17436,7 +17504,16 @@ class BaseInput extends AuroElement$1 {
17436
17504
 
17437
17505
  if (formattedValue !== this.inputElement.value) {
17438
17506
  this.skipNextProgrammaticInputEvent = true;
17439
- if (formattedValue) {
17507
+ if (this.maskInstance && this.type === 'credit-card') {
17508
+ // Route through the mask so its _value and el.value stay in lock-step
17509
+ // (set value calls updateControl which writes el.value = displayValue).
17510
+ // Writing el.value directly leaves the mask thinking displayValue is
17511
+ // stale; _saveSelection on the next focus/click then warns. Scoped to
17512
+ // credit-card so date's own formattedValue (raw ISO when calendar-invalid)
17513
+ // isn't re-masked through the mm/dd/yyyy mask, which would truncate it
17514
+ // and flip validity from patternMismatch to tooShort.
17515
+ this.maskInstance.value = formattedValue || '';
17516
+ } else if (formattedValue) {
17440
17517
  this.inputElement.value = formattedValue;
17441
17518
  } else {
17442
17519
  this.inputElement.value = '';
@@ -17545,58 +17622,73 @@ class BaseInput extends AuroElement$1 {
17545
17622
  * @returns {void}
17546
17623
  */
17547
17624
  configureAutoFormatting() {
17548
- if (this.maskInstance) {
17549
- this.maskInstance.destroy();
17550
- }
17551
-
17552
- // Pass new format to util
17553
- this.util.updateFormat(this.format);
17625
+ // Re-entrancy guard. The patched-setter's synthetic input event (suppressed
17626
+ // by _configuringMask above) could otherwise trigger handleInput →
17627
+ // processCreditCard → configureAutoFormatting before the outer call's
17628
+ // set value has finished its alignCursor pass.
17629
+ if (this._configuringMask) return;
17630
+ this._configuringMask = true;
17631
+ try {
17632
+ if (this.maskInstance) {
17633
+ this.maskInstance.destroy();
17634
+ }
17554
17635
 
17555
- const maskOptions = this.util.getMaskOptions(this.type, this.format);
17636
+ // Pass new format to util
17637
+ this.util.updateFormat(this.format);
17556
17638
 
17557
- if (this.inputElement && maskOptions.mask) {
17639
+ const maskOptions = this.util.getMaskOptions(this.type, this.format);
17558
17640
 
17559
- // Stash and clear any existing value before IMask init.
17560
- // IMask's constructor processes the current input value which requires
17561
- // selection state — clearing first avoids that scenario entirely.
17562
- // When the format changes (e.g. locale switch) and we have a valid ISO
17563
- // model value, compute the display string for the NEW format instead of
17564
- // re-using the old display string, which may be invalid in the new mask.
17565
- let existingValue = this.inputElement.value;
17566
- if (
17567
- this.util.isFullDateFormat(this.type, this.format) &&
17568
- this.value &&
17569
- dateFormatter.isValidDate(this.value) &&
17570
- this.valueObject instanceof Date &&
17571
- !Number.isNaN(this.valueObject.getTime()) &&
17572
- typeof maskOptions.format === 'function'
17573
- ) {
17574
- existingValue = maskOptions.format(this.valueObject);
17575
- }
17641
+ if (this.inputElement && maskOptions.mask) {
17576
17642
 
17577
- this.skipNextProgrammaticInputEvent = true;
17578
- this.inputElement.value = '';
17643
+ // Stash and clear any existing value before IMask init.
17644
+ // IMask's constructor processes the current input value which requires
17645
+ // selection state — clearing first avoids that scenario entirely.
17646
+ // When the format changes (e.g. locale switch) and we have a valid ISO
17647
+ // model value, compute the display string for the NEW format instead of
17648
+ // re-using the old display string, which may be invalid in the new mask.
17649
+ let existingValue = this.inputElement.value;
17650
+ if (
17651
+ this.util.isFullDateFormat(this.type, this.format) &&
17652
+ this.value &&
17653
+ dateFormatter.isValidDate(this.value) &&
17654
+ this.valueObject instanceof Date &&
17655
+ !Number.isNaN(this.valueObject.getTime()) &&
17656
+ typeof maskOptions.format === 'function'
17657
+ ) {
17658
+ existingValue = maskOptions.format(this.valueObject);
17659
+ }
17579
17660
 
17580
- this.maskInstance = IMask(this.inputElement, maskOptions);
17661
+ this.skipNextProgrammaticInputEvent = true;
17662
+ this.inputElement.value = '';
17663
+
17664
+ this.maskInstance = IMask(this.inputElement, maskOptions);
17665
+
17666
+ this.maskInstance.on('accept', () => {
17667
+ // Suppress propagation during configureAutoFormatting's own value-restoration
17668
+ // (line below) — the mask emits 'accept' on every value-set, including ours,
17669
+ // and we don't want to overwrite a value the parent just pushed.
17670
+ if (this._configuringMask) return;
17671
+ this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17672
+ if (this.type === "date") {
17673
+ this._rawMaskValue = this.maskInstance.value;
17674
+ }
17675
+ });
17581
17676
 
17582
- this.maskInstance.on('accept', () => {
17583
- this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17584
- if (this.type === "date") {
17585
- this._rawMaskValue = this.maskInstance.value;
17586
- }
17587
- });
17677
+ this.maskInstance.on('complete', () => {
17678
+ if (this._configuringMask) return;
17679
+ this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17680
+ if (this.type === "date") {
17681
+ this._rawMaskValue = this.maskInstance.value;
17682
+ }
17683
+ });
17588
17684
 
17589
- this.maskInstance.on('complete', () => {
17590
- this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17591
- if (this.type === "date") {
17592
- this._rawMaskValue = this.maskInstance.value;
17685
+ // Restore the stashed value through IMask so it's properly masked
17686
+ if (existingValue) {
17687
+ this.maskInstance.value = existingValue;
17593
17688
  }
17594
- });
17595
-
17596
- // Restore the stashed value through IMask so it's properly masked
17597
- if (existingValue) {
17598
- this.maskInstance.value = existingValue;
17599
17689
  }
17690
+ } finally {
17691
+ this._configuringMask = false;
17600
17692
  }
17601
17693
  }
17602
17694
 
@@ -17883,10 +17975,10 @@ class BaseInput extends AuroElement$1 {
17883
17975
  },
17884
17976
  {
17885
17977
  name: 'Diners club',
17886
- regex: /^(?<num>36|38)\d{0}/u,
17978
+ regex: /^(?<num>30[0-5]|36|38)\d{0}/u,
17887
17979
  formatLength: 16,
17888
17980
  errorMessage: CreditCardValidationMessage,
17889
- cardIcon: 'credit-card',
17981
+ cardIcon: 'cc-dinersclub',
17890
17982
  maskFormat: "0000 000000 0000"
17891
17983
  },
17892
17984
  {
@@ -18296,7 +18388,7 @@ let AuroHelpText$1 = class AuroHelpText extends LitElement {
18296
18388
  }
18297
18389
  };
18298
18390
 
18299
- var formkitVersion$1 = '202606151650';
18391
+ var formkitVersion$1 = '202606171946';
18300
18392
 
18301
18393
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18302
18394
  // See LICENSE in the project root for license information.
@@ -19417,7 +19509,7 @@ class AuroBibtemplate extends LitElement {
19417
19509
  }
19418
19510
  }
19419
19511
 
19420
- var formkitVersion = '202606151650';
19512
+ var formkitVersion = '202606171946';
19421
19513
 
19422
19514
  var styleCss$1 = css`.util_displayInline{display:inline}.util_displayInlineBlock{display:inline-block}.util_displayBlock{display:block}.util_displayFlex{display:flex}.util_displayHidden{display:none}.util_displayHiddenVisually{position:absolute;overflow:hidden;clip:rect(1px, 1px, 1px, 1px);width:1px;height:1px;padding:0;border:0}:host{display:block;text-align:left}:host [auro-dropdown]{--ds-auro-dropdown-trigger-background-color: transparent}:host #inputInBib::part(wrapper){box-shadow:none}:host #inputInBib::part(accent-left){display:none}:host([layout*=classic]) [auro-input]{width:100%}:host([layout*=classic]) [auro-input]::part(helpText){display:none}:host([layout*=classic]) #slotHolder{display:none}`;
19423
19515
 
@@ -19774,6 +19866,25 @@ function normalizeFilterValue(value) {
19774
19866
  return raw.trim() ? raw.trimEnd() : raw;
19775
19867
  }
19776
19868
 
19869
+ /**
19870
+ * Returns the option's plain-text label suitable for input.value, with
19871
+ * HTML-indentation whitespace collapsed and the displayValue slot text
19872
+ * removed (the slot renders separately in the trigger).
19873
+ * @param {HTMLElement} option - An auro-menuoption element.
19874
+ * @returns {string} Normalized label.
19875
+ */
19876
+ function getOptionLabel(option) {
19877
+ if (!option) {
19878
+ return '';
19879
+ }
19880
+ const clone = option.cloneNode(true);
19881
+ const displayValueEl = clone.querySelector('[slot="displayValue"]');
19882
+ if (displayValueEl) {
19883
+ displayValueEl.remove();
19884
+ }
19885
+ return (clone.textContent || '').replace(/\s+/gu, ' ').trim();
19886
+ }
19887
+
19777
19888
  // See https://git.io/JJ6SJ for "How to document your components using JSDoc"
19778
19889
  /**
19779
19890
  * The `auro-combobox` element provides users with a way to select an option from a list of filtered or suggested options based on user input.
@@ -19901,7 +20012,19 @@ class AuroCombobox extends AuroElement {
19901
20012
  availableOptions: {
19902
20013
  state: true,
19903
20014
  type: Array,
19904
- reflect: false
20015
+ reflect: false,
20016
+ hasChanged(newVal, oldVal) {
20017
+ if (!oldVal && !newVal) {
20018
+ return false;
20019
+ }
20020
+ if (!oldVal || !newVal) {
20021
+ return true;
20022
+ }
20023
+ if (newVal.length !== oldVal.length) {
20024
+ return true;
20025
+ }
20026
+ return newVal.some((opt, idx) => opt !== oldVal[idx]);
20027
+ }
19905
20028
  },
19906
20029
 
19907
20030
  /**
@@ -20332,18 +20455,15 @@ class AuroCombobox extends AuroElement {
20332
20455
  matchString = `${matchString} ${option.getAttribute('suggest')}`.toLowerCase();
20333
20456
  }
20334
20457
 
20335
- // If input is empty, show all options (except static ones)
20336
- if (!filterValue) {
20337
- if (!option.hasAttribute('static')) {
20338
- option.removeAttribute('hidden');
20339
- this.availableOptions.push(option);
20340
- }
20341
- } else if (matchString.includes(filterValue) && !option.hasAttribute('static')) {
20342
- // only count options that match the typed input value AND are not static
20458
+ if (!filterValue && option.hasAttribute('static')) {
20459
+ // Static options are shown when the input is empty so consumers can
20460
+ // surface headers / "add new" rows when no filter is active.
20461
+ option.removeAttribute('hidden');
20462
+ this.availableOptions.push(option);
20463
+ } else if (filterValue && matchString.includes(filterValue) && !option.hasAttribute('static')) {
20343
20464
  option.removeAttribute('hidden');
20344
20465
  this.availableOptions.push(option);
20345
20466
  } else if (!option.hasAttribute('persistent')) {
20346
- // Hide all other non-persistent options
20347
20467
  option.setAttribute('hidden', '');
20348
20468
  option.removeAttribute('aria-setsize');
20349
20469
  option.removeAttribute('aria-posinset');
@@ -20351,7 +20471,9 @@ class AuroCombobox extends AuroElement {
20351
20471
  });
20352
20472
 
20353
20473
  if (this.availableOptions.length === 0) {
20354
- if (this.noMatchOption) {
20474
+ // Only surface the nomatch option once the user has actually typed
20475
+ // something. Empty input is handled by the bib being closed.
20476
+ if (this.noMatchOption && filterValue) {
20355
20477
  this.noMatchOption.removeAttribute('hidden');
20356
20478
  } else if (!this.menu.loading || this.isHiddenWhileLoading) {
20357
20479
  this.hideBib();
@@ -20367,11 +20489,14 @@ class AuroCombobox extends AuroElement {
20367
20489
  * @returns {void}
20368
20490
  */
20369
20491
  syncValuesAndStates() {
20370
- // Only sync matchWord, don't set menu.value here since setMenuValue should handle that
20492
+ // Do NOT call setMenuValue here the caller (handleMenuOptions) already
20493
+ // guards setMenuValue with an option-match check. Calling it unconditionally
20494
+ // sets free-form values on the menu, triggering clearSelection which resets
20495
+ // _index and breaks subsequent makeSelection calls.
20371
20496
  if (this.menu) {
20372
20497
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20373
20498
  }
20374
- const label = this.menu.currentLabel;
20499
+ const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20375
20500
  this.updateTriggerTextDisplay(label || this.value);
20376
20501
  }
20377
20502
 
@@ -20386,7 +20511,39 @@ class AuroCombobox extends AuroElement {
20386
20511
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20387
20512
 
20388
20513
  if (!this.persistInput && !(this.behavior === 'suggestion' && isInputFocusedWithNoSelection)) {
20389
- this.input.value = label || this.value;
20514
+ const nextValue = label || this.value;
20515
+ // Only set the flag when there's an actual write to suppress —
20516
+ // syncValuesAndStates re-enters here during typing when both inputs
20517
+ // already match, and a no-op flag flip would make the bib branch's
20518
+ // bail eat legitimate user-input events.
20519
+ const triggerNeedsSync = this.input.value !== nextValue;
20520
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20521
+ if (triggerNeedsSync || bibNeedsSync) {
20522
+ this._syncingDisplayValue = true;
20523
+ if (triggerNeedsSync) {
20524
+ this.input.value = nextValue;
20525
+ }
20526
+ if (bibNeedsSync) {
20527
+ this.inputInBib.value = nextValue;
20528
+ }
20529
+ const pending = [];
20530
+ if (triggerNeedsSync) {
20531
+ pending.push(this.input.updateComplete);
20532
+ }
20533
+ if (bibNeedsSync) {
20534
+ pending.push(this.inputInBib.updateComplete);
20535
+ }
20536
+ Promise.all(pending).then(() => {
20537
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20538
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20539
+ this.input.maskInstance.updateValue();
20540
+ }
20541
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20542
+ this.inputInBib.maskInstance.updateValue();
20543
+ }
20544
+ this._syncingDisplayValue = false;
20545
+ });
20546
+ }
20390
20547
  }
20391
20548
 
20392
20549
  // update the displayValue in the trigger if displayValue slot content is present
@@ -20400,6 +20557,14 @@ class AuroCombobox extends AuroElement {
20400
20557
  const displayValueEl = this.menu.optionSelected.querySelector("[slot='displayValue']");
20401
20558
  if (displayValueEl) {
20402
20559
  this.input.appendChild(displayValueEl.cloneNode(true));
20560
+ // auro-input's hasDisplayValueContent is non-reactive; nudge it to
20561
+ // re-evaluate the slot and re-render so the displayValue wrapper
20562
+ // gets its hasContent class. Without this, the apple/icon stays
20563
+ // hidden when input.value is set in the same tick as the append.
20564
+ if (typeof this.input.checkDisplayValueSlotChange === 'function') {
20565
+ this.input.checkDisplayValueSlotChange();
20566
+ this.input.requestUpdate();
20567
+ }
20403
20568
  }
20404
20569
  }
20405
20570
 
@@ -20433,7 +20598,10 @@ class AuroCombobox extends AuroElement {
20433
20598
  this.syncValuesAndStates();
20434
20599
  }
20435
20600
 
20436
- if (!this.availableOptions.includes(this.menu.optionActive)) {
20601
+ // Re-activate when optionActive is no longer visible, or when _index has
20602
+ // been reset (e.g. by clearSelection in an async update) so that
20603
+ // makeSelection() can find the correct option.
20604
+ if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20437
20605
  this.activateFirstEnabledAvailableOption();
20438
20606
  }
20439
20607
  }
@@ -20529,6 +20697,14 @@ class AuroCombobox extends AuroElement {
20529
20697
  // keyboard navigation continues from the correct place in the page
20530
20698
  if (this.dropdown.isBibFullscreen) {
20531
20699
  restoreTriggerAfterClose(this.dropdown, this.input);
20700
+
20701
+ // After the rAF inside restoreTriggerAfterClose() refocuses the
20702
+ // trigger, park the caret at end-of-text. Without this, the trigger
20703
+ // shows the new value (e.g. "Peaches") but its caret sits at [0, 0]
20704
+ // because nothing wrote to the native input between close and focus.
20705
+ doubleRaf(() => {
20706
+ this.setInputFocus();
20707
+ });
20532
20708
  }
20533
20709
  }
20534
20710
 
@@ -20558,12 +20734,27 @@ class AuroCombobox extends AuroElement {
20558
20734
 
20559
20735
  guardTouchPassthrough(this.menu);
20560
20736
 
20561
- // Wait for the bibtemplate to fully render across
20562
- // multiple Lit update cycles before moving focus into the bib
20737
+ // The dialog's showModal() steals focus from the trigger.
20738
+ // A single rAF is enough for the dialog DOM to be painted and
20739
+ // focusable, then doubleRaf finalizes the transition.
20740
+ requestAnimationFrame(() => {
20741
+ this.setInputFocus();
20742
+ });
20743
+
20563
20744
  doubleRaf(() => {
20564
20745
  this.setInputFocus();
20565
20746
  this._inFullscreenTransition = false;
20566
20747
  });
20748
+ } else {
20749
+ // Desktop popover-open: restore the trigger caret to end-of-text.
20750
+ // Clicking the trigger lands on auro-input's floating <label for="…">
20751
+ // overlay; Chrome resets the native input's selection to [0, 0] on
20752
+ // label-focus before any JS runs. setInputFocus()'s non-fullscreen
20753
+ // branch parks the caret at end. doubleRaf lets the dropdown layout
20754
+ // settle first, matching the fullscreen branch timing.
20755
+ doubleRaf(() => {
20756
+ this.setInputFocus();
20757
+ });
20567
20758
  }
20568
20759
  }
20569
20760
  });
@@ -20640,16 +20831,53 @@ class AuroCombobox extends AuroElement {
20640
20831
  */
20641
20832
  setInputFocus() {
20642
20833
  if (this.dropdown.isBibFullscreen && this.dropdown.isPopoverVisible) {
20643
- this.inputInBib.focus();
20834
+ // Sync the native input value synchronously before focusing.
20835
+ // Lit's property-to-attribute sync is async (microtask), so the
20836
+ // native <input> inside inputInBib may still hold a stale value
20837
+ // when focus moves here during the fullscreen transition. Without
20838
+ // this, keystrokes (e.g. Backspace) operate on the wrong content.
20839
+ const nativeInput = this.inputInBib.inputElement;
20840
+ const triggerNativeInput = this.input.inputElement;
20841
+ if (nativeInput && triggerNativeInput && nativeInput.value !== triggerNativeInput.value) {
20842
+ this.inputInBib.skipNextProgrammaticInputEvent = true;
20843
+ nativeInput.value = triggerNativeInput.value || '';
20844
+ }
20845
+
20846
+ // Focus the native input directly to ensure it receives keystrokes
20847
+ // after the fullscreen dialog opens. The dialog's showModal() may
20848
+ // have moved focus to the dialog element itself; focusing the
20849
+ // auro-input custom element doesn't always reach the native input
20850
+ // when the dialog DOM isn't fully painted.
20851
+ if (nativeInput) {
20852
+ nativeInput.focus();
20853
+ } else {
20854
+ this.inputInBib.focus();
20855
+ }
20644
20856
 
20645
20857
  // Place cursor at end of existing text so the user can continue editing
20646
- const nativeInput = this.inputInBib.inputElement;
20647
20858
  if (nativeInput && nativeInput.value) {
20648
20859
  const len = nativeInput.value.length;
20649
20860
  nativeInput.setSelectionRange(len, len);
20650
20861
  }
20651
20862
  } else {
20652
- this.input.focus();
20863
+ // Safety net for the strategy-change setTimeout: a viewport-crossing
20864
+ // fullscreen→floating switch can close the dialog and leave focus on
20865
+ // body. Every other Branch 2 callsite already has focus on the input
20866
+ // via the focusin → this.focus() redirect (L1448-1452), so this is a
20867
+ // no-op in the common case.
20868
+ if (!this.input.componentHasFocus) {
20869
+ this.input.focus();
20870
+ }
20871
+
20872
+ // Park the trigger native input's caret at end-of-text every time the
20873
+ // popover opens. Chrome focuses the native via the floating <label for="…">
20874
+ // overlay on click and resets selection to [0, 0]; no JS fires between
20875
+ // mousedown and selectionchange, so we restore the caret here.
20876
+ const triggerNativeInput = this.input.inputElement;
20877
+ if (triggerNativeInput && triggerNativeInput.value) {
20878
+ const len = triggerNativeInput.value.length;
20879
+ triggerNativeInput.setSelectionRange(len, len);
20880
+ }
20653
20881
  }
20654
20882
  }
20655
20883
 
@@ -20745,53 +20973,92 @@ class AuroCombobox extends AuroElement {
20745
20973
  this.menu.setAttribute('nocheckmark', '');
20746
20974
  }
20747
20975
 
20748
- this.menu.addEventListener("auroMenu-deselectPrevented", () => {
20749
- this.hideBib();
20750
- });
20976
+ // Handle menu option selection
20977
+ this.menu.addEventListener('auroMenu-selectedOption', (evt) => {
20978
+ // Capture before the consumption below — true means this event is the
20979
+ // echo of our own setMenuValue (programmatic value sync), false means
20980
+ // a fresh user selection.
20981
+ const isEcho = this._pendingMenuValueSync === true;
20751
20982
 
20752
- // Handle menu option selection like select does
20753
- this.menu.addEventListener('auroMenu-selectedOption', (event) => {
20754
- // Update the optionSelected from the event details, not manually
20755
- [this.optionSelected] = event.detail.options;
20983
+ if (this.menu.optionSelected) {
20984
+ const selected = this.menu.optionSelected;
20756
20985
 
20757
- // Update the internal value to match the menu's value
20758
- this.value = event.detail.stringValue;
20986
+ if (!this.optionSelected || this.optionSelected !== selected) {
20987
+ this.optionSelected = selected;
20988
+ }
20759
20989
 
20760
- // Update display
20761
- this.updateTriggerTextDisplay(event.detail.label || event.detail.value);
20990
+ // Skip writeback when this event is the echo of our own setMenuValue —
20991
+ // otherwise it cascades against handleInputValueChange in suggestion mode.
20992
+ if (this._pendingMenuValueSync) {
20993
+ this._pendingMenuValueSync = false;
20994
+ } else if (!this.value || this.value !== this.optionSelected.value) {
20995
+ this.value = this.optionSelected.value;
20996
+ }
20762
20997
 
20763
- // Update match word for filtering
20764
- const trimmedInput = normalizeFilterValue(this.input.value);
20765
- if (this.menu.matchWord !== trimmedInput) {
20766
- this.menu.matchWord = trimmedInput;
20767
- }
20998
+ // Update display
20999
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
20768
21000
 
20769
- // Update available options based on selection
20770
- this.handleMenuOptions();
21001
+ // Update match word for filtering
21002
+ const trimmedInput = normalizeFilterValue(this.input.value);
21003
+ if (this.menu.matchWord !== trimmedInput) {
21004
+ this.menu.matchWord = trimmedInput;
21005
+ }
21006
+ }
20771
21007
 
20772
21008
  // Hide dropdown on selection (except during slot changes)
20773
- if (event.detail && event.detail.source !== 'slotchange') {
20774
- setTimeout(() => {
20775
- // do not close while typing in suggestion mode with no value selected, to allow freeform input
20776
- if (this.menu.value || this.behavior !== 'suggestion') {
20777
- this.hideBib();
20778
- }
20779
- }, 0);
21009
+ if (evt.detail && evt.detail.source !== 'slotchange') {
21010
+ // do not close while typing in suggestion mode with no value selected, to allow freeform input
21011
+ if (this.menu.value || this.behavior !== 'suggestion') {
21012
+ this.hideBib();
21013
+ }
20780
21014
 
20781
21015
  // Announce the selection after the dropdown closes so it isn't
20782
21016
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
20783
- const selectedValue = event.detail.stringValue;
21017
+ const selectedValue = this.menu.value;
20784
21018
  const announcementDelay = 300;
20785
21019
  setTimeout(() => {
20786
21020
  announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
20787
21021
  }, announcementDelay);
20788
21022
  }
21023
+
21024
+ // Programmatic value syncs leave availableOptions stale because
21025
+ // updateTriggerTextDisplay sets input.value with _syncingDisplayValue
21026
+ // and handleInputValueChange bails. Refresh the filter on echo events
21027
+ // only — fresh user selections take the existing hideBib path.
21028
+ if (isEcho && this.menu.optionSelected) {
21029
+ this._programmaticFilterRefresh = true;
21030
+ this.handleMenuOptions();
21031
+ setTimeout(() => {
21032
+ this._programmaticFilterRefresh = false;
21033
+ }, 0);
21034
+ }
21035
+
21036
+ // base-input skips auto-validate when focus is inside its own shadow,
21037
+ // which it is after setTriggerInputFocus — re-run so prior tooShort
21038
+ // clears. processCreditCard reasserts errorMessage on every render.
21039
+ if (!isEcho && this.menu.optionSelected && this.input.validate) {
21040
+ this.input.updateComplete.then(() => {
21041
+ this.input.validate(true);
21042
+ if (this.input.validity === 'valid') {
21043
+ this.input.errorMessage = '';
21044
+ }
21045
+ });
21046
+ }
20789
21047
  });
20790
21048
 
20791
21049
  this.menu.addEventListener('auroMenu-customEventFired', () => {
20792
21050
  this.hideBib();
20793
21051
  });
20794
21052
 
21053
+ // When the menu cannot match a programmatic value to any option,
21054
+ // clear the combobox selection state so it doesn't reference a
21055
+ // stale option. Safe from re-entrancy because any resulting
21056
+ // input.value changes dispatch isProgrammatic events.
21057
+ this.menu.addEventListener('auroMenu-selectValueFailure', () => {
21058
+ this.value = undefined;
21059
+ this.optionSelected = undefined;
21060
+ });
21061
+
20795
21062
  this.menu.addEventListener('auroMenu-activatedOption', (evt) => {
20796
21063
  this.optionActive = evt.detail;
20797
21064
 
@@ -20892,11 +21159,12 @@ class AuroCombobox extends AuroElement {
20892
21159
  // event from the trigger. The _syncingBibValue guard persists across the
20893
21160
  // async boundary and prevents that re-entrant event from running the
20894
21161
  // non-fullscreen path (which would call clear() → hideBib()).
20895
- // When the event comes from the fullscreen bib input, sync the value to
20896
- // the trigger and run filtering, but suppress the re-entrant input event
20897
- // that the trigger fires (via Lit updated() → notifyValueChanged()) so
20898
- // the non-fullscreen hide/clear logic doesn't close the dialog.
20899
21162
  if (event.target === this.inputInBib) {
21163
+ // Mirror the trigger-branch bail below — programmatic inputInBib writes
21164
+ // notify back into this branch via Lit's notifyValueChanged.
21165
+ if (this._syncingDisplayValue) {
21166
+ return;
21167
+ }
20900
21168
  this._syncingBibValue = true;
20901
21169
  this.input.value = this.inputInBib.value;
20902
21170
  this.input.updateComplete.then(() => {
@@ -20906,18 +21174,37 @@ class AuroCombobox extends AuroElement {
20906
21174
  // Run filtering inline — the re-entrant event won't reach this code.
20907
21175
  this.menu.matchWord = normalizeFilterValue(this.inputInBib.value);
20908
21176
  this.optionActive = null;
21177
+
21178
+ // In suggestion mode, keep the combobox value in sync with the typed
21179
+ // text so that freeform values are captured (mirroring the non-fullscreen
21180
+ // path). Clear the selection when the input is emptied.
21181
+ if (this.behavior === 'suggestion') {
21182
+ this.value = this.inputInBib.value || undefined;
21183
+ }
21184
+
20909
21185
  this.handleMenuOptions();
20910
21186
  this.dispatchEvent(new CustomEvent('inputValue', { detail: { value: this.inputValue } }));
20911
21187
  return;
20912
21188
  }
20913
21189
 
20914
- // Ignore re-entrant input events caused by the bib→trigger sync above.
20915
- if (this._syncingBibValue) {
21190
+ // Ignore re-entrant input events caused by programmatic value sets.
21191
+ if (this._syncingBibValue || this._syncingDisplayValue) {
20916
21192
  return;
20917
21193
  }
20918
21194
 
20919
21195
  this.inputInBib.value = this.input.value;
20920
21196
 
21197
+ // Also sync the native input immediately so keystrokes arriving
21198
+ // before Lit's async update cycle (e.g. rapid Backspaces during a
21199
+ // fullscreen transition) operate on the correct content.
21200
+ // Skip the next programmatic input event to prevent the patched setter
21201
+ // from re-entering handleInputValueChange via the bib path.
21202
+ const bibNativeInput = this.inputInBib.inputElement;
21203
+ if (bibNativeInput && bibNativeInput.value !== this.input.value) {
21204
+ this.inputInBib.skipNextProgrammaticInputEvent = true;
21205
+ bibNativeInput.value = this.input.value || '';
21206
+ }
21207
+
20921
21208
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20922
21209
  this.optionActive = null;
20923
21210
 
@@ -20925,7 +21212,7 @@ class AuroCombobox extends AuroElement {
20925
21212
  this.value = this.input.value;
20926
21213
  }
20927
21214
 
20928
- if (!this.input.value) {
21215
+ if (!this.input.value && !this._clearing) {
20929
21216
  this.clear();
20930
21217
  }
20931
21218
  this.handleMenuOptions();
@@ -20994,6 +21281,16 @@ class AuroCombobox extends AuroElement {
20994
21281
  return;
20995
21282
  }
20996
21283
 
21284
+ // Ignore dispatches from the bib (fullscreen) input. It's re-validated
21285
+ // inside this.validate()'s auroInputElements loop with its own
21286
+ // (often undefined) validity, and the event is composed/bubbles up to
21287
+ // this listener with `target` retargeted to the combobox. Letting it
21288
+ // through would overwrite the trigger input's correct validity with
21289
+ // the bib input's stale one (e.g. wiping `tooShort` during typing).
21290
+ if (this.inputInBib && evt.composedPath()[0] === this.inputInBib && this.inputInBib !== this.input) {
21291
+ return;
21292
+ }
21293
+
20997
21294
  this.input.validity = evt.detail.validity;
20998
21295
  this.input.errorMessage = evt.detail.message;
20999
21296
  this.validity = evt.detail.validity;
@@ -21055,10 +21352,18 @@ class AuroCombobox extends AuroElement {
21055
21352
  * @returns {void}
21056
21353
  */
21057
21354
  setMenuValue(value) {
21058
- if (!this.menu) {
21355
+ if (!this.menu || this.menu.value === value) {
21059
21356
  return;
21060
21357
  }
21358
+ // One-shot flag consumed by the auroMenu-selectedOption listener so the
21359
+ // echo of this sync doesn't propagate back to this.value.
21360
+ this._pendingMenuValueSync = true;
21061
21361
  this.menu.value = value;
21362
+ // Backup clear: if menu.value === menu.optionSelected.value already, the
21363
+ // listener won't fire and the flag would swallow the next user click.
21364
+ setTimeout(() => {
21365
+ this._pendingMenuValueSync = false;
21366
+ }, 0);
21062
21367
  }
21063
21368
 
21064
21369
  /**
@@ -21073,6 +21378,15 @@ class AuroCombobox extends AuroElement {
21073
21378
  this.menu.value = undefined;
21074
21379
  this.validation.reset(this);
21075
21380
  this.touched = false;
21381
+ // Force validity back to the cleared state. validation.reset() calls
21382
+ // validate() at the end, and (post-credit-card-fix) the trigger input's
21383
+ // residual validity may still be copied into the combobox by the
21384
+ // auroInputElements loop. Explicitly clear here so reset() actually
21385
+ // leaves the component in a "no validity" state.
21386
+ this.validity = undefined;
21387
+ this.errorMessage = '';
21388
+ this.input.validity = undefined;
21389
+ this.input.errorMessage = '';
21076
21390
  }
21077
21391
 
21078
21392
  /**
@@ -21080,16 +21394,24 @@ class AuroCombobox extends AuroElement {
21080
21394
  * @returns {void}
21081
21395
  */
21082
21396
  clear() {
21083
- // Clear combobox state first
21084
- this.optionSelected = undefined;
21085
- this.value = undefined;
21086
-
21087
- // Then clear input and menu
21088
- if (this.input.value) {
21089
- this.input.clear();
21397
+ // input.clear() fires an input event that re-enters handleInputValueChange
21398
+ // → which calls clear() again on empty input. Guard against that loop.
21399
+ if (this._clearing) {
21400
+ return;
21090
21401
  }
21091
- if (this.menu.value || this.menu.optionSelected) {
21092
- this.menu.reset();
21402
+ this._clearing = true;
21403
+ try {
21404
+ this.optionSelected = undefined;
21405
+ this.value = undefined;
21406
+
21407
+ if (this.input.value) {
21408
+ this.input.clear();
21409
+ }
21410
+ if (this.menu.value || this.menu.optionSelected) {
21411
+ this.menu.reset();
21412
+ }
21413
+ } finally {
21414
+ this._clearing = false;
21093
21415
  }
21094
21416
  }
21095
21417
 
@@ -21117,29 +21439,64 @@ class AuroCombobox extends AuroElement {
21117
21439
  this.input.value = this.value;
21118
21440
  }
21119
21441
 
21120
- if (this.menu && this.hasValue && this.menu.options) {
21121
- this.menu.options.forEach((opt) => {
21122
- if (!opt.hasAttribute('static')) {
21123
- opt.removeAttribute('hidden');
21124
- }
21125
- });
21126
- }
21127
-
21128
- if (this.behavior === 'suggestion') {
21129
- if (!this.menu.options || this.menu.options.length === 0) ; else if (this.menu.options.filter((opt) => opt.value === this.value).length > 0) {
21442
+ // Sync menu.value only when an option actually matches; otherwise clear it.
21443
+ if (this.menu.options && this.menu.options.length > 0) {
21444
+ if (this.menu.options.some((opt) => opt.value === this.value)) {
21445
+ this.setMenuValue(this.value);
21446
+ } else if (this.behavior === 'filter') {
21447
+ // In filter mode, freeform values aren't allowed. Push the unmatched
21448
+ // value through setMenuValue so menu dispatches auroMenu-selectValueFailure
21449
+ // and the listener at line 1206 clears combobox.value (mirrors select's
21450
+ // pattern). Bypassing setMenuValue here would skip the failure cascade.
21130
21451
  this.setMenuValue(this.value);
21131
21452
  } else {
21132
- this.menu.value = undefined;
21133
- // Sync the input display for freeform values that don't match any option
21134
- this.input.value = this.value;
21135
- }
21136
- } else {
21137
- // Use setMenuValue like select does instead of direct assignment
21138
- this.setMenuValue(this.value);
21139
- if (!this.value) {
21140
- this.clear();
21453
+ if (this.menu.value) {
21454
+ this.menu.value = undefined;
21455
+ }
21456
+ // Sync the display for programmatic freeform value changes (e.g. the
21457
+ // swap-values pattern). Guard with _syncingDisplayValue so that the
21458
+ // re-entrant input event from base-input.notifyValueChanged() is
21459
+ // ignored by handleInputValueChange. Skip the sync when input already
21460
+ // matches to avoid spurious events during the normal typing path.
21461
+ const nextValue = this.value || '';
21462
+ const triggerNeedsSync = this.input.value !== nextValue;
21463
+ const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
21464
+ if (triggerNeedsSync || bibNeedsSync) {
21465
+ this._syncingDisplayValue = true;
21466
+ if (triggerNeedsSync) {
21467
+ this.input.value = nextValue;
21468
+ }
21469
+ if (bibNeedsSync) {
21470
+ this.inputInBib.value = nextValue;
21471
+ }
21472
+ const pending = [];
21473
+ if (triggerNeedsSync) {
21474
+ pending.push(this.input.updateComplete);
21475
+ }
21476
+ if (bibNeedsSync) {
21477
+ pending.push(this.inputInBib.updateComplete);
21478
+ }
21479
+ Promise.all(pending).then(() => {
21480
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
21481
+ this.input.maskInstance.updateValue();
21482
+ }
21483
+ if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
21484
+ this.inputInBib.maskInstance.updateValue();
21485
+ }
21486
+ this._syncingDisplayValue = false;
21487
+ // handleInputValueChange bailed on the flag — refresh the filter.
21488
+ this._programmaticFilterRefresh = true;
21489
+ this.handleMenuOptions();
21490
+ setTimeout(() => {
21491
+ this._programmaticFilterRefresh = false;
21492
+ }, 0);
21493
+ });
21494
+ }
21141
21495
  }
21142
21496
  }
21497
+ if (!this.value) {
21498
+ this.clear();
21499
+ }
21143
21500
  if (this.value && !this.componentHasFocus) {
21144
21501
  // If the value got set programmatically make sure we hide the bib
21145
21502
  // when input is not taking the focus (input can be in dropdown.trigger or in bibtemplate)
@@ -21176,12 +21533,18 @@ class AuroCombobox extends AuroElement {
21176
21533
  // branch from calling hideBib() when the dropdown was just opened but
21177
21534
  // :focus-within hasn't propagated through the top-layer dialog's nested
21178
21535
  // shadow DOM boundaries.
21179
- if ((this.availableOptions.length > 0 && (this.componentHasFocus || this.dropdownOpen)) || (this.menu && this.menu.loading) || (this.availableOptions.length === 0 && this.noMatchOption)) {
21536
+ // Skip the show/hide side effects when the availableOptions change came
21537
+ // from a programmatic filter refresh after a value swap — the user
21538
+ // didn't interact, so we shouldn't auto-open the bib (especially for
21539
+ // the no-match path where the existing condition unconditionally fires).
21540
+ if (this._programmaticFilterRefresh) {
21541
+ this._programmaticFilterRefresh = false;
21542
+ } else if ((this.availableOptions.length > 0 && (this.componentHasFocus || this.dropdownOpen)) || (this.menu && this.menu.loading) || (this.availableOptions.length === 0 && this.noMatchOption)) {
21180
21543
  this.showBib();
21181
21544
  if (!this.availableOptions.includes(this.menu.optionActive)) {
21182
21545
  this.activateFirstEnabledAvailableOption();
21183
21546
  }
21184
- } else {
21547
+ } else if (this.dropdown && this.dropdown.isPopoverVisible) {
21185
21548
  this.hideBib();
21186
21549
  }
21187
21550
  }