slim-select 3.1.2 → 3.1.4

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.
@@ -840,6 +840,173 @@ describe('SlimSelect Module', () => {
840
840
  label.dispatchEvent(new MouseEvent('click', { bubbles: true }))
841
841
  }).not.toThrow()
842
842
  })
843
+
844
+ test('clicking main div closes SlimSelect when wrapped in label', async () => {
845
+ document.body.innerHTML = `
846
+ <label>
847
+ Select a Country
848
+ <select id="wrapped-label-close-test">
849
+ <option value="usa">United States</option>
850
+ <option value="uk">United Kingdom</option>
851
+ </select>
852
+ </label>
853
+ `
854
+
855
+ const slim = new SlimSelect({
856
+ select: '#wrapped-label-close-test',
857
+ settings: {
858
+ allowDeselect: true
859
+ }
860
+ })
861
+
862
+ // Initially closed
863
+ expect(slim.settings.isOpen).toBe(false)
864
+
865
+ // Click the label to open
866
+ const label = document.querySelector('label') as HTMLLabelElement
867
+ label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
868
+ await new Promise((r) => setTimeout(r, 300)) // after animation
869
+ expect(slim.settings.isOpen).toBe(true)
870
+
871
+ // Now click the main div to close
872
+ const mainDiv = document.querySelector(`.ss-main[data-id="${slim.settings.id}"]`) as HTMLElement
873
+ expect(mainDiv).toBeTruthy()
874
+
875
+ mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
876
+ await new Promise((r) => setTimeout(r, 300)) // after animation
877
+
878
+ // SlimSelect should be closed
879
+ expect(slim.settings.isOpen).toBe(false)
880
+
881
+ slim.destroy()
882
+ })
883
+
884
+ test('deselect button works when wrapped in label', async () => {
885
+ document.body.innerHTML = `
886
+ <label>
887
+ Select a Country
888
+ <select id="wrapped-label-deselect-test">
889
+ <option value="usa">United States</option>
890
+ <option value="uk">United Kingdom</option>
891
+ </select>
892
+ </label>
893
+ `
894
+
895
+ const slim = new SlimSelect({
896
+ select: '#wrapped-label-deselect-test',
897
+ settings: {
898
+ allowDeselect: true
899
+ }
900
+ })
901
+
902
+ // Select an option
903
+ slim.setSelected('usa')
904
+ await new Promise((r) => setTimeout(r, 10))
905
+ expect(slim.getSelected()).toEqual(['usa'])
906
+
907
+ // Click the deselect button
908
+ const deselectButton = document.querySelector(`[data-id="${slim.settings.id}"] .ss-deselect`) as HTMLElement
909
+ expect(deselectButton).toBeTruthy()
910
+
911
+ deselectButton.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
912
+ await new Promise((r) => setTimeout(r, 10))
913
+
914
+ // Should be deselected (returns array with empty string for single select)
915
+ expect(slim.getSelected()).toEqual([''])
916
+
917
+ slim.destroy()
918
+ })
919
+
920
+ test('clicking main div toggles when wrapped in label', async () => {
921
+ document.body.innerHTML = `
922
+ <label>
923
+ Select a Country
924
+ <select id="wrapped-label-toggle-test">
925
+ <option value="usa">United States</option>
926
+ <option value="uk">United Kingdom</option>
927
+ </select>
928
+ </label>
929
+ `
930
+
931
+ const slim = new SlimSelect({
932
+ select: '#wrapped-label-toggle-test'
933
+ })
934
+
935
+ // Initially closed
936
+ expect(slim.settings.isOpen).toBe(false)
937
+
938
+ // Click main div to open
939
+ const mainDiv = document.querySelector(`.ss-main[data-id="${slim.settings.id}"]`) as HTMLElement
940
+ mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
941
+ await new Promise((r) => setTimeout(r, 10))
942
+ expect(slim.settings.isOpen).toBe(true)
943
+
944
+ // Click main div again to close
945
+ mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
946
+ await new Promise((r) => setTimeout(r, 10))
947
+ expect(slim.settings.isOpen).toBe(false)
948
+
949
+ // Click main div again to open
950
+ mainDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
951
+ await new Promise((r) => setTimeout(r, 10))
952
+ expect(slim.settings.isOpen).toBe(true)
953
+
954
+ slim.destroy()
955
+ })
956
+
957
+ test('clicking label on second select closes first select when both have labels', async () => {
958
+ document.body.innerHTML = `
959
+ <label>
960
+ First Select
961
+ <select id="first-select">
962
+ <option value="1">Option 1</option>
963
+ <option value="2">Option 2</option>
964
+ </select>
965
+ </label>
966
+ <label>
967
+ Second Select
968
+ <select id="second-select">
969
+ <option value="a">Option A</option>
970
+ <option value="b">Option B</option>
971
+ </select>
972
+ </label>
973
+ `
974
+
975
+ const slim1 = new SlimSelect({
976
+ select: '#first-select'
977
+ })
978
+
979
+ const slim2 = new SlimSelect({
980
+ select: '#second-select'
981
+ })
982
+
983
+ // Both should be closed initially
984
+ expect(slim1.settings.isOpen).toBe(false)
985
+ expect(slim2.settings.isOpen).toBe(false)
986
+
987
+ // Click first label to open first select
988
+ const label1 = document.querySelector('label:first-of-type') as HTMLLabelElement
989
+ label1.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
990
+ await new Promise((r) => setTimeout(r, 300))
991
+ expect(slim1.settings.isOpen).toBe(true)
992
+ expect(slim2.settings.isOpen).toBe(false)
993
+
994
+ // Click second label - should close first and open second
995
+ const label2 = document.querySelector('label:last-of-type') as HTMLLabelElement
996
+ label2.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
997
+ await new Promise((r) => setTimeout(r, 300))
998
+ expect(slim1.settings.isOpen).toBe(false)
999
+ expect(slim2.settings.isOpen).toBe(true)
1000
+
1001
+ // Click first label again - should close second and open first
1002
+ label1.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
1003
+ await new Promise((r) => setTimeout(r, 300))
1004
+ expect(slim1.settings.isOpen).toBe(true)
1005
+ expect(slim2.settings.isOpen).toBe(false)
1006
+
1007
+ slim1.destroy()
1008
+ slim2.destroy()
1009
+ })
843
1010
  })
