bard-tag_field 0.6.1 → 0.7.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d92384a9af02fa9c8fa45c4123231f664f72da247417574e87881895d8de82b0
4
- data.tar.gz: d36533920260503358709949369202831b97f4d4689b6e2e87f3bcf3473dc95d
3
+ metadata.gz: 273e3dda2a790b12547aa1aecc3b897fcf114f667fe5e3b5c0d991b41e61c24f
4
+ data.tar.gz: 12174ee0c8167b487dd2081b32220ac257874bf26e1cbaab1d51f7b422497b55
5
5
  SHA512:
6
- metadata.gz: 3d94473aba5b1836f5e04fc82064b498abee2ee9e16f2d5fc80aa9f40d625542e1d8a5fcf6ee9fe9496599a51fc71e719e1fe1df3d147a3179aa38272ee94f97
7
- data.tar.gz: 7c99f188ddfc97d05ce63b7f71339c621ae6e0d1b9ded309565cd7767ab6e63ceb07ae9672c800a1f54d2358f4c3512e026fb77b426eb82d3840525150f175a3
6
+ metadata.gz: 1ae7bf9a3ff6260c925d22a890581e1995dd6e1327002abb91f70f9064c3c339f34517b92e5cc2ef24d78fcad67873c5fbb991240815f85bb2acf74333c1ccf1
7
+ data.tar.gz: 43cbe858f1ecad6feed1fd80006bb3939af4c3c1963921973ae24c9cffb52eb67e3620d346b97a46d572325e7a889af07a1aedb78aa62b92899afb54227cafc7
data/README.md CHANGED
@@ -168,6 +168,63 @@ Or include the precompiled asset (automatically added by this gem):
168
168
  //= require input-tag
169
169
  ```
170
170
 
171
+ ## Styling
172
+
173
+ The custom element uses Shadow DOM, so its internals can't be reached with normal selectors. Theme it by overriding these CSS custom properties on `input-tag` (or any ancestor).
174
+
175
+ ### Tag (`<tag-option>`)
176
+
177
+ | Variable | Default | Styles |
178
+ | --- | --- | --- |
179
+ | `--tag-option-bg` | `#588a00` | Tag background |
180
+ | `--tag-option-color` | `#fff` | Tag label color |
181
+ | `--tag-option-button-color` | `rgba(255, 255, 255, 0.6)` | Tag close-button color |
182
+
183
+ ### Container
184
+
185
+ | Variable | Default | Styles |
186
+ | --- | --- | --- |
187
+ | `--container-bg` | `rgba(255, 255, 255, 0.8)` | Background of the input area |
188
+ | `--container-border` | `#d0d0d0` | Border color (all sides) |
189
+ | `--container-border-left-width` | `1px` | Left border width (override to add a focus/error accent stripe) |
190
+ | `--container-border-left-color` | `#d0d0d0` | Left border color |
191
+ | `--container-shadow` | `#ccc` | Inset shadow color |
192
+
193
+ ### Input
194
+
195
+ | Variable | Default | Styles |
196
+ | --- | --- | --- |
197
+ | `--input-border` | `#d0d0d0` | Dashed border around the input |
198
+ | `--input-bg` | `#fff` | Input background |
199
+ | `--input-color` | `#333` | Input text color |
200
+
201
+ ### Suggestions toggle button
202
+
203
+ | Variable | Default | Styles |
204
+ | --- | --- | --- |
205
+ | `--button-border` | `#e0e0e0` | Button border color |
206
+ | `--button-color` | `#666` | Caret color |
207
+
208
+ ### Autocomplete menu
209
+
210
+ | Variable | Default | Styles |
211
+ | --- | --- | --- |
212
+ | `--menu-shadow` | `#ccc` | Drop shadow color |
213
+ | `--menu-bg` | `#fff` | Menu background |
214
+ | `--menu-color` | `#555` | Menu item text color |
215
+ | `--menu-hover` | `#e0e0e0` | Menu item hover background |
216
+
217
+ ### Example
218
+
219
+ ```css
220
+ input-tag {
221
+ --tag-option-bg: #1f6feb;
222
+ --container-border: #ccd;
223
+ --container-border-left-width: 3px;
224
+ --container-border-left-color: #1f6feb;
225
+ }
226
+ ```
227
+
171
228
  ## Browser Support
