slim-select 2.8.1 → 2.9.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 (69) hide show
  1. package/.prettierignore +1 -0
  2. package/.prettierrc +4 -3
  3. package/README.md +2 -0
  4. package/dist/classes.d.ts +43 -0
  5. package/dist/helpers.d.ts +1 -1
  6. package/dist/index.d.ts +5 -2
  7. package/dist/render.d.ts +4 -44
  8. package/dist/select.d.ts +4 -2
  9. package/dist/settings.d.ts +2 -1
  10. package/dist/slimselect.cjs.js +248 -120
  11. package/dist/slimselect.css +1 -1
  12. package/dist/slimselect.css.map +1 -1
  13. package/dist/slimselect.es.js +248 -120
  14. package/dist/slimselect.global.js +248 -120
  15. package/dist/slimselect.js +248 -120
  16. package/dist/slimselect.min.js +1 -1
  17. package/dist/slimselect.umd.js +248 -120
  18. package/dist/slimselect.umd.min.js +1 -1
  19. package/dist/store.d.ts +4 -2
  20. package/index.html +1 -4
  21. package/jest.config.js +6 -0
  22. package/package.json +30 -41
  23. package/src/slim-select/classes.test.ts +72 -0
  24. package/src/slim-select/classes.ts +109 -0
  25. package/src/slim-select/helpers.test.ts +160 -0
  26. package/src/slim-select/helpers.ts +3 -12
  27. package/src/slim-select/index.test.ts +109 -16
  28. package/src/slim-select/index.ts +41 -12
  29. package/src/slim-select/render.test.ts +1169 -18
  30. package/src/slim-select/render.ts +75 -96
  31. package/src/slim-select/rollup.config.mjs +28 -28
  32. package/src/slim-select/select.test.ts +306 -156
  33. package/src/slim-select/select.ts +69 -20
  34. package/src/slim-select/settings.test.ts +257 -0
  35. package/src/slim-select/settings.ts +4 -0
  36. package/src/slim-select/slimselect.scss +7 -3
  37. package/src/slim-select/store.test.ts +986 -235
  38. package/src/slim-select/store.ts +70 -28
  39. package/src/{vue/slimselect.vue → slim-select/vue.vue} +19 -17
  40. package/tsconfig.json +2 -3
  41. package/src/react/.eslintignore +0 -2
  42. package/src/react/.eslintrc.cjs +0 -24
  43. package/src/react/index.html +0 -14
  44. package/src/react/jestconfig.json +0 -11
  45. package/src/react/package.json +0 -32
  46. package/src/react/src/frameworkpage.tsx +0 -133
  47. package/src/react/src/index.ts +0 -3
  48. package/src/react/src/slimselect.tsx +0 -148
  49. package/src/react/tests/slimselect.test.tsx +0 -52
  50. package/src/react/tsconfig.json +0 -37
  51. package/src/react/tsconfig.node.json +0 -10
  52. package/src/react/vite.config.ts +0 -55
  53. package/src/vue/README.md +0 -48
  54. package/src/vue/dist/slim-select/helpers.d.ts +0 -5
  55. package/src/vue/dist/slim-select/index.d.ts +0 -46
  56. package/src/vue/dist/slim-select/render.d.ts +0 -120
  57. package/src/vue/dist/slim-select/select.d.ts +0 -28
  58. package/src/vue/dist/slim-select/settings.d.ts +0 -34
  59. package/src/vue/dist/slim-select/store.d.ts +0 -80
  60. package/src/vue/dist/slimselectvue.es.js +0 -1978
  61. package/src/vue/dist/slimselectvue.global.js +0 -1981
  62. package/src/vue/dist/slimselectvue.ssr.js +0 -1980
  63. package/src/vue/dist/vue/hello.d.ts +0 -2
  64. package/src/vue/dist/vue/slimselect.vue.d.ts +0 -326
  65. package/src/vue/hello.ts +0 -5
  66. package/src/vue/package.json +0 -28
  67. package/src/vue/rollup.config.mjs +0 -44
  68. package/src/vue/tsconfig.json +0 -14
  69. /package/{vite.config.ts → vite.config.mts} +0 -0
@@ -7,33 +7,1184 @@
7
7
  import { describe, expect, test } from '@jest/globals'
8
8
  import Render, { Callbacks } from './render'
9
9
  import Settings from './settings'
10
- import Store, { DataArray, Option } from './store'
10
+ import Store, { Option } from './store'
11
+ import CssClasses from './classes'
11
12
 
