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
+4 -1
View File
@@ -97,7 +97,10 @@ export function CommentMenu({
ref={menuRef} ref={menuRef}
role="menu" role="menu"
style={{ position: "fixed", top: pos.top, right: pos.right }} style={{ position: "fixed", top: pos.top, right: pos.right }}
className="z-[101] min-w-40 rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95" // pointer-events-auto: the comments drawer is modal (vaul sets
// body { pointer-events: none } while open); this portaled menu
// would be click-through without re-enabling it here.
className="pointer-events-auto z-[101] min-w-40 rounded-lg border border-border bg-card shadow-lg py-1 animate-in fade-in-0 zoom-in-95"
> >
{canPin ? ( {canPin ? (
<MenuItem <MenuItem
+70 -50
View File
@@ -13,7 +13,7 @@
* caller decides what to do with each via `onSelect`. * 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 { createPortal } from "react-dom";
import { Smile } from "lucide-react"; import { Smile } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -71,6 +71,32 @@ export function EmojiPicker({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [pos, setPos] = useState<{ bottom: number; left: number } | null>(null); const [pos, setPos] = useState<{ bottom: number; left: number } | null>(null);
const btnRef = useRef<HTMLButtonElement>(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() { function toggle() {
if (open) { if (open) {
@@ -108,59 +134,53 @@ export function EmojiPicker({
</button> </button>
{open && pos && typeof document !== "undefined" {open && pos && typeof document !== "undefined"
? createPortal( ? 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 <div
className="fixed inset-0 z-[100]" // min-h-0 lets this flex child shrink below its content so it
aria-hidden // actually scrolls; stopPropagation keeps the wheel/touch from
onClick={(e) => { // reaching the host drawer (vaul) and being hijacked.
e.stopPropagation(); className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2"
setOpen(false); style={{ WebkitOverflowScrolling: "touch", touchAction: "pan-y" }}
}} onWheel={(e) => e.stopPropagation()}
/> onTouchMove={(e) => e.stopPropagation()}
<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"
> >
<div {EMOJI_GROUPS.map((g) => (
// min-h-0 lets this flex child shrink below its content so it <div key={g.label} className="mb-1.5">
// actually scrolls; stopPropagation keeps the wheel/touch from <p className="px-1 pb-0.5 pt-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
// reaching the host drawer (vaul) and being hijacked. {g.label}
className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-2" </p>
style={{ WebkitOverflowScrolling: "touch", touchAction: "pan-y" }} <div className="grid grid-cols-8 gap-0.5">
onWheel={(e) => e.stopPropagation()} {g.emojis.map((e, i) => (
onTouchMove={(e) => e.stopPropagation()} <button
> key={`${e}-${i}`}
{EMOJI_GROUPS.map((g) => ( type="button"
<div key={g.label} className="mb-1.5"> onClick={() => onSelect(e)}
<p className="px-1 pb-0.5 pt-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground"> aria-label={`Emoji ${e}`}
{g.label} className="flex size-8 items-center justify-center rounded-md text-xl leading-none hover:bg-muted"
</p> >
<div className="grid grid-cols-8 gap-0.5"> {e}
{g.emojis.map((e, i) => ( </button>
<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> ))}
</div> </div>
</>, </div>,
document.body, document.body,
) )
: null} : null}