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,
};