svelte-multiselect 4.0.0 → 4.0.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.
@@ -1,8 +1,7 @@
1
1
  <script >import { createEventDispatcher, onMount, tick } from 'svelte';
2
2
  import { fly } from 'svelte/transition';
3
- import { onClickOutside } from './actions';
4
3
  import CircleSpinner from './CircleSpinner.svelte';
5
- import { CrossIcon, ExpandIcon, ReadOnlyIcon } from './icons';
4
+ import { CrossIcon, ExpandIcon, DisabledIcon } from './icons';
6
5
  import Wiggle from './Wiggle.svelte';
7
6
  export let selected = [];
8
7
  export let selectedLabels = [];
@@ -11,7 +10,8 @@ export let searchText = ``;
11
10
  export let showOptions = false;
12
11
  export let maxSelect = null; // null means any number of options are selectable
13
12
  export let maxSelectMsg = null;
14
- export let readonly = false;
13
+ export let disabled = false;
14
+ export let disabledTitle = `This field is disabled`;
15
15
  export let options;
16
16
  export let input = null;
17
17
  export let placeholder = undefined;
@@ -32,12 +32,12 @@ export let liOptionClass = ``;
32
32
  export let liActiveOptionClass = ``;
33
33
  export let removeBtnTitle = `Remove`;
34
34
  export let removeAllTitle = `Remove all`;
35
- // https://github.com/sveltejs/svelte/issues/6964
36
35
  export let defaultDisabledTitle = `This option is disabled`;
37
36
  export let allowUserOptions = false;
38
37
  export let autoScroll = true;
39
38
  export let loading = false;
40
39
  export let required = false;
40
+ export let autocomplete = `off`;
41
41
  if (maxSelect !== null && maxSelect < 0) {
42
42
  console.error(`maxSelect must be null or positive integer, got ${maxSelect}`);
43
43
  }
@@ -46,7 +46,7 @@ if (!(options?.length > 0))
46
46
  if (!Array.isArray(selected))
47
47
  console.error(`selected prop must be an array`);
48
48
  onMount(() => {
49
- selected = _options.filter((op) => op?.preselected);
49
+ selected = _options.filter((op) => op?.preselected) ?? [];
50
50
  });
51
51
  let wiggle = false;
52
52
  // formValue binds to input.form-control to prevent form submission if required
