Skip to content

Commit f53a3e2

Browse files
Fixed (Drawer/Dialog) open/close events firing on mount and unrelated… (#632)
* Fixed (Drawer/Dialog) open/close events firing on mount and unrelated updates. Now events are emitted only on actual state transitions by tracking the previous open value, preventing unintended closes and focus jumps. Added (beforeOptions/afterOptions) slots to the (SelectField, MultiSelect, MultiSelectField, MultiSelectMenu) components. Updated docs examples (beforeOptions/afterOptions). * pnpm prettier --write .
1 parent 012d2ca commit f53a3e2

File tree

8 files changed

+305
-14
lines changed

8 files changed

+305
-14
lines changed

.changeset/yummy-dogs-return.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'svelte-ux': patch
3+
---
4+
Fixed (Drawer/Dialog) open/close events firing on mount and unrelated updates. Now events are emitted only on actual state transitions by tracking the previous open value, preventing unintended closes and focus jumps.
5+
Added (beforeOptions/afterOptions) slots to the (SelectField, MultiSelect, MultiSelectField, MultiSelectMenu) components.
6+
Updated docs examples (beforeOptions/afterOptions).

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,14 @@
6969
}
7070
}
7171
72-
$: if (open) {
73-
dispatch('open');
74-
} else {
75-
dispatch('close');
72+
let _wasOpen = open;
73+
$: if (open !== _wasOpen) {
74+
if (open) {
75+
dispatch('open');
76+
} else if (_wasOpen) {
77+
dispatch('close');
78+
}
79+
_wasOpen = open;
7680
}
7781
</script>
7882

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,14 @@
4545
}
4646
}
4747
48-
$: if (open) {
49-
dispatch('open');
50-
} else {
51-
dispatch('close');
48+
let _wasOpen = open;
49+
$: if (open !== _wasOpen) {
50+
if (open) {
51+
dispatch('open');
52+
} else if (_wasOpen) {
53+
dispatch('close');
54+
}
55+
_wasOpen = open;
5256
}
5357
</script>
5458

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@
238238
239239
function onChange(e: ComponentEvents<TextField>['change']) {
240240
logger.debug('onChange');
241-
242241
searchText = e.detail.inputValue as string;
243242
dispatch('inputChange', searchText);
244243
show();
@@ -258,7 +257,10 @@
258257
fe.relatedTarget instanceof HTMLElement &&
259258
!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"
260259
fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar
261-
!fe.relatedTarget.closest('menu > [slot=actions]') && // click on action item
260+
// Allow focus to move into auxiliary slot areas (beforeOptions, afterOptions, actions)
261+
!fe.relatedTarget.closest(
262+
'menu > [slot=actions], menu > [slot=beforeOptions], menu > [slot=afterOptions]'
263+
) && // click on action / before / after item
262264
!selectFieldEl?.contains(fe.relatedTarget) && // click within <SelectField> (ex. toggleIcon)
263265
fe.relatedTarget !== selectFieldEl // click on SelectField itself
264266
) {
@@ -546,6 +548,7 @@
546548
on:close={() => hide('menu on:close')}
547549
{...menuProps}
548550
>
551+
<slot name="beforeOptions" {hide} />
549552
<!-- TODO: Rework into hierarchy of snippets in v2.0 -->
550553
<SelectListOptions
551554
bind:menuOptionsEl
@@ -601,6 +604,7 @@
601604
</svelte:fragment>
602605
</SelectListOptions>
603606

607+
<slot name="afterOptions" {hide} />
604608
<slot name="actions" {hide} />
605609
</Menu>
606610
{:else}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
MultiSelect,
1010
MultiSelectOption,
1111
ToggleButton,
12+
ToggleGroup,
13+
ToggleOption,
1214
} from 'svelte-ux';
1315
import Preview from '$lib/components/Preview.svelte';
1416
@@ -25,6 +27,15 @@
2527
}));
2628
2729
let value = [3];
30+
31+
// Filters (Any/Evens/Odds) for demos below
32+
let msSelectedStr: 'any' | 'even' | 'odds' = 'any';
33+
$: msOptionsFiltered =
34+
msSelectedStr === 'even'
35+
? options.filter((o) => typeof o.value === 'number' && o.value % 2 === 0)
36+
: msSelectedStr === 'odds'
37+
? options.filter((o) => typeof o.value === 'number' && o.value % 2 !== 0)
38+
: options;
2839
</script>
2940

3041
<h1>Examples</h1>
@@ -185,6 +196,64 @@
185196
</div>
186197
</Preview>
187198

