slim-select 3.4.3 → 3.5.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/dist/classes.d.ts +46 -0
- package/dist/helpers.d.ts +5 -0
- package/dist/index.d.ts +53 -319
- package/dist/index.umd.d.ts +2 -0
- package/dist/react/index.d.ts +4 -319
- package/dist/react/index.js +1459 -1697
- package/dist/react/index.umd.js +2 -2
- package/dist/react/react.d.ts +17 -0
- package/dist/react/react.test.d.ts +1 -0
- package/dist/react/slim-select/react/index.d.ts +4 -0
- package/dist/render.d.ts +88 -0
- package/dist/select.d.ts +38 -0
- package/dist/settings.d.ts +39 -0
- package/dist/slimselect.cjs.js +2 -2
- package/dist/slimselect.cjs.js.map +1 -1
- package/dist/slimselect.css +1 -1
- package/dist/slimselect.css.map +1 -1
- package/dist/slimselect.es.js +1233 -1385
- package/dist/slimselect.es.js.map +1 -1
- package/dist/slimselect.iife.js +2 -2
- package/dist/slimselect.iife.js.map +1 -1
- package/dist/slimselect.js +2 -2
- package/dist/slimselect.js.map +1 -1
- package/dist/src/docs/main.d.ts +0 -0
- package/dist/src/docs/router.d.ts +2 -0
- package/dist/src/docs/store.d.ts +12 -0
- package/dist/store.d.ts +53 -0
- package/dist/vue/index.d.ts +4 -319
- package/dist/vue/index.js +1335 -1504
- package/dist/vue/index.umd.js +1 -1
- package/dist/vue/slim-select/vue/index.d.ts +4 -0
- package/package.json +30 -36
- package/src/slim-select/slimselect.scss +34 -5
- package/.nvmrc +0 -1
- package/.prettierignore +0 -1
- package/.prettierrc +0 -7
- package/CODE_OF_CONDUCT.md +0 -46
- package/CONTRIBUTING.md +0 -1
- package/cdn.html +0 -21
- package/docs.go +0 -12
- package/example.html +0 -25
- package/index.html +0 -76
- package/release.js +0 -191
- package/sitemap.js +0 -108
- package/src/slim-select/accessibility.test.ts +0 -782
- package/src/slim-select/classes.test.ts +0 -73
- package/src/slim-select/classes.ts +0 -117
- package/src/slim-select/helpers.test.ts +0 -162
- package/src/slim-select/helpers.ts +0 -61
- package/src/slim-select/index.test.ts +0 -1356
- package/src/slim-select/index.ts +0 -594
- package/src/slim-select/index.umd.ts +0 -8
- package/src/slim-select/react/index.tsx +0 -9
- package/src/slim-select/react/react.test.tsx +0 -498
- package/src/slim-select/react/react.tsx +0 -209
- package/src/slim-select/render.test.ts +0 -1927
- package/src/slim-select/render.ts +0 -1771
- package/src/slim-select/select.test.ts +0 -683
- package/src/slim-select/select.ts +0 -659
- package/src/slim-select/settings.test.ts +0 -285
- package/src/slim-select/settings.ts +0 -78
- package/src/slim-select/store.test.ts +0 -1007
- package/src/slim-select/store.ts +0 -433
- package/src/slim-select/vue/index.ts +0 -9
- package/src/slim-select/vue/vue.test.ts +0 -1091
- package/src/slim-select/vue/vue.vue +0 -224
- package/tsconfig.json +0 -26
- package/vite.config.docs.mts +0 -29
- package/vite.config.lib.es-cjs.ts +0 -40
- package/vite.config.lib.umd-iife.ts +0 -34
- package/vite.config.react.mts +0 -41
- package/vite.config.vue.mts +0 -40
- 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
|
-
})
|