slim-select 3.4.3 → 3.6.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.
Files changed (73) hide show
  1. package/dist/classes.d.ts +47 -0
  2. package/dist/helpers.d.ts +5 -0
  3. package/dist/index.d.ts +53 -319
  4. package/dist/index.umd.d.ts +2 -0
  5. package/dist/react/index.d.ts +4 -319
  6. package/dist/react/index.js +1471 -1697
  7. package/dist/react/index.umd.js +2 -2
  8. package/dist/react/react.d.ts +17 -0
  9. package/dist/react/react.test.d.ts +1 -0
  10. package/dist/react/slim-select/react/index.d.ts +4 -0
  11. package/dist/render.d.ts +90 -0
  12. package/dist/select.d.ts +38 -0
  13. package/dist/settings.d.ts +40 -0
  14. package/dist/slimselect.cjs.js +2 -2
  15. package/dist/slimselect.cjs.js.map +1 -1
  16. package/dist/slimselect.css +1 -1
  17. package/dist/slimselect.css.map +1 -1
  18. package/dist/slimselect.es.js +1245 -1385
  19. package/dist/slimselect.es.js.map +1 -1
  20. package/dist/slimselect.iife.js +2 -2
  21. package/dist/slimselect.iife.js.map +1 -1
  22. package/dist/slimselect.js +2 -2
  23. package/dist/slimselect.js.map +1 -1
  24. package/dist/src/docs/main.d.ts +0 -0
  25. package/dist/src/docs/router.d.ts +2 -0
  26. package/dist/src/docs/store.d.ts +12 -0
  27. package/dist/store.d.ts +53 -0
  28. package/dist/vue/index.d.ts +4 -319
  29. package/dist/vue/index.js +1347 -1504
  30. package/dist/vue/index.umd.js +1 -1
  31. package/dist/vue/slim-select/vue/index.d.ts +4 -0
  32. package/package.json +30 -36
  33. package/src/slim-select/slimselect.scss +47 -5
  34. package/.nvmrc +0 -1
  35. package/.prettierignore +0 -1
  36. package/.prettierrc +0 -7
  37. package/CODE_OF_CONDUCT.md +0 -46
  38. package/CONTRIBUTING.md +0 -1
  39. package/cdn.html +0 -21
  40. package/docs.go +0 -12
  41. package/example.html +0 -25
  42. package/index.html +0 -76
  43. package/release.js +0 -191
  44. package/sitemap.js +0 -108
  45. package/src/slim-select/accessibility.test.ts +0 -782
  46. package/src/slim-select/classes.test.ts +0 -73
  47. package/src/slim-select/classes.ts +0 -117
  48. package/src/slim-select/helpers.test.ts +0 -162
  49. package/src/slim-select/helpers.ts +0 -61
  50. package/src/slim-select/index.test.ts +0 -1356
  51. package/src/slim-select/index.ts +0 -594
  52. package/src/slim-select/index.umd.ts +0 -8
  53. package/src/slim-select/react/index.tsx +0 -9
  54. package/src/slim-select/react/react.test.tsx +0 -498
  55. package/src/slim-select/react/react.tsx +0 -209
  56. package/src/slim-select/render.test.ts +0 -1927
  57. package/src/slim-select/render.ts +0 -1771
  58. package/src/slim-select/select.test.ts +0 -683
  59. package/src/slim-select/select.ts +0 -659
  60. package/src/slim-select/settings.test.ts +0 -285
  61. package/src/slim-select/settings.ts +0 -78
  62. package/src/slim-select/store.test.ts +0 -1007
  63. package/src/slim-select/store.ts +0 -433
  64. package/src/slim-select/vue/index.ts +0 -9
  65. package/src/slim-select/vue/vue.test.ts +0 -1091
  66. package/src/slim-select/vue/vue.vue +0 -224
  67. package/tsconfig.json +0 -26
  68. package/vite.config.docs.mts +0 -29
  69. package/vite.config.lib.es-cjs.ts +0 -40
  70. package/vite.config.lib.umd-iife.ts +0 -34
  71. package/vite.config.react.mts +0 -41
  72. package/vite.config.vue.mts +0 -40
  73. package/vitest.config.ts +0 -30
