Skip to content

feat(TextArea): enable errorplacement prop ISSUE-2270#2689

Open
KrotovPetr wants to merge 6 commits into
mainfrom
ISSUE-2270.text-area2
Open

feat(TextArea): enable errorplacement prop ISSUE-2270#2689
KrotovPetr wants to merge 6 commits into
mainfrom
ISSUE-2270.text-area2

Conversation

@KrotovPetr

@KrotovPetr KrotovPetr commented May 22, 2026

Copy link
Copy Markdown
Contributor

#2270

Summary by Sourcery

Add support for placing TextArea validation errors inside the control via an icon tooltip and update styles, tests, and docs accordingly.

New Features:

  • Introduce the errorPlacement prop for TextArea to render validation errors either outside or inside the control via an error icon tooltip.

Documentation:

  • Document the new TextArea errorPlacement prop, its default behavior, and add examples for inside error placement.

Tests:

  • Extend TextArea unit and visual tests to cover the new errorPlacement options, including inside placement with and without a clear button.

@sourcery-ai

sourcery-ai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements an errorPlacement prop for TextArea that allows rendering validation errors either outside (existing behavior) or inside the control as an error icon with tooltip, updates layout/styles to coexist with the clear button and scrollbars, and extends tests, stories, and docs accordingly.

File-Level Changes

Change Details Files
Add inside error placement option rendering an inline error icon with tooltip while preserving default outside error message behavior.
  • Extend TextArea props and errorPropsMapper usage to accept an errorPlacement prop defaulting to 'outside'.
  • Derive isErrorMsgVisible and isErrorIconVisible flags based on validationState, errorMessage, and errorPlacement.
  • Render a Popover-wrapped TriangleExclamation Icon inside the control when errorPlacement is 'inside', wiring QA data attributes for testing.
  • Pass a 'has-error-icon' modifier to the BEM class builder to drive conditional styling.
  • Update README and Storybook showcase to document and demonstrate inside error placement and add it to the public API table.
src/components/controls/TextArea/TextArea.tsx
src/components/controls/TextArea/README.md
src/components/controls/TextArea/__stories__/TextAreaShowcase.tsx
Adjust TextArea layout and spacing to support an inline error icon alongside the clear button and scrollbars across sizes.
  • Introduce error-icon and error-icon-wrap BEM elements with absolute positioning controlled via CSS custom properties.
  • For each size variant (s, m, l, xl), share padding rules between has-clear and has-error-icon and add combined rules when both are present to increase right padding.
  • Define per-size CSS variables for error icon inline/block offsets to align the icon visually in the control.
  • Handle scrollbar presence by shifting the error icon wrapper similarly to the clear button and add combined has-clear+has-error-icon rules to avoid overlap when scrollbars exist.
src/components/controls/TextArea/TextArea.scss
Expand unit and visual tests and test cases to cover inside error placement behavior.
  • Add errorPlacementCases to the shared test cases and pass them into the visual tests matrix so both inside and outside placements are captured.
  • Add visual smoke tests ensuring the inside error tooltip appears on hover and renders correctly with and without the clear button.
  • Add unit tests verifying that an error icon is rendered only when errorPlacement is 'inside' with non-empty error/errorMessage and validationState='invalid', and that no icon is shown for empty error texts.
src/components/controls/TextArea/__tests__/TextArea.visual.test.tsx
src/components/controls/TextArea/__tests__/TextArea.test.tsx
src/components/controls/TextArea/__tests__/cases.tsx

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gravity-ui

gravity-ui Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Preview is ready.

@gravity-ui

gravity-ui Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

🎭 Component Tests Report is ready.