199+
<h2>beforeOptions slot</h2>
200+
201+
<Preview>
202+
{value.length} selected
203+
<div class="flex flex-col max-h-[360px] overflow-auto">
204+
<MultiSelect
205+
options={msOptionsFiltered}
206+
{value}
207+
on:change={(e) => (value = e.detail.value)}
208+
search
209+
>
210+
<svelte:fragment slot="beforeOptions" let:selection>
211+
<div class="p-2 border-b">
212+
<ToggleGroup
213+
bind:value={msSelectedStr}
214+
classes={{ options: 'justify-start h-10' }}
215+
rounded="full"
216+
inset
217+
>
218+
<ToggleOption value="any">Any</ToggleOption>
219+
<ToggleOption value="even">Evens</ToggleOption>
220+
<ToggleOption value="odds">Odds</ToggleOption>
221+
</ToggleGroup>
222+
</div>
223+
</svelte:fragment>
224+
</MultiSelect>
225+
</div>
226+
</Preview>
227+
228+
<h2>afterOptions slot</h2>
229+
230+
<Preview>
231+
{value.length} selected
232+
<div class="flex flex-col max-h-[360px] overflow-auto">
233+
<MultiSelect
234+
options={msOptionsFiltered}
235+
{value}
236+
on:change={(e) => (value = e.detail.value)}
237+
search
238+
>
239+
<svelte:fragment slot="afterOptions" let:selection>
240+
<div class="p-2 border-t">
241+
<ToggleGroup
242+
bind:value={msSelectedStr}
243+
classes={{ options: 'justify-start h-10' }}
244+
rounded="full"
245+
inset
246+
>
247+
<ToggleOption value="any">Any</ToggleOption>
248+
<ToggleOption value="even">Evens</ToggleOption>
249+
<ToggleOption value="odds">Odds</ToggleOption>
250+
</ToggleGroup>
251+
</div>
252+
</svelte:fragment>
253+
</MultiSelect>
254+
</div>
255+
</Preview>
256+
188257
<h2>option slot with MultiSelectOption custom actions</h2>
189258

190259
<Preview>

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
import { slide } from 'svelte/transition';
33
import { mdiPlus } from '@mdi/js';
44
5-
import { Button, Drawer, MultiSelectField, MultiSelectOption, ToggleButton } from 'svelte-ux';
5+
import {
6+
Button,
7+
Drawer,
8+
MultiSelectField,
9+
MultiSelectOption,
10+
ToggleButton,
11+
ToggleGroup,
12+
ToggleOption,
13+
} from 'svelte-ux';
614
import Preview from '$lib/components/Preview.svelte';
715
816
const options = [
@@ -18,6 +26,15 @@
1826
}));
1927
2028
let value: number[] | undefined = [3];
29+
30+
// Filters (Any/Evens/Odds) for demos below
31+
let msfSelectedStr: 'any' | 'even' | 'odds' = 'any';
32+
$: msfOptionsFiltered =
33+
msfSelectedStr === 'even'
34+
? options.filter((o) => typeof o.value === 'number' && o.value % 2 === 0)
35+
: msfSelectedStr === 'odds'
36+
? options.filter((o) => typeof o.value === 'number' && o.value % 2 !== 0)
37+
: options;
2138
</script>
2239

2340
<h1>Examples</h1>
@@ -174,6 +191,56 @@
174191
</MultiSelectField>
175192
</Preview>
176193

194+
<h2>beforeOptions slot</h2>
195+
196+
<Preview>
197+
<MultiSelectField
198+
options={msfOptionsFiltered}
199+
{value}
200+
on:change={(e) => (value = e.detail.value)}
201+
>
202+
<svelte:fragment slot="beforeOptions">
203+
<div class="p-2 border-b">
204+
<ToggleGroup
205+
bind:value={msfSelectedStr}
206+
classes={{ options: 'justify-start h-10' }}
207+
rounded="full"
208+
inset
209+
>
210+
<ToggleOption value="any">Any</ToggleOption>
211+
<ToggleOption value="even">Evens</ToggleOption>
212+
<ToggleOption value="odds">Odds</ToggleOption>
213+
</ToggleGroup>
214+
</div>
215+
</svelte:fragment>
216+
</MultiSelectField>
217+
</Preview>
218+
219+
<h2>afterOptions slot</h2>
220+
221+
<Preview>
222+
<MultiSelectField
223+
options={msfOptionsFiltered}
224+
{value}
225+
on:change={(e) => (value = e.detail.value)}
226+
>
227+
<svelte:fragment slot="afterOptions">
228+
<div class="p-2 border-t">
229+
<ToggleGroup
230+
bind:value={msfSelectedStr}
231+
classes={{ options: 'justify-start h-10' }}
232+
rounded="full"
233+
inset
234+
>
235+
<ToggleOption value="any">Any</ToggleOption>
236+
<ToggleOption value="even">Evens</ToggleOption>
237+
<ToggleOption value="odds">Odds</ToggleOption>
238+
</ToggleGroup>
239+
</div>
240+
</svelte:fragment>
241+
</MultiSelectField>
242+
</Preview>
243+
177244
<h2>within Drawer</h2>
178245

