bard-tag_field 0.6.1 → 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: d92384a9af02fa9c8fa45c4123231f664f72da247417574e87881895d8de82b0
4
- data.tar.gz: d36533920260503358709949369202831b97f4d4689b6e2e87f3bcf3473dc95d
3
+ metadata.gz: c3d22221c1cc7dc87d084173f83e04a7a5307348c180d5ec4b7ecad432c6ba6e
4
+ data.tar.gz: 1dd1de41961bf59afa2b7a788871e39c785660a7cdc6ebf1b1eab8e54c9965ef
5
5
  SHA512:
6
- metadata.gz: 3d94473aba5b1836f5e04fc82064b498abee2ee9e16f2d5fc80aa9f40d625542e1d8a5fcf6ee9fe9496599a51fc71e719e1fe1df3d147a3179aa38272ee94f97
7
- data.tar.gz: 7c99f188ddfc97d05ce63b7f71339c621ae6e0d1b9ded309565cd7767ab6e63ceb07ae9672c800a1f54d2358f4c3512e026fb77b426eb82d3840525150f175a3
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,10 +1457,11 @@ 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
- autocomplete({
1464
+ this._autocompleteResult = autocomplete({
1459
1465
  input: this._taggleInputTarget,
1460
1466
  container: this.autocompleteContainerTarget,
1461
1467
  className: "ui-menu ui-autocomplete",
@@ -1478,6 +1484,7 @@ class InputTag extends HTMLElement {
1478
1484
  // Prevent adding multiple tags in single mode
1479
1485
  if (!this.multiple && this._taggle.getTagValues().length > 0) {
1480
1486
  this._taggleInputTarget.value = '';
1487
+ this._updateButtonContent();
1481
1488
  return
1482
1489
  }
1483
1490
 
@@ -1489,8 +1496,9 @@ class InputTag extends HTMLElement {
1489
1496
 
1490
1497
  // Clear input
1491
1498
  this._taggleInputTarget.value = '';
1499
+ this._updateButtonContent();
1492
1500
  },
1493
- minLength: 1,
1501
+ minLength: 0,
1494
1502
  customize: (input, inputRect, container, maxHeight) => {
1495
1503
  // Position autocomplete below the input-tag container, accounting for dynamic height
1496
1504
  this._updateAutocompletePosition(container);
@@ -1565,8 +1573,35 @@ class InputTag extends HTMLElement {
1565
1573
 
1566
1574
  _add(event) {
1567
1575
  event.preventDefault();
1568
- 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);
1569
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 ? "▾" : "+";
1570
1605
  }
1571
1606
 
1572
1607
  onTagAdd(event, tag) {
@@ -1581,6 +1616,8 @@ class InputTag extends HTMLElement {
1581
1616
  this.syncValue();
1582
1617
  this.checkRequired();
1583
1618
  this.updateInputVisibility();
1619
+ // Defer button update: taggle clears input.value after calling this callback
1620
+ setTimeout(() => this._updateButtonContent(), 0);
1584
1621
 
1585
1622
  // Update autocomplete position if it's currently open
1586
1623
  if (this._autocompleteContainer) {
@@ -1600,6 +1637,7 @@ class InputTag extends HTMLElement {
1600
1637
  this.syncValue();
1601
1638
  this.checkRequired();
1602
1639
  this.updateInputVisibility();
1640
+ this._updateButtonContent();
1603
1641
 
1604
1642
  // Update autocomplete position if it's currently open
1605
1643
  if (this._autocompleteContainer) {
@@ -1821,9 +1859,11 @@ class InputTag extends HTMLElement {
1821
1859
  this._taggleInputTarget.autocomplete = "off";
1822
1860
  this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
1823
1861
  this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
1862
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
1824
1863
 
1825
1864
  // Re-setup autocomplete
1826
1865
  this.setupAutocomplete();
1866
+ this._updateButtonContent();
1827
1867
 
1828
1868
  // Re-process existing tag options
1829
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,10 +461,11 @@ 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
- autocomplete({
468
+ this._autocompleteResult = autocomplete({
463
469
  input: this._taggleInputTarget,
464
470
  container: this.autocompleteContainerTarget,
465
471
  className: "ui-menu ui-autocomplete",
@@ -482,6 +488,7 @@ class InputTag extends HTMLElement {
482
488
  // Prevent adding multiple tags in single mode
483
489
  if (!this.multiple && this._taggle.getTagValues().length > 0) {
484
490
  this._taggleInputTarget.value = ''
491
+ this._updateButtonContent()
485
492
  return
486
493
  }
487
494
 
@@ -493,8 +500,9 @@ class InputTag extends HTMLElement {
493
500
 
494
501
  // Clear input
495
502
  this._taggleInputTarget.value = ''
503
+ this._updateButtonContent()
496
504
  },
497
- minLength: 1,
505
+ minLength: 0,
498
506
  customize: (input, inputRect, container, maxHeight) => {
499
507
  // Position autocomplete below the input-tag container, accounting for dynamic height
500
508
  this._updateAutocompletePosition(container);
@@ -569,8 +577,35 @@ class InputTag extends HTMLElement {
569
577
 
570
578
  _add(event) {
571
579
  event.preventDefault()
572
- 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)
573
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 ? "▾" : "+"
574
609
  }
575
610
 
576
611
  onTagAdd(event, tag) {
@@ -585,6 +620,8 @@ class InputTag extends HTMLElement {
585
620
  this.syncValue()
586
621
  this.checkRequired()
587
622
  this.updateInputVisibility()
623
+ // Defer button update: taggle clears input.value after calling this callback
624
+ setTimeout(() => this._updateButtonContent(), 0)
588
625
 
589
626
  // Update autocomplete position if it's currently open
590
627
  if (this._autocompleteContainer) {
@@ -604,6 +641,7 @@ class InputTag extends HTMLElement {
604
641
  this.syncValue()
605
642
  this.checkRequired()
606
643
  this.updateInputVisibility()
644
+ this._updateButtonContent()
607
645
 
608
646
  // Update autocomplete position if it's currently open
609
647
  if (this._autocompleteContainer) {
@@ -825,9 +863,11 @@ class InputTag extends HTMLElement {
825
863
  this._taggleInputTarget.autocomplete = "off";
826
864
  this._taggleInputTarget.setAttribute("data-turbo-permanent", true);
827
865
  this._taggleInputTarget.addEventListener("keyup", e => this.keyup(e));
866
+ this._taggleInputTarget.addEventListener("input", () => this._updateButtonContent());
828
867
 
829
868
  // Re-setup autocomplete
830
869
  this.setupAutocomplete();
870
+ this._updateButtonContent();
831
871
 
832
872
  // Re-process existing tag options
833
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'
@@ -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.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.1
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-18 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