// Copyright (c) Meta Platforms, Inc. and affiliates. /** * @file FieldStatus.tsx * @input Uses React, stylex, theme tokens, useAnnounce, Icon * @output Exports FieldStatus component, FieldStatusProps * @position Core implementation; consumed by Field, Switch, CheckboxInput, and the FieldStatus entrypoint * * SYNC: When modified, update these files to stay in sync: * - /packages/core/src/FieldStatus/FieldStatus.doc.mjs (props table, features, implementation notes) * - /packages/core/src/Field/Field.doc.mjs (compat docs when public API changes) * - /packages/core/src/FieldStatus/index.ts (exports if types change) * - /packages/core/src/Field/index.ts (compat re-export if public API changes) * - /packages/cli/assets/templates/blocks/components/FieldStatus/ (showcase blocks) */ 'use client'; import React, { useEffect } from 'react'; import * as stylex from "../style-runtime.js"; import type { BaseProps } from "../BaseProps.js"; import { useAnnounce } from "../hooks/useAnnounce.js"; import { mergeProps } from "../utils/index.js"; import "../theme/tokens.stylex.js"; import { colorVars, radiusVars, spacingVars, typographyVars, typeScaleVars } from "../theme/tokens.stylex.js"; import type { InputStatusType } from "../Field/types.js"; import { useEntryAnimation } from "../hooks/useEntryAnimation.js"; import { themeProps } from "../utils/themeProps.js"; import { Icon } from "../Icon/index.js"; import type { IconName } from "../Icon/index.js"; import type { FieldStatusVariantMap } from "./index.js"; /** * Maps each status type to its status glyph. Mirrors the mapping the input * controls already use for the on-field status affordance, so the detached * message shows the same icon a consumer sees elsewhere for that status. */ const statusIconMap: Record = { warning: 'warning', error: 'error', success: 'success' }; const ATTACHED_OVERLAP = `var(--_field-status-overlap, ${spacingVars['--spacing-1-5']})`; const styles = { base: { kMv6JI: "x9ynric", kGuDYH: "x141an7d", kLWn49: "x1ltkj2j", $$css: true as const }, attached: { keoZOQ: "x3dbumh", kLKAdn: "xdgtoc0", kGO01o: "x1wesfrj", kg3NbH: "xf314gf", kVL7Gh: "xquck67", kT0f0o: "x14i3lts", kfzvcC: "x47corl", $$css: true as const }, detached: { keoZOQ: "xcsaf9d", k8WAf4: "xce4md1", kg3NbH: "xf314gf", kaIpWk: "xh6dtrn", $$css: true as const } }; const colorStyles = { warning: { kWkggS: "x24i8r5", kMwMTN: "xdhq94a", $$css: true as const }, error: { kWkggS: "x1pritpl", kMwMTN: "x1joocv1", $$css: true as const }, success: { kWkggS: "xu13z74", kMwMTN: "xltfdvo", $$css: true as const } }; /** * FieldStatus variant type. Extensible via module augmentation of FieldStatusVariantMap. */ export type FieldStatusVariant = keyof FieldStatusVariantMap; export interface FieldStatusProps extends BaseProps { ref?: React.Ref; /** * The type of status to display. */ type: InputStatusType; /** * The status message to display. */ message: string; /** * Visual variant of the status message. * - 'attached': Overlaps with input above (used in Field) * - 'detached': Floats below with spacing (used in Switch, CheckboxInput) * @default 'attached' */ variant?: FieldStatusVariant; } /** * A status message component for form fields. * * The `detached` variant renders a leading status icon before the message so * status is not conveyed by color or position alone (WCAG 1.4.1). The icon is * decorative for assistive tech (`aria-hidden`): the message text already names * the status in words and is announced through the live region. The `attached` * variant keeps its status affordance on the bordered input, so it renders no * icon here to avoid a duplicate. The `tooltip` variant renders no message box * at all — the input surfaces the status through a tooltip on its on-field * icon — so callers skip rendering FieldStatus for it. * * Screen-reader announcements go through the persistent `useAnnounce` live * regions (assertive for errors, polite otherwise) rather than `role`/ * `aria-live` on the rendered element. Live regions that mount together with * their content are not reliably announced by assistive technology, and * FieldStatus is almost always conditionally rendered by its callers. The * message is announced whenever it appears — including on first mount — and * whenever it changes. * * @example * ``` * * * ``` */ export function FieldStatus({ ref, type, message, id, variant = 'attached', xstyle, className, style, ...rest }: FieldStatusProps) { const entryStyle = useEntryAnimation('slideDown'); const announce = useAnnounce(); // Announce the message through the persistently-mounted live regions. // Announce-on-mount is intentional: callers conditionally mount FieldStatus // when a status appears (and whole forms can mount with a server-side // validation error already present), and both cases must be heard. useEffect(() => { if (message) { announce(message, type === 'error' ? 'assertive' : 'polite'); } }, [announce, message, type]); return
{variant === 'detached' ? {message} : message}
; } FieldStatus.displayName = 'FieldStatus';