179246
<Preview>

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

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
<script lang="ts">
22
import { mdiPlus } from '@mdi/js';
33
4-
import { Button, MultiSelectMenu, MultiSelectOption, ToggleButton } from 'svelte-ux';
4+
import {
5+
Button,
6+
MultiSelectMenu,
7+
MultiSelectOption,
8+
ToggleButton,
9+
ToggleGroup,
10+
ToggleOption,
11+
} from 'svelte-ux';
512
import Preview from '$lib/components/Preview.svelte';
613
714
const options = [
@@ -17,6 +24,15 @@
1724
}));
1825
1926
let value = [3];
27+
28+
// Filters (Any/Evens/Odds) for demos below
29+
let msmSelectedStr: 'any' | 'even' | 'odds' = 'any';
30+
$: msmOptionsFiltered =
31+
msmSelectedStr === 'even'
32+
? options.filter((o) => typeof o.value === 'number' && o.value % 2 === 0)
33+
: msmSelectedStr === 'odds'
34+
? options.filter((o) => typeof o.value === 'number' && o.value % 2 !== 0)
35+
: options;
2036
</script>
2137

2238
<h1>Examples</h1>
@@ -91,7 +107,6 @@
91107
</ToggleButton>
92108
</div>
93109
</Preview>
94-
95110
<h2>search</h2>
96111

97112
<Preview>
@@ -297,6 +312,80 @@
297312
</span>
298313
</Preview>
299314

315+
<h2>beforeOptions slot</h2>
316+
317+
<Preview>
318+
<span>
319+
<ToggleButton let:on={open} let:toggleOff transition={false}>
320+
{value.length} selected
321+
<MultiSelectMenu
322+
options={msmOptionsFiltered}
323+
{value}
324+
on:change={(e) => {
325+
// @ts-expect-error
326+
value = e.detail.value;
327+
}}
328+
{open}
329+
on:close={toggleOff}
330+
classes={{ menu: 'w-[360px]' }}
331+
search
332+
>
333+
<svelte:fragment slot="beforeOptions">
334+
<div class="p-2 border-b">
335+
<ToggleGroup
336+
bind:value={msmSelectedStr}
337+
classes={{ options: 'justify-start h-10' }}
338+
rounded="full"
339+
inset
340+
>
341+
<ToggleOption value="any">Any</ToggleOption>
342+
<ToggleOption value="even">Evens</ToggleOption>
343+
<ToggleOption value="odds">Odds</ToggleOption>
344+
</ToggleGroup>
345+
</div>
346+
</svelte:fragment>
347+
</MultiSelectMenu>
348+
</ToggleButton>
349+
</span>
350+
</Preview>
351+
352+
<h2>afterOptions slot</h2>
353+
354+
<Preview>
355+
<span>
356+
<ToggleButton let:on={open} let:toggleOff transition={false}>
357+
{value.length} selected
358+
<MultiSelectMenu
359+
options={msmOptionsFiltered}
360+
{value}
361+
on:change={(e) => {
362+
// @ts-expect-error
363+
value = e.detail.value;
364+
}}
365+
{open}
366+
on:close={toggleOff}
367+
classes={{ menu: 'w-[360px]' }}
368+
search
369+
>
370+
<svelte:fragment slot="afterOptions">
371+
<div class="p-2 border-t">
372+
<ToggleGroup
373+
bind:value={msmSelectedStr}
374+
classes={{ options: 'justify-start h-10' }}
375+
rounded="full"
376+
inset
377+
>
378+
<ToggleOption value="any">Any</ToggleOption>
379+
<ToggleOption value="even">Evens</ToggleOption>
380+
<ToggleOption value="odds">Odds</ToggleOption>
381+
</ToggleGroup>
382+
</div>
383+
</svelte:fragment>
384+
</MultiSelectMenu>
385+
</ToggleButton>
386+
</span>
387+
</Preview>
388+
300389
<h2>option slot</h2>
301390

302391
<Preview>

0 commit comments

Comments
 (0)