svelte-multiselect 3.3.0 → 4.0.2

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.
@@ -0,0 +1,29 @@
1
+ <script >export let color = `cornflowerblue`;
2
+ export let duration = `1.5s`;
3
+ export let size = `1em`;
4
+ </script>
5
+
6
+ <div
7
+ style="--duration: {duration}"
8
+ style:border-color="{color} transparent {color}
9
+ {color}"
10
+ style:width={size}
11
+ style:height={size}
12
+ />
13
+
14
+ <style>
15
+ div {
16
+ display: inline-block;
17
+ vertical-align: middle;
18
+ margin: 0 3pt;
19
+ border-width: calc(1em / 5);
20
+ border-style: solid;
21
+ border-radius: 50%;
22
+ animation: var(--duration) infinite rotate;
23
+ }
24
+ @keyframes rotate {
25
+ 100% {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+ </style>
@@ -0,0 +1,18 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ color?: string | undefined;
5
+ duration?: string | undefined;
6
+ size?: string | undefined;
7
+ };
8
+ events: {
9
+ [evt: string]: CustomEvent<any>;
10
+ };
11
+ slots: {};
12
+ };
13
+ export declare type CircleSpinnerProps = typeof __propDef.props;
14
+ export declare type CircleSpinnerEvents = typeof __propDef.events;
15
+ export declare type CircleSpinnerSlots = typeof __propDef.slots;
16
+ export default class CircleSpinner extends SvelteComponentTyped<CircleSpinnerProps, CircleSpinnerEvents, CircleSpinnerSlots> {
17
+ }
18
+ export {};
@@ -1,7 +1,7 @@
1
- <script >import { createEventDispatcher, onMount } from 'svelte';
1
+ <script >import { createEventDispatcher, onMount, tick } from 'svelte';
2
2
  import { fly } from 'svelte/transition';
3
- import { onClickOutside } from './actions';
4
- import { CrossIcon, ExpandIcon, ReadOnlyIcon } from './icons';
3
+ import CircleSpinner from './CircleSpinner.svelte';
4
+ import { CrossIcon, ExpandIcon, DisabledIcon } from './icons';
5
5
  import Wiggle from './Wiggle.svelte';
6
6
  export let selected = [];
7
7
  export let selectedLabels = [];
@@ -10,7 +10,8 @@ export let searchText = ``;
10
10
  export let showOptions = false;
11
11
  export let maxSelect = null; // null means any number of options are selectable
12
12
  export let maxSelectMsg = null;
13
- export let readonly = false;
13
+ export let disabled = false;
14
+ export let disabledTitle = `This field is disabled`;
14
15
  export let options;
15
16
  export let input = null;
16
17
  export let placeholder = undefined;
@@ -31,8 +32,13 @@ export let liOptionClass = ``;
31
32
  export let liActiveOptionClass = ``;
32
33
  export let removeBtnTitle = `Remove`;
33
34
  export let removeAllTitle = `Remove all`;
34
- // https://github.com/sveltejs/svelte/issues/6964
35
35
  export let defaultDisabledTitle = `This option is disabled`;
36
+ export let allowUserOptions = false;
37
+ export let autoScroll = true;
38
+ export let loading = false;
39
+ export let required = false;
40
+ export let autocomplete = `off`;
41
+ export let invalid = false;
36
42
  if (maxSelect !== null && maxSelect < 0) {
37
43
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
38
44
  }
@@ -41,9 +47,8 @@ if (!(options?.length > 0))
41
47
  if (!Array.isArray(selected))
42
48
  console.error(`selected prop must be an array`);
43
49
  onMount(() => {
44
- selected = _options.filter((op) => op?.preselected);
50
+ selected = _options.filter((op) => op?.preselected) ?? [];
45
51
  });
46
- let wiggle = false;
47
52
  const dispatch = createEventDispatcher();
