hotwire_combobox 0.1.42 → 0.1.43

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fc3750ecb7bd58bd28e13d0cc2d543ae4c614535c927c9483e504b643171a8d
4
- data.tar.gz: dc6482de8a4344a6d801797d497f6adec8afcc0357e821e1dff8188ecf4c2f68
3
+ metadata.gz: 5d76e27ecc585f8ad3d4d596d2a6f8fe9916909734027d251cb35264604f192e
4
+ data.tar.gz: eda85b2dd3c21c407c806a5144c4d9b39c40a36c827c7d3b1986a5e27d432cf9
5
5
  SHA512:
6
- metadata.gz: 8884e9e8295b73d37d15762f08c1a69452a7117f99af09dba4369aedea9af2116bc9683127f6f29535813fa5182b21969eb35e4d077edee1f94ce1a6808aadfe
7
- data.tar.gz: b58dd5870a0508912c910b387f7970ffc4a08a16e40612b448e7c35ae244d0f8e02f9dd9356e5cdd8ae7f0c45c1bd65513ece94fcbeba38818c39d39c9ecd870
6
+ metadata.gz: 2cab9b83d4bf4caf3e741f5c12f336241e77f6eec0c97041f25162ecb4aade0d0c58b3ce59e5772b1e5edb89a2a6ad92051c8237f0d40bcee30e550e692add1b
7
+ data.tar.gz: 1d40dfa00ae3884e8c44693abfa542859ed27fb4fb124a90264c03a57ade1dcb31865f7d89078ea4304968f3adcaba51905d940c336a29ed860857fc5b6e5f40
@@ -80,7 +80,7 @@ export default class HwComboboxController extends Concerns(...concerns) {
80
80
 
81
81
  if (inputType && inputType !== "hw:lockInSelection") {
82
82
  if (delay) await sleep(delay)
83
- this._commitFilter({ inputType })
83
+ this._selectBasedOnQuery({ inputType })
84
84
  } else {
85
85
  this._preselectOption()
86
86
  }
@@ -37,6 +37,10 @@ Combobox.AsyncLoading = Base => class extends Base {
37
37
  get _isAsync() {
38
38
  return this.hasAsyncSrcValue
39
39
  }
40
+
41
+ get _isSync() {
42
+ return !this._isAsync
43
+ }
40
44
  };
41
45
 
42
46
  function Concerns(Base, ...mixins) {
@@ -128,16 +132,18 @@ Combobox.Autocomplete = Base => class extends Base {
128
132
  }
129
133
  }
130
134
 
131
- _autocompleteWith(option, { force }) {
132
- if (!this._autocompletesInline && !force) return
135
+ _replaceFullQueryWithAutocompletedValue(option) {
136
+ const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
137
+
138
+ this._fullQuery = autocompletedValue;
139
+ this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
140
+ }
133
141
 
142
+ _autocompleteMissingPortion(option) {
134
143
  const typedValue = this._typedQuery;
135
144
  const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
136
145
 
137
- if (force) {
138
- this._fullQuery = autocompletedValue;
139
- this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
140
- } else if (startsWith(autocompletedValue, typedValue)) {
146
+ if (this._autocompletesInline && startsWith(autocompletedValue, typedValue)) {
141
147
  this._fullQuery = autocompletedValue;
142
148
  this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
143
149
  }
@@ -236,8 +242,13 @@ Combobox.Dialog = Base => class extends Base {
236
242
  };
237
243
 
238
244
  Combobox.Events = Base => class extends Base {
239
- _dispatchSelectionEvent({ isNew }) {
240
- dispatch("hw-combobox:selection", { target: this.element, detail: { ...this._eventableDetails, isNew } });
245
+ _dispatchSelectionEvent({ isNewAndAllowed, previousValue }) {
246
+ if (previousValue !== this._value) {
247
+ dispatch("hw-combobox:selection", {
248
+ target: this.element,
249
+ detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
250
+ });
251
+ }
241
252
  }
242
253
 
243
254
  _dispatchClosedEvent() {
@@ -246,7 +257,7 @@ Combobox.Events = Base => class extends Base {
246
257
 
247
258
  get _eventableDetails() {
248
259
  return {
249
- value: this.hiddenFieldTarget.value,
260
+ value: this._value,
250
261
  display: this._fullQuery,
251
262
  query: this._typedQuery,
252
263
  fieldName: this.hiddenFieldTarget.name,
@@ -533,20 +544,28 @@ async function get(url, options) {
533
544
  // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
534
545
 
535
546
  Combobox.Filtering = Base => class extends Base {
536
- filter(event) {
547
+ filterAndSelect(event) {
548
+ this._filter(event);
549
+
550
+ if (this._isSync) {
551
+ this._selectBasedOnQuery(event);
552
+ }
553
+ }
554
+
555
+ _initializeFiltering() {
556
+ this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
557
+ }
558
+
559
+ _filter(event) {
537
560
  if (this._isAsync) {
538
561
  this._debouncedFilterAsync(event);
539
562
  } else {
540
- this._filterSync(event);
563
+ this._filterSync();
541
564
  }
542
565
 
543
566
  this._actingCombobox.toggleAttribute("data-queried", this._isQueried);
544
567
  }
545
568
 
546
- _initializeFiltering() {
547
- this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
548
- }
549
-
550
569
  _debouncedFilterAsync(event) {
551
570
  this._filterAsync(event);
552
571
  }
@@ -561,27 +580,14 @@ Combobox.Filtering = Base => class extends Base {
561
580
  await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
562
581
  }
563
582
 
564
- _filterSync(event) {
583
+ _filterSync() {
565
584
  this.open();
566
585
  this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }));
567
- this._commitFilter(event);
568
- }
569
-
570
- _commitFilter(event) {
571
- if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
572
- this._selectNew();
573
- } else if (isDeleteEvent(event)) {
574
- this._deselect();
575
- } else if (event.inputType === "hw:lockInSelection") {
576
- this._select(this._ensurableOption);
577
- } else if (this._isOpen) {
578
- this._select(this._visibleOptionElements[0]);
579
- }
580
586
  }
581
587
 
582
588
  _clearQuery() {
583
589
  this._fullQuery = "";
584
- this.filter({ inputType: "deleteContentBackward" });
590
+ this.filterAndSelect({ inputType: "deleteContentBackward" });
585
591
  }
586
592
 
587
593
  get _isQueried() {
@@ -675,9 +681,17 @@ Combobox.NewOptions = Base => class extends Base {
675
681
  };
676
682
 
677
683
  Combobox.Options = Base => class extends Base {
678
- _resetOptions() {
679
- this._deselect();
680
- this.hiddenFieldTarget.name = this.originalNameValue;
684
+ _resetOptionsSilently() {
685
+ this._resetOptions(this._deselect.bind(this));
686
+ }
687
+
688
+ _resetOptionsAndNotify() {
689
+ this._resetOptions(this._deselectAndNotify.bind(this));
690
+ }
691
+
692
+ _resetOptions(deselectionStrategy) {
693
+ this._setName(this.originalNameValue);
694
+ deselectionStrategy();
681
695
  }
682
696
 
683
697
  get _allowNew() {
@@ -705,7 +719,7 @@ Combobox.Options = Base => class extends Base {
705
719
  }
706
720
 
707
721
  get _isUnjustifiablyBlank() {
708
- const valueIsMissing = !this.hiddenFieldTarget.value;
722
+ const valueIsMissing = !this._value;
709
723
  const noBlankOptionSelected = !this._selectedOptionElement;
710
724
 
711
725
  return valueIsMissing && noBlankOptionSelected
@@ -714,8 +728,7 @@ Combobox.Options = Base => class extends Base {
714
728
 
715
729
  Combobox.Selection = Base => class extends Base {
716
730
  selectOptionOnClick(event) {
717
- this.filter(event);
718
- this._select(event.currentTarget, { forceAutocomplete: true });
731
+ this._forceSelectionAndFilter(event.currentTarget, event);
719
732
  this.close();
720
733
  }
721
734
 
@@ -725,91 +738,136 @@ Combobox.Selection = Base => class extends Base {
725
738
  }
726
739
  }
727
740
 
728
- _select(option, { forceAutocomplete = false } = {}) {
729
- this._resetOptions();
730
-
731
- if (option) {
732
- this._autocompleteWith(option, { force: forceAutocomplete });
733
- this._commitSelection(option, { selected: true });
734
- this._markValid();
735
- } else {
741
+ _selectBasedOnQuery(event) {
742
+ if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
743
+ this._selectNew();
744
+ } else if (isDeleteEvent(event)) {
745
+ this._deselect();
746
+ } else if (event.inputType === "hw:lockInSelection" && this._ensurableOption) {
747
+ this._selectAndAutocompleteMissingPortion(this._ensurableOption);
748
+ } else if (this._isOpen && this._visibleOptionElements[0]) {
749
+ this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0]);
750
+ } else if (this._isOpen) {
751
+ this._resetOptionsAndNotify();
736
752
  this._markInvalid();
737
- }
753
+ } else ;
738
754
  }
739
755
 
740
- _commitSelection(option, { selected }) {
741
- this._markSelected(option, { selected });
756
+ _select(option, autocompleteStrategy) {
757
+ const previousValue = this._value;
742
758
 
743
- if (selected) {
744
- this.hiddenFieldTarget.value = option.dataset.value;
745
- option.scrollIntoView({ block: "nearest" });
746
- }
759
+ this._resetOptionsSilently();
747
760
 
748
- this._dispatchSelectionEvent({ isNew: false });
761
+ autocompleteStrategy(option);
762
+
763
+ this._setValue(option.dataset.value);
764
+ this._markSelected(option);
765
+ this._markValid();
766
+ this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
767
+
768
+ option.scrollIntoView({ block: "nearest" });
749
769
  }
750
770
 
751
- _markSelected(option, { selected }) {
752
- if (this.hasSelectedClass) {
753
- option.classList.toggle(this.selectedClass, selected);
754
- }
771
+ _selectNew() {
772
+ const previousValue = this._value;
755
773
 
756
- option.setAttribute("aria-selected", selected);
757
- this._setActiveDescendant(selected ? option.id : "");
774
+ this._resetOptionsSilently();
775
+ this._setValue(this._fullQuery);
776
+ this._setName(this.nameWhenNewValue);
777
+ this._markValid();
778
+ this._dispatchSelectionEvent({ isNewAndAllowed: true, previousValue: previousValue });
758
779
  }
759
780
 
760
781
  _deselect() {
761
- const option = this._selectedOptionElement;
782
+ const previousValue = this._value;
762
783
 
763
- if (option) this._commitSelection(option, { selected: false });
784
+ if (this._selectedOptionElement) {
785
+ this._markNotSelected(this._selectedOptionElement);
786
+ }
764
787
 
765
- this.hiddenFieldTarget.value = null;
788
+ this._setValue(null);
766
789
  this._setActiveDescendant("");
767
790
 
768
- if (!option) this._dispatchSelectionEvent({ isNew: false });
791
+ return previousValue
769
792
  }
770
793
 
771
- _selectNew() {
772
- this._resetOptions();
773
- this.hiddenFieldTarget.value = this._fullQuery;
774
- this.hiddenFieldTarget.name = this.nameWhenNewValue;
775
- this._markValid();
794
+ _deselectAndNotify() {
795
+ const previousValue = this._deselect();
796
+ this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
797
+ }
776
798
 
777
- this._dispatchSelectionEvent({ isNew: true });
799
+ _forceSelectionAndFilter(option, event) {
800
+ this._forceSelectionWithoutFiltering(option);
801
+ this._filter(event);
802
+ }
803
+
804
+ _forceSelectionWithoutFiltering(option) {
805
+ this._selectAndReplaceFullQuery(option);
778
806
  }
779
807
 
780
808
  _selectIndex(index) {
781
809
  const option = wrapAroundAccess(this._visibleOptionElements, index);
782
- this._select(option, { forceAutocomplete: true });
810
+ this._forceSelectionWithoutFiltering(option);
783
811
  }
784
812
 
785
813
  _preselectOption() {
786
814
  if (this._hasValueButNoSelection && this._allOptions.length < 100) {
787
815
  const option = this._allOptions.find(option => {
788
- return option.dataset.value === this.hiddenFieldTarget.value
816
+ return option.dataset.value === this._value
789
817
  });
790
818
 
791
- if (option) this._markSelected(option, { selected: true });
819
+ if (option) this._markSelected(option);
792
820
  }
793
821
  }
794
822
 
823
+ _selectAndReplaceFullQuery(option) {
824
+ this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this));
825
+ }
826
+
827
+ _selectAndAutocompleteMissingPortion(option) {
828
+ this._select(option, this._autocompleteMissingPortion.bind(this));
829
+ }
830
+
795
831
  _lockInSelection() {
796
832
  if (this._shouldLockInSelection) {
797
- this._select(this._ensurableOption, { forceAutocomplete: true });
798
- this.filter({ inputType: "hw:lockInSelection" });
833
+ this._forceSelectionAndFilter(this._ensurableOption, { inputType: "hw:lockInSelection" });
799
834
  }
835
+ }
800
836
 
801
- if (this._isUnjustifiablyBlank) {
802
- this._deselect();
803
- this._clearQuery();
804
- }
837
+ _markSelected(option) {
838
+ if (this.hasSelectedClass) option.classList.add(this.selectedClass);
839
+ option.setAttribute("aria-selected", true);
840
+ this._setActiveDescendant(option.id);
841
+ }
842
+
843
+ _markNotSelected(option) {
844
+ if (this.hasSelectedClass) option.classList.remove(this.selectedClass);
845
+ option.removeAttribute("aria-selected");
846
+ this._removeActiveDescendant();
805
847
  }
806
848
 
807
849
  _setActiveDescendant(id) {
808
850
  this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id));
809
851
  }
810
852
 
853
+ _removeActiveDescendant() {
854
+ this._setActiveDescendant("");
855
+ }
856
+
857
+ _setValue(value) {
858
+ this.hiddenFieldTarget.value = value;
859
+ }
860
+
861
+ _setName(value) {
862
+ this.hiddenFieldTarget.name = value;
863
+ }
864
+
865
+ get _value() {
866
+ return this.hiddenFieldTarget.value
867
+ }
868
+
811
869
  get _hasValueButNoSelection() {
812
- return this.hiddenFieldTarget.value && !this._selectedOptionElement
870
+ return this._value && !this._selectedOptionElement
813
871
  }
814
872
 
815
873
  get _shouldLockInSelection() {
@@ -1103,7 +1161,10 @@ Combobox.Toggle = Base => class extends Base {
1103
1161
  close() {
1104
1162
  if (this._isOpen) {
1105
1163
  this._lockInSelection();
1164
+ this._clearInvalidQuery();
1165
+
1106
1166
  this.expandedValue = false;
1167
+
1107
1168
  this._dispatchClosedEvent();
1108
1169
  }
1109
1170
  }
@@ -1173,6 +1234,8 @@ Combobox.Toggle = Base => class extends Base {
1173
1234
  this._actingCombobox.setAttribute("aria-expanded", true); // needs to happen after setting acting combobox
1174
1235
  }
1175
1236
 
1237
+ // +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
1238
+ // it interprets a change in `expandedValue` — whereas `.close()` is only called internally by us.
1176
1239
  _collapse() {
1177
1240
  this._actingCombobox.setAttribute("aria-expanded", false); // needs to happen before resetting acting combobox
1178
1241
 
@@ -1213,6 +1276,13 @@ Combobox.Toggle = Base => class extends Base {
1213
1276
  enableBodyScroll(this.dialogListboxTarget);
1214
1277
  }
1215
1278
 
1279
+ _clearInvalidQuery() {
1280
+ if (this._isUnjustifiablyBlank) {
1281
+ this._deselect();
1282
+ this._clearQuery();
1283
+ }
1284
+ }
1285
+
1216
1286
  get _isOpen() {
1217
1287
  return this.expandedValue
1218
1288
  }
@@ -1256,7 +1326,7 @@ Combobox.Validity = Base => class extends Base {
1256
1326
  // +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
1257
1327
  // because the `required` attribute is only forwarded to the `comboboxTarget` element
1258
1328
  get _valueIsInvalid() {
1259
- const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value;
1329
+ const isRequiredAndEmpty = this.comboboxTarget.required && !this._value;
1260
1330
  return isRequiredAndEmpty
1261
1331
  }
1262
1332
  };
@@ -1339,7 +1409,7 @@ class HwComboboxController extends Concerns(...concerns) {
1339
1409
 
1340
1410
  if (inputType && inputType !== "hw:lockInSelection") {
1341
1411
  if (delay) await sleep(delay);
1342
- this._commitFilter({ inputType });
1412
+ this._selectBasedOnQuery({ inputType });
1343
1413
  } else {
1344
1414
  this._preselectOption();
1345
1415
  }
@@ -41,6 +41,10 @@
41
41
  get _isAsync() {
42
42
  return this.hasAsyncSrcValue
43
43
  }
44
+
45
+ get _isSync() {
46
+ return !this._isAsync
47
+ }
44
48
  };
45
49
 
46
50
  function Concerns(Base, ...mixins) {
@@ -132,16 +136,18 @@
132
136
  }
133
137
  }
134
138
 
135
- _autocompleteWith(option, { force }) {
136
- if (!this._autocompletesInline && !force) return
139
+ _replaceFullQueryWithAutocompletedValue(option) {
140
+ const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
141
+
142
+ this._fullQuery = autocompletedValue;
143
+ this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
144
+ }
137
145
 
146
+ _autocompleteMissingPortion(option) {
138
147
  const typedValue = this._typedQuery;
139
148
  const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue);
140
149
 
141
- if (force) {
142
- this._fullQuery = autocompletedValue;
143
- this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length);
144
- } else if (startsWith(autocompletedValue, typedValue)) {
150
+ if (this._autocompletesInline && startsWith(autocompletedValue, typedValue)) {
145
151
  this._fullQuery = autocompletedValue;
146
152
  this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length);
147
153
  }
@@ -240,8 +246,13 @@
240
246
  };
241
247
 
242
248
  Combobox.Events = Base => class extends Base {
243
- _dispatchSelectionEvent({ isNew }) {
244
- dispatch("hw-combobox:selection", { target: this.element, detail: { ...this._eventableDetails, isNew } });
249
+ _dispatchSelectionEvent({ isNewAndAllowed, previousValue }) {
250
+ if (previousValue !== this._value) {
251
+ dispatch("hw-combobox:selection", {
252
+ target: this.element,
253
+ detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
254
+ });
255
+ }
245
256
  }
246
257
 
247
258
  _dispatchClosedEvent() {
@@ -250,7 +261,7 @@
250
261
 
251
262
  get _eventableDetails() {
252
263
  return {
253
- value: this.hiddenFieldTarget.value,
264
+ value: this._value,
254
265
  display: this._fullQuery,
255
266
  query: this._typedQuery,
256
267
  fieldName: this.hiddenFieldTarget.name,
@@ -537,20 +548,28 @@
537
548
  // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
538
549
 
539
550
  Combobox.Filtering = Base => class extends Base {
540
- filter(event) {
551
+ filterAndSelect(event) {
552
+ this._filter(event);
553
+
554
+ if (this._isSync) {
555
+ this._selectBasedOnQuery(event);
556
+ }
557
+ }
558
+
559
+ _initializeFiltering() {
560
+ this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
561
+ }
562
+
563
+ _filter(event) {
541
564
  if (this._isAsync) {
542
565
  this._debouncedFilterAsync(event);
543
566
  } else {
544
- this._filterSync(event);
567
+ this._filterSync();
545
568
  }
546
569
 
547
570
  this._actingCombobox.toggleAttribute("data-queried", this._isQueried);
548
571
  }
549
572
 
550
- _initializeFiltering() {
551
- this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this));
552
- }
553
-
554
573
  _debouncedFilterAsync(event) {
555
574
  this._filterAsync(event);
556
575
  }
@@ -565,27 +584,14 @@
565
584
  await get(this.asyncSrcValue, { responseKind: "turbo-stream", query });
566
585
  }
567
586
 
568
- _filterSync(event) {
587
+ _filterSync() {
569
588
  this.open();
570
589
  this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }));
571
- this._commitFilter(event);
572
- }
573
-
574
- _commitFilter(event) {
575
- if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
576
- this._selectNew();
577
- } else if (isDeleteEvent(event)) {
578
- this._deselect();
579
- } else if (event.inputType === "hw:lockInSelection") {
580
- this._select(this._ensurableOption);
581
- } else if (this._isOpen) {
582
- this._select(this._visibleOptionElements[0]);
583
- }
584
590
  }
585
591
 
586
592
  _clearQuery() {
587
593
  this._fullQuery = "";
588
- this.filter({ inputType: "deleteContentBackward" });
594
+ this.filterAndSelect({ inputType: "deleteContentBackward" });
589
595
  }
590
596
 
591
597
  get _isQueried() {
@@ -679,9 +685,17 @@
679
685
  };
680
686
 
681
687
  Combobox.Options = Base => class extends Base {
682
- _resetOptions() {
683
- this._deselect();
684
- this.hiddenFieldTarget.name = this.originalNameValue;
688
+ _resetOptionsSilently() {
689
+ this._resetOptions(this._deselect.bind(this));
690
+ }
691
+
692
+ _resetOptionsAndNotify() {
693
+ this._resetOptions(this._deselectAndNotify.bind(this));
694
+ }
695
+
696
+ _resetOptions(deselectionStrategy) {
697
+ this._setName(this.originalNameValue);
698
+ deselectionStrategy();
685
699
  }
686
700
 
687
701
  get _allowNew() {
@@ -709,7 +723,7 @@
709
723
  }
710
724
 
711
725
  get _isUnjustifiablyBlank() {
712
- const valueIsMissing = !this.hiddenFieldTarget.value;
726
+ const valueIsMissing = !this._value;
713
727
  const noBlankOptionSelected = !this._selectedOptionElement;
714
728
 
715
729
  return valueIsMissing && noBlankOptionSelected
@@ -718,8 +732,7 @@
718
732
 
719
733
  Combobox.Selection = Base => class extends Base {
720
734
  selectOptionOnClick(event) {
721
- this.filter(event);
722
- this._select(event.currentTarget, { forceAutocomplete: true });
735
+ this._forceSelectionAndFilter(event.currentTarget, event);
723
736
  this.close();
724
737
  }
725
738
 
@@ -729,91 +742,136 @@
729
742
  }
730
743
  }
731
744
 
732
- _select(option, { forceAutocomplete = false } = {}) {
733
- this._resetOptions();
734
-
735
- if (option) {
736
- this._autocompleteWith(option, { force: forceAutocomplete });
737
- this._commitSelection(option, { selected: true });
738
- this._markValid();
739
- } else {
745
+ _selectBasedOnQuery(event) {
746
+ if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
747
+ this._selectNew();
748
+ } else if (isDeleteEvent(event)) {
749
+ this._deselect();
750
+ } else if (event.inputType === "hw:lockInSelection" && this._ensurableOption) {
751
+ this._selectAndAutocompleteMissingPortion(this._ensurableOption);
752
+ } else if (this._isOpen && this._visibleOptionElements[0]) {
753
+ this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0]);
754
+ } else if (this._isOpen) {
755
+ this._resetOptionsAndNotify();
740
756
  this._markInvalid();
741
- }
757
+ } else ;
742
758
  }
743
759
 
744
- _commitSelection(option, { selected }) {
745
- this._markSelected(option, { selected });
760
+ _select(option, autocompleteStrategy) {
761
+ const previousValue = this._value;
746
762
 
747
- if (selected) {
748
- this.hiddenFieldTarget.value = option.dataset.value;
749
- option.scrollIntoView({ block: "nearest" });
750
- }
763
+ this._resetOptionsSilently();
751
764
 
752
- this._dispatchSelectionEvent({ isNew: false });
765
+ autocompleteStrategy(option);
766
+
767
+ this._setValue(option.dataset.value);
768
+ this._markSelected(option);
769
+ this._markValid();
770
+ this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
771
+
772
+ option.scrollIntoView({ block: "nearest" });
753
773
  }
754
774
 
755
- _markSelected(option, { selected }) {
756
- if (this.hasSelectedClass) {
757
- option.classList.toggle(this.selectedClass, selected);
758
- }
775
+ _selectNew() {
776
+ const previousValue = this._value;
759
777
 
760
- option.setAttribute("aria-selected", selected);
761
- this._setActiveDescendant(selected ? option.id : "");
778
+ this._resetOptionsSilently();
779
+ this._setValue(this._fullQuery);
780
+ this._setName(this.nameWhenNewValue);
781
+ this._markValid();
782
+ this._dispatchSelectionEvent({ isNewAndAllowed: true, previousValue: previousValue });
762
783
  }
763
784
 
764
785
  _deselect() {
765
- const option = this._selectedOptionElement;
786
+ const previousValue = this._value;
766
787
 
767
- if (option) this._commitSelection(option, { selected: false });
788
+ if (this._selectedOptionElement) {
789
+ this._markNotSelected(this._selectedOptionElement);
790
+ }
768
791
 
769
- this.hiddenFieldTarget.value = null;
792
+ this._setValue(null);
770
793
  this._setActiveDescendant("");
771
794
 
772
- if (!option) this._dispatchSelectionEvent({ isNew: false });
795
+ return previousValue
773
796
  }
774
797
 
775
- _selectNew() {
776
- this._resetOptions();
777
- this.hiddenFieldTarget.value = this._fullQuery;
778
- this.hiddenFieldTarget.name = this.nameWhenNewValue;
779
- this._markValid();
798
+ _deselectAndNotify() {
799
+ const previousValue = this._deselect();
800
+ this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue });
801
+ }
780
802
 
781
- this._dispatchSelectionEvent({ isNew: true });
803
+ _forceSelectionAndFilter(option, event) {
804
+ this._forceSelectionWithoutFiltering(option);
805
+ this._filter(event);
806
+ }
807
+
808
+ _forceSelectionWithoutFiltering(option) {
809
+ this._selectAndReplaceFullQuery(option);
782
810
  }
783
811
 
784
812
  _selectIndex(index) {
785
813
  const option = wrapAroundAccess(this._visibleOptionElements, index);
786
- this._select(option, { forceAutocomplete: true });
814
+ this._forceSelectionWithoutFiltering(option);
787
815
  }
788
816
 
789
817
  _preselectOption() {
790
818
  if (this._hasValueButNoSelection && this._allOptions.length < 100) {
791
819
  const option = this._allOptions.find(option => {
792
- return option.dataset.value === this.hiddenFieldTarget.value
820
+ return option.dataset.value === this._value
793
821
  });
794
822
 
795
- if (option) this._markSelected(option, { selected: true });
823
+ if (option) this._markSelected(option);
796
824
  }
797
825
  }
798
826
 
827
+ _selectAndReplaceFullQuery(option) {
828
+ this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this));
829
+ }
830
+
831
+ _selectAndAutocompleteMissingPortion(option) {
832
+ this._select(option, this._autocompleteMissingPortion.bind(this));
833
+ }
834
+
799
835
  _lockInSelection() {
800
836
  if (this._shouldLockInSelection) {
801
- this._select(this._ensurableOption, { forceAutocomplete: true });
802
- this.filter({ inputType: "hw:lockInSelection" });
837
+ this._forceSelectionAndFilter(this._ensurableOption, { inputType: "hw:lockInSelection" });
803
838
  }
839
+ }
804
840
 
805
- if (this._isUnjustifiablyBlank) {
806
- this._deselect();
807
- this._clearQuery();
808
- }
841
+ _markSelected(option) {
842
+ if (this.hasSelectedClass) option.classList.add(this.selectedClass);
843
+ option.setAttribute("aria-selected", true);
844
+ this._setActiveDescendant(option.id);
845
+ }
846
+
847
+ _markNotSelected(option) {
848
+ if (this.hasSelectedClass) option.classList.remove(this.selectedClass);
849
+ option.removeAttribute("aria-selected");
850
+ this._removeActiveDescendant();
809
851
  }
810
852
 
811
853
  _setActiveDescendant(id) {
812
854
  this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id));
813
855
  }
814
856
 
857
+ _removeActiveDescendant() {
858
+ this._setActiveDescendant("");
859
+ }
860
+
861
+ _setValue(value) {
862
+ this.hiddenFieldTarget.value = value;
863
+ }
864
+
865
+ _setName(value) {
866
+ this.hiddenFieldTarget.name = value;
867
+ }
868
+
869
+ get _value() {
870
+ return this.hiddenFieldTarget.value
871
+ }
872
+
815
873
  get _hasValueButNoSelection() {
816
- return this.hiddenFieldTarget.value && !this._selectedOptionElement
874
+ return this._value && !this._selectedOptionElement
817
875
  }
818
876
 
819
877
  get _shouldLockInSelection() {
@@ -1107,7 +1165,10 @@
1107
1165
  close() {
1108
1166
  if (this._isOpen) {
1109
1167
  this._lockInSelection();
1168
+ this._clearInvalidQuery();
1169
+
1110
1170
  this.expandedValue = false;
1171
+
1111
1172
  this._dispatchClosedEvent();
1112
1173
  }
1113
1174
  }
@@ -1177,6 +1238,8 @@
1177
1238
  this._actingCombobox.setAttribute("aria-expanded", true); // needs to happen after setting acting combobox
1178
1239
  }
1179
1240
 
1241
+ // +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
1242
+ // it interprets a change in `expandedValue` — whereas `.close()` is only called internally by us.
1180
1243
  _collapse() {
1181
1244
  this._actingCombobox.setAttribute("aria-expanded", false); // needs to happen before resetting acting combobox
1182
1245
 
@@ -1217,6 +1280,13 @@
1217
1280
  enableBodyScroll(this.dialogListboxTarget);
1218
1281
  }
1219
1282
 
1283
+ _clearInvalidQuery() {
1284
+ if (this._isUnjustifiablyBlank) {
1285
+ this._deselect();
1286
+ this._clearQuery();
1287
+ }
1288
+ }
1289
+
1220
1290
  get _isOpen() {
1221
1291
  return this.expandedValue
1222
1292
  }
@@ -1260,7 +1330,7 @@
1260
1330
  // +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
1261
1331
  // because the `required` attribute is only forwarded to the `comboboxTarget` element
1262
1332
  get _valueIsInvalid() {
1263
- const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value;
1333
+ const isRequiredAndEmpty = this.comboboxTarget.required && !this._value;
1264
1334
  return isRequiredAndEmpty
1265
1335
  }
1266
1336
  };
@@ -1343,7 +1413,7 @@
1343
1413
 
1344
1414
  if (inputType && inputType !== "hw:lockInSelection") {
1345
1415
  if (delay) await sleep(delay);
1346
- this._commitFilter({ inputType });
1416
+ this._selectBasedOnQuery({ inputType });
1347
1417
  } else {
1348
1418
  this._preselectOption();
1349
1419
  }
@@ -4,4 +4,8 @@ Combobox.AsyncLoading = Base => class extends Base {
4
4
  get _isAsync() {
5
5
  return this.hasAsyncSrcValue
6
6
  }
7
+
8
+ get _isSync() {
9
+ return !this._isAsync
10
+ }
7
11
  }
@@ -8,16 +8,18 @@ Combobox.Autocomplete = Base => class extends Base {
8
8
  }
9
9
  }
10
10
 
11
- _autocompleteWith(option, { force }) {
12
- if (!this._autocompletesInline && !force) return
11
+ _replaceFullQueryWithAutocompletedValue(option) {
12
+ const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
13
+
14
+ this._fullQuery = autocompletedValue
15
+ this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
16
+ }
13
17
 
18
+ _autocompleteMissingPortion(option) {
14
19
  const typedValue = this._typedQuery
15
20
  const autocompletedValue = option.getAttribute(this.autocompletableAttributeValue)
16
21
 
17
- if (force) {
18
- this._fullQuery = autocompletedValue
19
- this._actingCombobox.setSelectionRange(autocompletedValue.length, autocompletedValue.length)
20
- } else if (startsWith(autocompletedValue, typedValue)) {
22
+ if (this._autocompletesInline && startsWith(autocompletedValue, typedValue)) {
21
23
  this._fullQuery = autocompletedValue
22
24
  this._actingCombobox.setSelectionRange(typedValue.length, autocompletedValue.length)
23
25
  }
@@ -2,8 +2,13 @@ import Combobox from "hw_combobox/models/combobox/base"
2
2
  import { dispatch } from "hw_combobox/helpers"
3
3
 
4
4
  Combobox.Events = Base => class extends Base {
5
- _dispatchSelectionEvent({ isNew }) {
6
- dispatch("hw-combobox:selection", { target: this.element, detail: { ...this._eventableDetails, isNew } })
5
+ _dispatchSelectionEvent({ isNewAndAllowed, previousValue }) {
6
+ if (previousValue !== this._value) {
7
+ dispatch("hw-combobox:selection", {
8
+ target: this.element,
9
+ detail: { ...this._eventableDetails, isNewAndAllowed, previousValue }
10
+ })
11
+ }
7
12
  }
8
13
 
9
14
  _dispatchClosedEvent() {
@@ -12,7 +17,7 @@ Combobox.Events = Base => class extends Base {
12
17
 
13
18
  get _eventableDetails() {
14
19
  return {
15
- value: this.hiddenFieldTarget.value,
20
+ value: this._value,
16
21
  display: this._fullQuery,
17
22
  query: this._typedQuery,
18
23
  fieldName: this.hiddenFieldTarget.name,
@@ -1,23 +1,33 @@
1
1
 
2
2
  import Combobox from "hw_combobox/models/combobox/base"
3
- import { applyFilter, debounce, isDeleteEvent, unselectedPortion } from "hw_combobox/helpers"
3
+ import { applyFilter, debounce, unselectedPortion } from "hw_combobox/helpers"
4
4
  import { get } from "hw_combobox/vendor/requestjs"
5
5
 
6
6
  Combobox.Filtering = Base => class extends Base {
7
- filter(event) {
8
- if (this._isAsync) {
9
- this._debouncedFilterAsync(event)
7
+ filterAndSelect(event) {
8
+ this._filter(event)
9
+
10
+ if (this._isSync) {
11
+ this._selectBasedOnQuery(event)
10
12
  } else {
11
- this._filterSync(event)
13
+ // noop, async selection is handled by stimulus callbacks
12
14
  }
13
-
14
- this._actingCombobox.toggleAttribute("data-queried", this._isQueried)
15
15
  }
16
16
 
17
17
  _initializeFiltering() {
18
18
  this._debouncedFilterAsync = debounce(this._debouncedFilterAsync.bind(this))
19
19
  }
20
20
 
21
+ _filter(event) {
22
+ if (this._isAsync) {
23
+ this._debouncedFilterAsync(event)
24
+ } else {
25
+ this._filterSync()
26
+ }
27
+
28
+ this._actingCombobox.toggleAttribute("data-queried", this._isQueried)
29
+ }
30
+
21
31
  _debouncedFilterAsync(event) {
22
32
  this._filterAsync(event)
23
33
  }
@@ -32,27 +42,14 @@ Combobox.Filtering = Base => class extends Base {
32
42
  await get(this.asyncSrcValue, { responseKind: "turbo-stream", query })
33
43
  }
34
44
 
35
- _filterSync(event) {
45
+ _filterSync() {
36
46
  this.open()
37
47
  this._allOptionElements.forEach(applyFilter(this._fullQuery, { matching: this.filterableAttributeValue }))
38
- this._commitFilter(event)
39
- }
40
-
41
- _commitFilter(event) {
42
- if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
43
- this._selectNew()
44
- } else if (isDeleteEvent(event)) {
45
- this._deselect()
46
- } else if (event.inputType === "hw:lockInSelection") {
47
- this._select(this._ensurableOption)
48
- } else if (this._isOpen) {
49
- this._select(this._visibleOptionElements[0])
50
- }
51
48
  }
52
49
 
53
50
  _clearQuery() {
54
51
  this._fullQuery = ""
55
- this.filter({ inputType: "deleteContentBackward" })
52
+ this.filterAndSelect({ inputType: "deleteContentBackward" })
56
53
  }
57
54
 
58
55
  get _isQueried() {
@@ -2,9 +2,17 @@ import Combobox from "hw_combobox/models/combobox/base"
2
2
  import { visible } from "hw_combobox/helpers"
3
3
 
4
4
  Combobox.Options = Base => class extends Base {
5
- _resetOptions() {
6
- this._deselect()
7
- this.hiddenFieldTarget.name = this.originalNameValue
5
+ _resetOptionsSilently() {
6
+ this._resetOptions(this._deselect.bind(this))
7
+ }
8
+
9
+ _resetOptionsAndNotify() {
10
+ this._resetOptions(this._deselectAndNotify.bind(this))
11
+ }
12
+
13
+ _resetOptions(deselectionStrategy) {
14
+ this._setName(this.originalNameValue)
15
+ deselectionStrategy()
8
16
  }
9
17
 
10
18
  get _allowNew() {
@@ -32,7 +40,7 @@ Combobox.Options = Base => class extends Base {
32
40
  }
33
41
 
34
42
  get _isUnjustifiablyBlank() {
35
- const valueIsMissing = !this.hiddenFieldTarget.value
43
+ const valueIsMissing = !this._value
36
44
  const noBlankOptionSelected = !this._selectedOptionElement
37
45
 
38
46
  return valueIsMissing && noBlankOptionSelected
@@ -1,10 +1,9 @@
1
1
  import Combobox from "hw_combobox/models/combobox/base"
2
- import { wrapAroundAccess } from "hw_combobox/helpers"
2
+ import { wrapAroundAccess, isDeleteEvent } from "hw_combobox/helpers"
3
3
 
4
4
  Combobox.Selection = Base => class extends Base {
5
5
  selectOptionOnClick(event) {
6
- this.filter(event)
7
- this._select(event.currentTarget, { forceAutocomplete: true })
6
+ this._forceSelectionAndFilter(event.currentTarget, event)
8
7
  this.close()
9
8
  }
10
9
 
@@ -14,91 +13,143 @@ Combobox.Selection = Base => class extends Base {
14
13
  }
15
14
  }
16
15
 
17
- _select(option, { forceAutocomplete = false } = {}) {
18
- this._resetOptions()
19
-
20
- if (option) {
21
- this._autocompleteWith(option, { force: forceAutocomplete })
22
- this._commitSelection(option, { selected: true })
23
- this._markValid()
24
- } else {
16
+ _selectBasedOnQuery(event) {
17
+ if (this._shouldTreatAsNewOptionForFiltering(!isDeleteEvent(event))) {
18
+ this._selectNew()
19
+ } else if (isDeleteEvent(event)) {
20
+ this._deselect()
21
+ } else if (event.inputType === "hw:lockInSelection" && this._ensurableOption) {
22
+ this._selectAndAutocompleteMissingPortion(this._ensurableOption)
23
+ } else if (this._isOpen && this._visibleOptionElements[0]) {
24
+ this._selectAndAutocompleteMissingPortion(this._visibleOptionElements[0])
25
+ } else if (this._isOpen) {
26
+ this._resetOptionsAndNotify()
25
27
  this._markInvalid()
28
+ } else {
29
+ // When selecting from an async dialog listbox: selection is forced, the listbox is filtered,
30
+ // and the dialog is closed. Filtering ends with an `endOfOptionsStream` target connected
31
+ // to the now invisible combobox, which is now closed because Turbo waits for "nextRepaint"
32
+ // before rendering turbo streams. This ultimately calls +_selectBasedOnQuery+. We do want
33
+ // to call +_selectBasedOnQuery+ in this case to account for e.g. selection of
34
+ // new options. But we will noop here if it's none of the cases checked above.
26
35
  }
27
36
  }
28
37
 
29
- _commitSelection(option, { selected }) {
30
- this._markSelected(option, { selected })
38
+ _select(option, autocompleteStrategy) {
39
+ const previousValue = this._value
31
40
 
32
- if (selected) {
33
- this.hiddenFieldTarget.value = option.dataset.value
34
- option.scrollIntoView({ block: "nearest" })
35
- }
41
+ this._resetOptionsSilently()
36
42
 
37
- this._dispatchSelectionEvent({ isNew: false })
43
+ autocompleteStrategy(option)
44
+
45
+ this._setValue(option.dataset.value)
46
+ this._markSelected(option)
47
+ this._markValid()
48
+ this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
49
+
50
+ option.scrollIntoView({ block: "nearest" })
38
51
  }
39
52
 
40
- _markSelected(option, { selected }) {
41
- if (this.hasSelectedClass) {
42
- option.classList.toggle(this.selectedClass, selected)
43
- }
53
+ _selectNew() {
54
+ const previousValue = this._value
44
55
 
45
- option.setAttribute("aria-selected", selected)
46
- this._setActiveDescendant(selected ? option.id : "")
56
+ this._resetOptionsSilently()
57
+ this._setValue(this._fullQuery)
58
+ this._setName(this.nameWhenNewValue)
59
+ this._markValid()
60
+ this._dispatchSelectionEvent({ isNewAndAllowed: true, previousValue: previousValue })
47
61
  }
48
62
 
49
63
  _deselect() {
50
- const option = this._selectedOptionElement
64
+ const previousValue = this._value
51
65
 
52
- if (option) this._commitSelection(option, { selected: false })
66
+ if (this._selectedOptionElement) {
67
+ this._markNotSelected(this._selectedOptionElement)
68
+ }
53
69
 
54
- this.hiddenFieldTarget.value = null
70
+ this._setValue(null)
55
71
  this._setActiveDescendant("")
56
72
 
57
- if (!option) this._dispatchSelectionEvent({ isNew: false })
73
+ return previousValue
58
74
  }
59
75
 
60
- _selectNew() {
61
- this._resetOptions()
62
- this.hiddenFieldTarget.value = this._fullQuery
63
- this.hiddenFieldTarget.name = this.nameWhenNewValue
64
- this._markValid()
76
+ _deselectAndNotify() {
77
+ const previousValue = this._deselect()
78
+ this._dispatchSelectionEvent({ isNewAndAllowed: false, previousValue: previousValue })
79
+ }
80
+
81
+ _forceSelectionAndFilter(option, event) {
82
+ this._forceSelectionWithoutFiltering(option)
83
+ this._filter(event)
84
+ }
65
85
 
66
- this._dispatchSelectionEvent({ isNew: true })
86
+ _forceSelectionWithoutFiltering(option) {
87
+ this._selectAndReplaceFullQuery(option)
67
88
  }
68
89
 
69
90
  _selectIndex(index) {
70
91
  const option = wrapAroundAccess(this._visibleOptionElements, index)
71
- this._select(option, { forceAutocomplete: true })
92
+ this._forceSelectionWithoutFiltering(option)
72
93
  }
73
94
 
74
95
  _preselectOption() {
75
96
  if (this._hasValueButNoSelection && this._allOptions.length < 100) {
76
97
  const option = this._allOptions.find(option => {
77
- return option.dataset.value === this.hiddenFieldTarget.value
98
+ return option.dataset.value === this._value
78
99
  })
79
100
 
80
- if (option) this._markSelected(option, { selected: true })
101
+ if (option) this._markSelected(option)
81
102
  }
82
103
  }
83
104
 
105
+ _selectAndReplaceFullQuery(option) {
106
+ this._select(option, this._replaceFullQueryWithAutocompletedValue.bind(this))
107
+ }
108
+
109
+ _selectAndAutocompleteMissingPortion(option) {
110
+ this._select(option, this._autocompleteMissingPortion.bind(this))
111
+ }
112
+
84
113
  _lockInSelection() {
85
114
  if (this._shouldLockInSelection) {
86
- this._select(this._ensurableOption, { forceAutocomplete: true })
87
- this.filter({ inputType: "hw:lockInSelection" })
115
+ this._forceSelectionAndFilter(this._ensurableOption, { inputType: "hw:lockInSelection" })
88
116
  }
117
+ }
89
118
 
90
- if (this._isUnjustifiablyBlank) {
91
- this._deselect()
92
- this._clearQuery()
93
- }
119
+ _markSelected(option) {
120
+ if (this.hasSelectedClass) option.classList.add(this.selectedClass)
121
+ option.setAttribute("aria-selected", true)
122
+ this._setActiveDescendant(option.id)
123
+ }
124
+
125
+ _markNotSelected(option) {
126
+ if (this.hasSelectedClass) option.classList.remove(this.selectedClass)
127
+ option.removeAttribute("aria-selected")
128
+ this._removeActiveDescendant()
94
129
  }
95
130
 
96
131
  _setActiveDescendant(id) {
97
132
  this._forAllComboboxes(el => el.setAttribute("aria-activedescendant", id))
98
133
  }
99
134
 
135
+ _removeActiveDescendant() {
136
+ this._setActiveDescendant("")
137
+ }
138
+
139
+ _setValue(value) {
140
+ this.hiddenFieldTarget.value = value
141
+ }
142
+
143
+ _setName(value) {
144
+ this.hiddenFieldTarget.name = value
145
+ }
146
+
147
+ get _value() {
148
+ return this.hiddenFieldTarget.value
149
+ }
150
+
100
151
  get _hasValueButNoSelection() {
101
- return this.hiddenFieldTarget.value && !this._selectedOptionElement
152
+ return this._value && !this._selectedOptionElement
102
153
  }
103
154
 
104
155
  get _shouldLockInSelection() {
@@ -9,7 +9,10 @@ Combobox.Toggle = Base => class extends Base {
9
9
  close() {
10
10
  if (this._isOpen) {
11
11
  this._lockInSelection()
12
+ this._clearInvalidQuery()
13
+
12
14
  this.expandedValue = false
15
+
13
16
  this._dispatchClosedEvent()
14
17
  }
15
18
  }
@@ -79,6 +82,8 @@ Combobox.Toggle = Base => class extends Base {
79
82
  this._actingCombobox.setAttribute("aria-expanded", true) // needs to happen after setting acting combobox
80
83
  }
81
84
 
85
+ // +._collapse()+ differs from `.close()` in that it might be called by stimulus on connect because
86
+ // it interprets a change in `expandedValue` — whereas `.close()` is only called internally by us.
82
87
  _collapse() {
83
88
  this._actingCombobox.setAttribute("aria-expanded", false) // needs to happen before resetting acting combobox
84
89
 
@@ -119,6 +124,13 @@ Combobox.Toggle = Base => class extends Base {
119
124
  enableBodyScroll(this.dialogListboxTarget)
120
125
  }
121
126
 
127
+ _clearInvalidQuery() {
128
+ if (this._isUnjustifiablyBlank) {
129
+ this._deselect()
130
+ this._clearQuery()
131
+ }
132
+ }
133
+
122
134
  get _isOpen() {
123
135
  return this.expandedValue
124
136
  }
@@ -34,7 +34,7 @@ Combobox.Validity = Base => class extends Base {
34
34
  // +_valueIsInvalid+ only checks if `comboboxTarget` (and not `_actingCombobox`) is required
35
35
  // because the `required` attribute is only forwarded to the `comboboxTarget` element
36
36
  get _valueIsInvalid() {
37
- const isRequiredAndEmpty = this.comboboxTarget.required && !this.hiddenFieldTarget.value
37
+ const isRequiredAndEmpty = this.comboboxTarget.required && !this._value
38
38
  return isRequiredAndEmpty
39
39
  }
40
40
  }
@@ -266,7 +266,7 @@ class HotwireCombobox::Component
266
266
  combobox_attrs.fetch(:data, {}).merge \
267
267
  action: "
268
268
  focus->hw-combobox#open
269
- input->hw-combobox#filter
269
+ input->hw-combobox#filterAndSelect
270
270
  keydown->hw-combobox#navigate
271
271
  click@window->hw-combobox#closeOnClickOutside
272
272
  focusin@window->hw-combobox#closeOnFocusOutside
@@ -316,7 +316,7 @@ class HotwireCombobox::Component
316
316
  def dialog_input_data
317
317
  {
318
318
  action: "
319
- input->hw-combobox#filter
319
+ input->hw-combobox#filterAndSelect
320
320
  keydown->hw-combobox#navigate
321
321
  click@window->hw-combobox#closeOnClickOutside".squish,
322
322
  hw_combobox_target: "dialogCombobox"
@@ -1,3 +1,3 @@
1
1
  module HotwireCombobox
2
- VERSION = "0.1.42"
2
+ VERSION = "0.1.43"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotwire_combobox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.42
4
+ version: 0.1.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jose Farias
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-14 00:00:00.000000000 Z
11
+ date: 2024-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails