From ff9e02685c4a9618a53ce300749af5df5ddf672f Mon Sep 17 00:00:00 2001 From: MaCleodWalker Date: Mon, 11 Aug 2025 16:42:08 +0000 Subject: [PATCH 1/4] 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 --- .changeset/eager-walls-admire.md | 5 + .../src/lib/components/Dialog.svelte | 3 + .../src/lib/components/Drawer.svelte | 3 + .../src/lib/components/MultiSelect.svelte | 2 +- .../src/lib/components/SelectField.svelte | 1 + .../docs/components/MultiSelect/+page.svelte | 130 +++++++++++++---- .../components/MultiSelectField/+page.svelte | 118 ++++++++++++--- .../components/MultiSelectMenu/+page.svelte | 121 ++++++++++++---- .../components/NumberStepper/+page.svelte | 24 +++- .../docs/components/SelectField/+page.svelte | 136 +++++++++++------- 10 files changed, 414 insertions(+), 129 deletions(-) create mode 100644 .changeset/eager-walls-admire.md diff --git a/.changeset/eager-walls-admire.md b/.changeset/eager-walls-admire.md new file mode 100644 index 000000000..e9feb6916 --- /dev/null +++ b/.changeset/eager-walls-admire.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +Fixed (Dialog/Drawer) event propagation preventing outside click detectionFixed (SelectField) focus management when used within dialogsEnhanced (MultiSelect/MultiSelectField/MultiSelectMenu) demo examples with functional item creation dialogsEnhanced (NumberStepper) documentation with prefix/suffix slot examplesEnhanced (SelectField) demo filtering logic and form handling diff --git a/packages/svelte-ux/src/lib/components/Dialog.svelte b/packages/svelte-ux/src/lib/components/Dialog.svelte index 426e26a42..07d7157f5 100644 --- a/packages/svelte-ux/src/lib/components/Dialog.svelte +++ b/packages/svelte-ux/src/lib/components/Dialog.svelte @@ -103,6 +103,9 @@ classes.root )} on:click={onClick} + on:mouseup={(e) => { + e.stopPropagation(); // Prevent mouseup from bubbling to outside click handlers (e.g., Popover/Menu clickOutside) + }} on:keydown={(e) => { if (e.key === 'Escape') { // Do not allow event to reach Popover's on:keydown diff --git a/packages/svelte-ux/src/lib/components/Drawer.svelte b/packages/svelte-ux/src/lib/components/Drawer.svelte index f26a8d477..04b764476 100644 --- a/packages/svelte-ux/src/lib/components/Drawer.svelte +++ b/packages/svelte-ux/src/lib/components/Drawer.svelte @@ -90,6 +90,9 @@ $$props.class )} style={$$props.style} + on:mouseup={(e) => { + e.stopPropagation(); // Prevent mouseup from bubbling to outside click handlers (e.g., Popover/Menu clickOutside) + }} in:fly|global={{ x: placement === 'left' ? '-100%' : placement === 'right' ? '100%' : 0, y: placement === 'top' ? '-100%' : placement === 'bottom' ? '100%' : 0, diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index e3a78ba8f..4f5d65ac2 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -122,7 +122,7 @@ } } // Re-filter options when `searchText` changes - $: searchText, updateFilteredOptions(); + $: (searchText, updateFilteredOptions()); const selection = selectionStore({ max }); // Only "subscribe" to value changes (not `$selection`) to fix correct `value` / topological ordering. Should be simplified with Svelte 5 diff --git a/packages/svelte-ux/src/lib/components/SelectField.svelte b/packages/svelte-ux/src/lib/components/SelectField.svelte index b1291c933..e4c7a4cb6 100644 --- a/packages/svelte-ux/src/lib/components/SelectField.svelte +++ b/packages/svelte-ux/src/lib/components/SelectField.svelte @@ -255,6 +255,7 @@ // Hide if focus not moved to menu (option clicked) if ( fe.relatedTarget instanceof HTMLElement && + !fe.relatedTarget.closest('[role="dialog"]') && !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" fe.relatedTarget !== menuOptionsEl?.offsetParent && // click on scroll bar // Allow focus to move into auxiliary slot areas (beforeOptions, afterOptions, actions) diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte index 40bafcbdd..cab460490 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte @@ -3,24 +3,32 @@ import { Button, + Dialog, Drawer, Form, Icon, MultiSelect, MultiSelectOption, + TextField, + Toggle, ToggleButton, ToggleGroup, ToggleOption, + type MenuOption, } from 'svelte-ux'; import Preview from '$lib/components/Preview.svelte'; - const options = [ + let options: MenuOption[] = [ { label: 'One', value: 1 }, { label: 'Two', value: 2 }, { label: 'Three', value: 3 }, { label: 'Four', value: 4 }, ]; + const newOption: () => MenuOption = () => { + return { label: '', value: null }; + }; + const manyOptions = Array.from({ length: 100 }).map((_, i) => ({ label: `${i + 1}`, value: i + 1, @@ -168,34 +176,6 @@ -

actions slot

- - - {value.length} selected -
- (value = e.detail.value)} search> -
- -
-
-
-
- -

actions slot with max warning

- - - {value.length} selected -
- (value = e.detail.value)} search max={2}> -
- {#if selection.isMaxSelected()} -
Maximum selection reached
- {/if} -
-
-
-
-

beforeOptions slot

@@ -254,6 +234,98 @@ +

actions slot

+ + + {value.length} selected +
+ (value = e.detail.value)} search> +
+ + +
{ + // Convert value to number if it's a valid number, otherwise keep as string + const newOptionData = { ...e.detail }; + if ( + newOptionData.value !== null && + newOptionData.value !== '' && + !isNaN(Number(newOptionData.value)) + ) { + newOptionData.value = Number(newOptionData.value); + } + options = [newOptionData, ...options]; + // Auto-select the newly created option + value = [...(value || []), newOptionData.value]; + }} + let:draft + let:current + let:commit + let:revert + > + { + toggle(); + }} + > +
Create new option
+
+ { + draft.label = e.detail.value; + }} + autofocus + /> + { + draft.value = e.detail.value; + }} + /> +
+
+ + +
+
+
+
+
+
+
+
+ +

