slim-select 3.1.0 → 3.1.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.
@@ -684,4 +684,360 @@ describe('SlimSelect Module', () => {
684
684
  expect(options.length).toBeGreaterThanOrEqual(11)
685
685
  })
686
686
  })
687
+
688
+ describe('label click support', () => {
689
+ test('clicking label with for attribute opens SlimSelect', async () => {
690
+ document.body.innerHTML = `
691
+ <label for="label-test-select">Select a Country</label>
692
+ <select id="label-test-select">
693
+ <option value="usa">United States</option>
694
+ <option value="uk">United Kingdom</option>
695
+ <option value="canada">Canada</option>
696
+ </select>
697
+ `
698
+
699
+ const slim = new SlimSelect({
700
+ select: '#label-test-select'
701
+ })
702
+
703
+ // Initially closed
704
+ expect(slim.settings.isOpen).toBe(false)
705
+
706
+ // Click the label
707
+ const label = document.querySelector('label[for="label-test-select"]') as HTMLLabelElement
708
+ label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
709
+
710
+ // Wait for setTimeout in label handler
711
+ await new Promise((r) => setTimeout(r, 10))
712
+
713
+ // SlimSelect should be open
714
+ expect(slim.settings.isOpen).toBe(true)
715
+
716
+ slim.destroy()
717
+ })
718
+
719
+ test('clicking wrapped label opens SlimSelect', async () => {
720
+ document.body.innerHTML = `
721
+ <label>
722
+ Select a Country
723
+ <select id="wrapped-label-select">
724
+ <option value="usa">United States</option>
725
+ <option value="uk">United Kingdom</option>
726
+ </select>
727
+ </label>
728
+ `
729
+
730
+ const slim = new SlimSelect({
731
+ select: '#wrapped-label-select'
732
+ })
733
+
734
+ // Initially closed
735
+ expect(slim.settings.isOpen).toBe(false)
736
+
737
+ // Click the label
738
+ const label = document.querySelector('label') as HTMLLabelElement
739
+ label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
740
+
741
+ // Wait for setTimeout in label handler
742
+ await new Promise((r) => setTimeout(r, 10))
743
+
744
+ // SlimSelect should be open
745
+ expect(slim.settings.isOpen).toBe(true)
746
+
747
+ slim.destroy()
748
+ })
749
+
750
+ test('clicking label does not open SlimSelect when disabled', async () => {
751
+ document.body.innerHTML = `
752
+ <label for="disabled-label-select">Select a Country</label>
753
+ <select id="disabled-label-select">
754
+ <option value="usa">United States</option>
755
+ </select>
756
+ `
757
+
758
+ const slim = new SlimSelect({
759
+ select: '#disabled-label-select',
760
+ settings: {
761
+ disabled: true
762
+ }
763
+ })
764
+
765
+ // Initially closed
766
+ expect(slim.settings.isOpen).toBe(false)
767
+
768
+ // Click the label
769
+ const label = document.querySelector('label[for="disabled-label-select"]') as HTMLLabelElement
770
+ label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
771
+
772
+ // Wait for setTimeout in label handler
773
+ await new Promise((r) => setTimeout(r, 10))
774
+
775
+ // SlimSelect should remain closed when disabled
776
+ expect(slim.settings.isOpen).toBe(false)
777
+
778
+ slim.destroy()
779
+ })
780
+
781
+ test('SlimSelect automatically assigns id to select if missing for label association', () => {
782
+ document.body.innerHTML = `
783
+ <label for="auto-id-select">Select a Country</label>
784
+ <select>
785
+ <option value="usa">United States</option>
786
+ </select>
787
+ `
788
+
789
+ // Note: This won't actually work because the label's 'for' points to 'auto-id-select'
790
+ // but we're selecting the select without an id. This test verifies that SlimSelect
791
+ // assigns an id. The actual id will be generated, so the label won't match.
792
+ // Let's test the id assignment instead.
793
+ document.body.innerHTML = '<select><option value="usa">United States</option></select>'
794
+
795
+ const selectElement = document.querySelector('select') as HTMLSelectElement
796
+ const slim = new SlimSelect({
797
+ select: selectElement
798
+ })
799
+
800
+ // SlimSelect should have assigned an id to the select
801
+ expect(selectElement.id).toBeTruthy()
802
+ expect(selectElement.id).toBe(slim.settings.id)
803
+
804
+ slim.destroy()
805
+ })
806
+
807
+ test('label handlers are cleaned up on destroy', async () => {
808
+ document.body.innerHTML = `
809
+ <label for="cleanup-test-select">Select a Country</label>
810
+ <select id="cleanup-test-select">
811
+ <option value="usa">United States</option>
812
+ </select>
813
+ `
814
+
815
+ const slim = new SlimSelect({
816
+ select: '#cleanup-test-select'
817
+ })
818
+
819
+ // Verify label handler works
820
+ const label = document.querySelector('label[for="cleanup-test-select"]') as HTMLLabelElement
821
+ label.dispatchEvent(new MouseEvent('click', { bubbles: true }))
822
+ await new Promise((r) => setTimeout(r, 10))
823
+ expect(slim.settings.isOpen).toBe(true)
824
+
825
+ // Close and destroy
826
+ slim.close()
827
+ slim.destroy()
828
+
829
+ // After destroy, label click should not open SlimSelect
830
+ // (though SlimSelect is destroyed, so we can't check isOpen)
831
+ // But we verify no errors are thrown
832
+ expect(() => {
833
+ label.dispatchEvent(new MouseEvent('click', { bubbles: true }))
834
+ }).not.toThrow()
835
+ })
836
+ })
837
+
838
+ describe('option changes race condition scenarios', () => {
839
+ test('options should persist when added to select and value is set immediately', async () => {
840
+ // Reproduce the issue: empty select, init SlimSelect, add options, set value
841
+ document.body.innerHTML = '<select id="test-select"></select>'
842
+
843
+ const selectElement = document.getElementById('test-select') as HTMLSelectElement
844
+ const slim = new SlimSelect({
845
+ select: selectElement
846
+ })
847
+
848
+ // Add option directly to the select element (not through SlimSelect API)
849
+ const newOption = document.createElement('option')
850
+ newOption.value = '1'
851
+ newOption.textContent = 'one'
852
+ selectElement.appendChild(newOption)
853
+
854
+ // Set the value immediately
855
+ selectElement.value = '1'
856
+
857
+ // Wait for mutation observer and any async operations
858
+ await new Promise((r) => setTimeout(r, 100))
859
+
860
+ // Verify the option still exists
861
+ expect(selectElement.options.length).toBe(1)
862
+ expect(selectElement.value).toBe('1')
863
+ expect(selectElement.options[0].value).toBe('1')
864
+ expect(selectElement.options[0].textContent).toBe('one')
865
+
866
+ // Verify SlimSelect also has the option
867
+ const data = slim.getData()
868
+ expect(data.length).toBe(1)
869
+ expect((data[0] as any).value).toBe('1')
870
+ expect((data[0] as any).text).toBe('one')
871
+
872
+ slim.destroy()
873
+ })
874
+
875
+ test('rapid option and value changes should preserve final state', async () => {
876
+ // Test multiple rapid mutations to ensure queue mechanism works correctly
877
+ document.body.innerHTML = '<select id="test-select"></select>'
878
+
879
+ const selectElement = document.getElementById('test-select') as HTMLSelectElement
880
+ const slim = new SlimSelect({
881
+ select: selectElement
882
+ })
883
+
884
+ // Add first option
885
+ const option1 = document.createElement('option')
886
+ option1.value = '1'
887
+ option1.textContent = 'one'
888
+ selectElement.appendChild(option1)
889
+ selectElement.value = '1'
890
+
891
+ // Immediately add second option
892
+ const option2 = document.createElement('option')
893
+ option2.value = '2'
894
+ option2.textContent = 'two'
895
+ selectElement.appendChild(option2)
896
+ selectElement.value = '2'
897
+
898
+ // Immediately add third option
899
+ const option3 = document.createElement('option')
900
+ option3.value = '3'
901
+ option3.textContent = 'three'
902
+ selectElement.appendChild(option3)
903
+ selectElement.value = '3'
904
+
905
+ // Wait for all mutations to process
906
+ await new Promise((r) => setTimeout(r, 150))
907
+
908
+ // Verify all options exist and final value is correct
909
+ expect(selectElement.options.length).toBe(3)
910
+ expect(selectElement.value).toBe('3')
911
+ expect(selectElement.options[0].value).toBe('1')
912
+ expect(selectElement.options[0].textContent).toBe('one')
913
+ expect(selectElement.options[1].value).toBe('2')
914
+ expect(selectElement.options[1].textContent).toBe('two')
915
+ expect(selectElement.options[2].value).toBe('3')
916
+ expect(selectElement.options[2].textContent).toBe('three')
917
+
918
+ // Verify SlimSelect also has all options
919
+ const data = slim.getData()
920
+ expect(data.length).toBe(3)
921
+ expect((data[0] as any).value).toBe('1')
922
+ expect((data[0] as any).text).toBe('one')
923
+ expect((data[1] as any).value).toBe('2')
924
+ expect((data[1] as any).text).toBe('two')
925
+ expect((data[2] as any).value).toBe('3')
926
+ expect((data[2] as any).text).toBe('three')
927
+
928
+ // Verify selected value
929
+ const selected = slim.getSelected()
930
+ expect(selected).toEqual(['3'])
931
+
932
+ slim.destroy()
933
+ })
934
+
935
+ test('should handle removing all options and adding new ones', async () => {
936
+ // Test scenario: start with options, remove all, then add new ones
937
+ // This tests that the queue mechanism handles the removal and addition correctly
938
+ document.body.innerHTML = `
939
+ <select id="test-select">
940
+ <option value="old1">Old One</option>
941
+ <option value="old2">Old Two</option>
942
+ <option value="old3">Old Three</option>
943
+ </select>
944
+ `
945
+
946
+ const selectElement = document.getElementById('test-select') as HTMLSelectElement
947
+ const slim = new SlimSelect({
948
+ select: selectElement
949
+ })
950
+
951
+ // Verify initial state
952
+ expect(selectElement.options.length).toBe(3)
953
+ expect(slim.getData().length).toBe(3)
954
+
955
+ // Remove all options (this will trigger mutation observer with empty data, which we skip)
956
+ selectElement.innerHTML = ''
957
+
958
+ // Wait for mutation observer to process the removal
959
+ await new Promise((r) => setTimeout(r, 100))
960
+
961
+ // Verify options are removed from DOM
962
+ expect(selectElement.options.length).toBe(0)
963
+
964
+ // Add completely new set of options one by one
965
+ const newOption1 = document.createElement('option')
966
+ newOption1.value = 'new1'
967
+ newOption1.textContent = 'New One'
968
+ selectElement.appendChild(newOption1)
969
+
970
+ const newOption2 = document.createElement('option')
971
+ newOption2.value = 'new2'
972
+ newOption2.textContent = 'New Two'
973
+ selectElement.appendChild(newOption2)
974
+
975
+ const newOption3 = document.createElement('option')
976
+ newOption3.value = 'new3'
977
+ newOption3.textContent = 'New Three'
978
+ selectElement.appendChild(newOption3)
979
+
980
+ // Wait for all mutations to process
981
+ await new Promise((r) => setTimeout(r, 200))
982
+
983
+ // The key test: verify new options persist in DOM (not deleted by race condition)
984
+ expect(selectElement.options.length).toBe(3)
985
+ expect(selectElement.options[0].value).toBe('new1')
986
+ expect(selectElement.options[0].textContent).toBe('New One')
987
+ expect(selectElement.options[1].value).toBe('new2')
988
+ expect(selectElement.options[1].textContent).toBe('New Two')
989
+ expect(selectElement.options[2].value).toBe('new3')
990
+ expect(selectElement.options[2].textContent).toBe('New Three')
991
+
992
+ slim.destroy()
993
+ })
994
+
995
+ test('should handle rapidly removing all options', async () => {
996
+ // Test scenario: start with options, rapidly remove them all
997
+ document.body.innerHTML = `
998
+ <select id="test-select">
999
+ <option value="opt1">Option 1</option>
1000
+ <option value="opt2">Option 2</option>
1001
+ <option value="opt3">Option 3</option>
1002
+ <option value="opt4">Option 4</option>
1003
+ </select>
1004
+ `
1005
+
1006
+ const selectElement = document.getElementById('test-select') as HTMLSelectElement
1007
+ const slim = new SlimSelect({
1008
+ select: selectElement
1009
+ })
1010
+
1011
+ // Set an initial value
1012
+ selectElement.value = 'opt2'
1013
+
1014
+ // Verify initial state
1015
+ expect(selectElement.options.length).toBe(4)
1016
+ expect(selectElement.value).toBe('opt2')
1017
+ expect(slim.getData().length).toBe(4)
1018
+
1019
+ // Rapidly remove all options
1020
+ selectElement.removeChild(selectElement.options[0])
1021
+ selectElement.removeChild(selectElement.options[0])
1022
+ selectElement.removeChild(selectElement.options[0])
1023
+ selectElement.removeChild(selectElement.options[0])
1024
+
1025
+ // Wait for all mutations to process
1026
+ await new Promise((r) => setTimeout(r, 200))
1027
+
1028
+ // Verify all options are removed
1029
+ expect(selectElement.options.length).toBe(0)
1030
+ expect(selectElement.value).toBe('')
1031
+
1032
+ // Verify SlimSelect also has no options
1033
+ const data = slim.getData()
1034
+ expect(data.length).toBe(0)
1035
+
1036
+ // Verify selected value is empty
1037
+ const selected = slim.getSelected()
1038
+ expect(selected).toEqual([])
1039
+
1040
+ slim.destroy()
1041
+ })
1042
+ })
687
1043
  })
@@ -146,8 +146,10 @@ export default class SlimSelect {
146
146
  }
147
147
  }
148
148
  this.select.onOptionsChange = (data: (Option | Optgroup)[]) => {
149
- // Run set data from the values given
150
- this.setData(data)
149
+ // Process the data (including empty data for clearing all options)
150
+ // Empty data is safe to process here because if we were updating, the change would be queued
151
+ // and onOptionsChange wouldn't be called directly
152
+ this.setData(data || [])
151
153
  }
152
154
  // Set up label click handler to open SlimSelect
153
155
  this.select.onLabelClick = () => {