fix(kez-chat/web): emoji picker pops up as overlay instead of squeezing compose
Bug: I was appending the <emoji-picker> 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 <noreply@anthropic.com>
This commit is contained in:
parent
d789e872b1
commit
ca5290dc0f
@ -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 <emoji-picker> custom element mounts. */
|
||||
let popoverEl: HTMLDivElement | null = $state(null);
|
||||
let pickerEl: HTMLElement | null = null;
|
||||
let loading = $state(false);
|
||||
let loadError = $state<string | null>(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 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative" bind:this={mountEl}>
|
||||
<!-- `relative` here is purely a positioning anchor for the absolute
|
||||
popover — the popover itself is taken out of flow so it doesn't
|
||||
squeeze the rest of the compose row. -->
|
||||
<div class="relative shrink-0" bind:this={wrapEl}>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-2 text-xl border border-gray-300 rounded hover:bg-gray-50 leading-none"
|
||||
@ -82,15 +97,16 @@
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="absolute bottom-full right-0 mb-2 z-50 shadow-lg rounded overflow-hidden bg-white"
|
||||
bind:this={popoverEl}
|
||||
class="absolute bottom-full left-0 mb-2 z-50 shadow-lg rounded overflow-hidden bg-white"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="p-4 text-xs text-gray-500">Loading…</div>
|
||||
{:else if loadError}
|
||||
<div class="p-4 text-xs text-red-700">Failed: {loadError}</div>
|
||||
{/if}
|
||||
<!-- The actual <emoji-picker> custom element is appended here
|
||||
imperatively by togglePicker() so we can control its lifecycle. -->
|
||||
<!-- The <emoji-picker> custom element is appended into this div
|
||||
imperatively by togglePicker() so we control its lifecycle. -->
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user