@@ -85,8 +85,7 @@ $: matchingEnabledOptions = matchingOptions.filter((op) => !op.disabled);
85
85
  function add(label) {
86
86
  if (maxSelect && maxSelect > 1 && selected.length >= maxSelect)
87
87
  wiggle = true;
88
- if (!readonly &&
89
- !selectedLabels.includes(label) &&
88
+ if (!selectedLabels.includes(label) &&
90
89
  // for maxselect = 1 we always replace current option with new selection
91
90
  (maxSelect === null || maxSelect === 1 || selected.length < maxSelect)) {
92
91
  searchText = ``; // reset search string on selection
@@ -108,7 +107,7 @@ function add(label) {
108
107
  }
109
108
  }
110
109
  function remove(label) {
111
- if (selected.length === 0 || readonly)
110
+ if (selected.length === 0)
112
111
  return;
113
112
  const option = _options.find((option) => option.label === label);
114
113
  if (!option) {
@@ -119,6 +118,8 @@ function remove(label) {
119
118
  dispatch(`change`, { option, type: `remove` });
120
119
  }
121
120
  function setOptionsVisible(show) {
121
+ if (disabled)
122
+ return;
122
123
  showOptions = show;
123
124
  if (show)
124
125
  input?.focus();
@@ -204,13 +205,16 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
204
205
  <!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
205
206
  display above those of another following shortly after it -->
206
207
  <div
207
- class:readonly
208
+ class:disabled
208
209
  class:single={maxSelect === 1}
209
210
  class:open={showOptions}
210
211
  class="multiselect {outerDivClass}"
211
212
  on:mouseup|stopPropagation={() => setOptionsVisible(true)}
212
- use:onClickOutside={() => setOptionsVisible(false)}
213
- use:onClickOutside={() => dispatch(`blur`)}
213
+ on:focusout={() => {
214
+ setOptionsVisible(false)
215
+ dispatch(`blur`)
216
+ }}
217
+ title={disabled ? disabledTitle : null}
214
218
  >
215
219
  <!-- invisible input, used only to prevent form submission if required=true and no options selected -->
216
220
  <input {required} bind:value={formValue} tabindex="-1" class="form-control" />
@@ -221,7 +225,7 @@ display above those of another following shortly after it -->
221
225
  <slot name="selected" {option} {idx}>
222
226
  {option.label}
223
227
  </slot>
224
- {#if !readonly}
228
+ {#if !disabled}
225
229
  <button
226
230
  on:mouseup|stopPropagation={() => remove(option.label)}
227
231
  on:keydown={handleEnterAndSpaceKeys(() => remove(option.label))}
@@ -236,13 +240,15 @@ display above those of another following shortly after it -->
236
240
  <li style="display: contents;">
237
241
  <input
238
242
  bind:this={input}
239
- autocomplete="off"
243
+ {autocomplete}
240
244
  bind:value={searchText}
241
245
  on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
242
246
  on:keydown={handleKeydown}
243
247
  on:focus={() => setOptionsVisible(true)}
248
+ on:blur={() => setOptionsVisible(false)}
244
249
  {id}
245
250
  {name}
251
+ {disabled}
246
252
  placeholder={selectedLabels.length ? `` : placeholder}
247
253
  />
248
254
  </li>
@@ -252,8 +258,10 @@ display above those of another following shortly after it -->
252
258
  <CircleSpinner />
253
259
  </slot>
254
260
  {/if}
255
- {#if readonly}
256
- <ReadOnlyIcon height="14pt" />
261
+ {#if disabled}
262
+ <slot name="disabled-icon">
263
+ <DisabledIcon height="14pt" />
264
+ </slot>
257
265
  {:else if selected.length > 0}
258
266
  {#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
259
267
  <Wiggle bind:wiggle angle={20}>
@@ -297,6 +305,16 @@ display above those of another following shortly after it -->
297
305
  class:active
298
306
  class:disabled
299
307
  class="{liOptionClass} {active ? liActiveOptionClass : ``}"
308
+ on:mouseover={() => {
309
+ if (disabled) return
310
+ activeOption = option
311
+ }}
312
+ on:focus={() => {
313
+ if (disabled) return
314
+ activeOption = option
315
+ }}
316
+ on:mouseout={() => (activeOption = null)}
317
+ on:blur={() => (activeOption = null)}
300
318
  >
301
319
  <slot name="option" {option} {idx}>
302
320
  {option.label}
@@ -318,9 +336,11 @@ display above those of another following shortly after it -->
318
336
  cursor: text;
319
337
  padding: 0 3pt;
320
338
  border: var(--sms-border, 1pt solid lightgray);
321
- border-radius: var(--sms-border-radius, 5pt);
339
+ border-radius: var(--sms-border-radius, 3pt);
322
340
  background: var(--sms-input-bg);
323
341
  min-height: var(--sms-input-min-height, 22pt);
342
+ color: var(--sms-text-color);
343
+ font-size: var(--sms-font-size, inherit);
324
344
  }
325
345
  :where(div.multiselect.open) {
326
346
  z-index: var(--sms-open-z-index, 4);
@@ -328,8 +348,9 @@ display above those of another following shortly after it -->
328
348
  :where(div.multiselect:focus-within) {
329
349
  border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
330
350
  }
331
- :where(div.multiselect.readonly) {
332
- background: var(--sms-readonly-bg, lightgray);
351
+ :where(div.multiselect.disabled) {
352
+ background: var(--sms-disabled-bg, lightgray);
353
+ cursor: not-allowed;
333
354
  }
334
355
 
335
356
  :where(div.multiselect > ul.selected) {
@@ -345,26 +366,24 @@ display above those of another following shortly after it -->
345
366
  display: flex;
346
367
  margin: 2pt;
347
368
  line-height: normal;
348
- padding: 1pt 2pt 1pt 5pt;
369
+ padding: 1pt 5pt;
349
370
  transition: 0.3s;
350
371
  white-space: nowrap;
351
- background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
372
+ background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
352
373
  height: var(--sms-selected-li-height);
374
+ color: var(--sms-selected-text-color, var(--sms-text-color));
353
375
  }
354
- :where(div.multiselect > ul.selected > li button, button.remove-all) {
355
- align-items: center;
376
+ :where(div.multiselect button) {
356
377
  border-radius: 50%;
357
378
  display: flex;
358
- cursor: pointer;
359
379
  transition: 0.2s;
360
- }
361
- :where(div.multiselect button) {
362
380
  color: inherit;
363
381
  background: transparent;
364
382
  border: none;
365
383
  cursor: pointer;
366
384
  outline: none;
367
- padding: 0 2pt;
385
+ padding: 0;
386
+ margin: 0 0 0 4pt; /* CSS reset */
368
387
  }
369
388
  :where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
370
389
  color: var(--sms-remove-x-hover-focus-color, lightskyblue);
@@ -373,16 +392,19 @@ display above those of another following shortly after it -->
373
392
  transform: scale(1.04);
374
393
  }
375
394
 
395
+ :where(div.multiselect input) {
396
+ margin: auto 0; /* CSS reset */
397
+ padding: 0; /* CSS reset */
398
+ }
376
399
  :where(div.multiselect > ul.selected > li > input) {
377
400
  border: none;
378
401
  outline: none;
379
402
  background: none;
380
403
  flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
381
404
  min-width: 2em;
382
- /* minimum font-size > 16px ensures iOS doesn't zoom in when focusing input */
383
- /* https://stackoverflow.com/a/6394497 */
384
- font-size: calc(16px + 0.1vw);
385
- color: var(--sms-text-color, inherit);
405
+ color: inherit;
406
+ font-size: inherit;
407
+ cursor: inherit; /* needed for disabled state */
386
408
  }
387
409
  :where(div.multiselect > input.form-control) {
388
410
  width: 2em;
@@ -392,6 +414,7 @@ display above those of another following shortly after it -->
392
414
  outline: none;
393
415
  z-index: -1;
394
416
  opacity: 0;
417
+ pointer-events: none;
395
418
  }
396
419
 
397
420
  :where(div.multiselect > ul.options) {
@@ -421,28 +444,15 @@ display above those of another following shortly after it -->
421
444
  padding: 3pt 2ex;
422
445
  }
423
446
  :where(div.multiselect > ul.options > li.selected) {
424
- border-left: var(
425
- --sms-li-selected-border-left,
426
- 3pt solid var(--sms-selected-color, green)
427
- );
428
- background: var(--sms-li-selected-bg, inherit);
429
- color: var(--sms-li-selected-color, inherit);
430
- }
431
- :where(div.multiselect > ul.options > li:not(.selected):hover) {
432
- border-left: var(
433
- --sms-li-not-selected-hover-border-left,
434
- 3pt solid var(--sms-active-color, cornflowerblue)
435
- );
447
+ background: var(--sms-li-selected-bg);
448
+ color: var(--sms-li-selected-color);
436
449
  }
437
450
  :where(div.multiselect > ul.options > li.active) {
438
- background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue));
451
+ background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
439
452
  }
440
453
  :where(div.multiselect > ul.options > li.disabled) {
441
454
  cursor: not-allowed;
442
455
  background: var(--sms-li-disabled-bg, #f5f5f6);
443
456
  color: var(--sms-li-disabled-text, #b8b8b8);
444
457
  }
445
- :where(div.multiselect > ul.options > li.disabled:hover) {
446
- border-left: unset;
447
- }
448
458
  </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;
@@ -31,6 +32,7 @@ declare const __propDef: {
31
32
  autoScroll?: boolean | undefined;
32
33
  loading?: boolean | undefined;
33
34
  required?: boolean | undefined;
35
+ autocomplete?: string | undefined;
34
36
  };
35
37
  events: {
36
38
  mouseup: MouseEvent;
@@ -43,6 +45,7 @@ declare const __propDef: {
43
45
  idx: any;
44
46
  };
45
47
  spinner: {};
48
+ 'disabled-icon': {};
46
49
  option: {
47
50
  option: Option;
48
51
  idx: any;
File without changes
@@ -10,9 +10,9 @@ declare const __propDef: {
10
10
  };
11
11
  slots: {};
12
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> {
13
+ export declare type DisabledProps = typeof __propDef.props;
14
+ export declare type DisabledEvents = typeof __propDef.events;
15
+ export declare type DisabledSlots = typeof __propDef.slots;
16
+ export default class Disabled extends SvelteComponentTyped<DisabledProps, DisabledEvents, DisabledSlots> {
17
17
  }
18
18
  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/package.json CHANGED
@@ -5,18 +5,22 @@
5
5
  "homepage": "https://svelte-multiselect.netlify.app",
6
6
  "repository": "https://github.com/janosh/svelte-multiselect",
7
7
  "license": "MIT",
8
- "version": "4.0.0",
8
+ "version": "4.0.1",
9
9
  "type": "module",
10
10
  "svelte": "index.js",
11
11
  "bugs": "https://github.com/janosh/svelte-multiselect/issues",
12
12
  "devDependencies": {
13
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",
14
+ "@sveltejs/kit": "^1.0.0-next.287",
15
+ "@sveltejs/vite-plugin-svelte": "^1.0.0-next.38",
16
+ "@testing-library/svelte": "^3.0.3",
17
+ "@typescript-eslint/eslint-plugin": "^5.12.1",
16
18
  "@typescript-eslint/parser": "^5.12.0",
19
+ "@vitest/ui": "^0.5.7",
17
20
  "eslint": "^8.9.0",
18
- "eslint-plugin-svelte3": "^3.4.0",
21
+ "eslint-plugin-svelte3": "^3.4.1",
19
22
  "hastscript": "^7.0.2",
23
+ "jsdom": "^19.0.0",
20
24
  "mdsvex": "^0.10.5",
21
25
  "prettier": "^2.5.1",
22
26
  "prettier-plugin-svelte": "^2.6.0",
@@ -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",
32
+ "svelte-preprocess": "^4.10.4",
29
33
  "svelte-toc": "^0.2.6",
30
34
  "svelte2tsx": "^0.5.5",
31
35
  "tslib": "^2.3.1",
32
36
  "typescript": "^4.5.5",
33
- "vite": "^2.8.4"
37
+ "vite": "^2.8.4",
38
+ "vitest": "^0.5.7"
34
39
  },
35
40
  "keywords": [
36
41
  "svelte",
package/readme.md CHANGED
@@ -5,6 +5,7 @@
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)
@@ -38,16 +39,19 @@
38
39
  - v3.0.0 changed the `event.detail` payload for `'add'`, `'remove'` and `'change'` events from `token` to `option`, e.g.
39
40
 
40
41
  ```js
41
- on:add={(e) => console.log(e.detail.token.label)} // v2.0.0
42
- on:add={(e) => console.log(e.detail.option.label)} // v3.0.0
42
+ on:add={(e) => console.log(e.detail.token.label)} // v2
43
+ on:add={(e) => console.log(e.detail.option.label)} // v3
43
44
  ```
44
45
 
45
46
  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`.
46
47
 
47
48
  - v4.0.0 renamed the slots for customizing how selected options and dropdown list items are rendered:
49
+
48
50
  - old: `<slot name="renderOptions" />`, new: `<slot name="option" />`
49
51
  - old: `<slot name="renderSelected" />`, new: `<slot name="selected" />`
50
52
 
53
+ - 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}`.
54
+
51
55
  ## Installation
52
56
 
53
57
  ```sh
@@ -74,7 +78,7 @@ yarn add -D svelte-multiselect
74
78
  `Spring`,
75
79
  ]
76
80
 
77
- let selected
81
+ let selected = []
78
82
  </script>
79
83
 
80
84
  Favorite Web Frameworks?
@@ -91,26 +95,31 @@ Full list of props/bindable variables for this component:
91
95
  <div class="table">
92
96
 
93
97
  <!-- prettier-ignore -->
94
- | name | default | description |
95
- | :----------------- | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
96
- | `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. |
97
- | `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
98
- | `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. |
99
- | `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
100
- | `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
101
- | `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
102
- | `selectedLabels` | `[]` | Labels of currently selected options. |
103
- | `selectedValues` | `[]` | Values of currently selected options. |
104
- | `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
105
- | `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
106
- | `placeholder` | `undefined` | String shown in the text input when no option is selected. |
107
- | `input` | `undefined` | Handle to the `<input>` DOM node. |
108
- | `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. |
109
- | `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>`. |
110
- | `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. |
111
- | `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
112
- | `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. |
113
- | `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. |
98
+ | name | default | description |
99
+ | :--------------------- | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
100
+ | `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. |
101
+ | `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
102
+ | `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. |
103
+ | `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
104
+ | `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
105
+ | `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
106
+ | `selectedLabels` | `[]` | Labels of currently selected options. |
107
+ | `selectedValues` | `[]` | Values of currently selected options. |
108
+ | `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
109
+ | `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
110
+ | `disabledTitle` | `This field is disabled` | Tooltip text to display on hover when the component is in `disabled` state. |
111
+ | `placeholder` | `undefined` | String shown in the text input when no option is selected. |
112
+ | `input` | `undefined` | Handle to the `<input>` DOM node. |
113
+ | `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. |
114
+ | `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>`. |
115
+ | `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. |
116
+ | `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
117
+ | `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. |
118
+ | `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. |
119
+ | `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
120
+ | `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
121
+ | `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. |
122
+ | `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. |
114
123
 
115
124
  </div>
116
125
 
@@ -138,6 +147,7 @@ Full list of props/bindable variables for this component:
138
147
  - `slot="option"`: Customize rendering of dropdown options. Receives as props the `option` object and the zero-indexed position (`idx`) it has in the dropdown.
139
148
  - `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.
140
149
  - `slot="spinner"`: Custom spinner component to display when in `loading` state. Receives no props.
150
+ - `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.
141
151
 
142
152
  Example:
143
153
 
@@ -225,20 +235,20 @@ If you only want to make small adjustments, you can pass the following CSS varia
225
235
 
226
236
  - `div.multiselect`
227
237
  - `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.
228
- - `border-radius: var(--sms-border-radius, 5pt)`
238
+ - `border-radius: var(--sms-border-radius, 3pt)`
229
239
  - `background: var(--sms-input-bg)`
230
240
  - `height: var(--sms-input-height, 2em)`
241
+ - `color: var(--sms-text-color)`
231
242
  - `div.multiselect.open`
232
243
  - `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
233
244
  - `div.multiselect:focus-within`
234
245
  - `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`.
235
- - `div.multiselect.readonly`
236
- - `background: var(--sms-readonly-bg, lightgray)`: Background when in readonly state.
237
- - `div.multiselect > ul.selected > li > input`
238
- - `color: var(--sms-text-color, inherit)`: Input text color.
246
+ - `div.multiselect.disabled`
247
+ - `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
239
248
  - `div.multiselect > ul.selected > li`
240
- - `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
249
+ - `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
241
250
  - `height: var(--sms-selected-li-height)`: Height of selected options.
251
+ - `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
242
252
  - `ul.selected > li button:hover, button.remove-all:hover, button:focus`
243
253
  - `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.
244
254
  - `div.multiselect > ul.options`
@@ -249,13 +259,10 @@ If you only want to make small adjustments, you can pass the following CSS varia
249
259
  - `div.multiselect > ul.options > li`
250
260
  - `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.
251
261
  - `div.multiselect > ul.options > li.selected`
252
- - `border-left: var(--sms-li-selected-border-left, 3pt solid var(--sms-selected-color, green))`
253
- - `background: var(--sms-li-selected-bg, inherit)`: Background of selected list items in options pane.
254
- - `color: var(--sms-li-selected-color, inherit)`: Text color of selected list items in options pane.
255
- - `div.multiselect > ul.options > li:not(.selected):hover`
256
- - `border-left: var(--sms-li-not-selected-hover-border-left, 3pt solid var(--sms-active-color, cornflowerblue))`
262
+ - `background: var(--sms-li-selected-bg)`: Background of selected list items in options pane.
263
+ - `color: var(--sms-li-selected-color)`: Text color of selected list items in options pane.
257
264
  - `div.multiselect > ul.options > li.active`
258
- - `background: var(--sms-li-active-bg, var(--sms-active-color, cornflowerblue))`: Background of active (currently with arrow keys highlighted) list item.
265
+ - `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.
259
266
  - `div.multiselect > ul.options > li.disabled`
260
267
  - `background: var(--sms-li-disabled-bg, #f5f5f6)`: Background of disabled options in the dropdown list.
261
268
  - `color: var(--sms-li-disabled-text, #b8b8b8)`: Text color of disabled option in the dropdown list.
@@ -305,8 +312,8 @@ You can alternatively style every part of this component with more fine-grained
305
312
  :global(div.multiselect.open) {
306
313
  /* top-level wrapper div when dropdown open */
307
314
  }
308
- :global(div.multiselect.readonly) {
309
- /* top-level wrapper div when in readonly state */
315
+ :global(div.multiselect.disabled) {
316
+ /* top-level wrapper div when in disabled state */
310
317
  }
311
318
  :global(div.multiselect > ul.selected) {
312
319
  /* selected list */
@@ -344,6 +351,40 @@ You can alternatively style every part of this component with more fine-grained
344
351
  }
345
352
  ```
346
353
 
354
+ ## Downstream testing
355
+
356
+ To test a Svelte component which imports `svelte-multiselect`, you need to configure your test runner to avoid [transpiling issues](https://github.com/EmilTholin/svelte-routing/issues/140#issuecomment-661682571).
357
+
358
+ For Jest, exclude `svelte-multiselect` from `transformIgnorePatterns` in your `jest.config.json`:
359
+
360
+ ```json
361
+ {
362
+ "transformIgnorePatterns": ["node_modules/?!(svelte-multiselect)"],
363
+ "transform": {
364
+ "^.+\\.[t|j]s?$": "esbuild-jest",
365
+ "^.+\\.svelte$": ["svelte-jester", { "preprocess": true }]
366
+ }
367
+ }
368
+ ```
369
+
370
+ For Vitest, include `svelte-multiselect` in `deps.inline`:
371
+
372
+ ```ts
373
+ // vite.config.ts
374
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
375
+
376
+ export default {
377
+ plugins: [svelte({ hot: !process.env.VITEST })],
378
+ test: {
379
+ deps: {
380
+ inline: [/svelte-multiselect/],
381
+ },
382
+ },
383
+ }
384
+ ```
385
+
386
+ 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).
387
+
347
388
  ## Want to contribute?
348
389
 
349
390
  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
- }