diff --git a/src/lib/actions/clickOutside.ts b/src/lib/actions/clickOutside.ts new file mode 100644 index 0000000..26ffbce --- /dev/null +++ b/src/lib/actions/clickOutside.ts @@ -0,0 +1,94 @@ +/** + * Svelte action that dispatches a 'clickoutside' event when the user clicks outside the element. + * + * @example + * ```svelte + *
isOpen = false}> + * Dropdown content + *
+ * ``` + * + * @example With options + * ```svelte + *
+ * Dropdown content + *
+ * ``` + */ + +export interface ClickOutsideOptions { + /** Whether the action is enabled. Defaults to true. */ + enabled?: boolean + /** Optional callback to execute on click outside. */ + callback?: () => void +} + +export function clickOutside( + element: HTMLElement, + options: ClickOutsideOptions | (() => void) = {} +) { + let enabled = true + let callback: (() => void) | undefined + + // Normalize options + if (typeof options === 'function') { + callback = options + } else { + enabled = options.enabled !== false + callback = options.callback + } + + function handleClick(event: MouseEvent) { + if (!enabled) return + + const target = event.target as Node + + // Check if click is outside the element + if (element && !element.contains(target)) { + // Dispatch custom event + element.dispatchEvent( + new CustomEvent('clickoutside', { + detail: { target } + }) + ) + + // Call callback if provided + if (callback) { + callback() + } + } + } + + // Add listener on next tick to avoid immediate triggering + setTimeout(() => { + if (enabled) { + document.addEventListener('click', handleClick, true) + } + }, 0) + + return { + update(newOptions: ClickOutsideOptions | (() => void)) { + // Remove old listener + document.removeEventListener('click', handleClick, true) + + // Normalize new options + if (typeof newOptions === 'function') { + enabled = true + callback = newOptions + } else { + enabled = newOptions.enabled !== false + callback = newOptions.callback + } + + // Add new listener if enabled + if (enabled) { + setTimeout(() => { + document.addEventListener('click', handleClick, true) + }, 0) + } + }, + destroy() { + document.removeEventListener('click', handleClick, true) + } + } +} diff --git a/src/lib/components/admin/BaseDropdown.svelte b/src/lib/components/admin/BaseDropdown.svelte index 3024a22..26fe0fc 100644 --- a/src/lib/components/admin/BaseDropdown.svelte +++ b/src/lib/components/admin/BaseDropdown.svelte @@ -2,6 +2,7 @@ import type { Snippet } from 'svelte' import Button from './Button.svelte' import DropdownMenuContainer from './DropdownMenuContainer.svelte' + import { clickOutside } from '$lib/actions/clickOutside' interface Props { isOpen?: boolean @@ -31,26 +32,17 @@ onToggle?.(isOpen) } - function handleClickOutside(event: MouseEvent) { - const target = event.target as HTMLElement - if (!target.closest(`.${className}`) && !target.closest('.dropdown-container')) { - isOpen = false - onToggle?.(false) - } + function handleClickOutside() { + isOpen = false + onToggle?.(false) } - - $effect(() => { - if (isOpen) { - // Use setTimeout to avoid immediate closing when clicking the trigger - setTimeout(() => { - document.addEventListener('click', handleClickOutside) - }, 0) - return () => document.removeEventListener('click', handleClickOutside) - } - }) -