bard-tag_field 0.6.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c380354a1b112458aab6f246cb9ef7a0521e381d781e7483c0a9bfa313c0f28
4
- data.tar.gz: 76952e1c29aab4a598817ea128017d94494b84402da2c23bb82ad8c64db65425
3
+ metadata.gz: c3d22221c1cc7dc87d084173f83e04a7a5307348c180d5ec4b7ecad432c6ba6e
4
+ data.tar.gz: 1dd1de41961bf59afa2b7a788871e39c785660a7cdc6ebf1b1eab8e54c9965ef
5
5
  SHA512:
6
- metadata.gz: a30d75fa9bfc4ddd4e94613797224d8a71db5f0f1751401a953a96448dd25d87da2e8dc47c1bfed2db0c187e7856e5ca6f5eb2faf806b9516103c218e192d8d9
7
- data.tar.gz: 84908e9d4ec5ee2c2e09ad387296b84ddb8236ce4e566036cfa8712c47913e435669805ed2a24ee04cedf7a22fd6a245f931ab89d0e7f93de14a6d84c4f35f94
6
+ metadata.gz: 00b31b2872c1a9bd7df2c4ad1439625d44df574efa3ecbb4450a181eda11594815a469e51f505c6e5809c3992670eecb37d5d8309280f54d8c9656b5c7c236aa
7
+ data.tar.gz: 21bc20e35322cc29d63fe74ddecaacbb1d15a4eb66497c5a4d73722d52de6adf71984e199581c3f9bcc8335d8cd96d9d8a89634648bf1bbc75927d0f066f138a
@@ -1337,6 +1337,7 @@ class InputTag extends HTMLElement {
1337
1337
  reset() {
1338
1338
  this._taggle.removeAll();
1339
1339
  this._taggleInputTarget.value = '';
1340
+ this._updateButtonContent();
1340
1341
  }
1341
1342
 
1342
1343
  get options() {
@@ -1437,11 +1438,15 @@ class InputTag extends HTMLElement {
1437
1438
  this.checkRequired();
1438
1439
 
1439
1440
  this.buttonTarget = document.createElement("button");
1441
+ this.buttonTarget.type = "button";
1440
1442
  this.buttonTarget.className = "add";
1441
1443
  this.buttonTarget.textContent = "+";
1444
+ this.buttonTarget.addEventListener("mousedown", e => e.preventDefault());
1442
1445
  this.buttonTarget.addEventListener("click", e => this._add(e));
1443
1446
  this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget);
1444
1447
 
1448
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
1449
+
1445
1450
  this.autocompleteContainerTarget = document.createElement("ul");
1446
1451
  this._wrapperTarget.appendChild(this.autocompleteContainerTarget);
1447
1452
 
@@ -1452,22 +1457,20 @@ class InputTag extends HTMLElement {
1452
1457
 
1453
1458
  // Update visibility based on current state
1454
1459
  this.updateInputVisibility();
1460
+ this._updateButtonContent();
1455
1461
  }
1456
1462
 
1457
1463
  setupAutocomplete() {
1458
- const optionsWithLabels = this._getOptionsWithLabels();
1459
-
1460
- autocomplete({
1464
+ this._autocompleteResult = autocomplete({
1461
1465
  input: this._taggleInputTarget,
1462
1466
  container: this.autocompleteContainerTarget,
1463
1467
  className: "ui-menu ui-autocomplete",
1464
1468
  fetch: (text, update) => {
1465
1469
  const currentTags = this._taggle.getTagValues();
1466
- const suggestions = optionsWithLabels.filter(option =>
1470
+ const suggestions = this._getOptionsWithLabels().filter(option =>
1467
1471
  option.label.toLowerCase().includes(text.toLowerCase()) &&
1468
1472
  !currentTags.includes(option.value)
1469
1473
  );
1470
- // Store the suggestions for testing (can't assign to getter, tests read from DOM)
1471
1474
  update(suggestions);
1472
1475
  },
1473
1476
  render: item => {
@@ -1481,6 +1484,7 @@ class InputTag extends HTMLElement {
1481
1484
  // Prevent adding multiple tags in single mode
1482
1485
  if (!this.multiple && this._taggle.getTagValues().length > 0) {
1483
1486
  this._taggleInputTarget.value = '';
1487
+ this._updateButtonContent();
1484
1488
  return
1485
1489
  }
1486
1490
 
@@ -1492,8 +1496,9 @@ class InputTag extends HTMLElement {
1492
1496
 
1493
1497
  // Clear input
1494
1498
  this._taggleInputTarget.value = '';
1499
+ this._updateButtonContent();
1495
1500
  },
1496
- minLength: 1,
1501
+ minLength: 0,
1497
1502
  customize: (input, inputRect, container, maxHeight) => {
1498
1503
  // Position autocomplete below the input-tag container, accounting for dynamic height
1499
1504
  this._updateAutocompletePosition(container);
@@ -1568,8 +1573,35 @@ class InputTag extends HTMLElement {
1568
1573
 
1569
1574
  _add(event) {
1570
1575
  event.preventDefault();
1571
- this._taggle.add(this._taggleInputTarget.value);
1576
+ const value = this._taggleInputTarget.value;
1577
+ if (value === '') {
1578
+ if (this._isAutocompleteOpen()) {
1579
+ this._closeAutocomplete();
1580
+ } else {
1581
+ this._taggleInputTarget.focus();
1582
+ this._autocompleteResult.fetch();
1583
+ }
1584
+ return
1585
+ }
1586
+ this._taggle.add(value);
1572
1587
  this._taggleInputTarget.value = '';
1588
+ this._updateButtonContent();
1589
+ }
1590
+
1591
+ _isAutocompleteOpen() {
1592
+ return this._taggleInputTarget.getAttribute("aria-expanded") === "true"
1593
+ }
1594
+
1595
+ _closeAutocomplete() {
1596
+ this._taggleInputTarget.setAttribute("aria-expanded", "false");
1597
+ this.autocompleteContainerTarget.remove();
1598
+ }
1599
+
1600
+ _updateButtonContent() {
1601
+ const isEmpty = !this._taggleInputTarget.value;
1602
+ const currentTags = this._taggle.getTagValues();
1603
+ const hasAvailableOption = this.options.some(value => !currentTags.includes(value));
1604
+ this.buttonTarget.textContent = isEmpty && hasAvailableOption ? "▾" : "+";
1573
1605
  }
1574
1606
 
1575
1607
  onTagAdd(event, tag) {
@@ -1584,6 +1616,8 @@ class InputTag extends HTMLElement {
1584
1616
  this.syncValue();
1585
1617
  this.checkRequired();
1586
1618
  this.updateInputVisibility();
1619
+ // Defer button update: taggle clears input.value after calling this callback
1620
+ setTimeout(() => this._updateButtonContent(), 0);
1587
1621
 
1588
1622
  // Update autocomplete position if it's currently open
1589
1623
  if (this._autocompleteContainer) {
@@ -1603,6 +1637,7 @@ class InputTag extends HTMLElement {
1603
1637
  this.syncValue();
1604
1638
  this.checkRequired();
1605
1639
  this.updateInputVisibility();
1640
+ this._updateButtonContent();
1606
1641
 
1607
1642
  // Update autocomplete position if it's currently open
1608
1643
  if (this._autocompleteContainer) {
@@ -1824,9 +1859,11 @@ class InputTag extends HTMLElement {
1824
1859
  this._taggleInputTarget.autocomplete = "off";
1825
1860
  this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
1826
1861
  this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
1862
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
1827
1863
 
1828
1864
  // Re-setup autocomplete
1829
1865
  this.setupAutocomplete();
1866
+ this._updateButtonContent();
1830
1867
 
1831
1868
  // Re-process existing tag options
1832
1869
  this.processTagOptions();
@@ -341,6 +341,7 @@ class InputTag extends HTMLElement {
341
341
  reset() {
342
342
  this._taggle.removeAll()
343
343
  this._taggleInputTarget.value = ''
344
+ this._updateButtonContent()
344
345
  }
345
346
 
346
347
  get options() {
@@ -441,11 +442,15 @@ class InputTag extends HTMLElement {
441
442
  this.checkRequired()
442
443
 
443
444
  this.buttonTarget = document.createElement("button")
445
+ this.buttonTarget.type = "button"
444
446
  this.buttonTarget.className = "add"
445
447
  this.buttonTarget.textContent = "+"
448
+ this.buttonTarget.addEventListener("mousedown", e => e.preventDefault())
446
449
  this.buttonTarget.addEventListener("click", e => this._add(e))
447
450
  this._taggleInputTarget.insertAdjacentElement("afterend", this.buttonTarget)
448
451
 
452
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent())
453
+
449
454
  this.autocompleteContainerTarget = document.createElement("ul");
450
455
  this._wrapperTarget.appendChild(this.autocompleteContainerTarget)
451
456
 
@@ -456,22 +461,20 @@ class InputTag extends HTMLElement {
456
461
 
457
462
  // Update visibility based on current state
458
463
  this.updateInputVisibility()
464
+ this._updateButtonContent()
459
465
  }
460
466
 
461
467
  setupAutocomplete() {
462
- const optionsWithLabels = this._getOptionsWithLabels()
463
-
464
- autocomplete({
468
+ this._autocompleteResult = autocomplete({
465
469
  input: this._taggleInputTarget,
466
470
  container: this.autocompleteContainerTarget,
467
471
  className: "ui-menu ui-autocomplete",
468
472
  fetch: (text, update) => {
469
473
  const currentTags = this._taggle.getTagValues()
470
- const suggestions = optionsWithLabels.filter(option =>
474
+ const suggestions = this._getOptionsWithLabels().filter(option =>
471
475
  option.label.toLowerCase().includes(text.toLowerCase()) &&
472
476
  !currentTags.includes(option.value)
473
477
  )
474
- // Store the suggestions for testing (can't assign to getter, tests read from DOM)
475
478
  update(suggestions)
476
479
  },
477
480
  render: item => {
@@ -485,6 +488,7 @@ class InputTag extends HTMLElement {
485
488
  // Prevent adding multiple tags in single mode
486
489
  if (!this.multiple && this._taggle.getTagValues().length > 0) {
487
490
  this._taggleInputTarget.value = ''
491
+ this._updateButtonContent()
488
492
  return
489
493
  }
490
494
 
@@ -496,8 +500,9 @@ class InputTag extends HTMLElement {
496
500
 
497
501
  // Clear input
498
502
  this._taggleInputTarget.value = ''
503
+ this._updateButtonContent()
499
504
  },
500
- minLength: 1,
505
+ minLength: 0,
501
506
  customize: (input, inputRect, container, maxHeight) => {
502
507
  // Position autocomplete below the input-tag container, accounting for dynamic height
503
508
  this._updateAutocompletePosition(container);
@@ -572,8 +577,35 @@ class InputTag extends HTMLElement {
572
577
 
573
578
  _add(event) {
574
579
  event.preventDefault()
575
- this._taggle.add(this._taggleInputTarget.value)
580
+ const value = this._taggleInputTarget.value
581
+ if (value === '') {
582
+ if (this._isAutocompleteOpen()) {
583
+ this._closeAutocomplete()
584
+ } else {
585
+ this._taggleInputTarget.focus()
586
+ this._autocompleteResult.fetch()
587
+ }
588
+ return
589
+ }
590
+ this._taggle.add(value)
576
591
  this._taggleInputTarget.value = ''
592
+ this._updateButtonContent()
593
+ }
594
+
595
+ _isAutocompleteOpen() {
596
+ return this._taggleInputTarget.getAttribute("aria-expanded") === "true"
597
+ }
598
+
599
+ _closeAutocomplete() {
600
+ this._taggleInputTarget.setAttribute("aria-expanded", "false")
601
+ this.autocompleteContainerTarget.remove()
602
+ }
603
+
604
+ _updateButtonContent() {
605
+ const isEmpty = !this._taggleInputTarget.value
606
+ const currentTags = this._taggle.getTagValues()
607
+ const hasAvailableOption = this.options.some(value => !currentTags.includes(value))
608
+ this.buttonTarget.textContent = isEmpty && hasAvailableOption ? "▾" : "+"
577
609
  }
578
610
 
579
611
  onTagAdd(event, tag) {
@@ -588,6 +620,8 @@ class InputTag extends HTMLElement {
588
620
  this.syncValue()
589
621
  this.checkRequired()
590
622
  this.updateInputVisibility()
623
+ // Defer button update: taggle clears input.value after calling this callback
624
+ setTimeout(() => this._updateButtonContent(), 0)
591
625
 
592
626
  // Update autocomplete position if it's currently open
593
627
  if (this._autocompleteContainer) {
@@ -607,6 +641,7 @@ class InputTag extends HTMLElement {
607
641
  this.syncValue()
608
642
  this.checkRequired()
609
643
  this.updateInputVisibility()
644
+ this._updateButtonContent()
610
645
 
611
646
  // Update autocomplete position if it's currently open
612
647
  if (this._autocompleteContainer) {
@@ -828,9 +863,11 @@ class InputTag extends HTMLElement {
828
863
  this._taggleInputTarget.autocomplete = "off";
829
864
  this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
830
865
  this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
866
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
831
867
 
832
868
  // Re-setup autocomplete
833
869
  this.setupAutocomplete();
870
+ this._updateButtonContent();
834
871
 
835
872
  // Re-process existing tag options
836
873
  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'
@@ -179,6 +181,27 @@ describe('Autocomplete', () => {
179
181
  expect(inputTag._autocompleteSuggestions).to.not.include('React')
180
182
  expect(inputTag._autocompleteSuggestions).to.not.include('Vue')
181
183
  })
184
+
185
+ it('should include options added to the datalist after initialization', async () => {
186
+ document.body.innerHTML = `
187
+ <input-tag name="frameworks" list="late-datalist" multiple></input-tag>
188
+ <datalist id="late-datalist">
189
+ <option value="react">React</option>
190
+ </datalist>
191
+ `
192
+ const inputTag = document.querySelector('input-tag')
193
+ const datalist = document.querySelector('#late-datalist')
194
+ await waitForBasicInitialization(inputTag)
195
+
196
+ const newOption = document.createElement('option')
197
+ newOption.value = 'added'
198
+ newOption.textContent = 'Added'
199
+ datalist.appendChild(newOption)
200
+
201
+ await simulateInput(inputTag._taggleInputTarget, 'add')
202
+
203
+ expect(inputTag._autocompleteSuggestions).to.include('Added')
204
+ })
182
205
  })
183
206
 
184
207
  describe('Autocomplete Selection Behavior', () => {
@@ -612,4 +635,165 @@ describe('Autocomplete', () => {
612
635
  expect(getTagValues(inputTag)).to.deep.equal(['option1'])
613
636
  })
614
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
+ })
615
799
  })
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bard
4
4
  module TagField
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
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.0
4
+ version: 0.7.0
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-17 00:00:00.000000000 Z
11
+ date: 2026-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails