1import type { ReactNode } from "react";
2import { useEffect, useRef, useState } from "react";
3import { View, type ViewInstance } from "react-native";
4
5import { useComposerPopoverHost } from "./ComposerPopoverHost";
6
7const COMPOSER_GAP = 8;
8
9type Frame = { readonly x: number; readonly y: number; readonly width: number };
10
11/**
12 * Places a composer popover just above the composer. Android only delivers
13 * drags to views inside their parent's bounds, so a popover hung above the
14 * composer with `bottom-full` takes taps but its list never scrolls. Render it
15 * into ComposerPopoverHost instead, which shares the composer's keyboard
16 * transform, so only the composer's resting layout needs measuring.
17 */
18export function ComposerPopoverAnchor(props: { readonly children: ReactNode }) {
19 const host = useComposerPopoverHost();
20 const anchorRef = useRef<ViewInstance>(null);
21 const [frame, setFrame] = useState<Frame | null>(null);
22
23 const measure = () => {
24 const hostView = host?.hostRef.current;
25 if (!hostView) {
26 return;
27 }
28 anchorRef.current?.measureLayout(hostView, (x, y, width) => {
29 setFrame((current) =>
30 current?.x === x && current.y === y && current.width === width ? current : { x, y, width },
31 );
32 });
33 };
34
35 // No dependency array: the composer re-renders when its padding or expanded
36 // state changes, which moves it without resizing the anchor, so measure and
37 // re-project after every render.
38 useEffect(() => {
39 measure();
40 host?.setContent(
41 frame === null ? null : (
42 <View
43 pointerEvents="box-none"
44 className="absolute top-0 justify-end"
45 style={{ left: frame.x, width: frame.width, height: frame.y - COMPOSER_GAP }}
46 >
47 {props.children}
48 </View>
49 ),
50 );
51 });
52
53 const setHostContent = host?.setContent;
54 useEffect(() => () => setHostContent?.(null), [setHostContent]);
55
56 if (host === null) {
57 return <View className="absolute inset-x-0 bottom-full z-10 mb-2">{props.children}</View>;
58 }
59
60 // Covers the composer, so its onLayout fires whenever the composer resizes.
61 return (
62 <View
63 ref={anchorRef}
64 collapsable={false}
65 pointerEvents="none"
66 className="absolute inset-0"
67 onLayout={measure}
68 />
69 );
70}