diff --git a/.changeset/dialog-drawer-event-propagation.md b/.changeset/dialog-drawer-event-propagation.md new file mode 100644 index 00000000..bbaf0e61 --- /dev/null +++ b/.changeset/dialog-drawer-event-propagation.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +fix(Dialog/Drawer): event propagation preventing outside click detection diff --git a/.changeset/multiselect-demo-enhancements.md b/.changeset/multiselect-demo-enhancements.md new file mode 100644 index 00000000..1cc5dea6 --- /dev/null +++ b/.changeset/multiselect-demo-enhancements.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +docs(MultiSelect/MultiSelectField/MultiSelectMenu): Enhanced demo examples with functional item creation dialogs diff --git a/.changeset/numberstepper-documentation.md b/.changeset/numberstepper-documentation.md new file mode 100644 index 00000000..4ca09b68 --- /dev/null +++ b/.changeset/numberstepper-documentation.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +docs(NumberStepper): demo example with prefix/suffix slot diff --git a/.changeset/selectfield-demo-improvements.md b/.changeset/selectfield-demo-improvements.md new file mode 100644 index 00000000..f0595c53 --- /dev/null +++ b/.changeset/selectfield-demo-improvements.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +docs(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 00000000..5883a3bd --- /dev/null +++ b/.changeset/selectfield-focus-management.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +fix(SelectField): focus management when used within dialogs diff --git a/packages/svelte-ux/src/lib/components/Dialog.svelte b/packages/svelte-ux/src/lib/components/Dialog.svelte index 426e26a4..07d7157f 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 f26a8d47..04b76447 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/SelectField.svelte b/packages/svelte-ux/src/lib/components/SelectField.svelte index b1291c93..e4c7a4cb 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 40bafcbd..cab46049 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 658e9131..6173559f 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 8e263ebb..5de8babf 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 f4757f2b..2504fd58 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 6893e178..94e53b6d 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