'use client'; import * as React from 'react'; import { View } from 'react-native'; import { useToastProviderContext } from '../provider/ToastProviderContext'; import { useRenderElement } from '../../use-render/useRenderElement'; import { useIsoLayoutEffect } from '../../hooks/useIsoLayoutEffect'; import { useMergedRefs } from '../../hooks/useMergedRefs'; import { useAnchorPositioning, type Align, type PhysicalSide, type UseAnchorPositioningSharedParameters, } from '../../utils/useAnchorPositioning'; import type { ZestUIComponentProps } from '../../types'; import { ToastPositionerContext } from './ToastPositionerContext'; import { useStoreState } from '../../store/ReactStore'; /** * Positions a toast against an anchor, for a toast that belongs to something on * screen rather than to the app as a whole. * Renders a ``. * * **Why this cannot just reuse the other Positioners.** `useAnchorPositioning` * measures anchors in screen coordinates, which every other popup family can use * as-is because they render inside a `statusBarTranslucent` `Modal` whose origin * *is* the screen origin. `Toast.Viewport` is not a Modal — it is an ordinary * View that can sit anywhere — so its own screen origin is subtracted here to * bring the anchor back into the viewport's coordinate space. */ export function ToastPositioner(componentProps: ToastPositioner.Props) { const { align = 'center', alignOffset = 0, anchor, arrowPadding = 5, className, collisionPadding = 5, render, side = 'top', sideOffset = 0, sticky = false, style, ref, ...elementProps } = componentProps; const store = useToastProviderContext(); const viewportOrigin = useStoreState(store, 'viewportOrigin'); const positioning = useAnchorPositioning({ align, alignOffset, arrowPadding, collisionPadding, side, sideOffset, sticky, }); const { arrowRef, arrowStyles, positionerStyles, refs, update } = positioning; useIsoLayoutEffect(() => { refs.setReference(anchor ?? null); }, [refs, anchor]); const mergedRef = useMergedRefs(ref, refs.setFloating); const viewportRelativeStyles = React.useMemo( () => ({ ...positionerStyles, left: positionerStyles.left - viewportOrigin.x, top: positionerStyles.top - viewportOrigin.y, }), [positionerStyles, viewportOrigin], ); const state: ToastPositionerState = { side: positioning.side, align: positioning.align }; const contextValue: ToastPositionerContext = React.useMemo( () => ({ side: positioning.side, align: positioning.align, arrowRef, arrowStyles }), [positioning.side, positioning.align, arrowRef, arrowStyles], ); const element = useRenderElement(View, componentProps, { state, ref: mergedRef, props: [ { style: viewportRelativeStyles, onLayout() { update(); }, }, elementProps, ], }); return ( {element} ); } export interface ToastPositionerState { side: PhysicalSide; align: Align; } export interface ToastPositionerProps extends UseAnchorPositioningSharedParameters, ZestUIComponentProps { /** * The element to position the toast against. Unlike the other popup families * there is no trigger to infer it from, so it is passed in — usually a ref's * `current`. */ anchor?: unknown; } export namespace ToastPositioner { export type State = ToastPositionerState; export type Props = ToastPositionerProps; }