@KrotovPetr
KrotovPetr marked this pull request as ready for review May 22, 2026 11:45
@KrotovPetr
KrotovPetr requested a review from korvin89 as a code owner May 22, 2026 11:45

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new inside error icon/tooltip flow doesn’t appear to expose the error text to assistive technologies (no aria-describedby from the textarea and no ARIA attributes on the icon/tooltip), so consider wiring the tooltip content into the existing accessibility path used for outside errors to keep the control screen‑reader friendly.
  • The error icon trigger is currently a plain <span> inside a Popover; consider giving it a semantic role (e.g., role="button" and tabIndex=0, or making it purely decorative and reflecting the error on the textarea itself) so keyboard users can reliably access the tooltip or aren’t required to interact with it.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new inside error icon/tooltip flow doesn’t appear to expose the error text to assistive technologies (no `aria-describedby` from the textarea and no ARIA attributes on the icon/tooltip), so consider wiring the tooltip content into the existing accessibility path used for outside errors to keep the control screen‑reader friendly.
- The error icon trigger is currently a plain `<span>` inside a `Popover`; consider giving it a semantic role (e.g., `role="button"` and `tabIndex=0`, or making it purely decorative and reflecting the error on the textarea itself) so keyboard users can reliably access the tooltip or aren’t required to interact with it.

## Individual Comments

### Comment 1
<location path="src/components/controls/TextArea/__tests__/TextArea.visual.test.tsx" line_range="124" />
<code_context>
     });
