slim-select 2.13.1 → 3.0.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.
- package/README.md +184 -43
- package/dist/index.d.ts +304 -52
- package/dist/react/index.d.ts +304 -0
- package/dist/react/index.js +1549 -0
- package/dist/react/index.umd.js +22 -0
- package/dist/slimselect.cjs.js +1 -2030
- package/dist/slimselect.css +1 -1
- package/dist/slimselect.css.map +1 -1
- package/dist/slimselect.es.js +1224 -2012
- package/dist/slimselect.umd.js +1 -2036
- package/dist/vue/index.d.ts +304 -0
- package/dist/vue/index.js +1341 -0
- package/dist/vue/index.umd.js +1 -0
- package/index.html +1 -0
- package/package.json +58 -36
- package/release.js +0 -7
- package/src/slim-select/accessibility.test.ts +782 -0
- package/src/slim-select/classes.test.ts +7 -6
- package/src/slim-select/classes.ts +8 -6
- package/src/slim-select/helpers.test.ts +10 -8
- package/src/slim-select/index.test.ts +509 -14
- package/src/slim-select/index.ts +58 -31
- package/src/slim-select/react/index.tsx +9 -0
- package/src/slim-select/react/react.tsx +112 -0
- package/src/slim-select/render.test.ts +479 -119
- package/src/slim-select/render.ts +213 -91
- package/src/slim-select/select.test.ts +28 -10
- package/src/slim-select/select.ts +35 -12
- package/src/slim-select/settings.test.ts +19 -8
- package/src/slim-select/settings.ts +2 -4
- package/src/slim-select/slimselect.scss +30 -15
- package/src/slim-select/store.test.ts +13 -16
- package/src/slim-select/store.ts +97 -86
- package/src/slim-select/vue/index.ts +9 -0
- package/src/slim-select/vue/vue.test.ts +832 -0
- package/src/slim-select/{vue.vue → vue/vue.vue} +45 -23
- package/tsconfig.json +6 -4
- package/{vite.config.mts → vite.config.docs.mts} +10 -0
- package/vite.config.lib.mts +42 -0
- package/vite.config.react.mts +40 -0
- package/vite.config.vue.mts +39 -0
- package/vitest.config.ts +29 -0
- package/dist/classes.d.ts +0 -43
- package/dist/helpers.d.ts +0 -5
- package/dist/render.d.ts +0 -81
- package/dist/select.d.ts +0 -30
- package/dist/settings.d.ts +0 -35
- package/dist/slimselect.global.js +0 -2033
- package/dist/slimselect.js +0 -2036
- package/dist/slimselect.min.js +0 -1
- package/dist/slimselect.umd.min.js +0 -1
- package/dist/store.d.ts +0 -84
- package/jest.config.js +0 -14
- package/src/slim-select/rollup.config.mjs +0 -127
- package/src/slim-select/tsconfig.json +0 -14
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
1
|
'use strict'
|
|
6
2
|
|
|
7
|
-
import { describe, expect, test } from '
|
|
3
|
+
import { describe, expect, test, vi, beforeEach } from 'vitest'
|
|
8
4
|
import Render, { Callbacks } from './render'
|
|
9
5
|
import Settings from './settings'
|
|
10
6
|
import Store, { Option } from './store'
|
|
@@ -12,14 +8,14 @@ import CssClasses from './classes'
|
|
|
12
8
|
|
|
13
9
|
describe('render module', () => {
|
|
14
10
|
let render: Render
|
|
15
|
-
let openMock:
|
|
16
|
-
let closeMock:
|
|
17
|
-
let addSelectedMock:
|
|
18
|
-
let setSelectedMock:
|
|
19
|
-
let addOptionMock:
|
|
20
|
-
let searchMock:
|
|
21
|
-
let afterChangeMock:
|
|
22
|
-
let beforeChangeMock:
|
|
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>
|
|
23
19
|
|
|
24
20
|
beforeEach(() => {
|
|
25
21
|
const store = new Store('single', [
|
|
@@ -42,14 +38,14 @@ describe('render module', () => {
|
|
|
42
38
|
const settings = new Settings()
|
|
43
39
|
const classes = new CssClasses()
|
|
44
40
|
|
|
45
|
-
openMock =
|
|
46
|
-
closeMock =
|
|
47
|
-
addSelectedMock =
|
|
48
|
-
setSelectedMock =
|
|
49
|
-
addOptionMock =
|
|
50
|
-
searchMock =
|
|
51
|
-
afterChangeMock =
|
|
52
|
-
beforeChangeMock =
|
|
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)
|
|
53
49
|
|
|
54
50
|
// default callbacks
|
|
55
51
|
const callbacks = {
|
|
@@ -123,24 +119,33 @@ describe('render module', () => {
|
|
|
123
119
|
render.open()
|
|
124
120
|
|
|
125
121
|
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
122
|
expect(render.main.main.getAttribute('aria-expanded')).toBe('true')
|
|
129
|
-
expect(render.content.main.classList.contains(render.classes.
|
|
130
|
-
|
|
123
|
+
expect(render.content.main.classList.contains(render.classes.contentOpen)).toBe(true)
|
|
124
|
+
// Direction class should be set on both main and content (dirAbove or dirBelow)
|
|
125
|
+
const mainHasDirection =
|
|
126
|
+
render.main.main.classList.contains(render.classes.dirAbove) ||
|
|
127
|
+
render.main.main.classList.contains(render.classes.dirBelow)
|
|
128
|
+
const contentHasDirection =
|
|
129
|
+
render.content.main.classList.contains(render.classes.dirAbove) ||
|
|
130
|
+
render.content.main.classList.contains(render.classes.dirBelow)
|
|
131
|
+
expect(mainHasDirection).toBe(true)
|
|
132
|
+
expect(contentHasDirection).toBe(true)
|
|
131
133
|
})
|
|
132
134
|
})
|
|
133
135
|
|
|
134
136
|
describe('close', () => {
|
|
135
137
|
test('close sets the correct attributes and CSS classes', () => {
|
|
138
|
+
render.open()
|
|
136
139
|
render.close()
|
|
137
140
|
|
|
138
141
|
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
142
|
expect(render.main.main.getAttribute('aria-expanded')).toBe('false')
|
|
142
|
-
expect(render.content.main.classList.contains(render.classes.
|
|
143
|
-
|
|
143
|
+
expect(render.content.main.classList.contains(render.classes.contentOpen)).toBe(false)
|
|
144
|
+
// Direction class should persist after close
|
|
145
|
+
const hasDirection =
|
|
146
|
+
render.content.main.classList.contains(render.classes.dirAbove) ||
|
|
147
|
+
render.content.main.classList.contains(render.classes.dirBelow)
|
|
148
|
+
expect(hasDirection).toBe(true)
|
|
144
149
|
})
|
|
145
150
|
})
|
|
146
151
|
|
|
@@ -200,9 +205,10 @@ describe('render module', () => {
|
|
|
200
205
|
|
|
201
206
|
expect(render.main.main.role).toBe('combobox')
|
|
202
207
|
expect(render.main.main.getAttribute('aria-haspopup')).toBe('listbox')
|
|
203
|
-
expect(render.main.main.getAttribute('aria-controls')).toBe(render.content.
|
|
208
|
+
expect(render.main.main.getAttribute('aria-controls')).toBe(render.content.list.id)
|
|
204
209
|
expect(render.main.main.getAttribute('aria-expanded')).toBe('false')
|
|
205
|
-
expect(render.content.
|
|
210
|
+
expect(render.content.list.getAttribute('role')).toBe('listbox')
|
|
211
|
+
expect(render.content.list.getAttribute('aria-label')).toContain('listbox')
|
|
206
212
|
})
|
|
207
213
|
})
|
|
208
214
|
|
|
@@ -235,7 +241,7 @@ describe('render module', () => {
|
|
|
235
241
|
})
|
|
236
242
|
|
|
237
243
|
test('arrow key events on main element move highlight', () => {
|
|
238
|
-
const highlightMock =
|
|
244
|
+
const highlightMock = vi.fn(() => {})
|
|
239
245
|
|
|
240
246
|
render.highlight = highlightMock
|
|
241
247
|
|
|
@@ -316,7 +322,7 @@ describe('render module', () => {
|
|
|
316
322
|
test('click on deselect on multiple select runs callbacks', () => {
|
|
317
323
|
render.settings.isMultiple = true
|
|
318
324
|
|
|
319
|
-
const deselectAllMock =
|
|
325
|
+
const deselectAllMock = vi.fn()
|
|
320
326
|
|
|
321
327
|
render.updateDeselectAll = deselectAllMock
|
|
322
328
|
|
|
@@ -333,10 +339,10 @@ describe('render module', () => {
|
|
|
333
339
|
})
|
|
334
340
|
|
|
335
341
|
describe('mainFocus', () => {
|
|
336
|
-
let focusMock:
|
|
342
|
+
let focusMock: ReturnType<typeof vi.fn>
|
|
337
343
|
|
|
338
344
|
beforeEach(() => {
|
|
339
|
-
focusMock =
|
|
345
|
+
focusMock = vi.fn(() => {})
|
|
340
346
|
render.main.main.focus = focusMock
|
|
341
347
|
})
|
|
342
348
|
|
|
@@ -485,17 +491,13 @@ describe('render module', () => {
|
|
|
485
491
|
})
|
|
486
492
|
})
|
|
487
493
|
|
|
488
|
-
describe('multipleValue', () => {})
|
|
489
|
-
|
|
490
|
-
describe('contentDiv', () => {})
|
|
491
|
-
|
|
492
494
|
describe('moveContent', () => {
|
|
493
|
-
let contentAboveMock:
|
|
494
|
-
let contentBelowMock:
|
|
495
|
+
let contentAboveMock: ReturnType<typeof vi.fn>
|
|
496
|
+
let contentBelowMock: ReturnType<typeof vi.fn>
|
|
495
497
|
|
|
496
498
|
beforeEach(() => {
|
|
497
|
-
contentAboveMock =
|
|
498
|
-
contentBelowMock =
|
|
499
|
+
contentAboveMock = vi.fn()
|
|
500
|
+
contentBelowMock = vi.fn()
|
|
499
501
|
|
|
500
502
|
render.moveContentAbove = contentAboveMock
|
|
501
503
|
render.moveContentBelow = contentBelowMock
|
|
@@ -526,7 +528,7 @@ describe('render module', () => {
|
|
|
526
528
|
})
|
|
527
529
|
|
|
528
530
|
test('content is moved above when putContent is up', () => {
|
|
529
|
-
render.putContent =
|
|
531
|
+
render.putContent = vi.fn(() => 'up') as any
|
|
530
532
|
|
|
531
533
|
render.moveContent()
|
|
532
534
|
expect(contentAboveMock).toHaveBeenCalled()
|
|
@@ -534,7 +536,7 @@ describe('render module', () => {
|
|
|
534
536
|
})
|
|
535
537
|
|
|
536
538
|
test('content is moved below when putContent is down', () => {
|
|
537
|
-
render.putContent =
|
|
539
|
+
render.putContent = vi.fn(() => 'down') as any
|
|
538
540
|
|
|
539
541
|
render.moveContent()
|
|
540
542
|
expect(contentAboveMock).not.toHaveBeenCalled()
|
|
@@ -564,7 +566,7 @@ describe('render module', () => {
|
|
|
564
566
|
|
|
565
567
|
test('arrow keys move highlight', () => {
|
|
566
568
|
const search = render.searchDiv()
|
|
567
|
-
const highlightMock =
|
|
569
|
+
const highlightMock = vi.fn(() => {})
|
|
568
570
|
|
|
569
571
|
render.highlight = highlightMock
|
|
570
572
|
|
|
@@ -596,7 +598,7 @@ describe('render module', () => {
|
|
|
596
598
|
|
|
597
599
|
test("enter and space don't call addable witout ctrl key", () => {
|
|
598
600
|
const search = render.searchDiv()
|
|
599
|
-
const addableMock =
|
|
601
|
+
const addableMock = vi.fn((s: string) => ({
|
|
600
602
|
text: s,
|
|
601
603
|
value: s.toLowerCase()
|
|
602
604
|
}))
|
|
@@ -611,7 +613,7 @@ describe('render module', () => {
|
|
|
611
613
|
})
|
|
612
614
|
|
|
613
615
|
test('enter and space call event and does nothing without input value', () => {
|
|
614
|
-
const addableMock =
|
|
616
|
+
const addableMock = vi.fn((s: string) => ({
|
|
615
617
|
text: s,
|
|
616
618
|
value: s.toLowerCase()
|
|
617
619
|
}))
|
|
@@ -626,7 +628,7 @@ describe('render module', () => {
|
|
|
626
628
|
})
|
|
627
629
|
|
|
628
630
|
test('enter and space call addable when defined', () => {
|
|
629
|
-
const addableMock =
|
|
631
|
+
const addableMock = vi.fn((s: string) => ({
|
|
630
632
|
text: s,
|
|
631
633
|
value: s.toLowerCase()
|
|
632
634
|
}))
|
|
@@ -642,6 +644,64 @@ describe('render module', () => {
|
|
|
642
644
|
expect(addableMock).toHaveBeenCalledTimes(1)
|
|
643
645
|
expect(addableMock.mock.calls[0]).toStrictEqual(['Search'])
|
|
644
646
|
})
|
|
647
|
+
|
|
648
|
+
test('enter selects highlighted option before calling addable', () => {
|
|
649
|
+
const addableMock = vi.fn((s: string) => ({
|
|
650
|
+
text: s,
|
|
651
|
+
value: s.toLowerCase()
|
|
652
|
+
}))
|
|
653
|
+
|
|
654
|
+
render.callbacks.addable = addableMock
|
|
655
|
+
|
|
656
|
+
// recreate search because we have added the addable callback
|
|
657
|
+
render.content.search = render.searchDiv()
|
|
658
|
+
|
|
659
|
+
// Render options
|
|
660
|
+
render.renderOptions(render.store.getDataOptions())
|
|
661
|
+
|
|
662
|
+
// Set search value
|
|
663
|
+
render.content.search.input.value = '1'
|
|
664
|
+
|
|
665
|
+
// Highlight first option (simulating arrow down)
|
|
666
|
+
render.highlight('down')
|
|
667
|
+
|
|
668
|
+
// Verify an option is highlighted
|
|
669
|
+
const highlighted = render.content.list.querySelector('.' + render.classes.highlighted)
|
|
670
|
+
expect(highlighted).toBeTruthy()
|
|
671
|
+
|
|
672
|
+
// Press Enter - should select highlighted option, NOT call addable
|
|
673
|
+
render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
|
|
674
|
+
|
|
675
|
+
// Addable should NOT have been called because an option was highlighted
|
|
676
|
+
expect(addableMock).not.toHaveBeenCalled()
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
test('enter calls addable when no option is highlighted', () => {
|
|
680
|
+
const addableMock = vi.fn((s: string) => ({
|
|
681
|
+
text: s,
|
|
682
|
+
value: s.toLowerCase()
|
|
683
|
+
}))
|
|
684
|
+
|
|
685
|
+
render.callbacks.addable = addableMock
|
|
686
|
+
|
|
687
|
+
// recreate search because we have added the addable callback
|
|
688
|
+
render.content.search = render.searchDiv()
|
|
689
|
+
|
|
690
|
+
// Render options
|
|
691
|
+
render.renderOptions(render.store.getDataOptions())
|
|
692
|
+
|
|
693
|
+
// Set search value
|
|
694
|
+
render.content.search.input.value = 'NewItem'
|
|
695
|
+
|
|
696
|
+
// Do NOT highlight any option (user just types and presses Enter)
|
|
697
|
+
|
|
698
|
+
// Press Enter - should call addable since no option is highlighted
|
|
699
|
+
render.content.search.input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }))
|
|
700
|
+
|
|
701
|
+
// Addable SHOULD have been called
|
|
702
|
+
expect(addableMock).toHaveBeenCalledTimes(1)
|
|
703
|
+
expect(addableMock.mock.calls[0]).toStrictEqual(['NewItem'])
|
|
704
|
+
})
|
|
645
705
|
})
|
|
646
706
|
|
|
647
707
|
describe('searchFocus', () => {
|
|
@@ -1001,8 +1061,6 @@ describe('render module', () => {
|
|
|
1001
1061
|
})
|
|
1002
1062
|
})
|
|
1003
1063
|
|
|
1004
|
-
describe('renderOptions', () => {})
|
|
1005
|
-
|
|
1006
1064
|
describe('option', () => {
|
|
1007
1065
|
test('add inline styles correctly', () => {
|
|
1008
1066
|
const option = render.option(
|
|
@@ -1080,6 +1138,62 @@ describe('render module', () => {
|
|
|
1080
1138
|
expect(option.querySelector('mark')?.textContent).toBe('opt')
|
|
1081
1139
|
})
|
|
1082
1140
|
|
|
1141
|
+
test('text highlighting with special regex characters does not break HTML', () => {
|
|
1142
|
+
render.settings.searchHighlight = true
|
|
1143
|
+
render.content.search.input.value = '<'
|
|
1144
|
+
|
|
1145
|
+
const option = render.option(
|
|
1146
|
+
new Option({
|
|
1147
|
+
text: 'option <test>'
|
|
1148
|
+
})
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
// Should not break the HTML structure
|
|
1152
|
+
expect(option.querySelector('.ss-option')).toBeFalsy() // No nested ss-option divs
|
|
1153
|
+
|
|
1154
|
+
// The < character should be highlighted if found in text
|
|
1155
|
+
const mark = option.querySelector('mark')
|
|
1156
|
+
if (mark) {
|
|
1157
|
+
expect(mark.textContent).toBe('<')
|
|
1158
|
+
}
|
|
1159
|
+
})
|
|
1160
|
+
|
|
1161
|
+
test('text highlighting escapes special regex characters', () => {
|
|
1162
|
+
render.settings.searchHighlight = true
|
|
1163
|
+
|
|
1164
|
+
// Test various special regex characters
|
|
1165
|
+
const specialChars = ['<', '>', '.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']
|
|
1166
|
+
|
|
1167
|
+
specialChars.forEach((char) => {
|
|
1168
|
+
render.content.search.input.value = char
|
|
1169
|
+
const option = render.option(
|
|
1170
|
+
new Option({
|
|
1171
|
+
text: `option ${char} test`
|
|
1172
|
+
})
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
// Should not throw an error and should contain the character
|
|
1176
|
+
expect(option.textContent).toContain(char)
|
|
1177
|
+
})
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
test('text highlighting with HTML content highlights only text nodes', () => {
|
|
1181
|
+
render.settings.searchHighlight = true
|
|
1182
|
+
render.content.search.input.value = 'test'
|
|
1183
|
+
|
|
1184
|
+
const option = render.option(
|
|
1185
|
+
new Option({
|
|
1186
|
+
text: 'test option',
|
|
1187
|
+
html: '<div class="wrapper">test option</div>'
|
|
1188
|
+
})
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
// Should not break HTML structure - the key fix for issue #570
|
|
1192
|
+
expect(option.querySelector('.wrapper')).toBeTruthy() // HTML structure preserved
|
|
1193
|
+
expect(option.querySelector('mark')).toBeTruthy() // Text is highlighted
|
|
1194
|
+
expect(option.querySelector('mark')?.textContent).toBe('test')
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1083
1197
|
test('click does nothing when option is disabled', () => {
|
|
1084
1198
|
const option = render.option(
|
|
1085
1199
|
new Option({
|
|
@@ -1142,26 +1256,30 @@ describe('render module', () => {
|
|
|
1142
1256
|
})
|
|
1143
1257
|
})
|
|
1144
1258
|
|
|
1145
|
-
describe('
|
|
1259
|
+
describe('native multi-select behavior', () => {
|
|
1146
1260
|
let render: Render
|
|
1147
|
-
let afterChangeMock:
|
|
1261
|
+
let afterChangeMock: ReturnType<typeof vi.fn>
|
|
1262
|
+
let closeMock: ReturnType<typeof vi.fn>
|
|
1148
1263
|
|
|
1149
1264
|
beforeEach(() => {
|
|
1150
|
-
// create a new store with
|
|
1265
|
+
// create a new store with 5 options for comprehensive testing
|
|
1151
1266
|
const store = new Store('multiple', [
|
|
1152
1267
|
{ text: 'test1', value: 'test1' },
|
|
1153
1268
|
{ text: 'test2', value: 'test2' },
|
|
1154
|
-
{ text: 'test3', value: 'test3' }
|
|
1269
|
+
{ text: 'test3', value: 'test3' },
|
|
1270
|
+
{ text: 'test4', value: 'test4' },
|
|
1271
|
+
{ text: 'test5', value: 'test5' }
|
|
1155
1272
|
])
|
|
1156
1273
|
|
|
1157
1274
|
const settings = new Settings()
|
|
1158
1275
|
const classes = new CssClasses()
|
|
1159
1276
|
|
|
1160
|
-
afterChangeMock =
|
|
1277
|
+
afterChangeMock = vi.fn(() => {})
|
|
1278
|
+
closeMock = vi.fn(() => {})
|
|
1161
1279
|
|
|
1162
1280
|
const callbacks = {
|
|
1163
1281
|
open: () => {},
|
|
1164
|
-
close:
|
|
1282
|
+
close: closeMock,
|
|
1165
1283
|
addSelected: () => {},
|
|
1166
1284
|
setSelected: (value) => {
|
|
1167
1285
|
store.setSelectedBy('id', typeof value === 'string' ? [value] : value)
|
|
@@ -1173,70 +1291,320 @@ describe('render module', () => {
|
|
|
1173
1291
|
|
|
1174
1292
|
render = new Render(settings, classes, store, callbacks)
|
|
1175
1293
|
render.settings.isMultiple = true
|
|
1176
|
-
render.settings.closeOnSelect =
|
|
1294
|
+
render.settings.closeOnSelect = true
|
|
1177
1295
|
})
|
|
1178
1296
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1297
|
+
describe('Regular Click (no modifiers)', () => {
|
|
1298
|
+
test('toggles option (add/remove) without affecting others', () => {
|
|
1299
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1300
|
+
const opts = render.getOptions(false, false, true)
|
|
1181
1301
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1302
|
+
// Click first option - adds it
|
|
1303
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1304
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1184
1305
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1306
|
+
// Click third option - adds it (first option still selected)
|
|
1307
|
+
opts[2].dispatchEvent(new MouseEvent('click'))
|
|
1308
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1309
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1310
|
+
expect.objectContaining({ value: 'test3' })
|
|
1311
|
+
])
|
|
1188
1312
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1313
|
+
// Click first option again - removes it
|
|
1314
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1315
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test3' })])
|
|
1316
|
+
})
|
|
1191
1317
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1318
|
+
test('closes dropdown on regular click when closeOnSelect is true', () => {
|
|
1319
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1320
|
+
const opts = render.getOptions(false, false, true)
|
|
1321
|
+
|
|
1322
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1323
|
+
expect(closeMock).toHaveBeenCalled()
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
test('regular click on selected option deselects it and closes dropdown', () => {
|
|
1327
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1328
|
+
const opts = render.getOptions(false, false, true)
|
|
1329
|
+
|
|
1330
|
+
// Select two options
|
|
1331
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1332
|
+
opts[1].dispatchEvent(new MouseEvent('click'))
|
|
1333
|
+
|
|
1334
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([
|
|
1335
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1336
|
+
expect.objectContaining({ value: 'test2' })
|
|
1337
|
+
])
|
|
1338
|
+
closeMock.mockClear()
|
|
1339
|
+
|
|
1340
|
+
// Click on already selected option - should deselect it and close
|
|
1341
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1342
|
+
|
|
1343
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test2' })])
|
|
1344
|
+
expect(closeMock).toHaveBeenCalledTimes(1)
|
|
1345
|
+
})
|
|
1346
|
+
|
|
1347
|
+
test('regression: regular click adds/removes and closes, Cmd/Ctrl keeps open', () => {
|
|
1348
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1349
|
+
const opts = render.getOptions(false, false, true)
|
|
1350
|
+
|
|
1351
|
+
// Regular click - should close
|
|
1352
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1353
|
+
expect(closeMock).toHaveBeenCalledTimes(1)
|
|
1354
|
+
closeMock.mockClear()
|
|
1355
|
+
|
|
1356
|
+
// Cmd+Click - should NOT close
|
|
1357
|
+
opts[1].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1358
|
+
expect(closeMock).not.toHaveBeenCalled()
|
|
1359
|
+
|
|
1360
|
+
// Ctrl+Click - should NOT close
|
|
1361
|
+
opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1362
|
+
expect(closeMock).not.toHaveBeenCalled()
|
|
1363
|
+
|
|
1364
|
+
// Regular click again - should close
|
|
1365
|
+
opts[3].dispatchEvent(new MouseEvent('click'))
|
|
1366
|
+
expect(closeMock).toHaveBeenCalledTimes(1)
|
|
1367
|
+
})
|
|
1198
1368
|
})
|
|
1199
1369
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1370
|
+
describe('Cmd/Ctrl+Click (toggle selection)', () => {
|
|
1371
|
+
test('Cmd+Click adds option without deselecting others (Mac)', () => {
|
|
1372
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1373
|
+
const opts = render.getOptions(false, false, true)
|
|
1202
1374
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1375
|
+
// Cmd+Click first option
|
|
1376
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1377
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1205
1378
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1379
|
+
// Cmd+Click third option - both should be selected
|
|
1380
|
+
opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1381
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1382
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1383
|
+
expect.objectContaining({ value: 'test3' })
|
|
1384
|
+
])
|
|
1385
|
+
})
|
|
1209
1386
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1387
|
+
test('Ctrl+Click adds option without deselecting others (Windows/Linux)', () => {
|
|
1388
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1389
|
+
const opts = render.getOptions(false, false, true)
|
|
1212
1390
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
expect.objectContaining({ value: '
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1391
|
+
// Ctrl+Click first option
|
|
1392
|
+
opts[0].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1393
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1394
|
+
|
|
1395
|
+
// Ctrl+Click third option - both should be selected
|
|
1396
|
+
opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1397
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1398
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1399
|
+
expect.objectContaining({ value: 'test3' })
|
|
1400
|
+
])
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
test('Cmd+Click on selected option deselects it (Mac)', () => {
|
|
1404
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1405
|
+
const opts = render.getOptions(false, false, true)
|
|
1406
|
+
|
|
1407
|
+
// Select multiple options with Cmd
|
|
1408
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1409
|
+
opts[1].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1410
|
+
opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1411
|
+
|
|
1412
|
+
// Cmd+Click on middle option to deselect it
|
|
1413
|
+
opts[1].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1414
|
+
|
|
1415
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1416
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1417
|
+
expect.objectContaining({ value: 'test3' })
|
|
1418
|
+
])
|
|
1419
|
+
})
|
|
1420
|
+
|
|
1421
|
+
test('Ctrl+Click on selected option deselects it (Windows/Linux)', () => {
|
|
1422
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1423
|
+
const opts = render.getOptions(false, false, true)
|
|
1424
|
+
|
|
1425
|
+
// Select multiple options with Ctrl
|
|
1426
|
+
opts[0].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1427
|
+
opts[1].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1428
|
+
opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1429
|
+
|
|
1430
|
+
// Ctrl+Click on middle option to deselect it
|
|
1431
|
+
opts[1].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1432
|
+
|
|
1433
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1434
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1435
|
+
expect.objectContaining({ value: 'test3' })
|
|
1436
|
+
])
|
|
1437
|
+
})
|
|
1438
|
+
|
|
1439
|
+
test('Cmd+Click toggles selection on and off', () => {
|
|
1440
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1441
|
+
const opts = render.getOptions(false, false, true)
|
|
1442
|
+
|
|
1443
|
+
// Click to select
|
|
1444
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1445
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1446
|
+
|
|
1447
|
+
// Click again to deselect
|
|
1448
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1449
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([])
|
|
1450
|
+
|
|
1451
|
+
// Click once more to select again
|
|
1452
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1453
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1454
|
+
})
|
|
1455
|
+
|
|
1456
|
+
test('Cmd/Ctrl+Click works even when allowDeselect is false', () => {
|
|
1457
|
+
render.settings.allowDeselect = false
|
|
1458
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1459
|
+
const opts = render.getOptions(false, false, true)
|
|
1460
|
+
|
|
1461
|
+
// Select with Cmd/Ctrl
|
|
1462
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1463
|
+
opts[1].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1464
|
+
|
|
1465
|
+
// Should be able to deselect with Cmd/Ctrl even though allowDeselect is false
|
|
1466
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1467
|
+
|
|
1468
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test2' })])
|
|
1469
|
+
})
|
|
1470
|
+
|
|
1471
|
+
test('does NOT close dropdown on Cmd+Click', () => {
|
|
1472
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1473
|
+
const opts = render.getOptions(false, false, true)
|
|
1474
|
+
|
|
1475
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1476
|
+
expect(closeMock).not.toHaveBeenCalled()
|
|
1477
|
+
})
|
|
1478
|
+
|
|
1479
|
+
test('does NOT close dropdown on Ctrl+Click', () => {
|
|
1480
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1481
|
+
const opts = render.getOptions(false, false, true)
|
|
1482
|
+
|
|
1483
|
+
opts[0].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1484
|
+
expect(closeMock).not.toHaveBeenCalled()
|
|
1485
|
+
})
|
|
1219
1486
|
})
|
|
1220
1487
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1488
|
+
describe('Shift+Click (range selection)', () => {
|
|
1489
|
+
test('selects range from last clicked to current', () => {
|
|
1490
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1491
|
+
const opts = render.getOptions(false, false, true)
|
|
1224
1492
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1493
|
+
// Click first option
|
|
1494
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1495
|
+
expect(afterChangeMock).toHaveBeenCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1227
1496
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1497
|
+
// Shift+Click third option - should select test1, test2, test3
|
|
1498
|
+
opts[2].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1499
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1500
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1501
|
+
expect.objectContaining({ value: 'test2' }),
|
|
1502
|
+
expect.objectContaining({ value: 'test3' })
|
|
1503
|
+
])
|
|
1504
|
+
})
|
|
1231
1505
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1506
|
+
test('selects range in reverse direction', () => {
|
|
1507
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1508
|
+
const opts = render.getOptions(false, false, true)
|
|
1234
1509
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1510
|
+
// Click fourth option
|
|
1511
|
+
opts[3].dispatchEvent(new MouseEvent('click'))
|
|
1512
|
+
|
|
1513
|
+
// Shift+Click second option - should select test2, test3, test4
|
|
1514
|
+
opts[1].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1515
|
+
expect(afterChangeMock).toHaveBeenCalledWith([
|
|
1516
|
+
expect.objectContaining({ value: 'test4' }),
|
|
1517
|
+
expect.objectContaining({ value: 'test2' }),
|
|
1518
|
+
expect.objectContaining({ value: 'test3' })
|
|
1519
|
+
])
|
|
1520
|
+
})
|
|
1521
|
+
|
|
1522
|
+
test('respects maxSelected limit', () => {
|
|
1523
|
+
render.settings.maxSelected = 2
|
|
1524
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1525
|
+
const opts = render.getOptions(false, false, true)
|
|
1526
|
+
|
|
1527
|
+
// Click first option
|
|
1528
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1529
|
+
|
|
1530
|
+
// Shift+Click fourth option - would select 4 items, exceeds limit
|
|
1531
|
+
opts[3].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1532
|
+
|
|
1533
|
+
// Should keep only the original selection
|
|
1534
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([expect.objectContaining({ value: 'test1' })])
|
|
1535
|
+
})
|
|
1536
|
+
|
|
1537
|
+
test('does NOT close dropdown on Shift+Click', () => {
|
|
1538
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1539
|
+
const opts = render.getOptions(false, false, true)
|
|
1540
|
+
|
|
1541
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1542
|
+
closeMock.mockClear()
|
|
1543
|
+
|
|
1544
|
+
opts[2].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1545
|
+
expect(closeMock).not.toHaveBeenCalled()
|
|
1546
|
+
})
|
|
1547
|
+
})
|
|
1548
|
+
|
|
1549
|
+
describe('Combined modifier keys', () => {
|
|
1550
|
+
test('Cmd/Ctrl+Click then Shift+Click selects range from last click', () => {
|
|
1551
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1552
|
+
const opts = render.getOptions(false, false, true)
|
|
1553
|
+
|
|
1554
|
+
// Cmd+Click first and Ctrl+Click third options (testing both)
|
|
1555
|
+
opts[0].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1556
|
+
opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1557
|
+
|
|
1558
|
+
// Shift+Click fifth option - should add test3, test4, test5 to selection
|
|
1559
|
+
opts[4].dispatchEvent(new MouseEvent('click', { shiftKey: true }))
|
|
1560
|
+
|
|
1561
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([
|
|
1562
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1563
|
+
expect.objectContaining({ value: 'test3' }),
|
|
1564
|
+
expect.objectContaining({ value: 'test4' }),
|
|
1565
|
+
expect.objectContaining({ value: 'test5' })
|
|
1566
|
+
])
|
|
1567
|
+
})
|
|
1568
|
+
|
|
1569
|
+
test('can build complex selections like native multi-select (Mac)', () => {
|
|
1570
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1571
|
+
const opts = render.getOptions(false, false, true)
|
|
1572
|
+
|
|
1573
|
+
// Select test1
|
|
1574
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1575
|
+
// Cmd+Add test3
|
|
1576
|
+
opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1577
|
+
// Cmd+Add test5
|
|
1578
|
+
opts[4].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1579
|
+
// Cmd+Remove test3
|
|
1580
|
+
opts[2].dispatchEvent(new MouseEvent('click', { metaKey: true }))
|
|
1581
|
+
|
|
1582
|
+
// Final selection should be test1 and test5
|
|
1583
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([
|
|
1584
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1585
|
+
expect.objectContaining({ value: 'test5' })
|
|
1586
|
+
])
|
|
1587
|
+
})
|
|
1588
|
+
|
|
1589
|
+
test('can build complex selections like native multi-select (Windows/Linux)', () => {
|
|
1590
|
+
render.renderOptions(render.store.getDataOptions())
|
|
1591
|
+
const opts = render.getOptions(false, false, true)
|
|
1592
|
+
|
|
1593
|
+
// Select test1
|
|
1594
|
+
opts[0].dispatchEvent(new MouseEvent('click'))
|
|
1595
|
+
// Ctrl+Add test3
|
|
1596
|
+
opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1597
|
+
// Ctrl+Add test5
|
|
1598
|
+
opts[4].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1599
|
+
// Ctrl+Remove test3
|
|
1600
|
+
opts[2].dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
|
|
1601
|
+
|
|
1602
|
+
// Final selection should be test1 and test5
|
|
1603
|
+
expect(afterChangeMock).toHaveBeenLastCalledWith([
|
|
1604
|
+
expect.objectContaining({ value: 'test1' }),
|
|
1605
|
+
expect.objectContaining({ value: 'test5' })
|
|
1606
|
+
])
|
|
1607
|
+
})
|
|
1240
1608
|
})
|
|
1241
1609
|
})
|
|
1242
1610
|
|
|
@@ -1260,11 +1628,10 @@ describe('render module', () => {
|
|
|
1260
1628
|
test('correct classes are set', () => {
|
|
1261
1629
|
render.moveContentAbove()
|
|
1262
1630
|
|
|
1263
|
-
expect(render.main.main.classList.contains(render.classes.
|
|
1264
|
-
expect(render.main.main.classList.contains(render.classes.
|
|
1265
|
-
|
|
1266
|
-
expect(render.content.main.classList.contains(render.classes.
|
|
1267
|
-
expect(render.content.main.classList.contains(render.classes.openBelow)).toBe(false)
|
|
1631
|
+
expect(render.main.main.classList.contains(render.classes.dirAbove)).toBe(true)
|
|
1632
|
+
expect(render.main.main.classList.contains(render.classes.dirBelow)).toBe(false)
|
|
1633
|
+
expect(render.content.main.classList.contains(render.classes.dirAbove)).toBe(true)
|
|
1634
|
+
expect(render.content.main.classList.contains(render.classes.dirBelow)).toBe(false)
|
|
1268
1635
|
})
|
|
1269
1636
|
})
|
|
1270
1637
|
|
|
@@ -1272,17 +1639,10 @@ describe('render module', () => {
|
|
|
1272
1639
|
test('correct classes are set', () => {
|
|
1273
1640
|
render.moveContentBelow()
|
|
1274
1641
|
|
|
1275
|
-
expect(render.main.main.classList.contains(render.classes.
|
|
1276
|
-
expect(render.main.main.classList.contains(render.classes.
|
|
1277
|
-
|
|
1278
|
-
expect(render.content.main.classList.contains(render.classes.
|
|
1279
|
-
expect(render.content.main.classList.contains(render.classes.openBelow)).toBe(true)
|
|
1642
|
+
expect(render.main.main.classList.contains(render.classes.dirAbove)).toBe(false)
|
|
1643
|
+
expect(render.main.main.classList.contains(render.classes.dirBelow)).toBe(true)
|
|
1644
|
+
expect(render.content.main.classList.contains(render.classes.dirAbove)).toBe(false)
|
|
1645
|
+
expect(render.content.main.classList.contains(render.classes.dirBelow)).toBe(true)
|
|
1280
1646
|
})
|
|
1281
1647
|
})
|
|
1282
|
-
|
|
1283
|
-
describe('ensureElementInView', () => {})
|
|
1284
|
-
|
|
1285
|
-
describe('putContent', () => {})
|
|
1286
|
-
|
|
1287
|
-
describe('updateDeselectAll', () => {})
|
|
1288
1648
|
})
|