slim-select 2.13.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +184 -43
  2. package/dist/index.d.ts +304 -52
  3. package/dist/react/index.d.ts +304 -0
  4. package/dist/react/index.js +1549 -0
  5. package/dist/react/index.umd.js +22 -0
  6. package/dist/slimselect.cjs.js +1 -2030
  7. package/dist/slimselect.css +1 -1
  8. package/dist/slimselect.css.map +1 -1
  9. package/dist/slimselect.es.js +1224 -2012
  10. package/dist/slimselect.umd.js +1 -2036
  11. package/dist/vue/index.d.ts +304 -0
  12. package/dist/vue/index.js +1341 -0
  13. package/dist/vue/index.umd.js +1 -0
  14. package/index.html +1 -0
  15. package/package.json +59 -37
  16. package/release.js +0 -7
  17. package/src/slim-select/accessibility.test.ts +782 -0
  18. package/src/slim-select/classes.test.ts +7 -6
  19. package/src/slim-select/classes.ts +8 -6
  20. package/src/slim-select/helpers.test.ts +10 -8
  21. package/src/slim-select/index.test.ts +509 -14
  22. package/src/slim-select/index.ts +58 -31
  23. package/src/slim-select/react/index.tsx +9 -0
  24. package/src/slim-select/react/react.tsx +112 -0
  25. package/src/slim-select/render.test.ts +479 -119
  26. package/src/slim-select/render.ts +213 -91
  27. package/src/slim-select/select.test.ts +28 -10
  28. package/src/slim-select/select.ts +35 -12
  29. package/src/slim-select/settings.test.ts +19 -8
  30. package/src/slim-select/settings.ts +2 -4
  31. package/src/slim-select/slimselect.scss +30 -15
  32. package/src/slim-select/store.test.ts +13 -16
  33. package/src/slim-select/store.ts +97 -86
  34. package/src/slim-select/vue/index.ts +9 -0
  35. package/src/slim-select/vue/vue.test.ts +832 -0
  36. package/src/slim-select/{vue.vue → vue/vue.vue} +45 -23
  37. package/tsconfig.json +6 -4
  38. package/{vite.config.mts → vite.config.docs.mts} +10 -0
  39. package/vite.config.lib.mts +42 -0
  40. package/vite.config.react.mts +40 -0
  41. package/vite.config.vue.mts +39 -0
  42. package/vitest.config.ts +29 -0
  43. package/dist/classes.d.ts +0 -43
  44. package/dist/helpers.d.ts +0 -5
  45. package/dist/render.d.ts +0 -81
  46. package/dist/select.d.ts +0 -30
  47. package/dist/settings.d.ts +0 -35
  48. package/dist/slimselect.global.js +0 -2033
  49. package/dist/slimselect.js +0 -2036
  50. package/dist/slimselect.min.js +0 -1
  51. package/dist/slimselect.umd.min.js +0 -1
  52. package/dist/store.d.ts +0 -84
  53. package/jest.config.js +0 -14
  54. package/src/slim-select/rollup.config.mjs +0 -127
  55. package/src/slim-select/tsconfig.json +0 -14
@@ -1,5 +1,5 @@
1
1
  import { kebabCase } from './helpers'
2
- import { DataArray, DataArrayPartial, Optgroup, OptgroupOptional, Option } from './store'
2
+ import { Optgroup, Option } from './store'
3
3
 
