feat: [DSR] Add React TextArea component#1036
Conversation
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Web Textarea uses visible border instead of transparent
- Replaced the non-error border class with border-transparent to match borderless design and Input behavior.
- ✅ Fixed: Textarea metrics constant duplicates Input's identical constant
- Removed duplicated RN Textarea metrics object and re-exported the Input metrics constant instead.
Or push these changes by commenting:
@cursor push 33e5ca596b
Preview (33e5ca596b)
diff --git a/packages/design-system-react-native/src/components/Textarea/Textarea.constants.ts b/packages/design-system-react-native/src/components/Textarea/Textarea.constants.ts
--- a/packages/design-system-react-native/src/components/Textarea/Textarea.constants.ts
+++ b/packages/design-system-react-native/src/components/Textarea/Textarea.constants.ts
@@ -1,73 +1,11 @@
-import { typography } from '@metamask/design-tokens';
+import { MAP_TEXT_VARIANT_INPUT_METRICS } from '../Input/Input.constants';
-import { TextVariant } from '../../types';
-
/**
* Typographic metrics for Textarea: same tokens as `text-*` utilities but **without** `lineHeight`.
* React Native `TextInput` with multiline aligns text more predictably when line height is not set
* from the design-system paragraph specs.
*/
-export const MAP_TEXT_VARIANT_TEXTAREA_METRICS: Record<
- TextVariant,
- { fontSize: number; letterSpacing: number }
-> = {
- [TextVariant.DisplayLg]: {
- fontSize: typography.sDisplayLG.fontSize,
- letterSpacing: typography.sDisplayLG.letterSpacing,
- },
- [TextVariant.DisplayMd]: {
- fontSize: typography.sDisplayMD.fontSize,
- letterSpacing: typography.sDisplayMD.letterSpacing,
- },
- [TextVariant.HeadingLg]: {
- fontSize: typography.sHeadingLG.fontSize,
- letterSpacing: typography.sHeadingLG.letterSpacing,
- },
- [TextVariant.HeadingMd]: {
- fontSize: typography.sHeadingMD.fontSize,
- letterSpacing: typography.sHeadingMD.letterSpacing,
- },
- [TextVariant.HeadingSm]: {
- fontSize: typography.sHeadingSM.fontSize,
- letterSpacing: typography.sHeadingSM.letterSpacing,
- },
- [TextVariant.BodyLg]: {
- fontSize: typography.sBodyLGMedium.fontSize,
- letterSpacing: typography.sBodyLGMedium.letterSpacing,
- },
- [TextVariant.BodyMd]: {
- fontSize: typography.sBodyMD.fontSize,
- letterSpacing: typography.sBodyMD.letterSpacing,
- },
- [TextVariant.BodySm]: {
- fontSize: typography.sBodySM.fontSize,
- letterSpacing: typography.sBodySM.letterSpacing,
- },
- [TextVariant.BodyXs]: {
- fontSize: typography.sBodyXS.fontSize,
- letterSpacing: typography.sBodyXS.letterSpacing,
- },
- [TextVariant.PageHeading]: {
- fontSize: typography.sPageHeading.fontSize,
- letterSpacing: typography.sPageHeading.letterSpacing,
- },
- [TextVariant.SectionHeading]: {
- fontSize: typography.sSectionHeading.fontSize,
- letterSpacing: typography.sSectionHeading.letterSpacing,
- },
- [TextVariant.ButtonLabelMd]: {
- fontSize: typography.sButtonLabelMd.fontSize,
- letterSpacing: typography.sButtonLabelMd.letterSpacing,
- },
- [TextVariant.ButtonLabelLg]: {
- fontSize: typography.sButtonLabelLg.fontSize,
- letterSpacing: typography.sButtonLabelLg.letterSpacing,
- },
- [TextVariant.AmountDisplayLg]: {
- fontSize: typography.sAmountDisplayLg.fontSize,
- letterSpacing: typography.sAmountDisplayLg.letterSpacing,
- },
-};
+export const MAP_TEXT_VARIANT_TEXTAREA_METRICS = MAP_TEXT_VARIANT_INPUT_METRICS;
/**
* Default number of lines displayed in the Textarea.
diff --git a/packages/design-system-react/src/components/Textarea/Textarea.tsx b/packages/design-system-react/src/components/Textarea/Textarea.tsx
--- a/packages/design-system-react/src/components/Textarea/Textarea.tsx
+++ b/packages/design-system-react/src/components/Textarea/Textarea.tsx
@@ -30,7 +30,7 @@
'placeholder:text-alternative',
isError
? 'border-error-default focus:border-error-default'
- : 'border-default focus:border-primary-default',
+ : 'border-transparent focus:border-primary-default',
CLASSMAP_TEXTAREA_RESIZE[resize],
CLASSMAP_TEXT_VARIANT_FONTSTYLE[textVariant],
CLASSMAP_TEXT_VARIANT_FONTWEIGHT[textVariant],You can send follow-ups to this agent here.
1d7f7e0 to
e907234
Compare
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Storybook argTypes includes forbidden
classNamecontrol- Removed the
classNameentry from TextArea.stories.tsx argTypes per project rules.
- Removed the
Or push these changes by commenting:
@cursor push 67e9b2eb28
Preview (67e9b2eb28)
diff --git a/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
--- a/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
+++ b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
@@ -62,10 +62,6 @@
rows: {
control: 'number',
},
- className: {
- control: 'text',
- description: 'Additional CSS classes merged with the component defaults',
- },
},
};You can send follow-ups to the cloud agent here.
226df62 to
1fbf352
Compare
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Default story uses named wrapper hiding component in View Code
- Replaced the Default story’s wrapper with inline useState in render so View Code shows TextArea.
Or push these changes by commenting:
@cursor push c5f346b89c
Preview (c5f346b89c)
diff --git a/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
--- a/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
+++ b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
@@ -78,7 +78,19 @@
value: '',
placeholder: 'Sample placeholder',
},
- render: (args) => <ControlledTextArea {...args} />,
+ render: (args) => {
+ const [value, setValue] = useState(args.value ?? '');
+ useEffect(() => {
+ setValue(args.value ?? '');
+ }, [args.value]);
+ return (
+ <TextArea
+ {...args}
+ value={value}
+ onChange={(event) => setValue(event.target.value)}
+ />
+ );
+ },
};
export const TextVariantStory: Story = {You can send follow-ups to the cloud agent here.
1fbf352 to
b67e92c
Compare
📖 Storybook Preview |
b67e92c to
9592065
Compare
📖 Storybook Preview |
9592065 to
e64f2bc
Compare
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Story wrapper silently drops onChange callback
- Updated ControlledTextArea to call props.onChange after setValue so Storybook args receive change events.
Or push these changes by commenting:
@cursor push a3c3f60e24
Preview (a3c3f60e24)
diff --git a/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
--- a/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
+++ b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
@@ -16,7 +16,10 @@
<TextArea
{...props}
value={value}
- onChange={(event) => setValue(event.target.value)}
+ onChange={(event) => {
+ setValue(event.target.value);
+ props.onChange?.(event);
+ }}
/>
);
}You can send follow-ups to the cloud agent here.
e64f2bc to
36b01ee
Compare
📖 Storybook Preview |
36b01ee to
d78c556
Compare
📖 Storybook Preview |
d78c556 to
7f32e89
Compare
📖 Storybook Preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Rest parameter uses
...restinstead of...props- Renamed the TextArea component’s spread parameter from ...rest to ...props and updated its spread usage on the root element to match.
Or push these changes by commenting:
@cursor push 955c5e84e7
Preview (955c5e84e7)
diff --git a/packages/design-system-react-native/src/components/TextArea/TextArea.tsx b/packages/design-system-react-native/src/components/TextArea/TextArea.tsx
--- a/packages/design-system-react-native/src/components/TextArea/TextArea.tsx
+++ b/packages/design-system-react-native/src/components/TextArea/TextArea.tsx
@@ -22,7 +22,6 @@
inputRef,
isDisabled = false,
isError = false,
- textVariant,
inputElement,
style,
twClassName,
@@ -97,7 +96,6 @@
onChangeText={onChangeText}
placeholder={placeholder}
isReadOnly={isReadOnly}
- textVariant={textVariant}
isDisabled={isDisabled}
autoFocus={autoFocus}
onBlur={onBlurHandler}
diff --git a/packages/design-system-react-native/src/components/TextArea/TextArea.types.ts b/packages/design-system-react-native/src/components/TextArea/TextArea.types.ts
--- a/packages/design-system-react-native/src/components/TextArea/TextArea.types.ts
+++ b/packages/design-system-react-native/src/components/TextArea/TextArea.types.ts
@@ -9,9 +9,10 @@
* Additional props merged onto the inner `Input` (`../Input/Input.tsx`).
*
* TextArea owns `value`, `onChangeText`, `placeholder`, `isReadOnly`, `onFocus`,
- * `onBlur`, `isDisabled`, `autoFocus`, `textVariant`, multiline (always on), and inner
- * layout (merged with any `twClassName` you pass here). `placeholderTextColor` is
- * omitted (Input sets it from theme).
+ * `onBlur`, `isDisabled`, `autoFocus`, multiline (always on), and inner layout
+ * (merged with any `twClassName` you pass here). `textVariant` is omitted so
+ * TextArea keeps fixed text styling. `placeholderTextColor` is omitted (Input
+ * sets it from theme).
*/
type TextAreaInputProps = Omit<
InputProps,
diff --git a/packages/design-system-react/src/components/TextArea/README.mdx b/packages/design-system-react/src/components/TextArea/README.mdx
new file mode 100644
--- /dev/null
+++ b/packages/design-system-react/src/components/TextArea/README.mdx
@@ -1,0 +1,328 @@
+import { Canvas, Controls } from '@storybook/addon-docs/blocks';
+import * as TextAreaStories from './TextArea.stories';
+
+# TextArea
+
+TextArea renders a controlled, multiline text input inside a bordered container. Use TextField when you need a single-line field or optional leading and trailing accessories.
+
+```tsx
+import { TextArea } from '@metamask/design-system-react';
+
+<TextArea value="" placeholder="Enter multiple lines..." />;
+```
+
+<Canvas of={TextAreaStories.Default} />
+
+## Props
+
+### `value`
+
+Required controlled value for the TextArea.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>string</code>
+ </td>
+ <td align="left">Yes</td>
+ <td align="left">N/A</td>
+ </tr>
+ </tbody>
+</table>
+
+### `onChange`
+
+Optional callback when the text changes.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>(event: ChangeEvent<HTMLTextAreaElement>) => void</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>undefined</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+### `placeholder`
+
+Optional placeholder string for the inner textarea.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>string</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>undefined</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+### `resize`
+
+Controls whether users can resize the inner textarea.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>TextAreaResize</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>TextAreaResize.None</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<Canvas of={TextAreaStories.Resize} />
+
+### `isDisabled`
+
+When true, the field applies reduced opacity and forwards disabled state to the inner textarea.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>boolean</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>false</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<Canvas of={TextAreaStories.IsDisabled} />
+
+### `isReadOnly`
+
+When true, the inner textarea is not editable.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>boolean</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>false</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<Canvas of={TextAreaStories.IsReadOnly} />
+
+### `isError`
+
+When true, the field shows an error state on the container border and marks the inner textarea as invalid.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>boolean</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>false</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<Canvas of={TextAreaStories.IsError} />
+
+### `rows`
+
+Optional number of visible text rows for the inner textarea.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>number</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>undefined</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<Canvas of={TextAreaStories.Rows} />
+
+### `inputProps`
+
+Additional props forwarded to the inner `textarea`. Do not pass `value`, `placeholder`, `isReadOnly`, `onFocus`, `onBlur`, `onChange`, `rows`, `cols`, or `required` here; use the TextArea-level props where applicable.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>TextAreaProps['inputProps']</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>undefined</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+### `inputRef`
+
+Ref to the inner `textarea`. The component `ref` points at the root `div`.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>Ref<HTMLTextAreaElement></code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>undefined</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+### `inputElement`
+
+Optional node that replaces the default textarea. `inputRef` is only forwarded when the default textarea is rendered.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>ReactNode</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>undefined</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+### `className`
+
+Use the `className` prop to add Tailwind CSS classes to the root container. These classes are merged with the component defaults using `twMerge`.
+
+<table>
+ <thead>
+ <tr>
+ <th align="left">TYPE</th>
+ <th align="left">REQUIRED</th>
+ <th align="left">DEFAULT</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td align="left">
+ <code>string</code>
+ </td>
+ <td align="left">No</td>
+ <td align="left">
+ <code>undefined</code>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+## Controls
+
+<Controls of={TextAreaStories.Default} />
diff --git a/packages/design-system-react/src/components/TextArea/TextArea.constants.ts b/packages/design-system-react/src/components/TextArea/TextArea.constants.ts
new file mode 100644
--- /dev/null
+++ b/packages/design-system-react/src/components/TextArea/TextArea.constants.ts
@@ -1,0 +1,34 @@
+/**
+ * TextAreaResize - resize
+ * Controls the resize behavior of the textarea element.
+ */
+export const TextAreaResize = {
+ /**
+ * The textarea cannot be resized.
+ */
+ None: 'none',
+ /**
+ * The textarea can be resized both horizontally and vertically.
+ */
+ Both: 'both',
+ /**
+ * The textarea can only be resized horizontally.
+ */
+ Horizontal: 'horizontal',
+ /**
+ * The textarea can only be resized vertically.
+ */
+ Vertical: 'vertical',
+} as const;
+export type TextAreaResize =
+ (typeof TextAreaResize)[keyof typeof TextAreaResize];
+
+/**
+ * Maps TextAreaResize values to Tailwind CSS resize classes.
+ */
+export const CLASSMAP_TEXTAREA_RESIZE: Record<TextAreaResize, string> = {
+ [TextAreaResize.None]: 'resize-none',
+ [TextAreaResize.Both]: 'resize',
+ [TextAreaResize.Horizontal]: 'resize-x',
+ [TextAreaResize.Vertical]: 'resize-y',
+};
diff --git a/packages/design-system-react/src/components/TextArea/TextArea.figma.tsx b/packages/design-system-react/src/components/TextArea/TextArea.figma.tsx
new file mode 100644
--- /dev/null
+++ b/packages/design-system-react/src/components/TextArea/TextArea.figma.tsx
@@ -1,0 +1,46 @@
+// import figma needs to remain as figma otherwise it breaks code connect
+// eslint-disable-next-line import-x/no-named-as-default
+import figma from '@figma/code-connect';
+import React from 'react';
+
+import { TextArea } from './TextArea';
+
+import { TextAreaResize } from '.';
+
+/**
+ * -- This file was auto-generated by Code Connect --
+ * React web implementation of TextArea component
+ * `props` includes a mapping from Figma properties and variants to
+ * suggested values. You should update this to match the props of your
+ * code component, and update the `example` function to return the
+ * code example you'd like to see in Figma
+ */
+
+figma.connect(
+ TextArea,
+ 'https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-WIP--MMDS-Components?node-id=12091%3A104',
+ {
+ props: {
+ isDisabled: figma.boolean('isDisabled'),
+ isError: figma.boolean('isError'),
+ isReadOnly: figma.boolean('isReadOnly'),
+ resize: figma.enum('resize', {
+ Vertical: TextAreaResize.Vertical,
+ Horizontal: TextAreaResize.Horizontal,
+ Both: TextAreaResize.Both,
+ None: TextAreaResize.None,
+ }),
+ placeholder: figma.string('placeholder'),
+ },
+ example: ({ isDisabled, isError, isReadOnly, resize, placeholder }) => (
+ <TextArea
+ value=""
+ isDisabled={isDisabled}
+ isError={isError}
+ isReadOnly={isReadOnly}
+ resize={resize}
+ placeholder={placeholder ?? 'Enter text'}
+ />
+ ),
+ },
+);
diff --git a/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
new file mode 100644
--- /dev/null
+++ b/packages/design-system-react/src/components/TextArea/TextArea.stories.tsx
@@ -1,0 +1,155 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+import React, { useEffect, useState } from 'react';
+
+import README from './README.mdx';
+import { TextArea } from './TextArea';
+import { TextAreaResize } from './TextArea.constants';
+import type { TextAreaProps } from './TextArea.types';
+
+function ControlledTextArea(props: TextAreaProps) {
+ const [value, setValue] = useState(props.value ?? '');
+ useEffect(() => {
+ setValue(props.value ?? '');
+ }, [props.value]);
+
+ return (
+ <TextArea
+ {...props}
+ value={value}
+ onChange={(event) => setValue(event.target.value)}
+ />
+ );
+}
+
+const meta: Meta<TextAreaProps> = {
+ title: 'React Components/TextArea',
+ component: TextArea,
+ parameters: {
+ docs: {
+ page: README,
+ },
+ },
+ argTypes: {
+ isError: {
+ control: 'boolean',
+ description: 'When true, applies error styling to the textarea',
+ },
+ isDisabled: {
+ control: 'boolean',
+ description: 'When true, disables the textarea',
+ },
+ isReadOnly: {
+ control: 'boolean',
+ description: 'When true, makes the textarea read-only',
+ },
+ value: {
+ control: 'text',
+ },
+ placeholder: {
+ control: 'text',
+ },
+ resize: {
+ control: 'select',
+ options: Object.values(TextAreaResize),
+ description: 'Controls the resize behavior of the textarea',
+ },
+ rows: {
+ control: 'number',
+ },
+ className: {
+ control: 'text',
+ description: 'Additional CSS classes merged with the component defaults',
+ },
+ },
+};
+
+export default meta;
+
+type Story = StoryObj<TextAreaProps>;
+
+export const Default: Story = {
+ args: {
+ value: '',
+ placeholder: 'Enter multiple lines...',
+ },
+ render: (args) => <ControlledTextArea {...args} />,
+};
+
+export const IsError: Story = {
+ render: () => (
+ <div className="flex flex-col gap-4">
+ <ControlledTextArea value="" placeholder="Default" />
+ <ControlledTextArea value="" placeholder="Error state" isError />
+ </div>
+ ),
+};
+
+export const IsDisabled: Story = {
+ render: () => (
+ <div className="flex flex-col gap-4">
+ <ControlledTextArea value="Editable" placeholder="Enabled" />
+ <ControlledTextArea
+ value="Not editable"
+ placeholder="Disabled"
+ isDisabled
+ />
+ </div>
+ ),
+};
+
+export const IsReadOnly: Story = {
+ render: () => (
+ <div className="flex flex-col gap-4">
+ <ControlledTextArea value="Editable" placeholder="Editable field" />
+ <ControlledTextArea
+ value="Cannot edit this value"
+ placeholder="Read-only"
+ isReadOnly
+ />
+ </div>
+ ),
+};
+
+export const Resize: Story = {
+ render: () => (
+ <div className="flex flex-col gap-4">
+ <ControlledTextArea
+ value="Cannot be resized"
+ placeholder="No resize (default)"
+ resize={TextAreaResize.None}
+ />
+ <ControlledTextArea
+ value="Resize vertical only"
+ placeholder="Vertical resize"
+ resize={TextAreaResize.Vertical}
+ />
+ <ControlledTextArea
+ value="Resize horizontal only"
+ placeholder="Horizontal resize"
+ resize={TextAreaResize.Horizontal}
+ />
+ <ControlledTextArea
+ value="Resize in both directions"
+ placeholder="Both directions"
+ resize={TextAreaResize.Both}
+ />
+ </div>
+ ),
+};
+
+export const Rows: Story = {
+ render: () => (
+ <div className="flex flex-col gap-4">
+ <ControlledTextArea
+ placeholder="3 rows"
+ rows={3}
+ value="3-row textarea"
+ />
+ <ControlledTextArea
+ placeholder="6 rows"
+ rows={6}
+ value="6-row textarea for longer content"
+ />
+ </div>
+ ),
+};
diff --git a/packages/design-system-react/src/components/TextArea/TextArea.test.tsx b/packages/design-system-react/src/components/TextArea/TextArea.test.tsx
new file mode 100644
--- /dev/null
+++ b/packages/design-system-react/src/components/TextArea/TextArea.test.tsx
@@ -1,0 +1,392 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import React, { createRef } from 'react';
+
+import { TextArea } from './TextArea';
+import { TextAreaResize } from './TextArea.constants';
+
+const ROOT_TEST_ID = 'text-area';
+const noop = () => undefined;
+
+describe('TextArea', () => {
+ describe('rendering', () => {
+ it('renders with default props', () => {
+ render(
+ <TextArea
+ data-testid={ROOT_TEST_ID}
+ onChange={noop}
+ placeholder="Enter text"
+ value=""
+ />,
+ );
+
+ expect(screen.getByTestId(ROOT_TEST_ID)).toBeInTheDocument();
+ expect(screen.getByRole('textbox')).toHaveAttribute(
+ 'placeholder',
+ 'Enter text',
+ );
+ expect(screen.getByRole('textbox')).not.toBeDisabled();
+ expect(screen.getByRole('textbox')).not.toHaveAttribute('readonly');
+ expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-invalid');
+ });
+
+ it('renders placeholder and value on the inner textarea', () => {
+ render(
+ <TextArea placeholder="Enter value" value="hello" onChange={noop} />,
+ );
+
+ expect(screen.getByRole('textbox')).toHaveValue('hello');
+ });
+
+ it('renders a custom inputElement when provided', () => {
+ render(
+ <TextArea
+ value=""
+ inputElement={<textarea data-testid="custom-input" />}
+ />,
+ );
+
+ expect(screen.getByTestId('custom-input')).toBeInTheDocument();
+ expect(screen.queryByRole('textbox')).toBe(
+ screen.getByTestId('custom-input'),
+ );
+ });
+ });
+
+ describe('inputProps', () => {
+ it('forwards inputProps to the inner textarea', () => {
+ render(
+ <TextArea
+ value=""
+ onChange={noop}
+ placeholder="forwarded"
+ inputProps={{ 'aria-label': 'forwarded-label' }}
+ />,
+ );
+
+ expect(screen.getByLabelText('forwarded-label')).toBe(
+ screen.getByRole('textbox'),
+ );
+ });
+
+ it('merges inputProps.className with the inner textarea default classes', () => {
+ render(
+ <TextArea
+ value=""
+ onChange={noop}
+ inputProps={{ className: 'mt-2' }}
+ />,
+ );
+
+ expect(screen.getByRole('textbox')).toHaveClass('mt-2', 'flex-1');
+ });
+ });
+
+ describe('resize', () => {
+ const cases: { resize: TextAreaResize; resizeClass: string }[] = [
+ { resize: TextAreaResize.Vertical, resizeClass: 'resize-y' },
+ { resize: TextAreaResize.None, resizeClass: 'resize-none' },
+ { resize: TextAreaResize.Both, resizeClass: 'resize' },
+ { resize: TextAreaResize.Horizontal, resizeClass: 'resize-x' },
+ ];
+
+ cases.forEach(({ resize, resizeClass }) => {
+ it(`applies ${resizeClass} when resize is ${resize}`, () => {
+ render(<TextArea value="" onChange={noop} resize={resize} />);
+
+ expect(screen.getByRole('textbox')).toHaveClass(resizeClass);
+ });
+ });
+
+ it('does not allow resize by default', () => {
+ render(<TextArea value="" onChange={noop} />);
+
+ expect(screen.getByRole('textbox')).toHaveClass('resize-none');
+ });
+ });
+
+ describe('state', () => {
+ it('applies disabled state when isDisabled is true', () => {
+ render(
+ <TextArea
+ data-testid={ROOT_TEST_ID}
+ isDisabled
+ onChange={noop}
+ value=""
+ />,
+ );
+
+ expect(screen.getByTestId(ROOT_TEST_ID)).toHaveClass(
+ 'cursor-not-allowed',
+ 'opacity-50',
+ );
+ expect(screen.getByRole('textbox')).toBeDisabled();
+ });
+
+ it('marks the inner textarea readonly when isReadOnly is true', () => {
+ render(<TextArea isReadOnly value="Locked" onChange={noop} />);
+
+ expect(screen.getByRole('textbox')).toHaveAttribute('readonly');
+ expect(screen.getByRole('textbox')).toHaveValue('Locked');
+ });
+
+ it('applies error styling and aria-invalid when isError is true', () => {
+ render(
+ <TextArea
+ data-testid={ROOT_TEST_ID}
+ isError
+ onChange={noop}
+ value=""
+ />,
+ );
+
+ expect(screen.getByTestId(ROOT_TEST_ID)).toHaveClass(
+ 'border-error-default',
+ );
+ expect(screen.getByRole('textbox')).toHaveAttribute(
+ 'aria-invalid',
+ 'true',
+ );
+ });
+
+ it('applies focused border on focus and restores muted border on blur', () => {
+ render(<TextArea data-testid={ROOT_TEST_ID} onChange={noop} value="" />);
+
+ const root = screen.getByTestId(ROOT_TEST_ID);
+ const textarea = screen.getByRole('textbox');
+
+ fireEvent.focus(textarea);
+ expect(root).toHaveClass('border-default');
+ expect(root).not.toHaveClass('border-muted');
+
+ fireEvent.blur(textarea);
+ expect(root).toHaveClass('border-muted');
+ expect(root).not.toHaveClass('border-default');
+ });
+
+ it('keeps error border when focused and isError is true', () => {
+ render(
+ <TextArea
+ data-testid={ROOT_TEST_ID}
+ isError
... diff truncated: showing 800 of 1331 linesYou can send follow-ups to the cloud agent here.
7f32e89 to
2c504c8
Compare
📖 Storybook Preview |
2c504c8 to
26e7a83
Compare
📖 Storybook Preview |
26e7a83 to
7126e38
Compare
📖 Storybook Preview |
📖 Storybook Preview |
| | 'readOnly' | ||
| | 'value' | ||
| > & | ||
| Omit<TextAreaPropsShared, 'inputElement' | 'textVariant'> & { |
There was a problem hiding this comment.
This React surface intentionally does not mirror the mobile escape hatch, because inputElement is not used by the web implementation and will be removed from RN in a follow-up. The migration docs should make that boundary explicit so extension consumers do not assume parity with mobile.
📖 Storybook Preview |
|
|
||
| TextArea renders a controlled, multiline native `textarea` with design system styling. Use TextField when you need a single-line field or optional leading and trailing accessories. | ||
|
|
||
| ```tsx |
There was a problem hiding this comment.
The docs use the real controlled example here so the displayed code matches the public React API instead of a wrapper helper. That keeps Storybook's Show code aligned with what consumers actually need to import and use.
|
|
||
| Use the `rows` prop to set the initial visible height of the TextArea, in lines. The component has a minimum height, so small `rows` values may not visibly change the rendered height. | ||
|
|
||
| TextArea renders at full container width. Use layout styles or `className` for width changes instead of the native `cols` attribute. |
There was a problem hiding this comment.
Rows is the only sizing knob that matters for this React API because the component is rendered at full width, so cols would not change the layout in practice. The migration docs should call out that extension consumers should map layout width to container styling and rows to height.
| value: '', | ||
| placeholder: 'Enter multiple lines...', | ||
| }, | ||
| render: function Render(args) { |
| TextArea, | ||
| 'https://www.figma.com/design/1D6tnzXqWgnUC3spaAOELN/%F0%9F%A6%8A-MMDS-Components?node-id=12302%3A528', | ||
| { | ||
| props: { |
There was a problem hiding this comment.
Only the React-facing Figma properties are mapped here, which keeps the design file aligned with the web API boundary. Leaving inputElement and textVariant out makes it clear that mobile parity is being handled separately.
| * TextAreaResize - resize | ||
| * Controls the resize behavior of the textarea element. | ||
| */ | ||
| export const TextAreaResize = { |
There was a problem hiding this comment.
Resize stays local to the React package because RN does not have an equivalent native resize axis to share. Keeping it in constants for now keeps the web API explicit without implying a cross-platform contract we cannot actually support.
| /> | ||
| ), | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Figma code connect aligned and working as expected
figma.code.connect720.mov
📖 Storybook Preview |
|
|
||
| **Prop forwarding convention:** | ||
|
|
||
| - When a component forwards the remaining props to its rendered element, name the destructured catch-all `...props` and spread `{...props}` onto the element. |
There was a problem hiding this comment.
I was noticing a few violations of this, including in this PR. Its a small nit but helps with consistency
When a component forwards the remaining element props, use ...props instead of ...rest so the public API and implementation naming stay consistent. That convention makes it easier to scan for pass-through props across the codebase.
|
|
||
| ### Platform-specific const objects | ||
|
|
||
| If a const object only exists for one platform's behavior or styling, keep it in that platform package near the component instead of moving it to `@metamask/design-system-shared` just to satisfy the barrel export. |
There was a problem hiding this comment.
Platform-specific const objects should stay local when only one platform needs them. Re-exporting them from the barrel is fine, but moving them into shared would imply a cross-platform contract that does not actually exist.
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
📖 Storybook Preview |
## Release 43.0.0 This release drops Node.js 18 support across the release line, adds several new components, and includes a small set of breaking API changes that are documented in the migration guides. ### 📦 Package Versions - `@metamask/design-system-shared`: **0.21.0** - `@metamask/design-system-react`: **0.25.0** - `@metamask/design-system-react-native`: **0.28.0** - `@metamask/design-tokens`: **8.5.0** - `@metamask/design-system-tailwind-preset`: **0.9.0** - `@metamask/design-system-twrnc-preset`: **0.5.0** ### 🔄 Shared Type Updates (0.21.0) #### Added - Added `ContentPropsShared` and `ContentVerticalAlignment` for React Native list-style rows and related layout patterns ([#1192](#1192)) #### Changed - **BREAKING:** Dropped Node.js 18 support for the release line; consumers must run Node 20 or newer ([#1206](#1206)) - **BREAKING:** Updated `TextAreaPropsShared` to remove `inputElement` so React Native `TextArea` can render the root `TextInput` directly ([#1205](#1205)) ### 🌐 React Web Updates (0.25.0) #### Added - Added `Popover` for anchored overlays such as menus, tooltips, and dialogs ([#1153](#1153)) - Added `TextArea` for controlled multiline text entry ([#1036](#1036)) - Added `TextFieldSearch` for controlled search-field flows on top of `TextField` ([#1171](#1171)) - Added `FormTextField` for labeled form controls built from `Label`, `TextField`, and `HelpText` ([#1197](#1197)) #### Changed - **BREAKING:** Dropped Node.js 18 support for the release line; consumers must run Node 20 or newer ([#1206](#1206)) - Updated avatar fallback handling so `AvatarToken`, `AvatarNetwork`, and `AvatarFavicon` resolve consistently when the requested image is unavailable ([#1212](#1212)) ### 📱 React Native Updates (0.28.0) #### Added - Added `Content` for composing scrollable and padded content sections on React Native screens; it is closely related to the upcoming `ListItem` work ([#1192](#1192)) #### Changed - **BREAKING:** Dropped Node.js 18 support for the release line; consumers must run Node 20 or newer ([#1206](#1206)) - Added default padding and `isInteractive` support to `SectionHeader` so section rows match the new mobile layout patterns ([#1210](#1210)) - **BREAKING:** Flattened `TextArea` so it renders the root `TextInput` directly; pass `TextInput` props on `TextArea`, use the component `ref` for the input, and stop relying on `inputProps` or `inputElement` ([#1205](#1205)) - Updated avatar fallback handling so `AvatarToken`, `AvatarNetwork`, and `AvatarFavicon` resolve consistently when the requested image is unavailable ([#1212](#1212)) ###⚠️ Breaking Changes #### Node.js 18 support removed **What Changed:** - The release line now requires Node 20 or newer. - This applies across the monorepo, including the shared package, web package, React Native package, tokens, and both preset packages. **Impact:** - Consumers still on Node 18 must upgrade their runtime before installing or developing against this release line. - Node 18 is end-of-life, so this change aligns the repo with the supported app runtimes. #### React Native `TextArea` flattening **What Changed:** - `TextArea` now renders the root `TextInput` directly. - `inputProps` and `inputElement` are removed. - `inputRef` is replaced by the component `ref`. **Migration:** ```tsx // Before (0.27.0) <TextArea inputProps={{ placeholder: 'Message' }} inputElement={<CustomInput />} /> // After (0.28.0) <TextArea placeholder="Message" ref={inputRef} /> ``` **Impact:** - Affects React Native consumers using `TextArea`. - Call sites that depended on the wrapper/input split need to be updated. See migration guides for complete instructions: - [React Migration Guide](./packages/design-system-react/MIGRATION.md#from-version-0220-to-0230) - [React Native Migration Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0270-to-0280) ### ✅ Checklist - [x] Changelogs updated with human-readable descriptions - [x] Changelog validation passed (`yarn changelog:validate`) - [x] Version bumps follow semantic versioning - [x] design-system-shared: minor (0.20.0 → 0.21.0) - shared type additions and breaking TextArea/shared runtime baseline - [x] design-system-react: minor (0.24.0 → 0.25.0) - new components and release-line update - [x] design-system-react-native: minor (0.27.0 → 0.28.0) - new component, SectionHeader update, and breaking TextArea change - [x] Breaking changes documented with migration guidance - [x] Migration guides updated with before/after examples (if breaking changes) - [x] PR references included in changelog entries ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) - [x] I've reviewed the [Release Workflow](./.cursor/rules/release-workflow.md) cursor rule - [ ] All tests pass (`yarn build && yarn test && yarn lint`) - [x] Changelog validation passes (`yarn changelog:validate`) ## **Pre-merge reviewer checklist** - [x] I've reviewed the [Reviewing Release PRs](./docs/reviewing-release-prs.md) guide - [x] Package versions follow semantic versioning - [x] Changelog entries are consumer-facing (not commit message regurgitation) - [x] Breaking changes are documented in MIGRATION.md with examples - [x] All unreleased changes are accounted for in changelogs <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking React Native TextArea and Node 18 removal affect consumer upgrades; most diff is release metadata with coordinated peer dependency bumps. > > **Overview** > **Release 43.0.0** bumps the monorepo root to **43.0.0** and publishes coordinated semver bumps across design-system packages, with **yarn.lock** peer ranges updated for `@metamask/design-system-tailwind-preset` **^0.9.0** and `@metamask/design-system-twrnc-preset` **^0.5.0**. > > Across the release line, changelogs record **Node.js 18 dropped** (Node **20+** required). **@metamask/design-system-react** **0.25.0** documents new **`Popover`**, **`TextArea`**, **`TextFieldSearch`**, and **`FormTextField`**, plus avatar fallback fixes. **@metamask/design-system-react-native** **0.28.0** adds **`Content`**, updates **`SectionHeader`** (default padding, **`isInteractive`**), and includes a **breaking** **`TextArea`** flattening (`inputProps` / `inputElement` / `inputRef` removed; props and **`ref`** target the root **`TextInput`**). **@metamask/design-system-shared** **0.21.0** adds **`ContentPropsShared`** / **`ContentVerticalAlignment`** and removes **`inputElement`** from shared **`TextArea`** props. > > Migration guide edits in this diff: React Native **0.27.0 → 0.28.0** **`TextArea`** guidance; React version heading **0.22.0 → 0.23.0** for **`BannerBase`** (changelog-driven **0.25.0** items are not new migration sections here). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 23b0cda. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Description
Adds the React
TextAreacomponent to@metamask/design-system-react, migrated from the MetaMask Extension component library and aligned with the current design-system API.The React component renders the native
<textarea>element directly. Native textarea attributes, events,className,style,data-testid, andrefare applied to the top-level TextArea instead of going through wrapper-only props such asinputPropsorinputRef.This PR is intentionally React-only. React Native
TextAreaandTextAreaPropsSharedwere restored to currentmainbehavior. Follow-up #1202 covers removing React Native TextAreainputElementandtextVariantsupport after the mobile usage audit.What is included
@metamask/design-system-reactTextAreacomponent as a controlled multiline native<textarea>TextAreaResizeconst object withNone,Both,Horizontal, andVertical; default isNonerows,cols,maxLength,required,id,name,data-testid,onKeyDown,onPaste,style,className, andrefinputElement,inputProps,inputRef, ortextVariantAPIaria-invalidsupport whenisErroris trueReviewer notes
inputElementwas intentionally not added to the React API. The remaining React Native use is being tracked separately in Remove React Native TextArea inputElement and textVariant support #1202, along withtextVariantcleanup.colsis supported as a native attribute, but the React docs/stories treat width as a layout concern (w-full/ container width), sorowsis the primary sizing control shown to reviewers.TextAreaAPI instead of a wrapper helper.Related issues
Fixes:
Follow-up:
Manual testing steps
yarn workspace @metamask/design-system-react test --testPathPattern=TextArea --passWithNoTests --no-coverageyarn eslint packages/design-system-react/src/components/TextArea/TextArea.tsx packages/design-system-react/src/components/TextArea/TextArea.types.ts packages/design-system-react/src/components/TextArea/TextArea.test.tsx packages/design-system-react/src/components/TextArea/TextArea.stories.tsxyarn workspace @metamask/design-system-react buildyarn workspace @metamask/storybook-react build-storybook --quietgit diff --checkScreenshots/Recordings
Before
No React
TextAreacomponent in@metamask/design-system-react.After
React TextArea in storybook with MDX docs and controls
textarea.after720.mov
React TextArea in MMDS aligns with Extension
mmds.vs.extension720.mov
React TextArea in MMDS aligns with MMDS React Native
react.vs.reactnative.mov
React TextArea and Figma alignment
React.figma.alignment720.mov
Pre-merge author checklist
Pre-merge reviewer checklist
Note
Low Risk
New public UI component in the design system with tests and docs; no auth, data, or shared-package contract changes beyond documentation.
Overview
Adds a React-only
TextAreato@metamask/design-system-react: a controlled multiline field that renders a native<textarea>as the root element, with design-system styling and package exports.The API drops Extension-style wrappers (
inputElement,inputProps,inputRef,textVariant) in favor of forwarding native textarea props, events,className,style,data-testid, andrefdirectly. Shared behavior comes fromTextAreaPropsShared(minus those RN-oriented fields); resize is React-only viaTextAreaResizeand Tailwind class mapping. States include disabled, read-only, error (aria-invalid), and CSS-driven focus borders.Also ships Storybook (MDX + stories), Figma Code Connect, unit tests, and architecture doc updates: standard
...propsforwarding on the rendered element, and guidance to keep platform-only consts (e.g.TextAreaResize) in the React package rather than shared.Reviewed by Cursor Bugbot for commit 12cb440. Bugbot is set up for automated code reviews on this repo. Configure here.