Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | 1x 1x 1x 11x 11x 11x 11x 12x 11x 11x 6x 6x 4x 4x 3x 6x 5x 5x 5x 5x 5x 130x 129x 129x 129x 69x 65x 65x 65x 4x 4x 127x 127x 5x 11x 11x | import { useCallback, useRef } from "react";
import { useSafeAreaFrame } from "react-native-safe-area-context";
import { Dimensions, InteractionManager, type View } from "react-native";
import { isAndroid } from "@repo/utils/platform";
/** Default gap between the chat FAB and the button it floats above (Next). */
export const FAB_ANCHOR_GAP = 16;
/** Gap used above the wider Submit button. */
export const FAB_SUBMIT_GAP = 22;
/**
* Measures a bottom action button (e.g. the "Next" or "Submit" button) and
* reports where the global chat FAB should rest: `gap`px above the button's top
* edge, expressed as the distance from the bottom of the screen to the FAB's
* bottom edge.
*
* The host interprets the reported value as an absolute `bottom` for the FAB,
* so a positive number produced here always anchors the FAB just above the
* measured button, regardless of device safe-area insets or platform.
*
* @param setFabOffset reporter passed down from the host
* @param gap vertical space above the button (defaults to {@link FAB_ANCHOR_GAP};
* pass {@link FAB_SUBMIT_GAP} for Submit buttons)
*
* Attach the returned `ref` and `onLayout` to the button whose top the FAB
* should sit above.
*/
export const useFabAnchor = (
setFabOffset?: (offset: number) => void,
gap: number = FAB_ANCHOR_GAP,
) => {
const ref = useRef<View>(null);
const frame = useSafeAreaFrame();
// Identifies the latest measure() call so an older Android frame-polling
// chain stops when a newer measurement starts.
const measureGenRef = useRef(0);
const measure = useCallback(() => {
if (!setFabOffset) return;
const generation = ++measureGenRef.current;
// iOS screen transitions slide horizontally, so the button's `y` is
// already correct even while the transition runs: measure once after
// interactions and the FAB springs concurrently with the transition.
if (!isAndroid()) {
InteractionManager.runAfterInteractions(() => {
if (generation !== measureGenRef.current || !ref.current) return;
ref.current.measureInWindow((_x, y, _width, height) => {
if (!height) return;
setFabOffset(Dimensions.get("window").height - y + gap);
});
});
return;
}
// Android transitions can move the button vertically while it animates in.
// Waiting for it to settle before reporting makes the FAB start moving only
// AFTER the transition — a visible two-step lag. Instead, report the
// button's position every frame so the FAB rides along with it, and stop
// once `y` has been stable for two consecutive frames. Offsets are reported
// against the safe-area frame bottom, which shares the window coordinates
// of `measureInWindow`; the host corrects for its container bottom (see
// host Navigation). No InteractionManager here — tracking must begin while
// the transition is still running.
const maxTries = 60; // ~1s safety cap
let prevY: number | null = null;
let stableFrames = 0;
let tries = 0;
const attempt = () => {
if (generation !== measureGenRef.current || !ref.current) return;
ref.current.measureInWindow((_x, y, _width, height) => {
Iif (generation !== measureGenRef.current) return;
if (height) {
if (y !== prevY) {
setFabOffset(frame.y + frame.height - y + gap);
stableFrames = 0;
prevY = y;
} else {
stableFrames += 1;
if (stableFrames >= 2) return; // settled
}
}
tries += 1;
if (tries < maxTries) requestAnimationFrame(attempt);
});
};
attempt();
}, [setFabOffset, gap, frame.y, frame.height]);
const onLayout = useCallback(() => measure(), [measure]);
return { ref, onLayout, remeasure: measure };
};
|