Skip to content

Commit 9287494

Browse files
authored
New prop resetFilterOnAdd (#137)
* clean up {Repo,Color}Slot.svelte * unskip test parseLabelsAsHtml+allowUserOptions gives errors * set playwright test mode to parallel now uses 5 instead of 1 workers * add IconifySlot.svelte used in Examples.svelte with new octicon options, pnpm add -D @iconify/svelte * refactor maxSelectMsg prop and invalid reset handling when new selected options change * assert aria-invalid attribute is removed on selecting a new option in div has 'invalid' class and input is aria-invalid when invalid=true * add prop resetFilterOnAdd Whether text entered into the input to filter options in the dropdown list is reset to empty string when user selects an option. * add test 'resetFilterOnAdd handles input value correctly after adding an option'
1 parent 914111e commit 9287494

File tree

12 files changed

+166
-71
lines changed

12 files changed

+166
-71
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"test": "vitest --run tests/unit/*.ts && playwright test tests/*.test.ts"
2121
},
2222
"devDependencies": {
23+
"@iconify/svelte": "^3.0.0",
2324
"@playwright/test": "^1.27.1",
2425
"@sveltejs/adapter-static": "^1.0.0-next.44",
2526
"@sveltejs/kit": "^1.0.0-next.516",

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readme.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ import type { Option } from 'svelte-multiselect'
217217
Positive integer to limit the number of options users can pick. `null` means no limit. `maxSelect={1}` will change the type of `selected` to be a single `Option` (or `null`) (not a length-1 array). Likewise, the type of `selectedLabels` changes from `(string | number)[]` to `string | number | null` and `selectedValues` from `unknown[]` to `unknown | null`. `maxSelect={1}` will also give `div.multiselect` a class of `single`. I.e. you can target the selector `div.multiselect.single` to give single selects a different appearance from multi selects.
218218

219219
1. ```ts
220-
maxSelectMsg: ((current: number, max: number) => string) | null = null
220+
maxSelectMsg: ((current: number, max: number) => string) | null = (
221+
current: number,
222+
max: number
223+
) => (max > 1 ? `${current}/${max}` : ``)
221224
```
222225

223226
Inform users how many of the maximum allowed options they have already selected. Set `maxSelectMsg={null}` to not show a message. Defaults to `null` when `maxSelect={1}` or `maxSelect={null}`. Else if `maxSelect > 1`, defaults to:
@@ -292,6 +295,12 @@ import type { Option } from 'svelte-multiselect'
292295

293296
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.
294297

298+
1. ```ts
299+
resetFilterOnAdd: boolean = true
300+
```
301+
302+
Whether text entered into the input to filter options in the dropdown list is reset to empty string when user selects an option.
303+
295304
1. ```ts
296305
searchText: string = ``
297306
```

src/app.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,6 @@ blockquote p:last-child {
117117
blockquote p:first-child {
118118
margin-top: 0;
119119
}
120+
div.multiselect.invalid {
121+
border: 1px solid red !important;
122+
}

src/components/ColorSlot.svelte

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
<script lang="ts">
2-
import type { ObjectOption } from '$lib'
3-
4-
export let option: ObjectOption
2+
export let option: string
53
export let idx: number
64
</script>
75

8-
<span>
9-
<small>{idx + 1}</small>
10-
<span style:background={option.label} />
11-
{option.label}
12-
</span>
6+
<div>
7+
{idx + 1}
8+
<span style:background={option} />
9+
{option}
10+
</div>
1311

1412
<style>
15-
span {
13+
div {
1614
display: flex;
1715
place-items: center;
18-
gap: 3pt;
19-
}
20-
span small {
21-
width: 10pt;
22-
text-align: right;
16+
gap: 5pt;
2317
}
24-
span > span {
18+
div > span {
2519
width: 1em;
2620
height: 1em;
27-
display: inline-block;
2821
border-radius: 2pt;
29-
margin: 0 5pt;
30-
vertical-align: middle;
3122
}
3223
</style>

src/components/Examples.svelte

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
<script lang="ts">
22
import type { ObjectOption } from '$lib'
33
import MultiSelect from '$lib'
4-
import { colors, frontend_libs, languages, ml_libs } from '../options'
4+
import { colors, frontend_libs, languages, ml_libs, octicons } from '../options'
55
import { language_store } from '../stores'
66
import ColorSlot from './ColorSlot.svelte'
77
import Confetti from './Confetti.svelte'
8+
import IconifySlotSlot from './IconifySlot.svelte'
89
import LanguageSlot from './LanguageSlot.svelte'
910
import RepoSlot from './RepoSlot.svelte'
1011
1112
let selected_ml: string[]
12-
let selected_colors: ObjectOption[]
13+
let selected_colors = [`red`, `orange`, `yellow`]
1314
1415
let showConfetti = false
1516
16-
const filterFunc = (op: ObjectOption, searchText: string) => {
17+
const frontend_libs_filter_func = (op: ObjectOption, searchText: string) => {
1718
if (!searchText) return true
1819
const [label, lang, searchStr] = [op.label, op.lang, searchText].map((s) =>
1920
`${s}`.toLowerCase()
@@ -50,10 +51,12 @@
5051
<section>
5152
<h3>Single Select</h3>
5253

53-
<label for="fav-ml-tool">with loading indicator on text input</label>
54+
<p>with loading indicator on text input</p>
5455

5556
<pre>selected = {JSON.stringify(selected_ml)}</pre>
5657

58+
<label for="fav-ml-tool">Favorite machine learning framework?</label>
59+
5760
<MultiSelect
5861
id="fav-ml-tool"
5962
maxSelect={1}
@@ -76,7 +79,7 @@
7679
options={frontend_libs}
7780
maxSelect={4}
7881
placeholder="Favorite web framework?"
79-
{filterFunc}
82+
filterFunc={frontend_libs_filter_func}
8083
on:add={(e) => {
8184
if (e.detail.option.label === `Svelte`) {
8285
showConfetti = true
@@ -100,7 +103,7 @@
100103
</label>
101104
<form
102105
on:submit|preventDefault={() => {
103-
alert(`You selected '${selected_colors.map((el) => el.label).join(`, `)}'`)
106+
alert(`You selected '${selected_colors.join(`, `)}'`)
104107
}}
105108
>
106109
<MultiSelect
@@ -126,12 +129,36 @@
126129
</form>
127130
</section>
128131

132+
<section>
133+
<h3>Very long Multi Select</h3>
134+
135+
<label for="octicons">List of GitHub's Octicons</label>
136+
137+
<MultiSelect
138+
id="octicons"
139+
options={octicons}
140+
placeholder="Take your pick..."
141+
maxSelect={20}
142+
maxSelectMsg={(current, max) =>
143+
current == max ? `Hold your horses!` : `${current} of ${max}`}
144+
>
145+
<IconifySlotSlot let:option {option} slot="selected" />
146+
<IconifySlotSlot let:option {option} slot="option" />
147+
</MultiSelect>
148+
</section>
149+
129150
<style>
130151
section {
131152
margin-top: 2em;
132153
background-color: #28154b;
133-
border-radius: 1ex;
134-
padding: 1pt 1.4ex;
154+
border-radius: 4pt;
155+
padding: 1pt 10pt;
156+
}
157+
section h3 {
158+
margin: 5pt 0 10pt;
159+
}
160+
section p {
161+
margin: 5pt 0;
135162
}
136163
pre {
137164
white-space: pre-wrap;

src/components/IconifySlot.svelte

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script lang="ts">
2+
import Icon from '@iconify/svelte'
3+
4+
export let option: string
5+
export let pack = `octicon`
6+
export let gap = `9pt`
7+
</script>
8+
9+
<span style:gap>
10+
<Icon icon="{pack}:{option}" inline />
11+
{option.replaceAll(`-`, ` `)}
12+
</span>
13+
14+
<style>
15+
span {
16+
display: flex;
17+
align-items: center;
18+
text-transform: capitalize;
19+
}
20+
</style>

src/components/RepoSlot.svelte

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
</script>
88

99
<span>
10-
<small>{idx + 1}</small>
10+
{idx + 1}
1111
<strong>{option.label}</strong>
1212
<small>
1313
<a
14+
on:click|stopPropagation
1415
href="https://github.com/{option.repo_handle}"
1516
target="_blank"
1617
rel="noreferrer"
17-
on:click|stopPropagation
1818
>
1919
<Octocat width="14pt" />{option.repo_handle}
2020
</a>
@@ -27,15 +27,9 @@
2727
place-items: center;
2828
gap: 10pt;
2929
}
30-
span small:first-child {
31-
width: 6pt;
32-
text-align: right;
33-
}
3430
a {
3531
display: flex;
3632
gap: 3pt;
37-
font-style: normal;
3833
font-weight: lighter;
39-
place-items: center;
4034
}
4135
</style>

src/lib/MultiSelect.svelte

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
export let loading: boolean = false
3737
export let matchingOptions: Option[] = []
3838
export let maxSelect: number | null = null // null means any number of options are selectable
39-
export let maxSelectMsg: ((current: number, max: number) => string) | null = null
39+
export let maxSelectMsg: ((current: number, max: number) => string) | null = (
40+
current: number,
41+
max: number
42+
) => (max > 1 ? `${current}/${max}` : ``)
4043
export let name: string | null = null
4144
export let noMatchingOptionsMsg: string = `No matching options`
4245
export let open: boolean = false
@@ -49,6 +52,7 @@
4952
export let removeAllTitle: string = `Remove all`
5053
export let removeBtnTitle: string = `Remove`
5154
export let required: boolean = false
55+
export let resetFilterOnAdd: boolean = true
5256
export let searchText: string = ``
5357
export let selected: Option[] | Option | null =
5458
options
@@ -106,11 +110,6 @@
106110
let add_option_msg_is_active: boolean = false // controls active state of <li>{addOptionMsg}</li>
107111
let window_width: number
108112
109-
// formValue binds to input.form-control to prevent form submission if required
110-
// prop is true and no options are selected
111-
$: form_value = _selectedValues.join(`,`)
112-
$: if (form_value) invalid = false // reset error status whenever component state changes
113-
114113
// options matching the current search text
115114
$: matchingOptions = options.filter(
116115
(op) => filterFunc(op, searchText) && !_selectedLabels.includes(get_label(op)) // remove already selected options from dropdown list
@@ -161,7 +160,7 @@
161160
if (option === undefined) {
162161
throw `Run time error, option with label ${label} not found in options list`
163162
}
164-
searchText = `` // reset search string on selection
163+
if (resetFilterOnAdd) searchText = `` // reset search string on selection
165164
if ([``, undefined, null].includes(option)) {
166165
console.error(
167166
`MultiSelect: encountered missing option with label ${label} (or option is poorly labeled)`
@@ -192,6 +191,8 @@
192191
}
193192
dispatch(`add`, { option })
194193
dispatch(`change`, { option, type: `add` })
194+
195+
invalid = false // reset error status whenever new items are selected
195196
}
196197
}
197198
@@ -215,6 +216,7 @@
215216
216217
dispatch(`remove`, { option })
217218
dispatch(`change`, { option, type: `remove` })
219+
invalid = false // reset error status whenever items are removed
218220
}
219221
220222
function open_dropdown(event: Event) {
@@ -338,10 +340,10 @@
338340
title={disabled ? disabledInputTitle : null}
339341
aria-disabled={disabled ? `true` : null}
340342
>
341-
<!-- formValue binds to input.form-control to prevent form submission if required prop is true and no options are selected -->
343+
<!-- bind:value={_selected} prevents form submission if required prop is true and no options are selected -->
342344
<input
343345
{required}
344-
bind:value={form_value}
346+
bind:value={_selected}
345347
tabindex="-1"
346348
aria-hidden="true"
347349
aria-label="ignore this, used only to prevent form submission if select is required but empty"
@@ -419,8 +421,7 @@
419421
{#if maxSelect && (maxSelect > 1 || maxSelectMsg)}
420422
<Wiggle bind:wiggle angle={20}>
421423
<span style="padding: 0 3pt;">
422-
{maxSelectMsg?.(_selected.length, maxSelect) ??
423-
(maxSelect > 1 ? `${_selected.length}/${maxSelect}` : ``)}
424+
{maxSelectMsg?.(_selected.length, maxSelect)}
424425
</span>
425426
</Wiggle>
426427
{/if}
@@ -454,7 +455,7 @@
454455
<li
455456
on:mousedown|stopPropagation
456457
on:mouseup|stopPropagation={(event) => {
457-
if (!disabled) is_selected(label) ? remove(label) : add(label, event)
458+
if (!disabled) add(label, event)
458459
}}
459460
title={disabled
460461
? disabledTitle

0 commit comments

Comments
 (0)