48
53
  function isObject(item) {
49
54
  return typeof item === `object` && !Array.isArray(item) && item !== null;
@@ -69,24 +74,21 @@ $: labels = _options.map((op) => op.label);
69
74
  $: if (new Set(labels).size !== options.length) {
70
75
  console.error(`Option labels must be unique. Duplicates found: ${labels.filter((label, idx) => labels.indexOf(label) !== idx)}`);
71
76
  }
77
+ let wiggle = false;
72
78
  $: selectedLabels = selected.map((op) => op.label);
73
79
  $: selectedValues = selected.map((op) => op.value);
80
+ // formValue binds to input.form-control to prevent form submission if required
81
+ // prop is true and no options are selected
82
+ $: formValue = selectedValues.join(`,`);
83
+ $: if (formValue)
84
+ invalid = false; // reset error status whenever component state changes
74
85
  // options matching the current search text
75
- $: matchingOptions = _options.filter((op) => filterFunc(op, searchText));
86
+ $: matchingOptions = _options.filter((op) => filterFunc(op, searchText) && !selectedLabels.includes(op.label));
76
87
  $: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
77
- $: if (
78
- // if there was an active option but it's not in the filtered list of options
79
- (activeOption &&
80
- !matchingEnabledOptions.map((op) => op.label).includes(activeOption.label)) ||
81
- // or there's no active option but the user entered search text
82
- (!activeOption && searchText))
83
- // make the first filtered option active
84
- activeOption = matchingEnabledOptions[0];
85
88
  function add(label) {
86
89
  if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
87
90
  wiggle = true;
88
- if (!readonly &&
89
- !selectedLabels.includes(label) &&
91
+ if (!selectedLabels.includes(label) &&
90
92
  // for maxselect = 1 we always replace current option with new selection
91
93
  (maxSelect === null || maxSelect === 1 || selected.length < maxSelect)) {
92
94
  searchText = ``; // reset search string on selection
@@ -108,24 +110,32 @@ function add(label) {
108
110
  }
109
111
  }
110
112
  function remove(label) {
111
- if (selected.length === 0 || readonly)
113
+ if (selected.length === 0)
112
114
  return;
113
- selected = selected.filter((option) => label !== option.label);
114
115
  const option = _options.find((option) => option.label === label);
116
+ if (!option) {
117
+ return console.error(`MultiSelect: option with label ${label} not found`);
118
+ }
119
+ selected = selected.filter((option) => label !== option.label);
115
120
  dispatch(`remove`, { option });
116
121
  dispatch(`change`, { option, type: `remove` });
117
122
  }
118
123
  function setOptionsVisible(show) {
124
+ if (disabled)
125
+ return;
119
126
  showOptions = show;
120
- if (show)
127
+ if (show) {
121
128
  input?.focus();
129
+ dispatch(`focus`);
130
+ }
122
131
  else {
123
132
  input?.blur();
124
133
  activeOption = null;
134
+ dispatch(`blur`);
125
135
  }
126
136
  }
127
137
  // handle all keyboard events this component receives
128
- function handleKeydown(event) {
138
+ async function handleKeydown(event) {
129
139
  // on escape: dismiss options dropdown and reset search text
130
140
  if (event.key === `Escape`) {
131
141
  setOptionsVisible(false);
@@ -137,7 +147,15 @@ function handleKeydown(event) {
137
147
  const { label } = activeOption;
138
148
  selectedLabels.includes(label) ? remove(label) : add(label);
139
149
  searchText = ``;
140
- } // no active option means the options dropdown is closed in which case enter means open it
150
+ }
151
+ else if ([true, `append`].includes(allowUserOptions)) {
152
+ selected = [...selected, { label: searchText, value: searchText }];
153
+ if (allowUserOptions === `append`)
154
+ options = [...options, { label: searchText, value: searchText }];
155
+ searchText = ``;
156
+ }
157
+ // no active option and no search text means the options dropdown is closed
158
+ // in which case enter means open it
141
159
  else
142
160
  setOptionsVisible(true);
143
161
  }
@@ -150,31 +168,25 @@ function handleKeydown(event) {
150
168
  }
151
169
  const increment = event.key === `ArrowUp` ? -1 : 1;
152
170
  const newActiveIdx = matchingEnabledOptions.indexOf(activeOption) + increment;
153
- const ulOps = document.querySelector(`ul.options`);
154
171
  if (newActiveIdx < 0) {
155
172
  // wrap around top
156
173
  activeOption = matchingEnabledOptions[matchingEnabledOptions.length - 1];
157
- if (ulOps)
158
- ulOps.scrollTop = ulOps.scrollHeight;
159
174
  }
160
175
  else if (newActiveIdx === matchingEnabledOptions.length) {
161
176
  // wrap around bottom
162
177
  activeOption = matchingEnabledOptions[0];
163
- if (ulOps)
164
- ulOps.scrollTop = 0;
165
178
  }
166
179
  else {
167
- // default case
180
+ // default case: select next/previous in item list
168
181
  activeOption = matchingEnabledOptions[newActiveIdx];
182
+ }
183
+ if (autoScroll) {
184
+ await tick();
169
185
  const li = document.querySelector(`ul.options > li.active`);
170
- // scrollIntoViewIfNeeded() scrolls top edge of element into view so when moving
171
- // downwards, we scroll to next sibling to make element fully visible
172
- if (increment === 1)
173
- li?.nextSibling?.scrollIntoViewIfNeeded();
174
- else
175
- li?.scrollIntoViewIfNeeded();
186
+ li?.scrollIntoViewIfNeeded();
176
187
  }
177
188
  }
189
+ // on backspace key: remove last selected option
178
190
  else if (event.key === `Backspace`) {
179
191
  const label = selectedLabels.pop();
180
192
  if (label && !searchText)
@@ -199,29 +211,42 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
199
211
  <!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
200
212
  display above those of another following shortly after it -->
201
213
  <div
202
- class:readonly
214
+ class:disabled
203
215
  class:single={maxSelect === 1}
204
216
  class:open={showOptions}
217
+ aria-expanded={showOptions}
218
+ aria-multiselectable={maxSelect === null || maxSelect > 1}
219
+ class:invalid
205
220
  class="multiselect {outerDivClass}"
206
221
  on:mouseup|stopPropagation={() => setOptionsVisible(true)}
207
- use:onClickOutside={() => setOptionsVisible(false)}
208
- use:onClickOutside={() => dispatch(`blur`)}
222
+ on:focusout={() => setOptionsVisible(false)}
223
+ title={disabled ? disabledTitle : null}
224
+ aria-disabled={disabled ? `true` : null}
209
225
  >
210
- <ExpandIcon style="min-width: 1em; padding: 0 1pt;" />
226
+ <!-- invisible input, used only to prevent form submission if required=true and no options selected -->
227
+ <input
228
+ {required}
229
+ bind:value={formValue}
230
+ tabindex="-1"
231
+ aria-hidden="true"
232
+ class="form-control"
233
+ on:invalid={() => (invalid = true)}
234
+ />
235
+ <ExpandIcon width="15px" style="min-width: 1em; padding: 0 1pt;" />
211
236
  <ul class="selected {ulSelectedClass}">
212
237
  {#each selected as option, idx}
213
- <li class={liSelectedClass}>
214
- <slot name="renderSelected" {option} {idx}>
238
+ <li class={liSelectedClass} aria-selected="true">
239
+ <slot name="selected" {option} {idx}>
215
240
  {option.label}
216
241
  </slot>
217
- {#if !readonly}
242
+ {#if !disabled}
218
243
  <button
219
244
  on:mouseup|stopPropagation={() => remove(option.label)}
220
245
  on:keydown={handleEnterAndSpaceKeys(() => remove(option.label))}
221
246
  type="button"
222
247
  title="{removeBtnTitle} {option.label}"
223
248
  >
224
- <CrossIcon height="12pt" />
249
+ <CrossIcon width="15px" />
225
250
  </button>
226
251
  {/if}
227
252
  </li>
@@ -229,19 +254,29 @@ display above those of another following shortly after it -->
229
254
  <li style="display: contents;">
230
255
  <input
231
256
  bind:this={input}
232
- autocomplete="off"
257
+ {autocomplete}
233
258
  bind:value={searchText}
234
259
  on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
235
260
  on:keydown={handleKeydown}
236
261
  on:focus={() => setOptionsVisible(true)}
262
+ on:blur={() => setOptionsVisible(false)}
237
263
  {id}
238
264
  {name}
265
+ {disabled}
239
266
  placeholder={selectedLabels.length ? `` : placeholder}
267
+ aria-invalid={invalid ? `true` : null}
240
268
  />
241
269
  </li>
242
270
  </ul>
243
- {#if readonly}
244
- <ReadOnlyIcon height="14pt" />
271
+ {#if loading}
272
+ <slot name="spinner">
273
+ <CircleSpinner />
274
+ </slot>
275
+ {/if}
276
+ {#if disabled}
277
+ <slot name="disabled-icon">
278
+ <DisabledIcon width="15px" />
279
+ </slot>
245
280
  {:else if selected.length > 0}
246
281
  {#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
247
282
  <Wiggle bind:wiggle angle={20}>
@@ -251,7 +286,7 @@ display above those of another following shortly after it -->
251
286
  </span>
252
287
  </Wiggle>
253
288
  {/if}
254
- {#if maxSelect !== 1}
289
+ {#if maxSelect !== 1 && selected.length > 1}
255
290
  <button
256
291
  type="button"
257
292
  class="remove-all"
@@ -259,7 +294,7 @@ display above those of another following shortly after it -->
259
294
  on:mouseup|stopPropagation={removeAll}
260
295
  on:keydown={handleEnterAndSpaceKeys(removeAll)}
261
296
  >
262
- <CrossIcon height="14pt" />
297
+ <CrossIcon width="15px" />
263
298
  </button>
264
299
  {/if}
265
300
  {/if}
@@ -285,8 +320,19 @@ display above those of another following shortly after it -->
285
320
  class:active
286
321
  class:disabled
287
322
  class="{liOptionClass} {active ? liActiveOptionClass : ``}"
323
+ on:mouseover={() => {
324
+ if (disabled) return
325
+ activeOption = option
326
+ }}
327
+ on:focus={() => {
328
+ if (disabled) return
329
+ activeOption = option
330
+ }}
331
+ on:mouseout={() => (activeOption = null)}
332
+ on:blur={() => (activeOption = null)}
333
+ aria-selected="false"
288
334
  >
289
- <slot name="renderOptions" {option} {idx}>
335
+ <slot name="option" {option} {idx}>
290
336
  {option.label}
291
337
  </slot>
292
338
  </li>
@@ -306,9 +352,11 @@ display above those of another following shortly after it -->
306
352
  cursor: text;
307
353
  padding: 0 3pt;
308
354
  border: var(--sms-border, 1pt solid lightgray);
309
- border-radius: var(--sms-border-radius, 5pt);
355
+ border-radius: var(--sms-border-radius, 3pt);
310
356
  background: var(--sms-input-bg);
311
357
  min-height: var(--sms-input-min-height, 22pt);
358
+ color: var(--sms-text-color);
359
+ font-size: var(--sms-font-size, inherit);
312
360
  }
313
361
  :where(div.multiselect.open) {
314
362
  z-index: var(--sms-open-z-index, 4);
@@ -316,8 +364,9 @@ display above those of another following shortly after it -->
316
364
  :where(div.multiselect:focus-within) {
317
365
  border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
318
366
  }
319
- :where(div.multiselect.readonly) {
320
- background: var(--sms-readonly-bg, lightgray);
367
+ :where(div.multiselect.disabled) {
368
+ background: var(--sms-disabled-bg, lightgray);
369
+ cursor: not-allowed;
321
370
  }
322
371
 
323
372
  :where(div.multiselect > ul.selected) {
@@ -333,49 +382,62 @@ display above those of another following shortly after it -->
333
382
  display: flex;
334
383
  margin: 2pt;
335
384
  line-height: normal;
336
- padding: 1pt 2pt 1pt 5pt;
385
+ padding: 1pt 5pt;
337
386
  transition: 0.3s;
338
387
  white-space: nowrap;
339
- background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
388
+ background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
340
389
  height: var(--sms-selected-li-height);
390
+ color: var(--sms-selected-text-color, var(--sms-text-color));
341
391
  }
342
- :where(div.multiselect > ul.selected > li button, button.remove-all) {
343
- align-items: center;
392
+ :where(div.multiselect button) {
344
393
  border-radius: 50%;
345
394
  display: flex;
346
- cursor: pointer;
347
395
  transition: 0.2s;
348
- }
349
- :where(div.multiselect button) {
350
396
  color: inherit;
351
397
  background: transparent;
352
398
  border: none;
353
399
  cursor: pointer;
354
400
  outline: none;
355
- padding: 0 2pt;
401
+ padding: 0;
402
+ margin: 0 0 0 3pt; /* CSS reset */
356
403
  }
357
- :where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
358
- color: var(--sms-remove-x-hover-focus-color, lightskyblue);
404
+ :where(div.multiselect button.remove-all) {
405
+ margin: 0 3pt;
359
406
  }
360
- :where(div.multiselect > button:focus) {
361
- transform: scale(1.04);
407
+ :where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
408
+ color: var(--sms-button-hover-color, lightskyblue);
362
409
  }
363
410
 
411
+ :where(div.multiselect input) {
412
+ margin: auto 0; /* CSS reset */
413
+ padding: 0; /* CSS reset */
414
+ }
364
415
  :where(div.multiselect > ul.selected > li > input) {
365
416
  border: none;
366
417
  outline: none;
367
418
  background: none;
368
419
  flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
369
420
  min-width: 2em;
370
- /* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
371
- /* https://stackoverflow.com/a/6394497 */
372
- font-size: calc(16px + 0.1vw);
373
- color: var(--sms-text-color, inherit);
421
+ color: inherit;
422
+ font-size: inherit;
423
+ cursor: inherit; /* needed for disabled state */
424
+ }
425
+ :where(div.multiselect > ul.selected > li > input)::placeholder {
426
+ color: var(--sms-placeholder-color);
427
+ }
428
+ :where(div.multiselect > input.form-control) {
429
+ width: 2em;
430
+ position: absolute;
431
+ background: transparent;
432
+ border: none;
433
+ outline: none;
434
+ z-index: -1;
435
+ opacity: 0;
436
+ pointer-events: none;
374
437
  }
375
438
 
376
439
  :where(div.multiselect > ul.options) {
377
440
  list-style: none;
378
- max-height: 50vh;
379
441
  padding: 0;
380
442
  top: 100%;
381
443
  left: 0;
@@ -384,6 +446,7 @@ display above those of another following shortly after it -->
384
446
  border-radius: 1ex;
385
447
  overflow: auto;
386
448
  background: var(--sms-options-bg, white);
449
+ max-height: var(--sms-options-max-height, 50vh);
387
450
  overscroll-behavior: var(--sms-options-overscroll, none);
388
451
  box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);
389
452
  }
@@ -393,34 +456,22 @@ display above those of another following shortly after it -->
393
456
  :where(div.multiselect > ul.options > li) {
394
457
  padding: 3pt 2ex;
395
458
  cursor: pointer;
459
+ scroll-margin: var(--sms-options-scroll-margin, 100px);
396
460
  }
397
461
  /* for noOptionsMsg */
398
462
  :where(div.multiselect > ul.options span) {
399
463
  padding: 3pt 2ex;
400
464
  }
401
465
  :where(div.multiselect > ul.options > li.selected) {
402
- border-left: var(
403
- --sms-li-selected-border-left,
404
- 3pt solid var(--sms-selected-color, green)
405
- );
406
- background: var(--sms-li-selected-bg, inherit);
407
- color: var(--sms-li-selected-color, inherit);
408
- }
409
- :where(div.multiselect > ul.options > li:not(.selected):hover) {
410
- border-left: var(
411
- --sms-li-not-selected-hover-border-left,
412
- 3pt solid var(--sms-active-color, cornflowerblue)
413
- );
466
+ background: var(--sms-li-selected-bg);
467
+ color: var(--sms-li-selected-color);
414
468
  }
415
469
  :where(div.multiselect > ul.options > li.active) {
416
- background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue));
470
+ background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
417
471
  }
418
472
  :where(div.multiselect > ul.options > li.disabled) {
419
473
  cursor: not-allowed;
420
474
  background: var(--sms-li-disabled-bg, #f5f5f6);
421
475
  color: var(--sms-li-disabled-text, #b8b8b8);
422
476
  }
423
- :where(div.multiselect > ul.options > li.disabled:hover) {
424
- border-left: unset;
425
- }
426
477
  </style>
@@ -9,7 +9,8 @@ declare const __propDef: {
9
9
  showOptions?: boolean | undefined;
10
10
  maxSelect?: number | null | undefined;
11
11
  maxSelectMsg?: ((current: number, max: number) => string) | null | undefined;
12
- readonly?: boolean | undefined;
12
+ disabled?: boolean | undefined;
13
+ disabledTitle?: string | undefined;
13
14
  options: ProtoOption[];
14
15
  input?: HTMLInputElement | null | undefined;
15
16
  placeholder?: string | undefined;
@@ -27,6 +28,12 @@ declare const __propDef: {
27
28
  removeBtnTitle?: string | undefined;
28
29
  removeAllTitle?: string | undefined;
29
30
  defaultDisabledTitle?: string | undefined;
31
+ allowUserOptions?: boolean | "append" | undefined;
32
+ autoScroll?: boolean | undefined;
33
+ loading?: boolean | undefined;
34
+ required?: boolean | undefined;
35
+ autocomplete?: string | undefined;
36
+ invalid?: boolean | undefined;
30
37
  };
31
38
  events: {
32
39
  mouseup: MouseEvent;
@@ -34,11 +41,13 @@ declare const __propDef: {
34
41
  [evt: string]: CustomEvent<any>;
35
42
  };
36
43
  slots: {
37
- renderSelected: {
44
+ selected: {
38
45
  option: Option;
39
46
  idx: any;
40
47
  };
41
- renderOptions: {
48
+ spinner: {};
49
+ 'disabled-icon': {};
50
+ option: {
42
51
  option: Option;
43
52
  idx: any;
44
53
  };
@@ -1,9 +1,4 @@
1
- <script >export let width = `1em`;
2
- export let height = width;
3
- export let style = ``;
4
- </script>
5
-
6
- <svg {width} {height} {style} fill="currentColor" viewBox="0 0 16 16">
1
+ <svg {...$$props} fill="currentColor" viewBox="0 0 16 16">
7
2
  <path
8
3
  d="M3.646 9.146a.5.5 0 0 1 .708 0L8 12.793l3.646-3.647a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 0-.708zm0-2.292a.5.5 0 0 0 .708 0L8 3.207l3.646 3.647a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 0 0 0 .708z"
9
4
  />
@@ -1,18 +1,23 @@
1
+ /** @typedef {typeof __propDef.props} ChevronExpandProps */
2
+ /** @typedef {typeof __propDef.events} ChevronExpandEvents */
3
+ /** @typedef {typeof __propDef.slots} ChevronExpandSlots */
4
+ export default class ChevronExpand extends SvelteComponentTyped<{
5
+ [x: string]: any;
6
+ }, {
7
+ [evt: string]: CustomEvent<any>;
8
+ }, {}> {
9
+ }
10
+ export type ChevronExpandProps = typeof __propDef.props;
11
+ export type ChevronExpandEvents = typeof __propDef.events;
12
+ export type ChevronExpandSlots = typeof __propDef.slots;
1
13
  import { SvelteComponentTyped } from "svelte";
2
14
  declare const __propDef: {
3
15
  props: {
4
- width?: string | number | undefined;
5
- height?: string | number | undefined;
6
- style?: string | undefined;
16
+ [x: string]: any;
7
17
  };
8
18
  events: {
9
19
  [evt: string]: CustomEvent<any>;
10
20
  };
11
21
  slots: {};
12
22
  };
13
- export declare type ChevronExpandProps = typeof __propDef.props;
14
- export declare type ChevronExpandEvents = typeof __propDef.events;
15
- export declare type ChevronExpandSlots = typeof __propDef.slots;
16
- export default class ChevronExpand extends SvelteComponentTyped<ChevronExpandProps, ChevronExpandEvents, ChevronExpandSlots> {
17
- }
18
23
  export {};
@@ -1,9 +1,4 @@
1
- <script >export let width = `1em`;
2
- export let height = width;
3
- export let style = ``;
4
- </script>
5
-
6
- <svg {width} {height} {style} viewBox="0 0 20 20" fill="currentColor">
1
+ <svg {...$$props} viewBox="0 0 20 20" fill="currentColor">
7
2
  <path
8
3
  d="M10 1.6a8.4 8.4 0 100 16.8 8.4 8.4 0 000-16.8zm4.789 11.461L13.06 14.79 10 11.729l-3.061 3.06L5.21 13.06 8.272 10 5.211 6.939 6.94 5.211 10 8.271l3.061-3.061 1.729 1.729L11.728 10l3.061 3.061z"
9
4
  />
@@ -1,18 +1,23 @@
1
+ /** @typedef {typeof __propDef.props} CrossProps */
2
+ /** @typedef {typeof __propDef.events} CrossEvents */
3
+ /** @typedef {typeof __propDef.slots} CrossSlots */
4
+ export default class Cross extends SvelteComponentTyped<{
5
+ [x: string]: any;
6
+ }, {
7
+ [evt: string]: CustomEvent<any>;
8
+ }, {}> {
9
+ }
10
+ export type CrossProps = typeof __propDef.props;
11
+ export type CrossEvents = typeof __propDef.events;
12
+ export type CrossSlots = typeof __propDef.slots;
1
13
  import { SvelteComponentTyped } from "svelte";
2
14
  declare const __propDef: {
3
15
  props: {
4
- width?: string | number | undefined;
5
- height?: string | number | undefined;
6
- style?: string | undefined;
16
+ [x: string]: any;
7
17
  };
8
18
  events: {
9
19
  [evt: string]: CustomEvent<any>;
10
20
  };
11
21
  slots: {};
12
22
  };
13
- export declare type CrossProps = typeof __propDef.props;
14
- export declare type CrossEvents = typeof __propDef.events;
15
- export declare type CrossSlots = typeof __propDef.slots;
16
- export default class Cross extends SvelteComponentTyped<CrossProps, CrossEvents, CrossSlots> {
17
- }
18
23
  export {};
@@ -1,9 +1,4 @@
1
- <script >export let width = `1em`;
2
- export let height = width;
3
- export let style = ``;
4
- </script>
5
-
6
- <svg {width} {height} {style} viewBox="0 0 24 24" fill="currentColor">
1
+ <svg {...$$props} viewBox="0 0 24 24" fill="currentColor">
7
2
  <path fill="none" d="M0 0h24v24H0V0z" />
8
3
  <path
9
4
  d="M14.48 11.95c.17.02.34.05.52.05 2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4c0 .18.03.35.05.52l3.43 3.43zm2.21 2.21L22.53 20H23v-2c0-2.14-3.56-3.5-6.31-3.84zM0 3.12l4 4V10H1v2h3v3h2v-3h2.88l2.51 2.51C9.19 15.11 7 16.3 7 18v2h9.88l4 4 1.41-1.41L1.41 1.71 0 3.12zM6.88 10H6v-.88l.88.88z"
@@ -0,0 +1,23 @@
1
+ /** @typedef {typeof __propDef.props} DisabledProps */
2
+ /** @typedef {typeof __propDef.events} DisabledEvents */
3
+ /** @typedef {typeof __propDef.slots} DisabledSlots */
4
+ export default class Disabled extends SvelteComponentTyped<{
5
+ [x: string]: any;
6
+ }, {
7
+ [evt: string]: CustomEvent<any>;
8
+ }, {}> {
9
+ }
10
+ export type DisabledProps = typeof __propDef.props;
11
+ export type DisabledEvents = typeof __propDef.events;
12
+ export type DisabledSlots = typeof __propDef.slots;
13
+ import { SvelteComponentTyped } from "svelte";
14
+ declare const __propDef: {
15
+ props: {
16
+ [x: string]: any;
17
+ };
18
+ events: {
19
+ [evt: string]: CustomEvent<any>;
20
+ };
21
+ slots: {};
22
+ };
23
+ export {};
package/icons/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { default as CrossIcon } from './Cross.svelte';
2
1
  export { default as ExpandIcon } from './ChevronExpand.svelte';
3
- export { default as ReadOnlyIcon } from './ReadOnly.svelte';
2
+ export { default as CrossIcon } from './Cross.svelte';
3
+ export { default as DisabledIcon } from './Disabled.svelte';
package/icons/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { default as CrossIcon } from './Cross.svelte';
2
1
  export { default as ExpandIcon } from './ChevronExpand.svelte';
3
- export { default as ReadOnlyIcon } from './ReadOnly.svelte';
2
+ export { default as CrossIcon } from './Cross.svelte';
3
+ export { default as DisabledIcon } from './Disabled.svelte';
package/index.d.ts CHANGED
@@ -13,3 +13,21 @@ export declare type Option = {
13
13
  export declare type ProtoOption = Primitive | (Omit<Option, `value`> & {
14
14
  value?: Primitive;
15
15
  });
16
+ export declare type DispatchEvents = {
17
+ add: {
18
+ option: Option;
19
+ };
20
+ remove: {
21
+ option: Option;
22
+ };
23
+ removeAll: {
24
+ options: Option[];
25
+ };
26
+ change: {
27
+ option?: Option;
28
+ options?: Option[];
29
+ type: 'add' | 'remove' | 'removeAll';
30
+ };
31
+ focus: undefined;
32
+ blur: undefined;
33
+ };
package/package.json CHANGED
@@ -5,19 +5,23 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "3.3.0",
8
+ "version": "4.0.2",
9
9
  "type": "module",
10
10
  "svelte": "index.js",
11
11
  "bugs": "https://github.com/janosh/svelte-multiselect/issues",
12
12
  "devDependencies": {
13
- "@sveltejs/adapter-static": "^1.0.0-next.28",
14
- "@sveltejs/kit": "^1.0.0-next.278",
15
- "@typescript-eslint/eslint-plugin": "^5.12.0",
16
- "@typescript-eslint/parser": "^5.12.0",
17
- "eslint": "^8.9.0",
18
- "eslint-plugin-svelte3": "^3.4.0",
13
+ "@sveltejs/adapter-static": "^1.0.0-next.29",
14
+ "@sveltejs/kit": "^1.0.0-next.295",
15
+ "@sveltejs/vite-plugin-svelte": "^1.0.0-next.39",
16
+ "@typescript-eslint/eslint-plugin": "^5.14.0",
17
+ "@typescript-eslint/parser": "^5.14.0",
18
+ "@vitest/ui": "^0.6.0",
19
+ "eslint": "^8.11.0",
20
+ "eslint-plugin-svelte3": "^3.4.1",
19
21
  "hastscript": "^7.0.2",
22
+ "jsdom": "^19.0.0",
20
23
  "mdsvex": "^0.10.5",
24
+ "playwright": "^1.19.2",
21
25
  "prettier": "^2.5.1",
22
26
  "prettier-plugin-svelte": "^2.6.0",
23
27
  "rehype-autolink-headings": "^6.1.1",
@@ -25,12 +29,13 @@
25
29
  "svelte": "^3.46.4",
26
30
  "svelte-check": "^2.4.5",
27
31
  "svelte-github-corner": "^0.1.0",
28
- "svelte-preprocess": "^4.10.3",
29
- "svelte-toc": "^0.2.6",
32
+ "svelte-preprocess": "^4.10.4",
33
+ "svelte-toc": "^0.2.7",
30
34
  "svelte2tsx": "^0.5.5",
31
35
  "tslib": "^2.3.1",
32
- "typescript": "^4.5.5",
33
- "vite": "^2.8.4"
36
+ "typescript": "^4.6.2",
37
+ "vite": "^2.8.6",
38
+ "vitest": "^0.6.0"
34
39
  },
35
40
  "keywords": [
36
41
  "svelte",
package/readme.md CHANGED
@@ -5,22 +5,17 @@
5
5
 
6
6
  <h4 align="center">
7
7
 
8
+ [![Tests](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml/badge.svg)](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
8
9
  [![Netlify Status](https://api.netlify.com/api/v1/badges/a45b62c3-ea45-4cfd-9912-77ec4fc8d7e8/deploy-status)](https://app.netlify.com/sites/svelte-multiselect/deploys)
9
10
  [![NPM version](https://img.shields.io/npm/v/svelte-multiselect?color=blue&logo=NPM)](https://npmjs.com/package/svelte-multiselect)
10
11
  [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/janosh/svelte-multiselect/main.svg)](https://results.pre-commit.ci/latest/github/janosh/svelte-multiselect/main)
11
- ![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/dev/svelte)
12
+ [![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/dev/svelte)](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
12
13
 
13
14
  </h4>
14
15
 
15
- <div class="hide-in-docs">
16
+ **Keyboard-friendly, zero-dependency multi-select Svelte component.** <strong class="hide-in-docs"><a href="https://svelte-multiselect.netlify.app">Live demo</a></strong>
16
17
 
17
- **[Live demo](https://svelte-multiselect.netlify.app)**.
18
-
19
- </div>
20
-
21
- **Keyboard-friendly, zero-dependency multi-select Svelte component.**
22
-
23
- <slot />
18
+ <slot name="examples" />
24
19
 
25
20
  ## Key features
26
21
 
@@ -33,18 +28,26 @@
33
28
  - **No dependencies:** needs only Svelte as dev dependency
34
29
  - **Keyboard friendly** for mouse-less form completion
35
30
 
31
+ <slot name="nav" />
32
+
36
33
  ## Recent breaking changes
37
34
 
38
- - v2.0.0 added the ability to pass options as objects. As a result, `bind:selected` no longer returns simple strings but objects, even if you still pass in `options` as strings. To get the same stuff you would have gotten from `bind:selected` before, there's now `bind:selectedLabels` (and `bind:selectedValues`).
39
35
  - v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
40
36
 
41
37
  ```js
42
- on:add={(e) => console.log(e.detail.token.label)} // v2.0.0
43
- on:add={(e) => console.log(e.detail.option.label)} // v3.0.0
38
+ on:add={(e) => console.log(e.detail.token.label)} // v2
39
+ on:add={(e) => console.log(e.detail.option.label)} // v3
44
40
  ```
45
41
 
46
42
  It also added a separate event type `removeAll` for when the user removes all currently selected options at once which previously fired a normal `remove`. The props `ulTokensClass` and `liTokenClass` were renamed to `ulSelectedClass` and `liSelectedClass`. Similarly, the CSS variable `--sms-token-bg` changed to `--sms-selected-bg`.
47
43
 
44
+ - v4.0.0 renamed the slots for customizing how selected options and dropdown list items are rendered:
45
+
46
+ - old: `<slot name="renderOptions" />`, new: `<slot name="option" />`
47
+ - old: `<slot name="renderSelected" />`, new: `<slot name="selected" />`
48
+
49
+ - v4.0.1 renamed the `readonly` prop to `disabled` which now prevents all form or user interaction with this component including opening the dropdown list which was still possible before. See [#45](https://github.com/janosh/svelte-multiselect/issues/45) for details. The associated CSS class applied to the outer `div` was likewise renamed to `div.multiselect.{readonly=>disabled}`.
50
+
48
51
  ## Installation
49
52
 
50
53
  ```sh
@@ -57,28 +60,16 @@ yarn add -D svelte-multiselect
57
60
  <script>
58
61
  import MultiSelect from 'svelte-multiselect'
59
62
 
60
- const webFrameworks = [
61
- `Svelte`,
62
- `React`,
63
- `Vue`,
64
- `Angular`,
65
- `Polymer`,
66
- `Ruby on Rails`,
67
- `ASP.net`,
68
- `Laravel`,
69
- `Django`,
70
- `Express`,
71
- `Spring`,
72
- ]
63
+ const ui_libs = [`Svelte`, `React`, `Vue`, `Angular`, `...`]
73
64
 
74
- let selected
65
+ let selected = []
75
66
  </script>
76
67
 
77
- Favorite Web Frameworks?
68
+ Favorite Frontend Frameworks?
78
69
 
79
70
  <code>selected = {JSON.stringify(selected)}</code>
80
71
 
81
- <MultiSelect bind:selected options={webFrameworks} />
72
+ <MultiSelect bind:selected options={ui_libs} />
82
73
  ```
83
74
 
84
75
  ## Props
@@ -88,22 +79,32 @@ Full list of props/bindable variables for this component:
88
79
  <div class="table">
89
80
 
90
81
  <!-- prettier-ignore -->
91
- | name | default | description |
92
- | :--------------- | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
- | `options` | required prop | Array of strings/numbers or `Option` objects that will be listed in the dropdown. See `src/lib/index.ts` for admissible fields. The `label` is the only mandatory one. It must also be unique. |
94
- | `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
95
- | `searchText` | `` | Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text. |
96
- | `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
97
- | `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
98
- | `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
99
- | `selectedLabels` | `[]` | Labels of currently selected options. |
100
- | `selectedValues` | `[]` | Values of currently selected options. |
101
- | `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
102
- | `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
103
- | `placeholder` | `undefined` | String shown in the text input when no option is selected. |
104
- | `input` | `undefined` | Handle to the `<input>` DOM node. |
105
- | `id` | `undefined` | Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component. |
106
- | `name` | `id` | Applied to the `<input>` element. If not provided, will be set to the value of `id`. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>`. |
82
+ | name | default | description |
83
+ | :--------------------- | :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `options` | required prop | Array of strings/numbers or `Option` objects that will be listed in the dropdown. See `src/lib/index.ts` for admissible fields. The `label` is the only mandatory one. It must also be unique. |
85
+ | `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
86
+ | `searchText` | `` | Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text. |
87
+ | `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
88
+ | `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
89
+ | `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
90
+ | `selectedLabels` | `[]` | Labels of currently selected options. |
91
+ | `selectedValues` | `[]` | Values of currently selected options. |
92
+ | `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
93
+ | `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
94
+ | `disabledTitle` | `This field is disabled` | Tooltip text to display on hover when the component is in `disabled` state. |
95
+ | `placeholder` | `undefined` | String shown in the text input when no option is selected. |
96
+ | `input` | `undefined` | Handle to the `<input>` DOM node. |
97
+ | `id` | `undefined` | Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component. |
98
+ | `name` | `id` | Applied to the `<input>` element. If not provided, will be set to the value of `id`. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>`. |
99
+ | `required` | `false` | Whether forms can be submitted without selecting any options. Aborts submission, is scrolled into view and shows help "Please fill out" message when true and user tries to submit with no options selected. |
100
+ | `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
101
+ | `allowUserOptions` | `false` | Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected. |
102
+ | `loading` | `false` | Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner. |
103
+ | `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
104
+ | `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
105
+ | `defaultDisabledTitle` | `'This option is disabled'` | Title text to display when user hovers over a disabled option. Each option can override this through its `disabledTitle` attribute. |
106
+ | `autocomplete` | `'off'` | Applied to the `<input>`. Specifies if browser is permitted to auto-fill this form field. See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for other admissible values. |
107
+ | `invalid` | `false` | If `required=true` and user tries to submit but `selected = []` is empty, `invalid` is automatically set to `true` and CSS class `invalid` applied to the top-level `div.multiselect`. `invalid` class is removed again as soon as the user selects an option. `invalid` can also be controlled externally by binding to it `<MultiSelect bind:invalid />` and setting it to `true` based on outside events or custom validation. |
107
108
 
108
109
  </div>
109
110
 
@@ -126,24 +127,28 @@ Full list of props/bindable variables for this component:
126
127
 
127
128
  ## Slots
128
129
 
129
- `MultiSelect.svelte` accepts two named slots
130
+ `MultiSelect.svelte` has 3 named slots:
130
131
 
131
- - `slot="renderOptions"`
132
- - `slot="renderSelected"`
132
+ - `slot="option"`: Customize rendering of dropdown options. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the dropdown.
133
+ - `slot="selected"`: Customize rendering selected tags. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the list of selected items.
134
+ - `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
135
+ - `slot="disabled-icon"`: Custom icon to display inside the input when in `disabled` state. Receives no props. Use an empty `<span slot="disabled-icon" />` or `div` to remove the default disabled icon.
133
136
 
134
- to customize rendering individual options in the dropdown and the list of selected tags, respectively. Each renderer receives the full `option` object along with the zero-indexed position (`idx`) in its list, both available via the `let:` directive:
137
+ Example:
135
138
 
136
139
  ```svelte
137
140
  <MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
138
- <span let:idx let:option slot="renderOptions">
141
+ <span let:idx let:option slot="option">
139
142
  {idx + 1}. {option.label}
140
143
  {option.label === `Mango` ? `🎉` : ``}
141
144
  </span>
142
145
 
143
- <span let:idx let:option slot="renderSelected">
146
+ <span let:idx let:option slot="selected">
144
147
  #{idx + 1}
145
148
  {option.label}
146
149
  </span>
150
+
151
+ <CustomSpinner slot="spinner">
147
152
  </MultiSelect>
148
153
  ```
149
154
 
@@ -215,34 +220,35 @@ If you only want to make small adjustments, you can pass the following CSS varia
215
220
 
216
221
  - `div.multiselect`
217
222
  - `border: var(--sms-border, 1pt solid lightgray)`: Change this to e.g. to `1px solid red` to indicate this form field is in an invalid state.
218
- - `border-radius: var(--sms-border-radius, 5pt)`
223
+ - `border-radius: var(--sms-border-radius, 3pt)`
219
224
  - `background: var(--sms-input-bg)`
220
225
  - `height: var(--sms-input-height, 2em)`
226
+ - `color: var(--sms-text-color)`
227
+ - `color: var(--sms-placeholder-color)`
221
228
  - `div.multiselect.open`
222
229
  - `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
223
230
  - `div.multiselect:focus-within`
224
231
  - `border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue))`: Border when component has focus. Defaults to `--sms-active-color` if not set which defaults to `cornflowerblue`.
225
- - `div.multiselect.readonly`
226
- - `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
227
- - `div.multiselect > ul.selected > li > input`
228
- - `color: var(--sms-text-color, inherit)`: Input text color.
232
+ - `div.multiselect.disabled`
233
+ - `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
229
234
  - `div.multiselect > ul.selected > li`
230
- - `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
235
+ - `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
231
236
  - `height: var(--sms-selected-li-height)`: Height of selected options.
237
+ - `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
232
238
  - `ul.selected > li button:hover, button.remove-all:hover, button:focus`
233
- - `color: var(--sms-remove-x-hover-focus-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
239
+ - `color: var(--sms-button-hover-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
234
240
  - `div.multiselect > ul.options`
235
241
  - `background: var(--sms-options-bg, white)`: Background of dropdown list.
242
+ - `max-height: var(--sms-options-max-height, 50vh)`: Maximum height of options dropdown.
236
243
  - `overscroll-behavior: var(--sms-options-overscroll, none)`: Whether scroll events bubble to parent elements when reaching the top/bottom of the options dropdown. See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior).
237
244
  - `box-shadow: var(--sms-options-shadow, 0 0 14pt -8pt black);`: Box shadow of dropdown list.
245
+ - `div.multiselect > ul.options > li`
246
+ - `scroll-margin: var(--sms-options-scroll-margin, 100px)`: Top/bottom margin to keep between dropdown list items and top/bottom screen edge when auto-scrolling list to keep items in view.
238
247
  - `div.multiselect > ul.options > li.selected`
239
- - `border-left: var(--sms-li-selected-border-left, 3pt solid var(--sms-selected-color, green))`
240
- - `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
241
- - `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
242
- - `div.multiselect > ul.options > li:not(.selected):hover`
243
- - `border-left: var(--sms-li-not-selected-hover-border-left, 3pt solid var(--sms-active-color, cornflowerblue))`
248
+ - `background: var(--sms-li-selected-bg)`: Background of selected list items in options pane.
249
+ - `color: var(--sms-li-selected-color)`: Text color of selected list items in options pane.
244
250
  - `div.multiselect > ul.options > li.active`
245
- - `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
251
+ - `background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)))`: Background of active dropdown item. Items become active either by mouseover or by navigating to them with arrow keys.
246
252
  - `div.multiselect > ul.options > li.disabled`
247
253
  - `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
248
254
  - `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
@@ -292,8 +298,8 @@ You can alternatively style every part of this component with more fine-grained
292
298
  :global(div.multiselect.open) {
293
299
  /* top-level wrapper div when dropdown open */
294
300
  }
295
- :global(div.multiselect.readonly) {
296
- /* top-level wrapper div when in readonly state */
301
+ :global(div.multiselect.disabled) {
302
+ /* top-level wrapper div when in disabled state */
297
303
  }
298
304
  :global(div.multiselect > ul.selected) {
299
305
  /* selected list */
@@ -331,6 +337,40 @@ You can alternatively style every part of this component with more fine-grained
331
337
  }
332
338
  ```
333
339
 
340
+ ## Downstream testing
341
+
342
+ To test a Svelte component which imports `svelte-multiselect`, you need to configure your test runner to avoid [transpiling issues](https://github.com/janosh/svelte-multiselect/issues/48).
343
+
344
+ For Jest, exclude `svelte-multiselect` from `transformIgnorePatterns` in your `jest.config.json`:
345
+
346
+ ```json
347
+ {
348
+ "transformIgnorePatterns": ["node_modules/?!(svelte-multiselect)"],
349
+ "transform": {
350
+ "^.+\\.[t|j]s?$": "esbuild-jest",
351
+ "^.+\\.svelte$": ["svelte-jester", { "preprocess": true }]
352
+ }
353
+ }
354
+ ```
355
+
356
+ For Vitest, include `svelte-multiselect` in `deps.inline`:
357
+
358
+ ```ts
359
+ // vite.config.ts
360
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
361
+
362
+ export default {
363
+ plugins: [svelte({ hot: !process.env.VITEST })],
364
+ test: {
365
+ deps: {
366
+ inline: [/svelte-multiselect/],
367
+ },
368
+ },
369
+ }
370
+ ```
371
+
372
+ Here's a [Stackblitz example](https://stackblitz.com/fork/github/davipon/test-svelte-multiselect?initialPath=__vitest__) that also uses [`'vitest-svelte-kit'`](https://github.com/nickbreaton/vitest-svelte-kit).
373
+
334
374
  ## Want to contribute?
335
375
 
336
376
  To submit a PR, clone the repo, install dependencies and start the dev server to try out your changes.
package/actions.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export declare function onClickOutside(node: HTMLElement, cb?: () => void): {
2
- destroy(): void;
3
- };
package/actions.js DELETED
@@ -1,16 +0,0 @@
1
- export function onClickOutside(node, cb) {
2
- const dispatchOnClickOutside = (event) => {
3
- const clickWasOutside = node && !node.contains(event.target);
4
- if (clickWasOutside && !event.defaultPrevented) {
5
- node.dispatchEvent(new CustomEvent(`clickOutside`));
6
- if (cb)
7
- cb();
8
- }
9
- };
10
- document.addEventListener(`click`, dispatchOnClickOutside);
11
- return {
12
- destroy() {
13
- document.removeEventListener(`click`, dispatchOnClickOutside);
14
- },
15
- };
16
- }
@@ -1,18 +0,0 @@
1
- import { SvelteComponentTyped } from "svelte";
2
- declare const __propDef: {
3
- props: {
4
- width?: string | number | undefined;
5
- height?: string | number | undefined;
6
- style?: string | undefined;
7
- };
8
- events: {
9
- [evt: string]: CustomEvent<any>;
10
- };
11
- slots: {};
12
- };
13
- export declare type ReadOnlyProps = typeof __propDef.props;
14
- export declare type ReadOnlyEvents = typeof __propDef.events;
15
- export declare type ReadOnlySlots = typeof __propDef.slots;
16
- export default class ReadOnly extends SvelteComponentTyped<ReadOnlyProps, ReadOnlyEvents, ReadOnlySlots> {
17
- }
18
- export {};