From ca5290dc0f169d4a5f7ea53039dc978329ab815c Mon Sep 17 00:00:00 2001 From: Jason Tudisco Date: Wed, 27 May 2026 00:03:38 -0600 Subject: [PATCH] fix(kez-chat/web): emoji picker pops up as overlay instead of squeezing compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: I was appending the custom element to the OUTER wrapper div (which is part of the flex compose row), so when the picker opened it became a flex sibling of the button and pushed the text input + Send button down to a tiny strip. Fix: append into the absolute-positioned popover container (new popoverEl ref) instead of the wrapper. The popover is taken out of flow so the compose row stays put and the picker floats above it. Also: - Outer wrapper gets shrink-0 so it doesn't expand even if the picker somehow leaks. - Click-outside check now looks at both wrapEl AND popoverEl (since the picker is no longer a descendant of the wrapper). - Popover anchors bottom-full left-0 — picker grows up and to the right of the 😀 button. Co-Authored-By: Claude Opus 4.7 --- kez-chat/web/src/lib/EmojiButton.svelte | 34 ++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/kez-chat/web/src/lib/EmojiButton.svelte b/kez-chat/web/src/lib/EmojiButton.svelte index f47257d..11ffd78 100644 --- a/kez-chat/web/src/lib/EmojiButton.svelte +++ b/kez-chat/web/src/lib/EmojiButton.svelte @@ -13,7 +13,10 @@ let { onpick }: Props = $props(); let open = $state(false); - let mountEl: HTMLDivElement | null = $state(null); + /** Outer wrapper — captures the document-click bounds (must include the button). */ + let wrapEl: HTMLDivElement | null = $state(null); + /** Floating popover container — where the custom element mounts. */ + let popoverEl: HTMLDivElement | null = $state(null); let pickerEl: HTMLElement | null = null; let loading = $state(false); let loadError = $state(null); @@ -33,15 +36,21 @@ pickerEl.addEventListener("emoji-click", onEmojiClick as EventListener); // Match the dark-on-light scheme of the rest of the app. pickerEl.classList.add("light"); - if (mountEl) mountEl.appendChild(pickerEl); } catch (e) { loadError = (e as Error).message; } finally { loading = false; } - } else if (mountEl && !mountEl.contains(pickerEl)) { - mountEl.appendChild(pickerEl); } + // Mount into the floating popover div on every open — the popover + // is conditionally rendered, so it's a fresh DOM node each time. + // Wait a tick so the {#if open} block actually mounts before we + // try to appendChild into it. + queueMicrotask(() => { + if (pickerEl && popoverEl && !popoverEl.contains(pickerEl)) { + popoverEl.appendChild(pickerEl); + } + }); document.addEventListener("click", onDocumentClick); } @@ -58,7 +67,10 @@ function onDocumentClick(ev: MouseEvent) { // Close when the user clicks anywhere outside the popover or trigger. - if (mountEl && !mountEl.contains(ev.target as Node)) close(); + const t = ev.target as Node; + const inside = + (wrapEl && wrapEl.contains(t)) || (popoverEl && popoverEl.contains(t)); + if (!inside) close(); } onDestroy(() => { @@ -67,7 +79,10 @@ }); -
+ +
{/if}