slim-select 2.3.3 → 2.4.1

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/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "slim-select",
3
3
  "description": "Slim advanced select dropdown",
4
- "version": "2.3.3",
4
+ "version": "2.4.1",
5
5
  "author": "Brian Voelker <brian@webiswhatido.com> (http://webiswhatido.com)",
6
6
  "homepage": "https://slimselectjs.com",
7
7
  "license": "MIT",
8
8
  "bugs": {
9
9
  "url": "https://github.com/brianvoe/slim-select/issues"
10
10
  },
11
+ "type": "module",
11
12
  "main": "dist/slimselect.ssr.js",
12
13
  "browser": "dist/slimselect.umd.js",
13
14
  "module": "dist/slimselect.es.js",
@@ -44,31 +45,31 @@
44
45
  "devDependencies": {
45
46
  "@jest/globals": "^29.3.1",
46
47
  "@rollup/plugin-babel": "^6.0.3",
47
- "@rollup/plugin-commonjs": "^23.0.3",
48
+ "@rollup/plugin-commonjs": "^24.0.0",
48
49
  "@rollup/plugin-node-resolve": "^15.0.1",
49
- "@rollup/plugin-terser": "^0.1.0",
50
- "@rollup/plugin-typescript": "^9.0.2",
50
+ "@rollup/plugin-terser": "^0.3.0",
51
+ "@rollup/plugin-typescript": "^11.0.0",
51
52
  "@slim-select/vue": "workspace:src/vue",
52
53
  "@types/downloadjs": "^1.4.3",
53
- "@vitejs/plugin-vue": "^3.2.0",
54
+ "@vitejs/plugin-vue": "^4.0.0",
54
55
  "clipboard": "^2.0.11",
55
56
  "downloadjs": "^1.4.7",
56
57
  "jest": "^29.3.1",
57
58
  "jest-environment-jsdom": "^29.3.1",
58
- "prettier": "^2.8.1",
59
+ "prettier": "^2.8.2",
59
60
  "prismjs": "^1.29.0",
60
- "rimraf": "^3.0.2",
61
- "rollup": "^2.79.1",
61
+ "rimraf": "^4.0.4",
62
+ "rollup": "^3.10.0",
62
63
  "rollup-plugin-typescript2": "^0.34.1",
63
64
  "rollup-plugin-vue": "^6.0.0",
64
- "sass": "^1.56.1",
65
- "ts-jest": "^29.0.3",
65
+ "sass": "^1.57.1",
66
+ "ts-jest": "^29.0.5",
66
67
  "tslib": "^2.4.1",
67
- "typescript": "^4.9.3",
68
- "vite": "^3.2.5",
68
+ "typescript": "^4.9.4",
69
+ "vite": "^4.0.4",
69
70
  "vue": "^3.2.45",
70
71
  "vue-router": "^4.1.6",
71
- "vue-tsc": "^1.0.11",
72
+ "vue-tsc": "^1.0.24",
72
73
  "vuex": "^4.0.2"
73
74
  }
74
75
  }
@@ -31,9 +31,10 @@ export function hasClassInTree(element: HTMLElement, className: string) {
31
31
  return hasClass(element, className) || parentByClass(element, className)
32
32
  }
33
33
 