actions slot with max warning

+ + + {value.length} selected +
+ (value = e.detail.value)} search max={2}> +
+ {#if selection.isMaxSelected()} +
Maximum selection reached
+ {/if} +
+
+
+
+

option slot with MultiSelectOption custom actions

diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelectField/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelectField/+page.svelte index 658e91315..6173559ff 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelectField/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelectField/+page.svelte @@ -4,22 +4,31 @@ import { Button, + Dialog, Drawer, + Form, MultiSelectField, MultiSelectOption, + TextField, + Toggle, ToggleButton, ToggleGroup, ToggleOption, + type MenuOption, } from 'svelte-ux'; import Preview from '$lib/components/Preview.svelte'; - const options = [ + let options: MenuOption[] = [ { label: 'One', value: 1 }, { label: 'Two', value: 2 }, { label: 'Three', value: 3 }, { label: 'Four', value: 4 }, ]; + const newOption: () => MenuOption = () => { + return { label: '', value: null }; + }; + const manyOptions = Array.from({ length: 100 }).map((_, i) => ({ label: `${i + 1}`, value: i + 1, @@ -169,28 +178,6 @@ /> -

actions slot

- - - (value = e.detail.value)}> -
- -
-
-
- -

actions slot with max warning

- - - (value = e.detail.value)} max={2}> -
- {#if selection.isMaxSelected()} -
Maximum selection reached
- {/if} -
-
-
-

beforeOptions slot

@@ -241,6 +228,91 @@ +

actions slot

+ + + (value = e.detail.value)}> +
+ + +
{ + // Convert value to number if it's a valid number, otherwise keep as string + const newOptionData = { ...e.detail }; + if ( + newOptionData.value !== null && + newOptionData.value !== '' && + !isNaN(Number(newOptionData.value)) + ) { + newOptionData.value = Number(newOptionData.value); + } + options = [newOptionData, ...options]; + // Auto-select the newly created option + value = [...(value || []), newOptionData.value]; + }} + let:draft + let:current + let:commit + let:revert + > + { + toggle(); + }} + > +
Create new option
+
+ { + draft.label = e.detail.value; + }} + autofocus + /> + { + draft.value = e.detail.value; + }} + /> +
+
+ + +
+
+
+
+
+
+
+ +

actions slot with max warning

+ + + (value = e.detail.value)} max={2}> +
+ {#if selection.isMaxSelected()} +
Maximum selection reached
+ {/if} +
+
+

within Drawer

diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte index 8e263ebba..5de8babf6 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte @@ -3,21 +3,30 @@ import { Button, + Dialog, + Form, MultiSelectMenu, MultiSelectOption, + TextField, + Toggle, ToggleButton, ToggleGroup, ToggleOption, + type MenuOption, } from 'svelte-ux'; import Preview from '$lib/components/Preview.svelte'; - const options = [ + let options: MenuOption[] = [ { label: 'One', value: 1 }, { label: 'Two', value: 2 }, { label: 'Three', value: 3 }, { label: 'Four', value: 4 }, ]; + const newOption: () => MenuOption = () => { + return { label: '', value: null }; + }; + const manyOptions = Array.from({ length: 100 }).map((_, i) => ({ label: `${i + 1}`, value: i + 1, @@ -286,14 +295,14 @@ -

actions slot

+

beforeOptions slot

{value.length} selected { // @ts-expect-error @@ -304,15 +313,26 @@ classes={{ menu: 'w-[360px]' }} search > -
- -
+ +
+ + Any + Evens + Odds + +
+
-

beforeOptions slot

+

afterOptions slot

@@ -330,8 +350,8 @@ classes={{ menu: 'w-[360px]' }} search > - -
+ +
-

afterOptions slot

+

actions slot

{value.length} selected { // @ts-expect-error @@ -367,20 +387,73 @@ classes={{ menu: 'w-[360px]' }} search > - -
- + + +
{ + // Convert value to number if it's a valid number, otherwise keep as string + const newOptionData = { ...e.detail }; + if ( + newOptionData.value !== null && + newOptionData.value !== '' && + !isNaN(Number(newOptionData.value)) + ) { + newOptionData.value = Number(newOptionData.value); + } + options = [newOptionData, ...options]; + // Auto-select the newly created option + value = [...(value || []), newOptionData.value]; + }} + let:draft + let:current + let:commit + let:revert > - Any - Evens - Odds - -
-
+ { + toggle(); + }} + > +
Create new option
+
+ { + draft.label = e.detail.value; + }} + autofocus + /> + { + draft.value = e.detail.value; + }} + /> +
+
+ + +
+
+ + +
diff --git a/packages/svelte-ux/src/routes/docs/components/NumberStepper/+page.svelte b/packages/svelte-ux/src/routes/docs/components/NumberStepper/+page.svelte index f4757f2bd..2504fd58a 100644 --- a/packages/svelte-ux/src/routes/docs/components/NumberStepper/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/NumberStepper/+page.svelte @@ -7,7 +7,7 @@

Examples

-

basic

+

Basic

@@ -26,20 +26,36 @@ console.log(e.detail.value)} /> -

dense

+

Dense

-

min / max

+

Min / Max

-

step

+

Step

+ +

Prefix

+ + + + $ + + + +

Suffix

+ + + + kg + + diff --git a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte index 6893e1789..94e53b6df 100644 --- a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte @@ -75,12 +75,19 @@ let selectedStr: 'any' | 'even' | 'odds' = 'any'; // Filter options based on toggle selection - $: optionsFiltered = - selectedStr === 'even' - ? options.filter((o) => typeof o.value === 'number' && o.value % 2 === 0) - : selectedStr === 'odds' - ? options.filter((o) => typeof o.value === 'number' && o.value % 2 !== 0) - : options; + $: optionsFiltered = options.map((o) => { + const matches = + selectedStr === 'even' + ? typeof o.value === 'number' && o.value % 2 === 0 + : selectedStr === 'odds' + ? typeof o.value === 'number' && o.value % 2 !== 0 + : true; + + return { + ...o, + disabled: (o.disabled ?? false) || !matches, + }; + });

Examples

@@ -415,7 +422,16 @@
{ - options = [e.detail, ...options]; + // Convert value to number if it's a valid number, otherwise keep as string + const newOptionData = { ...e.detail }; + if ( + newOptionData.value !== null && + newOptionData.value !== '' && + !isNaN(Number(newOptionData.value)) + ) { + newOptionData.value = Number(newOptionData.value); + } + options = [newOptionData, ...options]; }} let:draft let:commit @@ -448,6 +464,45 @@
+ +

`beforeOptions` slot

+ + + +
+ + Any + Evens + Odds + +
+
+
+ +

`afterOptions` slot

+ + + +
+ + Any + Evens + Odds + +
+
+
+

`actions` slot (menu)

@@ -458,7 +513,18 @@
{ - options = [e.detail, ...options]; + // Convert value to number if it's a valid number, otherwise keep as string + const newOptionData = { ...e.detail }; + if ( + newOptionData.value !== null && + newOptionData.value !== '' && + !isNaN(Number(newOptionData.value)) + ) { + newOptionData.value = Number(newOptionData.value); + } + options = [newOptionData, ...options]; + // Auto-select the newly created option + value = newOptionData.value; }} let:draft let:current @@ -469,7 +535,6 @@ {open} on:close={() => { toggle(); - hide(); }} >
Create new option
@@ -491,8 +556,20 @@ />
- - + +
@@ -501,43 +578,6 @@
-

`beforeOptions` slot (menu)

- - - -
- - Any - Evens - Odds - -
-
-
-

`afterOptions` slot (menu)

- - - -
- - Any - Evens - Odds - -
-
-
-

Icon

From 7ce535831186a86b4464067703d6448a092fe5f5 Mon Sep 17 00:00:00 2001 From: MaCleodWalker Date: Mon, 11 Aug 2025 16:48:40 +0000 Subject: [PATCH 2/4] npx prettier --write src/lib/components/MultiSelect.svelte --- packages/svelte-ux/src/lib/components/MultiSelect.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index 4f5d65ac2..e3a78ba8f 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -122,7 +122,7 @@ } } // Re-filter options when `searchText` changes - $: (searchText, updateFilteredOptions()); + $: searchText, updateFilteredOptions(); const selection = selectionStore({ max }); // Only "subscribe" to value changes (not `$selection`) to fix correct `value` / topological ordering. Should be simplified with Svelte 5 From e9e2a9575ef45c3965877cdf8485c75f433072ea Mon Sep 17 00:00:00 2001 From: MaCleodWalker Date: Mon, 11 Aug 2025 17:30:08 +0000 Subject: [PATCH 3/4] Split changesets into seperate files --- .changeset/dialog-drawer-event-propagation.md | 5 +++++ .changeset/eager-walls-admire.md | 5 ----- .changeset/multiselect-demo-enhancements.md | 5 +++++ .changeset/numberstepper-documentation.md | 5 +++++ .changeset/selectfield-demo-improvements.md | 5 +++++ .changeset/selectfield-focus-management.md | 5 +++++ 6 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .changeset/dialog-drawer-event-propagation.md delete mode 100644 .changeset/eager-walls-admire.md create mode 100644 .changeset/multiselect-demo-enhancements.md create mode 100644 .changeset/numberstepper-documentation.md create mode 100644 .changeset/selectfield-demo-improvements.md create mode 100644 .changeset/selectfield-focus-management.md diff --git a/.changeset/dialog-drawer-event-propagation.md b/.changeset/dialog-drawer-event-propagation.md new file mode 100644 index 000000000..f815c0593 --- /dev/null +++ b/.changeset/dialog-drawer-event-propagation.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +Fixed (Dialog/Drawer) event propagation preventing outside click detection diff --git a/.changeset/eager-walls-admire.md b/.changeset/eager-walls-admire.md deleted file mode 100644 index e9feb6916..000000000 --- a/.changeset/eager-walls-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte-ux': patch ---- - -Fixed (Dialog/Drawer) event propagation preventing outside click detectionFixed (SelectField) focus management when used within dialogsEnhanced (MultiSelect/MultiSelectField/MultiSelectMenu) demo examples with functional item creation dialogsEnhanced (NumberStepper) documentation with prefix/suffix slot examplesEnhanced (SelectField) demo filtering logic and form handling diff --git a/.changeset/multiselect-demo-enhancements.md b/.changeset/multiselect-demo-enhancements.md new file mode 100644 index 000000000..c079e96f2 --- /dev/null +++ b/.changeset/multiselect-demo-enhancements.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +Enhanced (MultiSelect/MultiSelectField/MultiSelectMenu) demo examples with functional item creation dialogs diff --git a/.changeset/numberstepper-documentation.md b/.changeset/numberstepper-documentation.md new file mode 100644 index 000000000..eb2946518 --- /dev/null +++ b/.changeset/numberstepper-documentation.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +Enhanced (NumberStepper) documentation with prefix/suffix slot examples diff --git a/.changeset/selectfield-demo-improvements.md b/.changeset/selectfield-demo-improvements.md new file mode 100644 index 000000000..e05af74c7 --- /dev/null +++ b/.changeset/selectfield-demo-improvements.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +Enhanced (SelectField) demo filtering logic and form handling diff --git a/.changeset/selectfield-focus-management.md b/.changeset/selectfield-focus-management.md new file mode 100644 index 000000000..a792af905 --- /dev/null +++ b/.changeset/selectfield-focus-management.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +Fixed (SelectField) focus management when used within dialogs From 4575db7e05abf7d09452d5949bdfc011b1e35766 Mon Sep 17 00:00:00 2001 From: MaCleodWalker Date: Tue, 12 Aug 2025 12:32:36 +0000 Subject: [PATCH 4/4] updated changesets to use conventional commits formatting --- .changeset/dialog-drawer-event-propagation.md | 2 +- .changeset/multiselect-demo-enhancements.md | 2 +- .changeset/numberstepper-documentation.md | 2 +- .changeset/selectfield-demo-improvements.md | 2 +- .changeset/selectfield-focus-management.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/dialog-drawer-event-propagation.md b/.changeset/dialog-drawer-event-propagation.md index f815c0593..bbaf0e613 100644 --- a/.changeset/dialog-drawer-event-propagation.md +++ b/.changeset/dialog-drawer-event-propagation.md @@ -2,4 +2,4 @@ 'svelte-ux': patch --- -Fixed (Dialog/Drawer) event propagation preventing outside click detection +fix(Dialog/Drawer): event propagation preventing outside click detection diff --git a/.changeset/multiselect-demo-enhancements.md b/.changeset/multiselect-demo-enhancements.md index c079e96f2..1cc5dea60 100644 --- a/.changeset/multiselect-demo-enhancements.md +++ b/.changeset/multiselect-demo-enhancements.md @@ -2,4 +2,4 @@ 'svelte-ux': patch --- -Enhanced (MultiSelect/MultiSelectField/MultiSelectMenu) demo examples with functional item creation dialogs +docs(MultiSelect/MultiSelectField/MultiSelectMenu): Enhanced demo examples with functional item creation dialogs diff --git a/.changeset/numberstepper-documentation.md b/.changeset/numberstepper-documentation.md index eb2946518..4ca09b687 100644 --- a/.changeset/numberstepper-documentation.md +++ b/.changeset/numberstepper-documentation.md @@ -2,4 +2,4 @@ 'svelte-ux': patch --- -Enhanced (NumberStepper) documentation with prefix/suffix slot examples +docs(NumberStepper): demo example with prefix/suffix slot diff --git a/.changeset/selectfield-demo-improvements.md b/.changeset/selectfield-demo-improvements.md index e05af74c7..f0595c53b 100644 --- a/.changeset/selectfield-demo-improvements.md +++ b/.changeset/selectfield-demo-improvements.md @@ -2,4 +2,4 @@ 'svelte-ux': patch --- -Enhanced (SelectField) demo filtering logic and form handling +docs(SelectField): demo filtering logic and form handling diff --git a/.changeset/selectfield-focus-management.md b/.changeset/selectfield-focus-management.md index a792af905..5883a3bd3 100644 --- a/.changeset/selectfield-focus-management.md +++ b/.changeset/selectfield-focus-management.md @@ -2,4 +2,4 @@ 'svelte-ux': patch --- -Fixed (SelectField) focus management when used within dialogs +fix(SelectField): focus management when used within dialogs