diff --git a/packages/design-system-react-native/src/components/Input/Input.test.tsx b/packages/design-system-react-native/src/components/Input/Input.test.tsx
index 678c7f051..fe91c003b 100644
--- a/packages/design-system-react-native/src/components/Input/Input.test.tsx
+++ b/packages/design-system-react-native/src/components/Input/Input.test.tsx
@@ -43,35 +43,67 @@ describe('Input', () => {
const input = getByTestId(TEST_ID);
expect(input).toBeDisabled();
- expect(input).toHaveStyle({ opacity: tw`opacity-50`.opacity });
+ const expectedOpacity = (tw.style('opacity-50') as TextStyle).opacity;
+ expect(input).toHaveStyle({ opacity: expectedOpacity });
});
- it('applies iOS placeholder lineHeight workaround when placeholder is visible', () => {
- if (Platform.OS !== 'ios') {
- return;
- }
+ describe('iOS placeholder lineHeight workaround', () => {
+ const originalOS = Platform.OS;
+ afterEach(() => {
+ Platform.OS = originalOS;
+ });
+
+ it('applies iOS placeholder lineHeight workaround when placeholder is visible and multiline is false', () => {
+ Platform.OS = 'ios';
+
+ const { getByTestId } = render(
+ ,
+ );
+
+ const input = getByTestId(TEST_ID);
+
+ expect(input).toHaveStyle({ lineHeight: 0 });
+ });
+
+ it('does not apply placeholder lineHeight workaround outside iOS', () => {
+ Platform.OS = 'android';
+
+ const { getByTestId } = render(
+ ,
+ );
+
+ const input = getByTestId(TEST_ID);
+
+ expect(input).not.toHaveStyle({ lineHeight: 0 });
+ });
+ });
+
+ it('when multiline is true, does not apply lineHeight zero for visible placeholder', () => {
const { getByTestId } = render(
- ,
+ ,
);
const input = getByTestId(TEST_ID);
- expect(input).toHaveStyle({ lineHeight: 0 });
+ expect(input).not.toHaveStyle({ lineHeight: 0 });
});
- it('does not apply placeholder lineHeight workaround outside iOS', () => {
- if (Platform.OS === 'ios') {
- return;
- }
-
+ it('when multiline is true, applies BodyMd paragraph lineHeight', () => {
const { getByTestId } = render(
- ,
+ ,
);
const input = getByTestId(TEST_ID);
+ const expectedLineHeight = (tw.style('text-body-md') as TextStyle)
+ .lineHeight;
- expect(input).not.toHaveStyle({ lineHeight: 0 });
+ expect(input).toHaveStyle({ lineHeight: expectedLineHeight });
});
it('removes placeholder lineHeight workaround after value changes from empty to non-empty', () => {
diff --git a/packages/design-system-react-native/src/components/Input/Input.tsx b/packages/design-system-react-native/src/components/Input/Input.tsx
index 8032fe19d..a592ec51b 100644
--- a/packages/design-system-react-native/src/components/Input/Input.tsx
+++ b/packages/design-system-react-native/src/components/Input/Input.tsx
@@ -30,6 +30,7 @@ export const Input = forwardRef(
onBlur,
onFocus,
autoFocus = false,
+ multiline,
...props
},
ref,
@@ -37,6 +38,7 @@ export const Input = forwardRef(
const [isFocused, setIsFocused] = useState(autoFocus);
const tw = useTailwind();
const theme = useTheme();
+ const isMultiline = multiline === true;
const placeholderTextColor = useMemo(
() =>
@@ -56,7 +58,30 @@ export const Input = forwardRef(
// scoped to the placeholder-visible state without affecting typed text.
const isPlaceholderVisible = hasPlaceholder && value === '';
- const inputStyle = useMemo(
+ // Multiline field styles
+ const multilineChromeStyle = useMemo(
+ () =>
+ tw.style(
+ 'text-default',
+ 'bg-default',
+ 'border',
+ 'border-transparent',
+ !isStateStylesDisabled && isDisabled && 'opacity-50',
+ !isStateStylesDisabled &&
+ !isDisabled &&
+ isFocused &&
+ 'border-primary-default',
+ twClassName,
+ ),
+ [isStateStylesDisabled, isDisabled, isFocused, twClassName, tw],
+ );
+ const multilineTypographyStyle = useMemo(
+ () => tw.style(`text-${textVariant}`, fontClass),
+ [textVariant, fontClass, tw],
+ );
+
+ // Single-line field styles
+ const singleLineChromeStyle = useMemo(
() =>
tw.style(
fontClass,
@@ -80,11 +105,26 @@ export const Input = forwardRef(
tw,
],
);
-
- const variantTextStyle = useMemo(
+ const singleLineTypographyStyle = useMemo(
() => MAP_TEXT_VARIANT_INPUT_METRICS[textVariant],
[textVariant],
);
+ // iOS-only single-line placeholder fix: native TextInput can render
+ // placeholder text vertically offset. Do not use on Android (lineHeight 0
+ // collapses text) or on multiline (breaks paragraph layout).
+ const iosSingleLinePlaceholderLineHeightFix = Platform.OS === 'ios' &&
+ isPlaceholderVisible && { lineHeight: 0 as const };
+
+ const resolvedStyle = (
+ isMultiline
+ ? [multilineChromeStyle, multilineTypographyStyle, style]
+ : [
+ singleLineChromeStyle,
+ singleLineTypographyStyle,
+ iosSingleLinePlaceholderLineHeightFix,
+ style,
+ ]
+ ).filter(Boolean);
useEffect(() => {
if (isDisabled || isReadOnly) {
@@ -107,21 +147,12 @@ export const Input = forwardRef(
},
[onFocus],
);
- const resolvedStyle = [
- inputStyle,
- variantTextStyle,
- // iOS-only workaround: when a placeholder is visible, native iOS
- // TextInput can render placeholder text vertically offset.
- // Keep this iOS-only because lineHeight: 0 can collapse text on Android.
- Platform.OS === 'ios' &&
- isPlaceholderVisible && { lineHeight: 0 as const },
- style,
- ].filter(Boolean);
return (
;
+```
+
+## Props
+
+### `value`
+
+Required controlled value for the TextArea.
+
+| TYPE | REQUIRED | DEFAULT |
+| -------- | -------- | ------- |
+| `string` | Yes | N/A |
+
+```tsx
+import { TextArea } from '@metamask/design-system-react-native';
+
+;
+```
+
+### `onChangeText`
+
+Optional callback when the text changes.
+
+| TYPE | REQUIRED | DEFAULT |
+| ------------------------ | -------- | ----------- |
+| `(text: string) => void` | No | `undefined` |
+
+```tsx
+import { TextArea } from '@metamask/design-system-react-native';
+
+