fix(messaging-ui): mail cards no longer clip + emoji picker portals above overflow (0.1.3)

- Mail reader: .miu-mail-msg gets flex:0 0 auto so cards keep their natural height
  in the scrolling column instead of being compressed + clipped by overflow:hidden.
- Reaction/emoji picker renders through a PopoverPortal to <body> (positioned +
  themed) so it floats above the message list instead of being cut by the scroll
  container's overflow. 72 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 02:52:18 +05:30
parent 2753a8a367
commit 3c5c6964e2
5 changed files with 97 additions and 30 deletions
@@ -0,0 +1,67 @@
import { useEffect, useLayoutEffect, useState, type CSSProperties, type ReactNode, type RefObject } from 'react';
import { createPortal } from 'react-dom';
import { copyThemeVars } from './modal-portal';
/**
* A small popover (e.g. the reaction picker) rendered into document.body so it is never clipped by
* a scroll container's `overflow: hidden` — the reported "emoji picker goes beneath the container"
* bug. Positioned fixed just below the anchor, right-aligned to it, and re-placed on scroll/resize.
* Closes on outside pointer-down, scroll of a different element, or Escape. Carries the SDK theme
* tokens (copied from the live surface) since a body-portaled node is outside the themed subtree.
*/
export function PopoverPortal({
anchorRef,
onClose,
children,
}: {
anchorRef: RefObject<HTMLElement | null>;
onClose: () => void;
children: ReactNode;
}) {
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
const [vars] = useState(copyThemeVars);
useLayoutEffect(() => {
const el = anchorRef.current;
if (!el) return;
const place = (): void => {
const r = el.getBoundingClientRect();
setPos({ top: r.bottom + 4, left: r.right });
};
place();
window.addEventListener('resize', place);
window.addEventListener('scroll', place, true);
return () => {
window.removeEventListener('resize', place);
window.removeEventListener('scroll', place, true);
};
}, [anchorRef]);
useEffect(() => {
const onDown = (e: PointerEvent): void => {
const target = e.target as Node;
if (anchorRef.current?.contains(target)) return;
if ((target as Element).closest?.('.miu-popover')) return;
onClose();
};
const onKey = (e: KeyboardEvent): void => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('pointerdown', onDown, true);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('pointerdown', onDown, true);
document.removeEventListener('keydown', onKey);
};
}, [anchorRef, onClose]);
if (typeof document === 'undefined' || !pos) return null;
return createPortal(
<div className="miu-portal">
<div className="miu-popover" style={{ top: pos.top, left: pos.left, ...vars } as CSSProperties}>
{children}
</div>
</div>,
document.body,
);
}