@@ -1,1927 +0,0 @@
1
- 'use strict'
2
-
3
- import { describe, expect, test, vi, beforeEach } from 'vitest'
4
- import Render, { Callbacks } from './render'
5
- import Settings from './settings'
6
- import Store, { Option } from './store'
7
- import CssClasses from './classes'
8
-
9
- describe('render module', () => {
10
- let render: Render
11
- let openMock: ReturnType<typeof vi.fn>
12
- let closeMock: ReturnType<typeof vi.fn>
13
- let addSelectedMock: ReturnType<typeof vi.fn>
14
- let setSelectedMock: ReturnType<typeof vi.fn>
15
- let addOptionMock: ReturnType<typeof vi.fn>
16
- let searchMock: ReturnType<typeof vi.fn>
17
- let afterChangeMock: ReturnType<typeof vi.fn>
18
- let beforeChangeMock: ReturnType<typeof vi.fn>
19
-
20
- beforeEach(() => {
21
- const store = new Store('single', [
22
- {
23
- text: 'test0',
24
- value: 'test0'
25
- },
26
- {
27
- text: 'test1',
28
- value: 'test1',
29
- html: '<span>test1</span>'
30
- },
31
- {
32
- text: 'test2',
33
- selected: true
34
- }
35
- ])
36
-
37
- // default settings
38
- const settings = new Settings()
39
- const classes = new CssClasses()
40
-
41
- openMock = vi.fn(() => {})
42
- closeMock = vi.fn(() => {})
43
- addSelectedMock = vi.fn(() => {})
44
- setSelectedMock = vi.fn(() => {})
45
- addOptionMock = vi.fn(() => {})
46
- searchMock = vi.fn(() => {})
47
- afterChangeMock = vi.fn(() => {})
48
- beforeChangeMock = vi.fn(() => true)
49
-
50
- // default callbacks
51
- const callbacks: Callbacks = {
52
- open: openMock as () => void,
53
- close: closeMock as () => void,
54
- setSelected: setSelectedMock as (value: string | string[], runAfterChange: boolean) => void,
55
- addOption: addOptionMock as (option: Option) => void,
56
- search: searchMock as (search: string) => void,
57
- afterChange: afterChangeMock as (newVal: Option[]) => void,
58
- beforeChange: beforeChangeMock as (newVal: Option[], oldVal: Option[]) => boolean | void
59
- }
60
-
61
- render = new Render(settings, classes, store, callbacks)
62
- })
63
-
64
- describe('constructor', () => {
65
- test('default constructor works', () => {
66
- // create a new store with 2 options
67
- const store = new Store('single', [
68
- { text: 'test1', value: 'test1' },
69
- { text: 'test2', value: 'test2' }
70
- ])
71
-
72
- // default settings
73
- const settings = new Settings()
74
- const classes = new CssClasses()
75
-
76
- // default callbacks
77
- const callbacks = {
78
- open: () => {},
79
- close: () => {},
80
- addSelected: () => {},
81
- setSelected: () => {},
82
- addOption: () => {},
83
- search: () => {},
84
- beforeChange: () => {
85
- return true
86
- }
87
- } as Callbacks
88
-
89
- const render = new Render(settings, classes, store, callbacks)
90
- expect(render).toBeInstanceOf(Render)
91
- expect(render.main.main).toBeInstanceOf(HTMLDivElement)
92
- expect(render.content.search.input).toBeInstanceOf(HTMLInputElement)
93
- })
94
- })
95
-
96
- describe('enable', () => {
97
- test('enable removes disabled class from main and enables search input', () => {
98
- // disable stuff directly
99
- render.main.main.classList.add(render.classes.disabled)
100
- render.content.search.input.disabled = true
101
-
102
- render.enable()
103
- expect(render.main.main.classList.contains(render.classes.disabled)).toBe(false)
104
- expect(render.content.search.input.disabled).toBe(false)
105
- })
106
- })
107
-
108
- describe('disable', () => {
109
- test('disable adds disabled class to main and disables search input', () => {
110
- render.disable()
111
- expect(render.main.main.classList.contains(render.classes.disabled)).toBe(true)
112
- expect(render.content.search.input.disabled).toBe(true)
113
- })
114
- })
115
-
116
- describe('open', () => {
117
- test('open sets the correct attributes and CSS classes', () => {
118
- render.open()
119
-
120
- expect(render.main.arrow.path.getAttribute('d')).toBe(render.classes.arrowOpen)
121
- expect(render.main.main.getAttribute('aria-expanded')).toBe('true')
122
- expect(render.content.main.classList.contains(render.classes.contentOpen)).toBe(true)
123
- // Direction class should be set on both main and content (dirAbove or dirBelow)
124
- const mainHasDirection =
125
- render.main.main.classList.contains(render.classes.dirAbove) ||
126
- render.main.main.classList.contains(render.classes.dirBelow)
127
- const contentHasDirection =
128
- render.content.main.classList.contains(render.classes.dirAbove) ||
129
- render.content.main.classList.contains(render.classes.dirBelow)
130
- expect(mainHasDirection).toBe(true)
131
- expect(contentHasDirection).toBe(true)
132
- })
133
- })
134
-
135
- describe('close', () => {
136
- test('close sets the correct attributes and CSS classes', () => {
137
- render.open()
138
- render.close()
139
-
140
- expect(render.main.arrow.path.getAttribute('d')).toBe(render.classes.arrowClose)
141
- expect(render.main.main.getAttribute('aria-expanded')).toBe('false')
142
- expect(render.content.main.classList.contains(render.classes.contentOpen)).toBe(false)
143
- // Direction class should persist after close
144
- const hasDirection =
145
- render.content.main.classList.contains(render.classes.dirAbove) ||
146
- render.content.main.classList.contains(render.classes.dirBelow)
147
- expect(hasDirection).toBe(true)
148
- })
149
- })
150
-
151
- describe('updateClassStyles', () => {
152
- test('existing classes and styles are cleared', () => {
153
- // manually set classes and styles for testing
154
- render.main.main.className = 'test'
155
- render.main.main.style.color = 'red'
156
-
157
- render.content.main.className = 'test'
158
- render.content.main.style.color = 'red'
159
-
160
- render.updateClassStyles()
161
-
162
- expect(render.main.main.classList.contains('test')).toBe(false)
163
- expect(render.main.main.style.color).toBeFalsy()
164
-
165
- expect(render.content.main.classList.contains('test')).toBe(false)
166
- expect(render.content.main.style.color).toBeFalsy()
167
- })
168
-
169
- test('inline styles are applied to main elements', () => {
170
- render.settings.style = 'color: red'
171
-
172
- render.updateClassStyles()
173
-
174
- expect(render.main.main.style.color).toBe('red')
175
- expect(render.content.main.style.color).toBe('red')
176
- })
177
-
178
- test('classes are applied to main elements', () => {
179
- render.settings.class = ['test0', 'test1', 'test2']
180
-
181
- render.updateClassStyles()
182
-
183
- expect(render.main.main.classList.contains('test0')).toBe(true)
184
- expect(render.main.main.classList.contains('test1')).toBe(true)
185
- expect(render.main.main.classList.contains('test2')).toBe(true)
186
-
187
- expect(render.content.main.classList.contains('test0')).toBe(true)
188
- expect(render.content.main.classList.contains('test1')).toBe(true)
189
- expect(render.content.main.classList.contains('test2')).toBe(true)
190
- })
191
-
192
- test('if content position is relative, class is added on content', () => {
193
- render.settings.contentPosition = 'relative'
194
-
195
- render.updateClassStyles()
196
-
197
- expect(render.content.main.classList.contains('ss-relative')).toBe(true)
198
- })
199
- })
200
-
201
- describe('updateAriaAttributes', () => {
202
- test('sets correct aria attributes', () => {
203
- render.updateAriaAttributes()
204
-
205
- expect(render.main.main.role).toBe('combobox')
206
- expect(render.main.main.getAttribute('aria-haspopup')).toBe('listbox')
207
- expect(render.main.main.getAttribute('aria-controls')).toBe(render.content.list.id)
208
- expect(render.main.main.getAttribute('aria-expanded')).toBe('false')
209
- expect(render.content.list.getAttribute('role')).toBe('listbox')
210
- expect(render.content.list.getAttribute('aria-label')).toContain('listbox')
211
- })
212
- })
213
-
214
- describe('mainDiv', () => {
215
- test('correct HTML element gets created', () => {
216
- const main = render.main.main
217
-
218
- expect(main.dataset.id).toBe(render.settings.id)
219
- expect(main.getAttribute('aria-label')).toBe(render.settings.ariaLabel)
220
- expect(main.tabIndex).toBe(0)
221
- expect(main.children).toHaveLength(3)
222
- expect(main.children.item(0)?.className).toBe(render.classes.values)
223
- expect(main.children.item(1)?.classList.contains(render.classes.deselect)).toBe(true)
224
- expect(main.children.item(1)?.classList.contains(render.classes.hide)).toBe(true)
225
- expect(main.children.item(1)?.children).toHaveLength(1)
226
- expect(main.children.item(1)?.children).toHaveLength(1)
227
- expect(main.children.item(1)?.children.item(0)).toBeInstanceOf(SVGElement)
228
- expect(main.children.item(2)?.classList.contains(render.classes.arrow)).toBe(true)
229
- expect(main.children.item(2)?.classList.contains(render.classes.hide)).toBe(false)
230
- expect(main.children.item(2)?.children.item(0)).toBeInstanceOf(SVGElement)
231
- })
232
-
233
- test('arrow is hidden when alwaysOpen is set', () => {
234
- render.settings.alwaysOpen = true
235
- const main = render.mainDiv().main
236
-
237
- expect(main.children.item(2)?.classList.contains(render.classes.arrow)).toBe(true)
238
- expect(main.children.item(2)?.classList.contains(render.classes.hide)).toBe(true)
239
- expect(main.children.item(2)?.children.item(0)).toBeInstanceOf(SVGElement)
240
- })
241
-
242
- test('arrow key events on main element move highlight', () => {
243
- const highlightMock = vi.fn(() => {})
244
-
245
- render.highlight = highlightMock
246
-
247
- render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
248
- expect(openMock).toHaveBeenCalled()
249
- expect(highlightMock).toHaveBeenCalledTimes(1)
250
- expect(highlightMock.mock.calls[0]).toStrictEqual(['up'])
251
-
252
- render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
253
- expect(openMock).toHaveBeenCalledTimes(2)
254
- expect(highlightMock).toHaveBeenCalledTimes(2)
255
- expect(highlightMock.mock.calls[1]).toStrictEqual(['down'])
256
- })
257
-
258
- test('tab and escape key event on main element triggers close callback', () => {
259
- render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
260
- expect(closeMock).toHaveBeenCalled()
261
- render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
262
- expect(closeMock).toHaveBeenCalledTimes(2)
263
- })
264
-
265
- test('enter and space key event on main element triggers open callback', () => {
266
- render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
267
- expect(openMock).toHaveBeenCalled()
268
- render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))
269
- expect(openMock).toHaveBeenCalledTimes(2)
270
- })
271
-
272
- test('click on main event does nothing if element is disabled', () => {
273
- render.settings.disabled = true
274
-
275
- render.main.main.dispatchEvent(new MouseEvent('click'))
276
-
277
- expect(openMock).not.toHaveBeenCalled()
278
- expect(closeMock).not.toHaveBeenCalled()
279
- })
280
-
281
- test('click on main event triggers open if element is closed', () => {
282
- render.main.main.dispatchEvent(new MouseEvent('click'))
283
-
284
- expect(openMock).toHaveBeenCalled()
285
- expect(closeMock).not.toHaveBeenCalled()
286
- })
287
-
288
- test('click on main event triggers close if element is opened', () => {
289
- render.settings.isOpen = true
290
-
291
- render.main.main.dispatchEvent(new MouseEvent('click'))
292
-
293
- expect(openMock).not.toHaveBeenCalled()
294
- expect(closeMock).toHaveBeenCalled()
295
- })
296
-
297
- test('click on deselect does nothing if element is disabled', () => {
298
- render.settings.disabled = true
299
-
300
- const deselectElement: HTMLDivElement = render.main.main.querySelector(
301
- '.' + render.classes.deselect
302
- ) as HTMLDivElement
303
-
304
- deselectElement.dispatchEvent(new MouseEvent('click'))
305
-
306
- expect(afterChangeMock).not.toHaveBeenCalled()
307
- })
308
-
309
- test('click on deselect on single select runs callbacks', () => {
310
- const deselectElement: HTMLDivElement = render.main.main.querySelector(
311
- '.' + render.classes.deselect
312
- ) as HTMLDivElement
313
-
314
- deselectElement.dispatchEvent(new MouseEvent('click'))
315
-
316
- expect(setSelectedMock).toHaveBeenCalled()
317
- expect(closeMock).toHaveBeenCalled()
318
- expect(afterChangeMock).toHaveBeenCalled()
319
- })
320
-
321
- test('click on deselect on multiple select runs callbacks', () => {
322
- render.settings.isMultiple = true
323
-
324
- const deselectAllMock = vi.fn()
325
-
326
- render.updateDeselectAll = deselectAllMock
327
-
328
- const deselectElement: HTMLDivElement = render.main.main.querySelector(
329
- '.' + render.classes.deselect
330
- ) as HTMLDivElement
331
- deselectElement.dispatchEvent(new MouseEvent('click'))
332
-
333
- expect(deselectAllMock).toHaveBeenCalled()
334
- expect(setSelectedMock).toHaveBeenCalled()
335
- expect(closeMock).toHaveBeenCalled()
336
- expect(afterChangeMock).toHaveBeenCalled()
337
- })
338
- })
339
-
340
- describe('mainFocus', () => {
341
- let focusMock: (options?: FocusOptions | undefined) => void
342
-
343
- beforeEach(() => {
344
- focusMock = vi.fn(() => {}) as (options?: FocusOptions | undefined) => void
345
- render.main.main.focus = focusMock
346
- })
347
-
348
- test('mainFocus does nothing if the event is click', () => {
349
- render.mainFocus('click')
350
- expect(focusMock).not.toHaveBeenCalled()
351
- })
352
-
353
- test('mainFocus triggers focus if the event is not click', () => {
354
- render.mainFocus('keydown')
355
- expect(focusMock).toHaveBeenCalled()
356
- render.mainFocus('keyup')
357
- expect(focusMock).toHaveBeenCalledTimes(2)
358
- render.mainFocus('mouse')
359
- expect(focusMock).toHaveBeenCalledTimes(3)
360
- })
361
- })
362
-
363
- describe('placeholder', () => {
364
- test('placeholder uses fallback text if no option is found', () => {
365
- render.settings.placeholderText = 'placeholder text'
366
- const placeholder = render.placeholder()
367
-
368
- expect(placeholder.innerHTML).toBe(render.settings.placeholderText)
369
- })
370
-
371
- test('placeholder uses option html if option is found', () => {
372
- render.store.setData([
373
- {
374
- text: 'opt text',
375
- html: '<h1>Option HTML</h1>',
376
- placeholder: true
377
- }
378
- ])
379
- const placeholder = render.placeholder()
380
-
381
- expect(placeholder.innerHTML).toBe('<h1>Option HTML</h1>')
382
- })
383
-
384
- test('placeholder uses option text if option is found and no HTML is set', () => {
385
- render.store.setData([
386
- {
387
- text: 'opt text',
388
- placeholder: true
389
- }
390
- ])
391
- const placeholder = render.placeholder()
392
-
393
- expect(placeholder.innerHTML).toBe('opt text')
394
- })
395
- })
396
-
397
- describe('renderValues', () => {
398
- test('single select renders only one value', () => {
399
- render.renderValues()
400
-
401
- expect(render.main.values.children).toHaveLength(1)
402
- })
403
-
404
- test('single select renders HTML option', () => {
405
- render.store.setData([
406
- {
407
- text: 'opt0'
408
- },
409
- {
410
- text: 'opt1',
411
- html: '<span>opt1</span>',
412
- selected: true
413
- }
414
- ])
415
- render.renderValues()
416
-
417
- expect(render.main.values.children).toHaveLength(1)
418
- expect(render.main.values.children.item(0)?.innerHTML).toBe('<span>opt1</span>')
419
- })
420
-
421
- test('multiple select renders all selected values', () => {
422
- render.settings.isMultiple = true
423
- render.store = new Store('multiple', [
424
- {
425
- text: 'opt0',
426
- value: 'opt0',
427
- selected: true
428
- },
429
- {
430
- text: 'opt1',
431
- value: 'opt1',
432
- html: '<span>opt1</span>',
433
- selected: true
434
- },
435
- {
436
- text: 'opt2'
437
- }
438
- ])
439
-
440
- render.renderValues()
441
-
442
- expect(render.main.values.children).toHaveLength(2)
443
- expect(render.main.values.children.item(0)).toBeInstanceOf(HTMLDivElement)
444
- expect((render.main.values.children.item(0) as HTMLDivElement).textContent).toBe('opt0')
445
- expect(render.main.values.children.item(1)).toBeInstanceOf(HTMLDivElement)
446
- expect((render.main.values.children.item(1) as HTMLDivElement).textContent).toBe('opt1')
447
- })
448
-
449
- test('multiple select renders counter element when maxValuesShown is set', () => {
450
- render.settings.isMultiple = true
451
- render.settings.maxValuesShown = 2
452
- render.store = new Store('multiple', [
453
- {
454
- text: 'opt0',
455
- value: 'opt0',
456
- selected: true
457
- },
458
- {
459
- text: 'opt1',
460
- value: 'opt1',
461
- html: '<span>opt1</span>',
462
- selected: true
463
- },
464
- {
465
- text: 'opt2',
466
- value: 'opt2',
467
- selected: true
468
- },
469
- {
470
- text: 'opt4'
471
- }
472
- ])
473
-
474
- render.renderValues()
475
-
476
- expect(render.main.values.children).toHaveLength(1)
477
- expect(render.main.values.children.item(0)?.innerHTML).toBe('3 selected')
478
- })
479
-
480
- test('remove old options from values', () => {
481
- render.renderValues()
482
- expect(render.main.values.children).toHaveLength(1)
483
- expect(render.main.values.innerText).toBeFalsy()
484
-
485
- render.store.setSelectedBy('value', ['test1'])
486
- render.renderValues()
487
-
488
- expect(render.main.values.children).toHaveLength(1)
489
- expect(render.main.values.children.item(0)?.innerHTML).toBe('<span>test1</span>')
490
- })
491
- })
492
-
493
- describe('moveContent', () => {
494
- let contentAboveMock: () => void
495
- let contentBelowMock: () => void
496
-
497
- beforeEach(() => {
498
- contentAboveMock = vi.fn() as () => void
499
- contentBelowMock = vi.fn() as () => void
500
-
501
- render.moveContentAbove = contentAboveMock
502
- render.moveContentBelow = contentBelowMock
503
- })
504
-
505
- test('content is moved below when position is relative', () => {
506
- render.settings.contentPosition = 'relative'
507
-
508
- render.moveContent()
509
- expect(contentAboveMock).not.toHaveBeenCalled()
510
- expect(contentBelowMock).toHaveBeenCalled()
511
- })
512
-
513
- test('content is moved below when open position is down', () => {
514
- render.settings.openPosition = 'down'
515
-
516
- render.moveContent()
517
- expect(contentAboveMock).not.toHaveBeenCalled()
518
- expect(contentBelowMock).toHaveBeenCalled()
519
- })
520
-
521
- test('content is moved above when open position is up', () => {
522
- render.settings.openPosition = 'up'
523
-
524
- render.moveContent()
525
- expect(contentAboveMock).toHaveBeenCalled()
526
- expect(contentBelowMock).not.toHaveBeenCalled()
527
- })
528
-
529
- test('content is moved above when putContent is up', () => {
530
- render.putContent = vi.fn(() => 'up') as any
531
-
532
- render.moveContent()
533
- expect(contentAboveMock).toHaveBeenCalled()
534
- expect(contentBelowMock).not.toHaveBeenCalled()
535
- })
536
-
537
- test('content is moved below when putContent is down', () => {
538
- render.putContent = vi.fn(() => 'down') as any
539
-
540
- render.moveContent()
541
- expect(contentAboveMock).not.toHaveBeenCalled()
542
- expect(contentBelowMock).toHaveBeenCalled()
543
- })
544
- })
545
-
546
- describe('searchDiv', () => {
547
- test('search is hidden when showSearch setting is false', () => {
548
- render.settings.showSearch = false
549
-
550
- const search = render.searchDiv()
551
-
552
- expect(search.main.classList.contains(render.classes.hide)).toBe(true)
553
- })
554
-
555
- test('input is debounced', async () => {
556
- const search = render.searchDiv()
557
-
558
- search.input.dispatchEvent(new InputEvent('input', { data: 'asdf' }))
559
-
560
- // wait for debounce to trigger
561
- await new Promise((r) => setTimeout(r, 101))
562
-
563
- expect(searchMock).toHaveBeenCalled()
564
- })
565
-
566
- test('arrow keys move highlight', () => {
567
- const search = render.searchDiv()
568
- const highlightMock = vi.fn(() => {})
569
-
570
- render.highlight = highlightMock
571
-
572
- search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
573
- expect(highlightMock).toHaveBeenCalledTimes(1)
574
- expect(highlightMock.mock.calls[0]).toStrictEqual(['up'])
575
-
576
- search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
577
- expect(highlightMock).toHaveBeenCalledTimes(2)
578
- expect(highlightMock.mock.calls[1]).toStrictEqual(['down'])
579
- })
580
-
581
- test('tab triggers close callback', () => {
582
- const search = render.searchDiv()
583
-
584
- search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
585
-
586
- expect(closeMock).toHaveBeenCalled()
587
- })
588
-
589
- test('escape triggers close callback', () => {
590
- // separate test in case we want to also test the event someday
591
- const search = render.searchDiv()
592
-
593
- search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
594
-
595
- expect(closeMock).toHaveBeenCalled()
596
- })
597
-
598
- test("enter and space don't call addable witout ctrl key", () => {
599
- const search = render.searchDiv()
600
- const addableMock = vi.fn((s: string) => ({
601
- text: s,
602
- value: s.toLowerCase()
603
- }))
604
-
605
- render.callbacks.addable = addableMock
606
-
607
- search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
608
- expect(addableMock).not.toHaveBeenCalled()
609
-
610
- search.input.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))
611
- expect(addableMock).not.toHaveBeenCalled()
612
- })
613
-
614
- test('enter and space call event and does nothing without input value', () => {
615
- const addableMock = vi.fn((s: string) => ({
616
- text: s,
617
- value: s.toLowerCase()
618
- }))
619
-
620
- render.callbacks.addable = addableMock
621
-
622
- // recreate search because we have added the addable callback
623
- render.content.search = render.searchDiv()
624
-
625
- render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
626
- expect(addableMock).not.toHaveBeenCalled()
627
- })
628
-
629
- test('enter and space call addable when defined', () => {
630
- const addableMock = vi.fn((s: string) => ({
631
- text: s,
632
- value: s.toLowerCase()
633
- }))
634
-
635
- render.callbacks.addable = addableMock
636
-
637
- // recreate search because we have added the addable callback
638
- render.content.search = render.searchDiv()
639
-
640
- render.content.search.input.value = 'Search'
641
-
642
- render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
643
- expect(addableMock).toHaveBeenCalledTimes(1)
644
- expect(addableMock.mock.calls[0]).toStrictEqual(['Search'])
645
- })
646
-
647
- test('enter selects highlighted option before calling addable', () => {
648
- const addableMock = vi.fn((s: string) => ({
649
- text: s,
650
- value: s.toLowerCase()
651
- }))
652
-
653
- render.callbacks.addable = addableMock
654
-
655
- // recreate search because we have added the addable callback
656
- render.content.search = render.searchDiv()
657
-
658
- // Render options
659
- render.renderOptions(render.store.getDataOptions())
660
-
661
- // Set search value
662
- render.content.search.input.value = '1'
663
-
664
- // Highlight first option (simulating arrow down)
665
- render.highlight('down')
666
-
667
- // Verify an option is highlighted
668
- const highlighted = render.content.list.querySelector('.' + render.classes.highlighted)
669
- expect(highlighted).toBeTruthy()
670
-
671
- // Press Enter - should select highlighted option, NOT call addable
672
- render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
673
-
674
- // Addable should NOT have been called because an option was highlighted
675
- expect(addableMock).not.toHaveBeenCalled()
676
- })
677
-
678
- test('enter calls addable when no option is highlighted', () => {
679
- const addableMock = vi.fn((s: string) => ({
680
- text: s,
681
- value: s.toLowerCase()
682
- }))
683
-
684
- render.callbacks.addable = addableMock
685
-
686
- // recreate search because we have added the addable callback
687
- render.content.search = render.searchDiv()
688
-
689
- // Render options
690
- render.renderOptions(render.store.getDataOptions())
691
-
692
- // Set search value
693
- render.content.search.input.value = 'NewItem'
694
-
695
- // Do NOT highlight any option (user just types and presses Enter)
696
-
697
- // Press Enter - should call addable since no option is highlighted
698
- render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
699
-
700
- // Addable SHOULD have been called
701
- expect(addableMock).toHaveBeenCalledTimes(1)
702
- expect(addableMock.mock.calls[0]).toStrictEqual(['NewItem'])
703
- })
704
- })
705
-
706
- describe('searchFocus', () => {
707
- test('search is focused', () => {
708
- expect(document.activeElement).not.toBe(render.content.search.input)
709
-
710
- render.searchFocus()
711
- expect(document.activeElement).toBe(render.content.search.input)
712
- })
713
- })
714
-
715
- describe('getOptions', () => {
716
- test('returns all options when called without parameters', () => {
717
- // render all 3 default options and get them back
718
- render.renderOptions(render.store.getDataOptions())
719
- const opts = render.getOptions()
720
-
721
- expect(opts).toHaveLength(3)
722
- })
723
-
724
- test('filters correctly when filtering out placeholders', () => {
725
- render.renderOptions(
726
- render.store.partialToFullData([
727
- {
728
- text: 'opt0',
729
- placeholder: true
730
- },
731
- {
732
- text: 'opt1'
733
- },
734
- {
735
- text: 'opt2'
736
- }
737
- ])
738
- )
739
-
740
- const opts = render.getOptions(true, false, true)
741
- expect(opts).toHaveLength(2)
742
- })
743
-
744
- test('filters correctly when filtering out disabled options', () => {
745
- render.renderOptions(
746
- render.store.partialToFullData([
747
- {
748
- text: 'opt0',
749
- disabled: true
750
- },
751
- {
752
- text: 'opt1'
753
- },
754
- {
755
- text: 'opt2'
756
- }
757
- ])
758
- )
759
-
760
- const opts = render.getOptions(false, true)
761
- expect(opts).toHaveLength(2)
762
- })
763
-
764
- test('filters correctly when filtering out hidden options', () => {
765
- render.renderOptions(
766
- render.store.partialToFullData([
767
- {
768
- text: 'opt0',
769
- placeholder: true
770
- },
771
- {
772
- text: 'opt1'
773
- },
774
- {
775
- text: 'opt2'
776
- }
777
- ])
778
- )
779
-
780
- const opts = render.getOptions(false, false, true)
781
- expect(opts).toHaveLength(2)
782
- })
783
-
784
- test('filters correctly when filtering out hidden and disabled options', () => {
785
- render.renderOptions(
786
- render.store.partialToFullData([
787
- {
788
- text: 'opt0',
789
- disabled: true
790
- },
791
- {
792
- text: 'opt1',
793
- placeholder: true
794
- },
795
- {
796
- text: 'opt2'
797
- }
798
- ])
799
- )
800
-
801
- const opts = render.getOptions(false, true, true)
802
- expect(opts).toHaveLength(1)
803
- })
804
- })
805
-
806
- describe('highlight', () => {
807
- test('simply do nothing without breaking when options are empty', () => {
808
- render.renderOptions([])
809
-
810
- expect(() => render.highlight('up')).not.toThrow()
811
- })
812
-
813
- test('highlight single option that is not already highlighted', () => {
814
- render.renderOptions(
815
- render.store.partialToFullData([
816
- {
817
- text: 'opt0'
818
- }
819
- ])
820
- )
821
-
822
- render.highlight('up')
823
-
824
- expect(render.getOptions()[0].classList.contains(render.classes.highlighted)).toBe(true)
825
- })
826
-
827
- test('select first option with down when no option is highlighted or selected', () => {
828
- render.renderOptions(
829
- render.store.partialToFullData([
830
- {
831
- text: 'opt0'
832
- },
833
- {
834
- text: 'opt1'
835
- },
836
- {
837
- text: 'opt2'
838
- }
839
- ])
840
- )
841
-
842
- render.highlight('down')
843
-
844
- expect(render.getOptions()[0].classList.contains(render.classes.highlighted)).toBe(true)
845
- })
846
-
847
- test('select last option with up when no option is highlighted or selected', () => {
848
- render.renderOptions(
849
- render.store.partialToFullData([
850
- {
851
- text: 'opt0'
852
- },
853
- {
854
- text: 'opt1'
855
- },
856
- {
857
- text: 'opt2'
858
- }
859
- ])
860
- )
861
-
862
- render.highlight('up')
863
-
864
- expect(render.getOptions()[2].classList.contains(render.classes.highlighted)).toBe(true)
865
- })
866
-
867
- test('highlight next option on down after highlighted option', () => {
868
- render.renderOptions(
869
- render.store.partialToFullData([
870
- {
871
- text: 'opt0',
872
- class: render.classes.highlighted
873
- },
874
- {
875
- text: 'opt1'
876
- },
877
- {
878
- text: 'opt2'
879
- }
880
- ])
881
- )
882
-
883
- render.highlight('down')
884
-
885
- expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
886
- })
887
-
888
- test('highlight previous option on up before highlighted option', () => {
889
- render.renderOptions(
890
- render.store.partialToFullData([
891
- {
892
- text: 'opt0'
893
- },
894
- {
895
- text: 'opt1'
896
- },
897
- {
898
- text: 'opt2',
899
- class: render.classes.highlighted
900
- }
901
- ])
902
- )
903
-
904
- render.highlight('up')
905
-
906
- expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
907
- })
908
-
909
- test('highlight next option on down after selected option when no options is highlighted', () => {
910
- render.renderOptions(
911
- render.store.partialToFullData([
912
- {
913
- text: 'opt0',
914
- selected: true
915
- },
916
- {
917
- text: 'opt1'
918
- },
919
- {
920
- text: 'opt2'
921
- }
922
- ])
923
- )
924
-
925
- render.highlight('down')
926
-
927
- expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
928
- })
929
-
930
- test('skip to last option when using up at the first option', () => {
931
- render.renderOptions(
932
- render.store.partialToFullData([
933
- {
934
- text: 'opt0',
935
- selected: true
936
- },
937
- {
938
- text: 'opt1'
939
- },
940
- {
941
- text: 'opt2'
942
- }
943
- ])
944
- )
945
-
946
- render.highlight('up')
947
-
948
- expect(render.getOptions()[2].classList.contains(render.classes.highlighted)).toBe(true)
949
- })
950
-
951
- test('highlight next option within opt group on down', () => {
952
- render.renderOptions(
953
- render.store.partialToFullData([
954
- {
955
- text: 'opt0',
956
- selected: true
957
- },
958
- {
959
- label: 'opt group',
960
- options: [
961
- {
962
- text: 'opt1'
963
- }
964
- ]
965
- },
966
- {
967
- text: 'opt2'
968
- }
969
- ])
970
- )
971
-
972
- render.highlight('down')
973
-
974
- expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
975
- })
976
-
977
- test('highlight previous option within opt group on up', () => {
978
- render.renderOptions(
979
- render.store.partialToFullData([
980
- {
981
- text: 'opt0'
982
- },
983
- {
984
- label: 'opt group',
985
- options: [
986
- {
987
- text: 'opt1',
988
- selected: true
989
- }
990
- ]
991
- },
992
- {
993
- text: 'opt2'
994
- }
995
- ])
996
- )
997
-
998
- render.highlight('up')
999
-
1000
- expect(render.getOptions()[0].classList.contains(render.classes.highlighted)).toBe(true)
1001
- })
1002
- })
1003
-
1004
- describe('listDiv', () => {
1005
- test('list div has correct class', () => {
1006
- const list = render.listDiv()
1007
-
1008
- expect(list.classList.contains(render.classes.list)).toBe(true)
1009
- })
1010
- })
1011
-
1012
- describe('renderError', () => {
1013
- test('error message is rendered correctly', () => {
1014
- expect(render.content.list.children).toHaveLength(0)
1015
-
1016
- render.renderError('test error')
1017
-
1018
- expect(render.content.list.children).toHaveLength(1)
1019
- expect(render.content.list.children.item(0)).toBeInstanceOf(HTMLDivElement)
1020
- expect(render.content.list.children.item(0)?.className).toBe(render.classes.error)
1021
- expect(render.content.list.children.item(0)?.textContent).toBe('test error')
1022
- })
1023
-
1024
- test('list is reset on new error', () => {
1025
- expect(render.content.list.children).toHaveLength(0)
1026
-
1027
- render.renderError('test error')
1028
- expect(render.content.list.children).toHaveLength(1)
1029
-
1030
- render.renderError('error 2')
1031
- expect(render.content.list.children).toHaveLength(1)
1032
- expect(render.content.list.children.item(0)?.textContent).toBe('error 2')
1033
- })
1034
- })
1035
-
1036
- describe('renderSearching', () => {
1037
- test('search text is rendered correctly', () => {
1038
- expect(render.content.list.children).toHaveLength(0)
1039
-
1040
- render.settings.searchingText = 'search'
1041
- render.renderSearching()
1042
-
1043
- expect(render.content.list.children).toHaveLength(1)
1044
- expect(render.content.list.children.item(0)).toBeInstanceOf(HTMLDivElement)
1045
- expect(render.content.list.children.item(0)?.className).toBe(render.classes.searching)
1046
- expect(render.content.list.children.item(0)?.textContent).toBe('search')
1047
- })
1048
-
1049
- test('list is reset on new search text', () => {
1050
- expect(render.content.list.children).toHaveLength(0)
1051
-
1052
- render.settings.searchingText = 'search'
1053
- render.renderSearching()
1054
- expect(render.content.list.children).toHaveLength(1)
1055
-
1056
- render.settings.searchingText = 'search 2'
1057
- render.renderSearching()
1058
- expect(render.content.list.children).toHaveLength(1)
1059
- expect(render.content.list.children.item(0)?.textContent).toBe('search 2')
1060
- })
1061
- })
1062
-
1063
- describe('option', () => {
1064
- test('add inline styles correctly', () => {
1065
- const option = render.option(
1066
- new Option({
1067
- text: 'opt',
1068
- style: 'color: red'
1069
- })
1070
- )
1071
-
1072
- expect(option.style.color).toBe('red')
1073
- })
1074
-
1075
- test('add hidden class on option with display false', () => {
1076
- const option = render.option(
1077
- new Option({
1078
- text: 'opt',
1079
- display: false
1080
- })
1081
- )
1082
-
1083
- expect(option.classList.contains(render.classes.hide)).toBe(true)
1084
- })
1085
-
1086
- test('add hidden class on selected option when hideSelected setting is true', () => {
1087
- render.settings.hideSelected = true
1088
-
1089
- const option = render.option(
1090
- new Option({
1091
- text: 'opt',
1092
- selected: true
1093
- })
1094
- )
1095
-
1096
- expect(option.classList.contains(render.classes.hide)).toBe(true)
1097
- })
1098
-
1099
- test('title attribute is set when showOptionTooltips setting is true', () => {
1100
- render.settings.showOptionTooltips = true
1101
-
1102
- const option = render.option(
1103
- new Option({
1104
- text: 'opt'
1105
- })
1106
- )
1107
-
1108
- expect(option.getAttribute('title')).toBe('opt')
1109
- })
1110
-
1111
- test('text is highlighted correctly with option text', () => {
1112
- render.settings.searchHighlight = true
1113
- render.content.search.input.value = 'opt'
1114
-
1115
- const option = render.option(
1116
- new Option({
1117
- text: 'opt 1'
1118
- })
1119
- )
1120
-
1121
- expect(option.querySelector('mark')).toBeTruthy()
1122
- expect(option.querySelector('mark')?.textContent).toBe('opt')
1123
- })
1124
-
1125
- test('text is highlighted correctly with option HTML', () => {
1126
- render.settings.searchHighlight = true
1127
- render.content.search.input.value = 'opt'
1128
-
1129
- const option = render.option(
1130
- new Option({
1131
- text: 'opt 1',
1132
- html: '<h1>opt 1</h1>'
1133
- })
1134
- )
1135
-
1136
- expect(option.querySelector('mark')).toBeTruthy()
1137
- expect(option.querySelector('mark')?.textContent).toBe('opt')
1138
- })
1139
-
1140
- test('text highlighting with special regex characters does not break HTML', () => {
1141
- render.settings.searchHighlight = true
1142
- render.content.search.input.value = '<'
1143
-
1144
- const option = render.option(
1145
- new Option({
1146
- text: 'option <test>'
1147
- })
1148
- )
1149
-
1150
- // Should not break the HTML structure
1151
- expect(option.querySelector('.ss-option')).toBeFalsy() // No nested ss-option divs
1152
-
1153
- // The < character should be highlighted if found in text
1154
- const mark = option.querySelector('mark')
1155
- if (mark) {
1156
- expect(mark.textContent).toBe('<')
1157
- }
1158
- })
1159
-
1160
- test('text highlighting escapes special regex characters', () => {
1161
- render.settings.searchHighlight = true
1162
-
1163
- // Test various special regex characters
1164
- const specialChars = ['<', '>', '.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']
1165
-
1166
- specialChars.forEach((char) => {
1167
- render.content.search.input.value = char
1168
- const option = render.option(
1169
- new Option({
1170
- text: `option ${char} test`
1171
- })
1172
- )
1173
-
1174
- // Should not throw an error and should contain the character
1175
- expect(option.textContent).toContain(char)
1176
- })
1177
- })
1178
-
1179
- test('text highlighting with HTML content highlights only text nodes', () => {
1180
- render.settings.searchHighlight = true
1181
- render.content.search.input.value = 'test'
1182
-
1183
- const option = render.option(
1184
- new Option({
1185
- text: 'test option',
1186
- html: '<div class="wrapper">test option</div>'
1187
- })
1188
- )
1189
-
1190
- // Should not break HTML structure - the key fix for issue #570
1191
- expect(option.querySelector('.wrapper')).toBeTruthy() // HTML structure preserved
1192
- expect(option.querySelector('mark')).toBeTruthy() // Text is highlighted
1193
- expect(option.querySelector('mark')?.textContent).toBe('test')
1194
- })
1195
-
1196
- test('click does nothing when option is disabled', () => {
1197
- const option = render.option(
1198
- new Option({
1199
- text: 'opt 1',
1200
- disabled: true
1201
- })
1202
- )
1203
- option.dispatchEvent(new MouseEvent('click'))
1204
-
1205
- expect(addOptionMock).not.toHaveBeenCalled()
1206
- expect(setSelectedMock).not.toHaveBeenCalled()
1207
- })
1208
-
1209
- test('click does nothing when max count of selected options is reached', () => {
1210
- render.settings.isMultiple = true
1211
- render.settings.maxSelected = 1
1212
-
1213
- const option = render.option(
1214
- new Option({
1215
- text: 'opt 1'
1216
- })
1217
- )
1218
- option.dispatchEvent(new MouseEvent('click'))
1219
-
1220
- expect(addOptionMock).not.toHaveBeenCalled()
1221
- expect(setSelectedMock).not.toHaveBeenCalled()
1222
- })
1223
-
1224
- test('click does nothing when min count of selected options is reached', () => {
1225
- render.settings.isMultiple = true
1226
- render.settings.allowDeselect = true
1227
- render.settings.minSelected = 1
1228
-
1229
- const option = render.option(
1230
- new Option({
1231
- text: 'opt 1',
1232
- selected: true
1233
- })
1234
- )
1235
- option.dispatchEvent(new MouseEvent('click'))
1236
-
1237
- expect(addOptionMock).not.toHaveBeenCalled()
1238
- expect(setSelectedMock).not.toHaveBeenCalled()
1239
- })
1240
-
1241
- test('click does nothing when trying to deselect a mandatory option', () => {
1242
- const option = render.option(
1243
- new Option({
1244
- text: 'mandatory option',
1245
- selected: true,
1246
- mandatory: true
1247
- })
1248
- )
1249
- option.dispatchEvent(new MouseEvent('click'))
1250
-
1251
- expect(addOptionMock).not.toHaveBeenCalled()
1252
- expect(setSelectedMock).not.toHaveBeenCalled()
1253
- })
1254
-
1255
- test('click removes option', () => {
1256
- const option = render.option(
1257
- new Option({
1258
- text: 'new opt 1'
1259
- })
1260
- )
1261
-
1262
- option.dispatchEvent(new MouseEvent('click'))
1263
-
1264
- expect(addOptionMock).toHaveBeenCalled()
1265
-
1266
- // check that we add the right option
1267
- expect(addOptionMock.mock.calls[0][0].text).toBe('new opt 1')
1268
- expect(setSelectedMock).toHaveBeenCalled()
1269
- })
1270
- })
1271
-
1272
- describe('native multi-select behavior', () => {
1273
- let render: Render
1274
- let afterChangeMock: ReturnType<typeof vi.fn>
1275
- let closeMock: ReturnType<typeof vi.fn>
1276
-
1277
- beforeEach(() => {
1278
- // create a new store with 5 options for comprehensive testing
1279
- const store = new Store('multiple', [
1280
- { text: 'test1', value: 'test1' },
1281
- { text: 'test2', value: 'test2' },
1282
- { text: 'test3', value: 'test3' },
1283
- { text: 'test4', value: 'test4' },
1284
- { text: 'test5', value: 'test5' }
1285
- ])
1286
-
1287
- const settings = new Settings()
1288
- const classes = new CssClasses()
1289
-
1290
- afterChangeMock = vi.fn(() => {})
1291
- closeMock = vi.fn(() => {})
1292
-
1293
- const callbacks = {
1294
- open: () => {},
1295
- close: closeMock,
1296
- addSelected: () => {},
1297
- setSelected: (value) => {
1298
- store.setSelectedBy('id', typeof value === 'string' ? [value] : value)
1299
- },
1300
- addOption: () => {},
1301
- search: () => {},
1302
- afterChange: afterChangeMock
1303
- } as Callbacks
1304
-
1305
- render = new Render(settings, classes, store, callbacks)
1306
- render.settings.isMultiple = true
1307
- render.settings.closeOnSelect = true
1308
- })
1309
-
1310
- describe('Regular Click (no modifiers)', () => {
1311
- test('toggles option (add/remove) without affecting others', () => {
1312
- render.renderOptions(render.store.getDataOptions())
1313
- const opts = render.getOptions(false, false, true)
1314
-
1315
- // Click first option - adds it
1316
- opts[0].dispatchEvent(new MouseEvent('click'))
1317
- expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
1318
-
1319
- // Click third option - adds it (first option still selected)
1320
- opts[2].dispatchEvent(new MouseEvent('click'))
1321
- expect(afterChangeMock).toHaveBeenCalledWith([
1322
- expect.objectContaining({ value: 'test1' }),
1323
- expect.objectContaining({ value: 'test3' })
1324
- ])
1325
-
1326
- // Click first option again - removes it
1327
- opts[0].dispatchEvent(new MouseEvent('click'))
1328
- expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test3' })])
1329
- })
1330
-
1331
- test('closes dropdown on regular click when closeOnSelect is true', () => {
1332
- render.renderOptions(render.store.getDataOptions())
1333
- const opts = render.getOptions(false, false, true)
1334
-
1335
- opts[0].dispatchEvent(new MouseEvent('click'))
1336
- expect(closeMock).toHaveBeenCalled()
1337
- })
1338
-
1339
- test('regular click on selected option deselects it and closes dropdown', () => {
1340
- render.renderOptions(render.store.getDataOptions())
1341
- const opts = render.getOptions(false, false, true)
1342
-
1343
- // Select two options
1344
- opts[0].dispatchEvent(new MouseEvent('click'))
1345
- opts[1].dispatchEvent(new MouseEvent('click'))
1346
-
1347
- expect(afterChangeMock).toHaveBeenLastCalledWith([
1348
- expect.objectContaining({ value: 'test1' }),
1349
- expect.objectContaining({ value: 'test2' })
1350
- ])
1351
- closeMock.mockClear()
1352
-
1353
- // Click on already selected option - should deselect it and close
1354
- opts[0].dispatchEvent(new MouseEvent('click'))
1355
-
1356
- expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test2' })])
1357
- expect(closeMock).toHaveBeenCalledTimes(1)
1358
- })
1359
-
1360
- test('regression: regular click adds/removes and closes, Cmd/Ctrl keeps open', () => {
1361
- render.renderOptions(render.store.getDataOptions())
1362
- const opts = render.getOptions(false, false, true)
1363
-
1364
- // Regular click - should close
1365
- opts[0].dispatchEvent(new MouseEvent('click'))
1366
- expect(closeMock).toHaveBeenCalledTimes(1)
1367
- closeMock.mockClear()
1368
-
1369
- // Cmd+Click - should NOT close
1370
- opts[1].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1371
- expect(closeMock).not.toHaveBeenCalled()
1372
-
1373
- // Ctrl+Click - should NOT close
1374
- opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1375
- expect(closeMock).not.toHaveBeenCalled()
1376
-
1377
- // Regular click again - should close
1378
- opts[3].dispatchEvent(new MouseEvent('click'))
1379
- expect(closeMock).toHaveBeenCalledTimes(1)
1380
- })
1381
- })
1382
-
1383
- describe('Cmd/Ctrl+Click (toggle selection)', () => {
1384
- test('Cmd+Click adds option without deselecting others (Mac)', () => {
1385
- render.renderOptions(render.store.getDataOptions())
1386
- const opts = render.getOptions(false, false, true)
1387
-
1388
- // Cmd+Click first option
1389
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1390
- expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
1391
-
1392
- // Cmd+Click third option - both should be selected
1393
- opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1394
- expect(afterChangeMock).toHaveBeenCalledWith([
1395
- expect.objectContaining({ value: 'test1' }),
1396
- expect.objectContaining({ value: 'test3' })
1397
- ])
1398
- })
1399
-
1400
- test('Ctrl+Click adds option without deselecting others (Windows/Linux)', () => {
1401
- render.renderOptions(render.store.getDataOptions())
1402
- const opts = render.getOptions(false, false, true)
1403
-
1404
- // Ctrl+Click first option
1405
- opts[0].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1406
- expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
1407
-
1408
- // Ctrl+Click third option - both should be selected
1409
- opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1410
- expect(afterChangeMock).toHaveBeenCalledWith([
1411
- expect.objectContaining({ value: 'test1' }),
1412
- expect.objectContaining({ value: 'test3' })
1413
- ])
1414
- })
1415
-
1416
- test('Cmd+Click on selected option deselects it (Mac)', () => {
1417
- render.renderOptions(render.store.getDataOptions())
1418
- const opts = render.getOptions(false, false, true)
1419
-
1420
- // Select multiple options with Cmd
1421
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1422
- opts[1].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1423
- opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1424
-
1425
- // Cmd+Click on middle option to deselect it
1426
- opts[1].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1427
-
1428
- expect(afterChangeMock).toHaveBeenCalledWith([
1429
- expect.objectContaining({ value: 'test1' }),
1430
- expect.objectContaining({ value: 'test3' })
1431
- ])
1432
- })
1433
-
1434
- test('Ctrl+Click on selected option deselects it (Windows/Linux)', () => {
1435
- render.renderOptions(render.store.getDataOptions())
1436
- const opts = render.getOptions(false, false, true)
1437
-
1438
- // Select multiple options with Ctrl
1439
- opts[0].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1440
- opts[1].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1441
- opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1442
-
1443
- // Ctrl+Click on middle option to deselect it
1444
- opts[1].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1445
-
1446
- expect(afterChangeMock).toHaveBeenCalledWith([
1447
- expect.objectContaining({ value: 'test1' }),
1448
- expect.objectContaining({ value: 'test3' })
1449
- ])
1450
- })
1451
-
1452
- test('Cmd+Click toggles selection on and off', () => {
1453
- render.renderOptions(render.store.getDataOptions())
1454
- const opts = render.getOptions(false, false, true)
1455
-
1456
- // Click to select
1457
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1458
- expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test1' })])
1459
-
1460
- // Click again to deselect
1461
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1462
- expect(afterChangeMock).toHaveBeenLastCalledWith([])
1463
-
1464
- // Click once more to select again
1465
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1466
- expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test1' })])
1467
- })
1468
-
1469
- test('Cmd/Ctrl+Click works even when allowDeselect is false', () => {
1470
- render.settings.allowDeselect = false
1471
- render.renderOptions(render.store.getDataOptions())
1472
- const opts = render.getOptions(false, false, true)
1473
-
1474
- // Select with Cmd/Ctrl
1475
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1476
- opts[1].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1477
-
1478
- // Should be able to deselect with Cmd/Ctrl even though allowDeselect is false
1479
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1480
-
1481
- expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test2' })])
1482
- })
1483
-
1484
- test('does NOT close dropdown on Cmd+Click', () => {
1485
- render.renderOptions(render.store.getDataOptions())
1486
- const opts = render.getOptions(false, false, true)
1487
-
1488
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1489
- expect(closeMock).not.toHaveBeenCalled()
1490
- })
1491
-
1492
- test('does NOT close dropdown on Ctrl+Click', () => {
1493
- render.renderOptions(render.store.getDataOptions())
1494
- const opts = render.getOptions(false, false, true)
1495
-
1496
- opts[0].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1497
- expect(closeMock).not.toHaveBeenCalled()
1498
- })
1499
- })
1500
-
1501
- describe('Shift+Click (range selection)', () => {
1502
- test('selects range from last clicked to current', () => {
1503
- render.renderOptions(render.store.getDataOptions())
1504
- const opts = render.getOptions(false, false, true)
1505
-
1506
- // Click first option
1507
- opts[0].dispatchEvent(new MouseEvent('click'))
1508
- expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
1509
-
1510
- // Shift+Click third option - should select test1, test2, test3
1511
- opts[2].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
1512
- expect(afterChangeMock).toHaveBeenCalledWith([
1513
- expect.objectContaining({ value: 'test1' }),
1514
- expect.objectContaining({ value: 'test2' }),
1515
- expect.objectContaining({ value: 'test3' })
1516
- ])
1517
- })
1518
-
1519
- test('selects range in reverse direction', () => {
1520
- render.renderOptions(render.store.getDataOptions())
1521
- const opts = render.getOptions(false, false, true)
1522
-
1523
- // Click fourth option
1524
- opts[3].dispatchEvent(new MouseEvent('click'))
1525
-
1526
- // Shift+Click second option - should select test2, test3, test4
1527
- opts[1].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
1528
- expect(afterChangeMock).toHaveBeenCalledWith([
1529
- expect.objectContaining({ value: 'test4' }),
1530
- expect.objectContaining({ value: 'test2' }),
1531
- expect.objectContaining({ value: 'test3' })
1532
- ])
1533
- })
1534
-
1535
- test('respects maxSelected limit', () => {
1536
- render.settings.maxSelected = 2
1537
- render.renderOptions(render.store.getDataOptions())
1538
- const opts = render.getOptions(false, false, true)
1539
-
1540
- // Click first option
1541
- opts[0].dispatchEvent(new MouseEvent('click'))
1542
-
1543
- // Shift+Click fourth option - would select 4 items, exceeds limit
1544
- opts[3].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
1545
-
1546
- // Should keep only the original selection
1547
- expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test1' })])
1548
- })
1549
-
1550
- test('does NOT close dropdown on Shift+Click', () => {
1551
- render.renderOptions(render.store.getDataOptions())
1552
- const opts = render.getOptions(false, false, true)
1553
-
1554
- opts[0].dispatchEvent(new MouseEvent('click'))
1555
- closeMock.mockClear()
1556
-
1557
- opts[2].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
1558
- expect(closeMock).not.toHaveBeenCalled()
1559
- })
1560
-
1561
- test('Shift+Click with search filter selects only the filtered elements', async () => {
1562
- render.settings.showSearch = true
1563
- render.store = new Store('multiple', [
1564
- {
1565
- text: 'A0',
1566
- value: 'A0'
1567
- },
1568
- {
1569
- text: 'A1',
1570
- value: 'A1'
1571
- },
1572
- {
1573
- text: 'B0',
1574
- value: 'B0'
1575
- },
1576
- {
1577
- text: 'C0',
1578
- value: 'C0'
1579
- }
1580
- ])
1581
-
1582
- render.renderValues()
1583
- render.renderOptions(render.store.getDataOptions())
1584
- let opts = render.getOptions(false, false, true)
1585
- expect(opts.length).toEqual(4)
1586
-
1587
- // Simulate search
1588
- const searchFilter = (opt: Option, search: string): boolean => {
1589
- return opt.text.toLowerCase().indexOf(search.toLowerCase()) !== -1
1590
- }
1591
- const searchResults = render.store.search('0', searchFilter)
1592
- expect(searchResults).toHaveLength(3)
1593
- render.renderOptions(searchResults)
1594
-
1595
- opts = render.getOptions(false, false, true)
1596
- expect(opts.length).toEqual(3)
1597
-
1598
- // Click first option
1599
- opts[0].dispatchEvent(new MouseEvent('click'))
1600
- expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'A0' })])
1601
-
1602
- // Shift+Click third option - should select A0, B0, C0 and should not select A1
1603
- opts[2].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
1604
- expect(afterChangeMock).toHaveBeenCalledWith([
1605
- expect.objectContaining({ value: 'A0' }),
1606
- expect.objectContaining({ value: 'B0' }),
1607
- expect.objectContaining({ value: 'C0' })
1608
- ])
1609
- })
1610
- })
1611
-
1612
- describe('Combined modifier keys', () => {
1613
- test('Cmd/Ctrl+Click then Shift+Click selects range from last click', () => {
1614
- render.renderOptions(render.store.getDataOptions())
1615
- const opts = render.getOptions(false, false, true)
1616
-
1617
- // Cmd+Click first and Ctrl+Click third options (testing both)
1618
- opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1619
- opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1620
-
1621
- // Shift+Click fifth option - should add test3, test4, test5 to selection
1622
- opts[4].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
1623
-
1624
- expect(afterChangeMock).toHaveBeenLastCalledWith([
1625
- expect.objectContaining({ value: 'test1' }),
1626
- expect.objectContaining({ value: 'test3' }),
1627
- expect.objectContaining({ value: 'test4' }),
1628
- expect.objectContaining({ value: 'test5' })
1629
- ])
1630
- })
1631
-
1632
- test('can build complex selections like native multi-select (Mac)', () => {
1633
- render.renderOptions(render.store.getDataOptions())
1634
- const opts = render.getOptions(false, false, true)
1635
-
1636
- // Select test1
1637
- opts[0].dispatchEvent(new MouseEvent('click'))
1638
- // Cmd+Add test3
1639
- opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1640
- // Cmd+Add test5
1641
- opts[4].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1642
- // Cmd+Remove test3
1643
- opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
1644
-
1645
- // Final selection should be test1 and test5
1646
- expect(afterChangeMock).toHaveBeenLastCalledWith([
1647
- expect.objectContaining({ value: 'test1' }),
1648
- expect.objectContaining({ value: 'test5' })
1649
- ])
1650
- })
1651
-
1652
- test('can build complex selections like native multi-select (Windows/Linux)', () => {
1653
- render.renderOptions(render.store.getDataOptions())
1654
- const opts = render.getOptions(false, false, true)
1655
-
1656
- // Select test1
1657
- opts[0].dispatchEvent(new MouseEvent('click'))
1658
- // Ctrl+Add test3
1659
- opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1660
- // Ctrl+Add test5
1661
- opts[4].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1662
- // Ctrl+Remove test3
1663
- opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
1664
-
1665
- // Final selection should be test1 and test5
1666
- expect(afterChangeMock).toHaveBeenLastCalledWith([
1667
- expect.objectContaining({ value: 'test1' }),
1668
- expect.objectContaining({ value: 'test5' })
1669
- ])
1670
- })
1671
- })
1672
- })
1673
-
1674
- describe('destroy', () => {
1675
- test('elements get removed', () => {
1676
- expect(render.main.main).toBeInstanceOf(HTMLDivElement)
1677
- expect(render.content.main).toBeInstanceOf(HTMLDivElement)
1678
-
1679
- // set some simple IDs, so we can search for the elements in the DOM
1680
- render.main.main.id = 'main-id'
1681
- render.content.main.id = 'content-id'
1682
-
1683
- render.destroy()
1684
-
1685
- expect(document.getElementById('main-id')).toBeNull()
1686
- expect(document.getElementById('#content-id')).toBeNull()
1687
- })
1688
- })
1689
-
1690
- describe('moveContentAbove', () => {
1691
- test('correct classes are set', () => {
1692
- render.moveContentAbove()
1693
-
1694
- expect(render.main.main.classList.contains(render.classes.dirAbove)).toBe(true)
1695
- expect(render.main.main.classList.contains(render.classes.dirBelow)).toBe(false)
1696
- expect(render.content.main.classList.contains(render.classes.dirAbove)).toBe(true)
1697
- expect(render.content.main.classList.contains(render.classes.dirBelow)).toBe(false)
1698
- })
1699
- })
1700
-
1701
- describe('moveContentBelow', () => {
1702
- test('correct classes are set', () => {
1703
- render.moveContentBelow()
1704
-
1705
- expect(render.main.main.classList.contains(render.classes.dirAbove)).toBe(false)
1706
- expect(render.main.main.classList.contains(render.classes.dirBelow)).toBe(true)
1707
- expect(render.content.main.classList.contains(render.classes.dirAbove)).toBe(false)
1708
- expect(render.content.main.classList.contains(render.classes.dirBelow)).toBe(true)
1709
- })
1710
- })
1711
-
1712
- describe('setContentPosition (via moveContentBelow)', () => {
1713
- // Helper: mock getBoundingClientRect on an element to return specific values
1714
- function mockRect(el: HTMLElement, rect: Partial<DOMRect>) {
1715
- el.getBoundingClientRect = vi.fn(
1716
- () =>
1717
- ({
1718
- top: 0,
1719
- left: 0,
1720
- right: 0,
1721
- bottom: 0,
1722
- width: 0,
1723
- height: 0,
1724
- x: 0,
1725
- y: 0,
1726
- toJSON: () => {},
1727
- ...rect
1728
- }) as DOMRect
1729
- )
1730
- }
1731
-
1732
- test('absolute (default): content aligns below trigger using document coords', () => {
1733
- // Simulate trigger at viewport (100, 50) with height 40 and width 200
1734
- mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
1735
-
1736
- // No scroll
1737
- Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
1738
- Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
1739
-
1740
- render.settings.contentPosition = 'absolute'
1741
- render.moveContentBelow()
1742
-
1743
- // top = viewportTop(100) + scrollY(0) + height(40) = 140
1744
- // left = viewportLeft(50) + scrollX(0) = 50
1745
- expect(render.content.main.style.top).toBe('140px')
1746
- expect(render.content.main.style.left).toBe('50px')
1747
- expect(render.content.main.style.width).toBe('200px')
1748
- })
1749
-
1750
- test('absolute: content accounts for page scroll', () => {
1751
- // Trigger at viewport (100, 50) but page is scrolled down 300px and right 20px
1752
- mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
1753
-
1754
- Object.defineProperty(window, 'scrollY', { value: 300, writable: true })
1755
- Object.defineProperty(window, 'scrollX', { value: 20, writable: true })
1756
-
1757
- render.settings.contentPosition = 'absolute'
1758
- render.moveContentBelow()
1759
-
1760
- // top = viewportTop(100) + scrollY(300) + height(40) = 440
1761
- // left = viewportLeft(50) + scrollX(20) = 70
1762
- expect(render.content.main.style.top).toBe('440px')
1763
- expect(render.content.main.style.left).toBe('70px')
1764
- expect(render.content.main.style.width).toBe('200px')
1765
- })
1766
-
1767
- test('absolute: content aligns correctly when trigger is near top of page', () => {
1768
- // Trigger is at very top, no scroll
1769
- mockRect(render.main.main, { top: 0, left: 10, height: 30, width: 150 })
1770
-
1771
- Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
1772
- Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
1773
-
1774
- render.settings.contentPosition = 'absolute'
1775
- render.moveContentBelow()
1776
-
1777
- // top = 0 + 0 + 30 = 30
1778
- // left = 10 + 0 = 10
1779
- expect(render.content.main.style.top).toBe('30px')
1780
- expect(render.content.main.style.left).toBe('10px')
1781
- expect(render.content.main.style.width).toBe('150px')
1782
- })
1783
-
1784
- test('fixed: content uses viewport coords without scroll offset', () => {
1785
- // Same trigger position and scroll as the absolute+scroll test
1786
- mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
1787
-
1788
- Object.defineProperty(window, 'scrollY', { value: 300, writable: true })
1789
- Object.defineProperty(window, 'scrollX', { value: 20, writable: true })
1790
-
1791
- render.settings.contentPosition = 'fixed'
1792
- render.moveContentBelow()
1793
-
1794
- // Fixed ignores scroll: top = viewportTop(100) + height(40) = 140
1795
- // left = viewportLeft(50) = 50
1796
- expect(render.content.main.style.top).toBe('140px')
1797
- expect(render.content.main.style.left).toBe('50px')
1798
- expect(render.content.main.style.width).toBe('200px')
1799
- })
1800
-
1801
- test('relative: content position styles are not set', () => {
1802
- mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
1803
-
1804
- render.settings.contentPosition = 'relative'
1805
-
1806
- // Reset styles to confirm they aren't changed
1807
- render.content.main.style.top = ''
1808
- render.content.main.style.left = ''
1809
- render.content.main.style.width = ''
1810
-
1811
- render.moveContentBelow()
1812
-
1813
- // setContentPosition returns early for relative, so no inline positioning
1814
- expect(render.content.main.style.top).toBe('')
1815
- expect(render.content.main.style.left).toBe('')
1816
- expect(render.content.main.style.width).toBe('')
1817
- })
1818
-
1819
- test('absolute vs fixed diverge only by scroll offset', () => {
1820
- const triggerRect = { top: 200, left: 80, height: 36, width: 250 }
1821
-
1822
- Object.defineProperty(window, 'scrollY', { value: 500, writable: true })
1823
- Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
1824
-
1825
- // Absolute
1826
- mockRect(render.main.main, triggerRect)
1827
- render.settings.contentPosition = 'absolute'
1828
- render.moveContentBelow()
1829
- const absTop = render.content.main.style.top
1830
- const absLeft = render.content.main.style.left
1831
-
1832
- // Fixed
1833
- mockRect(render.main.main, triggerRect)
1834
- render.settings.contentPosition = 'fixed'
1835
- render.moveContentBelow()
1836
- const fixedTop = render.content.main.style.top
1837
- const fixedLeft = render.content.main.style.left
1838
-
1839
- // Absolute top should be exactly scrollY more than fixed top
1840
- expect(parseFloat(absTop)).toBe(parseFloat(fixedTop) + 500)
1841
- // Left should differ by scrollX (which is 0 here, so equal)
1842
- expect(parseFloat(absLeft)).toBe(parseFloat(fixedLeft))
1843
- })
1844
-
1845
- describe('contentWidth setting', () => {
1846
- beforeEach(() => {
1847
- mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
1848
- Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
1849
- Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
1850
- render.settings.contentPosition = 'absolute'
1851
- })
1852
-
1853
- test('default (empty): width matches trigger', () => {
1854
- render.settings.contentWidth = ''
1855
- render.moveContentBelow()
1856
-
1857
- expect(render.content.main.style.width).toBe('200px')
1858
- expect(render.content.main.style.minWidth).toBe('')
1859
- expect(render.content.main.style.maxWidth).toBe('')
1860
- })
1861
-
1862
- test('exact value: sets width to given value', () => {
1863
- render.settings.contentWidth = '500px'
1864
- render.moveContentBelow()
1865
-
1866
- expect(render.content.main.style.width).toBe('500px')
1867
- expect(render.content.main.style.minWidth).toBe('')
1868
- expect(render.content.main.style.maxWidth).toBe('')
1869
- })
1870
-
1871
- test('> prefix: sets min-width, no fixed width', () => {
1872
- render.settings.contentWidth = '>300px'
1873
- render.moveContentBelow()
1874
-
1875
- expect(render.content.main.style.minWidth).toBe('300px')
1876
- expect(render.content.main.style.width).toBe('')
1877
- expect(render.content.main.style.maxWidth).toBe('')
1878
- })
1879
-
1880
- test('< prefix: sets max-width, no fixed width', () => {
1881
- render.settings.contentWidth = '<600px'
1882
- render.moveContentBelow()
1883
-
1884
- expect(render.content.main.style.maxWidth).toBe('600px')
1885
- expect(render.content.main.style.width).toBe('')
1886
- expect(render.content.main.style.minWidth).toBe('')
1887
- })
1888
-
1889
- test('exact value with percentage', () => {
1890
- render.settings.contentWidth = '100%'
1891
- render.moveContentBelow()
1892
-
1893
- expect(render.content.main.style.width).toBe('100%')
1894
- expect(render.content.main.style.minWidth).toBe('')
1895
- expect(render.content.main.style.maxWidth).toBe('')
1896
- })
1897
-
1898
- test('clears previous width styles when switching modes', () => {
1899
- // First set min-width
1900
- render.settings.contentWidth = '>300px'
1901
- render.moveContentBelow()
1902
- expect(render.content.main.style.minWidth).toBe('300px')
1903
-
1904
- // Now switch to exact width — min-width should be cleared
1905
- render.settings.contentWidth = '500px'
1906
- render.moveContentBelow()
1907
- expect(render.content.main.style.width).toBe('500px')
1908
- expect(render.content.main.style.minWidth).toBe('')
1909
- expect(render.content.main.style.maxWidth).toBe('')
1910
- })
1911
-
1912
- test('clears previous width styles when switching to default', () => {
1913
- // First set max-width
1914
- render.settings.contentWidth = '<600px'
1915
- render.moveContentBelow()
1916
- expect(render.content.main.style.maxWidth).toBe('600px')
1917
-
1918
- // Now switch to default — max-width should be cleared, width matches trigger
1919
- render.settings.contentWidth = ''
1920
- render.moveContentBelow()
1921
- expect(render.content.main.style.width).toBe('200px')
1922
- expect(render.content.main.style.minWidth).toBe('')
1923
- expect(render.content.main.style.maxWidth).toBe('')
1924
- })
1925
- })
1926
- })
1927
- })