34
- export function debounce(func: (...params: any[]) => void, wait = 50, immediate = false): () => void {
34
+ // debounce will call the last requested function after the wait time
35
+ export function debounce<T extends (...args: any[]) => void>(func: T, wait = 50, immediate = false): () => void {
35
36
  let timeout: any
36
- return function (this: any, ...args: any[]) {
37
+ return function (this: any, ...args: any[]): void {
37
38
  const context = self
38
39
  const later = () => {
39
40
  timeout = null
@@ -50,6 +51,15 @@ export function debounce(func: (...params: any[]) => void, wait = 50, immediate
50
51
  }
51
52
  }
52
53
 
54
+ // reverseDebounce will call the function on the first call and then debounce
55
+ function reverseDebounce<T extends (...args: any[]) => void>(func: T, timeout: number): T {
56
+ let timer: NodeJS.Timeout | null = null
57
+ return function (...args: any[]): void {
58
+ if (!timer) func(...args)
59
+ timer = setTimeout(() => (timer = null), timeout)
60
+ } as T
61
+ }
62
+
53
63
  export function isEqual(a: any, b: any) {
54
64
  return JSON.stringify(a) === JSON.stringify(b)
55
65
  }
@@ -153,6 +153,16 @@ export default class SlimSelect {
153
153
  this.render.renderValues()
154
154
  this.render.renderOptions(this.store.getData())
155
155
 
156
+ // Add aria-label or aria-labelledby if exists
157
+ const selectAriaLabel = this.selectEl.getAttribute('aria-label')
158
+ const selectAriaLabelledBy = this.selectEl.getAttribute('aria-labelledby')
159
+
160
+ if (selectAriaLabel) {
161
+ this.render.main.main.setAttribute('aria-label', selectAriaLabel)
162
+ } else if (selectAriaLabelledBy) {
163
+ this.render.main.main.setAttribute('aria-labelledby', selectAriaLabelledBy)
164
+ }
165
+
156
166
  // Add render after original select element
157
167
  if (this.selectEl.parentNode) {
158
168
  this.selectEl.parentNode.insertBefore(this.render.main.main, this.selectEl.nextSibling)
@@ -309,9 +319,10 @@ export default class SlimSelect {
309
319
 
310
320
  // Focus on input field only if search is enabled
311
321
  if (this.settings.showSearch) {
312
- this.render.searchFocus(false)
322
+ this.render.searchFocus()
313
323
  }
314
324
 
325
+ this.settings.isOpen = true
315
326
  // setTimeout is for animation completion
316
327
  setTimeout(() => {
317
328
  // Run afterOpen callback
@@ -320,7 +331,11 @@ export default class SlimSelect {
320
331
  }
321
332
 
322
333
  // Update settings
323
- this.settings.isOpen = true
334
+ // Prevent overide if user close fast without wait full open
335
+ // For detail see issue https://github.com/brianvoe/slim-select/issues/397
336
+ if (this.settings.isOpen) {
337
+ this.settings.isFullOpen = true
338
+ }
324
339
  }, this.settings.timeoutDelay)
325
340
 
326
341
  // Start an interval to check if main has moved
@@ -333,7 +348,7 @@ export default class SlimSelect {
333
348
  }
334
349
  }
335
350
 
336
- public close(): void {
351
+ public close(eventType: string | null = null): void {
337
352
  // Dont do anything if the content is already closed
338
353
  // Dont do anything if alwaysOpen is true
339
354
  if (!this.settings.isOpen || this.settings.alwaysOpen) {
@@ -354,17 +369,17 @@ export default class SlimSelect {
354
369
  }
355
370
 
356
371
  // If we arent tabbing focus back on the main element
357
- this.render.mainFocus(false)
372
+ this.render.mainFocus(eventType)
358
373
 
374
+ // Update settings
375
+ this.settings.isOpen = false
376
+ this.settings.isFullOpen = false
359
377
  // Reset the content below
360
378
  setTimeout(() => {
361
379
  // Run afterClose callback
362
380
  if (this.events.afterClose) {
363
381
  this.events.afterClose()
364
382
  }
365
-
366
- // Update settings
367
- this.settings.isOpen = false
368
383
  }, this.settings.timeoutDelay)
369
384
 
370
385
  if (this.settings.intervalMove) {
@@ -437,7 +452,7 @@ export default class SlimSelect {
437
452
  }
438
453
 
439
454
  private windowResize: (e: Event) => void = debounce(() => {
440
- if (!this.settings.isOpen) {
455
+ if (!this.settings.isOpen && !this.settings.isFullOpen) {
441
456
  return
442
457
  }
443
458
 
@@ -447,7 +462,7 @@ export default class SlimSelect {
447
462
  // Event listener for window scrolling
448
463
  private windowScroll: (e: Event) => void = debounce(() => {
449
464
  // If the content is not open, there is no need to move it
450
- if (!this.settings.isOpen) {
465
+ if (!this.settings.isOpen && !this.settings.isFullOpen) {
451
466
  return
452
467
  }
453
468
 
@@ -463,19 +478,14 @@ export default class SlimSelect {
463
478
 
464
479
  // Check if the click was on the content by looking at the parents
465
480
  if (e.target && !hasClassInTree(e.target as HTMLElement, this.settings.id)) {
466
- this.close()
481
+ this.close(e.type)
467
482
  }
468
483
  }
469
484
 
470
485
  // Event Listener for window visibility change
471
486
  private windowVisibilityChange: (e: Event) => void = () => {
472
487
  if (document.hidden) {
473
- this.settings.isWindowFocused = false
474
488
  this.close()
475
- } else {
476
- setTimeout(() => {
477
- this.settings.isWindowFocused = true
478
- }, 20)
479
489
  }
480
490
  }
481
491
  }
@@ -127,6 +127,7 @@ export default class Render {
127
127
 
128
128
  // Add classes and styles to main/content
129
129
  this.updateClassStyles()
130
+ this.updateAriaAttributes()
130
131
 
131
132
  // Add content to the content location settings
132
133
  this.settings.contentLocation.appendChild(this.content.main)
@@ -155,6 +156,7 @@ export default class Render {
155
156
 
156
157
  // Add class to main container
157
158
  this.main.main.classList.add(this.settings.openPosition === 'up' ? this.classes.openAbove : this.classes.openBelow)
159
+ this.main.main.setAttribute('aria-expanded', 'true')
158
160
 
159
161
  // move the content in to the right location
160
162
  this.moveContent()
@@ -173,6 +175,7 @@ export default class Render {
173
175
  public close(): void {
174
176
  this.main.main.classList.remove(this.classes.openAbove)
175
177
  this.main.main.classList.remove(this.classes.openBelow)
178
+ this.main.main.setAttribute('aria-expanded', 'false')
176
179
  this.content.main.classList.remove(this.classes.openAbove)
177
180
  this.content.main.classList.remove(this.classes.openBelow)
178
181
  this.main.arrow.path.setAttribute('d', this.classes.arrowClose)
@@ -211,23 +214,26 @@ export default class Render {
211
214
  }
212
215
  }
213
216
 
217
+ public updateAriaAttributes() {
218
+ this.main.main.role = 'combobox'
219
+ this.main.main.setAttribute('aria-haspopup', 'listbox')
220
+ this.main.main.setAttribute('aria-controls', this.content.main.id)
221
+ this.main.main.setAttribute('aria-expanded', 'false')
222
+ this.content.main.setAttribute('role', 'listbox')
223
+ // do an aria-labelledby here maybe
224
+ }
225
+
214
226
  public mainDiv(): Main {
215
227
  // Create main container
216
228
  const main = document.createElement('div')
217
229
 
218
230
  // Add id to data-id
219
231
  main.dataset.id = this.settings.id
232
+ main.id = this.settings.id
220
233
 
221
234
  // Set tabable to allow tabbing to the element
222
235
  main.tabIndex = 0
223
236
 
224
- // If main gets focus, open the content
225
- main.onfocus = () => {
226
- if (this.settings.triggerFocus && this.settings.isWindowFocused) {
227
- this.callbacks.open()
228
- }
229
- }
230
-
231
237
  // Deal with keyboard events on the main div
232
238
  // This is to allow for normal selecting
233
239
  // when you may not have a search bar
@@ -243,6 +249,8 @@ export default class Render {
243
249
  this.callbacks.close()
244
250
  return true // Continue doing normal tabbing
245
251
  case 'Enter':
252
+ case ' ':
253
+ this.callbacks.open()
246
254
  const highlighted = this.content.list.querySelector('.' + this.classes.highlighted) as HTMLDivElement
247
255
  if (highlighted) {
248
256
  highlighted.click()
@@ -346,13 +354,14 @@ export default class Render {
346
354
  }
347
355
  }
348
356
 
349
- public mainFocus(trigger: boolean): void {
350
- if (!trigger) {
351
- this.settings.triggerFocus = false
352
- }
357
+ public mainFocus(eventType: string | null): void {
353
358
  // Trigger focus but dont scroll to it
354
- this.main.main.focus({ preventScroll: true })
355
- this.settings.triggerFocus = true
359
+ // Need for prevent refocus the element if event is not keyboard event.
360
+ // For example if event is mouse click or tachpad click this condition prevent refocus on element
361
+ // because click by mouse change focus position and not need return focus to element.
362
+ if (eventType !== 'click') {
363
+ this.main.main.focus({ preventScroll: true })
364
+ }
356
365
  }
357
366
 
358
367
  public placeholder(): HTMLDivElement {
@@ -595,6 +604,7 @@ export default class Render {
595
604
 
596
605
  // Add id to data-id
597
606
  main.dataset.id = this.settings.id
607
+ main.id = this.settings.id
598
608
 
599
609
  // Add search
600
610
  const search = this.searchDiv()
@@ -671,7 +681,6 @@ export default class Render {
671
681
  switch (e.key) {
672
682
  case 'ArrowUp':
673
683
  case 'ArrowDown':
674
- this.callbacks.open()
675
684
  e.key === 'ArrowDown' ? this.highlight('down') : this.highlight('up')
676
685
  return false
677
686
  case 'Tab':
@@ -685,6 +694,7 @@ export default class Render {
685
694
  this.callbacks.close()
686
695
  return false
687
696
  case 'Enter':
697
+ case ' ':
688
698
  if (this.callbacks.addable && e.ctrlKey) {
689
699
  addable.click()
690
700
  } else {
@@ -697,15 +707,6 @@ export default class Render {
697
707
  }
698
708
  }
699
709
 
700
- // If focus is on the search input, open the dropdown
701
- input.onfocus = () => {
702
- // If we are already open, do nothing
703
- if (this.settings.isOpen) {
704
- return
705
- }
706
-
707
- this.callbacks.open()
708
- }
709
710
  main.appendChild(input)
710
711
 
711
712
  // If addable is enabled, add the addable div
@@ -807,12 +808,8 @@ export default class Render {
807
808
  return searchReturn
808
809
  }
809
810
 
810
- public searchFocus(trigger: boolean): void {
811
- if (!trigger) {
812
- this.settings.triggerFocus = false
813
- }
811
+ public searchFocus(): void {
814
812
  this.content.search.input.focus()
815
- this.settings.triggerFocus = true
816
813
  }
817
814
 
818
815
  public getOptions(notPlaceholder = false, notDisabled = false, notHidden = false): HTMLDivElement[] {
@@ -897,7 +894,6 @@ export default class Render {
897
894
  public listDiv(): HTMLDivElement {
898
895
  const options = document.createElement('div')
899
896
  options.classList.add(this.classes.list)
900
- options.setAttribute('role', 'listbox')
901
897
 
902
898
  return options
903
899
  }
@@ -1117,6 +1113,7 @@ export default class Render {
1117
1113
  // Create option
1118
1114
  const optionEl = document.createElement('div')
1119
1115
  optionEl.dataset.id = option.id // Dataset id for identifying an option
1116
+ optionEl.id = option.id
1120
1117
  optionEl.classList.add(this.classes.option)
1121
1118
  optionEl.setAttribute('role', 'option') // WCAG attribute
1122
1119
  if (option.class) {
@@ -1164,8 +1161,11 @@ export default class Render {
1164
1161
  // If option is selected
1165
1162
  if (option.selected) {
1166
1163
  optionEl.classList.add(this.classes.selected)
1164
+ optionEl.setAttribute('aria-selected', 'true')
1165
+ this.main.main.setAttribute('aria-activedescendant', optionEl.id)
1167
1166
  } else {
1168
1167
  optionEl.classList.remove(this.classes.selected)
1168
+ optionEl.setAttribute('aria-selected', 'false')
1169
1169
  }
1170
1170
 
1171
1171
  // Add click event listener
@@ -10,8 +10,7 @@ export default class Settings {
10
10
  // Dynamic settings
11
11
  public isMultiple: boolean = false
12
12
  public isOpen: boolean = false
13
- public isWindowFocused: boolean = true
14
- public triggerFocus: boolean = true
13
+ public isFullOpen: boolean = false
15
14
  public intervalMove: NodeJS.Timeout | null = null
16
15
 
17
16
  // Fields set from constructor
@@ -68,6 +68,7 @@
68
68
  outline: 0;
69
69
  box-sizing: border-box;
70
70
  transition: background-color var(--ss-animation-timing);
71
+ overflow: hidden;
71
72
 
72
73
  &:focus {
73
74
  box-shadow: 0 0 5px var(--ss-primary-color);
@@ -1,5 +1,5 @@
1
1
  export declare function generateID(): string;
2
2
  export declare function hasClassInTree(element: HTMLElement, className: string): any;
3
- export declare function debounce(func: (...params: any[]) => void, wait?: number, immediate?: boolean): () => void;
3
+ export declare function debounce<T extends (...args: any[]) => void>(func: T, wait?: number, immediate?: boolean): () => void;
4
4
  export declare function isEqual(a: any, b: any): boolean;
5
5
  export declare function kebabCase(str: string): string;
@@ -36,7 +36,7 @@ export default class SlimSelect {
36
36
  setSelected(value: string | string[], runAfterChange?: boolean): void;
37
37
  addOption(option: OptionOptional): void;
38
38
  open(): void;
39
- close(): void;
39
+ close(eventType?: string | null): void;
40
40
  search(value: string): void;
41
41
  destroy(): void;
42
42
  private windowResize;
@@ -91,8 +91,9 @@ export default class Render {
91
91
  open(): void;
92
92
  close(): void;
93
93
  updateClassStyles(): void;
94
+ updateAriaAttributes(): void;
94
95
  mainDiv(): Main;
95
- mainFocus(trigger: boolean): void;
96
+ mainFocus(eventType: string | null): void;
96
97
  placeholder(): HTMLDivElement;
97
98
  renderValues(): void;
98
99
  private renderSingleValue;
@@ -101,7 +102,7 @@ export default class Render {
101
102
  contentDiv(): Content;
102
103
  moveContent(): void;
103
104
  searchDiv(): Search;
104
- searchFocus(trigger: boolean): void;
105
+ searchFocus(): void;
105
106
  getOptions(notPlaceholder?: boolean, notDisabled?: boolean, notHidden?: boolean): HTMLDivElement[];
106
107
  highlight(dir: 'up' | 'down'): void;
107
108
  listDiv(): HTMLDivElement;
@@ -6,8 +6,7 @@ export default class Settings {
6
6
  class: string[];
7
7
  isMultiple: boolean;
8
8
  isOpen: boolean;
9
- isWindowFocused: boolean;
10
- triggerFocus: boolean;
9
+ isFullOpen: boolean;
11
10
  intervalMove: NodeJS.Timeout | null;
12
11
  disabled: boolean;
13
12
  alwaysOpen: boolean;