From 56c1f9e1711bfce1386288b4f4bcae6d72afa8f3 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 12:34:15 -0700 Subject: [PATCH 1/5] feat(FieldStatus): deprecate the tooltip variant; field controls keep statusVariant="tooltip" component:FieldStatus FR7 and DEC-2: direct FieldStatus supports only attached and detached, and the Field family consumes tooltip before rendering FieldStatus. FieldStatusVariantMap.tooltip is marked @deprecated, naming the field control's statusVariant="tooltip" as the replacement, and stays until the next minor. Every field control's statusVariant now takes a Field-local FieldStatusPlacement (keyof FieldStatusVariantMap | 'tooltip'), so dropping tooltip from the public map later leaves them unchanged. No runtime warning (spec:AST-017 FR29, FR29a). [feat] Changeset with migration. --- .changeset/fieldstatus-tooltip-deprecated.md | 23 +++++++++++++++++++ .../src/ComplexSelector/ComplexSelector.tsx | 5 ++-- packages/core/src/DateInput/DateInput.tsx | 4 ++-- .../src/DateRangeInput/DateRangeInput.tsx | 4 ++-- packages/core/src/Field/Field.tsx | 4 ++-- .../core/src/Field/fieldStatusPlacement.ts | 18 +++++++++++++++ packages/core/src/FieldStatus/index.ts | 6 +++++ packages/core/src/FileInput/FileInput.tsx | 10 +++----- .../core/src/MultiSelector/MultiSelector.tsx | 4 ++-- packages/core/src/NumberInput/NumberInput.tsx | 4 ++-- packages/core/src/PowerSearch/PowerSearch.tsx | 5 ++-- packages/core/src/Selector/Selector.tsx | 4 ++-- packages/core/src/TextArea/TextArea.tsx | 4 ++-- packages/core/src/TextInput/TextInput.tsx | 4 ++-- packages/core/src/TimeInput/TimeInput.tsx | 4 ++-- packages/core/src/Tokenizer/Tokenizer.tsx | 4 ++-- packages/core/src/Typeahead/Typeahead.tsx | 4 ++-- .../core/src/hooks/useInputStatusIcon.tsx | 4 ++-- 18 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 .changeset/fieldstatus-tooltip-deprecated.md create mode 100644 packages/core/src/Field/fieldStatusPlacement.ts diff --git a/.changeset/fieldstatus-tooltip-deprecated.md b/.changeset/fieldstatus-tooltip-deprecated.md new file mode 100644 index 0000000000000..2142ecb990efd --- /dev/null +++ b/.changeset/fieldstatus-tooltip-deprecated.md @@ -0,0 +1,23 @@ +--- +'@astryxdesign/core': patch +--- + +[feat] FieldStatus: the `tooltip` variant is deprecated and is removed in the next minor release. FieldStatus renders no message box for it, so a direct `` shows nothing. To show a status message from a field's on-field icon, set `statusVariant="tooltip"` on the field control instead; that stays supported. + +Before: + +```tsx + +``` + +After: + +```tsx + +``` + +@cixzhang diff --git a/packages/core/src/ComplexSelector/ComplexSelector.tsx b/packages/core/src/ComplexSelector/ComplexSelector.tsx index 1ff17bf856ad2..9724af078e97e 100644 --- a/packages/core/src/ComplexSelector/ComplexSelector.tsx +++ b/packages/core/src/ComplexSelector/ComplexSelector.tsx @@ -37,7 +37,7 @@ import {useDevWarning} from '../hooks/useDevWarning'; import {mergeRefs} from '../utils/mergeRefs'; import {useMergedRefs} from '../hooks/useMergedRefs'; import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement'; -import {Field, inputWrapperStyles, type FieldStatusVariant} from '../Field'; +import {Field, inputWrapperStyles} from '../Field'; import {Icon, renderIconSlot, type IconType} from '../Icon'; import {Spinner} from '../Spinner'; import {useTranslator} from '../i18n'; @@ -65,6 +65,7 @@ import {usePressFeedback} from '../hooks/usePressFeedback'; import type {SizeValue} from '../utils/types'; import {themeProps} from '../utils/themeProps'; import {layerViewportInset} from '../Layer/layerViewportInset.stylex'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; // The popup content's own block-size cap; the viewport cap is the layer // runtime's. @@ -417,7 +418,7 @@ export interface ComplexSelectorProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' for input selectors; 'detached' for secondary and ghost selectors */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** Tooltip text displayed next to the label. */ labelTooltip?: string; /** Trigger and field size. */ diff --git a/packages/core/src/DateInput/DateInput.tsx b/packages/core/src/DateInput/DateInput.tsx index 72699ae10ab84..1b9c9a8ee2932 100644 --- a/packages/core/src/DateInput/DateInput.tsx +++ b/packages/core/src/DateInput/DateInput.tsx @@ -41,7 +41,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {Icon} from '../Icon'; import {VisuallyHidden} from '../VisuallyHidden'; @@ -215,6 +214,7 @@ import {focusOutlineStyles} from '../utils/focusOutline.stylex'; import {useLocale, useTranslator} from '../i18n'; import {useMergedRefs} from '../hooks/useMergedRefs'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; export interface DateInputProps extends Omit< FieldInputBaseProps, 'onChange' | 'defaultValue' @@ -344,7 +344,7 @@ export interface DateInputProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Width of the field. Numbers are treated as pixels, strings are used as-is diff --git a/packages/core/src/DateRangeInput/DateRangeInput.tsx b/packages/core/src/DateRangeInput/DateRangeInput.tsx index b1d1c698412c5..5e8612002251f 100644 --- a/packages/core/src/DateRangeInput/DateRangeInput.tsx +++ b/packages/core/src/DateRangeInput/DateRangeInput.tsx @@ -50,7 +50,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {Icon} from '../Icon'; import {Spinner} from '../Spinner'; @@ -82,6 +81,7 @@ import {themeProps} from '../utils/themeProps'; import {focusOutlineStyles} from '../utils/focusOutline.stylex'; import {useLocale, useTranslator} from '../i18n'; import type {Locale} from '../i18n/types'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; export type {DateRange} from '../Calendar'; @@ -443,7 +443,7 @@ export interface DateRangeInputProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Width of the field. Numbers are treated as pixels, strings are used as-is diff --git a/packages/core/src/Field/Field.tsx b/packages/core/src/Field/Field.tsx index fe9790fbfa586..924a5a55dcbe6 100644 --- a/packages/core/src/Field/Field.tsx +++ b/packages/core/src/Field/Field.tsx @@ -22,7 +22,6 @@ import type {BaseProps} from '../BaseProps'; import type {SizeValue} from '../utils/types'; import {FieldLabel} from './FieldLabel'; import {FieldStatus} from '../FieldStatus/FieldStatus'; -import type {FieldStatusVariant} from '../FieldStatus/FieldStatus'; import {useFieldStatusAnnouncement} from '../FieldStatus/useFieldStatusAnnouncement'; import {spacingVars, borderVars, sizeVars} from '../theme/tokens.stylex'; import type {IconType} from '../Icon'; @@ -32,6 +31,7 @@ import {FormLayoutContext} from '../FormLayout/FormLayoutContext'; import {Text} from '../Text/Text'; import {VisuallyHidden} from '../VisuallyHidden'; import {themeProps} from '../utils/themeProps'; +import type {FieldStatusPlacement} from './fieldStatusPlacement'; const styles = stylex.create({ container: { @@ -187,7 +187,7 @@ export interface FieldProps extends Omit< * - 'tooltip': No message box; the input surfaces status through a tooltip on its on-field icon * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Width of the field. Numbers are treated as pixels, strings are used as-is * (e.g. `'100%'`). Sizes the whole field — label, control, and status — so diff --git a/packages/core/src/Field/fieldStatusPlacement.ts b/packages/core/src/Field/fieldStatusPlacement.ts new file mode 100644 index 0000000000000..dc4551bdbe585 --- /dev/null +++ b/packages/core/src/Field/fieldStatusPlacement.ts @@ -0,0 +1,18 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +/** + * @file fieldStatusPlacement.ts + * @input The FieldStatus variant map + * @output FieldStatusPlacement, where a field control shows its status + * @position Field family type behind every field control's `statusVariant` + * + * A field control places its status message as a FieldStatus variant, or under + * `'tooltip'`, which the Field family consumes before rendering FieldStatus and + * shows through the control's on-field status icon (`component:FieldStatus` + * FR7, DEC-2). `'tooltip'` belongs here, not to FieldStatus itself, whose map + * keeps it only as a deprecated entry until the next minor. + */ + +import type {FieldStatusVariantMap} from '../FieldStatus'; + +export type FieldStatusPlacement = keyof FieldStatusVariantMap | 'tooltip'; diff --git a/packages/core/src/FieldStatus/index.ts b/packages/core/src/FieldStatus/index.ts index fe674412d418f..fdbbb48af2400 100644 --- a/packages/core/src/FieldStatus/index.ts +++ b/packages/core/src/FieldStatus/index.ts @@ -25,6 +25,12 @@ export interface FieldStatusVariantMap { attached: true; detached: true; + /** + * @deprecated FieldStatus does not render a `tooltip` status: it shows no + * message box (`component:FieldStatus` FR7). Set `statusVariant="tooltip"` + * on the field control (TextInput, Selector, and the rest), which shows the + * message from its on-field status icon. Removed in the next minor release. + */ tooltip: true; } diff --git a/packages/core/src/FileInput/FileInput.tsx b/packages/core/src/FileInput/FileInput.tsx index 65e9038aa12c5..cba10eb0d4143 100644 --- a/packages/core/src/FileInput/FileInput.tsx +++ b/packages/core/src/FileInput/FileInput.tsx @@ -37,12 +37,7 @@ import { durationVars, easeVars, } from '../theme/tokens.stylex'; -import { - Field, - InputClearButton, - type InputStatus, - type FieldStatusVariant, -} from '../Field'; +import {Field, InputClearButton, type InputStatus} from '../Field'; import {Icon} from '../Icon'; import {Spinner} from '../Spinner'; import {VisuallyHidden} from '../VisuallyHidden'; @@ -70,6 +65,7 @@ import type {TranslatorFn} from '../i18n'; import {useMergedRefs} from '../hooks/useMergedRefs'; import {useListFormatter} from '../i18n/useListFormatter'; import {useResolvedRequired} from '../hooks/useResolvedRequired'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; function formatFileSize(bytes: number, t: TranslatorFn): string { if (bytes < 1024) { return t('@astryx.fileInput.fileSize.bytes', {size: bytes}); @@ -406,7 +402,7 @@ export interface FileInputProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Description text displayed below the label. */ diff --git a/packages/core/src/MultiSelector/MultiSelector.tsx b/packages/core/src/MultiSelector/MultiSelector.tsx index aa23936e92a03..6c72ed4e654bb 100644 --- a/packages/core/src/MultiSelector/MultiSelector.tsx +++ b/packages/core/src/MultiSelector/MultiSelector.tsx @@ -39,7 +39,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputWrapperStyles, - type FieldStatusVariant, } from '../Field'; import {useKeepLayerOpenProps} from '../Layer/useLayer'; import {InternalInputClearButton} from '../Field/InputClearButton'; @@ -110,6 +109,7 @@ import type {AdaptivePresentation} from '../hooks/useAdaptivePresentation'; import {SelectorBottomSheet} from '../Selector/SelectorBottomSheet'; import {useSelectorPresentation} from '../Selector/useSelectorPresentation'; import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; // Sentinel value for the select-all item in keyboard navigation const SELECT_ALL_VALUE = '__xds_select_all__'; @@ -807,7 +807,7 @@ export interface MultiSelectorProps< * - 'tooltip': message is exposed from the on-field status icon * @default 'attached' for input selectors; 'detached' for secondary and ghost selectors */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Width of the field. Numbers are treated as pixels, strings are used as-is diff --git a/packages/core/src/NumberInput/NumberInput.tsx b/packages/core/src/NumberInput/NumberInput.tsx index 81e208dc0b7bd..4c2b810df9e86 100644 --- a/packages/core/src/NumberInput/NumberInput.tsx +++ b/packages/core/src/NumberInput/NumberInput.tsx @@ -47,7 +47,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {Icon, renderIconSlot, type IconType} from '../Icon'; import {VisuallyHidden} from '../VisuallyHidden'; @@ -235,6 +234,7 @@ import {themeProps} from '../utils/themeProps'; import {useTranslator, useLocale} from '../i18n'; import {formatEditableNumber} from './numberParser'; import {parseNumberInput, resolveNumberInputCommit} from './numberInputCommit'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; interface NumberInputPropsBase extends Omit< FieldInputBaseProps, @@ -324,7 +324,7 @@ interface NumberInputPropsBase extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * The size of the input. * - 'sm': Compact size (28px height) diff --git a/packages/core/src/PowerSearch/PowerSearch.tsx b/packages/core/src/PowerSearch/PowerSearch.tsx index 2927f7021c502..986d508fc98aa 100644 --- a/packages/core/src/PowerSearch/PowerSearch.tsx +++ b/packages/core/src/PowerSearch/PowerSearch.tsx @@ -38,7 +38,7 @@ import {layerAnimations} from '../Layer/layerAnimations.stylex'; import {Icon} from '../Icon'; import type {IconType} from '../Icon'; import type {IconName} from '../Icon/globalIconRegistry'; -import type {InputStatus, FieldStatusVariant} from '../Field'; +import type {InputStatus} from '../Field'; import {usePopover} from '../Popover/usePopover'; import {layerViewportInset} from '../Layer/layerViewportInset.stylex'; import {useAnnounce} from '../hooks/useAnnounce'; @@ -77,6 +77,7 @@ import type { } from './types'; import {useMergedRefs} from '../hooks/useMergedRefs'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; // ============================================================================= // Icon mapping for typeahead entries // ============================================================================= @@ -413,7 +414,7 @@ export interface PowerSearchProps extends Omit< * - 'detached': message floats below as a separate element with spacing * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** Max width for dropdown menu. */ menuWidth?: number; /** Max display length for filter token values. @default 40 */ diff --git a/packages/core/src/Selector/Selector.tsx b/packages/core/src/Selector/Selector.tsx index 5e4528be5cb90..eb77b4e5648e1 100644 --- a/packages/core/src/Selector/Selector.tsx +++ b/packages/core/src/Selector/Selector.tsx @@ -42,7 +42,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputWrapperStyles, - type FieldStatusVariant, } from '../Field'; import {Divider} from '../Divider'; import {layerAnimations} from '../Layer/layerAnimations.stylex'; @@ -104,6 +103,7 @@ import type {AdaptivePresentation} from '../hooks/useAdaptivePresentation'; import {SelectorBottomSheet} from './SelectorBottomSheet'; import {useSelectorPresentation} from './useSelectorPresentation'; import {selectorPresentationStyles} from './selectorPresentation.stylex'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; const styles = stylex.create({ // Trigger container — the enhanced click target wrapping the combobox button and clear button as siblings @@ -726,7 +726,7 @@ interface SelectorPropsBase< * - 'tooltip': message is exposed from the on-field status icon * @default 'attached' for input selectors; 'detached' for secondary/ghost selectors */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Width of the field. Numbers are treated as pixels, strings are used as-is diff --git a/packages/core/src/TextArea/TextArea.tsx b/packages/core/src/TextArea/TextArea.tsx index f74186ac66a1f..767b56d248de0 100644 --- a/packages/core/src/TextArea/TextArea.tsx +++ b/packages/core/src/TextArea/TextArea.tsx @@ -41,7 +41,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {Icon, renderIconSlot, type IconType} from '../Icon'; import {Spinner} from '../Spinner'; @@ -66,6 +65,7 @@ import {useTranslator} from '../i18n'; import {useMergedRefs} from '../hooks/useMergedRefs'; import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; const COUNTER_WARNING_THRESHOLD = 0.8; const styles = stylex.create({ @@ -304,7 +304,7 @@ export interface TextAreaProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Width of the field. Numbers are treated as pixels, strings are used as-is * (e.g. `'100%'`). Sizes the whole field (label, control, and status) so they diff --git a/packages/core/src/TextInput/TextInput.tsx b/packages/core/src/TextInput/TextInput.tsx index e2897888e623f..c74803d88ff13 100644 --- a/packages/core/src/TextInput/TextInput.tsx +++ b/packages/core/src/TextInput/TextInput.tsx @@ -42,7 +42,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {renderIconSlot, type IconType} from '../Icon'; import {Spinner} from '../Spinner'; @@ -123,6 +122,7 @@ import {useTranslator} from '../i18n'; import {useMergedRefs} from '../hooks/useMergedRefs'; import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; export type TextInputType = 'text' | 'password' | 'email'; export interface TextInputProps extends Omit< @@ -214,7 +214,7 @@ export interface TextInputProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * The size of the input. * - 'sm': Compact size (18px height) diff --git a/packages/core/src/TimeInput/TimeInput.tsx b/packages/core/src/TimeInput/TimeInput.tsx index ae586664d258d..128877640abed 100644 --- a/packages/core/src/TimeInput/TimeInput.tsx +++ b/packages/core/src/TimeInput/TimeInput.tsx @@ -45,7 +45,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {Icon} from '../Icon'; import {Spinner} from '../Spinner'; @@ -94,6 +93,7 @@ import { type InputPresentation, } from '../utils/inputPresentation'; import {useDevWarning} from '../hooks/useDevWarning'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; const TOUCH_POINTER_QUERY = '(pointer: coarse)'; @@ -377,7 +377,7 @@ export interface TimeInputProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Width of the field. Numbers are treated as pixels, strings are used as-is diff --git a/packages/core/src/Tokenizer/Tokenizer.tsx b/packages/core/src/Tokenizer/Tokenizer.tsx index 8c914bb3b4878..eb0102c23b867 100644 --- a/packages/core/src/Tokenizer/Tokenizer.tsx +++ b/packages/core/src/Tokenizer/Tokenizer.tsx @@ -69,7 +69,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {Token} from '../Token'; import {Spinner} from '../Spinner'; @@ -92,6 +91,7 @@ import {themeProps} from '../utils/themeProps'; import {useTranslator} from '../i18n'; import type {TokenizerBottomSheetProps} from './TokenizerBottomSheet'; import {useResolvedRequired} from '../hooks/useResolvedRequired'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; // Re-export status types for convenience export type { @@ -193,7 +193,7 @@ export interface TokenizerProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Icon to display at the start of the input. * Accepts a ReactNode (e.g. ``) or an SVG icon component directly. diff --git a/packages/core/src/Typeahead/Typeahead.tsx b/packages/core/src/Typeahead/Typeahead.tsx index c8de37c11d971..42a3d37985bff 100644 --- a/packages/core/src/Typeahead/Typeahead.tsx +++ b/packages/core/src/Typeahead/Typeahead.tsx @@ -46,7 +46,6 @@ import { inputStatusBorderStyles, inputStatusHoverShadowStyles, inputStatusFocusWithinStyles, - type FieldStatusVariant, } from '../Field'; import {Token} from '../Token'; import {useTooltip} from '../Tooltip'; @@ -77,6 +76,7 @@ import {useTranslator} from '../i18n'; import {useMergedRefs} from '../hooks/useMergedRefs'; import {useBusyAnnouncement} from '../hooks/useBusyAnnouncement'; import {useResolvedRequired} from '../hooks/useResolvedRequired'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; export type { InputStatus as TypeaheadStatus, InputStatusType as TypeaheadStatusType, @@ -116,7 +116,7 @@ export interface TypeaheadProps extends Omit< * - 'tooltip': no message box; the status icon becomes a focusable info-tip button that reveals the message on hover, keyboard focus, or tap * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** * Icon to display at the start of the input. * Accepts a ReactNode (e.g. ``) or an SVG icon component directly. diff --git a/packages/core/src/hooks/useInputStatusIcon.tsx b/packages/core/src/hooks/useInputStatusIcon.tsx index ce4fddb8ad04a..5985f2f537560 100644 --- a/packages/core/src/hooks/useInputStatusIcon.tsx +++ b/packages/core/src/hooks/useInputStatusIcon.tsx @@ -36,13 +36,13 @@ import {useCallback, useEffect, useState} from 'react'; import type {ReactNode} from 'react'; import * as stylex from '@stylexjs/stylex'; import {Icon, type IconName, type IconSize} from '../Icon'; -import type {FieldStatusVariant} from '../FieldStatus/FieldStatus'; import type {InputStatus, InputStatusType} from '../Field/types'; import {useTooltip} from '../Tooltip'; import {useTranslator} from '../i18n'; import {radiusVars} from '../theme/tokens.stylex'; import {themeProps} from '../utils/themeProps'; import {focusOutlineStyles} from '../utils/focusOutline.stylex'; +import type {FieldStatusPlacement} from '../Field/fieldStatusPlacement'; /** * Maps each status type to its glyph. Shared so every input shows the same icon @@ -104,7 +104,7 @@ export interface UseInputStatusIconOptions { /** The input's status, or undefined when there is none. */ status?: InputStatus; /** How the status is presented relative to the input. */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusPlacement; /** Whether the input is inside an InputGroup (which owns status rendering). */ isInGroup?: boolean; /** Size of the on-field icon. @default 'md' */ From 9251bd62f4300c0729b6a795e610ce1586fee217 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 14:51:07 -0700 Subject: [PATCH 2/5] fix(FieldStatus): a direct variant="tooltip" resolves to a deprecated signature, and the Changeset says what it draws CodeBunny on #9196: @deprecated on the map entry never surfaced at , and FieldStatus does draw a message box for it. FieldStatus now has a deprecated overload for variant: 'tooltip' ahead of its general one, so an editor marks that call, while every other call, a FieldStatusVariant-typed value included, resolves to the general signature. A language-service test checks only the direct tooltip is marked and every case compiles. The Changeset, the map JSDoc and the component docs say it draws a plain message box with no tooltip. --- .changeset/fieldstatus-tooltip-deprecated.md | 2 +- .../FieldStatus.deprecation.test.ts | 85 +++++++++++++++++++ packages/core/src/FieldStatus/FieldStatus.tsx | 20 ++++- packages/core/src/FieldStatus/index.ts | 9 +- 4 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/FieldStatus/FieldStatus.deprecation.test.ts diff --git a/.changeset/fieldstatus-tooltip-deprecated.md b/.changeset/fieldstatus-tooltip-deprecated.md index 2142ecb990efd..5c437ee9c96c3 100644 --- a/.changeset/fieldstatus-tooltip-deprecated.md +++ b/.changeset/fieldstatus-tooltip-deprecated.md @@ -2,7 +2,7 @@ '@astryxdesign/core': patch --- -[feat] FieldStatus: the `tooltip` variant is deprecated and is removed in the next minor release. FieldStatus renders no message box for it, so a direct `` shows nothing. To show a status message from a field's on-field icon, set `statusVariant="tooltip"` on the field control instead; that stays supported. +[feat] FieldStatus: the `tooltip` variant is deprecated and is removed in the next minor release. FieldStatus has no tooltip presentation: a direct `` draws a plain message box with no tooltip, and your editor now marks it deprecated. To show a status message from a field's on-field icon, set `statusVariant="tooltip"` on the field control instead; that stays supported. Before: diff --git a/packages/core/src/FieldStatus/FieldStatus.deprecation.test.ts b/packages/core/src/FieldStatus/FieldStatus.deprecation.test.ts new file mode 100644 index 0000000000000..bd15a2fee0225 --- /dev/null +++ b/packages/core/src/FieldStatus/FieldStatus.deprecation.test.ts @@ -0,0 +1,85 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +/** + * @file FieldStatus.deprecation.test.ts + * @input The TypeScript language service and FieldStatus.tsx + * @output Verifies an editor marks a direct `variant="tooltip"` deprecated, + * and leaves the supported variants and Field-family placements unmarked + * @position Static deprecation evidence for `component:FieldStatus` FR7 + * (`spec:AST-017` FR29a: the warning lives in the declarations) + */ + +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import ts from 'typescript'; +import {describe, expect, it} from 'vitest'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CORE_ROOT = path.resolve(__dirname, '../..'); +const FIXTURE = path.join(__dirname, '__deprecation_fixture__.tsx'); + +const SOURCE = ` +import {FieldStatus} from './FieldStatus'; +import type {FieldStatusVariant} from './FieldStatus'; +import {TextInput} from '../TextInput/TextInput'; + +declare const someVariant: FieldStatusVariant; + +export const direct = ; +export const attached = ; +export const detached = ; +export const typed = ; +export const field = ( + {}} status={{type: 'error', message: 'm'}} statusVariant="tooltip" /> +); +`; + +function deprecatedLines(): number[] { + const configPath = path.join(CORE_ROOT, 'tsconfig.json'); + const config = ts.readConfigFile(configPath, ts.sys.readFile); + const parsed = ts.parseJsonConfigFileContent( + config.config, + ts.sys, + CORE_ROOT, + undefined, + configPath, + ); + const files = new Map([[FIXTURE, SOURCE]]); + const host: ts.LanguageServiceHost = { + getScriptFileNames: () => [FIXTURE], + getScriptVersion: () => '1', + getScriptSnapshot: name => { + const text = files.get(name) ?? ts.sys.readFile(name); + return text == null ? undefined : ts.ScriptSnapshot.fromString(text); + }, + getCurrentDirectory: () => CORE_ROOT, + getCompilationSettings: () => ({...parsed.options, noEmit: true}), + getDefaultLibFileName: options => ts.getDefaultLibFilePath(options), + fileExists: name => files.has(name) || ts.sys.fileExists(name), + readFile: name => files.get(name) ?? ts.sys.readFile(name), + readDirectory: ts.sys.readDirectory, + directoryExists: ts.sys.directoryExists, + getDirectories: ts.sys.getDirectories, + }; + const service = ts.createLanguageService(host); + const errors = service.getSemanticDiagnostics(FIXTURE); + expect( + errors.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')), + ).toEqual([]); + const source = service.getProgram()!.getSourceFile(FIXTURE)!; + return service + .getSuggestionDiagnostics(FIXTURE) + .filter(d => d.reportsDeprecated) + .map(d => source.getLineAndCharacterOfPosition(d.start ?? 0).line); +} + +function lineOf(name: string): number { + return SOURCE.split('\n').findIndex(line => line.includes(`const ${name} =`)); +} + +describe('FieldStatus tooltip deprecation (component:FieldStatus FR7)', () => { + it('marks a direct variant="tooltip" deprecated and nothing else', () => { + const lines = deprecatedLines(); + expect(lines).toEqual([lineOf('direct')]); + }, 120_000); +}); diff --git a/packages/core/src/FieldStatus/FieldStatus.tsx b/packages/core/src/FieldStatus/FieldStatus.tsx index 3307dc4734173..36209c9789c25 100644 --- a/packages/core/src/FieldStatus/FieldStatus.tsx +++ b/packages/core/src/FieldStatus/FieldStatus.tsx @@ -143,6 +143,18 @@ export interface FieldStatusProps extends BaseProps { variant?: FieldStatusVariant; } +// A direct `tooltip` resolves to the deprecated signature first; every other +// call, a `FieldStatusVariant`-typed value included, resolves to the second, +// which `ComponentProps` reads. +/** + * @deprecated FieldStatus has no `tooltip` presentation + * (`component:FieldStatus` FR7): it draws a plain message box with no tooltip. + * Set `statusVariant="tooltip"` on the field control instead, which shows the + * message from its on-field status icon. Removed in the next minor release. + */ +export function FieldStatus( + props: FieldStatusProps & {variant: 'tooltip'}, +): React.JSX.Element; /** * A status message component for form fields. * @@ -151,9 +163,10 @@ export interface FieldStatusProps extends BaseProps { * 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. + * icon here to avoid a duplicate. FieldStatus has no `tooltip` presentation: + * a field control given `statusVariant="tooltip"` surfaces the status through + * a tooltip on its on-field icon and renders no FieldStatus for it + * (`component:FieldStatus` FR7). * * Screen-reader announcements go through the persistent `useAnnounce` live * regions (assertive for errors, polite otherwise) rather than `role`/ @@ -176,6 +189,7 @@ export interface FieldStatusProps extends BaseProps { * /> * ``` */ +export function FieldStatus(props: FieldStatusProps): React.JSX.Element; export function FieldStatus({ ref, type, diff --git a/packages/core/src/FieldStatus/index.ts b/packages/core/src/FieldStatus/index.ts index fdbbb48af2400..302aac21318d1 100644 --- a/packages/core/src/FieldStatus/index.ts +++ b/packages/core/src/FieldStatus/index.ts @@ -26,10 +26,11 @@ export interface FieldStatusVariantMap { attached: true; detached: true; /** - * @deprecated FieldStatus does not render a `tooltip` status: it shows no - * message box (`component:FieldStatus` FR7). Set `statusVariant="tooltip"` - * on the field control (TextInput, Selector, and the rest), which shows the - * message from its on-field status icon. Removed in the next minor release. + * @deprecated FieldStatus has no `tooltip` presentation + * (`component:FieldStatus` FR7): given it directly, it draws a plain message + * box with no tooltip. Set `statusVariant="tooltip"` on the field control + * (TextInput, Selector, and the rest), which shows the message from its + * on-field status icon. Removed in the next minor release. */ tooltip: true; } From 4d4518259b52f7003d8613bbcc8e5aeeae087489 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 20:02:22 -0700 Subject: [PATCH 3/5] fix(RichTextEditor, MobilePowerSearch): statusVariant names 'tooltip' itself; the migration example compiles CodeBunny on #9196: RichTextEditor (and Lab's MobilePowerSearch) typed statusVariant as FieldStatusVariant alone, so 'tooltip' reached them only through the deprecated map entry. They now take FieldStatusVariant | 'tooltip', like core's field controls, and TransferList's doc names the three placements. The Changeset's TextInput example passes value and onChange, which TextInput requires. --- .changeset/fieldstatus-tooltip-deprecated.md | 2 ++ packages/lab/src/MobilePowerSearch/MobilePowerSearch.tsx | 2 +- packages/lab/src/TransferList/TransferList.doc.mjs | 2 +- packages/richtext/src/RichTextEditor.tsx | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.changeset/fieldstatus-tooltip-deprecated.md b/.changeset/fieldstatus-tooltip-deprecated.md index 5c437ee9c96c3..ca5dcd9961832 100644 --- a/.changeset/fieldstatus-tooltip-deprecated.md +++ b/.changeset/fieldstatus-tooltip-deprecated.md @@ -15,6 +15,8 @@ After: ```tsx diff --git a/packages/lab/src/MobilePowerSearch/MobilePowerSearch.tsx b/packages/lab/src/MobilePowerSearch/MobilePowerSearch.tsx index 6e6d154cecb69..9b11636504262 100644 --- a/packages/lab/src/MobilePowerSearch/MobilePowerSearch.tsx +++ b/packages/lab/src/MobilePowerSearch/MobilePowerSearch.tsx @@ -207,7 +207,7 @@ export interface MobilePowerSearchProps extends Omit< /** Validation status. */ status?: InputStatus; /** How the status message is placed. @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusVariant | 'tooltip'; /** Max display length for token values. @default 40 */ maxTokenLength?: number; /** Max suggestions in string and entity value typeaheads. @default 10 */ diff --git a/packages/lab/src/TransferList/TransferList.doc.mjs b/packages/lab/src/TransferList/TransferList.doc.mjs index fb6373d330019..b536351c9b57a 100644 --- a/packages/lab/src/TransferList/TransferList.doc.mjs +++ b/packages/lab/src/TransferList/TransferList.doc.mjs @@ -224,7 +224,7 @@ export const docs = { }, { name: 'statusVariant', - type: 'FieldStatusVariant', + type: "'attached' | 'detached' | 'tooltip'", description: 'Placement treatment for the status message.', }, { diff --git a/packages/richtext/src/RichTextEditor.tsx b/packages/richtext/src/RichTextEditor.tsx index d64beeffa5a5a..2ce42ba809011 100644 --- a/packages/richtext/src/RichTextEditor.tsx +++ b/packages/richtext/src/RichTextEditor.tsx @@ -370,7 +370,7 @@ export interface RichTextEditorProps extends Omit< * info-tip button that reveals the message * @default 'attached' */ - statusVariant?: FieldStatusVariant; + statusVariant?: FieldStatusVariant | 'tooltip'; /** * Width of the field. Numbers are treated as pixels, strings are used as-is * (e.g. `'100%'`). From c63d90bcd8d581f9a1b848d17c5fb19d8777ea9d Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 21:03:49 -0700 Subject: [PATCH 4/5] chore(changeset): a richtext Changeset for RichTextEditor's statusVariant type CodeBunny on #9196: the RichTextEditor type change touches the published @astryxdesign/richtext surface, so it gets its own Changeset (spec:AST-017 FR7). --- .changeset/richtext-status-placement-tooltip.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/richtext-status-placement-tooltip.md diff --git a/.changeset/richtext-status-placement-tooltip.md b/.changeset/richtext-status-placement-tooltip.md new file mode 100644 index 0000000000000..d3d757a647876 --- /dev/null +++ b/.changeset/richtext-status-placement-tooltip.md @@ -0,0 +1,7 @@ +--- +'@astryxdesign/richtext': patch +--- + +[chore] RichTextEditor: `statusVariant` names `'tooltip'` in its own type, so it keeps accepting `statusVariant="tooltip"` after FieldStatus drops its deprecated `tooltip` variant in the next minor release. No behavior change. + +@cixzhang From 8315c1002ad00a73949921695b54a652a2b56e97 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 22:00:40 -0700 Subject: [PATCH 5/5] docs(FieldStatus): the full component docs sit on the implementation, where Storybook reads them CodeBunny on #9196: with the docs on an overload, Storybook's docgen, which reads the implementation, showed FieldStatus with no description. The full JSDoc is back on the implementation; the general overload keeps a short summary and example for editor hovers. --- packages/core/src/FieldStatus/FieldStatus.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/FieldStatus/FieldStatus.tsx b/packages/core/src/FieldStatus/FieldStatus.tsx index 36209c9789c25..304a452644e64 100644 --- a/packages/core/src/FieldStatus/FieldStatus.tsx +++ b/packages/core/src/FieldStatus/FieldStatus.tsx @@ -155,6 +155,16 @@ export interface FieldStatusProps extends BaseProps { export function FieldStatus( props: FieldStatusProps & {variant: 'tooltip'}, ): React.JSX.Element; +/** + * A status message component for form fields: an `attached` message under a + * field, or a `detached` one with its own status icon. + * + * @example + * ``` + * + * ``` + */ +export function FieldStatus(props: FieldStatusProps): React.JSX.Element; /** * A status message component for form fields. * @@ -189,7 +199,6 @@ export function FieldStatus( * /> * ``` */ -export function FieldStatus(props: FieldStatusProps): React.JSX.Element; export function FieldStatus({ ref, type,