'use client'; import * as React from 'react'; import { Pressable, type GestureResponderEvent } from 'react-native'; import { useControlled } from '../hooks/useControlled'; import { useId } from '../hooks/useId'; import { useIsoLayoutEffect } from '../hooks/useIsoLayoutEffect'; import { useRenderElement } from '../use-render/useRenderElement'; import { useButton } from '../internals/use-button/useButton'; import { useToggleGroupContext } from '../toggle-group/ToggleGroupContext'; import type { ZestUIComponentProps } from '../types'; import { error } from '../utils/error'; import { createChangeEventDetails, type ZestChangeEventDetails, } from '../utils/createChangeEventDetails'; import { REASONS } from '../utils/reasons'; /** * A two-state button that can be on or off. * Renders a ``. */ export function Toggle(componentProps: Toggle.Props) { const { className, defaultPressed: defaultPressedProp = false, disabled: disabledProp = false, onPressedChange, pressed: pressedProp, render, style, value: valueProp, ref, ...elementProps } = componentProps; // `|| undefined` handles cases where value is falsy (i.e. "") // The group is consumed as `string`-valued: a Toggle's `value` is always a string at // runtime (falling back to a generated id), and `Value extends string`. const value = useId(valueProp || undefined); const groupContext = useToggleGroupContext(); const groupValue = groupContext?.value ?? []; const defaultPressed = groupContext ? undefined : defaultPressedProp; const disabled = (disabledProp || groupContext?.disabled) ?? false; // The warning is development-only, but its hook is called unconditionally so that the Hook // order stays identical on every render. The `process.env.NODE_ENV` guard lives inside the // effect body instead of around the hook call. useIsoLayoutEffect(() => { if (process.env.NODE_ENV === 'production') { return; } if (groupContext && valueProp === undefined && groupContext.isValueInitialized) { error( 'A `` component rendered in a `` has no explicit `value` prop.', 'This will cause issues between the Toggle Group and Toggle values.', 'Provide the `` with a `value` prop matching the `` values prop type.', ); } }, [groupContext, valueProp, groupContext?.isValueInitialized]); const [pressed, setPressedState] = useControlled({ controlled: groupContext ? value !== undefined && groupValue.indexOf(value) > -1 : pressedProp, default: defaultPressed, name: 'Toggle', state: 'pressed', }); const { getButtonProps } = useButton({ disabled }); const state: ToggleState = React.useMemo( () => ({ disabled, pressed }), [disabled, pressed], ); return useRenderElement(Pressable, componentProps, { state, ref, props: [ { accessibilityRole: 'button' as const, accessibilityState: { selected: pressed, disabled: disabled || undefined }, 'aria-pressed': pressed, onPress(event: GestureResponderEvent) { if (disabled) { return; } const nextPressed = !pressed; const eventDetails = createChangeEventDetails(REASONS.none, event); // `onPressedChange` runs before the group commits so that canceling here // can also veto the group value change, which shares this `eventDetails` object. onPressedChange?.(nextPressed, eventDetails); if (eventDetails.isCanceled) { return; } if (value) { groupContext?.setGroupValue?.(value, nextPressed, eventDetails); } if (eventDetails.isCanceled) { return; } setPressedState(nextPressed); }, }, elementProps, getButtonProps, ], }); } export interface ToggleState { /** * Whether the toggle is currently pressed. */ pressed: boolean; /** * Whether the toggle should ignore user interaction. */ disabled: boolean; } export interface ToggleProps extends Omit, 'onPress'> { /** * Whether the toggle button is currently pressed. * This is the controlled counterpart of `defaultPressed`. */ pressed?: boolean | undefined; /** * Whether the toggle button is currently pressed. * This is the uncontrolled counterpart of `pressed`. * @default false */ defaultPressed?: boolean | undefined; /** * Whether the component should ignore user interaction. * @default false */ disabled?: boolean | undefined; /** * Callback fired when the pressed state is changed. */ onPressedChange?: | ((pressed: boolean, eventDetails: Toggle.ChangeEventDetails) => void) | undefined; /** * A unique string that identifies the toggle when used * inside a toggle group. */ value?: Value | undefined; } export type ToggleChangeEventReason = typeof REASONS.none; export type ToggleChangeEventDetails = ZestChangeEventDetails; export namespace Toggle { export type State = ToggleState; export type Props = ToggleProps; export type ChangeEventReason = ToggleChangeEventReason; export type ChangeEventDetails = ToggleChangeEventDetails; }