12
- describe('select module', () => {
13
- test('constructor', () => {
14
- // create a new store with 2 options
13
+ describe('render module', () => {
14
+ let render: Render
15
+ let openMock: jest.Mock
16
+ let closeMock: jest.Mock
17
+ let addSelectedMock: jest.Mock
18
+ let setSelectedMock: jest.Mock
19
+ let addOptionMock: jest.Mock
20
+ let searchMock: jest.Mock
21
+ let afterChangeMock: jest.Mock
22
+ let beforeChangeMock: jest.Mock
23
+
24
+ beforeEach(() => {
15
25
  const store = new Store('single', [
16
- { text: 'test1', value: 'test1' },
17
- { text: 'test2', value: 'test2' },
26
+ {
27
+ text: 'test0',
28
+ value: 'test0'
29
+ },
30
+ {
31
+ text: 'test1',
32
+ value: 'test1',
33
+ html: '<span>test1</span>'
34
+ },
35
+ {
36
+ text: 'test2',
37
+ selected: true
38
+ }
18
39
  ])
19
40
 
20
41
  // default settings
21
42
  const settings = new Settings()
43
+ const classes = new CssClasses()
44
+
45
+ openMock = jest.fn(() => {})
46
+ closeMock = jest.fn(() => {})
47
+ addSelectedMock = jest.fn(() => {})
48
+ setSelectedMock = jest.fn(() => {})
49
+ addOptionMock = jest.fn(() => {})
50
+ searchMock = jest.fn(() => {})
51
+ afterChangeMock = jest.fn(() => {})
52
+ beforeChangeMock = jest.fn(() => true)
22
53
 
23
54
  // default callbacks
24
55
  const callbacks = {
25
- open: () => {},
26
- close: () => {},
27
- addSelected: (value: string) => {},
28
- setSelected: (value: string[]) => {},
29
- addOption: (option: Option) => {},
30
- search: (search: string) => {},
31
- beforeChange: (before: DataArray, after: DataArray) => {
32
- return true
33
- },
34
- } as Callbacks
56
+ open: openMock,
57
+ close: closeMock,
58
+ addSelected: addSelectedMock,
59
+ setSelected: setSelectedMock,
60
+ addOption: addOptionMock,
61
+ search: searchMock,
62
+ afterChange: afterChangeMock,
63
+ beforeChange: beforeChangeMock
64
+ }
65
+
66
+ render = new Render(settings, classes, store, callbacks)
67
+ })
68
+
69
+ describe('constructor', () => {
70
+ test('default constructor works', () => {
71
+ // create a new store with 2 options
72
+ const store = new Store('single', [
73
+ { text: 'test1', value: 'test1' },
74
+ { text: 'test2', value: 'test2' }
75
+ ])
76
+
77
+ // default settings
78
+ const settings = new Settings()
79
+ const classes = new CssClasses()
80
+
81
+ // default callbacks
82
+ const callbacks = {
83
+ open: () => {},
84
+ close: () => {},
85
+ addSelected: () => {},
86
+ setSelected: () => {},
87
+ addOption: () => {},
88
+ search: () => {},
89
+ beforeChange: () => {
90
+ return true
91
+ }
92
+ } as Callbacks
93
+
94
+ const render = new Render(settings, classes, store, callbacks)
95
+ expect(render).toBeInstanceOf(Render)
96
+ expect(render.main.main).toBeInstanceOf(HTMLDivElement)
97
+ expect(render.content.search.input).toBeInstanceOf(HTMLInputElement)
98
+ })
99
+ })
100
+
101
+ describe('enable', () => {
102
+ test('enable removes disabled class from main and enables search input', () => {
103
+ // disable stuff directly
104
+ render.main.main.classList.add(render.classes.disabled)
105
+ render.content.search.input.disabled = true
106
+
107
+ render.enable()
108
+ expect(render.main.main.classList.contains(render.classes.disabled)).toBe(false)
109
+ expect(render.content.search.input.disabled).toBe(false)
110
+ })
111
+ })
112
+
113
+ describe('disable', () => {
114
+ test('disable adds disabled class to main and disables search input', () => {
115
+ render.disable()
116
+ expect(render.main.main.classList.contains(render.classes.disabled)).toBe(true)
117
+ expect(render.content.search.input.disabled).toBe(true)
118
+ })
119
+ })
120
+
121
+ describe('open', () => {
122
+ test('open sets the correct attributes and CSS classes', () => {
123
+ render.open()
124
+
125
+ expect(render.main.arrow.path.getAttribute('d')).toBe(render.classes.arrowOpen)
126
+ expect(render.main.main.classList.contains(render.classes.openBelow)).toBe(true)
127
+ expect(render.main.main.classList.contains(render.classes.openAbove)).toBe(false)
128
+ expect(render.main.main.getAttribute('aria-expanded')).toBe('true')
129
+ expect(render.content.main.classList.contains(render.classes.openBelow)).toBe(true)
130
+ expect(render.content.main.classList.contains(render.classes.openAbove)).toBe(false)
131
+ })
132
+ })
133
+
134
+ describe('close', () => {
135
+ test('close sets the correct attributes and CSS classes', () => {
136
+ render.close()
137
+
138
+ expect(render.main.arrow.path.getAttribute('d')).toBe(render.classes.arrowClose)
139
+ expect(render.main.main.classList.contains(render.classes.openBelow)).toBe(false)
140
+ expect(render.main.main.classList.contains(render.classes.openAbove)).toBe(false)
141
+ expect(render.main.main.getAttribute('aria-expanded')).toBe('false')
142
+ expect(render.content.main.classList.contains(render.classes.openBelow)).toBe(false)
143
+ expect(render.content.main.classList.contains(render.classes.openAbove)).toBe(false)
144
+ })
145
+ })
146
+
147
+ describe('updateClassStyles', () => {
148
+ test('existing classes and styles are cleared', () => {
149
+ // manually set classes and styles for testing
150
+ render.main.main.className = 'test'
151
+ render.main.main.style.color = 'red'
152
+
153
+ render.content.main.className = 'test'
154
+ render.content.main.style.color = 'red'
155
+
156
+ render.updateClassStyles()
157
+
158
+ expect(render.main.main.classList.contains('test')).toBe(false)
159
+ expect(render.main.main.style.color).toBeFalsy()
160
+
161
+ expect(render.content.main.classList.contains('test')).toBe(false)
162
+ expect(render.content.main.style.color).toBeFalsy()
163
+ })
164
+
165
+ test('inline styles are applied to main elements', () => {
166
+ render.settings.style = 'color: red'
167
+
168
+ render.updateClassStyles()
169
+
170
+ expect(render.main.main.style.color).toBe('red')
171
+ expect(render.content.main.style.color).toBe('red')
172
+ })
173
+
174
+ test('classes are applied to main elements', () => {
175
+ render.settings.class = ['test0', 'test1', 'test2']
176
+
177
+ render.updateClassStyles()
178
+
179
+ expect(render.main.main.classList.contains('test0')).toBe(true)
180
+ expect(render.main.main.classList.contains('test1')).toBe(true)
181
+ expect(render.main.main.classList.contains('test2')).toBe(true)
182
+
183
+ expect(render.content.main.classList.contains('test0')).toBe(true)
184
+ expect(render.content.main.classList.contains('test1')).toBe(true)
185
+ expect(render.content.main.classList.contains('test2')).toBe(true)
186
+ })
187
+
188
+ test('if content position is relative, class is added on content', () => {
189
+ render.settings.contentPosition = 'relative'
190
+
191
+ render.updateClassStyles()
192
+
193
+ expect(render.content.main.classList.contains('ss-relative')).toBe(true)
194
+ })
195
+ })
196
+
197
+ describe('updateAriaAttributes', () => {
198
+ test('sets correct aria attributes', () => {
199
+ render.updateAriaAttributes()
200
+
201
+ expect(render.main.main.role).toBe('combobox')
202
+ expect(render.main.main.getAttribute('aria-haspopup')).toBe('listbox')
203
+ expect(render.main.main.getAttribute('aria-controls')).toBe(render.content.main.id)
204
+ expect(render.main.main.getAttribute('aria-expanded')).toBe('false')
205
+ expect(render.content.main.getAttribute('role')).toBe('listbox')
206
+ })
207
+ })
208
+
209
+ describe('mainDiv', () => {
210
+ test('correct HTML element gets created', () => {
211
+ const main = render.main.main
212
+
213
+ expect(main.dataset.id).toBe(render.settings.id)
214
+ expect(main.getAttribute('aria-label')).toBe(render.settings.ariaLabel)
215
+ expect(main.tabIndex).toBe(0)
216
+ expect(main.children).toHaveLength(3)
217
+ expect(main.children.item(0)?.className).toBe(render.classes.values)
218
+ expect(main.children.item(1)?.classList.contains(render.classes.deselect)).toBe(true)
219
+ expect(main.children.item(1)?.classList.contains(render.classes.hide)).toBe(true)
220
+ expect(main.children.item(1)?.children).toHaveLength(1)
221
+ expect(main.children.item(1)?.children).toHaveLength(1)
222
+ expect(main.children.item(1)?.children.item(0)).toBeInstanceOf(SVGElement)
223
+ expect(main.children.item(2)?.classList.contains(render.classes.arrow)).toBe(true)
224
+ expect(main.children.item(2)?.classList.contains(render.classes.hide)).toBe(false)
225
+ expect(main.children.item(2)?.children.item(0)).toBeInstanceOf(SVGElement)
226
+ })
227
+
228
+ test('arrow is hidden when alwaysOpen is set', () => {
229
+ render.settings.alwaysOpen = true
230
+ const main = render.mainDiv().main
231
+
232
+ expect(main.children.item(2)?.classList.contains(render.classes.arrow)).toBe(true)
233
+ expect(main.children.item(2)?.classList.contains(render.classes.hide)).toBe(true)
234
+ expect(main.children.item(2)?.children.item(0)).toBeInstanceOf(SVGElement)
235
+ })
236
+
237
+ test('arrow key events on main element move highlight', () => {
238
+ const highlightMock = jest.fn(() => {})
239
+
240
+ render.highlight = highlightMock
241
+
242
+ render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
243
+ expect(openMock).toHaveBeenCalled()
244
+ expect(highlightMock).toHaveBeenCalledTimes(1)
245
+ expect(highlightMock.mock.calls[0]).toStrictEqual(['up'])
246
+
247
+ render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
248
+ expect(openMock).toHaveBeenCalledTimes(2)
249
+ expect(highlightMock).toHaveBeenCalledTimes(2)
250
+ expect(highlightMock.mock.calls[1]).toStrictEqual(['down'])
251
+ })
252
+
253
+ test('tab and escape key event on main element triggers close callback', () => {
254
+ render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
255
+ expect(closeMock).toHaveBeenCalled()
256
+ render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
257
+ expect(closeMock).toHaveBeenCalledTimes(2)
258
+ })
259
+
260
+ test('enter and space key event on main element triggers open callback', () => {
261
+ render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
262
+ expect(openMock).toHaveBeenCalled()
263
+ render.main.main.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))
264
+ expect(openMock).toHaveBeenCalledTimes(2)
265
+ })
266
+
267
+ test('click on main event does nothing if element is disabled', () => {
268
+ render.settings.disabled = true
269
+
270
+ render.main.main.dispatchEvent(new MouseEvent('click'))
271
+
272
+ expect(openMock).not.toHaveBeenCalled()
273
+ expect(closeMock).not.toHaveBeenCalled()
274
+ })
275
+
276
+ test('click on main event triggers open if element is closed', () => {
277
+ render.main.main.dispatchEvent(new MouseEvent('click'))
278
+
279
+ expect(openMock).toHaveBeenCalled()
280
+ expect(closeMock).not.toHaveBeenCalled()
281
+ })
282
+
283
+ test('click on main event triggers close if element is opened', () => {
284
+ render.settings.isOpen = true
285
+
286
+ render.main.main.dispatchEvent(new MouseEvent('click'))
287
+
288
+ expect(openMock).not.toHaveBeenCalled()
289
+ expect(closeMock).toHaveBeenCalled()
290
+ })
291
+
292
+ test('click on deselect does nothing if element is disabled', () => {
293
+ render.settings.disabled = true
294
+
295
+ const deselectElement: HTMLDivElement = render.main.main.querySelector(
296
+ '.' + render.classes.deselect
297
+ ) as HTMLDivElement
298
+
299
+ deselectElement.dispatchEvent(new MouseEvent('click'))
35
300
 
36
- const render = new Render(settings, store, callbacks)
37
- expect(render).toBeInstanceOf(Render)
301
+ expect(afterChangeMock).not.toHaveBeenCalled()
302
+ })
303
+
304
+ test('click on deselect on single select runs callbacks', () => {
305
+ const deselectElement: HTMLDivElement = render.main.main.querySelector(
306
+ '.' + render.classes.deselect
307
+ ) as HTMLDivElement
308
+
309
+ deselectElement.dispatchEvent(new MouseEvent('click'))
310
+
311
+ expect(setSelectedMock).toHaveBeenCalled()
312
+ expect(closeMock).toHaveBeenCalled()
313
+ expect(afterChangeMock).toHaveBeenCalled()
314
+ })
315
+
316
+ test('click on deselect on multiple select runs callbacks', () => {
317
+ render.settings.isMultiple = true
318
+
319
+ const deselectAllMock = jest.fn()
320
+
321
+ render.updateDeselectAll = deselectAllMock
322
+
323
+ const deselectElement: HTMLDivElement = render.main.main.querySelector(
324
+ '.' + render.classes.deselect
325
+ ) as HTMLDivElement
326
+ deselectElement.dispatchEvent(new MouseEvent('click'))
327
+
328
+ expect(deselectAllMock).toHaveBeenCalled()
329
+ expect(setSelectedMock).toHaveBeenCalled()
330
+ expect(closeMock).toHaveBeenCalled()
331
+ expect(afterChangeMock).toHaveBeenCalled()
332
+ })
333
+ })
334
+
335
+ describe('mainFocus', () => {
336
+ let focusMock: jest.Mock
337
+
338
+ beforeEach(() => {
339
+ focusMock = jest.fn(() => {})
340
+ render.main.main.focus = focusMock
341
+ })
342
+
343
+ test('mainFocus does nothing if the event is click', () => {
344
+ render.mainFocus('click')
345
+ expect(focusMock).not.toHaveBeenCalled()
346
+ })
347
+
348
+ test('mainFocus triggers focus if the event is not click', () => {
349
+ render.mainFocus('keydown')
350
+ expect(focusMock).toHaveBeenCalled()
351
+ render.mainFocus('keyup')
352
+ expect(focusMock).toHaveBeenCalledTimes(2)
353
+ render.mainFocus('mouse')
354
+ expect(focusMock).toHaveBeenCalledTimes(3)
355
+ })
356
+ })
357
+
358
+ describe('placeholder', () => {
359
+ test('placeholder uses fallback text if no option is found', () => {
360
+ render.settings.placeholderText = 'placeholder text'
361
+ const placeholder = render.placeholder()
362
+
363
+ expect(placeholder.innerHTML).toBe(render.settings.placeholderText)
364
+ })
365
+
366
+ test('placeholder uses option html if option is found', () => {
367
+ render.store.setData([
368
+ {
369
+ text: 'opt text',
370
+ html: '<h1>Option HTML</h1>',
371
+ placeholder: true
372
+ }
373
+ ])
374
+ const placeholder = render.placeholder()
375
+
376
+ expect(placeholder.innerHTML).toBe('<h1>Option HTML</h1>')
377
+ })
378
+
379
+ test('placeholder uses option text if option is found and no HTML is set', () => {
380
+ render.store.setData([
381
+ {
382
+ text: 'opt text',
383
+ placeholder: true
384
+ }
385
+ ])
386
+ const placeholder = render.placeholder()
387
+
388
+ expect(placeholder.innerHTML).toBe('opt text')
389
+ })
38
390
  })
391
+
392
+ describe('renderValues', () => {
393
+ test('single select renders only one value', () => {
394
+ render.renderValues()
395
+
396
+ expect(render.main.values.children).toHaveLength(1)
397
+ })
398
+
399
+ test('single select renders HTML option', () => {
400
+ render.store.setData([
401
+ {
402
+ text: 'opt0'
403
+ },
404
+ {
405
+ text: 'opt1',
406
+ html: '<span>opt1</span>',
407
+ selected: true
408
+ }
409
+ ])
410
+ render.renderValues()
411
+
412
+ expect(render.main.values.children).toHaveLength(1)
413
+ expect(render.main.values.children.item(0)?.innerHTML).toBe('<span>opt1</span>')
414
+ })
415
+
416
+ test('multiple select renders all selected values', () => {
417
+ render.settings.isMultiple = true
418
+ render.store = new Store('multiple', [
419
+ {
420
+ text: 'opt0',
421
+ value: 'opt0',
422
+ selected: true
423
+ },
424
+ {
425
+ text: 'opt1',
426
+ value: 'opt1',
427
+ html: '<span>opt1</span>',
428
+ selected: true
429
+ },
430
+ {
431
+ text: 'opt2'
432
+ }
433
+ ])
434
+
435
+ render.renderValues()
436
+
437
+ expect(render.main.values.children).toHaveLength(2)
438
+ expect(render.main.values.children.item(0)).toBeInstanceOf(HTMLDivElement)
439
+ expect((render.main.values.children.item(0) as HTMLDivElement).textContent).toBe('opt0')
440
+ expect(render.main.values.children.item(1)).toBeInstanceOf(HTMLDivElement)
441
+ expect((render.main.values.children.item(1) as HTMLDivElement).textContent).toBe('opt1')
442
+ })
443
+
444
+ test('multiple select renders counter element when maxValuesShown is set', () => {
445
+ render.settings.isMultiple = true
446
+ render.settings.maxValuesShown = 2
447
+ render.store = new Store('multiple', [
448
+ {
449
+ text: 'opt0',
450
+ value: 'opt0',
451
+ selected: true
452
+ },
453
+ {
454
+ text: 'opt1',
455
+ value: 'opt1',
456
+ html: '<span>opt1</span>',
457
+ selected: true
458
+ },
459
+ {
460
+ text: 'opt2',
461
+ value: 'opt2',
462
+ selected: true
463
+ },
464
+ {
465
+ text: 'opt4'
466
+ }
467
+ ])
468
+
469
+ render.renderValues()
470
+
471
+ expect(render.main.values.children).toHaveLength(1)
472
+ expect(render.main.values.children.item(0)?.innerHTML).toBe('3 selected')
473
+ })
474
+
475
+ test('remove old options from values', () => {
476
+ render.renderValues()
477
+ expect(render.main.values.children).toHaveLength(1)
478
+ expect(render.main.values.innerText).toBeFalsy()
479
+
480
+ render.store.setSelectedBy('value', ['test1'])
481
+ render.renderValues()
482
+
483
+ expect(render.main.values.children).toHaveLength(1)
484
+ expect(render.main.values.children.item(0)?.innerHTML).toBe('<span>test1</span>')
485
+ })
486
+ })
487
+
488
+ describe('multipleValue', () => {})
489
+
490
+ describe('contentDiv', () => {})
491
+
492
+ describe('moveContent', () => {
493
+ let contentAboveMock: jest.Mock
494
+ let contentBelowMock: jest.Mock
495
+
496
+ beforeEach(() => {
497
+ contentAboveMock = jest.fn()
498
+ contentBelowMock = jest.fn()
499
+
500
+ render.moveContentAbove = contentAboveMock
501
+ render.moveContentBelow = contentBelowMock
502
+ })
503
+
504
+ test('content is moved below when position is relative', () => {
505
+ render.settings.contentPosition = 'relative'
506
+
507
+ render.moveContent()
508
+ expect(contentAboveMock).not.toHaveBeenCalled()
509
+ expect(contentBelowMock).toHaveBeenCalled()
510
+ })
511
+
512
+ test('content is moved below when open position is down', () => {
513
+ render.settings.openPosition = 'down'
514
+
515
+ render.moveContent()
516
+ expect(contentAboveMock).not.toHaveBeenCalled()
517
+ expect(contentBelowMock).toHaveBeenCalled()
518
+ })
519
+
520
+ test('content is moved above when open position is up', () => {
521
+ render.settings.openPosition = 'up'
522
+
523
+ render.moveContent()
524
+ expect(contentAboveMock).toHaveBeenCalled()
525
+ expect(contentBelowMock).not.toHaveBeenCalled()
526
+ })
527
+
528
+ test('content is moved above when putContent is up', () => {
529
+ render.putContent = jest.fn(() => 'up')
530
+
531
+ render.moveContent()
532
+ expect(contentAboveMock).toHaveBeenCalled()
533
+ expect(contentBelowMock).not.toHaveBeenCalled()
534
+ })
535
+
536
+ test('content is moved below when putContent is down', () => {
537
+ render.putContent = jest.fn(() => 'down')
538
+
539
+ render.moveContent()
540
+ expect(contentAboveMock).not.toHaveBeenCalled()
541
+ expect(contentBelowMock).toHaveBeenCalled()
542
+ })
543
+ })
544
+
545
+ describe('searchDiv', () => {
546
+ test('search is hidden when showSearch setting is false', () => {
547
+ render.settings.showSearch = false
548
+
549
+ const search = render.searchDiv()
550
+
551
+ expect(search.main.classList.contains(render.classes.hide)).toBe(true)
552
+ })
553
+
554
+ test('input is debounced', async () => {
555
+ const search = render.searchDiv()
556
+
557
+ search.input.dispatchEvent(new InputEvent('input', { data: 'asdf' }))
558
+
559
+ // wait for debounce to trigger
560
+ await new Promise((r) => setTimeout(r, 101))
561
+
562
+ expect(searchMock).toHaveBeenCalled()
563
+ })
564
+
565
+ test('arrow keys move highlight', () => {
566
+ const search = render.searchDiv()
567
+ const highlightMock = jest.fn(() => {})
568
+
569
+ render.highlight = highlightMock
570
+
571
+ search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }))
572
+ expect(highlightMock).toHaveBeenCalledTimes(1)
573
+ expect(highlightMock.mock.calls[0]).toStrictEqual(['up'])
574
+
575
+ search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
576
+ expect(highlightMock).toHaveBeenCalledTimes(2)
577
+ expect(highlightMock.mock.calls[1]).toStrictEqual(['down'])
578
+ })
579
+
580
+ test('tab triggers close callback', () => {
581
+ const search = render.searchDiv()
582
+
583
+ search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
584
+
585
+ expect(closeMock).toHaveBeenCalled()
586
+ })
587
+
588
+ test('escape triggers close callback', () => {
589
+ // separate test in case we want to also test the event someday
590
+ const search = render.searchDiv()
591
+
592
+ search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
593
+
594
+ expect(closeMock).toHaveBeenCalled()
595
+ })
596
+
597
+ test("enter and space don't call addable witout ctrl key", () => {
598
+ const search = render.searchDiv()
599
+ const addableMock = jest.fn((s: string) => ({
600
+ text: s,
601
+ value: s.toLowerCase()
602
+ }))
603
+
604
+ render.callbacks.addable = addableMock
605
+
606
+ search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
607
+ expect(addableMock).not.toHaveBeenCalled()
608
+
609
+ search.input.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))
610
+ expect(addableMock).not.toHaveBeenCalled()
611
+ })
612
+
613
+ test('enter and space call event and does nothing without input value', () => {
614
+ const addableMock = jest.fn((s: string) => ({
615
+ text: s,
616
+ value: s.toLowerCase()
617
+ }))
618
+
619
+ render.callbacks.addable = addableMock
620
+
621
+ // recreate search because we have added the addable callback
622
+ render.content.search = render.searchDiv()
623
+
624
+ render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
625
+ expect(addableMock).not.toHaveBeenCalled()
626
+ })
627
+
628
+ test('enter and space call addable when defined', () => {
629
+ const addableMock = jest.fn((s: string) => ({
630
+ text: s,
631
+ value: s.toLowerCase()
632
+ }))
633
+
634
+ render.callbacks.addable = addableMock
635
+
636
+ // recreate search because we have added the addable callback
637
+ render.content.search = render.searchDiv()
638
+
639
+ render.content.search.input.value = 'Search'
640
+
641
+ render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
642
+ expect(addableMock).toHaveBeenCalledTimes(1)
643
+ expect(addableMock.mock.calls[0]).toStrictEqual(['Search'])
644
+ })
645
+ })
646
+
647
+ describe('searchFocus', () => {
648
+ test('search is focused', () => {
649
+ expect(document.activeElement).not.toBe(render.content.search.input)
650
+
651
+ render.searchFocus()
652
+ expect(document.activeElement).toBe(render.content.search.input)
653
+ })
654
+ })
655
+
656
+ describe('getOptions', () => {
657
+ test('returns all options when called without parameters', () => {
658
+ // render all 3 default options and get them back
659
+ render.renderOptions(render.store.getDataOptions())
660
+ const opts = render.getOptions()
661
+
662
+ expect(opts).toHaveLength(3)
663
+ })
664
+
665
+ test('filters correctly when filtering out placeholders', () => {
666
+ render.renderOptions(
667
+ render.store.partialToFullData([
668
+ {
669
+ text: 'opt0',
670
+ placeholder: true
671
+ },
672
+ {
673
+ text: 'opt1'
674
+ },
675
+ {
676
+ text: 'opt2'
677
+ }
678
+ ])
679
+ )
680
+
681
+ const opts = render.getOptions(true, false, true)
682
+ expect(opts).toHaveLength(2)
683
+ })
684
+
685
+ test('filters correctly when filtering out disabled options', () => {
686
+ render.renderOptions(
687
+ render.store.partialToFullData([
688
+ {
689
+ text: 'opt0',
690
+ disabled: true
691
+ },
692
+ {
693
+ text: 'opt1'
694
+ },
695
+ {
696
+ text: 'opt2'
697
+ }
698
+ ])
699
+ )
700
+
701
+ const opts = render.getOptions(false, true)
702
+ expect(opts).toHaveLength(2)
703
+ })
704
+
705
+ test('filters correctly when filtering out hidden options', () => {
706
+ render.renderOptions(
707
+ render.store.partialToFullData([
708
+ {
709
+ text: 'opt0',
710
+ placeholder: true
711
+ },
712
+ {
713
+ text: 'opt1'
714
+ },
715
+ {
716
+ text: 'opt2'
717
+ }
718
+ ])
719
+ )
720
+
721
+ const opts = render.getOptions(false, false, true)
722
+ expect(opts).toHaveLength(2)
723
+ })
724
+
725
+ test('filters correctly when filtering out hidden and disabled options', () => {
726
+ render.renderOptions(
727
+ render.store.partialToFullData([
728
+ {
729
+ text: 'opt0',
730
+ disabled: true
731
+ },
732
+ {
733
+ text: 'opt1',
734
+ placeholder: true
735
+ },
736
+ {
737
+ text: 'opt2'
738
+ }
739
+ ])
740
+ )
741
+
742
+ const opts = render.getOptions(false, true, true)
743
+ expect(opts).toHaveLength(1)
744
+ })
745
+ })
746
+
747
+ describe('highlight', () => {
748
+ test('simply do nothing without breaking when options are empty', () => {
749
+ render.renderOptions([])
750
+
751
+ expect(() => render.highlight('up')).not.toThrowError()
752
+ })
753
+
754
+ test('highlight single option that is not already highlighted', () => {
755
+ render.renderOptions(
756
+ render.store.partialToFullData([
757
+ {
758
+ text: 'opt0'
759
+ }
760
+ ])
761
+ )
762
+
763
+ render.highlight('up')
764
+
765
+ expect(render.getOptions()[0].classList.contains(render.classes.highlighted)).toBe(true)
766
+ })
767
+
768
+ test('select first option with down when no option is highlighted or selected', () => {
769
+ render.renderOptions(
770
+ render.store.partialToFullData([
771
+ {
772
+ text: 'opt0'
773
+ },
774
+ {
775
+ text: 'opt1'
776
+ },
777
+ {
778
+ text: 'opt2'
779
+ }
780
+ ])
781
+ )
782
+
783
+ render.highlight('down')
784
+
785
+ expect(render.getOptions()[0].classList.contains(render.classes.highlighted)).toBe(true)
786
+ })
787
+
788
+ test('select last option with up when no option is highlighted or selected', () => {
789
+ render.renderOptions(
790
+ render.store.partialToFullData([
791
+ {
792
+ text: 'opt0'
793
+ },
794
+ {
795
+ text: 'opt1'
796
+ },
797
+ {
798
+ text: 'opt2'
799
+ }
800
+ ])
801
+ )
802
+
803
+ render.highlight('up')
804
+
805
+ expect(render.getOptions()[2].classList.contains(render.classes.highlighted)).toBe(true)
806
+ })
807
+
808
+ test('highlight next option on down after highlighted option', () => {
809
+ render.renderOptions(
810
+ render.store.partialToFullData([
811
+ {
812
+ text: 'opt0',
813
+ class: render.classes.highlighted
814
+ },
815
+ {
816
+ text: 'opt1'
817
+ },
818
+ {
819
+ text: 'opt2'
820
+ }
821
+ ])
822
+ )
823
+
824
+ render.highlight('down')
825
+
826
+ expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
827
+ })
828
+
829
+ test('highlight previous option on up before highlighted option', () => {
830
+ render.renderOptions(
831
+ render.store.partialToFullData([
832
+ {
833
+ text: 'opt0'
834
+ },
835
+ {
836
+ text: 'opt1'
837
+ },
838
+ {
839
+ text: 'opt2',
840
+ class: render.classes.highlighted
841
+ }
842
+ ])
843
+ )
844
+
845
+ render.highlight('up')
846
+
847
+ expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
848
+ })
849
+
850
+ test('highlight next option on down after selected option when no options is highlighted', () => {
851
+ render.renderOptions(
852
+ render.store.partialToFullData([
853
+ {
854
+ text: 'opt0',
855
+ selected: true
856
+ },
857
+ {
858
+ text: 'opt1'
859
+ },
860
+ {
861
+ text: 'opt2'
862
+ }
863
+ ])
864
+ )
865
+
866
+ render.highlight('down')
867
+
868
+ expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
869
+ })
870
+
871
+ test('skip to last option when using up at the first option', () => {
872
+ render.renderOptions(
873
+ render.store.partialToFullData([
874
+ {
875
+ text: 'opt0',
876
+ selected: true
877
+ },
878
+ {
879
+ text: 'opt1'
880
+ },
881
+ {
882
+ text: 'opt2'
883
+ }
884
+ ])
885
+ )
886
+
887
+ render.highlight('up')
888
+
889
+ expect(render.getOptions()[2].classList.contains(render.classes.highlighted)).toBe(true)
890
+ })
891
+
892
+ test('highlight next option within opt group on down', () => {
893
+ render.renderOptions(
894
+ render.store.partialToFullData([
895
+ {
896
+ text: 'opt0',
897
+ selected: true
898
+ },
899
+ {
900
+ label: 'opt group',
901
+ options: [
902
+ {
903
+ text: 'opt1'
904
+ }
905
+ ]
906
+ },
907
+ {
908
+ text: 'opt2'
909
+ }
910
+ ])
911
+ )
912
+
913
+ render.highlight('down')
914
+
915
+ expect(render.getOptions()[1].classList.contains(render.classes.highlighted)).toBe(true)
916
+ })
917
+
918
+ test('highlight previous option within opt group on up', () => {
919
+ render.renderOptions(
920
+ render.store.partialToFullData([
921
+ {
922
+ text: 'opt0'
923
+ },
924
+ {
925
+ label: 'opt group',
926
+ options: [
927
+ {
928
+ text: 'opt1',
929
+ selected: true
930
+ }
931
+ ]
932
+ },
933
+ {
934
+ text: 'opt2'
935
+ }
936
+ ])
937
+ )
938
+
939
+ render.highlight('up')
940
+
941
+ expect(render.getOptions()[0].classList.contains(render.classes.highlighted)).toBe(true)
942
+ })
943
+ })
944
+
945
+ describe('listDiv', () => {
946
+ test('list div has correct class', () => {
947
+ const list = render.listDiv()
948
+
949
+ expect(list.classList.contains(render.classes.list)).toBe(true)
950
+ })
951
+ })
952
+
953
+ describe('renderError', () => {
954
+ test('error message is rendered correctly', () => {
955
+ expect(render.content.list.children).toHaveLength(0)
956
+
957
+ render.renderError('test error')
958
+
959
+ expect(render.content.list.children).toHaveLength(1)
960
+ expect(render.content.list.children.item(0)).toBeInstanceOf(HTMLDivElement)
961
+ expect(render.content.list.children.item(0)?.className).toBe(render.classes.error)
962
+ expect(render.content.list.children.item(0)?.textContent).toBe('test error')
963
+ })
964
+
965
+ test('list is reset on new error', () => {
966
+ expect(render.content.list.children).toHaveLength(0)
967
+
968
+ render.renderError('test error')
969
+ expect(render.content.list.children).toHaveLength(1)
970
+
971
+ render.renderError('error 2')
972
+ expect(render.content.list.children).toHaveLength(1)
973
+ expect(render.content.list.children.item(0)?.textContent).toBe('error 2')
974
+ })
975
+ })
976
+
977
+ describe('renderSearching', () => {
978
+ test('search text is rendered correctly', () => {
979
+ expect(render.content.list.children).toHaveLength(0)
980
+
981
+ render.settings.searchingText = 'search'
982
+ render.renderSearching()
983
+
984
+ expect(render.content.list.children).toHaveLength(1)
985
+ expect(render.content.list.children.item(0)).toBeInstanceOf(HTMLDivElement)
986
+ expect(render.content.list.children.item(0)?.className).toBe(render.classes.searching)
987
+ expect(render.content.list.children.item(0)?.textContent).toBe('search')
988
+ })
989
+
990
+ test('list is reset on new search text', () => {
991
+ expect(render.content.list.children).toHaveLength(0)
992
+
993
+ render.settings.searchingText = 'search'
994
+ render.renderSearching()
995
+ expect(render.content.list.children).toHaveLength(1)
996
+
997
+ render.settings.searchingText = 'search 2'
998
+ render.renderSearching()
999
+ expect(render.content.list.children).toHaveLength(1)
1000
+ expect(render.content.list.children.item(0)?.textContent).toBe('search 2')
1001
+ })
1002
+ })
1003
+
1004
+ describe('renderOptions', () => {})
1005
+
1006
+ describe('option', () => {
1007
+ test('add inline styles correctly', () => {
1008
+ const option = render.option(
1009
+ new Option({
1010
+ text: 'opt',
1011
+ style: 'color: red'
1012
+ })
1013
+ )
1014
+
1015
+ expect(option.style.color).toBe('red')
1016
+ })
1017
+
1018
+ test('add hidden class on option with display false', () => {
1019
+ const option = render.option(
1020
+ new Option({
1021
+ text: 'opt',
1022
+ display: false
1023
+ })
1024
+ )
1025
+
1026
+ expect(option.classList.contains(render.classes.hide)).toBe(true)
1027
+ })
1028
+
1029
+ test('add hidden class on selected option when hideSelected setting is true', () => {
1030
+ render.settings.hideSelected = true
1031
+
1032
+ const option = render.option(
1033
+ new Option({
1034
+ text: 'opt',
1035
+ selected: true
1036
+ })
1037
+ )
1038
+
1039
+ expect(option.classList.contains(render.classes.hide)).toBe(true)
1040
+ })
1041
+
1042
+ test('title attribute is set when showOptionTooltips setting is true', () => {
1043
+ render.settings.showOptionTooltips = true
1044
+
1045
+ const option = render.option(
1046
+ new Option({
1047
+ text: 'opt'
1048
+ })
1049
+ )
1050
+
1051
+ expect(option.getAttribute('title')).toBe('opt')
1052
+ })
1053
+
1054
+ test('text is highlighted correctly with option text', () => {
1055
+ render.settings.searchHighlight = true
1056
+ render.content.search.input.value = 'opt'
1057
+
1058
+ const option = render.option(
1059
+ new Option({
1060
+ text: 'opt 1'
1061
+ })
1062
+ )
1063
+
1064
+ expect(option.querySelector('mark')).toBeTruthy()
1065
+ expect(option.querySelector('mark')?.textContent).toBe('opt')
1066
+ })
1067
+
1068
+ test('text is highlighted correctly with option HTML', () => {
1069
+ render.settings.searchHighlight = true
1070
+ render.content.search.input.value = 'opt'
1071
+
1072
+ const option = render.option(
1073
+ new Option({
1074
+ text: 'opt 1',
1075
+ html: '<h1>opt 1</h1>'
1076
+ })
1077
+ )
1078
+
1079
+ expect(option.querySelector('mark')).toBeTruthy()
1080
+ expect(option.querySelector('mark')?.textContent).toBe('opt')
1081
+ })
1082
+
1083
+ test('click does nothing when option is disabled', () => {
1084
+ const option = render.option(
1085
+ new Option({
1086
+ text: 'opt 1',
1087
+ disabled: true
1088
+ })
1089
+ )
1090
+ option.dispatchEvent(new MouseEvent('click'))
1091
+
1092
+ expect(addOptionMock).not.toHaveBeenCalled()
1093
+ expect(setSelectedMock).not.toHaveBeenCalled()
1094
+ })
1095
+
1096
+ test('click does nothing when max count of selected options is reached', () => {
1097
+ render.settings.isMultiple = true
1098
+ render.settings.maxSelected = 1
1099
+
1100
+ const option = render.option(
1101
+ new Option({
1102
+ text: 'opt 1'
1103
+ })
1104
+ )
1105
+ option.dispatchEvent(new MouseEvent('click'))
1106
+
1107
+ expect(addOptionMock).not.toHaveBeenCalled()
1108
+ expect(setSelectedMock).not.toHaveBeenCalled()
1109
+ })
1110
+
1111
+ test('click does nothing when min count of selected options is reached', () => {
1112
+ render.settings.isMultiple = true
1113
+ render.settings.allowDeselect = true
1114
+ render.settings.minSelected = 1
1115
+
1116
+ const option = render.option(
1117
+ new Option({
1118
+ text: 'opt 1',
1119
+ selected: true
1120
+ })
1121
+ )
1122
+ option.dispatchEvent(new MouseEvent('click'))
1123
+
1124
+ expect(addOptionMock).not.toHaveBeenCalled()
1125
+ expect(setSelectedMock).not.toHaveBeenCalled()
1126
+ })
1127
+
1128
+ test('click removes option', () => {
1129
+ const option = render.option(
1130
+ new Option({
1131
+ text: 'new opt 1'
1132
+ })
1133
+ )
1134
+
1135
+ option.dispatchEvent(new MouseEvent('click'))
1136
+
1137
+ expect(addOptionMock).toHaveBeenCalled()
1138
+
1139
+ // check that we add the right option
1140
+ expect(addOptionMock.mock.calls[0][0].text).toBe('new opt 1')
1141
+ expect(setSelectedMock).toHaveBeenCalled()
1142
+ })
1143
+ })
1144
+
1145
+ describe('destroy', () => {
1146
+ test('elements get removed', () => {
1147
+ expect(render.main.main).toBeInstanceOf(HTMLDivElement)
1148
+ expect(render.content.main).toBeInstanceOf(HTMLDivElement)
1149
+
1150
+ // set some simple IDs, so we can search for the elements in the DOM
1151
+ render.main.main.id = 'main-id'
1152
+ render.content.main.id = 'content-id'
1153
+
1154
+ render.destroy()
1155
+
1156
+ expect(document.getElementById('main-id')).toBeNull()
1157
+ expect(document.getElementById('#content-id')).toBeNull()
1158
+ })
1159
+ })
1160
+
1161
+ describe('moveContentAbove', () => {
1162
+ test('correct classes are set', () => {
1163
+ render.moveContentAbove()
1164
+
1165
+ expect(render.main.main.classList.contains(render.classes.openAbove)).toBe(true)
1166
+ expect(render.main.main.classList.contains(render.classes.openBelow)).toBe(false)
1167
+
1168
+ expect(render.content.main.classList.contains(render.classes.openAbove)).toBe(true)
1169
+ expect(render.content.main.classList.contains(render.classes.openBelow)).toBe(false)
1170
+ })
1171
+ })
1172
+
1173
+ describe('moveContentBelow', () => {
1174
+ test('correct classes are set', () => {
1175
+ render.moveContentBelow()
1176
+
1177
+ expect(render.main.main.classList.contains(render.classes.openAbove)).toBe(false)
1178
+ expect(render.main.main.classList.contains(render.classes.openBelow)).toBe(true)
1179
+
1180
+ expect(render.content.main.classList.contains(render.classes.openAbove)).toBe(false)
1181
+ expect(render.content.main.classList.contains(render.classes.openBelow)).toBe(true)
1182
+ })
1183
+ })
1184
+
1185
+ describe('ensureElementInView', () => {})
1186
+
1187
+ describe('putContent', () => {})
1188
+
1189
+ describe('updateDeselectAll', () => {})
39
1190
  })