@aurodesignsystem-dev/auro-formkit 0.0.0-pr1516.0 → 0.0.0-pr1519.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 (54) hide show
  1. package/components/checkbox/demo/customize.min.js +18 -10
  2. package/components/checkbox/demo/getting-started.min.js +18 -10
  3. package/components/checkbox/demo/index.min.js +18 -10
  4. package/components/checkbox/dist/index.js +18 -10
  5. package/components/checkbox/dist/registered.js +18 -10
  6. package/components/combobox/README.md +1 -1
  7. package/components/combobox/demo/customize.md +2 -10
  8. package/components/combobox/demo/customize.min.js +454 -467
  9. package/components/combobox/demo/getting-started.min.js +454 -463
  10. package/components/combobox/demo/index.md +1 -1
  11. package/components/combobox/demo/index.min.js +454 -463
  12. package/components/combobox/demo/keyboard-behavior.md +2 -142
  13. package/components/combobox/demo/readme.md +1 -1
  14. package/components/combobox/demo/why-combobox.md +2 -2
  15. package/components/combobox/dist/auro-combobox.d.ts +30 -14
  16. package/components/combobox/dist/index.js +454 -463
  17. package/components/combobox/dist/registered.js +454 -463
  18. package/components/counter/demo/customize.min.js +32 -12
  19. package/components/counter/demo/index.min.js +32 -12
  20. package/components/counter/dist/index.js +32 -12
  21. package/components/counter/dist/registered.js +32 -12
  22. package/components/datepicker/demo/customize.min.js +183 -167
  23. package/components/datepicker/demo/index.min.js +183 -167
  24. package/components/datepicker/dist/index.js +183 -167
  25. package/components/datepicker/dist/registered.js +183 -167
  26. package/components/dropdown/demo/customize.min.js +14 -2
  27. package/components/dropdown/demo/getting-started.min.js +14 -2
  28. package/components/dropdown/demo/index.min.js +14 -2
  29. package/components/dropdown/dist/index.js +14 -2
  30. package/components/dropdown/dist/registered.js +14 -2
  31. package/components/form/demo/customize.min.js +851 -829
  32. package/components/form/demo/getting-started.min.js +851 -829
  33. package/components/form/demo/index.min.js +851 -829
  34. package/components/form/demo/registerDemoDeps.min.js +851 -829
  35. package/components/input/demo/api.md +58 -57
  36. package/components/input/demo/customize.min.js +114 -155
  37. package/components/input/demo/getting-started.min.js +114 -155
  38. package/components/input/demo/index.min.js +114 -155
  39. package/components/input/dist/base-input.d.ts +9 -51
  40. package/components/input/dist/index.js +114 -155
  41. package/components/input/dist/registered.js +114 -155
  42. package/components/input/dist/utilities.d.ts +9 -0
  43. package/components/radio/demo/customize.min.js +18 -10
  44. package/components/radio/demo/getting-started.min.js +18 -10
  45. package/components/radio/demo/index.min.js +18 -10
  46. package/components/radio/dist/index.js +18 -10
  47. package/components/radio/dist/registered.js +18 -10
  48. package/components/select/demo/customize.min.js +32 -12
  49. package/components/select/demo/getting-started.min.js +32 -12
  50. package/components/select/demo/index.min.js +32 -12
  51. package/components/select/dist/index.js +32 -12
  52. package/components/select/dist/registered.js +32 -12
  53. package/custom-elements.json +142 -278
  54. package/package.json +1 -1
@@ -402,11 +402,17 @@ let AuroFormValidation$1 = class AuroFormValidation {
402
402
  return;
403
403
  }
404
404
 
405
- // Validate that the date passed was the correct format and is a valid date
405
+ // Validate that the date passed was the correct format and is a valid date.
406
+ // For partial date formats, valueObject is never populated; validate them directly.
406
407
  if (elem.value && !elem.valueObject) {
407
- elem.validity = 'patternMismatch';
408
- elem.errorMessage = elem.setCustomValidityPatternMismatch || elem.setCustomValidity || 'Invalid Date Format Entered';
409
- return;
408
+ const isPartialDateFormat = elem.util && !elem.util.isFullDateFormat(elem.type, elem.format);
409
+ const isValidPartial = isPartialDateFormat && elem.util.isValidPartialDate(elem.value, elem.format);
410
+
411
+ if (!isValidPartial) {
412
+ elem.validity = 'patternMismatch';
413
+ elem.errorMessage = elem.setCustomValidityPatternMismatch || elem.setCustomValidity || 'Invalid Date Format Entered';
414
+ return;
415
+ }
410
416
  }
411
417
 
412
418
  // Perform the rest of the validation
@@ -500,15 +506,17 @@ let AuroFormValidation$1 = class AuroFormValidation {
500
506
  );
501
507
  }
502
508
 
