svelte-multiselect 4.0.2 → 4.0.5

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,9 +1,8 @@
1
- <script >import { createEventDispatcher, onMount, tick } from 'svelte';
1
+ <script >import { createEventDispatcher, tick } from 'svelte';
2
2
  import { fly } from 'svelte/transition';
3
3
  import CircleSpinner from './CircleSpinner.svelte';
4
4
  import { CrossIcon, ExpandIcon, DisabledIcon } from './icons';
5
5
  import Wiggle from './Wiggle.svelte';
6
- export let selected = [];
7
6
  export let selectedLabels = [];
8
7
  export let selectedValues = [];
9
8
  export let searchText = ``;
@@ -13,7 +12,9 @@ export let maxSelectMsg = null;
13
12
  export let disabled = false;
14
13
  export let disabledTitle = `This field is disabled`;
15
14
  export let options;
15
+ export let selected = options.filter((op) => op?.preselected) ?? [];
16
16
  export let input = null;
17
+ export let outerDiv = null;
17
18
  export let placeholder = undefined;
18
19
  export let id = undefined;
19
20
  export let name = id;
@@ -30,6 +31,7 @@ export let liSelectedClass = ``;
30
31
  export let ulOptionsClass = ``;
31
32
  export let liOptionClass = ``;
32
33
  export let liActiveOptionClass = ``;
34
+ export let inputClass = ``;
33
35
  export let removeBtnTitle = `Remove`;
34
36
  export let removeAllTitle = `Remove all`;
35
37
  export let defaultDisabledTitle = `This option is disabled`;
@@ -46,25 +48,21 @@ if (!(options?.length > 0))
46
48
  console.error(`MultiSelect missing options`);
47
49
  if (!Array.isArray(selected))
48
50
  console.error(`selected prop must be an array`);
49
- onMount(() => {
50
- selected = _options.filter((op) => op?.preselected) ?? [];
51
- });
52
51
  const dispatch = createEventDispatcher();
53
52
  function isObject(item) {
54
53
  return typeof item === `object` && !Array.isArray(item) && item !== null;
55
54
  }
56
55
  // process proto options to full ones with mandatory labels
