import React from "react"; import { StyleSheet } from "react-native"; import * as Primitive from "@rn-primitives/select"; import { SafeAreaInsetsContext } from "react-native-safe-area-context"; import { useStyles } from "./SelectPrimitive.style"; import type { SelectPrimitiveContentProps, SelectPrimitiveGroupProps, SelectPrimitiveItemIndicatorProps, SelectPrimitiveItemProps, SelectPrimitiveItemTextProps, SelectPrimitiveLabelProps, SelectPrimitiveOverlayProps, SelectPrimitivePortalProps, SelectPrimitiveRootProps, SelectPrimitiveSeparatorProps, SelectPrimitiveTriggerProps, SelectPrimitiveValueProps, SelectPrimitiveViewportProps, } from "./types"; import { tokens as staticTokens } from "../../utils/design"; import { AtlantisThemeContextProvider, useAtlantisTheme, } from "../../AtlantisThemeContext"; /** Default trigger testID. Consumer-supplied `testID`s are used verbatim. */ export const SELECT_TRIGGER_DEFAULT_TEST_ID = "ATL-Select-Trigger"; function useIsDarkTheme() { return useAtlantisTheme().effectiveTheme === "dark"; } // @rn-primitives/select treats `undefined` as uncontrolled. Keep an explicitly // controlled empty value defined so clearing the prop cannot reveal stale // internal state. Its undefined fields cannot match a public string item value. const EMPTY_CONTROLLED_VALUE = {} as NonNullable< SelectPrimitiveRootProps["value"] >; function SelectPrimitiveRoot(props: SelectPrimitiveRootProps) { const isControlled = Object.prototype.hasOwnProperty.call(props, "value"); const value = isControlled && props.value === undefined ? EMPTY_CONTROLLED_VALUE : props.value; return ; } /** * Styled trigger field. Renders only its `children` — compose * `SelectPrimitive.Value` and any trailing icon (e.g. a chevron) here; the * primitive provides no chevron of its own. */ function SelectPrimitiveTrigger({ children, style, testID = SELECT_TRIGGER_DEFAULT_TEST_ID, invalid, ref, ...props }: SelectPrimitiveTriggerProps) { const styles = useStyles(); const { open, value, disabled: rootDisabled } = Primitive.useRootContext(); // `disabled` is Root-level; the primitive's Trigger won't read it from // context, so resolve it here and pass it down to block interaction. const isDisabled = rootDisabled ?? false; const isInvalid = !isDisabled && (invalid ?? false); // Announce the selected option as the trigger's value. const accessibilityValue = value?.label ? { text: value.label } : undefined; return ( [ styles.trigger, pressed && !open && styles.triggerPressed, open && styles.triggerOpen, isInvalid && styles.triggerInvalid, isDisabled && styles.triggerDisabled, style, ]} {...props} > {children} ); } function SelectPrimitiveValue({ style, ...props }: SelectPrimitiveValueProps) { const styles = useStyles(); const { value, disabled } = Primitive.useRootContext(); const isEmpty = value?.value === undefined; return ( ); } /** Portals the dropdown into the mounted `AtlantisPortalHost`. */ function SelectPrimitivePortal({ children, ...props }: SelectPrimitivePortalProps) { const { effectiveTheme } = useAtlantisTheme(); return ( {children} ); } /** Tap-outside dismiss surface behind the dropdown (no scrim). */ function SelectPrimitiveOverlay({ style, testID = "ATL-Select-Overlay", ...props }: SelectPrimitiveOverlayProps) { const styles = useStyles(); return ( ); } /** * Anchored dropdown surface: matches the trigger width, applies safe-area * insets, and flips above the trigger when space is tight. Compose it inside * `Portal`, alongside `Overlay`, wrapping the items in a `Viewport`. */ function SelectPrimitiveContent({ children, style, sideOffset = staticTokens["space-smaller"], insets, ref, ...props }: SelectPrimitiveContentProps) { const styles = useStyles(); const isDarkTheme = useIsDarkTheme(); // Read the context directly, not `useSafeAreaInsets` (which throws without a // provider): `Content` renders at the portal host, which may sit outside one. const safeAreaInsets = React.useContext(SafeAreaInsetsContext) ?? undefined; const { triggerPosition } = Primitive.useRootContext(); return ( {children} ); } /** Scroll container for the items (a no-op passthrough on native). */ function SelectPrimitiveViewport({ ...props }: SelectPrimitiveViewportProps) { return ; } /** * Styled, pressable option row. Compose its content from `SelectPrimitive.ItemText` * and `SelectPrimitive.ItemIndicator` (and anything else) as `children`. */ function SelectPrimitiveItem({ style, disabled, ref, ...props }: SelectPrimitiveItemProps) { const styles = useStyles(); const isDarkTheme = useIsDarkTheme(); return ( [ styles.item, pressed && styles.itemPressed, pressed && isDarkTheme && styles.itemPressedDarkElevated, style, ]} {...props} /> ); } /** * The option label as styled text (reads the label from the item context). The * `disabled` flag is explicit — the composing layer (e.g. the sugared * `Select.Item`) forwards the row's disabled state to grey the text. */ function SelectPrimitiveItemText({ style, disabled, ref, ...props }: SelectPrimitiveItemTextProps) { const styles = useStyles(); return ( ); } /** * The selected-state marker slot. Renders its `children` (the marker, e.g. a * checkmark) on the selected row only — it self-hides on unselected rows. The * marker glyph is supplied by the composing layer, not defaulted here. */ function SelectPrimitiveItemIndicator({ style, ref, ...props }: SelectPrimitiveItemIndicatorProps) { const styles = useStyles(); return ( ); } /** * Groups related options, typically with a `SelectPrimitive.Label` heading. */ function SelectPrimitiveGroup({ ...props }: SelectPrimitiveGroupProps) { return ; } /** * A styled group heading rendered inside `SelectPrimitive.Group`. */ function SelectPrimitiveLabel({ style, ref, ...props }: SelectPrimitiveLabelProps) { const styles = useStyles(); return ( ); } /** * A styled divider between groups or options. */ function SelectPrimitiveSeparator({ style, ref, ...props }: SelectPrimitiveSeparatorProps) { const styles = useStyles(); return ( ); } export const SelectPrimitive = { Root: SelectPrimitiveRoot, Trigger: SelectPrimitiveTrigger, Value: SelectPrimitiveValue, Portal: SelectPrimitivePortal, Overlay: SelectPrimitiveOverlay, Content: SelectPrimitiveContent, Viewport: SelectPrimitiveViewport, Item: SelectPrimitiveItem, ItemText: SelectPrimitiveItemText, ItemIndicator: SelectPrimitiveItemIndicator, Group: SelectPrimitiveGroup, Label: SelectPrimitiveLabel, Separator: SelectPrimitiveSeparator, };