slim-select 3.1.4 → 3.3.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.
@@ -54,6 +54,9 @@ export default class Render {
54
54
  // Used to compute the range selection
55
55
  private lastSelectedOption: Option | null
56
56
 
57
+ // Timeout tracking for cleanup
58
+ private closeAnimationTimeout: ReturnType<typeof setTimeout> | null = null
59
+
57
60
  // Elements
58
61
  public main: Main
59
62
  public content: Content
@@ -92,10 +95,33 @@ export default class Render {
92
95
  }
93
96
  }
94
97
 
98
+ // Helper method to add classes that may contain spaces
99
+ // Splits by spaces and adds each class individually to avoid DOMException
100
+ private addClasses(element: HTMLElement | SVGElement, classValue: string): void {
101
+ if (!classValue || classValue.trim() === '') {
102
+ return
103
+ }
104
+ const classes = classValue.split(' ').filter((c) => c.trim() !== '')
105
+ for (const cls of classes) {
106
+ element.classList.add(cls.trim())
107
+ }
108
+ }
109
+
110
+ // Helper method to remove classes that may contain spaces
111
+ private removeClasses(element: HTMLElement | SVGElement, classValue: string): void {
112
+ if (!classValue || classValue.trim() === '') {
113
+ return
114
+ }
115
+ const classes = classValue.split(' ').filter((c) => c.trim() !== '')
116
+ for (const cls of classes) {
117
+ element.classList.remove(cls.trim())
118
+ }
119
+ }
120
+
95
121
  // Remove disabled classes
