fix: ui fixes in emoji picker

This commit is contained in:
gosangam
2026-06-13 02:53:25 +05:30
parent 9e68184750
commit 397a09e814
2 changed files with 74 additions and 51 deletions
+70 -50
View File
@@ -13,7 +13,7 @@
* caller decides what to do with each via `onSelect`.
*/
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Smile } from "lucide-react";
import { cn } from "@/lib/utils";
@@ -71,6 +71,32 @@ export function EmojiPicker({
const [open, setOpen] = useState(false);
const [pos, setPos] = useState<{ bottom: number; left: number } | null>(null);
const btnRef = useRef<HTMLButtonElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
// Dismiss on any outside pointer-down or Escape. Deferred a tick so the
// opening click doesn't immediately re-close it; capture phase so it fires
// wherever the target lives. No close-on-scroll here — the panel scrolls
// internally, so a scroll inside it must NOT dismiss.
useEffect(() => {
if (!open) return;
function onDown(e: PointerEvent) {
const t = e.target as Node;
if (panelRef.current?.contains(t) || btnRef.current?.contains(t)) return;
setOpen(false);
}
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
const id = window.setTimeout(() => {
document.addEventListener("pointerdown", onDown, true);
document.addEventListener("keydown", onKey);
}, 0);
return () => {
window.clearTimeout(id);
document.removeEventListener("pointerdown", onDown, true);
document.removeEventListener("keydown", onKey);
};
}, [open]);
function toggle() {
if (open) {
@@ -108,59 +134,53 @@ export function EmojiPicker({
</button>
{open && pos && typeof document !== "undefined"
? createPortal(
<>
<div
ref={panelRef}
role="dialog"
aria-label="Emoji picker"
style={{
position: "fixed",
bottom: pos.bottom,
left: pos.left,
width: PANEL_WIDTH,
maxWidth: "calc(100vw - 1rem)",
}}
// pointer-events-auto: vaul makes the drawer modal, setting
// body { pointer-events: none } while open — a body-portaled
// panel inherits it and would be click-through without this.
className="pointer-events-auto z-[101] flex max-h-80 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-xl animate-in fade-in-0 zoom-in-95"
>
<div
className="fixed inset-0 z-[100]"
aria-hidden
onClick={(e) => {
e.stopPropagation();
setOpen(false);
}}
/>
<div
role="dialog"
aria-label="Emoji picker"
style={{
position: "fixed",
bottom: pos.bottom,
left: pos.left,
width: PANEL_WIDTH,
maxWidth: "calc(100vw - 1rem)",
}}
className="z-[101] flex max-h-[min(60vh,20rem)] flex-col rounded-xl border border-border bg-card shadow-xl animate-in fade-in-0 zoom-in-95"
// min-h-0 lets this flex child shrink below its content so it
// actually scrolls; stopPropagation keeps the wheel/touch from
// reaching the host drawer (vaul) and being hijacked.
className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2"
style={{ WebkitOverflowScrolling: "touch", touchAction: "pan-y" }}
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
<div
// min-h-0 lets this flex child shrink below its content so it
// actually scrolls; stopPropagation keeps the wheel/touch from
// reaching the host drawer (vaul) and being hijacked.
className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2"
style={{ WebkitOverflowScrolling: "touch", touchAction: "pan-y" }}
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
{EMOJI_GROUPS.map((g) => (
<div key={g.label} className="mb-1.5">
<p className="px-1 pb-0.5 pt-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
{g.label}
</p>
<div className="grid grid-cols-8 gap-0.5">
{g.emojis.map((e, i) => (
<button
key={`${e}-${i}`}
type="button"
onClick={() => onSelect(e)}
aria-label={`Emoji ${e}`}
className="flex size-8 items-center justify-center rounded-md text-xl leading-none hover:bg-muted"
>
{e}
</button>
))}
</div>
{EMOJI_GROUPS.map((g) => (
<div key={g.label} className="mb-1.5">
<p className="px-1 pb-0.5 pt-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
{g.label}
</p>
<div className="grid grid-cols-8 gap-0.5">
{g.emojis.map((e, i) => (
<button
key={`${e}-${i}`}
type="button"
onClick={() => onSelect(e)}
aria-label={`Emoji ${e}`}
className="flex size-8 items-center justify-center rounded-md text-xl leading-none hover:bg-muted"
>
{e}
</button>
))}
</div>
))}
</div>
</div>
))}
</div>
</>,
</div>,
document.body,
)
: null}