Skip to content

Commit 5772985

Browse files
authored
Multiselect portal (#306)
* fix drag and drop selected options and reinstate playwright test also reinstate multiselect test 'opens dropdown on focus', no longer fails * more CmdPalette coverage with vitest in tests/unit/CmdPalette.svelte.test.ts * simplify CSS scoping * add/remove use generic T for option arg type, rename arg for clarity * minor refactor CmdPalette * fix escape key navigating back to landing page from demo pages * add portal functionality to MultiSelect component (closes #301) to extend dropdown outside of parent component (e.g. inside a modal dialog) - new `portal` function manages dropdown rendering if portal prop is not empty - `MultiSelectProps` include optional `portal` parameter to enable and customize portal functionality - refactored the `add` function to simplify max selection logic - improved CSS for dropdown positioning and z-index management when using the portal * add /modal/+page.svelte demo page for portal feature * playwright test for MultiSelect portal feature
1 parent 05a4013 commit 5772985

14 files changed

+592
-139
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ repos:
3838
exclude: changelog\.md
3939

4040
- repo: https://github.com/pre-commit/mirrors-eslint
41-
rev: v9.26.0
41+
rev: v9.27.0
4242
hooks:
4343
- id: eslint
4444
types: [file]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"package": "svelte-package",
1717
"serve": "vite build && vite preview",
1818
"check": "svelte-check src",
19-
"test": "vitest --run --coverage tests/unit/*.ts && npm run test:e2e",
19+
"test": "vitest run && playwright test",
2020
"test:unit": "vitest tests/unit/*.ts",
2121
"test:e2e": "playwright test tests/*.test.ts",
2222
"changelog": "npx auto-changelog --package --output changelog.md --hide-empty-releases --hide-credit --commit-limit false",

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[![Tests](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml/badge.svg)](https://github.com/janosh/svelte-multiselect/actions/workflows/test.yml)
99
[![GitHub Pages](https://github.com/janosh/svelte-multiselect/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/janosh/svelte-multiselect/actions/workflows/gh-pages.yml)
1010
[![NPM version](https://img.shields.io/npm/v/svelte-multiselect?logo=NPM&color=purple)](https://npmjs.com/package/svelte-multiselect)
11-
[![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/svelte?color=teal&logo=Svelte&label=Svelte)](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md)
11+
[![Needs Svelte version](https://img.shields.io/npm/dependency-version/svelte-multiselect/peer/svelte?color=teal&logo=Svelte&label=Svelte)](https://github.com/sveltejs/svelte/blob/master/packages/svelte/CHANGELOG.md)
1212
[![REPL](https://img.shields.io/badge/Svelte-REPL-blue?label=Try%20it!)](https://svelte.dev/repl/a5a14b8f15d64cb083b567292480db05)
1313
[![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-darkblue?logo=stackblitz)](https://stackblitz.com/github/janosh/svelte-multiselect)
1414

src/app.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
--night: #0b0b1b;
33
--blue: cornflowerblue;
44
--text-color: #ccc;
5+
--main-max-width: 50em;
56

67
--sms-active-color: rgba(255, 255, 255, 0.1);
78
--sms-focus-border: 1pt solid cornflowerblue;
@@ -28,7 +29,7 @@ main {
2829
margin: auto;
2930
margin-bottom: 3em;
3031
width: 100%;
31-
max-width: 50em;
32+
max-width: var(--main-max-width);
3233
}
3334
button,
3435
a.btn {
@@ -139,7 +140,7 @@ div.multiselect.invalid {
139140
aside.toc.desktop {
140141
position: fixed;
141142
top: 3em;
142-
left: calc(50vw + 50em / 2);
143+
left: calc(50vw + var(--main-max-width) / 2);
143144
max-width: 16em;
144145
}
145146

src/lib/CmdPalette.svelte

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<script lang="ts">
2-
import { tick, type Snippet } from 'svelte'
32
import { fade } from 'svelte/transition'
4-
53
import Select from './MultiSelect.svelte'
64
import type { MultiSelectProps } from './props'
75
import type { ObjectOption } from './types'
@@ -22,34 +20,31 @@
2220
fade_duration?: number // in ms
2321
style?: string // for dialog
2422
// for span in option snippet, has no effect when specifying a custom option snippet
25-
span_style?: string
2623
open?: boolean
2724
dialog?: HTMLDialogElement | null
2825
input?: HTMLInputElement | null
2926
placeholder?: string
30-
children?: Snippet
3127
}
3228
let {
3329
actions,
3430
triggers = [`k`],
3531
close_keys = [`Escape`],
3632
fade_duration = 200,
3733
style = ``,
38-
span_style = ``,
3934
open = $bindable(false),
4035
dialog = $bindable(null),
4136
input = $bindable(null),
4237
placeholder = `Filter actions...`,
43-
children,
4438
...rest
4539
}: Props = $props()
4640
41+
$effect(() => {
42+
if (open && input) input?.focus() // focus input when palette is opened
43+
})
44+
4745
async function toggle(event: KeyboardEvent) {
4846
if (triggers.includes(event.key) && event.metaKey && !open) {
49-
// open on cmd+trigger
5047
open = true
51-
await tick() // wait for dialog to open and input to be mounted
52-
input?.focus()
5348
} else if (close_keys.includes(event.key) && open) {
5449
open = false
5550
}
@@ -65,8 +60,6 @@
6560
option.action(option.label)
6661
open = false
6762
}
68-
69-
const children_render = $derived(children)
7063
</script>
7164

7265
<svelte:window onkeydown={toggle} onclick={close_if_outside} />
@@ -80,22 +73,18 @@
8073
onadd={trigger_action_and_close}
8174
onkeydown={toggle}
8275
{...rest}
83-
>
84-
{#snippet children({ option })}
85-
<!-- wait for https://github.com/sveltejs/svelte/pull/8304 -->
86-
{#if children_render}
87-
{@render children_render()}
88-
{:else}
89-
<span style={span_style}>{option.label}</span>
90-
{/if}
91-
{/snippet}
92-
</Select>
76+
--sms-bg="var(--sms-options-bg)"
77+
--sms-width="min(20em, 90vw)"
78+
--sms-max-width="none"
79+
--sms-placeholder-color="lightgray"
80+
--sms-options-margin="1px 0"
81+
--sms-options-border-radius="0 0 1ex 1ex"
82+
/>
9383
</dialog>
9484
{/if}
9585

9686
<style>
97-
/* TODO maybe remove global */
98-
:where(:global(dialog)) {
87+
:where(dialog) {
9988
position: fixed;
10089
top: 30%;
10190
border: none;
@@ -106,12 +95,4 @@
10695
z-index: 10;
10796
font-size: 2.4ex;
10897
}
109-
dialog :global(div.multiselect) {
110-
--sms-bg: var(--sms-options-bg);
111-
--sms-width: min(20em, 90vw);
112-
--sms-max-width: none;
113-
--sms-placeholder-color: lightgray;
114-
--sms-options-margin: 1px 0;
115-
--sms-options-border-radius: 0 0 1ex 1ex;
116-
}
11798
</style>

0 commit comments

Comments
 (0)