96
122
  public enable(): void {
97
123
  // Remove disabled class
98
- this.main.main.classList.remove(this.classes.disabled)
124
+ this.removeClasses(this.main.main, this.classes.disabled)
99
125
  this.main.main.setAttribute('aria-disabled', 'false')
100
126
 
101
127
  // Set search input to "enabled"
@@ -105,7 +131,7 @@ export default class Render {
105
131
  // Set disabled classes
106
132
  public disable(): void {
107
133
  // Add disabled class
108
- this.main.main.classList.add(this.classes.disabled)
134
+ this.addClasses(this.main.main, this.classes.disabled)
109
135
  this.main.main.setAttribute('aria-disabled', 'true')
110
136
 
111
137
  // Set search input to disabled
@@ -116,14 +142,20 @@ export default class Render {
116
142
  this.main.arrow.path.setAttribute('d', this.classes.arrowOpen)
117
143
  this.main.main.setAttribute('aria-expanded', 'true')
118
144
 
145
+ // Clear any pending close animation timeout to prevent race conditions
146
+ if (this.closeAnimationTimeout) {
147
+ clearTimeout(this.closeAnimationTimeout)
148
+ this.closeAnimationTimeout = null
149
+ }
150
+
119
151
  // Set direction class on both main and content (persists, never removed)
120
152
  const isAbove = this.settings.openPosition === 'up'
121
153
  const dirClass = isAbove ? this.classes.dirAbove : this.classes.dirBelow
122
- this.main.main.classList.add(dirClass)
123
- this.content.main.classList.add(dirClass)
154
+ this.addClasses(this.main.main, dirClass)
155
+ this.addClasses(this.content.main, dirClass)
124
156
 
125
157
  // Add open class to content to trigger open animation
126
- this.content.main.classList.add(this.classes.contentOpen)
158
+ this.addClasses(this.content.main, this.classes.contentOpen)
127
159
 
128
160
  // Make search visible to screen readers when opened
129
161
  this.content.search.input.removeAttribute('aria-hidden')
@@ -148,7 +180,7 @@ export default class Render {
148
180
 
149
181
  // Remove open class from content to trigger close animation
150
182
  // Direction class (dirAbove/dirBelow) persists to maintain correct transform-origin
151
- this.content.main.classList.remove(this.classes.contentOpen)
183
+ this.removeClasses(this.content.main, this.classes.contentOpen)
152
184
 
153
185
  // Hide search from screen readers when closed
154
186
  this.content.search.input.setAttribute('aria-hidden', 'true')
@@ -157,10 +189,31 @@ export default class Render {
157
189
  this.main.main.removeAttribute('aria-activedescendant')
158
190
 
159
191
  // Remove direction class from main and content after animation is complete
160
- setTimeout(() => {
161
- this.main.main.classList.remove(this.classes.dirAbove, this.classes.dirBelow)
162
- this.content.main.classList.remove(this.classes.dirAbove, this.classes.dirBelow)
163
- }, 100)
192
+ const animationTiming = this.getAnimationTiming()
193
+ this.closeAnimationTimeout = setTimeout(() => {
194
+ this.removeClasses(this.main.main, this.classes.dirAbove)
195
+ this.removeClasses(this.main.main, this.classes.dirBelow)
196
+ this.removeClasses(this.content.main, this.classes.dirAbove)
197
+ this.removeClasses(this.content.main, this.classes.dirBelow)
198
+ this.closeAnimationTimeout = null
199
+ }, animationTiming)
200
+ }
201
+
202
+ private getAnimationTiming(): number {
203
+ const computedStyle = getComputedStyle(this.content.main)
204
+ const cssValue = computedStyle.getPropertyValue('--ss-animation-timing').trim()
205
+
206
+ if (cssValue) {
207
+ // Parse CSS time value (e.g., "0.2s" or "200ms")
208
+ if (cssValue.endsWith('ms')) {
209
+ return parseFloat(cssValue)
210
+ } else if (cssValue.endsWith('s')) {
211
+ return parseFloat(cssValue) * 1000
212
+ }
213
+ }
214
+
215
+ // Fall back to default 200ms
216
+ return 200
164
217
  }
165
218
 
166
219
  public updateClassStyles(): void {
@@ -171,8 +224,8 @@ export default class Render {
171
224
  this.content.main.removeAttribute('style')
172
225
 
173
226
  // Make sure main/content has its base class
174
- this.main.main.classList.add(this.classes.main)
175
- this.content.main.classList.add(this.classes.content)
227
+ this.addClasses(this.main.main, this.classes.main)
228
+ this.addClasses(this.content.main, this.classes.content)
176
229
 
177
230
  // Add styles
178
231
  if (this.settings.style !== '') {
@@ -249,7 +302,9 @@ export default class Render {
249
302
  case 'Enter':
250
303
  case ' ':
251
304
  this.callbacks.open()
252
- const highlighted = this.content.list.querySelector('.' + this.classes.highlighted) as HTMLDivElement
305
+ const highlighted = this.content.list.querySelector(
306
+ '.' + this.classes.getFirst('highlighted')
307
+ ) as HTMLDivElement
253
308
  if (highlighted) {
254
309
  highlighted.click()
255
310
  }
@@ -279,19 +334,19 @@ export default class Render {
279
334
 
280
335
  // Add values
281
336
  const values = document.createElement('div')
282
- values.classList.add(this.classes.values)
337
+ this.addClasses(values, this.classes.values)
283
338
  main.appendChild(values)
284
339
 
285
340
  // Add deselect
286
341
  const deselect = document.createElement('div')
287
- deselect.classList.add(this.classes.deselect)
342
+ this.addClasses(deselect, this.classes.deselect)
288
343
 
289
344
  // Check if deselect is to be shown or not
290
345
  const selectedOptions = this.store?.getSelectedOptions()
291
346
  if (!this.settings.allowDeselect || (this.settings.isMultiple && selectedOptions && selectedOptions.length <= 0)) {
292
- deselect.classList.add(this.classes.hide)
347
+ this.addClasses(deselect, this.classes.hide)
293
348
  } else {
294
- deselect.classList.remove(this.classes.hide)
349
+ this.removeClasses(deselect, this.classes.hide)
295
350
  }
296
351
 
297
352
  // Add deselect onclick event
@@ -348,12 +403,12 @@ export default class Render {
348
403
 
349
404
  // Add arrow
350
405
  const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
351
- arrow.classList.add(this.classes.arrow)
406
+ this.addClasses(arrow, this.classes.arrow)
352
407
  arrow.setAttribute('viewBox', '0 0 100 100')
353
408
  const arrowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
354
409
  arrowPath.setAttribute('d', this.classes.arrowClose)
355
410
  if (this.settings.alwaysOpen) {
356
- arrow.classList.add(this.classes.hide)
411
+ this.addClasses(arrow, this.classes.hide)
357
412
  }
358
413
  arrow.appendChild(arrowPath)
359
414
  main.appendChild(arrow)
@@ -402,7 +457,7 @@ export default class Render {
402
457
 
403
458
  // Create placeholder div
404
459
  const placeholder = document.createElement('div')
405
- placeholder.classList.add(this.classes.placeholder)
460
+ this.addClasses(placeholder, this.classes.placeholder)
406
461
  placeholder.innerHTML = placeholderText
407
462
  return placeholder
408
463
  }
@@ -433,7 +488,7 @@ export default class Render {
433
488
  } else {
434
489
  // Create single value container
435
490
  const singleValue = document.createElement('div')
436
- singleValue.classList.add(this.classes.single)
491
+ this.addClasses(singleValue, this.classes.single)
437
492
  if (selectedSingle.html) {
438
493
  singleValue.innerHTML = selectedSingle.html
439
494
  } else {
@@ -446,9 +501,9 @@ export default class Render {
446
501
 
447
502
  // If allowDeselect is false or selected value is empty just hide deselect
448
503
  if (!this.settings.allowDeselect || !selected.length) {
449
- this.main.deselect.main.classList.add(this.classes.hide)
504
+ this.addClasses(this.main.deselect.main, this.classes.hide)
450
505
  } else {
451
- this.main.deselect.main.classList.remove(this.classes.hide)
506
+ this.removeClasses(this.main.deselect.main, this.classes.hide)
452
507
  }
453
508
  }
454
509
 
@@ -466,7 +521,7 @@ export default class Render {
466
521
  return
467
522
  } else {
468
523
  // If there is a placeholder, remove it
469
- const placeholder = this.main.values.querySelector('.' + this.classes.placeholder)
524
+ const placeholder = this.main.values.querySelector('.' + this.classes.getFirst('placeholder'))
470
525
  if (placeholder) {
471
526
  placeholder.remove()
472
527
  }
@@ -476,7 +531,7 @@ export default class Render {
476
531
  if (selectedOptions.length > this.settings.maxValuesShown) {
477
532
  // Creating the element that shows the number of selected items
478
533
  const singleValue = document.createElement('div')
479
- singleValue.classList.add(this.classes.max)
534
+ this.addClasses(singleValue, this.classes.max)
480
535
  singleValue.textContent = this.settings.maxValuesMessage.replace('{number}', selectedOptions.length.toString())
481
536
 
482
537
  // If there is a selected value, set a single div
@@ -484,7 +539,7 @@ export default class Render {
484
539
  return
485
540
  } else {
486
541
  // If there is a message, remove it
487
- const maxValuesMessage = this.main.values.querySelector('.' + this.classes.max)
542
+ const maxValuesMessage = this.main.values.querySelector('.' + this.classes.getFirst('max'))
488
543
  if (maxValuesMessage) {
489
544
  maxValuesMessage.remove()
490
545
  }
@@ -515,7 +570,7 @@ export default class Render {
515
570
 
516
571
  // Loop through and remove
517
572
  for (const n of removeNodes) {
518
- n.classList.add(this.classes.valueOut)
573
+ this.addClasses(n, this.classes.valueOut)
519
574
  setTimeout(() => {
520
575
  if (this.main.values.hasChildNodes() && this.main.values.contains(n)) {
521
576
  this.main.values.removeChild(n)
@@ -554,11 +609,11 @@ export default class Render {
554
609
 
555
610
  public multipleValue(option: Option): HTMLDivElement {
556
611
  const value = document.createElement('div')
557
- value.classList.add(this.classes.value)
612
+ this.addClasses(value, this.classes.value)
558
613
  value.dataset.id = option.id
559
614
 
560
615
  const text = document.createElement('div')
561
- text.classList.add(this.classes.valueText)
616
+ this.addClasses(text, this.classes.valueText)
562
617
  text.textContent = option.text // For multiple values always use text
563
618
  value.appendChild(text)
564
619
 
@@ -566,7 +621,7 @@ export default class Render {
566
621
  if (!option.mandatory) {
567
622
  // Create delete div element
568
623
  const deleteDiv = document.createElement('div')
569
- deleteDiv.classList.add(this.classes.valueDelete)
624
+ this.addClasses(deleteDiv, this.classes.valueDelete)
570
625
  deleteDiv.setAttribute('tabindex', '0') // Make the div focusable for tab navigation
571
626
 
572
627
  // Add delete onclick event
@@ -699,7 +754,7 @@ export default class Render {
699
754
  const main = document.createElement('div')
700
755
  const input = document.createElement('input')
701
756
  const addable = document.createElement('div')
702
- main.classList.add(this.classes.search)
757
+ this.addClasses(main, this.classes.search)
703
758
 
704
759
  // Setup search return object
705
760
  const searchReturn: Search = {
@@ -709,7 +764,7 @@ export default class Render {
709
764
 
710
765
  // We still want the search to be tabable but not shown
711
766
  if (!this.settings.showSearch) {
712
- main.classList.add(this.classes.hide)
767
+ this.addClasses(main, this.classes.hide)
713
768
  input.readOnly = true
714
769
  }
715
770
 
@@ -746,7 +801,9 @@ export default class Render {
746
801
  this.callbacks.close()
747
802
  return false
748
803
  case ' ':
749
- const highlighted = this.content.list.querySelector('.' + this.classes.highlighted) as HTMLDivElement
804
+ const highlighted = this.content.list.querySelector(
805
+ '.' + this.classes.getFirst('highlighted')
806
+ ) as HTMLDivElement
750
807
  if (highlighted) {
751
808
  highlighted.click()
752
809
  return false
@@ -754,7 +811,9 @@ export default class Render {
754
811
  return true
755
812
  case 'Enter':
756
813
  // Check if there's a highlighted option first
757
- const highlightedEnter = this.content.list.querySelector('.' + this.classes.highlighted) as HTMLDivElement
814
+ const highlightedEnter = this.content.list.querySelector(
815
+ '.' + this.classes.getFirst('highlighted')
816
+ ) as HTMLDivElement
758
817
  if (highlightedEnter) {
759
818
  // If an option is highlighted, select it (even if addable is enabled)
760
819
  highlightedEnter.click()
@@ -775,7 +834,7 @@ export default class Render {
775
834
  // If addable is enabled, add the addable div
776
835
  if (this.callbacks.addable) {
777
836
  // Add main class
778
- addable.classList.add(this.classes.addable)
837
+ this.addClasses(addable, this.classes.addable)
779
838
 
780
839
  // Add svg icon
781
840
  const plus = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
@@ -881,20 +940,20 @@ export default class Render {
881
940
  }
882
941
 
883
942
  public searchFocus(): void {
884
- this.content.search.input.focus()
943
+ this.content.search.input.focus({ preventScroll: true })
885
944
  }
886
945
 
887
946
  public getOptions(notPlaceholder = false, notDisabled = false, notHidden = false): HTMLDivElement[] {
888
947
  // Put together query string
889
- let query = '.' + this.classes.option
948
+ let query = '.' + this.classes.getFirst('option')
890
949
  if (notPlaceholder) {
891
- query += ':not(.' + this.classes.placeholder + ')'
950
+ query += ':not(.' + this.classes.getFirst('placeholder') + ')'
892
951
  }
893
952
  if (notDisabled) {
894
- query += ':not(.' + this.classes.disabled + ')'
953
+ query += ':not(.' + this.classes.getFirst('disabled') + ')'
895
954
  }
896
955
  if (notHidden) {
897
- query += ':not(.' + this.classes.hide + ')'
956
+ query += ':not(.' + this.classes.getFirst('hide') + ')'
898
957
  }
899
958
 
900
959
  return Array.from(this.content.list.querySelectorAll(query))
@@ -913,8 +972,8 @@ export default class Render {
913
972
  // If length is 1, highlight it
914
973
  if (options.length === 1) {
915
974
  // Check if option doesnt already have highlighted class
916
- if (!options[0].classList.contains(this.classes.highlighted)) {
917
- options[0].classList.add(this.classes.highlighted)
975
+ if (!options[0].classList.contains(this.classes.getFirst('highlighted'))) {
976
+ this.addClasses(options[0], this.classes.highlighted)
918
977
  return
919
978
  }
920
979
  }
@@ -922,7 +981,7 @@ export default class Render {
922
981
  // Loop through options and see if there are no highlighted ones
923
982
  let highlighted = false
924
983
  for (const o of options) {
925
- if (o.classList.contains(this.classes.highlighted)) {
984
+ if (o.classList.contains(this.classes.getFirst('highlighted'))) {
926
985
  highlighted = true
927
986
  }
928
987
  }
@@ -930,8 +989,8 @@ export default class Render {
930
989
  // If no highlighted, see if any are selected and if so highlight selected first one
931
990
  if (!highlighted) {
932
991
  for (const o of options) {
933
- if (o.classList.contains(this.classes.selected)) {
934
- o.classList.add(this.classes.highlighted)
992
+ if (o.classList.contains(this.classes.getFirst('selected'))) {
993
+ this.addClasses(o, this.classes.highlighted)
935
994
  break
936
995
  }
937
996
  }
@@ -940,15 +999,15 @@ export default class Render {
940
999
  // Loop through options and find the highlighted one
941
1000
  for (let i = 0; i < options.length; i++) {
942
1001
  // Found highlighted option
943
- if (options[i].classList.contains(this.classes.highlighted)) {
1002
+ if (options[i].classList.contains(this.classes.getFirst('highlighted'))) {
944
1003
  const prevOption = options[i]
945
1004
  // Remove highlighted class from current one
946
- prevOption.classList.remove(this.classes.highlighted)
1005
+ this.removeClasses(prevOption, this.classes.highlighted)
947
1006
 
948
1007
  // If previous option has parent classes ss-optgroup with ss-open then click it
949
1008
  const prevParent = prevOption.parentElement
950
- if (prevParent && prevParent.classList.contains(this.classes.mainOpen)) {
951
- const optgroupLabel = prevParent.querySelector('.' + this.classes.optgroupLabel) as HTMLDivElement
1009
+ if (prevParent && prevParent.classList.contains(this.classes.getFirst('mainOpen'))) {
1010
+ const optgroupLabel = prevParent.querySelector('.' + this.classes.getFirst('optgroupLabel')) as HTMLDivElement
952
1011
  if (optgroupLabel) {
953
1012
  optgroupLabel.click()
954
1013
  }
@@ -957,7 +1016,7 @@ export default class Render {
957
1016
  // Highlight the next one
958
1017
  let selectOption =
959
1018
  options[dir === 'down' ? (i + 1 < options.length ? i + 1 : 0) : i - 1 >= 0 ? i - 1 : options.length - 1]
960
- selectOption.classList.add(this.classes.highlighted)
1019
+ this.addClasses(selectOption, this.classes.highlighted)
961
1020
  this.ensureElementInView(this.content.list, selectOption)
962
1021
 
963
1022
  // Update aria-activedescendant for screen readers
@@ -967,8 +1026,10 @@ export default class Render {
967
1026
 
968
1027
  // If selected option has parent classes ss-optgroup with ss-close then click it
969
1028
  const selectParent = selectOption.parentElement
970
- if (selectParent && selectParent.classList.contains(this.classes.close)) {
971
- const optgroupLabel = selectParent.querySelector('.' + this.classes.optgroupLabel) as HTMLDivElement
1029
+ if (selectParent && selectParent.classList.contains(this.classes.getFirst('close'))) {
1030
+ const optgroupLabel = selectParent.querySelector(
1031
+ '.' + this.classes.getFirst('optgroupLabel')
1032
+ ) as HTMLDivElement
972
1033
  if (optgroupLabel) {
973
1034
  optgroupLabel.click()
974
1035
  }
@@ -981,7 +1042,7 @@ export default class Render {
981
1042
  // If we get here, there is no highlighted option
982
1043
  // So we will highlight the first or last based upon direction
983
1044
  const firstHighlight = options[dir === 'down' ? 0 : options.length - 1]
984
- firstHighlight.classList.add(this.classes.highlighted)
1045
+ this.addClasses(firstHighlight, this.classes.highlighted)
985
1046
 
986
1047
  // Update aria-activedescendant for screen readers
987
1048
  if (firstHighlight.id) {
@@ -995,7 +1056,7 @@ export default class Render {
995
1056
  // Create main container that options will reside
996
1057
  public listDiv(): HTMLDivElement {
997
1058
  const options = document.createElement('div')
998
- options.classList.add(this.classes.list)
1059
+ this.addClasses(options, this.classes.list)
999
1060
 
1000
1061
  // Add id for ARIA controls reference
1001
1062
  const listId = this.settings.id + '-list'
@@ -1010,7 +1071,7 @@ export default class Render {
1010
1071
  this.content.list.innerHTML = ''
1011
1072
 
1012
1073
  const errorDiv = document.createElement('div')
1013
- errorDiv.classList.add(this.classes.error)
1074
+ this.addClasses(errorDiv, this.classes.error)
1014
1075
  errorDiv.textContent = error
1015
1076
  this.content.list.appendChild(errorDiv)
1016
1077
  }
@@ -1020,7 +1081,7 @@ export default class Render {
1020
1081
  this.content.list.innerHTML = ''
1021
1082
 
1022
1083
  const searchingDiv = document.createElement('div')
1023
- searchingDiv.classList.add(this.classes.searching)
1084
+ this.addClasses(searchingDiv, this.classes.searching)
1024
1085
  searchingDiv.textContent = this.settings.searchingText
1025
1086
  this.content.list.appendChild(searchingDiv)
1026
1087
  }
@@ -1033,7 +1094,7 @@ export default class Render {
1033
1094
  // If no results show no results text
1034
1095
  if (data.length === 0) {
1035
1096
  const noResults = document.createElement('div')
1036
- noResults.classList.add(this.classes.search)
1097
+ this.addClasses(noResults, this.classes.search)
1037
1098
 
1038
1099
  //
1039
1100
  if (this.callbacks.addable) {
@@ -1069,29 +1130,29 @@ export default class Render {
1069
1130
  if (d instanceof Optgroup) {
1070
1131
  // Create optgroup
1071
1132
  const optgroupEl = document.createElement('div')
1072
- optgroupEl.classList.add(this.classes.optgroup)
1133
+ this.addClasses(optgroupEl, this.classes.optgroup)
1073
1134
 
1074
1135
  // Create label
1075
1136
  const optgroupLabel = document.createElement('div')
1076
- optgroupLabel.classList.add(this.classes.optgroupLabel)
1137
+ this.addClasses(optgroupLabel, this.classes.optgroupLabel)
1077
1138
  optgroupEl.appendChild(optgroupLabel)
1078
1139
 
1079
1140
  // Create label text div element
1080
1141
  const optgroupLabelText = document.createElement('div')
1081
- optgroupLabelText.classList.add(this.classes.optgroupLabelText)
1142
+ this.addClasses(optgroupLabelText, this.classes.optgroupLabelText)
1082
1143
  optgroupLabelText.textContent = d.label
1083
1144
  optgroupLabel.appendChild(optgroupLabelText)
1084
1145
 
1085
1146
  // Create options container
1086
1147
  const optgroupActions = document.createElement('div')
1087
- optgroupActions.classList.add(this.classes.optgroupActions)
1148
+ this.addClasses(optgroupActions, this.classes.optgroupActions)
1088
1149
  optgroupLabel.appendChild(optgroupActions)
1089
1150
 
1090
1151
  // If selectByGroup is true and isMultiple then add click event to label
1091
1152
  if (this.settings.isMultiple && d.selectAll) {
1092
1153
  // Create new div to hold a checkbox svg
1093
1154
  const selectAll = document.createElement('div')
1094
- selectAll.classList.add(this.classes.optgroupSelectAll)
1155
+ this.addClasses(selectAll, this.classes.optgroupSelectAll)
1095
1156
 
1096
1157
  // Check options and if all are selected, if so add class selected
1097
1158
  let allSelected = true
@@ -1104,7 +1165,7 @@ export default class Render {
1104
1165
 
1105
1166
  // Add class if all selected
1106
1167
  if (allSelected) {
1107
- selectAll.classList.add(this.classes.selected)
1168
+ this.addClasses(selectAll, this.classes.selected)
1108
1169
  }
1109
1170
 
1110
1171
  // Add select all text span
@@ -1177,12 +1238,12 @@ export default class Render {
1177
1238
  if (d.closable !== 'off') {
1178
1239
  // Create new div to hold a checkbox svg
1179
1240
  const optgroupClosable = document.createElement('div')
1180
- optgroupClosable.classList.add(this.classes.optgroupClosable)
1241
+ this.addClasses(optgroupClosable, this.classes.optgroupClosable)
1181
1242
 
1182
1243
  // Create svg arrow
1183
1244
  const optgroupClosableSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
1184
1245
  optgroupClosableSvg.setAttribute('viewBox', '0 0 100 100')
1185
- optgroupClosableSvg.classList.add(this.classes.arrow)
1246
+ this.addClasses(optgroupClosableSvg, this.classes.arrow)
1186
1247
  optgroupClosable.appendChild(optgroupClosableSvg)
1187
1248
 
1188
1249
  // Create new path for arrow
@@ -1191,13 +1252,13 @@ export default class Render {
1191
1252
 
1192
1253
  // If any options are selected or someone is searching, set optgroup to open
1193
1254
  if (d.options.some((o) => o.selected) || this.content.search.input.value.trim() !== '') {
1194
- optgroupClosable.classList.add(this.classes.mainOpen)
1255
+ this.addClasses(optgroupClosable, this.classes.mainOpen)
1195
1256
  optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
1196
1257
  } else if (d.closable === 'open') {
1197
- optgroupEl.classList.add(this.classes.mainOpen)
1258
+ this.addClasses(optgroupEl, this.classes.mainOpen)
1198
1259
  optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
1199
1260
  } else if (d.closable === 'close') {
1200
- optgroupEl.classList.add(this.classes.close)
1261
+ this.addClasses(optgroupEl, this.classes.close)
1201
1262
  optgroupClosableArrow.setAttribute('d', this.classes.arrowClose)
1202
1263
  }
1203
1264
 
@@ -1207,13 +1268,13 @@ export default class Render {
1207
1268
  e.stopPropagation()
1208
1269
 
1209
1270
  // If optgroup is closed, open it
1210
- if (optgroupEl.classList.contains(this.classes.close)) {
1211
- optgroupEl.classList.remove(this.classes.close)
1212
- optgroupEl.classList.add(this.classes.mainOpen)
1271
+ if (optgroupEl.classList.contains(this.classes.getFirst('close'))) {
1272
+ this.removeClasses(optgroupEl, this.classes.close)
1273
+ this.addClasses(optgroupEl, this.classes.mainOpen)
1213
1274
  optgroupClosableArrow.setAttribute('d', this.classes.arrowOpen)
1214
1275
  } else {
1215
- optgroupEl.classList.remove(this.classes.mainOpen)
1216
- optgroupEl.classList.add(this.classes.close)
1276
+ this.removeClasses(optgroupEl, this.classes.mainOpen)
1277
+ this.addClasses(optgroupEl, this.classes.close)
1217
1278
  optgroupClosableArrow.setAttribute('d', this.classes.arrowClose)
1218
1279
  }
1219
1280
  })
@@ -1247,8 +1308,8 @@ export default class Render {
1247
1308
  // Add hidden placeholder
1248
1309
  if (option.placeholder) {
1249
1310
  const placeholder = document.createElement('div')
1250
- placeholder.classList.add(this.classes.option)
1251
- placeholder.classList.add(this.classes.hide)
1311
+ this.addClasses(placeholder, this.classes.option)
1312
+ this.addClasses(placeholder, this.classes.hide)
1252
1313
  return placeholder
1253
1314
  }
1254
1315
 
@@ -1256,7 +1317,7 @@ export default class Render {
1256
1317
  const optionEl = document.createElement('div')
1257
1318
  optionEl.dataset.id = option.id // Dataset id for identifying an option
1258
1319
  optionEl.id = this.settings.id + '-' + option.id // Unique ID for ARIA references
1259
- optionEl.classList.add(this.classes.option)
1320
+ this.addClasses(optionEl, this.classes.option)
1260
1321
  optionEl.setAttribute('role', 'option') // WCAG attribute
1261
1322
  if (option.class) {
1262
1323
  option.class.split(' ').forEach((dataClass: string) => {
@@ -1287,26 +1348,26 @@ export default class Render {
1287
1348
 
1288
1349
  // If option is disabled
1289
1350
  if (!option.display) {
1290
- optionEl.classList.add(this.classes.hide)
1351
+ this.addClasses(optionEl, this.classes.hide)
1291
1352
  }
1292
1353
 
1293
1354
  // If allowed to deselect, null onclick and add disabled
1294
1355
  if (option.disabled) {
1295
- optionEl.classList.add(this.classes.disabled)
1356
+ this.addClasses(optionEl, this.classes.disabled)
1296
1357
  }
1297
1358
 
1298
1359
  // If option is selected and hideSelectedOption is true, hide it
1299
1360
  if (option.selected && this.settings.hideSelected) {
1300
- optionEl.classList.add(this.classes.hide)
1361
+ this.addClasses(optionEl, this.classes.hide)
1301
1362
  }
1302
1363
 
1303
1364
  // If option is selected
1304
1365
  if (option.selected) {
1305
- optionEl.classList.add(this.classes.selected)
1366
+ this.addClasses(optionEl, this.classes.selected)
1306
1367
  optionEl.setAttribute('aria-selected', 'true')
1307
1368
  this.main.main.setAttribute('aria-activedescendant', optionEl.id)
1308
1369
  } else {
1309
- optionEl.classList.remove(this.classes.selected)
1370
+ this.removeClasses(optionEl, this.classes.selected)
1310
1371
  optionEl.setAttribute('aria-selected', 'false')
1311
1372
  }
1312
1373
 
@@ -1462,6 +1523,12 @@ export default class Render {
1462
1523
  }
1463
1524
 
1464
1525
  public destroy(): void {
1526
+ // Clear any pending timeouts
1527
+ if (this.closeAnimationTimeout) {
1528
+ clearTimeout(this.closeAnimationTimeout)
1529
+ this.closeAnimationTimeout = null
1530
+ }
1531
+
1465
1532
  // Remove main
1466
1533
  this.main.main.remove()
1467
1534
 
@@ -1521,48 +1588,62 @@ export default class Render {
1521
1588
  return tempDiv.innerHTML
1522
1589
  }
1523
1590
 
1524
- public moveContentAbove(): void {
1525
- // Get main and content height
1526
- const mainHeight = this.main.main.offsetHeight
1527
- const contentHeight = this.content.main.offsetHeight
1591
+ private setContentDirection(direction: 'above' | 'below'): void {
1592
+ const isAbove = direction === 'above'
1593
+ const addClass = isAbove ? this.classes.dirAbove : this.classes.dirBelow
1594
+ const removeClass = isAbove ? this.classes.dirBelow : this.classes.dirAbove
1528
1595
 
1529
1596
  // Set direction classes on both main and content
1530
- this.main.main.classList.remove(this.classes.dirBelow)
1531
- this.main.main.classList.add(this.classes.dirAbove)
1532
- this.content.main.classList.remove(this.classes.dirBelow)
1533
- this.content.main.classList.add(this.classes.dirAbove)
1597
+ this.removeClasses(this.main.main, removeClass)
1598
+ this.addClasses(this.main.main, addClass)
1599
+ this.removeClasses(this.content.main, removeClass)
1600
+ this.addClasses(this.content.main, addClass)
1601
+
1602
+ // Set margin to position content
1603
+ if (isAbove) {
1604
+ const mainHeight = this.main.main.offsetHeight
1605
+ const contentHeight = this.content.main.offsetHeight
1606
+ this.content.main.style.margin = '-' + (mainHeight + contentHeight - 1) + 'px 0px 0px 0px'
1607
+ } else {
1608
+ this.content.main.style.margin = '-1px 0px 0px 0px'
1609
+ }
1610
+ }
1611
+
1612
+ private setContentPosition(): void {
1613
+ if (this.settings.contentPosition === 'relative') {
1614
+ return
1615
+ }
1534
1616
 
1535
- // Set the content position
1536
1617
  const containerRect = this.main.main.getBoundingClientRect()
1537
- this.content.main.style.margin = '-' + (mainHeight + contentHeight - 1) + 'px 0px 0px 0px'
1538
- this.content.main.style.top =
1539
- containerRect.top + containerRect.height + (this.settings.contentPosition === 'fixed' ? 0 : window.scrollY) + 'px'
1540
- this.content.main.style.left =
1541
- containerRect.left + (this.settings.contentPosition === 'fixed' ? 0 : window.scrollX) + 'px'
1618
+ let top: number
1619
+ let left: number
1620
+
1621
+ if (this.settings.contentPosition === 'fixed') {
1622
+ // Fixed positioning - use viewport coordinates directly
1623
+ top = containerRect.top + containerRect.height
1624
+ left = containerRect.left
1625
+ } else {
1626
+ // Absolute positioning - calculate relative to offsetParent
1627
+ const offsetParent = this.content.main.offsetParent as HTMLElement
1628
+ const offsetParentRect = offsetParent ? offsetParent.getBoundingClientRect() : { top: 0, left: 0 }
1629
+
1630
+ top = containerRect.top - offsetParentRect.top + containerRect.height - (offsetParent?.clientTop || 0)
1631
+ left = containerRect.left - offsetParentRect.left - (offsetParent?.clientLeft || 0)
1632
+ }
1633
+
1634
+ this.content.main.style.top = top + 'px'
1635
+ this.content.main.style.left = left + 'px'
1542
1636
  this.content.main.style.width = containerRect.width + 'px'
1543
1637
  }
1544
1638
 
1545
- public moveContentBelow(): void {
1546
- // Set direction classes on both main and content
1547
- this.main.main.classList.remove(this.classes.dirAbove)
1548
- this.main.main.classList.add(this.classes.dirBelow)
1549
- this.content.main.classList.remove(this.classes.dirAbove)
1550
- this.content.main.classList.add(this.classes.dirBelow)
1639
+ public moveContentAbove(): void {
1640
+ this.setContentDirection('above')
1641
+ this.setContentPosition()
1642
+ }
1551
1643
 
1552
- // Set the content position
1553
- const containerRect = this.main.main.getBoundingClientRect()
1554
- this.content.main.style.margin = '-1px 0px 0px 0px'
1555
- // Dont do anything if the content is relative
1556
- if (this.settings.contentPosition !== 'relative') {
1557
- this.content.main.style.top =
1558
- containerRect.top +
1559
- containerRect.height +
1560
- (this.settings.contentPosition === 'fixed' ? 0 : window.scrollY) +
1561
- 'px'
1562
- this.content.main.style.left =
1563
- containerRect.left + (this.settings.contentPosition === 'fixed' ? 0 : window.scrollX) + 'px'
1564
- this.content.main.style.width = containerRect.width + 'px'
1565
- }
1644
+ public moveContentBelow(): void {
1645
+ this.setContentDirection('below')
1646
+ this.setContentPosition()
1566
1647
  }
1567
1648
 
1568
1649
  public ensureElementInView(container: HTMLElement, element: HTMLElement): void {
@@ -1621,9 +1702,9 @@ export default class Render {
1621
1702
  const hideClass = this.classes.hide
1622
1703
 
1623
1704
  if (allowDeselect && !(isMultiple && !hasSelectedItems)) {
1624
- deselectButton.classList.remove(hideClass)
1705
+ this.removeClasses(deselectButton, hideClass)
1625
1706
  } else {
1626
- deselectButton.classList.add(hideClass)
1707
+ this.addClasses(deselectButton, hideClass)
1627
1708
  }
1628
1709
  }
1629
1710
  }