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 { onpick }: Props = $props();
|
||||||
|
|
||||||
let open = $state(false);
|
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 pickerEl: HTMLElement | null = null;
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
let loadError = $state<string | null>(null);
|
let loadError = $state<string | null>(null);
|
||||||
@ -33,15 +36,21 @@
|
|||||||
pickerEl.addEventListener("emoji-click", onEmojiClick as EventListener);
|
pickerEl.addEventListener("emoji-click", onEmojiClick as EventListener);
|
||||||
// Match the dark-on-light scheme of the rest of the app.
|
// Match the dark-on-light scheme of the rest of the app.
|
||||||
pickerEl.classList.add("light");
|
pickerEl.classList.add("light");
|
||||||
if (mountEl) mountEl.appendChild(pickerEl);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadError = (e as Error).message;
|
loadError = (e as Error).message;
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
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);
|
document.addEventListener("click", onDocumentClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +67,10 @@
|
|||||||
|
|
||||||
function onDocumentClick(ev: MouseEvent) {
|
function onDocumentClick(ev: MouseEvent) {
|
||||||
// Close when the user clicks anywhere outside the popover or trigger.
|
// 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(() => {
|
onDestroy(() => {
|
||||||
@ -67,7 +79,10 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="px-2 py-2 text-xl border border-gray-300 rounded hover:bg-gray-50 leading-none"
|
class="px-2 py-2 text-xl border border-gray-300 rounded hover:bg-gray-50 leading-none"
|
||||||
@ -82,15 +97,16 @@
|
|||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<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}
|
{#if loading}
|
||||||
<div class="p-4 text-xs text-gray-500">Loading…</div>
|
<div class="p-4 text-xs text-gray-500">Loading…</div>
|
||||||
{:else if loadError}
|
{:else if loadError}
|
||||||
<div class="p-4 text-xs text-red-700">Failed: {loadError}</div>
|
<div class="p-4 text-xs text-red-700">Failed: {loadError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- The actual <emoji-picker> custom element is appended here
|
<!-- The <emoji-picker> custom element is appended into this div
|
||||||
imperatively by togglePicker() so we can control its lifecycle. -->
|
imperatively by togglePicker() so we control its lifecycle. -->
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user