Skip to content

Commit 3cff15d

Browse files
committed
Merge branch 'main' into next
2 parents ca33a56 + 06ccb84 commit 3cff15d

File tree

14 files changed

+445
-135
lines changed

14 files changed

+445
-135
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

.changeset/fine-vans-move.md

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): keep menu open on reactive updates; close only on selection or outside click
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
@@ -106,6 +106,9 @@
106106
classes.root
107107
)}
108108
on:click={onClick}
109+
on:mouseup={(e) => {
110+
e.stopPropagation(); // Prevent mouseup from bubbling to outside click handlers (e.g., Popover/Menu clickOutside)
111+
}}
109112
on:keydown={(e) => {
110113
if (e.key === 'Escape') {
111114
// 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
@@ -92,6 +92,9 @@
9292
classes.root,
9393
className
9494
)}
95+
on:mouseup={(e) => {
96+
e.stopPropagation(); // Prevent mouseup from bubbling to outside click handlers (e.g., Popover/Menu clickOutside)
97+
}}
9598
in:fly|global={{
9699
x: placement === 'left' ? '-100%' : placement === 'right' ? '100%' : 0,
97100
y: placement === 'top' ? '-100%' : placement === 'bottom' ? '100%' : 0,

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@
122122
123123
// Capture for next change
124124
prevValue = selected?.value;
125-
prevSelected = selectOption(selected);
125+
// Do not close menu when selection is updated reactively
126+
prevSelected = selectOption(selected, false);
126127
} else if (/*value !== undefined &&*/ value !== prevValue) {
127128
// Removed `value !== undefined` to clear searchText when value is set to undefined. Might be a breaking change
128129
logger.info('value changed', {
@@ -135,7 +136,7 @@
135136
136137
// Capture for next change
137138
prevValue = value;
138-
prevSelected = selectValue(value);
139+
prevSelected = selectValue(value, false);
139140
} else {
140141
logger.info('neither selected or value changed (options only)');
141142
// Reselect value if menu is not open and options possibly changed (which could result in new display text for the select value)
@@ -259,6 +260,7 @@
259260
// Hide if focus not moved to menu (option clicked)
260261
if (
261262
fe.relatedTarget instanceof HTMLElement &&
263+
!fe.relatedTarget.closest('[role="dialog"]') &&
262264
!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"
263265
fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar
264266
// Allow focus to move into auxiliary slot areas (beforeOptions, afterOptions, actions)
@@ -361,17 +363,17 @@
361363
/**
362364
* Select option by value
363365
*/
364-
function selectValue(value: TValue | null | undefined) {
366+
function selectValue(value: TValue | null | undefined, closeMenu: boolean = true) {
365367
logger.debug('selectValue', { value, options, filteredOptions });
366368
367369
const option = options?.find((option) => option.value === value) ?? null;
368-
return selectOption(option);
370+
return selectOption(option, closeMenu);
369371
}
370372
371373
/**
372374
* Select option by object
373375
*/
374-
function selectOption(option: MenuOption<TValue> | null) {
376+
function selectOption(option: MenuOption<TValue> | null, closeMenu: boolean = true) {
375377
logger.info('selectOption', { option });
376378
377379
const previousValue = value;
@@ -392,7 +394,9 @@
392394
dispatch('change', { option, value });
393395
}
394396
395-
hide('selectOption');
397+
if (closeMenu) {
398+
hide('selectOption');
399+
}
396400
397401
return option;
398402
}
@@ -421,7 +425,8 @@
421425
422426
function clear() {
423427
logger.info('clear');
424-
selectOption(null);
428+
// Clearing should not close the menu🤞; keep it open if it already is
429+
selectOption(null, false);
425430
filteredOptions = options;
426431
}
427432
</script>

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

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
<script lang="ts">
22
import {
33
Button,
4+
Dialog,
45
Drawer,
56
Form,
67
getSettings,
78
Icon,
89
MultiSelect,
910
MultiSelectOption,
11+
TextField,
12+
Toggle,
1013
ToggleButton,
1114
ToggleGroup,
1215
ToggleOption,
16+
type MenuOption,
1317
} from 'svelte-ux';
1418
import Preview from '$lib/components/Preview.svelte';
1519
1620
const { icons } = getSettings();
1721
18-
const options = [
22+
let options: MenuOption[] = [
1923
{ label: 'One', value: 1 },
2024
{ label: 'Two', value: 2 },
2125
{ label: 'Three', value: 3 },
2226
{ label: 'Four', value: 4 },
2327
];
2428
29+
const newOption: () => MenuOption = () => {
30+
return { label: '', value: null };
31+
};
32+
2533
const manyOptions = Array.from({ length: 100 }).map((_, i) => ({
2634
label: `${i + 1}`,
2735
value: i + 1,
@@ -169,34 +177,6 @@
169177
</div>
170178
</Preview>
171179

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

202182
<Preview>
@@ -255,6 +235,98 @@
255235
</div>
256236
</Preview>
257237

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

260332
<Preview>

0 commit comments

Comments
 (0)