4
4
  export default class Select {
5
5
  public select: HTMLSelectElement
@@ -8,7 +8,7 @@ export default class Select {
8
8
  public onValueChange?: (value: Option[]) => void
9
9
  public onClassChange?: (classes: string[]) => void
10
10
  public onDisabledChange?: (disabled: boolean) => void
11
- public onOptionsChange?: (data: DataArrayPartial) => void
11
+ public onOptionsChange?: (data: (Option | Optgroup)[]) => void
12
12
 
13
13
  // Change observers
14
14
  public listen: boolean = false
@@ -41,13 +41,36 @@ export default class Select {
41
41
 
42
42
  public hideUI(): void {
43
43
  this.select.tabIndex = -1
44
- this.select.style.display = 'none'
44
+ // Visually hide but keep focusable for form validation
45
+ // Use 1px dimensions so browser validation popup can display
46
+ this.select.style.position = 'absolute'
47
+ this.select.style.width = '1px'
48
+ this.select.style.height = '1px'
49
+ this.select.style.opacity = '0'
50
+ this.select.style.overflow = 'hidden'
51
+ this.select.style.pointerEvents = 'none'
52
+ // Remove any spacing or borders that could affect layout
53
+ this.select.style.margin = '0'
54
+ this.select.style.padding = '0'
55
+ this.select.style.borderWidth = '0'
56
+ // Clip to completely hide the 1px
57
+ this.select.style.clip = 'rect(0 0 0 0)'
45
58
  this.select.setAttribute('aria-hidden', 'true')
46
59
  }
47
60
 
48
61
  public showUI(): void {
49
62
  this.select.removeAttribute('tabindex')
50
- this.select.style.display = ''
63
+ // Reset visual hiding styles
64
+ this.select.style.position = ''
65
+ this.select.style.width = ''
66
+ this.select.style.height = ''
67
+ this.select.style.opacity = ''
68
+ this.select.style.overflow = ''
69
+ this.select.style.pointerEvents = ''
70
+ this.select.style.margin = ''
71
+ this.select.style.padding = ''
72
+ this.select.style.borderWidth = ''
73
+ this.select.style.clip = ''
51
74
  this.select.removeAttribute('aria-hidden')
52
75
  }
53
76
 
@@ -109,7 +132,7 @@ export default class Select {
109
132
  }
110
133
 
111
134
  if (m.type === 'childList') {
112
- for (const n of m.addedNodes) {
135
+ for (const n of Array.from(m.addedNodes)) {
113
136
  if (n.nodeName === 'OPTION' && (<HTMLOptionElement>n).value === this.select.value) {
114
137
  // we added a new option that's now the select value
115
138
  this.select.dispatchEvent(new Event('change'))
@@ -149,8 +172,8 @@ export default class Select {
149
172
  }
150
173
 
151
174
  // From the select element pull optgroup and options into data
152
- public getData(): DataArrayPartial {
153
- let data = [] as DataArrayPartial
175
+ public getData(): (Option | Optgroup)[] {
176
+ let data = []
154
177
 
155
178
  // Loop through nodes and get data
156
179
  const nodes = this.select.childNodes as any as HTMLOptGroupElement[] | HTMLOptionElement[]
@@ -169,7 +192,7 @@ export default class Select {
169
192
  return data
170
193
  }
171
194
 
172
- public getDataFromOptgroup(optgroup: HTMLOptGroupElement): OptgroupOptional {
195
+ public getDataFromOptgroup(optgroup: HTMLOptGroupElement): Optgroup {
173
196
  let data = {
174
197
  id: optgroup.id,
175
198
  label: optgroup.label,
@@ -177,7 +200,7 @@ export default class Select {
177
200
  selectAllText: optgroup.dataset ? optgroup.dataset.selectalltext : 'Select all',
178
201
  closable: optgroup.dataset ? optgroup.dataset.closable : 'off',
179
202
  options: []
180
- } as OptgroupOptional
203
+ } as Optgroup
181
204
 
182
205
  const options = optgroup.childNodes as any as HTMLOptionElement[]
183
206
  for (const o of options) {
@@ -329,7 +352,7 @@ export default class Select {
329
352
  this.changeListen(true)
330
353
  }
331
354
 
332
- public updateOptions(data: DataArray): void {
355
+ public updateOptions(data: (Option | Optgroup)[]): void {
333
356
  // Stop listening to changes
334
357
  this.changeListen(false)
335
358
 
@@ -364,8 +387,8 @@ export default class Select {
364
387
  optgroupEl.dataset.closable = optgroup.closable
365
388
  }
366
389
  if (optgroup.options) {
367
- for (const o of optgroup.options) {
368
- optgroupEl.appendChild(this.createOption(o))
390
+ for (const option of optgroup.options) {
391
+ optgroupEl.appendChild(this.createOption(option as Option))
369
392
  }
370
393
  }
371
394
  return optgroupEl
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- import { describe, expect, test } from '@jest/globals'
3
+ import { describe, expect, test, beforeEach } from 'vitest'
4
4
  import Settings from './settings'
5
5
  import Select from './select'
6
6
  import { Option, Optgroup } from './store'
@@ -109,12 +109,22 @@ describe('Settings module', () => {
109
109
  test('showUI', () => {
110
110
  select.select.setAttribute('tabindex', '1')
111
111
  select.select.setAttribute('aria-hidden', 'true')
112
- select.select.style.display = 'none'
112
+ select.select.style.position = 'absolute'
113
+ select.select.style.width = '0'
114
+ select.select.style.opacity = '0'
115
+ select.select.style.margin = '0'
116
+ select.select.style.padding = '0'
117
+ select.select.style.borderWidth = '0'
113
118
 
114
119
  select.showUI()
115
120
  expect(select.select.getAttribute('tabindex')).toBeFalsy()
116
121
  expect(select.select.getAttribute('aria-hidden')).toBeFalsy()
117
- expect(select.select.style.display).toBeFalsy()
122
+ expect(select.select.style.position).toBeFalsy()
123
+ expect(select.select.style.width).toBeFalsy()
124
+ expect(select.select.style.opacity).toBeFalsy()
125
+ expect(select.select.style.margin).toBeFalsy()
126
+ expect(select.select.style.padding).toBeFalsy()
127
+ // borderWidth gets reset
118
128
  })
119
129
 
120
130
  describe('createOptgroup', () => {
@@ -197,12 +207,13 @@ describe('Settings module', () => {
197
207
 
198
208
  test('malicious text is inserted with innerText', () => {
199
209
  // decoded text: <img src=x onerror=alert(1)></img>
200
- const str = '&#x3c;&#x69;&#x6d;&#x67;&#x20;&#x73;&#x72;&#x63;&#x3d;&#x78;&#x20;&#x6f;&#x6e;&#x65;&#x72;&#x72;&#x6f;&#x72;&#x3d;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3e;&#x3c;&#x2f;&#x69;&#x6d;&#x67;&#x3e;'
210
+ const str =
211
+ '&#x3c;&#x69;&#x6d;&#x67;&#x20;&#x73;&#x72;&#x63;&#x3d;&#x78;&#x20;&#x6f;&#x6e;&#x65;&#x72;&#x72;&#x6f;&#x72;&#x3d;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x3e;&#x3c;&#x2f;&#x69;&#x6d;&#x67;&#x3e;'
201
212
  // const str = 'opt'
202
- const decode = (string: string|null) => {
203
- if(string === null) return ''
204
- const doc = new DOMParser().parseFromString(string, "text/html")
205
- return doc.documentElement.textContent;
213
+ const decode = (string: string | null) => {
214
+ if (string === null) return ''
215
+ const doc = new DOMParser().parseFromString(string, 'text/html')
216
+ return doc.documentElement.textContent
206
217
  }
207
218
  const option = new Option({ text: str })
208
219
  const optionElement = select.createOption(option)
@@ -1,7 +1,5 @@
1
1
  import { generateID } from './helpers'
2
2
 
3
- export type SettingsPartial = Partial<Settings>
4
-
5
3
  export default class Settings {
6
4
  public id: string = '' // Primary ID for the select
7
5
  public style: string = '' // Style attribute from the select element
@@ -11,7 +9,7 @@ export default class Settings {
11
9
  public isMultiple: boolean = false
12
10
  public isOpen: boolean = false
13
11
  public isFullOpen: boolean = false
14
- public intervalMove: NodeJS.Timeout | null = null
12
+ public intervalMove: ReturnType<typeof setInterval> | null = null
15
13
 
16
14
  // Fields set from constructor
17
15
  public disabled: boolean
@@ -39,7 +37,7 @@ export default class Settings {
39
37
  public maxValuesMessage: string
40
38
  public addableText: string
41
39
 
42
- constructor(settings?: SettingsPartial) {
40
+ constructor(settings?: Partial<Settings>) {
43
41
  if (!settings) {
44
42
  settings = {}
45
43
  }
@@ -92,11 +92,11 @@
92
92
  }
93
93
  }
94
94
 
95
- &.ss-open-above {
95
+ &.ss-dir-above {
96
96
  border-top-left-radius: 0px;
97
97
  border-top-right-radius: 0px;
98
98
  }
99
- &.ss-open-below {
99
+ &.ss-dir-below {
100
100
  border-bottom-left-radius: 0px;
101
101
  border-bottom-right-radius: 0px;
102
102
  }
@@ -106,14 +106,17 @@
106
106
  flex-wrap: wrap;
107
107
  gap: var(--ss-spacing-m);
108
108
  flex: 1 1 100%;
109
+ min-width: 0; // Allow container to shrink
110
+ max-width: 100%; // Ensure container doesn't exceed parent
111
+ overflow: hidden; // Prevent overflow
109
112
 
110
113
  .ss-placeholder {
111
- display: flex;
114
+ display: block; // Use block for proper ellipsis
115
+ width: 100%;
116
+ min-width: 0; // Allow placeholder to shrink
112
117
  padding: var(--ss-spacing-s) var(--ss-spacing-m) var(--ss-spacing-s) var(--ss-spacing-m);
113
118
  margin: auto 0px auto 0px;
114
119
  line-height: 1em;
115
- align-items: center;
116
- width: 100%;
117
120
  color: var(--ss-font-placeholder-color);
118
121
  overflow: hidden;
119
122
  text-overflow: ellipsis;
@@ -136,8 +139,14 @@
136
139
 
137
140
  // Used for single select selected value
138
141
  .ss-single {
139
- display: flex;
142
+ display: block; // Use block for proper ellipsis
140
143
  margin: auto 0px auto var(--ss-spacing-s);
144
+ min-width: 0; // Allow flex item to shrink below content size
145
+ overflow: hidden;
146
+ text-overflow: ellipsis;
147
+ white-space: nowrap; // Prevent text wrapping
148
+ flex: 1 1 auto; // Allow it to grow but also shrink
149
+ max-width: calc(100% - 20px); // Leave space for arrow and padding
141
150
  }
142
151
 
143
152
  // Used for the selected values
@@ -146,6 +155,7 @@
146
155
  user-select: none;
147
156
  align-items: center;
148
157
  width: fit-content;
158
+ max-width: 100%; // Ensure value doesn't exceed container width
149
159
  background-color: var(--ss-primary-color);
150
160
  border-radius: var(--ss-border-radius);
151
161
  animation-name: ss-valueIn;
@@ -160,10 +170,15 @@
160
170
  }
161
171
 
162
172
  .ss-value-text {
173
+ flex: 1; // Take up available space in flex container
174
+ min-width: 0; // Allow text to shrink
163
175
  font-size: 12px;
164
- color: var(--ss-bg-color);
165
176
  line-height: 1;
177
+ color: var(--ss-bg-color);
166
178
  padding: var(--ss-spacing-s) var(--ss-spacing-m);
179
+ overflow: hidden;
180
+ text-overflow: ellipsis;
181
+ white-space: nowrap;
167
182
  }
168
183
 
169
184
  .ss-value-delete {
@@ -252,7 +267,6 @@
252
267
  opacity var(--ss-animation-timing);
253
268
  opacity: 0;
254
269
  transform: scaleY(0);
255
- transform-origin: center top;
256
270
  overflow: hidden;
257
271
  z-index: 10000;
258
272
 
@@ -265,23 +279,24 @@
265
279
  position: fixed;
266
280
  }
267
281
 
268
- &.ss-open-above {
269
- flex-direction: column-reverse;
270
- opacity: 1;
271
- transform: scaleY(1);
282
+ &.ss-dir-above {
272
283
  transform-origin: center bottom;
284
+ flex-direction: column-reverse;
273
285
  border-top-left-radius: var(--ss-border-radius);
274
286
  border-top-right-radius: var(--ss-border-radius);
275
287
  }
276
288
 
277
- &.ss-open-below {
278
- opacity: 1;
279
- transform: scaleY(1);
289
+ &.ss-dir-below {
280
290
  transform-origin: center top;
281
291
  border-bottom-left-radius: var(--ss-border-radius);
282
292
  border-bottom-right-radius: var(--ss-border-radius);
283
293
  }
284
294
 
295
+ &.ss-open {
296
+ opacity: 1;
297
+ transform: scaleY(1);
298
+ }
299
+
285
300
  .ss-search {
286
301
  flex: 0 1 auto;
287
302
  display: flex;
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- import { describe, expect, test } from '@jest/globals'
4
- import Store, { DataArray, DataObjectPartial, Optgroup, Option } from './store'
3
+ import { describe, expect, test, beforeAll, beforeEach } from 'vitest'
4
+ import Store, { Optgroup, Option } from './store'
5
5
 
6
6
  describe('store module', () => {
7
7
  describe('constructor', () => {
@@ -208,17 +208,17 @@ describe('store module', () => {
208
208
  })
209
209
 
210
210
  test('invalid data returns error', () => {
211
- const invalidData = { test: true }
211
+ const invalidData = { test: true } as any
212
212
 
213
- const res = store.validateDataArray(invalidData as unknown as DataArray)
213
+ const res = store.validateDataArray(invalidData)
214
214
  expect(res).toBeInstanceOf(Error)
215
215
  expect(res?.message).toBe('Data must be an array')
216
216
  })
217
217
 
218
218
  test('single invalid data entry returns error', () => {
219
- const invalidData = [{ name: 'this is invalid' }]
219
+ const invalidData = [{ name: 'this is invalid' }] as any
220
220
 
221
- const res = store.validateDataArray(invalidData as unknown as DataArray)
221
+ const res = store.validateDataArray(invalidData)
222
222
  expect(res).toBeInstanceOf(Error)
223
223
  expect(res?.message).toBe('Data object must be a valid optgroup or option')
224
224
  })
@@ -231,9 +231,9 @@ describe('store module', () => {
231
231
  {
232
232
  text: 'valid option'
233
233
  }
234
- ]
234
+ ] as any
235
235
 
236
- const res = store.validateDataArray(invalidData as unknown as DataArray)
236
+ const res = store.validateDataArray(invalidData)
237
237
  expect(res).toBeInstanceOf(Error)
238
238
  expect(res?.message).toBe('Data object must be a valid optgroup or option')
239
239
  })
@@ -246,9 +246,9 @@ describe('store module', () => {
246
246
  {
247
247
  name: 'this is invalid'
248
248
  }
249
- ]
249
+ ] as any
250
250
 
251
- const res = store.validateDataArray(invalidData as unknown as DataArray)
251
+ const res = store.validateDataArray(invalidData)
252
252
  expect(res).toBeInstanceOf(Error)
253
253
  expect(res?.message).toBe('Data object must be a valid optgroup or option')
254
254
  })
@@ -263,9 +263,9 @@ describe('store module', () => {
263
263
  }
264
264
  ]
265
265
  }
266
- ]
266
+ ] as any
267
267
 
268
- const res = store.validateDataArray(invalidData as unknown as DataArray)
268
+ const res = store.validateDataArray(invalidData)
269
269
  expect(res).toBeInstanceOf(Error)
270
270
  expect(res?.message).toBe('Option must have a text')
271
271
  })
@@ -389,10 +389,7 @@ describe('store module', () => {
389
389
  })
390
390
 
391
391
  test("invalid data get's ignored", () => {
392
- const res = store.partialToFullData([
393
- { error: 'this is invalid' } as unknown as DataObjectPartial,
394
- { text: 'valid' }
395
- ])
392
+ const res = store.partialToFullData([{ error: 'this is invalid' } as any, { text: 'valid' }])
396
393
  expect(res).toBeInstanceOf(Array)
397
394
  expect(res).toHaveLength(1)
398
395