503
- // If there is a second input in the elem and that value is undefined or an empty string set hasValue to false;
504
- if (this.auroInputElements?.length === 2) {
505
- if (!this.auroInputElements[1].value || this.auroInputElements[1].length === 0) {
509
+ const isCombobox = this.runtimeUtils.elementMatch(elem, 'auro-combobox');
510
+
511
+ // If there is a second input in the elem and that value is undefined or an empty string set hasValue to false.
512
+ // Skip for combobox: its second auro-input is the fullscreen-bib mirror of the same value, not an independent
513
+ // field (datepicker is the intended consumer — start/end are independently required).
514
+ if (this.auroInputElements?.length === 2 && !isCombobox) {
515
+ if (!this.auroInputElements[1].value || this.auroInputElements[1].value.length === 0) {
506
516
  hasValue = false;
507
517
  }
508
518
  }
509
519
 
510
- const isCombobox = this.runtimeUtils.elementMatch(elem, 'auro-combobox');
511
-
512
520
  if (isCombobox) {
513
521
 
514
522
  if (!elem.persistInput || elem.behavior === "filter") {
@@ -825,6 +833,8 @@ function navigateArrow(component, direction, options = {}) {
825
833
  }
826
834
  }
827
835
 
836
+ /* eslint-disable no-underscore-dangle */
837
+
828
838
  /**
829
839
  * Returns the clear button element from the active input's shadow
830
840
  * DOM, if available.
@@ -860,11 +870,9 @@ function isClearBtnFocused(ctx, clearBtn = getClearBtn(ctx)) {
860
870
  * @param {Object} menu - The menu component.
861
871
  */
862
872
  function reconcileMenuIndex(menu) {
863
- // eslint-disable-next-line no-underscore-dangle
864
873
  if (menu._index < 0 && menu.optionActive && menu.items) {
865
874
  const idx = menu.items.indexOf(menu.optionActive);
866
875
  if (idx >= 0) {
867
- // eslint-disable-next-line no-underscore-dangle
868
876
  menu._index = idx;
869
877
  }
870
878
  }
@@ -883,11 +891,7 @@ const comboboxKeyboardStrategy = {
883
891
 
884
892
  // navigate if bib is open otherwise open it
885
893
  if (component.dropdown.isPopoverVisible) {
886
- if (evt.altKey || evt.ctrlKey || evt.metaKey) {
887
- component.activateLastEnabledAvailableOption();
888
- } else {
889
- navigateArrow(component, 'down');
890
- }
894
+ navigateArrow(component, 'down');
891
895
  } else {
892
896
  component.showBib();
893
897
  }
@@ -906,11 +910,7 @@ const comboboxKeyboardStrategy = {
906
910
 
907
911
  // navigate if bib is open otherwise open it
908
912
  if (component.dropdown.isPopoverVisible) {
909
- if (evt.altKey || evt.ctrlKey || evt.metaKey) {
910
- component.activateFirstEnabledAvailableOption();
911
- } else {
912
- navigateArrow(component, 'up');
913
- }
913
+ navigateArrow(component, 'up');
914
914
  } else {
915
915
  component.showBib();
916
916
  }
@@ -926,13 +926,18 @@ const comboboxKeyboardStrategy = {
926
926
  },
927
927
 
928
928
  Enter(component, evt, ctx) {
929
+ // Forms should not submit on Enter from a combobox, regardless of which
930
+ // child element (input, clear button, menu) is focused.
931
+ evt.stopPropagation();
932
+
929
933
  if (isClearBtnFocused(ctx)) {
930
- // If the clear button has focus, let the browser activate it normally.
931
- // stopPropagation prevents parent containers (e.g., forms) from treating
932
- // Enter as a submit, but we must NOT call preventDefault — that would
933
- // block the browser's built-in "Enter activates focused button" behavior.
934
- evt.stopPropagation();
935
- } else if (ctx.isExpanded && component.menu.optionActive) {
934
+ // Let the browser dispatch Enter to the focused clear button so its
935
+ // built-in activation fires and clears the selection. Do NOT call
936
+ // preventDefault — that would block the activation.
937
+ return;
938
+ }
939
+
940
+ if (ctx.isExpanded && component.menu.optionActive) {
936
941
  reconcileMenuIndex(component.menu);
937
942
  component.menu.makeSelection();
938
943
 
@@ -941,14 +946,8 @@ const comboboxKeyboardStrategy = {
941
946
  }
942
947
 
943
948
  evt.preventDefault();
944
- evt.stopPropagation();
945
949
  } else {
946
- // Prevent the keypress from bubbling to parent containers (e.g., forms)
947
- // which could interpret Enter as a submit or trigger other unintended behavior.
948
- // This is safe because showBib() opens the dialog programmatically,
949
- // not via event propagation.
950
950
  evt.preventDefault();
951
- evt.stopPropagation();
952
951
  component.showBib();
953
952
  }
954
953
  },
@@ -993,7 +992,7 @@ const comboboxKeyboardStrategy = {
993
992
  component.setClearBtnFocus();
994
993
  }
995
994
  }
996
- },
995
+ }
997
996
  };
998
997
 
999
998
  /**
@@ -4888,7 +4887,7 @@ let AuroHelpText$2 = class AuroHelpText extends i$3 {
4888
4887
  }
4889
4888
  };
4890
4889
 
4891
- var formkitVersion$2 = '202606292156';
4890
+ var formkitVersion$2 = '202607011722';
4892
4891
 
4893
4892
  let AuroElement$2 = class AuroElement extends i$3 {
4894
4893
  static get properties() {
@@ -5640,7 +5639,19 @@ class AuroDropdown extends AuroElement$2 {
5640
5639
  if (!this.isPopoverVisible && this.hasFocus && eventType === "keydown") {
5641
5640
  this.trigger.focus();
5642
5641
  }
5643
- }
5642
+
5643
+
5644
+ if (!this.isPopoverVisible) {
5645
+ // wait til the bib gets fully closed and rendered
5646
+ setTimeout(() => {
5647
+ // check if it's still closed and the focus is still within the dropdown, but not in the bib. If so, move focus to the trigger.
5648
+ if (!this.isPopoverVisible || !this.bib.matches(':focus-within')) {
5649
+ return;
5650
+ }
5651
+ // Move focus out of bib into trigger.
5652
+ this.trigger.focus();
5653
+ });
5654
+ } }
5644
5655
 
5645
5656
  firstUpdated() {
5646
5657
  // Configure the floater to, this will generate the ID for the bib
@@ -10572,11 +10583,17 @@ class AuroFormValidation {
10572
10583
  return;
10573
10584
  }
10574
10585
 
10575
- // Validate that the date passed was the correct format and is a valid date
10586
+ // Validate that the date passed was the correct format and is a valid date.
10587
+ // For partial date formats, valueObject is never populated; validate them directly.
10576
10588
  if (elem.value && !elem.valueObject) {
10577
- elem.validity = 'patternMismatch';
10578
- elem.errorMessage = elem.setCustomValidityPatternMismatch || elem.setCustomValidity || 'Invalid Date Format Entered';
10579
- return;
10589
+ const isPartialDateFormat = elem.util && !elem.util.isFullDateFormat(elem.type, elem.format);
10590
+ const isValidPartial = isPartialDateFormat && elem.util.isValidPartialDate(elem.value, elem.format);
10591
+
10592
+ if (!isValidPartial) {
10593
+ elem.validity = 'patternMismatch';
10594
+ elem.errorMessage = elem.setCustomValidityPatternMismatch || elem.setCustomValidity || 'Invalid Date Format Entered';
10595
+ return;
10596
+ }
10580
10597
  }
10581
10598
 
10582
10599
  // Perform the rest of the validation
@@ -10670,15 +10687,17 @@ class AuroFormValidation {
10670
10687
  );
10671
10688
  }
10672
10689
 
10673
- // If there is a second input in the elem and that value is undefined or an empty string set hasValue to false;
10674
- if (this.auroInputElements?.length === 2) {
10675
- if (!this.auroInputElements[1].value || this.auroInputElements[1].length === 0) {
10690
+ const isCombobox = this.runtimeUtils.elementMatch(elem, 'auro-combobox');
10691
+
10692
+ // If there is a second input in the elem and that value is undefined or an empty string set hasValue to false.
10693
+ // Skip for combobox: its second auro-input is the fullscreen-bib mirror of the same value, not an independent
10694
+ // field (datepicker is the intended consumer — start/end are independently required).
10695
+ if (this.auroInputElements?.length === 2 && !isCombobox) {
10696
+ if (!this.auroInputElements[1].value || this.auroInputElements[1].value.length === 0) {
10676
10697
  hasValue = false;
10677
10698
  }
10678
10699
  }
10679
10700
 
10680
- const isCombobox = this.runtimeUtils.elementMatch(elem, 'auro-combobox');
10681
-
10682
10701
  if (isCombobox) {
10683
10702
 
10684
10703
  if (!elem.persistInput || elem.behavior === "filter") {
@@ -16578,6 +16597,42 @@ class AuroInputUtilities {
16578
16597
  return type === 'date' && normalizedFormat.includes('yy') && normalizedFormat.includes('mm') && normalizedFormat.includes('dd');
16579
16598
  }
16580
16599
 
16600
+ /**
16601
+ * Validates a value against a partial date format (one that lacks yy/mm/dd all three).
16602
+ * Single-component formats are checked as integer ranges; multi-component formats use
16603
+ * a date-fns parse + round-trip to confirm both validity and exact formatting.
16604
+ * @param {string} value - The user-facing display value.
16605
+ * @param {string} format - The partial date format string (e.g. "mm/yyyy", "yyyy", "dd").
16606
+ * @returns {boolean}
16607
+ */
16608
+ isValidPartialDate(value, format$1) {
16609
+ if (!value || !format$1) {
16610
+ return false;
16611
+ }
16612
+ const normalizedFormat = format$1.toLowerCase();
16613
+
16614
+ if (normalizedFormat === 'dd') {
16615
+ const num = Number(value);
16616
+ return (/^\d{2}$/u).test(value) && num >= 1 && num <= 31;
16617
+ }
16618
+ if (normalizedFormat === 'yy') {
16619
+ const num = Number(value);
16620
+ return (/^\d{2}$/u).test(value) && num >= 0 && num <= 99;
16621
+ }
16622
+ if (normalizedFormat === 'yyyy') {
16623
+ const num = Number(value);
16624
+ return (/^\d{4}$/u).test(value) && num >= 1 && num <= 9999;
16625
+ }
16626
+
16627
+ const dateFnsMask = this.toDateFnsMask(normalizedFormat);
16628
+ // Use the 1st of the current month as the reference so that formats
16629
+ // omitting a day (e.g. MM/yyyy) never roll over on days 29–31.
16630
+ const referenceDate = new Date();
16631
+ referenceDate.setDate(1);
16632
+ const parsed = parse(value, dateFnsMask, referenceDate);
16633
+ return isValid(parsed) && format(parsed, dateFnsMask) === value;
16634
+ }
16635
+
16581
16636
  /**
16582
16637
  * Converts a display string to its model value.
16583
16638
  * For full date formats, converts the display string to an ISO date string.
@@ -16625,6 +16680,7 @@ class AuroInputUtilities {
16625
16680
  const maskOptions = this.getMaskOptions('date', normalizedFormat);
16626
16681
 
16627
16682
  if (!(valueObject instanceof Date) || Number.isNaN(valueObject.getTime()) || !maskOptions || typeof maskOptions.format !== 'function') {
16683
+ console.debug('Invalid date object or mask options for formatting', { valueObject, maskOptions }); // eslint-disable-line no-console
16628
16684
  return undefined;
16629
16685
  }
16630
16686
 
@@ -16782,6 +16838,7 @@ class BaseInput extends AuroElement$1 {
16782
16838
  // so a parent (datepicker/combobox) calling `validate()` synchronously
16783
16839
  // during its own update cycle sees a populated util instance.
16784
16840
  this.activeLabel = false;
16841
+
16785
16842
  /** @private */
16786
16843
  this.allowedInputTypes = [
16787
16844
  "text",
@@ -16792,6 +16849,7 @@ class BaseInput extends AuroElement$1 {
16792
16849
  "tel"
16793
16850
  ];
16794
16851
  this.appearance = "default";
16852
+
16795
16853
  /** @private */
16796
16854
  this.dateFormatMap = {
16797
16855
  'mm/dd/yyyy': 'dateMMDDYYYY',
@@ -16810,23 +16868,24 @@ class BaseInput extends AuroElement$1 {
16810
16868
  'mm/dd': 'dateMMDD'
16811
16869
  };
16812
16870
  this.disabled = false;
16871
+
16813
16872
  /** @private */
16814
16873
  this.domHandler = new DomHandler();
16815
16874
  this.dvInputOnly = false;
16816
16875
  this.hasValue = false;
16817
16876
  this.hideLabelVisually = false;
16818
16877
  this.icon = false;
16878
+
16819
16879
  /** @private */
16820
16880
  this.inputIconName = undefined;
16881
+
16821
16882
  /** @private */
16822
16883
  this.label = 'Input label is undefined';
16823
16884
  this.layout = 'classic';
16824
16885
  this.locale = 'en-US';
16825
16886
  this.max = undefined;
16826
- this._maxObject = undefined;
16827
16887
  this.maxLength = undefined;
16828
16888
  this.min = undefined;
16829
- this._minObject = undefined;
16830
16889
  this.minLength = undefined;
16831
16890
  this.noValidate = false;
16832
16891
  this.onDark = false;
@@ -16843,23 +16902,27 @@ class BaseInput extends AuroElement$1 {
16843
16902
  "email"
16844
16903
  ];
16845
16904
  this.shape = 'classic';
16905
+
16846
16906
  /** @private */
16847
16907
  this.showPassword = false;
16848
16908
  this.size = 'lg';
16849
16909
  this.touched = false;
16910
+
16850
16911
  /** @private */
16851
16912
  this.uniqueId = new UniqueId().create();
16913
+
16852
16914
  /** @private */
16853
16915
  this.util = new AuroInputUtilities({
16854
16916
  locale: this.locale,
16855
16917
  format: this.format
16856
16918
  });
16919
+
16857
16920
  /** @private */
16858
16921
  this.validation = new AuroFormValidation();
16922
+
16859
16923
  /** @private */
16860
16924
  this.validationCCLength = undefined;
16861
16925
  this.value = undefined;
16862
- this._valueObject = undefined;
16863
16926
  }
16864
16927
 
16865
16928
  // function to define props used within the scope of this component
@@ -17189,6 +17252,13 @@ class BaseInput extends AuroElement$1 {
17189
17252
  type: String
17190
17253
  },
17191
17254
 
17255
+ /**
17256
+ * Custom help text message to display when validity = `patternMismatch`.
17257
+ */
17258
+ setCustomValidityPatternMismatch: {
17259
+ type: String
17260
+ },
17261
+
17192
17262
  /**
17193
17263
  * Custom help text message to display when validity = `rangeOverflow`.
17194
17264
  */
@@ -17259,7 +17329,7 @@ class BaseInput extends AuroElement$1 {
17259
17329
 
17260
17330
  /**
17261
17331
  * Populates the `type` attribute on the input.
17262
- * @type {'text' | 'password' | 'email' | 'credit-card' | 'tel' | 'number'}
17332
+ * @type {'text' | 'password' | 'email' | 'credit-card' | 'tel' | 'number' | 'date'}
17263
17333
  * @default 'text'
17264
17334
  */
17265
17335
  type: {
@@ -17297,7 +17367,7 @@ class BaseInput extends AuroElement$1 {
17297
17367
  * @returns {Date|undefined}
17298
17368
  */
17299
17369
  get valueObject() {
17300
- return this._valueObject || this._computeDateObjectFallback(this.value);
17370
+ return this.value && dateFormatter.isValidDate(this.value) ? dateFormatter.stringToDateInstance(this.value) : undefined;
17301
17371
  }
17302
17372
 
17303
17373
  /**
@@ -17305,7 +17375,7 @@ class BaseInput extends AuroElement$1 {
17305
17375
  * @returns {Date|undefined}
17306
17376
  */
17307
17377
  get minObject() {
17308
- return this._minObject || this._computeDateObjectFallback(this.min);
17378
+ return this.min && dateFormatter.isValidDate(this.min) ? dateFormatter.stringToDateInstance(this.min) : undefined;
17309
17379
  }
17310
17380
 
17311
17381
  /**
@@ -17313,50 +17383,7 @@ class BaseInput extends AuroElement$1 {
17313
17383
  * @returns {Date|undefined}
17314
17384
  */
17315
17385
  get maxObject() {
17316
- return this._maxObject || this._computeDateObjectFallback(this.max);
17317
- }
17318
-
17319
- /**
17320
- * Parses a date string into a Date object when the corresponding `_*Object`
17321
- * field hasn't been synced yet by `updated()`. Returns undefined when the
17322
- * input type/format isn't a full date or the string is not a valid date.
17323
- *
17324
- * Why this exists: a parent (datepicker) can call `inputN.validate()` from
17325
- * inside its own `updated()` before this input's `updated()` has run
17326
- * `syncDateValues()` — so `_valueObject`/`_maxObject` are still `undefined`
17327
- * and range checks would otherwise silently no-op (flipping the result to
17328
- * `valid` or `patternMismatch`).
17329
- * @private
17330
- * @param {string|undefined} dateStr - ISO date string from `value`/`min`/`max`.
17331
- * @returns {Date|undefined}
17332
- */
17333
- _computeDateObjectFallback(dateStr) {
17334
- if (!dateStr || !this.util || !this.util.isFullDateFormat(this.type, this.format)) {
17335
- return undefined;
17336
- }
17337
- if (!dateFormatter.isValidDate(dateStr)) {
17338
- return undefined;
17339
- }
17340
- return dateFormatter.stringToDateInstance(dateStr);
17341
- }
17342
-
17343
- /**
17344
- * Internal setter for readonly date object properties.
17345
- * @private
17346
- * @param {'valueObject'|'minObject'|'maxObject'} propertyName - Public object property name.
17347
- * @param {Date|undefined} propertyValue - Value to assign.
17348
- * @returns {void}
17349
- */
17350
- setDateObjectProperty(propertyName, propertyValue) {
17351
- const internalPropertyName = `_${propertyName}`;
17352
- const previousValue = this[internalPropertyName];
17353
-
17354
- if (previousValue === propertyValue) {
17355
- return;
17356
- }
17357
-
17358
- this[internalPropertyName] = propertyValue;
17359
- this.requestUpdate(propertyName, previousValue);
17386
+ return this.max && dateFormatter.isValidDate(this.max) ? dateFormatter.stringToDateInstance(this.max) : undefined;
17360
17387
  }
17361
17388
 
17362
17389
  connectedCallback() {
@@ -17385,7 +17412,6 @@ class BaseInput extends AuroElement$1 {
17385
17412
  format: this.format
17386
17413
  });
17387
17414
  this.configureDataForType();
17388
- this.syncDateValues();
17389
17415
  }
17390
17416
 
17391
17417
  disconnectedCallback() {
@@ -17424,7 +17450,6 @@ class BaseInput extends AuroElement$1 {
17424
17450
  this.setCustomHelpTextMessage();
17425
17451
  this.configureAutoFormatting();
17426
17452
  this.configureDataForType();
17427
- this.syncDateValues();
17428
17453
  }
17429
17454
 
17430
17455
  /**
@@ -17568,8 +17593,6 @@ class BaseInput extends AuroElement$1 {
17568
17593
  this.configureDataForType();
17569
17594
  }
17570
17595
 
17571
- this.syncDateValues(changedProperties);
17572
-
17573
17596
  if (changedProperties.has('value')) {
17574
17597
  if (this.value && this.value.length > 0) {
17575
17598
  this.hasValue = true;
@@ -17591,14 +17614,14 @@ class BaseInput extends AuroElement$1 {
17591
17614
 
17592
17615
  if (formattedValue !== this.inputElement.value) {
17593
17616
  this.skipNextProgrammaticInputEvent = true;
17594
- if (this.maskInstance && this.type === 'credit-card') {
17617
+ if (this.maskInstance && this.type !== 'date') {
17595
17618
  // Route through the mask so its _value and el.value stay in lock-step
17596
17619
  // (set value calls updateControl which writes el.value = displayValue).
17597
17620
  // Writing el.value directly leaves the mask thinking displayValue is
17598
- // stale; _saveSelection on the next focus/click then warns. Scoped to
17599
- // credit-card so date's own formattedValue (raw ISO when calendar-invalid)
17600
- // isn't re-masked through the mm/dd/yyyy mask, which would truncate it
17601
- // and flip validity from patternMismatch to tooShort.
17621
+ // stale; _saveSelection on the next focus/click then warns. Date is
17622
+ // excluded because its formattedValue can be raw ISO when the calendar
17623
+ // is invalid, and re-masking through mm/dd/yyyy would truncate it and
17624
+ // flip validity from patternMismatch to tooShort.
17602
17625
  this.maskInstance.value = formattedValue || '';
17603
17626
  } else if (formattedValue) {
17604
17627
  this.inputElement.value = formattedValue;
@@ -17640,120 +17663,65 @@ class BaseInput extends AuroElement$1 {
17640
17663
  }));
17641
17664
  }
17642
17665
 
17643
-
17644
- /**
17645
- * Synchronizes the ISO string values and Date object representations for date-related properties.
17646
- * This keeps the model and display values aligned when either side changes.
17647
- *
17648
- * When a full date format is in use, this method updates `value`, `min`, and `max` from their corresponding
17649
- * Date objects or vice versa, based on which properties have changed. It only runs when the current configuration
17650
- * represents a full year/month/day date format.
17651
- *
17652
- * @param {Map<string, unknown>|undefined} [changedProperties=undefined] - Optional map of changed properties used to limit which values are synchronized.
17653
- * @returns {void}
17654
- * @private
17655
- */
17656
- syncDateValues(changedProperties = undefined) {
17657
- if (!this.util.isFullDateFormat(this.type, this.format)) {
17658
- return;
17659
- }
17660
-
17661
- this.syncSingleDateValue(changedProperties, 'valueObject', 'value');
17662
- this.syncSingleDateValue(changedProperties, 'minObject', 'min');
17663
- this.syncSingleDateValue(changedProperties, 'maxObject', 'max');
17664
- }
17665
-
17666
- /**
17667
- * Synchronizes one date object/string property pair.
17668
- * @private
17669
- * @param {Map<string, unknown>|undefined} changedProperties - Map of changed properties from Lit.
17670
- * @param {string} objectProperty - Date object property name.
17671
- * @param {string} valueProperty - ISO string property name.
17672
- * @returns {void}
17673
- */
17674
- syncSingleDateValue(changedProperties, objectProperty, valueProperty) {
17675
- const objectPropertyChanged = !changedProperties || changedProperties.has(objectProperty);
17676
-
17677
- // objectProperty wins over valueProperty when both changed
17678
- if (objectPropertyChanged && this[objectProperty]) {
17679
- this[valueProperty] = dateFormatter.toISOFormatString(this[objectProperty]);
17680
- return;
17681
- }
17682
-
17683
- const valuePropertyChanged = !changedProperties || changedProperties.has(valueProperty);
17684
- if (!valuePropertyChanged) {
17685
- return;
17686
- }
17687
-
17688
- // 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)
17689
- if (
17690
- changedProperties &&
17691
- valueProperty === 'value' &&
17692
- changedProperties.get('value') === undefined &&
17693
- this[objectProperty] instanceof Date &&
17694
- this[valueProperty] === dateFormatter.toISOFormatString(this[objectProperty])
17695
- ) {
17696
- return;
17697
- }
17698
-
17699
- if (dateFormatter.isValidDate(this[valueProperty])) {
17700
- this.setDateObjectProperty(objectProperty, dateFormatter.stringToDateInstance(this[valueProperty]));
17701
- } else {
17702
- this.setDateObjectProperty(objectProperty, undefined);
17703
- }
17704
- }
17705
-
17706
17666
  /**
17707
17667
  * Sets up IMasks and logic based on auto-formatting requirements.
17708
17668
  * @private
17709
17669
  * @returns {void}
17710
17670
  */
17711
17671
  configureAutoFormatting() {
17712
- // Re-entrancy guard. The patched-setter's synthetic input event (suppressed
17713
- // by _configuringMask above) could otherwise trigger handleInput
17714
- // processCreditCard configureAutoFormatting before the outer call's
17715
- // set value has finished its alignCursor pass.
17672
+ // _configuringMask gates two things: external re-entry into this method
17673
+ // while setup is mid-flight (from property changes that call back here),
17674
+ // and the accept/complete listeners below — both need to ignore the mask
17675
+ // events fired by our own value-restore step.
17716
17676
  if (this._configuringMask) return;
17717
17677
  this._configuringMask = true;
17718
17678
  try {
17679
+ // Destroy any prior mask so IMask can attach fresh under the new format.
17680
+ // Null the reference too — if the new maskOptions.mask is falsy (e.g.
17681
+ // type switched from credit-card to text) the IMask() reassignment
17682
+ // below is skipped, and downstream writes at line ~823 would otherwise
17683
+ // route through the destroyed instance.
17719
17684
  if (this.maskInstance) {
17720
17685
  this.maskInstance.destroy();
17686
+ this.maskInstance = null;
17721
17687
  }
17722
17688
 
17723
- // Pass new format to util
17724
17689
  this.util.updateFormat(this.format);
17725
17690
 
17726
17691
  const maskOptions = this.util.getMaskOptions(this.type, this.format);
17727
17692
 
17728
17693
  if (this.inputElement && maskOptions.mask) {
17729
-
17730
- // Stash and clear any existing value before IMask init.
17731
- // IMask's constructor processes the current input value which requires
17732
- // selection state clearing first avoids that scenario entirely.
17733
- // When the format changes (e.g. locale switch) and we have a valid ISO
17734
- // model value, compute the display string for the NEW format instead of
17735
- // re-using the old display string, which may be invalid in the new mask.
17694
+ // Capture the current display so it can be re-applied after IMask
17695
+ // attaches. The restore at the bottom goes through maskInstance.value
17696
+ // (not inputElement.value directly) so the mask's internal state and
17697
+ // the input's displayed text stay in lock-step.
17736
17698
  let existingValue = this.inputElement.value;
17699
+
17700
+ // Format-change case (e.g. locale switch): existingValue is the OLD
17701
+ // mask's display string and may not parse under the new mask. When
17702
+ // we have a valid date model, rebuild the display from valueObject
17703
+ // (the canonical source) using the new mask's format function.
17737
17704
  if (
17738
17705
  this.util.isFullDateFormat(this.type, this.format) &&
17739
17706
  this.value &&
17740
- dateFormatter.isValidDate(this.value) &&
17741
- this.valueObject instanceof Date &&
17742
- !Number.isNaN(this.valueObject.getTime()) &&
17707
+ this.valueObject &&
17743
17708
  typeof maskOptions.format === 'function'
17744
17709
  ) {
17745
17710
  existingValue = maskOptions.format(this.valueObject);
17746
17711
  }
17747
17712
 
17713
+ // Clear before IMask attaches so the constructor seeds an empty
17714
+ // internal value. Otherwise IMask reads the stale unmasked string
17715
+ // and emits a spurious 'accept' before the restore below runs.
17748
17716
  this.skipNextProgrammaticInputEvent = true;
17749
17717
  this.inputElement.value = '';
17750
17718
 
17751
17719
  this.maskInstance = IMask(this.inputElement, maskOptions);
17752
17720
 
17721
+ // Mask fires 'accept' on every value change, including the restore
17722
+ // step below. Skip events fired during configureAutoFormatting so
17723
+ // we don't overwrite a value the parent just pushed.
17753
17724
  this.maskInstance.on('accept', () => {
17754
- // Suppress propagation during configureAutoFormatting's own value-restoration
17755
- // (line below) — the mask emits 'accept' on every value-set, including ours,
17756
- // and we don't want to overwrite a value the parent just pushed.
17757
17725
  if (this._configuringMask) return;
17758
17726
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
17759
17727
  if (this.type === "date") {
@@ -17761,6 +17729,8 @@ class BaseInput extends AuroElement$1 {
17761
17729
  }
17762
17730
  });
17763
17731
 
17732
+ // Mask fires 'complete' on the restore step below for any value that
17733
+ // happens to be a complete match. Same setup-suppression as 'accept'.
17764
17734
  this.maskInstance.on('complete', () => {
17765
17735
  if (this._configuringMask) return;
17766
17736
  this.value = this.util.toModelValue(this.maskInstance.value, this.format);
@@ -17769,7 +17739,9 @@ class BaseInput extends AuroElement$1 {
17769
17739
  }
17770
17740
  });
17771
17741
 
17772
- // Restore the stashed value through IMask so it's properly masked
17742
+ // Write existingValue through the mask (not the input directly) so
17743
+ // the mask reformats it under the new rules and keeps its internal
17744
+ // _value aligned with the input's displayed text.
17773
17745
  if (existingValue) {
17774
17746
  this.maskInstance.value = existingValue;
17775
17747
  }
@@ -17863,9 +17835,9 @@ class BaseInput extends AuroElement$1 {
17863
17835
  // the gated types currently support it, since the list is a public
17864
17836
  // property a consumer could mutate.
17865
17837
  if (this.setSelectionInputTypes.includes(this.type)) {
17866
- let selectionStart;
17838
+ let selectionStart = null;
17867
17839
  try {
17868
- selectionStart = this.inputElement.selectionStart;
17840
+ ({ selectionStart } = this.inputElement);
17869
17841
  } catch (error) { // eslint-disable-line no-unused-vars
17870
17842
  return;
17871
17843
  }
@@ -17969,7 +17941,6 @@ class BaseInput extends AuroElement$1 {
17969
17941
  */
17970
17942
  reset() {
17971
17943
  this.value = undefined;
17972
- this.setDateObjectProperty('valueObject', undefined);
17973
17944
  this.validation.reset(this);
17974
17945
  }
17975
17946
 
@@ -17978,7 +17949,6 @@ class BaseInput extends AuroElement$1 {
17978
17949
  */
17979
17950
  clear() {
17980
17951
  this.value = undefined;
17981
- this.setDateObjectProperty('valueObject', undefined);
17982
17952
  }
17983
17953
 
17984
17954
  /**
@@ -18507,7 +18477,7 @@ let AuroHelpText$1 = class AuroHelpText extends i$3 {
18507
18477
  }
18508
18478
  };
18509
18479
 
18510
- var formkitVersion$1 = '202606292156';
18480
+ var formkitVersion$1 = '202607011722';
18511
18481
 
18512
18482
  // Copyright (c) 2025 Alaska Airlines. All right reserved. Licensed under the Apache-2.0 license
18513
18483
  // See LICENSE in the project root for license information.
@@ -19633,7 +19603,7 @@ class AuroBibtemplate extends i$3 {
19633
19603
  }
19634
19604
  }
19635
19605
 
19636
- var formkitVersion = '202606292156';
19606
+ var formkitVersion = '202607011722';
19637
19607
 
19638
19608
  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}`;
19639
19609
 
@@ -20001,12 +19971,23 @@ function getOptionLabel(option) {
20001
19971
  if (!option) {
20002
19972
  return '';
20003
19973
  }
20004
- const clone = option.cloneNode(true);
20005
- const displayValueEl = clone.querySelector('[slot="displayValue"]');
20006
- if (displayValueEl) {
20007
- displayValueEl.remove();
19974
+
19975
+ // Consumer-provided override: short-circuit the DOM walk entirely.
19976
+ if (option.dataset && option.dataset.label) {
19977
+ return option.dataset.label;
19978
+ }
19979
+
19980
+ // Walk direct children — the `slot` attribute only applies to direct children
19981
+ // of the slot host, so a shallow filter is sufficient. Avoids the cloneNode +
19982
+ // querySelector allocation that ran on every keystroke via syncValuesAndStates.
19983
+ let text = '';
19984
+ for (const node of option.childNodes) {
19985
+ const isDisplayValueSlot = node.nodeType === Node.ELEMENT_NODE && node.getAttribute('slot') === 'displayValue';
19986
+ if (!isDisplayValueSlot) {
19987
+ text += node.textContent || '';
19988
+ }
20008
19989
  }
20009
- return (clone.textContent || '').replace(/\s+/gu, ' ').trim();
19990
+ return text.replace(/\s+/gu, ' ').trim();
20010
19991
  }
20011
19992
 
20012
19993
  // See https://git.io/JJ6SJ for "How to document your components using JSDoc"
@@ -20081,8 +20062,6 @@ class AuroCombobox extends AuroElement {
20081
20062
  this.availableOptions = [];
20082
20063
  this.dropdownId = undefined;
20083
20064
  this.dropdownOpen = false;
20084
- this.triggerExpandedState = false;
20085
- this._expandedTimeout = null;
20086
20065
  this._inFullscreenTransition = false;
20087
20066
  this.errorMessage = null;
20088
20067
  this.isHiddenWhileLoading = false;
@@ -20093,6 +20072,30 @@ class AuroCombobox extends AuroElement {
20093
20072
  this.touched = false;
20094
20073
  this.validation = new AuroFormValidation$1();
20095
20074
  this.validity = undefined;
20075
+ this._userTyped = false;
20076
+ // Tracks every setTimeout scheduled via _scheduleTimer so
20077
+ // disconnectedCallback can cancel them. Without this, a detached
20078
+ // combobox's pending timers still fire — most are no-ops, but
20079
+ // configureMenu's racing-condition retry would otherwise loop.
20080
+ this._pendingTimers = new Set();
20081
+ }
20082
+
20083
+ /**
20084
+ * setTimeout wrapper that records the timer id so disconnectedCallback
20085
+ * can cancel any outstanding callbacks. The id is removed from the set
20086
+ * once the callback fires so the set doesn't grow unbounded.
20087
+ * @param {Function} fn - Callback to run.
20088
+ * @param {number} ms - Delay in milliseconds.
20089
+ * @returns {number} The timer id.
20090
+ * @private
20091
+ */
20092
+ _scheduleTimer(fn, ms) {
20093
+ const id = setTimeout(() => {
20094
+ this._pendingTimers.delete(id);
20095
+ fn();
20096
+ }, ms);
20097
+ this._pendingTimers.add(id);
20098
+ return id;
20096
20099
  }
20097
20100
 
20098
20101
  // This function is to define props used within the scope of this component
@@ -20454,17 +20457,6 @@ class AuroCombobox extends AuroElement {
20454
20457
  attribute: false
20455
20458
  },
20456
20459
 
20457
- /**
20458
- * Deferred aria-expanded state for the trigger input.
20459
- * Delays the "true" transition so VoiceOver finishes its character echo
20460
- * before announcing "expanded".
20461
- * @private
20462
- */
20463
- triggerExpandedState: {
20464
- type: Boolean,
20465
- reflect: false,
20466
- attribute: false
20467
- },
20468
20460
  };
20469
20461
  }
20470
20462
 
@@ -20487,13 +20479,6 @@ class AuroCombobox extends AuroElement {
20487
20479
  return this.input.value;
20488
20480
  }
20489
20481
 
20490
- // /**
20491
- // * Sets the value of the input element within the combobox.
20492
- // */
20493
- // set inputValue(value) {
20494
- // this.input.value = value;
20495
- // }
20496
-
20497
20482
  /**
20498
20483
  * Checks if the element is valid.
20499
20484
  * @returns {boolean} - Returns true if the element is valid, false otherwise.
@@ -20620,7 +20605,7 @@ class AuroCombobox extends AuroElement {
20620
20605
  if (this.menu) {
20621
20606
  this.menu.matchWord = normalizeFilterValue(this.input.value);
20622
20607
  }
20623
- const label = getOptionLabel(this.menu.optionSelected) || this.menu.currentLabel;
20608
+ const label = getOptionLabel(this.menu.optionSelected);
20624
20609
  this.updateTriggerTextDisplay(label || this.value);
20625
20610
  }
20626
20611
 
@@ -20634,44 +20619,16 @@ class AuroCombobox extends AuroElement {
20634
20619
  // in suggestion mode, do not override input value if no selection has been made and the input currently has focus
20635
20620
  const isInputFocusedWithNoSelection = !this.menu.value && (this.input.matches(':focus-within') || (this.inputInBib && this.inputInBib.matches(':focus-within')));
20636
20621
 
20637
- if (!this.persistInput && !(this.behavior === 'suggestion' && isInputFocusedWithNoSelection)) {
20638
- const nextValue = label || this.value;
20639
- // Only set the flag when there's an actual write to suppress —
20640
- // syncValuesAndStates re-enters here during typing when both inputs
20641
- // already match, and a no-op flag flip would make the bib branch's
20642
- // bail eat legitimate user-input events.
20643
- const triggerNeedsSync = this.input.value !== nextValue;
20644
- const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
20645
- if (triggerNeedsSync || bibNeedsSync) {
20646
- this._syncingDisplayValue = true;
20647
- if (triggerNeedsSync) {
20648
- this.input.value = nextValue;
20649
- }
20650
- if (bibNeedsSync) {
20651
- this.inputInBib.value = nextValue;
20652
- }
20653
- const pending = [];
20654
- if (triggerNeedsSync) {
20655
- pending.push(this.input.updateComplete);
20656
- }
20657
- if (bibNeedsSync) {
20658
- pending.push(this.inputInBib.updateComplete);
20659
- }
20660
- Promise.all(pending).then(() => {
20661
- // imask reasserts otherwise, and handleBlur reverts to its stale value.
20662
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20663
- this.input.maskInstance.updateValue();
20664
- }
20665
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20666
- this.inputInBib.maskInstance.updateValue();
20667
- }
20668
- this._syncingDisplayValue = false;
20669
- });
20670
- }
20622
+ const suppressed = this.persistInput || (this.behavior === 'suggestion' && isInputFocusedWithNoSelection);
20623
+ if (!suppressed) {
20624
+ this.syncInputValuesAcrossTriggerAndBib(label || this.value);
20671
20625
  }
20672
20626
 
20673
- // update the displayValue in the trigger if displayValue slot content is present
20674
- const displayValueInTrigger = this.input.querySelector('[slot="displayValue"]');
20627
+ // Replace any previously appended displayValue clone in the trigger.
20628
+ // :not(slot) excludes the template's <slot name="displayValue"
20629
+ // slot="displayValue"> forwarder, which also has slot="displayValue"
20630
+ // and would otherwise be matched first and removed.
20631
+ const displayValueInTrigger = this.input.querySelector('[slot="displayValue"]:not(slot)');
20675
20632
 
20676
20633
  if (displayValueInTrigger) {
20677
20634
  displayValueInTrigger.remove();
@@ -20695,6 +20652,53 @@ class AuroCombobox extends AuroElement {
20695
20652
  this.requestUpdate();
20696
20653
  }
20697
20654
 
20655
+ /**
20656
+ * Writes nextValue to the trigger input and the bib input when their current
20657
+ * value differs, then re-asserts imask after Lit's update flushes.
20658
+ * @param {string} nextValue - The value to write to both inputs.
20659
+ * @returns {Promise<void>} Resolves after both inputs flush and imask
20660
+ * re-asserts; resolves immediately when no sync is needed.
20661
+ * @private
20662
+ */
20663
+ async syncInputValuesAcrossTriggerAndBib(nextValue) {
20664
+ // Only set the flag when there's an actual write to suppress —
20665
+ // syncValuesAndStates re-enters here during typing when both inputs
20666
+ // already match, and a no-op flag flip would make the bib branch's
20667
+ // bail eat legitimate user-input events.
20668
+ const triggerNeedsSync = this.input.value !== nextValue;
20669
+ const bibNeedsSync = Boolean(this.inputInBib) && this.inputInBib.value !== nextValue;
20670
+ if (!triggerNeedsSync && !bibNeedsSync) {
20671
+ return;
20672
+ }
20673
+
20674
+ this._syncingDisplayValue = true;
20675
+
20676
+ const pending = [];
20677
+ if (triggerNeedsSync) {
20678
+ this.input.value = nextValue;
20679
+ pending.push(this.input.updateComplete);
20680
+ }
20681
+ if (bibNeedsSync) {
20682
+ this.inputInBib.value = nextValue;
20683
+ pending.push(this.inputInBib.updateComplete);
20684
+ }
20685
+ // finally — not a bare .then — so that an imask throw (see commit
20686
+ // d1857401c: imask can throw on credit-card format change) doesn't strand
20687
+ // _syncingDisplayValue=true and silently swallow every subsequent input.
20688
+ try {
20689
+ await Promise.all(pending);
20690
+ // imask reasserts otherwise, and handleBlur reverts to its stale value.
20691
+ if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
20692
+ this.input.maskInstance.updateValue();
20693
+ }
20694
+ if (bibNeedsSync && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
20695
+ this.inputInBib.maskInstance.updateValue();
20696
+ }
20697
+ } finally {
20698
+ this._syncingDisplayValue = false;
20699
+ }
20700
+ }
20701
+
20698
20702
  /**
20699
20703
  * Processes hidden state of all menu options and determines if there are any available options not hidden.
20700
20704
  * @private
@@ -20703,6 +20707,13 @@ class AuroCombobox extends AuroElement {
20703
20707
  handleMenuOptions() {
20704
20708
  this.generateOptionsArray();
20705
20709
  this.availableOptions = [];
20710
+ // Single source of truth for the menu's filter/highlight token per call.
20711
+ // syncValuesAndStates re-writes the same value on exact-match keystrokes —
20712
+ // Lit's hasChanged makes that a no-op — and the prior duplicate set inside
20713
+ // handleTriggerInputValueChange is gone.
20714
+ if (this.menu) {
20715
+ this.menu.matchWord = normalizeFilterValue(this.input.value);
20716
+ }
20706
20717
  this.updateFilter();
20707
20718
 
20708
20719
  // Set aria-setsize/aria-posinset on each visible option so screen readers
@@ -20714,17 +20725,14 @@ class AuroCombobox extends AuroElement {
20714
20725
  option.setAttribute('aria-posinset', index + 1);
20715
20726
  });
20716
20727
 
20717
- if (this.value && this.input.value && !this.menu.value) {
20718
- if (this.behavior === 'suggestion' && this.menu.options && this.menu.options.some((opt) => opt.value === this.value)) {
20719
- this.setMenuValue(this.value);
20720
- }
20721
-
20728
+ if (this.input.value && this.menu.options && this.menu.options.some((opt) => opt.value === this.input.value)) {
20729
+ this.setMenuValue(this.input.value);
20722
20730
  this.syncValuesAndStates();
20723
20731
  }
20724
20732
 
20725
- // Re-activate when optionActive is no longer visible, or when _index has
20726
- // been reset (e.g. by clearSelection in an async update) so that
20727
- // makeSelection() can find the correct option.
20733
+ // Re-activate when optionActive is not in the current scrollable viewport,
20734
+ // or when _index has been reset (e.g. by clearSelection in an async update)
20735
+ // so that makeSelection() can find the correct option.
20728
20736
  if (!this.availableOptions.includes(this.menu.optionActive) || this.menu._index < 0) {
20729
20737
  this.activateFirstEnabledAvailableOption();
20730
20738
  }
@@ -20797,18 +20805,6 @@ class AuroCombobox extends AuroElement {
20797
20805
  this.dropdownOpen = ev.detail.expanded;
20798
20806
  this.updateMenuShapeSize();
20799
20807
 
20800
- // Defer aria-expanded "true" so VoiceOver finishes character echo
20801
- // before announcing "expanded". Set "false" immediately on close.
20802
- clearTimeout(this._expandedTimeout);
20803
- if (this.dropdownOpen) {
20804
- const expandedDelay = 150;
20805
- this._expandedTimeout = setTimeout(() => {
20806
- this.triggerExpandedState = true;
20807
- }, expandedDelay);
20808
- } else {
20809
- this.triggerExpandedState = false;
20810
- }
20811
-
20812
20808
  // Clear aria-activedescendant when dropdown closes
20813
20809
  if (!this.dropdownOpen && this.input) {
20814
20810
  this.input.setActiveDescendant(null);
@@ -20858,28 +20854,25 @@ class AuroCombobox extends AuroElement {
20858
20854
 
20859
20855
  guardTouchPassthrough(this.menu);
20860
20856
 
20861
- // The dialog's showModal() steals focus from the trigger.
20862
- // A single rAF is enough for the dialog DOM to be painted and
20863
- // focusable, then doubleRaf finalizes the transition.
20857
+ // showModal() takes focus away from the trigger. Early focus once
20858
+ // the dialog has painted; the shared doubleRaf below is the fallback
20859
+ // for when the dialog needs an extra frame and also clears the
20860
+ // validation guard.
20864
20861
  requestAnimationFrame(() => {
20865
20862
  this.setInputFocus();
20866
20863
  });
20867
-
20868
- doubleRaf(() => {
20869
- this.setInputFocus();
20870
- this._inFullscreenTransition = false;
20871
- });
20872
- } else {
20873
- // Desktop popover-open: restore the trigger caret to end-of-text.
20874
- // Clicking the trigger lands on auro-input's floating <label for="…">
20875
- // overlay; Chrome resets the native input's selection to [0, 0] on
20876
- // label-focus before any JS runs. setInputFocus()'s non-fullscreen
20877
- // branch parks the caret at end. doubleRaf lets the dropdown layout
20878
- // settle first, matching the fullscreen branch timing.
20879
- doubleRaf(() => {
20880
- this.setInputFocus();
20881
- });
20882
20864
  }
20865
+ // else (desktop popover-open): Chrome resets the trigger caret to
20866
+ // [0, 0] when its floating <label for="…"> overlay receives focus.
20867
+ // The shared doubleRaf below parks the caret back at end-of-text
20868
+ // after the dropdown layout settles.
20869
+
20870
+ doubleRaf(() => {
20871
+ this.setInputFocus();
20872
+ if (this._inFullscreenTransition) {
20873
+ this._inFullscreenTransition = false;
20874
+ }
20875
+ });
20883
20876
  }
20884
20877
  });
20885
20878
 
@@ -20916,7 +20909,7 @@ class AuroCombobox extends AuroElement {
20916
20909
  this.dropdown.trigger.inert = false;
20917
20910
  }
20918
20911
 
20919
- setTimeout(() => {
20912
+ this._scheduleTimer(() => {
20920
20913
  this.setInputFocus();
20921
20914
  }, 0);
20922
20915
  });
@@ -21060,7 +21053,7 @@ class AuroCombobox extends AuroElement {
21060
21053
 
21061
21054
  // racing condition on custom-combobox with custom-menu
21062
21055
  if (!this.menu) {
21063
- setTimeout(() => {
21056
+ this._scheduleTimer(() => {
21064
21057
  this.configureMenu();
21065
21058
  }, 0);
21066
21059
  return;
@@ -21090,7 +21083,7 @@ class AuroCombobox extends AuroElement {
21090
21083
  if (this.menu.optionSelected) {
21091
21084
  const selected = this.menu.optionSelected;
21092
21085
 
21093
- if (!this.optionSelected || this.optionSelected !== selected) {
21086
+ if (this.optionSelected !== selected) {
21094
21087
  this.optionSelected = selected;
21095
21088
  }
21096
21089
 
@@ -21103,7 +21096,7 @@ class AuroCombobox extends AuroElement {
21103
21096
  }
21104
21097
 
21105
21098
  // Update display
21106
- this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected) || this.menu.value);
21099
+ this.updateTriggerTextDisplay(getOptionLabel(this.optionSelected));
21107
21100
 
21108
21101
  // Update match word for filtering
21109
21102
  const trimmedInput = normalizeFilterValue(this.input.value);
@@ -21115,17 +21108,24 @@ class AuroCombobox extends AuroElement {
21115
21108
  // Hide dropdown on selection (except during slot changes)
21116
21109
  if (evt.detail && evt.detail.source !== 'slotchange') {
21117
21110
  // do not close while typing in suggestion mode with no value selected, to allow freeform input
21118
- if (this.menu.value || this.behavior !== 'suggestion') {
21119
- this.hideBib();
21111
+ this.hideBib();
21112
+
21113
+ // Move focus to the clear button when the user makes a selection.
21114
+ if (!isEcho && this.menu.value !== undefined) {
21115
+ this.setClearBtnFocus();
21120
21116
  }
21121
21117
 
21122
21118
  // Announce the selection after the dropdown closes so it isn't
21123
21119
  // overridden by VoiceOver's "collapsed" announcement from aria-expanded.
21120
+ // Skip when there's no selected value (e.g. menu.clearSelection() from
21121
+ // the unmatched-value path), otherwise VoiceOver reads "undefined".
21124
21122
  const selectedValue = this.menu.value;
21125
- const announcementDelay = 300;
21126
- setTimeout(() => {
21127
- announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21128
- }, announcementDelay);
21123
+ if (selectedValue) {
21124
+ const announcementDelay = 300;
21125
+ this._scheduleTimer(() => {
21126
+ announceToScreenReader(this._getAnnouncementRoot(), `${selectedValue}, selected`);
21127
+ }, announcementDelay);
21128
+ }
21129
21129
  }
21130
21130
 
21131
21131
  // Programmatic value syncs leave availableOptions stale because
@@ -21134,22 +21134,6 @@ class AuroCombobox extends AuroElement {
21134
21134
  // only — fresh user selections take the existing hideBib path.
21135
21135
  if (isEcho && this.menu.optionSelected) {
21136
21136
  this._programmaticFilterRefresh = true;
21137
- this.handleMenuOptions();
21138
- setTimeout(() => {
21139
- this._programmaticFilterRefresh = false;
21140
- }, 0);
21141
- }
21142
-
21143
- // base-input skips auto-validate when focus is inside its own shadow,
21144
- // which it is after setTriggerInputFocus — re-run so prior tooShort
21145
- // clears. processCreditCard reasserts errorMessage on every render.
21146
- if (!isEcho && this.menu.optionSelected && this.input.validate) {
21147
- this.input.updateComplete.then(() => {
21148
- this.input.validate(true);
21149
- if (this.input.validity === 'valid') {
21150
- this.input.errorMessage = '';
21151
- }
21152
- });
21153
21137
  }
21154
21138
  });
21155
21139
 
@@ -21162,6 +21146,14 @@ class AuroCombobox extends AuroElement {
21162
21146
  // stale option. Safe from re-entrancy because any resulting
21163
21147
  // input.value changes dispatch isProgrammatic events.
21164
21148
  this.menu.addEventListener('auroMenu-selectValueFailure', () => {
21149
+ // Announce the rejection BEFORE we clear `this.value` so the live
21150
+ // region carries the attempted value — without this the bib closes
21151
+ // silently and screen-reader users get no signal that their request
21152
+ // (e.g. a direct setMenuValue() call) was dropped.
21153
+ const attemptedValue = this.value;
21154
+ if (attemptedValue) {
21155
+ announceToScreenReader(this._getAnnouncementRoot(), `No matching option for ${attemptedValue}`);
21156
+ }
21165
21157
  this.value = undefined;
21166
21158
  this.optionSelected = undefined;
21167
21159
  });
@@ -21173,10 +21165,13 @@ class AuroCombobox extends AuroElement {
21173
21165
  this.input.setActiveDescendant(this.optionActive);
21174
21166
  }
21175
21167
 
21176
- // Announce the active option for screen readers including position,
21177
- // since shadow DOM boundaries prevent native reading of
21178
- // aria-setsize/aria-posinset via aria-activedescendant.
21179
- if (this.optionActive) {
21168
+ // In fullscreen mode the menu sits inside a nested <dialog> shadow root,
21169
+ // and aria-activedescendant references across that boundary are lost —
21170
+ // VoiceOver/NVDA don't read the active option natively, so we mirror it
21171
+ // into the polite live region. In popover mode aria-activedescendant on
21172
+ // the trigger input is read natively; double-announcing would flood the
21173
+ // queue on arrow-key repeat.
21174
+ if (this.optionActive && this.dropdown.isBibFullscreen) {
21180
21175
  const optionText = this.optionActive.textContent.trim();
21181
21176
  const selectedState = this.optionActive.hasAttribute('selected') ? ', selected' : ', not selected';
21182
21177
  const optionIndex = this.availableOptions.indexOf(this.optionActive) + 1;
@@ -21209,7 +21204,12 @@ class AuroCombobox extends AuroElement {
21209
21204
  * Validate every time we remove focus from the combo box.
21210
21205
  */
21211
21206
  this.addEventListener('focusout', () => {
21212
- if (!this.componentHasFocus && !this._inFullscreenTransition) {
21207
+ // Skip while the dropdown is open — focus transits out briefly on
21208
+ // mousedown of a menu option (popover top-layer breaks :focus-within),
21209
+ // and validating against the pre-selection value flashes a stale error
21210
+ // between mousedown and mouseup. The next focusout fires after the
21211
+ // dropdown closes and validates against the post-selection value.
21212
+ if (!this.componentHasFocus && !this._inFullscreenTransition && !this.dropdownOpen) {
21213
21213
  this.validate();
21214
21214
  }
21215
21215
  });
@@ -21272,49 +21272,40 @@ class AuroCombobox extends AuroElement {
21272
21272
  if (this._syncingDisplayValue) {
21273
21273
  return;
21274
21274
  }
21275
- this._syncingBibValue = true;
21276
- this.input.value = this.inputInBib.value;
21277
- this.input.updateComplete.then(() => {
21278
- this._syncingBibValue = false;
21279
- });
21280
21275
 
21281
- // Run filtering inline — the re-entrant event won't reach this code.
21282
- this.menu.matchWord = normalizeFilterValue(this.inputInBib.value);
21283
- this.optionActive = null;
21284
-
21285
- // In suggestion mode, keep the combobox value in sync with the typed
21286
- // text so that freeform values are captured (mirroring the non-fullscreen
21287
- // path). Clear the selection when the input is emptied.
21288
- if (this.behavior === 'suggestion') {
21289
- this.value = this.inputInBib.value || undefined;
21276
+ // Filtering runs via re-entrance: writing this.input.value below
21277
+ // dispatches an 'input' event that the trigger's listener routes to
21278
+ // handleTriggerInputValueChange, which refreshes the menu filter.
21279
+ if (this.input.value !== this.inputInBib.value) {
21280
+ this._syncingBibValue = true;
21281
+ this.input.value = this.inputInBib.value;
21282
+ this.input.updateComplete.then(() => {
21283
+ this._syncingBibValue = false;
21284
+ });
21290
21285
  }
21291
21286
 
21292
- this.handleMenuOptions();
21293
21287
  this.dispatchEvent(new CustomEvent('inputValue', { detail: { value: this.inputValue } }));
21294
- return;
21288
+ } else if (event.target === this.input) {
21289
+
21290
+ // Also sync the native input immediately so keystrokes arriving
21291
+ // before Lit's async update cycle (e.g. rapid Backspaces during a
21292
+ // fullscreen transition) operate on the correct content.
21293
+ // Skip the next programmatic input event to prevent the patched setter
21294
+ // from re-entering handleInputValueChange via the bib path.
21295
+ const bibNativeInput = this.inputInBib.inputElement;
21296
+ if (bibNativeInput && bibNativeInput.value !== this.input.value) {
21297
+ this.inputInBib.skipNextProgrammaticInputEvent = true;
21298
+ bibNativeInput.value = this.input.value || '';
21299
+ }
21295
21300
  }
21296
21301
 
21302
+ this.value = this.input.value;
21303
+
21297
21304
  // Ignore re-entrant input events caused by programmatic value sets.
21298
21305
  if (this._syncingBibValue || this._syncingDisplayValue) {
21299
21306
  return;
21300
21307
  }
21301
21308
 
21302
- this.inputInBib.value = this.input.value;
21303
-
21304
- // Also sync the native input immediately so keystrokes arriving
21305
- // before Lit's async update cycle (e.g. rapid Backspaces during a
21306
- // fullscreen transition) operate on the correct content.
21307
- // Skip the next programmatic input event to prevent the patched setter
21308
- // from re-entering handleInputValueChange via the bib path.
21309
- const bibNativeInput = this.inputInBib.inputElement;
21310
- if (bibNativeInput && bibNativeInput.value !== this.input.value) {
21311
- this.inputInBib.skipNextProgrammaticInputEvent = true;
21312
- bibNativeInput.value = this.input.value || '';
21313
- }
21314
-
21315
- this.menu.matchWord = normalizeFilterValue(this.input.value);
21316
- this.optionActive = null;
21317
-
21318
21309
  if (this.behavior === 'suggestion') {
21319
21310
  this.value = this.input.value;
21320
21311
  }
@@ -21322,7 +21313,6 @@ class AuroCombobox extends AuroElement {
21322
21313
  if (!this.input.value && !this._clearing) {
21323
21314
  this.clear();
21324
21315
  }
21325
- this.handleMenuOptions();
21326
21316
 
21327
21317
  // Validate only if the value was set programmatically (not during user
21328
21318
  // interaction). In fullscreen dialog mode, componentHasFocus returns false
@@ -21332,34 +21322,44 @@ class AuroCombobox extends AuroElement {
21332
21322
  this.validate();
21333
21323
  }
21334
21324
 
21335
- if (this.input.value && this.input.value.length === 0) {
21336
- // Hide menu if value is empty, otherwise show if there are available suggestions
21337
- this.hideBib();
21338
- } else if (this.menu.loading) {
21339
- // if input has value but menu is loading, show bib immediately
21340
- this.showBib();
21341
- } else if (this.availableOptions.length === 0 && !this.dropdown.isBibFullscreen) {
21342
- // Force dropdown bib to hide if input value has no matching suggestions
21343
- this.hideBib();
21325
+ this.dispatchEvent(new CustomEvent('inputValue', { detail: { value: this.inputValue } }));
21326
+ }
21327
+
21328
+ /**
21329
+ * Handles input value changes originating from the trigger input.
21330
+ * Refreshes menu options and filtering, delegates to handleInputValueChange
21331
+ * for value synchronization, and manages fullscreen bib focus.
21332
+ * @private
21333
+ * @param {Event} event - The input event from the trigger input element.
21334
+ * @returns {void}
21335
+ */
21336
+ handleTriggerInputValueChange(event) {
21337
+ // 'input' fires for every user-initiated value change — typing, paste,
21338
+ // IME composition end, dead-key composition, drag-drop. Flip _userTyped
21339
+ // here so updated('availableOptions') auto-opens the bib for sources
21340
+ // that keydown alone misses: paste fires no keydown, IME uses
21341
+ // key='Process', and dead keys produce multi-char keys (all bypass the
21342
+ // prior keydown.key.length===1 gate). Skip programmatic syncs.
21343
+ if (!this._syncingDisplayValue && !this._syncingBibValue && !this._programmaticFilterRefresh) {
21344
+ this._userTyped = true;
21344
21345
  }
21345
21346
 
21346
- // iOS virtual keyboard retention: when in fullscreen mode, ensure the
21347
- // dialog opens and the bib input is focused synchronously within the
21348
- // input event (user gesture) chain. Without this, Lit's async update
21349
- // cycle delays showModal() past the user activation window, causing
21350
- // iOS Safari to dismiss the virtual keyboard when the fullscreen
21351
- // dialog opens — the user then has to tap the input again to resume
21352
- // typing.
21353
- if (this.dropdown.isBibFullscreen && this.input.value && this.input.value.length > 0) {
21354
- if (!this.dropdown.isPopoverVisible) {
21355
- this.showBib();
21356
- }
21357
- if (this.dropdown.isPopoverVisible) {
21358
- this.setInputFocus();
21359
- }
21347
+ this.handleMenuOptions();
21348
+ this.optionActive = null;
21349
+
21350
+ if (this.value === this.input.value) {
21351
+ return;
21360
21352
  }
21361
21353
 
21362
- this.dispatchEvent(new CustomEvent('inputValue', { detail: { value: this.inputValue } }));
21354
+ this.handleInputValueChange(event);
21355
+
21356
+ if (this.dropdown.isBibFullscreen && this.input.value && this.input.value.length > 0 && this.dropdown.isPopoverVisible) {
21357
+ this.setInputFocus();
21358
+ }
21359
+
21360
+ if (this._programmaticFilterRefresh) {
21361
+ this._programmaticFilterRefresh = false;
21362
+ }
21363
21363
  }
21364
21364
 
21365
21365
  /**
@@ -21427,6 +21427,11 @@ class AuroCombobox extends AuroElement {
21427
21427
  disconnectedCallback() {
21428
21428
  super.disconnectedCallback();
21429
21429
  this._inFullscreenTransition = false;
21430
+ // Cancel any outstanding timers so detached callbacks don't fire on
21431
+ // disposed DOM — most are no-ops, but configureMenu's racing-condition
21432
+ // retry would otherwise keep rescheduling itself indefinitely.
21433
+ this._pendingTimers.forEach((id) => clearTimeout(id));
21434
+ this._pendingTimers.clear();
21430
21435
  }
21431
21436
 
21432
21437
  firstUpdated() {
@@ -21468,7 +21473,7 @@ class AuroCombobox extends AuroElement {
21468
21473
  this.menu.value = value;
21469
21474
  // Backup clear: if menu.value === menu.optionSelected.value already, the
21470
21475
  // listener won't fire and the flag would swallow the next user click.
21471
- setTimeout(() => {
21476
+ this._scheduleTimer(() => {
21472
21477
  this._pendingMenuValueSync = false;
21473
21478
  }, 0);
21474
21479
  }
@@ -21485,15 +21490,7 @@ class AuroCombobox extends AuroElement {
21485
21490
  this.menu.value = undefined;
21486
21491
  this.validation.reset(this);
21487
21492
  this.touched = false;
21488
- // Force validity back to the cleared state. validation.reset() calls
21489
- // validate() at the end, and (post-credit-card-fix) the trigger input's
21490
- // residual validity may still be copied into the combobox by the
21491
- // auroInputElements loop. Explicitly clear here so reset() actually
21492
- // leaves the component in a "no validity" state.
21493
21493
  this.validity = undefined;
21494
- this.errorMessage = '';
21495
- this.input.validity = undefined;
21496
- this.input.errorMessage = '';
21497
21494
  }
21498
21495
 
21499
21496
  /**
@@ -21511,6 +21508,17 @@ class AuroCombobox extends AuroElement {
21511
21508
  this.optionSelected = undefined;
21512
21509
  this.value = undefined;
21513
21510
 
21511
+ // Clear the appended displayValue clone in the trigger if present.
21512
+ // :not(slot) excludes the template's <slot name="displayValue"
21513
+ // slot="displayValue"> forwarder (line 1816), which also has
21514
+ // slot="displayValue" and would otherwise be matched first and removed,
21515
+ // permanently breaking the consumer-provided displayValue slot.
21516
+ const displayValueInTrigger = this.input.querySelector('[slot="displayValue"]:not(slot)');
21517
+
21518
+ if (displayValueInTrigger) {
21519
+ displayValueInTrigger.remove();
21520
+ }
21521
+
21514
21522
  if (this.input.value) {
21515
21523
  this.input.clear();
21516
21524
  }
@@ -21534,81 +21542,50 @@ class AuroCombobox extends AuroElement {
21534
21542
  }
21535
21543
 
21536
21544
  updated(changedProperties) {
21537
- // After the component is ready, send direct value changes to auro-menu.
21545
+ // After the component is ready, propagate direct changes down to child components.
21538
21546
  if (changedProperties.has('value')) {
21539
- if (this.value && this.value.length > 0) {
21540
- this.hasValue = true;
21541
- } else {
21542
- this.hasValue = false;
21547
+ // Only flag programmatic refreshes — user-typed value changes must not
21548
+ // suppress the availableOptions branch's showBib(). Firefox batches
21549
+ // 'value' and 'availableOptions' into the same updated() call, so
21550
+ // setting the flag unconditionally here masks the user-typed open path.
21551
+ if (!this._userTyped) {
21552
+ this._programmaticFilterRefresh = true;
21543
21553
  }
21544
21554
 
21545
- if (this.hasValue && !this.input.value && (!this.menu.options || this.menu.options.length === 0)) {
21546
- this.input.value = this.value;
21547
- }
21555
+ if (this.input.value !== this.value) {
21556
+ // Clear menu.value AND menu.optionSelected together. Clearing only
21557
+ // menu.value leaves the previously-selected option element pinned
21558
+ // as menu.optionSelected; a later auroMenu-selectedOption event
21559
+ // would then write its stale .value back into combobox.value
21560
+ // (e.g. Tab-after-Backspace re-selecting the prior option).
21561
+ if (this.menu.value || this.menu.optionSelected) {
21562
+ this.menu.clearSelection();
21563
+ }
21548
21564
 
21549
- // Sync menu.value only when an option actually matches; otherwise clear it.
21550
- if (this.menu.options && this.menu.options.length > 0) {
21551
- if (this.menu.options.some((opt) => opt.value === this.value)) {
21552
- this.setMenuValue(this.value);
21553
- } else if (this.behavior === 'filter') {
21554
- // In filter mode, freeform values aren't allowed. Push the unmatched
21555
- // value through setMenuValue so menu dispatches auroMenu-selectValueFailure
21556
- // and the listener at line 1206 clears combobox.value (mirrors select's
21557
- // pattern). Bypassing setMenuValue here would skip the failure cascade.
21558
- this.setMenuValue(this.value);
21559
- } else {
21560
- if (this.menu.value) {
21561
- this.menu.value = undefined;
21562
- }
21563
- // Sync the display for programmatic freeform value changes (e.g. the
21564
- // swap-values pattern). Guard with _syncingDisplayValue so that the
21565
- // re-entrant input event from base-input.notifyValueChanged() is
21566
- // ignored by handleInputValueChange. Skip the sync when input already
21567
- // matches to avoid spurious events during the normal typing path.
21568
- const nextValue = this.value || '';
21569
- const triggerNeedsSync = this.input.value !== nextValue;
21570
- const bibNeedsSync = Boolean(this.inputInBib && this.inputInBib !== this.input && this.inputInBib.value !== nextValue);
21571
- if (triggerNeedsSync || bibNeedsSync) {
21572
- this._syncingDisplayValue = true;
21573
- if (triggerNeedsSync) {
21574
- this.input.value = nextValue;
21575
- }
21576
- if (bibNeedsSync) {
21577
- this.inputInBib.value = nextValue;
21578
- }
21579
- const pending = [];
21580
- if (triggerNeedsSync) {
21581
- pending.push(this.input.updateComplete);
21582
- }
21583
- if (bibNeedsSync) {
21584
- pending.push(this.inputInBib.updateComplete);
21585
- }
21586
- Promise.all(pending).then(() => {
21587
- if (triggerNeedsSync && this.input.maskInstance && typeof this.input.maskInstance.updateValue === 'function') {
21588
- this.input.maskInstance.updateValue();
21589
- }
21590
- if (bibNeedsSync && this.inputInBib && this.inputInBib.maskInstance && typeof this.inputInBib.maskInstance.updateValue === 'function') {
21591
- this.inputInBib.maskInstance.updateValue();
21592
- }
21593
- this._syncingDisplayValue = false;
21594
- // handleInputValueChange bailed on the flag — refresh the filter.
21595
- this._programmaticFilterRefresh = true;
21596
- this.handleMenuOptions();
21597
- setTimeout(() => {
21598
- this._programmaticFilterRefresh = false;
21599
- }, 0);
21600
- });
21601
- }
21565
+ if (!this.persistInput) {
21566
+ this.syncInputValuesAcrossTriggerAndBib(this.value || '');
21567
+ }
21568
+
21569
+ // Programmatic value with no matching option: updateFilter will close
21570
+ // the bib silently (see line 648 no noMatchOption + 0 results
21571
+ // hides). Announce so screen-reader users hear the request was
21572
+ // dropped. Gated on `input.value !== this.value` so this never fires
21573
+ // for user typing that path always reconciles input.value to
21574
+ // this.value before updated() runs.
21575
+ if (
21576
+ this.value &&
21577
+ this.menu &&
21578
+ this.menu.options &&
21579
+ this.menu.options.length > 0 &&
21580
+ !this.menu.options.some((opt) => opt.value === this.value)
21581
+ ) {
21582
+ announceToScreenReader(this._getAnnouncementRoot(), `No matching option for ${this.value}`);
21602
21583
  }
21603
21584
  }
21585
+
21604
21586
  if (!this.value) {
21605
21587
  this.clear();
21606
21588
  }
21607
- if (this.value && !this.componentHasFocus) {
21608
- // If the value got set programmatically make sure we hide the bib
21609
- // when input is not taking the focus (input can be in dropdown.trigger or in bibtemplate)
21610
- this.hideBib();
21611
- }
21612
21589
 
21613
21590
  // Sync the input and match word, but don't directly set menu.value again
21614
21591
  if (this.menu) {
@@ -21621,7 +21598,7 @@ class AuroCombobox extends AuroElement {
21621
21598
  composed: true,
21622
21599
  detail: {
21623
21600
  optionSelected: this.menu.optionSelected,
21624
- value: this.menu.value
21601
+ value: this.value
21625
21602
  }
21626
21603
  }));
21627
21604
 
@@ -21644,16 +21621,30 @@ class AuroCombobox extends AuroElement {
21644
21621
  // from a programmatic filter refresh after a value swap — the user
21645
21622
  // didn't interact, so we shouldn't auto-open the bib (especially for
21646
21623
  // the no-match path where the existing condition unconditionally fires).
21647
- if (this._programmaticFilterRefresh) {
21648
- this._programmaticFilterRefresh = false;
21649
- } else if ((this.availableOptions.length > 0 && (this.componentHasFocus || this.dropdownOpen)) || (this.menu && this.menu.loading) || (this.availableOptions.length === 0 && this.noMatchOption)) {
21650
- this.showBib();
21624
+ if ((this.menu && !this._programmaticFilterRefresh)) {
21625
+ if (
21626
+ this.availableOptions.length > 0 ||
21627
+ this.menu.loading ||
21628
+ this.noMatchOption
21629
+ ) {
21630
+ if (this._userTyped) {
21631
+ if (!this.dropdownOpen) {
21632
+ this.showBib();
21633
+ }
21634
+ this._userTyped = false;
21635
+ }
21636
+ }
21637
+
21651
21638
  if (!this.availableOptions.includes(this.menu.optionActive)) {
21652
21639
  this.activateFirstEnabledAvailableOption();
21653
21640
  }
21654
- } else if (this.dropdown && this.dropdown.isPopoverVisible) {
21641
+ } else if (!this.dropdown.isBibFullscreen) {
21655
21642
  this.hideBib();
21656
21643
  }
21644
+
21645
+ if (this._programmaticFilterRefresh) {
21646
+ this._programmaticFilterRefresh = false;
21647
+ }
21657
21648
  }
21658
21649
 
21659
21650
  if (changedProperties.has('error')) {
@@ -21795,10 +21786,10 @@ class AuroCombobox extends AuroElement {
21795
21786
  shape="${this.shape}"
21796
21787
  size="${this.size}">
21797
21788
  <${this.inputTag}
21798
- @input="${this.handleInputValueChange}"
21789
+ @input="${this.handleTriggerInputValueChange}"
21799
21790
  appearance="${this.onDark ? 'inverse' : this.appearance}"
21800
21791
  .a11yActivedescendant="${this.dropdownOpen && this.optionActive ? this.optionActive.id : undefined}"
21801
- .a11yExpanded="${this.triggerExpandedState}"
21792
+ .a11yExpanded="${this.dropdownOpen}"
21802
21793
  .a11yControls="${this.dropdownId}"
21803
21794
  .autocomplete="${this.autocomplete}"
21804
21795
  .inputmode="${this.inputmode}"