57
56
  $: _options = options.map((rawOp) => {
58
- // convert to objects internally if user passed list of strings or numbers as options
59
57
  if (isObject(rawOp)) {
60
- const op = { ...rawOp };
61
- if (!op.value)
62
- op.value = op.label;
63
- return op;
58
+ const option = { ...rawOp };
59
+ if (option.value === undefined)
60
+ option.value = option.label;
61
+ return option;
64
62
  }
65
63
  else {
66
64
  if (![`string`, `number`].includes(typeof rawOp)) {
67
- console.error(`MultiSelect options must be objects, strings or numbers, got ${typeof rawOp}`);
65
+ console.warn(`MultiSelect options must be objects, strings or numbers, got ${typeof rawOp}`);
68
66
  }
69
67
  // even if we logged error above, try to proceed hoping user knows what they're doing
70
68
  return { label: rawOp, value: rawOp };
@@ -72,7 +70,7 @@ $: _options = options.map((rawOp) => {
72
70
  });
73
71
  $: labels = _options.map((op) => op.label);
74
72
  $: if (new Set(labels).size !== options.length) {
75
- console.error(`Option labels must be unique. Duplicates found: ${labels.filter((label, idx) => labels.indexOf(label) !== idx)}`);
73
+ console.warn(`Option labels should be unique. Duplicates found: ${labels.filter((label, idx) => labels.indexOf(label) !== idx)}`);
76
74
  }
77
75
  let wiggle = false;
78
76
  $: selectedLabels = selected.map((op) => op.label);
@@ -136,8 +134,8 @@ function setOptionsVisible(show) {
136
134
  }
137
135
  // handle all keyboard events this component receives
138
136
  async function handleKeydown(event) {
139
- // on escape: dismiss options dropdown and reset search text
140
- if (event.key === `Escape`) {
137
+ // on escape or tab out of input: dismiss options dropdown and reset search text
138
+ if (event.key === `Escape` || event.key === `Tab`) {
141
139
  setOptionsVisible(false);
142
140
  searchText = ``;
143
141
  }
@@ -208,9 +206,18 @@ const handleEnterAndSpaceKeys = (handler) => (event) => {
208
206
  };
209
207
  </script>
210
208
 
209
+ <svelte:window
210
+ on:click={(event) => {
211
+ if (outerDiv && !outerDiv.contains(event.target)) {
212
+ setOptionsVisible(false)
213
+ }
214
+ }}
215
+ />
216
+
211
217
  <!-- z-index: 2 when showOptions is true ensures the ul.selected of one <MultiSelect />
212
218
  display above those of another following shortly after it -->
213
219
  <div
220
+ bind:this={outerDiv}
214
221
  class:disabled
215
222
  class:single={maxSelect === 1}
216
223
  class:open={showOptions}
@@ -219,16 +226,15 @@ display above those of another following shortly after it -->
219
226
  class:invalid
220
227
  class="multiselect {outerDivClass}"
221
228
  on:mouseup|stopPropagation={() => setOptionsVisible(true)}
222
- on:focusout={() => setOptionsVisible(false)}
223
229
  title={disabled ? disabledTitle : null}
224
230
  aria-disabled={disabled ? `true` : null}
225
231
  >
226
- <!-- invisible input, used only to prevent form submission if required=true and no options selected -->
227
232
  <input
228
233
  {required}
229
234
  bind:value={formValue}
230
235
  tabindex="-1"
231
236
  aria-hidden="true"
237
+ aria-label="ignore this, used only to prevent form submission if select is required but empty"
232
238
  class="form-control"
233
239
  on:invalid={() => (invalid = true)}
234
240
  />
@@ -253,13 +259,13 @@ display above those of another following shortly after it -->
253
259
  {/each}
254
260
  <li style="display: contents;">
255
261
  <input
262
+ class={inputClass}
256
263
  bind:this={input}
257
264
  {autocomplete}
258
265
  bind:value={searchText}
259
266
  on:mouseup|self|stopPropagation={() => setOptionsVisible(true)}
260
267
  on:keydown={handleKeydown}
261
268
  on:focus={() => setOptionsVisible(true)}
262
- on:blur={() => setOptionsVisible(false)}
263
269
  {id}
264
270
  {name}
265
271
  {disabled}
@@ -350,13 +356,14 @@ display above those of another following shortly after it -->
350
356
  align-items: center;
351
357
  display: flex;
352
358
  cursor: text;
353
- padding: 0 3pt;
354
359
  border: var(--sms-border, 1pt solid lightgray);
355
360
  border-radius: var(--sms-border-radius, 3pt);
356
- background: var(--sms-input-bg);
357
- min-height: var(--sms-input-min-height, 22pt);
361
+ background: var(--sms-bg);
362
+ max-width: var(--sms-max-width);
363
+ padding: var(--sms-padding, 0 3pt);
358
364
  color: var(--sms-text-color);
359
365
  font-size: var(--sms-font-size, inherit);
366
+ min-height: var(--sms-min-height, 19pt);
360
367
  }
361
368
  :where(div.multiselect.open) {
362
369
  z-index: var(--sms-open-z-index, 4);
@@ -378,15 +385,14 @@ display above those of another following shortly after it -->
378
385
  }
379
386
  :where(div.multiselect > ul.selected > li) {
380
387
  align-items: center;
381
- border-radius: 4pt;
388
+ border-radius: 3pt;
382
389
  display: flex;
383
390
  margin: 2pt;
384
391
  line-height: normal;
385
- padding: 1pt 5pt;
386
392
  transition: 0.3s;
387
393
  white-space: nowrap;
388
394
  background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
389
- height: var(--sms-selected-li-height);
395
+ padding: var(--sms-selected-li-padding, 1pt 5pt);
390
396
  color: var(--sms-selected-text-color, var(--sms-text-color));
391
397
  }
392
398
  :where(div.multiselect button) {
@@ -2,7 +2,6 @@ import { SvelteComponentTyped } from "svelte";
2
2
  import type { Option, Primitive, ProtoOption } from './';
3
3
  declare const __propDef: {
4
4
  props: {
5
- selected?: Option[] | undefined;
6
5
  selectedLabels?: Primitive[] | undefined;
7
6
  selectedValues?: Primitive[] | undefined;
8
7
  searchText?: string | undefined;
@@ -12,7 +11,9 @@ declare const __propDef: {
12
11
  disabled?: boolean | undefined;
13
12
  disabledTitle?: string | undefined;
14
13
  options: ProtoOption[];
14
+ selected?: Option[] | undefined;
15
15
  input?: HTMLInputElement | null | undefined;
16
+ outerDiv?: HTMLDivElement | null | undefined;
16
17
  placeholder?: string | undefined;
17
18
  id?: string | undefined;
18
19
  name?: string | undefined;
@@ -25,6 +26,7 @@ declare const __propDef: {
25
26
  ulOptionsClass?: string | undefined;
26
27
  liOptionClass?: string | undefined;
27
28
  liActiveOptionClass?: string | undefined;
29
+ inputClass?: string | undefined;
28
30
  removeBtnTitle?: string | undefined;
29
31
  removeAllTitle?: string | undefined;
30
32
  defaultDisabledTitle?: string | undefined;
package/package.json CHANGED
@@ -5,37 +5,37 @@
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.2",
8
+ "version": "4.0.5",
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.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",
14
+ "@sveltejs/kit": "^1.0.0-next.302",
15
+ "@sveltejs/vite-plugin-svelte": "^1.0.0-next.40",
16
+ "@typescript-eslint/eslint-plugin": "^5.16.0",
17
+ "@typescript-eslint/parser": "^5.16.0",
18
+ "@vitest/ui": "^0.7.9",
19
19
  "eslint": "^8.11.0",
20
20
  "eslint-plugin-svelte3": "^3.4.1",
21
21
  "hastscript": "^7.0.2",
22
22
  "jsdom": "^19.0.0",
23
23
  "mdsvex": "^0.10.5",
24
- "playwright": "^1.19.2",
25
- "prettier": "^2.5.1",
24
+ "playwright": "^1.20.0",
25
+ "prettier": "^2.6.0",
26
26
  "prettier-plugin-svelte": "^2.6.0",
27
27
  "rehype-autolink-headings": "^6.1.1",
28
28
  "rehype-slug": "^5.0.1",
29
29
  "svelte": "^3.46.4",
30
- "svelte-check": "^2.4.5",
30
+ "svelte-check": "^2.4.6",
31
31
  "svelte-github-corner": "^0.1.0",
32
32
  "svelte-preprocess": "^4.10.4",
33
- "svelte-toc": "^0.2.7",
34
- "svelte2tsx": "^0.5.5",
33
+ "svelte-toc": "^0.2.9",
34
+ "svelte2tsx": "^0.5.6",
35
35
  "tslib": "^2.3.1",
36
36
  "typescript": "^4.6.2",
37
37
  "vite": "^2.8.6",
38
- "vitest": "^0.6.0"
38
+ "vitest": "^0.7.9"
39
39
  },
40
40
  "keywords": [
41
41
  "svelte",
package/readme.md CHANGED
@@ -1,19 +1,22 @@
1
1
  <h1 align="center">
2
- <img src="https://raw.githubusercontent.com/janosh/svelte-toc/main/static/favicon.svg" alt="Svelte MultiSelect" height=60>
3
- <br>&ensp;Svelte MultiSelect
2
+ <img src="https://raw.githubusercontent.com/janosh/svelte-multiselect/main/static/favicon.svg" alt="Svelte MultiSelect" height="60" width="60">
3
+ <br class="hide-in-docs">&ensp;Svelte MultiSelect
4
4
  </h1>
5
5
 
6
6
  <h4 align="center">
7
7
 
8
+ [![REPL](https://img.shields.io/badge/Svelte-REPL-blue)](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
8
9
  [![Tests](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml/badge.svg)](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
9
10
  [![Netlify Status](https://api.netlify.com/api/v1/badges/a45b62c3-ea45-4cfd-9912-77ec4fc8d7e8/deploy-status)](https://app.netlify.com/sites/svelte-multiselect/deploys)
10
- [![NPM version](https://img.shields.io/npm/v/svelte-multiselect?color=blue&logo=NPM)](https://npmjs.com/package/svelte-multiselect)
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)
12
- [![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/dev/svelte)](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
11
+ [![NPM version](https://img.shields.io/npm/v/svelte-multiselect?logo=NPM&color=purple)](https://npmjs.com/package/svelte-multiselect)
12
+ [![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/dev/svelte?color=teal)](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
13
13
 
14
14
  </h4>
15
15
 
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
+ **Keyboard-friendly, accessible multi-select Svelte component.**
17
+ <strong class="hide-in-docs">
18
+ <a href="https://svelte-multiselect.netlify.app">Docs</a>
19
+ </strong>
17
20
 
18
21
  <slot name="examples" />
19
22
 
@@ -48,6 +51,8 @@
48
51
 
49
52
  - 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
53
 
54
+ - v4.0.3 CSS variables starting with `--sms-input-<attr>` were renamed to just `--sms-<attr>`. E.g. `--sms-input-min-height` is now `--sms-min-height`.
55
+
51
56
  ## Installation
52
57
 
53
58
  ```sh
@@ -93,7 +98,8 @@ Full list of props/bindable variables for this component:
93
98
  | `disabled` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
94
99
  | `disabledTitle` | `This field is disabled` | Tooltip text to display on hover when the component is in `disabled` state. |
95
100
  | `placeholder` | `undefined` | String shown in the text input when no option is selected. |
96
- | `input` | `undefined` | Handle to the `<input>` DOM node. |
101
+ | `input` | `null` | Handle to the `<input>` DOM node. Only available after component mounts (`null` before then). |
102
+ | `outerDiv` | `null` | Handle to outer `<div class="multiselect">` that wraps the whole component. Only available after component mounts (`null` before then). |
97
103
  | `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
104
  | `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
105
  | `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. |
@@ -137,15 +143,17 @@ Full list of props/bindable variables for this component:
137
143
  Example:
138
144
 
139
145
  ```svelte
140
- <MultiSelect options={[`Banana`, `Apple`, `Mango`]}>
146
+ <MultiSelect options={[`Red`, `Green`, `Blue`, `Yellow`, `Purple`]}>
141
147
  <span let:idx let:option slot="option">
142
- {idx + 1}. {option.label}
143
- {option.label === `Mango` ? `🎉` : ``}
148
+ {idx + 1}
149
+ {option.label}
150
+ <span style:background={option.label} style=" width: 1em; height: 1em;" />
144
151
  </span>
145
152
 
146
153
  <span let:idx let:option slot="selected">
147
- #{idx + 1}
154
+ {idx + 1}
148
155
  {option.label}
156
+ <span style:background={option.label} style=" width: 1em; height: 1em;" />
149
157
  </span>
150
158
 
151
159
  <CustomSpinner slot="spinner">
@@ -156,13 +164,13 @@ Example:
156
164
 
157
165
  `MultiSelect.svelte` dispatches the following events:
158
166
 
159
- | name | detail | description |
160
- | ----------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
161
- | `add` | `{ option: Option }` | Triggers when a new option is selected. |
162
- | `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
163
- | `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
164
- | `change` | `{ option?: Option, options?: Option[] }`, `type: 'add' \| 'remove' \| 'removeAll'` | Triggers when a option is either added or removed, or all options are removed at once. |
165
- | `blur` | none | Triggers when the input field looses focus. |
167
+ | name | detail | description |
168
+ | ----------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
169
+ | `add` | `{ option: Option }` | Triggers when a new option is selected. |
170
+ | `remove` | `{ option: Option }` | Triggers when one selected option provided as `event.detail.option` is removed. |
171
+ | `removeAll` | `options: Option[]` | Triggers when all selected options are removed. The payload `event.detail.options` gives the options that were previously selected. |
172
+ | `change` | `type: 'add' \| 'remove' \| 'removeAll'` | Triggers when a option is either added or removed, or all options are removed at once. Payload will be a single or an aarray of `Option` objects, respectively. |
173
+ | `blur` | none | Triggers when the input field looses focus. |
166
174
 
167
175
  ### Examples
168
176
 
@@ -221,19 +229,22 @@ If you only want to make small adjustments, you can pass the following CSS varia
221
229
  - `div.multiselect`
222
230
  - `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.
223
231
  - `border-radius: var(--sms-border-radius, 3pt)`
224
- - `background: var(--sms-input-bg)`
225
- - `height: var(--sms-input-height, 2em)`
232
+ - `padding: var(--sms-padding, 0 3pt)`
233
+ - `background: var(--sms-bg)`
226
234
  - `color: var(--sms-text-color)`
227
- - `color: var(--sms-placeholder-color)`
235
+ - `min-height: var(--sms-min-height)`
236
+ - `max-width: var(--sms-max-width)`
228
237
  - `div.multiselect.open`
229
238
  - `z-index: var(--sms-open-z-index, 4)`: Increase this if needed to ensure the dropdown list is displayed atop all other page elements.
230
239
  - `div.multiselect:focus-within`
231
240
  - `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`.
232
241
  - `div.multiselect.disabled`
233
242
  - `background: var(--sms-disabled-bg, lightgray)`: Background when in disabled state.
243
+ - `div.multiselect input::placeholder`
244
+ - `color: var(--sms-placeholder-color)`
234
245
  - `div.multiselect > ul.selected > li`
235
246
  - `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
236
- - `height: var(--sms-selected-li-height)`: Height of selected options.
247
+ - `padding: var(--sms-selected-li-padding, 5pt 1pt)`: Height of selected options.
237
248
  - `color: var(--sms-selected-text-color, var(--sms-text-color))`: Text color for selected options.
238
249
  - `ul.selected > li button:hover, button.remove-all:hover, button:focus`
239
250
  - `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.
@@ -274,6 +285,7 @@ This simplified version of the DOM structure of this component shows where these
274
285
 
275
286
  ```svelte
276
287
  <div class="multiselect {outerDivClass}">
288
+ <input class={inputClass} />
277
289
  <ul class="selected {ulSelectedClass}">
278
290
  <li class={liSelectedClass}>Selected 1</li>
279
291
  <li class={liSelectedClass}>Selected 2</li>
@@ -369,7 +381,7 @@ export default {
369
381
  }
370
382
  ```
371
383
 
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).
384
+ 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
385
 
374
386
  ## Want to contribute?
375
387