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.
- package/README.md +4 -3
- package/dist/index.d.ts +2 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +358 -324
- package/dist/react/index.umd.js +4 -4
- package/dist/slimselect.cjs.js +1 -1
- package/dist/slimselect.cjs.js.map +1 -1
- package/dist/slimselect.es.js +54 -38
- package/dist/slimselect.es.js.map +1 -1
- package/dist/slimselect.iife.js +1 -1
- package/dist/slimselect.iife.js.map +1 -1
- package/dist/slimselect.js +1 -1
- package/dist/slimselect.js.map +1 -1
- package/dist/vue/index.d.ts +2 -0
- package/dist/vue/index.js +67 -35
- package/dist/vue/index.umd.js +1 -1
- package/package.json +2 -1
- package/src/slim-select/index.test.ts +356 -0
- package/src/slim-select/index.ts +4 -2
- package/src/slim-select/react/react.test.tsx +498 -0
- package/src/slim-select/react/react.tsx +102 -5
- package/src/slim-select/select.test.ts +234 -0
- package/src/slim-select/select.ts +45 -0
- package/src/slim-select/vue/vue.test.ts +259 -0
- package/src/slim-select/vue/vue.vue +85 -25
- package/vitest.config.ts +3 -2
|
@@ -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
|
})
|
package/src/slim-select/index.ts
CHANGED
|
@@ -146,8 +146,10 @@ export default class SlimSelect {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
this.select.onOptionsChange = (data: (Option | Optgroup)[]) => {
|
|
149
|
-
//
|
|
150
|
-
|
|
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 = () => {
|