import React, { useCallback, useMemo, useRef, useState } from "react"; import type { ReactNode } from "react"; import { View } from "react-native"; import { useStyles } from "./SelectRoot.style"; import type { SelectRootProps } from "./SelectComposableTypes"; import { SelectFieldContext } from "./components/SelectFieldContext"; import { FieldLabel } from "./components/SelectLabel"; import { HelperText } from "../primitives/HelperText"; import { SelectPrimitive } from "../primitives/Select"; /** * Composable `Select.Root` chrome: renders the optional `description` / `error` * below the field (via `HelperText`), owns the field-state context, and places the * field label. The label content is authored via `Select.Label` (registered into * this context); `labelPlacement` decides whether it renders in the above slot * (here) or the inside slot (`Select.Trigger`). `error` takes priority over * `description`. See `Select.guide.md` for the label / accessibility / layering * model. */ export function SelectRoot({ labelPlacement = "inside", accessibilityLabel: accessibilityLabelProp, description, error, invalid, status, readOnly, children, ...rootProps }: SelectRootProps) { const styles = useStyles(); // An empty / whitespace-only string error counts as "no error". const hasError = typeof error === "string" ? error.trim().length > 0 : error != null; const isInvalid = invalid === true || status === "critical" || hasError; // A composed `Select.Label` registers its content + (for a string) its // accessible name here. const [registered, setRegistered] = useState<{ content: ReactNode; name?: string; }>(); // Track how many `Select.Label`s are mounted to warn on unsupported // multi-label usage (the registry holds a single label — the last one wins). const labelCount = useRef(0); const registerLabel = useCallback((content: ReactNode, name?: string) => { labelCount.current += 1; if (labelCount.current > 1) { console.warn( "Select: multiple are composed — only one is supported; the last one wins.", ); } setRegistered({ content, name }); }, []); const unregisterLabel = useCallback(() => { labelCount.current = Math.max(0, labelCount.current - 1); setRegistered(undefined); }, []); const labelContent = registered?.content; const accessibilityLabel = accessibilityLabelProp ?? registered?.name; const fieldState = useMemo( () => ({ labelPlacement, labelContent, accessibilityLabel, invalid: isInvalid, readOnly, registerLabel, unregisterLabel, }), [ labelPlacement, labelContent, accessibilityLabel, isInvalid, readOnly, registerLabel, unregisterLabel, ], ); return ( {labelPlacement === "above" && } {children} {renderBelowField(hasError ? error : undefined, description)} ); } function renderBelowField( error: SelectRootProps["error"], description: SelectRootProps["description"], ): ReactNode { if (error != null) { return typeof error === "string" ? ( ) : ( error ); } if (description != null) { return typeof description === "string" ? ( ) : ( description ); } return null; }