// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file FormLayout.tsx * @input Uses React, FormLayoutContext, StyleX * @output Exports FormLayout component and FormLayoutProps * @position Core implementation; consumed by index.ts, tested by FormLayout.test.tsx * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/FormLayout/FormLayout.test.tsx (tests for new/changed behavior) * - /packages/core/src/FormLayout/FormLayoutContext.ts (context value shape) * - /packages/core/src/FormLayout/FormLayout.doc.mjs (props table) * - /packages/core/src/FormLayout/index.ts (exports if types change) * - /packages/core/src/Field/FieldLabel.tsx (defaultOptionality indicator resolution) * - /apps/storybook/stories/FormLayout.stories.tsx (storybook stories) * - /packages/cli/assets/templates/blocks/components/FormLayout/ (showcase blocks) */ import { useMemo, type ReactNode } from 'react'; import type { BaseProps } from "../BaseProps.js"; import * as stylex from "../style-runtime.js"; import "../theme/tokens.stylex.js"; import { spacingVars } from "../theme/tokens.stylex.js"; import { FormLayoutContext, type FormLayoutDirection, type FormOptionality } from "./FormLayoutContext.js"; import { mergeProps } from "../utils/index.js"; import { themeProps } from "../utils/themeProps.js"; // ============================================================================= // Responsive breakpoint for horizontal-labels collapse // ============================================================================= const HORIZONTAL_LABELS_COLLAPSE = '@media (max-width: 480px)'; // ============================================================================= // Styles // ============================================================================= const styles = { base: { k1xSpc: "x78zum5", kXwgrk: "xdt5ytf", kOIVth: "x18g69wz", $$css: true as const }, horizontal: { k1xSpc: "xrvj5dj", kprqdN: "x1mt1orb", klIVar: "xu6a5m6", $$css: true as const }, horizontalLabels: { k1xSpc: "xrvj5dj", kumcoG: "x1pmbctz", kOIVth: "xlaq8a2", kGNEyG: "x7a106z", k41HbU: "xedohl4", kUxVDj: "x1rpgqan", k3RL8M: "x1a1jff", $$css: true as const } }; // ============================================================================= // Props // ============================================================================= export interface FormLayoutProps extends BaseProps { /** Ref forwarded to the root element */ ref?: React.Ref; /** * Form fields to arrange. Accepts Astryx inputs (TextInput, Selector, etc.) * and Field-wrapped custom controls. */ children?: ReactNode; /** * Direction of field arrangement. * * - `'vertical'` — Fields stack top-to-bottom (default). Most common. * - `'horizontal'` — Fields arrange left-to-right in equal-width columns * using CSS Grid. Each child occupies one equal column. * - `'horizontal-labels'` — CSS Grid with labels to the left of inputs. * Collapses to vertical when the container is narrow (≤480px). * * @default 'vertical' */ direction?: FormLayoutDirection; /** * Which state the form treats as its default, so only the *exception* * carries a visible optional/required indicator. It also resolves each * field's `aria-required` so the unmarked majority is still announced * correctly — but only `aria-required`, never the native `required` * attribute, so a layout default can't switch on browser validation. * * - `'optional'` — fields read as optional; only a field with `isRequired` * shows an indicator (the "required" one). * - `'required'` — fields read as required; only a field with `isOptional` * shows an indicator (the "optional" one). Fields without `isOptional` * expose `aria-required` even though they show no indicator. * - unset — today's behavior: `isRequired` and `isOptional` each show their * own indicator independently. * * A field that merely restates the default (e.g. `isOptional` under * `'optional'`) shows nothing. An inner `FormLayout` shadows an outer one. */ defaultOptionality?: FormOptionality; } // ============================================================================= // Component // ============================================================================= /** * Spatial layout container for form fields. * * Arranges form fields with consistent spacing and direction. Renders a `
` * (not a `
` — form submission is a separate concern). For label wrapping * of custom controls, use `Field` directly. * * Provides direction context to children via `FormLayoutContext`. * Supports nesting — a horizontal layout inside a vertical layout works naturally. * * @example * ``` * * * * * ``` */ export function FormLayout({ children, direction = 'vertical', defaultOptionality, xstyle, className, style, ref, ...props }: FormLayoutProps) { const contextValue = useMemo(() => ({ direction, defaultOptionality }), [direction, defaultOptionality]); return
{children}
; } FormLayout.displayName = 'FormLayout';