172
229
 
173
230
  - Modern browsers that support custom elements
@@ -1001,7 +1001,7 @@ function autocomplete(settings) {
1001
1001
  const tagOptionStyleSheet = new CSSStyleSheet();
1002
1002
  tagOptionStyleSheet.replaceSync(`
1003
1003
  :host {
1004
- background: #588a00;
1004
+ background: var(--tag-option-bg, #588a00);
1005
1005
  padding: 3px 10px 3px 10px !important;
1006
1006
  margin-right: 4px !important;
1007
1007
  margin-bottom: 2px !important;
@@ -1011,7 +1011,7 @@ tagOptionStyleSheet.replaceSync(`
1011
1011
  font-size: 14px;
1012
1012
  line-height: 1;
1013
1013
  min-height: 32px;
1014
- color: #fff;
1014
+ color: var(--tag-option-color, #fff);
1015
1015
  text-transform: none;
1016
1016
  border-radius: 3px;
1017
1017
  position: relative;
@@ -1023,7 +1023,7 @@ tagOptionStyleSheet.replaceSync(`
1023
1023
  background: none;
1024
1024
  font-size: 20px;
1025
1025
  display: inline-block;
1026
- color: rgba(255, 255, 255, 0.6);
1026
+ color: var(--tag-option-button-color, rgba(255, 255, 255, 0.6));
1027
1027
  right: 10px;
1028
1028
  height: 100%;
1029
1029
  cursor: pointer;
@@ -1040,7 +1040,7 @@ inputTagStyleSheet.replaceSync(`
1040
1040
  padding: 0;
1041
1041
  }
1042
1042
  #container {
1043
- background: rgba(255, 255, 255, 0.8);
1043
+ background: var(--container-bg, rgba(255, 255, 255, 0.8));
1044
1044
  padding: 6px 6px 3px;
1045
1045
  max-height: none;
1046
1046
  display: flex;
@@ -1050,9 +1050,11 @@ inputTagStyleSheet.replaceSync(`
1050
1050
  min-height: 48px;
1051
1051
  line-height: 48px;
1052
1052
  width: 100%;
1053
- border: 1px solid #d0d0d0;
1053
+ border: 1px solid var(--container-border, #d0d0d0);
1054
+ border-left-width: var(--container-border-left-width, 1px);
1055
+ border-left-color: var(--container-border-left-color, #d0d0d0);
1054
1056
  outline: 1px solid transparent;
1055
- box-shadow: #ccc 0 1px 4px 0 inset;
1057
+ box-shadow: var(--container-shadow, #ccc) 0 1px 4px 0 inset;
1056
1058
  border-radius: 2px;
1057
1059
  cursor: text;
1058
1060
  color: #333;
@@ -1072,21 +1074,21 @@ inputTagStyleSheet.replaceSync(`
1072
1074
  width: 100%;
1073
1075
  line-height: 2;
1074
1076
  padding: 0 0 0 10px;
1075
- border: 1px dashed #d0d0d0;
1077
+ border: 1px dashed var(--input-border, #d0d0d0);
1076
1078
  outline: 1px solid transparent;
1077
- background: #fff;
1079
+ background: var(--input-bg, #fff);
1078
1080
  box-shadow: none;
1079
1081
  border-radius: 2px;
1080
1082
  cursor: text;
1081
- color: #333;
1083
+ color: var(--input-color, #333);
1082
1084
  }
1083
1085
  button {
1084
1086
  width: 38px;
1085
1087
  text-align: center;
1086
1088
  line-height: 36px;
1087
- border: 1px solid #e0e0e0;
1089
+ border: 1px solid var(--button-border, #e0e0e0);
1088
1090
  font-size: 20px;
1089
- color: #666;
1091
+ color: var(--button-color, #666);
1090
1092
  position: absolute !important;
1091
1093
  z-index: 10;
1092
1094
  right: 0px;
@@ -1111,11 +1113,11 @@ inputTagStyleSheet.replaceSync(`
1111
1113
  .ui-menu{
1112
1114
  margin: 0;
1113
1115
  padding: 6px;
1114
- box-shadow: #ccc 0 1px 6px;
1116
+ box-shadow: var(--menu-shadow, #ccc) 0 1px 6px;
1115
1117
  z-index: 2;
1116
1118
  display: flex;
1117
1119
  flex-wrap: wrap;
1118
- background: #fff;
1120
+ background: var(--menu-bg, #fff);
1119
1121
  list-style: none;
1120
1122
  font-size: 14px;
1121
1123
  min-width: 200px;
@@ -1130,10 +1132,10 @@ inputTagStyleSheet.replaceSync(`
1130
1132
  border-radius: 2px;
1131
1133
  width: auto;
1132
1134
  cursor: pointer;
1133
- color: #555;
1135
+ color: var(--menu-color, #555);
1134
1136
  }
1135
1137
  .ui-menu .ui-menu-item::before{ display: none; }
1136
- .ui-menu .ui-menu-item:hover{ background: #e0e0e0; }
1138
+ .ui-menu .ui-menu-item:hover{ background: var(--menu-hover, #e0e0e0); }
1137
1139
  .ui-state-active{
1138
1140
  padding: 0;
1139
1141
  border: none;
@@ -1337,6 +1339,7 @@ class InputTag extends HTMLElement {
1337
1339
  reset() {
1338
1340
  this._taggle.removeAll();
1339
1341
  this._taggleInputTarget.value = '';
1342
+ this._updateButtonContent();
1340
1343
  }
1341
1344
 
1342
1345
  get options() {
@@ -1437,11 +1440,15 @@ class InputTag extends HTMLElement {
1437
1440
  this.checkRequired();
1438
1441
 
1439
1442
  this.buttonTarget = document.createElement("button");
1443
+ this.buttonTarget.type = "button";
1440
1444
  this.buttonTarget.className = "add";
1441
1445
  this.buttonTarget.textContent = "+";
1446
+ this.buttonTarget.addEventListener("mousedown", e => e.preventDefault());
1442
1447
  this.buttonTarget.addEventListener("click", e => this._add(e));
1443
1448
  this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget);
1444
1449
 
1450
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
1451
+
1445
1452
  this.autocompleteContainerTarget = document.createElement("ul");
1446
1453
  this._wrapperTarget.appendChild(this.autocompleteContainerTarget);
1447
1454
 
@@ -1452,10 +1459,11 @@ class InputTag extends HTMLElement {
1452
1459
 
1453
1460
  // Update visibility based on current state
1454
1461
  this.updateInputVisibility();
1462
+ this._updateButtonContent();
1455
1463
  }
1456
1464
 
1457
1465
  setupAutocomplete() {
1458
- autocomplete({
1466
+ this._autocompleteResult = autocomplete({
1459
1467
  input: this._taggleInputTarget,
1460
1468
  container: this.autocompleteContainerTarget,
1461
1469
  className: "ui-menu ui-autocomplete",
@@ -1478,6 +1486,7 @@ class InputTag extends HTMLElement {
1478
1486
  // Prevent adding multiple tags in single mode
1479
1487
  if (!this.multiple && this._taggle.getTagValues().length > 0) {
1480
1488
  this._taggleInputTarget.value = '';
1489
+ this._updateButtonContent();
1481
1490
  return
1482
1491
  }
1483
1492
 
@@ -1489,8 +1498,9 @@ class InputTag extends HTMLElement {
1489
1498
 
1490
1499
  // Clear input
1491
1500
  this._taggleInputTarget.value = '';
1501
+ this._updateButtonContent();
1492
1502
  },
1493
- minLength: 1,
1503
+ minLength: 0,
1494
1504
  customize: (input, inputRect, container, maxHeight) => {
1495
1505
  // Position autocomplete below the input-tag container, accounting for dynamic height
1496
1506
  this._updateAutocompletePosition(container);
@@ -1565,8 +1575,35 @@ class InputTag extends HTMLElement {
1565
1575
 
1566
1576
  _add(event) {
1567
1577
  event.preventDefault();
1568
- this._taggle.add(this._taggleInputTarget.value);
1578
+ const value = this._taggleInputTarget.value;
1579
+ if (value === '') {
1580
+ if (this._isAutocompleteOpen()) {
1581
+ this._closeAutocomplete();
1582
+ } else {
1583
+ this._taggleInputTarget.focus();
1584
+ this._autocompleteResult.fetch();
1585
+ }
1586
+ return
1587
+ }
1588
+ this._taggle.add(value);
1569
1589
  this._taggleInputTarget.value = '';
1590
+ this._updateButtonContent();
1591
+ }
1592
+
1593
+ _isAutocompleteOpen() {
1594
+ return this._taggleInputTarget.getAttribute("aria-expanded") === "true"
1595
+ }
1596
+
1597
+ _closeAutocomplete() {
1598
+ this._taggleInputTarget.setAttribute("aria-expanded", "false");
1599
+ this.autocompleteContainerTarget.remove();
1600
+ }
1601
+
1602
+ _updateButtonContent() {
1603
+ const isEmpty = !this._taggleInputTarget.value;
1604
+ const currentTags = this._taggle.getTagValues();
1605
+ const hasAvailableOption = this.options.some(value => !currentTags.includes(value));
1606
+ this.buttonTarget.textContent = isEmpty && hasAvailableOption ? "▾" : "+";
1570
1607
  }
1571
1608
 
1572
1609
  onTagAdd(event, tag) {
@@ -1581,6 +1618,8 @@ class InputTag extends HTMLElement {
1581
1618
  this.syncValue();
1582
1619
  this.checkRequired();
1583
1620
  this.updateInputVisibility();
1621
+ // Defer button update: taggle clears input.value after calling this callback
1622
+ setTimeout(() => this._updateButtonContent(), 0);
1584
1623
 
1585
1624
  // Update autocomplete position if it's currently open
1586
1625
  if (this._autocompleteContainer) {
@@ -1600,6 +1639,7 @@ class InputTag extends HTMLElement {
1600
1639
  this.syncValue();
1601
1640
  this.checkRequired();
1602
1641
  this.updateInputVisibility();
1642
+ this._updateButtonContent();
1603
1643
 
1604
1644
  // Update autocomplete position if it's currently open
1605
1645
  if (this._autocompleteContainer) {
@@ -1821,9 +1861,11 @@ class InputTag extends HTMLElement {
1821
1861
  this._taggleInputTarget.autocomplete = "off";
1822
1862
  this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
1823
1863
  this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
1864
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
1824
1865
 
1825
1866
  // Re-setup autocomplete
1826
1867
  this.setupAutocomplete();
1868
+ this._updateButtonContent();
1827
1869
 
1828
1870
  // Re-process existing tag options
1829
1871
  this.processTagOptions();
@@ -5,7 +5,7 @@ import autocomplete from "autocompleter"
5
5
  const tagOptionStyleSheet = new CSSStyleSheet()
6
6
  tagOptionStyleSheet.replaceSync(`
7
7
  :host {
8
- background: #588a00;
8
+ background: var(--tag-option-bg, #588a00);
9
9
  padding: 3px 10px 3px 10px !important;
10
10
  margin-right: 4px !important;
11
11
  margin-bottom: 2px !important;
@@ -15,7 +15,7 @@ tagOptionStyleSheet.replaceSync(`
15
15
  font-size: 14px;
16
16
  line-height: 1;
17
17
  min-height: 32px;
18
- color: #fff;
18
+ color: var(--tag-option-color, #fff);
19
19
  text-transform: none;
20
20
  border-radius: 3px;
21
21
  position: relative;
@@ -27,7 +27,7 @@ tagOptionStyleSheet.replaceSync(`
27
27
  background: none;
28
28
  font-size: 20px;
29
29
  display: inline-block;
30
- color: rgba(255, 255, 255, 0.6);
30
+ color: var(--tag-option-button-color, rgba(255, 255, 255, 0.6));
31
31
  right: 10px;
32
32
  height: 100%;
33
33
  cursor: pointer;
@@ -44,7 +44,7 @@ inputTagStyleSheet.replaceSync(`
44
44
  padding: 0;
45
45
  }
46
46
  #container {
47
- background: rgba(255, 255, 255, 0.8);
47
+ background: var(--container-bg, rgba(255, 255, 255, 0.8));
48
48
  padding: 6px 6px 3px;
49
49
  max-height: none;
50
50
  display: flex;
@@ -54,9 +54,11 @@ inputTagStyleSheet.replaceSync(`
54
54
  min-height: 48px;
55
55
  line-height: 48px;
56
56
  width: 100%;
57
- border: 1px solid #d0d0d0;
57
+ border: 1px solid var(--container-border, #d0d0d0);
58
+ border-left-width: var(--container-border-left-width, 1px);
59
+ border-left-color: var(--container-border-left-color, #d0d0d0);
58
60
  outline: 1px solid transparent;
59
- box-shadow: #ccc 0 1px 4px 0 inset;
61
+ box-shadow: var(--container-shadow, #ccc) 0 1px 4px 0 inset;
60
62
  border-radius: 2px;
61
63
  cursor: text;
62
64
  color: #333;
@@ -76,21 +78,21 @@ inputTagStyleSheet.replaceSync(`
76
78
  width: 100%;
77
79
  line-height: 2;
78
80
  padding: 0 0 0 10px;
79
- border: 1px dashed #d0d0d0;
81
+ border: 1px dashed var(--input-border, #d0d0d0);
80
82
  outline: 1px solid transparent;
81
- background: #fff;
83
+ background: var(--input-bg, #fff);
82
84
  box-shadow: none;
83
85
  border-radius: 2px;
84
86
  cursor: text;
85
- color: #333;
87
+ color: var(--input-color, #333);
86
88
  }
87
89
  button {
88
90
  width: 38px;
89
91
  text-align: center;
90
92
  line-height: 36px;
91
- border: 1px solid #e0e0e0;
93
+ border: 1px solid var(--button-border, #e0e0e0);
92
94
  font-size: 20px;
93
- color: #666;
95
+ color: var(--button-color, #666);
94
96
  position: absolute !important;
95
97
  z-index: 10;
96
98
  right: 0px;
@@ -115,11 +117,11 @@ inputTagStyleSheet.replaceSync(`
115
117
  .ui-menu{
116
118
  margin: 0;
117
119
  padding: 6px;
118
- box-shadow: #ccc 0 1px 6px;
120
+ box-shadow: var(--menu-shadow, #ccc) 0 1px 6px;
119
121
  z-index: 2;
120
122
  display: flex;
121
123
  flex-wrap: wrap;
122
- background: #fff;
124
+ background: var(--menu-bg, #fff);
123
125
  list-style: none;
124
126
  font-size: 14px;
125
127
  min-width: 200px;
@@ -134,10 +136,10 @@ inputTagStyleSheet.replaceSync(`
134
136
  border-radius: 2px;
135
137
  width: auto;
136
138
  cursor: pointer;
137
- color: #555;
139
+ color: var(--menu-color, #555);
138
140
  }
139
141
  .ui-menu .ui-menu-item::before{ display: none; }
140
- .ui-menu .ui-menu-item:hover{ background: #e0e0e0; }
142
+ .ui-menu .ui-menu-item:hover{ background: var(--menu-hover, #e0e0e0); }
141
143
  .ui-state-active{
142
144
  padding: 0;
143
145
  border: none;
@@ -341,6 +343,7 @@ class InputTag extends HTMLElement {
341
343
  reset() {
342
344
  this._taggle.removeAll()
343
345
  this._taggleInputTarget.value = ''
346
+ this._updateButtonContent()
344
347
  }
345
348
 
346
349
  get options() {
@@ -441,11 +444,15 @@ class InputTag extends HTMLElement {
441
444
  this.checkRequired()
442
445
 
443
446
  this.buttonTarget = document.createElement("button")
447
+ this.buttonTarget.type = "button"
444
448
  this.buttonTarget.className = "add"
445
449
  this.buttonTarget.textContent = "+"
450
+ this.buttonTarget.addEventListener("mousedown", e => e.preventDefault())
446
451
  this.buttonTarget.addEventListener("click", e => this._add(e))
447
452
  this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget)
448
453
 
454
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent())
455
+
449
456
  this.autocompleteContainerTarget = document.createElement("ul");
450
457
  this._wrapperTarget.appendChild(this.autocompleteContainerTarget)
451
458
 
@@ -456,10 +463,11 @@ class InputTag extends HTMLElement {
456
463
 
457
464
  // Update visibility based on current state
458
465
  this.updateInputVisibility()
466
+ this._updateButtonContent()
459
467
  }
460
468
 
461
469
  setupAutocomplete() {
462
- autocomplete({
470
+ this._autocompleteResult = autocomplete({
463
471
  input: this._taggleInputTarget,
464
472
  container: this.autocompleteContainerTarget,
465
473
  className: "ui-menu ui-autocomplete",
@@ -482,6 +490,7 @@ class InputTag extends HTMLElement {
482
490
  // Prevent adding multiple tags in single mode
483
491
  if (!this.multiple && this._taggle.getTagValues().length > 0) {
484
492
  this._taggleInputTarget.value = ''
493
+ this._updateButtonContent()
485
494
  return
486
495
  }
487
496
 
@@ -493,8 +502,9 @@ class InputTag extends HTMLElement {
493
502
 
494
503
  // Clear input
495
504
  this._taggleInputTarget.value = ''
505
+ this._updateButtonContent()
496
506
  },
497
- minLength: 1,
507
+ minLength: 0,
498
508
  customize: (input, inputRect, container, maxHeight) => {
499
509
  // Position autocomplete below the input-tag container, accounting for dynamic height
500
510
  this._updateAutocompletePosition(container);
@@ -569,8 +579,35 @@ class InputTag extends HTMLElement {
569
579
 
570
580
  _add(event) {
571
581
  event.preventDefault()
572
- this._taggle.add(this._taggleInputTarget.value)
582
+ const value = this._taggleInputTarget.value
583
+ if (value === '') {
584
+ if (this._isAutocompleteOpen()) {
585
+ this._closeAutocomplete()
586
+ } else {
587
+ this._taggleInputTarget.focus()
588
+ this._autocompleteResult.fetch()
589
+ }
590
+ return
591
+ }
592
+ this._taggle.add(value)
573
593
  this._taggleInputTarget.value = ''
594
+ this._updateButtonContent()
595
+ }
596
+
597
+ _isAutocompleteOpen() {
598
+ return this._taggleInputTarget.getAttribute("aria-expanded") === "true"
599
+ }
600
+
601
+ _closeAutocomplete() {
602
+ this._taggleInputTarget.setAttribute("aria-expanded", "false")
603
+ this.autocompleteContainerTarget.remove()
604
+ }
605
+
606
+ _updateButtonContent() {
607
+ const isEmpty = !this._taggleInputTarget.value
608
+ const currentTags = this._taggle.getTagValues()
609
+ const hasAvailableOption = this.options.some(value => !currentTags.includes(value))
610
+ this.buttonTarget.textContent = isEmpty && hasAvailableOption ? "▾" : "+"
574
611
  }
575
612
 
576
613
  onTagAdd(event, tag) {
@@ -585,6 +622,8 @@ class InputTag extends HTMLElement {
585
622
  this.syncValue()
586
623
  this.checkRequired()
587
624
  this.updateInputVisibility()
625
+ // Defer button update: taggle clears input.value after calling this callback
626
+ setTimeout(() => this._updateButtonContent(), 0)
588
627
 
589
628
  // Update autocomplete position if it's currently open
590
629
  if (this._autocompleteContainer) {
@@ -604,6 +643,7 @@ class InputTag extends HTMLElement {
604
643
  this.syncValue()
605
644
  this.checkRequired()
606
645
  this.updateInputVisibility()
646
+ this._updateButtonContent()
607
647
 
608
648
  // Update autocomplete position if it's currently open
609
649
  if (this._autocompleteContainer) {
@@ -825,9 +865,11 @@ class InputTag extends HTMLElement {
825
865
  this._taggleInputTarget.autocomplete = "off";
826
866
  this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
827
867
  this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
868
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
828
869
 
829
870
  // Re-setup autocomplete
830
871
  this.setupAutocomplete();
872
+ this._updateButtonContent();
831
873
 
832
874
  // Re-process existing tag options
833
875
  this.processTagOptions();
@@ -7,6 +7,8 @@ import {
7
7
  waitForBasicInitialization,
8
8
  waitForUpdate,
9
9
  simulateInput,
10
+ simulateKeyup,
11
+ simulateClick,
10
12
  getTagElements,
11
13
  getTagValues
12
14
  } from './lib/test-utils.js'
@@ -633,4 +635,165 @@ describe('Autocomplete', () => {
633
635
  expect(getTagValues(inputTag)).to.deep.equal(['option1'])
634
636
  })
635
637
  })
638
+
639
+ describe('Dropdown on Empty Input', () => {
640
+ async function setupDropdownTest() {
641
+ document.body.innerHTML = `
642
+ <input-tag name="frameworks" list="suggestions" multiple></input-tag>
643
+ <datalist id="suggestions">
644
+ <option value="react">React</option>
645
+ <option value="vue">Vue</option>
646
+ <option value="angular">Angular</option>
647
+ </datalist>
648
+ `
649
+ const inputTag = document.querySelector('input-tag')
650
+ await waitForBasicInitialization(inputTag)
651
+ return inputTag
652
+ }
653
+
654
+ it('should show all options on ArrowDown when input is empty', async () => {
655
+ const inputTag = await setupDropdownTest()
656
+ const input = inputTag._taggleInputTarget
657
+
658
+ simulateKeyup(input, 40, { key: 'ArrowDown' })
659
+ await waitForUpdate()
660
+
661
+ expect(inputTag._autocompleteSuggestions).to.include.members(['React', 'Vue', 'Angular'])
662
+ })
663
+
664
+ it('should exclude already-selected tags from ArrowDown dropdown', async () => {
665
+ const inputTag = await setupDropdownTest()
666
+ inputTag.add('react')
667
+ await waitForUpdate()
668
+ const input = inputTag._taggleInputTarget
669
+
670
+ simulateKeyup(input, 40, { key: 'ArrowDown' })
671
+ await waitForUpdate()
672
+
673
+ expect(inputTag._autocompleteSuggestions).to.include.members(['Vue', 'Angular'])
674
+ expect(inputTag._autocompleteSuggestions).to.not.include('React')
675
+ })
676
+
677
+ it('should show down chevron on the button when input is empty', async () => {
678
+ const inputTag = await setupDropdownTest()
679
+
680
+ expect(inputTag.buttonTarget.textContent).to.equal('▾')
681
+ })
682
+
683
+ it('should keep + button when there are no autocomplete options', async () => {
684
+ const inputTag = await setupInputTag('<input-tag name="tags" multiple></input-tag>')
685
+
686
+ expect(inputTag.buttonTarget.textContent).to.equal('+')
687
+ })
688
+
689
+ it('should keep + button when datalist is empty', async () => {
690
+ document.body.innerHTML = `
691
+ <input-tag name="tags" list="empty" multiple></input-tag>
692
+ <datalist id="empty"></datalist>
693
+ `
694
+ const inputTag = document.querySelector('input-tag')
695
+ await waitForBasicInitialization(inputTag)
696
+
697
+ expect(inputTag.buttonTarget.textContent).to.equal('+')
698
+ })
699
+
700
+ it('should swap to + when all datalist options are already selected', async () => {
701
+ const inputTag = await setupDropdownTest()
702
+
703
+ expect(inputTag.buttonTarget.textContent).to.equal('▾')
704
+
705
+ inputTag.add('react')
706
+ inputTag.add('vue')
707
+ inputTag.add('angular')
708
+ await waitForUpdate()
709
+
710
+ expect(inputTag.buttonTarget.textContent).to.equal('+')
711
+ })
712
+
713
+ it('should close the dropdown when chevron is clicked while open', async () => {
714
+ const inputTag = await setupDropdownTest()
715
+ const input = inputTag._taggleInputTarget
716
+
717
+ simulateClick(inputTag.buttonTarget)
718
+ await waitForUpdate()
719
+ expect(input.getAttribute('aria-expanded')).to.equal('true')
720
+
721
+ simulateClick(inputTag.buttonTarget)
722
+ await waitForUpdate()
723
+
724
+ expect(input.getAttribute('aria-expanded')).to.equal('false')
725
+ })
726
+
727
+ it('should swap back to chevron after removing a tag frees an option', async () => {
728
+ const inputTag = await setupDropdownTest()
729
+
730
+ inputTag.add('react')
731
+ inputTag.add('vue')
732
+ inputTag.add('angular')
733
+ await waitForUpdate()
734
+ expect(inputTag.buttonTarget.textContent).to.equal('+')
735
+
736
+ inputTag.remove('react')
737
+ await waitForUpdate()
738
+
739
+ expect(inputTag.buttonTarget.textContent).to.equal('▾')
740
+ })
741
+
742
+ it('should show + on the button when input has text', async () => {
743
+ const inputTag = await setupDropdownTest()
744
+ const input = inputTag._taggleInputTarget
745
+
746
+ await simulateInput(input, 'r')
747
+
748
+ expect(inputTag.buttonTarget.textContent).to.equal('+')
749
+ })
750
+
751
+ it('should swap button content as input changes', async () => {
752
+ const inputTag = await setupDropdownTest()
753
+ const input = inputTag._taggleInputTarget
754
+
755
+ expect(inputTag.buttonTarget.textContent).to.equal('▾')
756
+
757
+ await simulateInput(input, 'react')
758
+ expect(inputTag.buttonTarget.textContent).to.equal('+')
759
+
760
+ await simulateInput(input, '')
761
+ expect(inputTag.buttonTarget.textContent).to.equal('▾')
762
+ })
763
+
764
+ it('should open autocomplete dropdown when chevron is clicked', async () => {
765
+ const inputTag = await setupDropdownTest()
766
+
767
+ simulateClick(inputTag.buttonTarget)
768
+ await waitForUpdate()
769
+
770
+ expect(inputTag._autocompleteSuggestions).to.include.members(['React', 'Vue', 'Angular'])
771
+ })
772
+
773
+ it('should still add typed text as tag when + button is clicked', async () => {
774
+ const inputTag = await setupDropdownTest()
775
+ const input = inputTag._taggleInputTarget
776
+
777
+ await simulateInput(input, 'custom-tag')
778
+ simulateClick(inputTag.buttonTarget)
779
+ await waitForUpdate()
780
+
781
+ expect(getTagValues(inputTag)).to.include('custom-tag')
782
+ expect(input.value).to.equal('')
783
+ expect(inputTag.buttonTarget.textContent).to.equal('▾')
784
+ })
785
+
786
+ it('should show chevron again after a tag is added', async () => {
787
+ const inputTag = await setupDropdownTest()
788
+ const input = inputTag._taggleInputTarget
789
+
790
+ await simulateInput(input, 'react')
791
+ expect(inputTag.buttonTarget.textContent).to.equal('+')
792
+
793
+ inputTag.add('react')
794
+ await waitForUpdate()
795
+
796
+ expect(inputTag.buttonTarget.textContent).to.equal('▾')
797
+ })
798
+ })
636
799
  })
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bard
4
4
  module TagField
5
- VERSION = "0.6.1"
5
+ VERSION = "0.7.1"
6
6
  end
7
7
  end
@@ -0,0 +1 @@
1
+ require "bard/tag_field"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bard-tag_field
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-18 00:00:00.000000000 Z
11
+ date: 2026-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -183,6 +183,7 @@ files:
183
183
  - input-tag/test/nested-datalist.test.js
184
184
  - input-tag/test/value-label-separation.test.js
185
185
  - input-tag/web-test-runner.config.mjs
186
+ - lib/bard-tag_field.rb
186
187
  - lib/bard/tag_field.rb
187
188
  - lib/bard/tag_field/cucumber.rb
188
189
  - lib/bard/tag_field/field.rb