'use client'; import * as React from 'react'; import { View } from 'react-native'; import { useSelectRootContext } from '../root/SelectRootContext'; import { useSelectPortalContext } from '../portal/SelectPortalContext'; 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 { SelectPositionerContext } from './SelectPositionerContext'; import { useStoreState } from '../../store/ReactStore'; /** * Positions the select popup against the trigger. * Renders a ``. */ export function SelectPositioner(componentProps: SelectPositioner.Props) { const { align = 'start', alignOffset = 0, arrowPadding = 5, className, collisionPadding = 5, render, side = 'bottom', sideOffset = 0, sticky = false, style, ref, ...elementProps } = componentProps; useSelectPortalContext(); const store = useSelectRootContext(); const open = useStoreState(store, 'open'); const triggerNode = useStoreState(store, 'triggerNode'); const triggerWidth = useStoreState(store, 'triggerWidth'); const triggerHeight = useStoreState(store, 'triggerHeight'); const positioning = useAnchorPositioning({ align, open, alignOffset, arrowPadding, collisionPadding, side, sideOffset, sticky, }); const { positionerStyles, refs, update, arrowRef, arrowStyles, onArrowLayout } = positioning; useIsoLayoutEffect(() => { refs.setReference(triggerNode ?? null); }, [refs, triggerNode]); useIsoLayoutEffect(() => { store.set('update', update); return () => { store.set('update', undefined); }; }, [store, update]); const mergedRef = useMergedRefs(ref, refs.setFloating); const state: SelectPositionerState = { open, side: positioning.side, align: positioning.align, triggerWidth, triggerHeight, }; const contextValue: SelectPositionerContext = React.useMemo( () => ({ side: positioning.side, align: positioning.align, arrowRef, arrowStyles, onArrowLayout, }), [positioning.side, positioning.align, arrowRef, arrowStyles, onArrowLayout], ); const element = useRenderElement(View, componentProps, { state, ref: mergedRef, props: [ { style: positionerStyles, onLayout() { update(); }, }, elementProps, ], }); return ( {element} ); } export interface SelectPositionerState { open: boolean; side: PhysicalSide; align: Align; /** * The trigger's measured width, available for consumers to apply to the popup. * This is the React Native equivalent of the web's `--anchor-width` CSS variable. */ triggerWidth: number | undefined; /** * The trigger's measured height. */ triggerHeight: number | undefined; } export interface SelectPositionerProps extends UseAnchorPositioningSharedParameters, ZestUIComponentProps {} export namespace SelectPositioner { export type State = SelectPositionerState; export type Props = SelectPositionerProps; }