+
+    test(
+        'smoke inside error placement tooltip',
+        {tag: ['@smoke']},
+        async ({mount, page, expectScreenshot}) => {
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the smoke tooltip test by asserting the tooltip content text, not just visibility

Right now the test only checks that `.g-popup` becomes visible after hovering the error icon, so it won’t catch cases where the tooltip shows the wrong text. Please also assert that the popup contains `Test error message` (or at least the `errorMessage` prop value) to better guard against regressions in tooltip content.

Suggested implementation:

```typescript
        'smoke inside error placement tooltip',
        {tag: ['@smoke']},
        async ({mount, page, expectScreenshot}) => {
            const props: TextAreaProps = {
                ...defaultProps,
                value: 'Text',
                validationState: 'invalid',
                errorMessage: 'Test error message',
                errorPlacement: 'inside',
            };

            const root = await mount(
                <div style={{width: 250}}>

```

```typescript
        const popup = page.locator('.g-popup');
        await expect(popup).toBeVisible();
        await expect(popup).toContainText('Test error message');

```

If the test currently uses a different selector or assertion for the tooltip (for example, a more specific locator than `.g-popup`, or `toHaveText` instead of `toBeVisible`), adapt the `popup` locator and the added `toContainText('Test error message')` call accordingly.  
If `errorMessage` is changed in this test in the future, consider asserting `props.errorMessage` instead of the literal string to keep the test in sync:
```ts
await expect(popup).toContainText(props.errorMessage);
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

});

test(
'smoke inside error placement tooltip',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Strengthen the smoke tooltip test by asserting the tooltip content text, not just visibility

Right now the test only checks that .g-popup becomes visible after hovering the error icon, so it won’t catch cases where the tooltip shows the wrong text. Please also assert that the popup contains Test error message (or at least the errorMessage prop value) to better guard against regressions in tooltip content.

Suggested implementation:

        'smoke inside error placement tooltip',
        {tag: ['@smoke']},
        async ({mount, page, expectScreenshot}) => {
            const props: TextAreaProps = {
                ...defaultProps,
                value: 'Text',
                validationState: 'invalid',
                errorMessage: 'Test error message',
                errorPlacement: 'inside',
            };

            const root = await mount(
                <div style={{width: 250}}>
        const popup = page.locator('.g-popup');
        await expect(popup).toBeVisible();
        await expect(popup).toContainText('Test error message');

If the test currently uses a different selector or assertion for the tooltip (for example, a more specific locator than .g-popup, or toHaveText instead of toBeVisible), adapt the popup locator and the added toContainText('Test error message') call accordingly.
If errorMessage is changed in this test in the future, consider asserting props.errorMessage instead of the literal string to keep the test in sync:

await expect(popup).toContainText(props.errorMessage);

@korvin89 korvin89 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 follow-up notes on errorPlacement="inside" from the review (CI/tests are green and rendering is correct — this is polish). I'd fix #1#2 before merge; #3#5 are optional. All a11y findings are inherited from TextInput and split out into a separate follow-up.

Comment on lines +236 to 244
&_has-clear#{$block}_has-error-icon {
#{$block}__clear {
inset-inline-end: calc(var(--_--clear-offset) + 20px);
}

&#{$block}_has-scrollbar #{$block}__clear {
inset-inline-end: calc(var(--g-scrollbar-width) + 20px);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] On l/xl the error icon overlaps the clear button (~4px hit-box overlap).

Here the clear button is pushed to a hardcoded clear-offset + 20px (≈22px). But the icon's far edge is error-icon-offset-inline + 16px: for s/m that's 6+16=22px (flush, ok), while for l/xl it's 10+16=26px, i.e. it slides ~4px under the clear button. The +20px only matches the s/m offset. This is invisible in review because the showcase and both visual tests use size m.

I'd tie the offset to error-icon-offset-inline so the clear button lands exactly at the icon's far edge for every size (s/m unchanged: 6+16=22; l/xl: 10+16=26):

Suggested change
&_has-clear#{$block}_has-error-icon {
#{$block}__clear {
inset-inline-end: calc(var(--_--clear-offset) + 20px);
}
&#{$block}_has-scrollbar #{$block}__clear {
inset-inline-end: calc(var(--g-scrollbar-width) + 20px);
}
}
&_has-clear#{$block}_has-error-icon {
#{$block}__clear {
inset-inline-end: calc(var(--_--clear-offset) + var(--_--error-icon-offset-inline) + 14px);
}
&#{$block}_has-scrollbar #{$block}__clear {
inset-inline-end: calc(var(--g-scrollbar-width) + var(--_--error-icon-offset-inline) + 14px);
}
}

Would also be good to add a visual scenario for l/xl + hasClear + errorPlacement="inside" so this case is screenshot-covered.

Comment on lines +95 to +99
&__error-icon-wrap {
position: absolute;
inset-inline-end: var(--_--error-icon-offset-inline);
inset-block-start: var(--_--error-icon-offset-block);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit / robustness] Equal-specificity position conflict relies on stylesheet source order.

className={b('error-icon-wrap')} (TextArea.tsx:188) lands on the legacy Popover's root <div>, so the same element carries both .g-popover-legacy { position: relative } and .g-text-area__error-icon-wrap { position: absolute } — both (0,1,0) selectors. absolute only wins because TextArea's styles are emitted after the Popover's; a change in import/bundler order could silently break the positioning. TextInput avoids this by not passing className to its Popover.

Simplest fix — raise the rule's specificity so it wins deterministically:

Suggested change
&__error-icon-wrap {
position: absolute;
inset-inline-end: var(--_--error-icon-offset-inline);
inset-block-start: var(--_--error-icon-offset-block);
}
&__error-icon-wrap#{$block}__error-icon-wrap {
position: absolute;
inset-inline-end: var(--_--error-icon-offset-inline);
inset-block-start: var(--_--error-icon-offset-block);
}

A cleaner alternative — don't put the class on the Popover at all; wrap it in a dedicated <span className={b('error-icon-wrap')}> and leave the Popover with its position: relative.

Comment on lines 132 to 134
&#{$block}_has-clear #{$block}__control,
&#{$block}_has-error-icon #{$block}__control {
padding-inline-end: 26px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] view="clear" + errorPlacement="inside" keeps the control's 26/36px end padding.

These _has-error-icon __control { padding-inline-end } rules have specificity (0,3,0), while the clear-view reset .g-text-area_view_clear .g-text-area__control { padding-inline: 0 } is (0,2,0), so it loses: in clear view the text doesn't sit flush as expected.

I'd scope the per-size padding under view_normal so it doesn't fight the clear-view reset, e.g.:

&#{$block}_view_normal#{$block}_has-clear #{$block}__control,
&#{$block}_view_normal#{$block}_has-error-icon #{$block}__control {
    padding-inline-end: 26px;
}

(same for 46px, and for l/xl). Alternatively, zero out --_--error-icon-offset-inline and the padding inside the &_view_clear block. And add a clear+inside screenshot. Affects all sizes: s (≈113), m (132), l (152), xl (172).


&_has-clear#{$block}_has-error-icon {
#{$block}__clear {
inset-inline-end: calc(var(--_--clear-offset) + 20px);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] On l/xl the clear button loses its +1px horizontal nudge when the icon appears.

The base clear button on l/xl sits at calc(var(--_--clear-offset) + 1px), but this override (and its scrollbar variant on line 242) uses + 20px without the +1, causing a 1px horizontal jitter when the icon shows up. Vertical is fine (inset-block-start is untouched).

If the offset-inline-based formula from the overlap comment is adopted, positioning becomes consistent across sizes and this point essentially goes away. Otherwise, carry the per-size +1px via a dedicated variable. Low priority.

&__error-icon-wrap {
position: absolute;
inset-inline-end: var(--_--error-icon-offset-inline);
inset-block-start: var(--_--error-icon-offset-block);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The icon's vertical center is ~1px off from the clear button's center.

The clear button is centered exactly on the control's vertical center at every size, but because of the hardcoded --_--error-icon-offset-block (5/7/11/15px) the icon's center is 1px lower on m/l/xl and 1px higher on s. Only noticeable when both the clear button and the inside icon are shown.

Cleaner to center generically instead of the per-size hardcode (then the --_--error-icon-offset-block variables can be dropped):

&__error-icon-wrap {
    /* ... */
    inset-block-start: 50%;
    transform: translateY(-50%);
}

This mirrors the auto-centering TextInput already uses (flex). NB: a blanket -1px on the offsets won't work — s would get worse.

@KrotovPetr

Copy link
Copy Markdown
Contributor Author
telegram-cloud-photo-size-2-5274043269547498574-y telegram-cloud-photo-size-2-5274043269547498575-y

@KrotovPetr
KrotovPetr requested a review from korvin89 June 8, 2026 11:37
@KrotovPetr
KrotovPetr force-pushed the ISSUE-2270.text-area2 branch from 5fc89b1 to d66a7c8 Compare June 9, 2026 10:17

@korvin89 korvin89 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review — align the inside error icon (errorPlacement="inside") with TextInput.

After the fix the measurements match on both axes for every size (centerY 12/14/18/22, right-gap 5/5/9/13).

&__error-icon-wrap {
position: absolute;
display: inline-flex;
inset-inline-end: 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated suggestion

The wrap is pinned to the outer edge (outside the 1px border), so the icon sits 1px closer to the right edge than in TextInput. Offsetting by the border width aligns them pixel-perfectly.

Suggested change
inset-inline-end: 0;
inset-inline-end: var(--g-text-area-border-width, var(--_--border-width));

padding-inline-end: 42px;
}

--_--error-icon-padding-block: 5px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated suggestion

padding-block is 1px short of true centering, so the glyph sits 1px above center (size s). +1px matches the glyph center to the field center, like TextInput.

Suggested change
--_--error-icon-padding-block: 5px;
--_--error-icon-padding-block: 6px;

padding-inline-end: 48px;
}

--_--error-icon-padding-block: 5px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated suggestion

Same as size s: the error icon's vertical centering is 1px short (size m).

Suggested change
--_--error-icon-padding-block: 5px;
--_--error-icon-padding-block: 6px;

padding-inline-end: 62px;
}

--_--error-icon-padding-block: 9px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated suggestion

The error icon's vertical centering is 1px short (size l).

Suggested change
--_--error-icon-padding-block: 9px;
--_--error-icon-padding-block: 10px;

padding-inline-end: 66px;
}

--_--error-icon-padding-block: 13px;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated suggestion

The error icon's vertical centering is 1px short (size xl).

Suggested change
--_--error-icon-padding-block: 13px;
--_--error-icon-padding-block: 14px;

}

#{$block}__error-icon-wrap {
inset-inline-end: var(--g-scrollbar-width);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated suggestion

With a scrollbar the icon must also be offset by the border width so the right-side gap matches TextInput.

Suggested change
inset-inline-end: var(--g-scrollbar-width);
inset-inline-end: calc(
var(--g-scrollbar-width) + var(--g-text-area-border-width, var(--_--border-width))
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants