import { type Accessor, createContext, useContext } from "solid-js"; import type { BaseUIChangeEventDetails, REASONS, } from "../internals/change-event-details"; import type { FieldRootState } from "../internals/field-constants"; /** * Reason string attached to every `onValueChange` event details object. * Matches Base UI React's `RadioGroup.ChangeEventReason`. */ export type RadioGroupChangeEventReason = typeof REASONS.none; /** * Shape of the details bag handed to `onValueChange`. The consumer may call * `details.cancel()` on the uncontrolled branch to veto the internal write. */ export type RadioGroupChangeEventDetails = BaseUIChangeEventDetails; /** * State exposed by ``. Inherits the surrounding `` * state so the shared `data-valid` / `data-touched` / `data-dirty` / * `data-filled` / `data-focused` / `data-invalid` projection flows through. */ export interface RadioGroupState extends FieldRootState { /** Whether the user should be unable to change the selected radio. */ readOnly: boolean; /** Whether the user must choose a radio before submitting a form. */ required: boolean; } /** * Context value consumed by child `` elements. Every reactive * slot is exposed as an `Accessor` so descendants pull the latest value * on demand — keeps Solid 2's no-top-level-reactive-reads rule (RFC 01) * satisfied. * * Shape diverges from the React port: React stores a plain object + triggers * re-renders; Solid needs live accessors per slot so child radios stay * reactive without a memo ping-pong. */ export interface RadioGroupContextValue { /** Currently selected value — `undefined` when nothing is selected. */ checkedValue: Accessor; /** * Request a change to the selected value. Fires the consumer's * `onValueChange` (with a cancel-capable details bag) then — if not * canceled — writes the internal signal in the uncontrolled branch. */ setCheckedValue: ( value: unknown, eventDetails: RadioGroupChangeEventDetails, ) => void; /** Whether user interaction with the group is disabled. */ disabled: Accessor; /** Whether the group rejects user-driven selection changes. */ readOnly: Accessor; /** Whether the user must select a radio before submitting. */ required: Accessor; /** `name` applied to every child radio's hidden input. */ name: Accessor; /** Shared `form` attribute applied to every child radio's hidden input. */ form: Accessor; /** * Latched by the group's capturing `onKeyDown` whenever the user presses * an arrow key, and read by a focused radio's `onFocus` handler so the * radio can auto-select on arrow-key navigation (mirrors React's * `touched` / `setTouched` dance in `RadioGroup.tsx:176`). */ touched: Accessor; /** Flip the `touched` latch. Consumers call `setTouched(false)` after * acting on a touched-focus to disarm it. */ setTouched: (next: boolean) => void; /** Marker — `true` when this context was provided by a real ``. */ readonly present: boolean; } const NOOP = () => {}; /** * Sentinel returned when a `` renders outside a ``. * Every setter is a no-op and every accessor returns "nothing". Callers * branch on `.present` to decide whether to fall back to their standalone * path (e.g. the radio's own consumer-owned signal for checked state). */ export const NOOP_RADIO_GROUP_CONTEXT: RadioGroupContextValue = { checkedValue: () => undefined, setCheckedValue: NOOP, disabled: () => false, readOnly: () => false, required: () => false, name: () => undefined, form: () => undefined, touched: () => false, setTouched: NOOP, present: false, }; /** * Context carrying the current `` state. Provided via Solid 2's * context-as-provider pattern (``). */ export const RadioGroupContext = createContext( NOOP_RADIO_GROUP_CONTEXT, ); /** * Read the nearest `` context. Returns {@link NOOP_RADIO_GROUP_CONTEXT} * when there is no group ancestor — callers can branch on `.present` to * decide between "group owns the state" and standalone fallback paths. */ export function useRadioGroupContext(): RadioGroupContextValue { return useContext(RadioGroupContext); }