From 5d1c1e2dcc7b2112c1bd4a85d943a05c154987f0 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 23 Jul 2026 22:00:31 -0500 Subject: [PATCH] fix(TextArea): fix empty S2 TextArea height jumping when overlays are open (#9933) * fix(TextArea): fix empty S2 TextArea height jumping when overlays are open * adjust fieldGroup to use `alignItems: 'start'` * offset the hidden baseline-anchor ::before down to the textarea's first line * format * add minHeight to fix prefix/validation icon alignment * only opt out textarea from baseline align * make textarea fix chrome-only --- packages/@react-spectrum/s2/src/TextField.tsx | 24 ++++++++++++++++--- .../s2/stories/TextField.stories.tsx | 20 ++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/@react-spectrum/s2/src/TextField.tsx b/packages/@react-spectrum/s2/src/TextField.tsx index d0d1064bdff..e536bbd06c9 100644 --- a/packages/@react-spectrum/s2/src/TextField.tsx +++ b/packages/@react-spectrum/s2/src/TextField.tsx @@ -47,6 +47,7 @@ import { } from '@react-types/shared'; import {FormContext, useFormProps} from './Form'; import {InputContext, InputProps} from 'react-aria-components/Input'; +import {isChrome} from 'react-aria/private/utils/platform'; import {mergeRefs} from 'react-aria/mergeRefs'; import {StyleString} from '../style/types'; import {TextContext} from './Content'; @@ -123,8 +124,18 @@ export const TextArea = forwardRef(function TextArea( ref={ref} fieldGroupCss={style({ alignItems: 'baseline', - height: 'auto' - })}> + height: 'auto', + paddingTop: { + // The textarea opts out of baseline alignment (see TextAreaInput) because Chrome + // computes an unstable baseline for empty textareas, causing the field height to + // jump when an overlay opens. Instead, we pad the group's hidden ::before baseline + // anchor down so its baseline lands on the textarea's first line of text, keeping + // the prefix, error icon, and side label aligned as if the textarea participated. + isChrome: { + '::before': '[calc((var(--field-height) - 1lh) / 2)]' + } + } + })({isChrome: isChrome()})}> ); @@ -270,13 +281,20 @@ function TextAreaInput() { fontSize: 'inherit', fontWeight: 'inherit', lineHeight: 'inherit', + // Don't baseline align with the FieldGroup - Chrome's baseline for an empty textarea + // is unstable (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/textarea#baseline_inconsistency). + // paddingY centers the first line within --field-height, matching the padded + // ::before baseline anchor on the FieldGroup. + alignSelf: { + isChrome: 'start' + }, flexGrow: 1, minWidth: 0, outlineStyle: 'none', borderStyle: 'none', resize: 'none', overflowX: 'hidden' - })} + })({isChrome: isChrome()})} /> ); } diff --git a/packages/@react-spectrum/s2/stories/TextField.stories.tsx b/packages/@react-spectrum/s2/stories/TextField.stories.tsx index 7627b43425e..2c4c6ecabb7 100644 --- a/packages/@react-spectrum/s2/stories/TextField.stories.tsx +++ b/packages/@react-spectrum/s2/stories/TextField.stories.tsx @@ -110,6 +110,26 @@ export const TextAreaExample: StoryTextArea = { } }; +export const TextAreaContextualHelpExample: StoryTextArea = { + render: args => ( +