844
1011
 
845
1012
  describe('option changes race condition scenarios', () => {
@@ -151,10 +151,11 @@ export default class SlimSelect {
151
151
  // and onOptionsChange wouldn't be called directly
152
152
  this.setData(data || [])
153
153
  }
154
- // Set up label click handler to open SlimSelect
154
+ // Set up label click handler to toggle SlimSelect
155
+ // This allows clicking the label to open/close, matching the main div behavior
155
156
  this.select.onLabelClick = () => {
156
157
  if (!this.settings.disabled) {
157
- this.open()
158
+ this.settings.isOpen ? this.close() : this.open()
158
159
  }
159
160
  }
160
161
 
@@ -75,6 +75,17 @@ export default class Render {
75
75
  this.updateClassStyles()
76
76
  this.updateAriaAttributes()
77
77
 
78
+ // Position content off-screen initially to prevent scrollbars
79
+ // This must be after updateClassStyles() since it removes all style attributes
80
+ // The main element isn't in the DOM yet, so getBoundingClientRect() would return zeros
81
+ // Once opened, moveContent() will position it correctly
82
+ if (this.settings.contentPosition !== 'relative') {
83
+ this.content.main.style.top = '-9999px'
84
+ this.content.main.style.left = '-9999px'
85
+ this.content.main.style.margin = '0'
86
+ this.content.main.style.width = 'auto'
87
+ }
88
+
78
89
  // Add content to the content location settings
79
90
  if (this.settings.contentLocation) {
80
91
  this.settings.contentLocation.appendChild(this.content.main)
@@ -556,17 +556,22 @@ export default class Select {
556
556
 
557
557
  // Add click handler to label
558
558
  const labelClickHandler = (e: MouseEvent) => {
559
+ const target = e.target as HTMLElement
560
+
561
+ // Only continue if the target is the label
562
+ if (target !== label) {
563
+ return
564
+ }
565
+
566
+ // At this point, we know the click is directly on the label element
559
567
  // Prevent default label behavior (focusing the select)
568
+ // Don't stop propagation to allow other handlers (like closing other SlimSelect instances)
560
569
  e.preventDefault()
561
- e.stopPropagation()
570
+ // e.stopPropagation() // dont stop propagation
562
571
 
563
- // Trigger the callback to open SlimSelect
572
+ // Trigger the callback to toggle SlimSelect
564
573
  if (this.onLabelClick) {
565
- // Use setTimeout to ensure this happens after any browser-initiated events
566
- // are prevented by our prevent handlers
567
- setTimeout(() => {
568
- this.onLabelClick!()
569
- }, 0)
574
+ this.onLabelClick!()
570
575
  }
571
576
  }
572
577