/** * Field — the layout and validation-state kit a form control composes into. * * PanelUI's own controls (`Input`, `Checkbox`, `Switch`, `RadioGroup`) already * carry their own label, description and error slot, so the common case needs * no wrapper at all — pass those props straight through. `Field` exists for * what doesn't fit that shape: a horizontal row pairing a control with a * label off to the side (a `Switch` in a settings list), several controls * grouped under one legend, or a rule breaking a long form into sections. * * `invalid` / `disabled` / `required` set on the root flow to `Field.Label` * and `Field.Description` through context, so a multi-field row states its * validity once instead of repeating the prop at every leaf. * * ```tsx * * * Marketing emails * Product updates, at most weekly. * * * * ``` */ import { createContext, forwardRef, useContext, useId, useMemo, type ReactNode, } from 'react'; import { View, type Text as RNText, type ViewProps } from 'react-native'; import { tv } from 'tailwind-variants'; import { Text, type TextProps, textChildren } from '../../primitives/text'; import { Label, type LabelProps } from '../label'; import { Separator } from '../separator'; const fieldVariants = tv({ slots: { root: 'gap-2', content: 'flex-1 flex-col gap-0.5', description: 'text-sm text-muted-foreground', error: 'gap-1', errorText: 'text-sm text-destructive', bullet: 'flex-row gap-1.5', bulletDot: 'text-sm text-destructive', set: 'gap-4', legend: 'text-foreground', group: 'gap-6', separator: 'flex-row items-center gap-3', separatorLabel: 'text-xs text-muted-foreground', title: 'text-base font-medium text-foreground', }, variants: { orientation: { vertical: { root: 'flex-col' }, horizontal: { root: 'flex-row items-center justify-between gap-4' }, }, disabled: { true: { root: 'opacity-[0.64]' }, }, legendVariant: { legend: { legend: 'text-base font-semibold' }, label: { legend: 'text-sm font-medium' }, }, }, defaultVariants: { orientation: 'vertical', }, }); interface FieldState { invalid: boolean; disabled: boolean; required: boolean; relationshipIds: string[]; } const FieldContext = createContext({ invalid: false, disabled: false, required: false, relationshipIds: [], }); /** Android can announce the label, help and error text rendered beside a control. */ export function useFieldLabelledBy(): string[] | undefined { const { relationshipIds } = useContext(FieldContext); return relationshipIds.length > 0 ? relationshipIds : undefined; } export interface FieldProps extends ViewProps { className?: string; /** `horizontal` puts a label beside the control instead of above it. */ orientation?: 'vertical' | 'horizontal'; /** Marks every `Field.Label`/`Field.Description` below as invalid. */ invalid?: boolean; disabled?: boolean; /** Marks every `Field.Label` below as required, same as `Label`'s own prop. */ required?: boolean; children?: ReactNode; } const FieldRoot = forwardRef( ( { className, orientation = 'vertical', invalid = false, disabled = false, required = false, children, ...props }, ref ) => { const { root } = fieldVariants({ orientation, disabled }); const id = useId().replace(/:/g, ''); const labelId = `panelui-field-${id}-label`; const titleId = `panelui-field-${id}-title`; const descriptionId = `panelui-field-${id}-description`; const errorId = `panelui-field-${id}-error`; const state = useMemo( () => ({ invalid, disabled, required, relationshipIds: [labelId, titleId, descriptionId, errorId], }), [invalid, disabled, required, labelId, titleId, descriptionId, errorId] ); return ( {textChildren(children)} ); } ); FieldRoot.displayName = 'Field'; export interface FieldContentProps extends ViewProps { className?: string; children?: ReactNode; } const FieldContent = forwardRef( ({ className, ...props }, ref) => { const { content } = fieldVariants(); return ; } ); FieldContent.displayName = 'Field.Content'; export interface FieldLabelProps extends LabelProps {} const FieldLabel = forwardRef( ({ isRequired, isInvalid, isDisabled, nativeID, ...props }, ref) => { const ctx = useContext(FieldContext); return (