From 7432ffb1ec810d3af7458bced1da6e753ae4b1db Mon Sep 17 00:00:00 2001 From: xarmian Date: Fri, 10 Apr 2026 20:06:19 +0000 Subject: [PATCH] fix: emoji picker dropdown escapes dialog overflow clipping Portal the dropdown into the nearest element so it stays in the browser's top layer, and temporarily set overflow:visible on the dialog while the picker is open so it isn't clipped. --- .../common/EmojiPickerButton.svelte | 65 +++++++++++++++---- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/web/src/lib/components/common/EmojiPickerButton.svelte b/web/src/lib/components/common/EmojiPickerButton.svelte index cd0ba5ce..e25d8f89 100644 --- a/web/src/lib/components/common/EmojiPickerButton.svelte +++ b/web/src/lib/components/common/EmojiPickerButton.svelte @@ -10,13 +10,62 @@ let { value = $bindable(), placeholder = '⚡', size = 'sm' }: Props = $props(); let open = $state(false); + let triggerEl = $state(); + let dropdownX = $state(0); + let dropdownY = $state(0); + + /** + * Portal into the nearest (stays in top-layer, escapes overflow + * children like .dialog-body), or document.body if not inside a dialog. + */ + function portal(node: HTMLElement) { + const dialog = triggerEl?.closest('dialog'); + const target = dialog || document.body; + target.appendChild(node); + + // has browser-default overflow:auto which clips children. + // Temporarily override while the dropdown is mounted. + if (dialog) { + dialog.style.overflow = 'visible'; + } + + return { + destroy() { + node.remove(); + if (dialog) { + dialog.style.overflow = ''; + } + } + }; + } function handleWindowClick(e: MouseEvent) { - if (open && !(e.target as HTMLElement)?.closest('.emoji-picker-button')) { - open = false; + if (open) { + const target = e.target as HTMLElement; + if (!target?.closest('.emoji-picker-button') && !target?.closest('.epb-dropdown')) { + open = false; + } } } + function toggleOpen() { + if (!open && triggerEl) { + const triggerRect = triggerEl.getBoundingClientRect(); + const dialog = triggerEl.closest('dialog'); + if (dialog) { + // dialog with transform creates a containing block — + // position: absolute is relative to it, so offset accordingly + const dialogRect = dialog.getBoundingClientRect(); + dropdownX = triggerRect.left - dialogRect.left; + dropdownY = triggerRect.bottom - dialogRect.top + 4; + } else { + dropdownX = triggerRect.left; + dropdownY = triggerRect.bottom + 4; + } + } + open = !open; + } + function handleSelect(emoji: string) { value = emoji; open = false; @@ -27,17 +76,18 @@
{#if open} -