Skip to content

Commit 17cb522

Browse files
Enhanced (SelectField) demo filtering logic and form handling... (#635)
* Fixed (Dialog/Drawer) event propagation preventing outside click detection Fixed (SelectField) focus management when used within dialogs Enhanced (MultiSelect/MultiSelectField/MultiSelectMenu) demo examples with functional item creation dialogs Enhanced (NumberStepper) documentation with prefix/suffix slot examples Enhanced (SelectField) demo filtering logic and form handling * npx prettier --write src/lib/components/MultiSelect.svelte * Split changesets into seperate files * updated changesets to use conventional commits formatting
1 parent 1bba3f6 commit 17cb522

File tree

13 files changed

+433
-128
lines changed

13 files changed

+433
-128
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-ux': patch
3+
---
4+
5+
fix(Dialog/Drawer): event propagation preventing outside click detection
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-ux': patch
3+
---
4+
5+
docs(MultiSelect/MultiSelectField/MultiSelectMenu): Enhanced demo examples with functional item creation dialogs
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-ux': patch
3+
---
4+
5+
docs(NumberStepper): demo example with prefix/suffix slot
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-ux': patch
3+
---
4+
5+
docs(SelectField): demo filtering logic and form handling
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-ux': patch
3+
---
4+
5+
fix(SelectField): focus management when used within dialogs

packages/svelte-ux/src/lib/components/Dialog.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@
103103
classes.root
104104
)}
105105
on:click={onClick}
106+
on:mouseup={(e) => {
107+
e.stopPropagation(); // Prevent mouseup from bubbling to outside click handlers (e.g., Popover/Menu clickOutside)
108+
}}
106109
on:keydown={(e) => {
107110
if (e.key === 'Escape') {
108111
// Do not allow event to reach Popover's on:keydown

packages/svelte-ux/src/lib/components/Drawer.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@
9090
$$props.class
9191
)}
9292
style={$$props.style}
93+
on:mouseup={(e) => {
94+
e.stopPropagation(); // Prevent mouseup from bubbling to outside click handlers (e.g., Popover/Menu clickOutside)
95+
}}
9396
in:fly|global={{
9497
x: placement === 'left' ? '-100%' : placement === 'right' ? '100%' : 0,
9598
y: placement === 'top' ? '-100%' : placement === 'bottom' ? '100%' : 0,

packages/svelte-ux/src/lib/components/SelectField.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@
255255
// Hide if focus not moved to menu (option clicked)
256256
if (
257257
fe.relatedTarget instanceof HTMLElement &&
258+
!fe.relatedTarget.closest('[role="dialog"]') &&
258259
!menuOptionsEl?.contains(fe.relatedTarget) && // TODO: Oddly Safari does not set `relatedTarget` to the clicked on menu option (like Chrome and Firefox) but instead appears to take `tabindex` into consideration. Currently resolves to `.options` after setting `tabindex="-1"
259260
fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar
260261
// Allow focus to move into auxiliary slot areas (beforeOptions, afterOptions, actions)

packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,32 @@
33
44
import {
55
Button,
6+
Dialog,
67
Drawer,
78
Form,
89
Icon,
910
MultiSelect,
1011
MultiSelectOption,
12+
TextField,
13+
Toggle,
1114
ToggleButton,
1215
ToggleGroup,
1316
ToggleOption,
17+
type MenuOption,
1418
} from 'svelte-ux';
1519
import Preview from '$lib/components/Preview.svelte';
1620
17-
const options = [
21+
let options: MenuOption[] = [
1822
{ label: 'One', value: 1 },
1923
{ label: 'Two', value: 2 },
2024
{ label: 'Three', value: 3 },
2125
{ label: 'Four', value: 4 },
2226
];
2327
28+
const newOption: () => MenuOption = () => {
29+
return { label: '', value: null };
30+
};
31+
2432
const manyOptions = Array.from({ length: 100 }).map((_, i) => ({
2533
label: `${i + 1}`,
2634
value: i + 1,
@@ -168,34 +176,6 @@
168176
</div>
169177
</Preview>
170178

171-
<h2>actions slot</h2>
172-
173-
<Preview>
174-
{value.length} selected
175-
<div class="flex flex-col max-h-[360px] overflow-auto">
176-
<MultiSelect {options} {value} on:change={(e) => (value = e.detail.value)} search>
177-
<div slot="actions">
178-
<Button color="primary" icon={mdiPlus}>Add item</Button>
179-
</div>
180-
</MultiSelect>
181-
</div>
182-
</Preview>
183-
184-
<h2>actions slot with max warning</h2>
185-
186-
<Preview>
187-
{value.length} selected
188-
<div class="flex flex-col max-h-[360px] overflow-auto">
189-
<MultiSelect {options} {value} on:change={(e) => (value = e.detail.value)} search max={2}>
190-
<div slot="actions" let:selection class="flex items-center">
191-
{#if selection.isMaxSelected()}
192-
<div class="text-sm text-danger">Maximum selection reached</div>
193-
{/if}
194-
</div>
195-
</MultiSelect>
196-
</div>
197-
</Preview>
198-
199179
<h2>beforeOptions slot</h2>
200180

201181
<Preview>
@@ -254,6 +234,98 @@
254234
</div>
255235
</Preview>
256236

237+
<h2>actions slot</h2>
238+
239+
<Preview>
240+
{value.length} selected
241+
<div class="flex flex-col max-h-[360px] overflow-auto">
242+
<MultiSelect {options} {value} on:change={(e) => (value = e.detail.value)} search>
243+
<div slot="actions" class="p-2" on:click|stopPropagation role="none">
244+
<Toggle let:on={open} let:toggle>
245+
<Button icon={mdiPlus} color="primary" on:click={toggle}>New item</Button>
246+
<Form
247+
initial={newOption()}
248+
on:change={(e) => {
249+
// Convert value to number if it's a valid number, otherwise keep as string
250+
const newOptionData = { ...e.detail };
251+
if (
252+
newOptionData.value !== null &&
253+
newOptionData.value !== '' &&
254+
!isNaN(Number(newOptionData.value))
255+
) {
256+
newOptionData.value = Number(newOptionData.value);
257+
}
258+
options = [newOptionData, ...options];
259+
// Auto-select the newly created option
260+
value = [...(value || []), newOptionData.value];
261+
}}
262+
let:draft
263+
let:current
264+
let:commit
265+
let:revert
266+
>
267+
<Dialog
268+
{open}
269+
on:close={() => {
270+
toggle();
271+
}}
272+
>
273+
<div slot="title">Create new option</div>
274+
<div class="px-6 py-3 w-96 grid gap-2">
275+
<TextField
276+
label="Label"
277+
value={current.label}
278+
on:change={(e) => {
279+
draft.label = e.detail.value;
280+
}}
281+
autofocus
282+
/>
283+
<TextField
284+
label="Value"
285+
value={draft.value}
286+
on:change={(e) => {
287+
draft.value = e.detail.value;
288+
}}
289+
/>
290+
</div>
291+
<div slot="actions">
292+
<Button
293+
on:click={() => {
294+
commit();
295+
toggle();
296+
}}
297+
color="primary">Add option</Button
298+
>
299+
<Button
300+
on:click={() => {
301+
revert();
302+
toggle();
303+
}}>Cancel</Button
304+
>
305+
</div>
306+
</Dialog>
307+
</Form>
308+
</Toggle>
309+
</div>
310+
</MultiSelect>
311+
</div>
312+
</Preview>
313+
314+
<h2>actions slot with max warning</h2>
315+
316+
<Preview>
317+
{value.length} selected
318+
<div class="flex flex-col max-h-[360px] overflow-auto">
319+
<MultiSelect {options} {value} on:change={(e) => (value = e.detail.value)} search max={2}>
320+
<div slot="actions" let:selection class="flex items-center">
321+
{#if selection.isMaxSelected()}
322+
<div class="text-sm text-danger">Maximum selection reached</div>
323+
{/if}
324+
</div>
325+
</MultiSelect>
326+
</div>
327+
</Preview>
328+
257329
<h2>option slot with MultiSelectOption custom actions</h2>
258330

259331
<Preview>

packages/svelte-ux/src/routes/docs/components/MultiSelectField/+page.svelte

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,31 @@
44
55
import {
66
Button,
7+
Dialog,
78
Drawer,
9+
Form,
810
MultiSelectField,
911
MultiSelectOption,
12+
TextField,
13+
Toggle,
1014
ToggleButton,
1115
ToggleGroup,
1216
ToggleOption,
17+
type MenuOption,
1318
} from 'svelte-ux';
1419
import Preview from '$lib/components/Preview.svelte';
1520
16-
const options = [
21+
let options: MenuOption[] = [
1722
{ label: 'One', value: 1 },
1823
{ label: 'Two', value: 2 },
1924
{ label: 'Three', value: 3 },
2025
{ label: 'Four', value: 4 },
2126
];
2227
28+
const newOption: () => MenuOption = () => {
29+
return { label: '', value: null };
30+
};
31+
2332
const manyOptions = Array.from({ length: 100 }).map((_, i) => ({
2433
label: `${i + 1}`,
2534
value: i + 1,
@@ -169,28 +178,6 @@
169178
/>
170179
</Preview>
171180

172-
<h2>actions slot</h2>
173-
174-
<Preview>
175-
<MultiSelectField {options} {value} on:change={(e) => (value = e.detail.value)}>
176-
<div slot="actions">
177-
<Button color="primary" icon={mdiPlus}>Add item</Button>
178-
</div>
179-
</MultiSelectField>
180-
</Preview>
181-
182-
<h2>actions slot with max warning</h2>
183-
184-
<Preview>
185-
<MultiSelectField {options} {value} on:change={(e) => (value = e.detail.value)} max={2}>
186-
<div slot="actions" let:selection class="flex items-center">
187-
{#if selection.isMaxSelected()}
188-
<div class="text-sm text-danger">Maximum selection reached</div>
189-
{/if}
190-
</div>
191-
</MultiSelectField>
192-
</Preview>
193-
194181
<h2>beforeOptions slot</h2>
195182

196183
<Preview>
@@ -241,6 +228,91 @@
241228
</MultiSelectField>
242229
</Preview>
243230

231+
<h2>actions slot</h2>
232+
233+
<Preview>
234+
<MultiSelectField {options} {value} on:change={(e) => (value = e.detail.value)}>
235+
<div slot="actions" class="p-2" on:click|stopPropagation role="none">
236+
<Toggle let:on={open} let:toggle>
237+
<Button icon={mdiPlus} color="primary" on:click={toggle}>New item</Button>
238+
<Form
239+
initial={newOption()}
240+
on:change={(e) => {
241+
// Convert value to number if it's a valid number, otherwise keep as string
242+
const newOptionData = { ...e.detail };
243+
if (
244+
newOptionData.value !== null &&
245+
newOptionData.value !== '' &&
246+
!isNaN(Number(newOptionData.value))
247+
) {
248+
newOptionData.value = Number(newOptionData.value);
249+
}
250+
options = [newOptionData, ...options];
251+
// Auto-select the newly created option
252+
value = [...(value || []), newOptionData.value];
253+
}}
254+
let:draft
255+
let:current
256+
let:commit
257+
let:revert
258+
>
259+
<Dialog
260+
{open}
261+
on:close={() => {
262+
toggle();
263+
}}
264+
>
265+
<div slot="title">Create new option</div>
266+
<div class="px-6 py-3 w-96 grid gap-2">
267+
<TextField
268+
label="Label"
269+
value={current.label}
270+
on:change={(e) => {
271+
draft.label = e.detail.value;
272+
}}
273+
autofocus
274+
/>
275+
<TextField
276+
label="Value"
277+
value={draft.value}
278+
on:change={(e) => {
279+
draft.value = e.detail.value;
280+
}}
281+
/>
282+
</div>
283+
<div slot="actions">
284+
<Button
285+
on:click={() => {
286+
commit();
287+
toggle();
288+
}}
289+
color="primary">Add option</Button
290+
>
291+
<Button
292+
on:click={() => {
293+
revert();
294+
toggle();
295+
}}>Cancel</Button
296+
>
297+
</div>
298+
</Dialog>
299+
</Form>
300+
</Toggle>
301+
</div>
302+
</MultiSelectField>
303+
</Preview>
304+
305+
<h2>actions slot with max warning</h2>
306+
307+
<Preview>
308+
<MultiSelectField {options} {value} on:change={(e) => (value = e.detail.value)} max={2}>
309+
<div slot="actions" let:selection class="flex items-center">
310+
{#if selection.isMaxSelected()}
311+
<div class="text-sm text-danger">Maximum selection reached</div>
312+
{/if}
313+
</div>
314+
</MultiSelectField>
315+
</Preview>
244316
<h2>within Drawer</h2>
245317

246318
<Preview>

0 commit comments

Comments
 (0)