slim-select 3.4.2 → 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 -318
- package/dist/index.umd.d.ts +2 -0
- package/dist/react/index.d.ts +4 -318
- package/dist/react/index.js +1459 -1696
- 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 -1384
- 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 -318
- package/dist/vue/index.js +1335 -1503
- 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 -1877
- package/src/slim-select/render.ts +0 -1765
- 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,498 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import { render, cleanup, act, waitFor } from '@testing-library/react'
|
|
3
|
-
import { useState, useEffect, useRef } from 'react'
|
|
4
|
-
import SlimSelect, { Option } from '../index'
|
|
5
|
-
import SlimSelectReact, { SlimSelectRef } from './react'
|
|
6
|
-
|
|
7
|
-
describe('SlimSelect React Component', () => {
|
|
8
|
-
let consoleInfoSpy: any
|
|
9
|
-
let consoleWarnSpy: any
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
// Mock console methods
|
|
13
|
-
consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
|
|
14
|
-
consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
cleanup()
|
|
19
|
-
// Restore console methods
|
|
20
|
-
consoleInfoSpy.mockRestore()
|
|
21
|
-
consoleWarnSpy.mockRestore()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('Basic Rendering', () => {
|
|
25
|
-
test('renders a select element', () => {
|
|
26
|
-
const { container } = render(<SlimSelectReact />)
|
|
27
|
-
const select = container.querySelector('select')
|
|
28
|
-
expect(select).toBeTruthy()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('renders with single select by default', () => {
|
|
32
|
-
const { container } = render(<SlimSelectReact />)
|
|
33
|
-
const select = container.querySelector('select')
|
|
34
|
-
expect(select?.hasAttribute('multiple')).toBe(false)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('renders with multiple attribute when multiple prop is true', () => {
|
|
38
|
-
const { container } = render(<SlimSelectReact multiple />)
|
|
39
|
-
const select = container.querySelector('select')
|
|
40
|
-
expect(select?.hasAttribute('multiple')).toBe(true)
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
describe('Value and onChange binding', () => {
|
|
45
|
-
test('accepts initial value for single select', async () => {
|
|
46
|
-
const TestComponent = () => {
|
|
47
|
-
const [value, setValue] = useState('2')
|
|
48
|
-
const ref = useRef<SlimSelectRef>(null)
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<SlimSelectReact
|
|
52
|
-
ref={ref}
|
|
53
|
-
value={value}
|
|
54
|
-
onChange={(val) => setValue(val as string)}
|
|
55
|
-
data={[
|
|
56
|
-
{ value: '1', text: 'Option 1' },
|
|
57
|
-
{ value: '2', text: 'Option 2' },
|
|
58
|
-
{ value: '3', text: 'Option 3' }
|
|
59
|
-
]}
|
|
60
|
-
/>
|
|
61
|
-
)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const { container } = render(<TestComponent />)
|
|
65
|
-
|
|
66
|
-
await waitFor(
|
|
67
|
-
() => {
|
|
68
|
-
const select = container.querySelector('select')
|
|
69
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
70
|
-
if (slimInstance) {
|
|
71
|
-
const selected = slimInstance.getSelected()
|
|
72
|
-
expect(selected[0]).toBe('2')
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
{ timeout: 500 }
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('accepts initial value for multiple select', async () => {
|
|
80
|
-
const TestComponent = () => {
|
|
81
|
-
const [value, setValue] = useState(['2', '3'])
|
|
82
|
-
return (
|
|
83
|
-
<SlimSelectReact
|
|
84
|
-
value={value}
|
|
85
|
-
onChange={(val) => setValue(val as string[])}
|
|
86
|
-
multiple
|
|
87
|
-
data={[
|
|
88
|
-
{ value: '1', text: 'Option 1' },
|
|
89
|
-
{ value: '2', text: 'Option 2' },
|
|
90
|
-
{ value: '3', text: 'Option 3' }
|
|
91
|
-
]}
|
|
92
|
-
/>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const { container } = render(<TestComponent />)
|
|
97
|
-
|
|
98
|
-
await waitFor(
|
|
99
|
-
() => {
|
|
100
|
-
const select = container.querySelector('select')
|
|
101
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
102
|
-
if (slimInstance) {
|
|
103
|
-
const selected = slimInstance.getSelected()
|
|
104
|
-
expect(selected).toEqual(['2', '3'])
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
{ timeout: 500 }
|
|
108
|
-
)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('calls onChange when selection changes', async () => {
|
|
112
|
-
const onChangeMock = vi.fn()
|
|
113
|
-
const TestComponent = () => {
|
|
114
|
-
return (
|
|
115
|
-
<SlimSelectReact
|
|
116
|
-
value="1"
|
|
117
|
-
onChange={onChangeMock}
|
|
118
|
-
data={[
|
|
119
|
-
{ value: '1', text: 'Option 1' },
|
|
120
|
-
{ value: '2', text: 'Option 2' }
|
|
121
|
-
]}
|
|
122
|
-
/>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const { container } = render(<TestComponent />)
|
|
127
|
-
|
|
128
|
-
await waitFor(
|
|
129
|
-
async () => {
|
|
130
|
-
const select = container.querySelector('select')
|
|
131
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
132
|
-
if (slimInstance) {
|
|
133
|
-
await act(async () => {
|
|
134
|
-
slimInstance.setSelected('2')
|
|
135
|
-
})
|
|
136
|
-
await waitFor(
|
|
137
|
-
() => {
|
|
138
|
-
expect(onChangeMock).toHaveBeenCalledWith('2')
|
|
139
|
-
},
|
|
140
|
-
{ timeout: 500 }
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
{ timeout: 500 }
|
|
145
|
-
)
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test('updates selection when value prop changes', async () => {
|
|
149
|
-
const TestComponent = ({ initialValue }: { initialValue: string }) => {
|
|
150
|
-
const [value, setValue] = useState(initialValue)
|
|
151
|
-
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
// Simulate parent updating value
|
|
154
|
-
const timer = setTimeout(() => {
|
|
155
|
-
act(() => {
|
|
156
|
-
setValue('2')
|
|
157
|
-
})
|
|
158
|
-
}, 100)
|
|
159
|
-
return () => clearTimeout(timer)
|
|
160
|
-
}, [])
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<SlimSelectReact
|
|
164
|
-
value={value}
|
|
165
|
-
onChange={(val) => setValue(val as string)}
|
|
166
|
-
data={[
|
|
167
|
-
{ value: '1', text: 'Option 1' },
|
|
168
|
-
{ value: '2', text: 'Option 2' }
|
|
169
|
-
]}
|
|
170
|
-
/>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const { container } = render(<TestComponent initialValue="1" />)
|
|
175
|
-
|
|
176
|
-
await waitFor(
|
|
177
|
-
() => {
|
|
178
|
-
const select = container.querySelector('select')
|
|
179
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
180
|
-
if (slimInstance) {
|
|
181
|
-
const selected = slimInstance.getSelected()
|
|
182
|
-
expect(selected[0]).toBe('2')
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
{ timeout: 500 }
|
|
186
|
-
)
|
|
187
|
-
})
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
describe('Data Prop', () => {
|
|
191
|
-
test('initializes with data prop', async () => {
|
|
192
|
-
const testData = [
|
|
193
|
-
{ value: '1', text: 'Option 1' },
|
|
194
|
-
{ value: '2', text: 'Option 2' },
|
|
195
|
-
{ value: '3', text: 'Option 3' }
|
|
196
|
-
]
|
|
197
|
-
|
|
198
|
-
const { container } = render(<SlimSelectReact data={testData} />)
|
|
199
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
200
|
-
|
|
201
|
-
const select = container.querySelector('select')
|
|
202
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
203
|
-
if (slimInstance) {
|
|
204
|
-
const data = slimInstance.getData()
|
|
205
|
-
expect(data).toHaveLength(3)
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
test('updates data when data prop changes', async () => {
|
|
210
|
-
const TestComponent = () => {
|
|
211
|
-
const [data, setData] = useState([
|
|
212
|
-
{ value: '1', text: 'Option 1' },
|
|
213
|
-
{ value: '2', text: 'Option 2' }
|
|
214
|
-
])
|
|
215
|
-
|
|
216
|
-
useEffect(() => {
|
|
217
|
-
const timer = setTimeout(() => {
|
|
218
|
-
act(() => {
|
|
219
|
-
setData([
|
|
220
|
-
{ value: '3', text: 'Option 3' },
|
|
221
|
-
{ value: '4', text: 'Option 4' },
|
|
222
|
-
{ value: '5', text: 'Option 5' }
|
|
223
|
-
])
|
|
224
|
-
})
|
|
225
|
-
}, 100)
|
|
226
|
-
return () => clearTimeout(timer)
|
|
227
|
-
}, [])
|
|
228
|
-
|
|
229
|
-
return <SlimSelectReact data={data} />
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const { container } = render(<TestComponent />)
|
|
233
|
-
|
|
234
|
-
await waitFor(
|
|
235
|
-
() => {
|
|
236
|
-
const select = container.querySelector('select')
|
|
237
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
238
|
-
if (slimInstance) {
|
|
239
|
-
const data = slimInstance.getData()
|
|
240
|
-
expect(data).toHaveLength(3)
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
{ timeout: 500 }
|
|
244
|
-
)
|
|
245
|
-
})
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
describe('Settings Prop', () => {
|
|
249
|
-
test('applies settings prop', async () => {
|
|
250
|
-
const { container } = render(
|
|
251
|
-
<SlimSelectReact
|
|
252
|
-
data={[{ value: '1', text: 'Option 1' }]}
|
|
253
|
-
settings={{
|
|
254
|
-
showSearch: false,
|
|
255
|
-
placeholderText: 'Custom Placeholder'
|
|
256
|
-
}}
|
|
257
|
-
/>
|
|
258
|
-
)
|
|
259
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
260
|
-
|
|
261
|
-
const select = container.querySelector('select')
|
|
262
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
263
|
-
if (slimInstance) {
|
|
264
|
-
expect(slimInstance.settings.showSearch).toBe(false)
|
|
265
|
-
expect(slimInstance.settings.placeholderText).toBe('Custom Placeholder')
|
|
266
|
-
}
|
|
267
|
-
})
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
describe('Events Prop', () => {
|
|
271
|
-
test('calls afterChange event from events prop', async () => {
|
|
272
|
-
const afterChangeMock = vi.fn()
|
|
273
|
-
|
|
274
|
-
const TestComponent = () => {
|
|
275
|
-
return (
|
|
276
|
-
<SlimSelectReact
|
|
277
|
-
value="1"
|
|
278
|
-
data={[
|
|
279
|
-
{ value: '1', text: 'Option 1' },
|
|
280
|
-
{ value: '2', text: 'Option 2' }
|
|
281
|
-
]}
|
|
282
|
-
events={{
|
|
283
|
-
afterChange: afterChangeMock
|
|
284
|
-
}}
|
|
285
|
-
/>
|
|
286
|
-
)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const { container } = render(<TestComponent />)
|
|
290
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
291
|
-
|
|
292
|
-
const select = container.querySelector('select')
|
|
293
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
294
|
-
if (slimInstance) {
|
|
295
|
-
slimInstance.setSelected('2')
|
|
296
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
297
|
-
expect(afterChangeMock).toHaveBeenCalled()
|
|
298
|
-
}
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
test('calls both custom afterChange and onChange', async () => {
|
|
302
|
-
const afterChangeMock = vi.fn()
|
|
303
|
-
const onChangeMock = vi.fn()
|
|
304
|
-
|
|
305
|
-
const TestComponent = () => {
|
|
306
|
-
return (
|
|
307
|
-
<SlimSelectReact
|
|
308
|
-
value="1"
|
|
309
|
-
onChange={onChangeMock}
|
|
310
|
-
data={[
|
|
311
|
-
{ value: '1', text: 'Option 1' },
|
|
312
|
-
{ value: '2', text: 'Option 2' }
|
|
313
|
-
]}
|
|
314
|
-
events={{
|
|
315
|
-
afterChange: afterChangeMock
|
|
316
|
-
}}
|
|
317
|
-
/>
|
|
318
|
-
)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const { container } = render(<TestComponent />)
|
|
322
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
323
|
-
|
|
324
|
-
const select = container.querySelector('select')
|
|
325
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
326
|
-
if (slimInstance) {
|
|
327
|
-
slimInstance.setSelected('2')
|
|
328
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
329
|
-
expect(afterChangeMock).toHaveBeenCalled()
|
|
330
|
-
expect(onChangeMock).toHaveBeenCalledWith('2')
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
describe('Empty and Invalid Values', () => {
|
|
336
|
-
test('handles empty string as value for single select', async () => {
|
|
337
|
-
const TestComponent = () => {
|
|
338
|
-
const [value, setValue] = useState('')
|
|
339
|
-
return (
|
|
340
|
-
<SlimSelectReact
|
|
341
|
-
value={value}
|
|
342
|
-
onChange={(val) => setValue(val as string)}
|
|
343
|
-
data={[
|
|
344
|
-
{ value: '1', text: 'Option 1' },
|
|
345
|
-
{ value: '2', text: 'Option 2' }
|
|
346
|
-
]}
|
|
347
|
-
/>
|
|
348
|
-
)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const { container } = render(<TestComponent />)
|
|
352
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
353
|
-
|
|
354
|
-
const select = container.querySelector('select')
|
|
355
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
356
|
-
if (slimInstance) {
|
|
357
|
-
// Verify placeholder exists in data
|
|
358
|
-
const data = slimInstance.getData()
|
|
359
|
-
const hasPlaceholder = (data as Option[]).some((opt: any) => opt.placeholder)
|
|
360
|
-
expect(hasPlaceholder).toBe(true)
|
|
361
|
-
|
|
362
|
-
// CRITICAL: Verify what's actually selected - should be empty string (placeholder)
|
|
363
|
-
const selectedValues = slimInstance.getSelected()
|
|
364
|
-
expect(selectedValues).toEqual([''])
|
|
365
|
-
|
|
366
|
-
// Verify no valid option is selected
|
|
367
|
-
const hasValidOptionSelected = selectedValues.some((val) => val !== '' && ['1', '2'].includes(val))
|
|
368
|
-
expect(hasValidOptionSelected).toBe(false)
|
|
369
|
-
}
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
test('shows placeholder when value does not exist in options for single select', async () => {
|
|
373
|
-
const TestComponent = () => {
|
|
374
|
-
const [value, setValue] = useState('banana')
|
|
375
|
-
return (
|
|
376
|
-
<SlimSelectReact
|
|
377
|
-
value={value}
|
|
378
|
-
onChange={(val) => setValue(val as string)}
|
|
379
|
-
data={[
|
|
380
|
-
{ value: '1', text: 'Option 1' },
|
|
381
|
-
{ value: '2', text: 'Option 2' },
|
|
382
|
-
{ value: '3', text: 'Option 3' }
|
|
383
|
-
]}
|
|
384
|
-
/>
|
|
385
|
-
)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const { container } = render(<TestComponent />)
|
|
389
|
-
await waitFor(
|
|
390
|
-
() => {
|
|
391
|
-
const select = container.querySelector('select')
|
|
392
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
393
|
-
if (slimInstance) {
|
|
394
|
-
// Verify placeholder exists in data (with empty value, not 'banana')
|
|
395
|
-
const data = slimInstance.getData()
|
|
396
|
-
const hasPlaceholder = (data as Option[]).some((opt: any) => opt.placeholder)
|
|
397
|
-
expect(hasPlaceholder).toBe(true)
|
|
398
|
-
|
|
399
|
-
// CRITICAL: Check what's actually displayed in the SlimSelect UI
|
|
400
|
-
// SlimSelect renders a custom UI - check the .ss-single or .ss-placeholder element
|
|
401
|
-
const slimMain = container.querySelector('.ss-main') as HTMLElement
|
|
402
|
-
expect(slimMain).toBeDefined()
|
|
403
|
-
|
|
404
|
-
// Check what's in the .ss-values container
|
|
405
|
-
const ssValues = slimMain?.querySelector('.ss-values') as HTMLElement
|
|
406
|
-
expect(ssValues).toBeDefined()
|
|
407
|
-
|
|
408
|
-
// If placeholder is selected, it should show placeholder (no .ss-single element)
|
|
409
|
-
// If a real option is selected, it will show .ss-single with the option text
|
|
410
|
-
const singleValue = ssValues?.querySelector('.ss-single') as HTMLElement
|
|
411
|
-
const placeholderEl = ssValues?.querySelector('.ss-placeholder') as HTMLElement
|
|
412
|
-
|
|
413
|
-
// CRITICAL: If placeholder is correctly selected, we should NOT see .ss-single with "Option 1"
|
|
414
|
-
// Instead, we should either see placeholder OR empty (if placeholder text is empty)
|
|
415
|
-
if (singleValue) {
|
|
416
|
-
const displayedText = singleValue.textContent?.trim() || ''
|
|
417
|
-
// This should NOT be showing a real option
|
|
418
|
-
expect(displayedText).not.toBe('Option 1')
|
|
419
|
-
expect(displayedText).not.toBe('Option 2')
|
|
420
|
-
expect(displayedText).not.toBe('Option 3')
|
|
421
|
-
|
|
422
|
-
// If it's showing something, it might be the placeholder (empty text is OK)
|
|
423
|
-
// But we should verify the placeholder option is actually selected in the store
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Check the store to see what's actually selected
|
|
427
|
-
const selectedOptions = (slimInstance as any).store.getSelectedOptions()
|
|
428
|
-
const hasPlaceholderSelected = selectedOptions.some((opt: any) => opt.placeholder && opt.selected)
|
|
429
|
-
const hasValidOptionSelected = selectedOptions.some(
|
|
430
|
-
(opt: any) => opt.selected && !opt.placeholder && ['1', '2', '3'].includes(opt.value)
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
// The placeholder should be selected, not a valid option
|
|
434
|
-
expect(hasPlaceholderSelected).toBe(true)
|
|
435
|
-
expect(hasValidOptionSelected).toBe(false)
|
|
436
|
-
}
|
|
437
|
-
},
|
|
438
|
-
{ timeout: 500 }
|
|
439
|
-
)
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
test('handles empty array as value for multiple select', async () => {
|
|
443
|
-
const TestComponent = () => {
|
|
444
|
-
const [value, setValue] = useState([] as string[])
|
|
445
|
-
return (
|
|
446
|
-
<SlimSelectReact
|
|
447
|
-
value={value}
|
|
448
|
-
onChange={(val) => setValue(val as string[])}
|
|
449
|
-
multiple
|
|
450
|
-
data={[
|
|
451
|
-
{ value: '1', text: 'Option 1' },
|
|
452
|
-
{ value: '2', text: 'Option 2' }
|
|
453
|
-
]}
|
|
454
|
-
/>
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const { container } = render(<TestComponent />)
|
|
459
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
460
|
-
|
|
461
|
-
// Should not throw errors
|
|
462
|
-
expect(container.querySelector('select')).toBeTruthy()
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
test('updates onChange for multiple select when invalid values are provided', async () => {
|
|
466
|
-
const onChangeMock = vi.fn()
|
|
467
|
-
|
|
468
|
-
const TestComponent = () => {
|
|
469
|
-
return (
|
|
470
|
-
<SlimSelectReact
|
|
471
|
-
value={['banana', 'apple']}
|
|
472
|
-
onChange={onChangeMock}
|
|
473
|
-
multiple
|
|
474
|
-
data={[
|
|
475
|
-
{ value: '1', text: 'Option 1' },
|
|
476
|
-
{ value: '2', text: 'Option 2' },
|
|
477
|
-
{ value: '3', text: 'Option 3' }
|
|
478
|
-
]}
|
|
479
|
-
/>
|
|
480
|
-
)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const { container } = render(<TestComponent />)
|
|
484
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
485
|
-
|
|
486
|
-
const select = container.querySelector('select')
|
|
487
|
-
const slimInstance = (select as any)?._slimSelect as SlimSelect
|
|
488
|
-
if (slimInstance) {
|
|
489
|
-
// When user manually selects valid options, onChange should update
|
|
490
|
-
slimInstance.setSelected(['1', '2'])
|
|
491
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
492
|
-
|
|
493
|
-
// Verify onChange has been called with the valid values
|
|
494
|
-
expect(onChangeMock).toHaveBeenCalledWith(['1', '2'])
|
|
495
|
-
}
|
|
496
|
-
})
|
|
497
|
-
})
|
|
498
|
-
})
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
|
|
2
|
-
import SlimSelectCore from '../index'
|
|
3
|
-
import type { Config, Option, Optgroup } from '../index'
|
|
4
|
-
|
|
5
|
-
export interface SlimSelectProps {
|
|
6
|
-
data?: (Partial<Option> | Partial<Optgroup>)[]
|
|
7
|
-
settings?: Config['settings']
|
|
8
|
-
events?: Config['events']
|
|
9
|
-
cssClasses?: Config['cssClasses']
|
|
10
|
-
value?: string | string[]
|
|
11
|
-
onChange?: (value: string | string[]) => void
|
|
12
|
-
children?: React.ReactNode
|
|
13
|
-
multiple?: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface SlimSelectRef {
|
|
17
|
-
slimSelect: SlimSelectCore | null
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const SlimSelect = forwardRef<SlimSelectRef, SlimSelectProps>(
|
|
21
|
-
({ data, settings, events, cssClasses, value, onChange, children, multiple }, ref) => {
|
|
22
|
-
const selectRef = useRef<HTMLSelectElement>(null)
|
|
23
|
-
const slimSelectRef = useRef<SlimSelectCore | null>(null)
|
|
24
|
-
const isInitialMount = useRef(true)
|
|
25
|
-
const currentValueRef = useRef<string | string[] | undefined>(value)
|
|
26
|
-
|
|
27
|
-
// Expose SlimSelect instance to parent via ref
|
|
28
|
-
useImperativeHandle(ref, () => ({
|
|
29
|
-
slimSelect: slimSelectRef.current
|
|
30
|
-
}))
|
|
31
|
-
|
|
32
|
-
// Helper to get clean value (similar to Vue's getCleanValue)
|
|
33
|
-
const getCleanValue = (val: string | string[] | undefined): string | string[] => {
|
|
34
|
-
// If its multiple and the value is a string, return an array with the string
|
|
35
|
-
if (typeof val === 'string') {
|
|
36
|
-
return multiple ? [val] : val
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// If its not multiple and the value is an array, return the first item
|
|
40
|
-
if (Array.isArray(val)) {
|
|
41
|
-
return multiple ? val : val[0]
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return multiple ? [] : ''
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Helper to sync value to SlimSelect (similar to Vue's syncModelValueToSlimSelect)
|
|
48
|
-
const syncValueToSlimSelect = (val: string | string[] | undefined, runAfterChange: boolean = false) => {
|
|
49
|
-
if (!slimSelectRef.current) return
|
|
50
|
-
|
|
51
|
-
// Only sync if value is explicitly set (not undefined)
|
|
52
|
-
// This prevents adding placeholders when value is not provided
|
|
53
|
-
if (val === undefined) {
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const cleanValue = getCleanValue(val)
|
|
58
|
-
const data = slimSelectRef.current.getData()
|
|
59
|
-
// Extract options from data (flatten optgroups if any)
|
|
60
|
-
const options = data.flatMap((item: Option | Optgroup) => ('label' in item ? item.options : [item])) as Option[]
|
|
61
|
-
|
|
62
|
-
// Check if the value exists in options
|
|
63
|
-
const valueExists = Array.isArray(cleanValue)
|
|
64
|
-
? cleanValue.length > 0 && cleanValue.every((val) => options.some((opt) => opt.value === val))
|
|
65
|
-
: cleanValue !== '' && options.some((opt) => opt.value === cleanValue)
|
|
66
|
-
|
|
67
|
-
// If value doesn't exist in options, add a placeholder option
|
|
68
|
-
if (!valueExists) {
|
|
69
|
-
// If its not multiple, add a placeholder option
|
|
70
|
-
if (!Array.isArray(cleanValue)) {
|
|
71
|
-
// For single select, check if placeholder already exists
|
|
72
|
-
const hasPlaceholder = options.some((opt) => opt.placeholder)
|
|
73
|
-
if (!hasPlaceholder) {
|
|
74
|
-
// Get current data and prepend placeholder option
|
|
75
|
-
const currentData = slimSelectRef.current.getData()
|
|
76
|
-
const placeholderOption: Partial<Option> = {
|
|
77
|
-
value: '',
|
|
78
|
-
text: '',
|
|
79
|
-
placeholder: true
|
|
80
|
-
}
|
|
81
|
-
slimSelectRef.current.setData([placeholderOption].concat(currentData))
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Set the selected value (will select placeholder if it was just added)
|
|
87
|
-
// Match Vue component exactly - call setSelected with cleanValue directly
|
|
88
|
-
slimSelectRef.current.setSelected(cleanValue, runAfterChange)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Initialize SlimSelect
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (!selectRef.current) return
|
|
94
|
-
|
|
95
|
-
const config: Config = {
|
|
96
|
-
select: selectRef.current
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (data) {
|
|
100
|
-
config.data = data
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (settings) {
|
|
104
|
-
config.settings = settings
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (cssClasses) {
|
|
108
|
-
config.cssClasses = cssClasses
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Wrap onChange in afterChange event
|
|
112
|
-
const ogAfterChange = events?.afterChange
|
|
113
|
-
config.events = {
|
|
114
|
-
...events,
|
|
115
|
-
afterChange: (newVal) => {
|
|
116
|
-
if (!slimSelectRef.current) return
|
|
117
|
-
|
|
118
|
-
const newValue = multiple ? newVal.map((option) => option.value) : (newVal[0]?.value ?? '')
|
|
119
|
-
const slimData = slimSelectRef.current.getData()
|
|
120
|
-
const options = slimData.flatMap((item: Option | Optgroup) =>
|
|
121
|
-
'label' in item ? item.options : [item]
|
|
122
|
-
) as Option[]
|
|
123
|
-
|
|
124
|
-
// Check if the current value exists in options
|
|
125
|
-
const currentValue = currentValueRef.current
|
|
126
|
-
const currentValueExists =
|
|
127
|
-
currentValue === undefined
|
|
128
|
-
? false
|
|
129
|
-
: Array.isArray(currentValue)
|
|
130
|
-
? currentValue.length > 0 && currentValue.every((val) => options.some((opt) => opt.value === val))
|
|
131
|
-
: currentValue !== '' && options.some((opt) => opt.value === currentValue)
|
|
132
|
-
|
|
133
|
-
// Check if the new value is valid (exists in options)
|
|
134
|
-
const newValueIsValid = Array.isArray(newValue)
|
|
135
|
-
? newValue.length > 0 && newValue.every((val) => options.some((opt) => opt.value === val))
|
|
136
|
-
: newValue !== '' && options.some((opt) => opt.value === newValue)
|
|
137
|
-
|
|
138
|
-
// Check if value actually changed (properly compare arrays)
|
|
139
|
-
const valueChanged =
|
|
140
|
-
Array.isArray(newValue) && Array.isArray(currentValue)
|
|
141
|
-
? JSON.stringify(newValue.sort()) !== JSON.stringify(currentValue.sort())
|
|
142
|
-
: currentValue !== newValue
|
|
143
|
-
|
|
144
|
-
// Only call onChange if:
|
|
145
|
-
// 1. The value actually changed, AND
|
|
146
|
-
// 2. Either the current value exists in options, OR we're setting to a valid non-empty value
|
|
147
|
-
// This prevents clearing invalid values when we show placeholder
|
|
148
|
-
if (onChange && valueChanged && (currentValueExists || newValueIsValid)) {
|
|
149
|
-
onChange(newValue)
|
|
150
|
-
currentValueRef.current = newValue
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (ogAfterChange) {
|
|
154
|
-
ogAfterChange(newVal)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
slimSelectRef.current = new SlimSelectCore(config)
|
|
160
|
-
|
|
161
|
-
// Sync initial value if provided
|
|
162
|
-
if (value !== undefined) {
|
|
163
|
-
syncValueToSlimSelect(value, false)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
if (slimSelectRef.current) {
|
|
168
|
-
slimSelectRef.current.destroy()
|
|
169
|
-
slimSelectRef.current = null
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}, []) // Only run on mount/unmount
|
|
173
|
-
|
|
174
|
-
// Handle value changes from parent
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
if (isInitialMount.current) {
|
|
177
|
-
isInitialMount.current = false
|
|
178
|
-
currentValueRef.current = value
|
|
179
|
-
return
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (slimSelectRef.current && value !== undefined) {
|
|
183
|
-
currentValueRef.current = value
|
|
184
|
-
syncValueToSlimSelect(value, false)
|
|
185
|
-
}
|
|
186
|
-
}, [value])
|
|
187
|
-
|
|
188
|
-
// Handle data changes
|
|
189
|
-
useEffect(() => {
|
|
190
|
-
if (slimSelectRef.current && data && !isInitialMount.current) {
|
|
191
|
-
slimSelectRef.current.setData(data)
|
|
192
|
-
// Re-sync value after data changes
|
|
193
|
-
if (value !== undefined) {
|
|
194
|
-
syncValueToSlimSelect(value, false)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}, [data])
|
|
198
|
-
|
|
199
|
-
return (
|
|
200
|
-
<select ref={selectRef} multiple={multiple}>
|
|
201
|
-
{children}
|
|
202
|
-
</select>
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
SlimSelect.displayName = 'SlimSelect'
|
|
208
|
-
|
|
209
|
-
export default SlimSelect
|