diff --git a/src/lib/ui/core/Popover/Popover.svelte b/src/lib/ui/core/Popover/Popover.svelte index 98f232538..ff54cb847 100644 --- a/src/lib/ui/core/Popover/Popover.svelte +++ b/src/lib/ui/core/Popover/Popover.svelte @@ -11,6 +11,8 @@ import { cn } from '$ui/utils/index.js' import { flyAndScaleOutTransition } from '$ui/utils/transitions.js' + import { useCloseOnOutsideClick } from './closeOutside.svelte.js' + type TProps = { class?: string children?: PopoverTriggerProps['child'] @@ -30,6 +32,7 @@ openDelay?: number closeDelay?: number openOnHover?: boolean + closeOnOutsideClick?: boolean } let { @@ -39,6 +42,7 @@ noStyles = false, matchTriggerWidth = false, openOnHover = false, + closeOnOutsideClick = false, isOpened = $bindable(false), openDelay = 0, @@ -54,10 +58,16 @@ }: TProps = $props() const preventFocus = (e: Event) => e.preventDefault() + + const { triggerEl, contentEl } = useCloseOnOutsideClick({ + enabled: closeOnOutsideClick, + getIsOpened: () => isOpened, + close: () => (isOpened = false), + }) - + boolean + close: () => void + enabled?: boolean +} + +export const useCloseOnOutsideClick = ({ + getIsOpened, + close, + enabled = true, +}: TCloseOutsideProps) => { + let triggerEl = $state(null) + let contentEl = $state(null) + + const isOpened = $derived(getIsOpened()) + + if (enabled) { + $effect(() => { + if (!isOpened) return + + return on(window, 'pointerdown', (e) => { + if (contentEl && e.composedPath().includes(contentEl)) return + if (triggerEl && e.composedPath().includes(triggerEl)) return + + close() + }) + }) + } + + return { + triggerEl: { + get $() { + return triggerEl + }, + set $(el: HTMLElement | null) { + triggerEl = el + }, + }, + + contentEl: { + get $() { + return contentEl + }, + set $(el: HTMLElement | null) { + contentEl = el + }, + }, + } +} diff --git a/src/lib/ui/core/Tooltip/Tooltip.svelte b/src/lib/ui/core/Tooltip/Tooltip.svelte index 930a3e7cd..99a7a0376 100644 --- a/src/lib/ui/core/Tooltip/Tooltip.svelte +++ b/src/lib/ui/core/Tooltip/Tooltip.svelte @@ -8,6 +8,8 @@ import { useMelt } from '$ui/utils/melt-ui.js' import { flyAndScaleOutTransition } from '$ui/utils/transitions.js' + import { useCloseOnOutsideClick } from '../Popover/closeOutside.svelte.js' + type FloatingConfig = NonNullable type TooltipType = 'plain' | 'arrow' @@ -63,19 +65,11 @@ useMelt(triggerRef, trigger) - let contentEl = $state() - - if (closeOnOutsideClick) { - $effect(() => { - if (!$open) return - - return on(window, 'pointerdown', (e) => { - if (contentEl && e.composedPath().includes(contentEl)) return - - open.set(false) - }) - }) - } + const { contentEl } = useCloseOnOutsideClick({ + enabled: closeOnOutsideClick, + getIsOpened: () => $open, + close: () => open.set(false), + }) $effect(() => { open.set(isOpened) @@ -87,7 +81,7 @@ {#if $open}
- This popover opens on hover and its content is aligned with the start of the trigger. - +
This popover can stay open on another button click
{/snippet} - + {#snippet children({ props })} {/snippet} {#snippet content()} -
- This popover also opens on hover, with content aligned to the end of the trigger. -
+
This popover should close on click outside of its content
{/snippet}
diff --git a/src/stories/Design System - Core UI/Popover/index.stories.ts b/src/stories/Design System - Core UI/Popover/index.stories.ts index 270d85694..da3050fa6 100644 --- a/src/stories/Design System - Core UI/Popover/index.stories.ts +++ b/src/stories/Design System - Core UI/Popover/index.stories.ts @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/svelte' import component from './index.svelte' -import PopoverWithButtonComponent from './PopoverWithButton.svelte' +import HoverWithButtonComponent from './HoverWithButton.svelte' import PositionedPopover from './PositionedPopover.svelte' const meta = { @@ -22,8 +22,8 @@ export const PopoverWithPositionConfig: StoryObj = { }), } -export const PopoverWithButton: StoryObj = { +export const HoverWithButton: StoryObj = { render: () => ({ - Component: PopoverWithButtonComponent, + Component: HoverWithButtonComponent, }), }