Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/design-system-react-native/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This guide provides detailed instructions for migrating your project from one ve
- [Icon Component](#icon-component)
- [Checkbox Component](#checkbox-component)
- [Version Updates](#version-updates)
- [From version 0.19.0 to 0.20.0](#from-version-0190-to-0200)
- [From version 0.18.0 to 0.19.0](#from-version-0180-to-0190)
- [From version 0.16.0 to 0.17.0](#from-version-0160-to-0170)
- [From version 0.15.0 to 0.16.0](#from-version-0150-to-0160)
Expand All @@ -28,6 +29,70 @@ This guide provides detailed instructions for migrating your project from one ve

## Version Updates

### From version 0.19.0 to 0.20.0

#### TextField and TextFieldSearch: layered props (`inputProps` and root `Box`)

**What changed:**

- **`TextField`** is a root **`Box`** (a styled **`View`**) with an inner **`Input`**. Props that belong on the native text control must be passed in **`inputProps`** (for example `keyboardType`, `secureTextEntry`, `returnKeyType`, `autoCapitalize`, `accessibilityLabel`, `accessibilityState`).
- **`placeholder`**, **`isReadonly`**, **`onFocus`**, and **`onBlur`** are owned at the **`TextField` / `TextFieldSearch` top level** and forwarded to the inner `Input`. Do not pass them only through **`inputProps`**.
- **`placeholderTextColor`** is not supported on the public **`TextField`** API; the inner **`Input`** sets placeholder color from the theme.
- Remaining top-level props on **`TextField`** are **`BoxProps`** (layout and **`View`** props from React Native), except for keys reserved by **`TextField`** (see type **`TextFieldProps`**). **`hitSlop`**, **`onPress`**, and other **`Pressable`**-only APIs are not supported on the root; tap-to-focus on the chrome is removed—users focus by tapping the **`Input`** / **`TextInput`**.
- Cross-platform field definitions live in **`TextFieldPropsShared`** in **`@metamask/design-system-shared`** (also re-exported from **`@metamask/design-system-react-native`**).
- **`TextFieldSearchProps`** extends **`TextFieldProps`**; the same layering applies. **`onPressClearButton`**, **`clearButtonProps`**, **`startAccessory`**, **`endAccessory`**, and **`style`** behavior are unchanged.

**Migration:**

Move inner `TextInput` props from the root into **`inputProps`**. Keep **`placeholder`**, **`onFocus`**, and **`onBlur`** on the component root when you use them.
Comment thread
brianacnguyen marked this conversation as resolved.

```tsx
// Before (0.19.0) — native TextInput props on TextField
<TextField
value={query}
onChangeText={setQuery}
placeholder="Search"
keyboardType="default"
secureTextEntry
onFocus={handleFocus}
/>

// After (0.20.0)
<TextField
value={query}
onChangeText={setQuery}
placeholder="Search"
onFocus={handleFocus}
inputProps={{
keyboardType: 'default',
secureTextEntry: true,
}}
/>
```

If you relied on **`hitSlop`** or a larger tap target on the field chrome, wrap **`TextField`** in your own **`Pressable`** (or enlarge the inner input via **`inputProps`**) at the app level.

Remove **`placeholderTextColor`** from **`TextField`** call sites; rely on theme behavior from **`Input`**.

**Impact:**

- Any **`TextField`** or **`TextFieldSearch`** usage that spread or passed **`TextInput`** props on the root must move those keys into **`inputProps`**, except for the props **`TextField`** owns (**`value`**, **`onChangeText`**, **`placeholder`**, **`isReadonly`**, **`onFocus`**, **`onBlur`**, **`isDisabled`**, **`autoFocus`**, **`isError`**, accessories, **`inputElement`**, **`testID`**, **`style`**, **`twClassName`**) and valid **`BoxProps`** / **`View`** props you pass at the top level.
- Call sites that passed **`Pressable`**-only props (**`hitSlop`**, root **`onPress`**, root **`disabled`**) must be updated: the root is no longer a **`Pressable`**.
- Type-only consumers can extend or intersect **`TextFieldPropsShared`** from **`@metamask/design-system-shared`** for shared forms code.

#### Input: theme `placeholderTextColor` always wins

**What changed:**

**`Input`** used to pass **`placeholderTextColor`** on the native **`TextInput`** **before** **`{...props}`**, so a **`placeholderTextColor`** included in **`props`** could override the theme-derived color. **`Input`** now passes **`placeholderTextColor`** **after** **`{...props}`**, so the **theme token for placeholder text is always applied** and **is not overridden** by caller props.

**Impact:**

- Passing **`placeholderTextColor`** on **`Input`** has **no effect** on the rendered placeholder tint; remove dead props if you had any.
- **`TextField`** already omits **`placeholderTextColor`** from its public API and forwards inner **`Input`** behavior only.

---

### From version 0.18.0 to 0.19.0

#### HeaderRoot: `titleAccessory` no longer renders without `title`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export const Input = forwardRef<TextInput, InputProps>(
return (
<TextInput
ref={ref}
placeholderTextColor={placeholderTextColor}
{...props}
placeholder={placeholder}
placeholderTextColor={placeholderTextColor}
value={value}
style={resolvedStyle}
editable={!isDisabled && !isReadonly}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TextField

TextField is a controlled-only boxed input that can include accessories before or after the input.
TextField is used to render a controlled, single-line text input inside a fixed-height row with optional leading and trailing content.

```tsx
import { TextField } from '@metamask/design-system-react-native';
Expand All @@ -10,8 +10,6 @@ import { TextField } from '@metamask/design-system-react-native';

## Props

This component extends [Input](../Input/Input.tsx) props (which extends React Native's [TextInput](https://reactnative.dev/docs/textinput)), excluding `textVariant` and `isStateStylesDisabled`.

### `value`

Required controlled value for the TextField.
Expand All @@ -20,57 +18,171 @@ Required controlled value for the TextField.
| -------- | -------- | ------- |
| `string` | Yes | N/A |

### Layout
```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="hello" placeholder="Value example" />;
```

### `onChangeText`

Optional callback when the text changes.

| TYPE | REQUIRED | DEFAULT |
| ------------------------ | -------- | ----------- |
| `(text: string) => void` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" onChangeText={(text) => {}} placeholder="Change handler" />;
```

### `placeholder`

Optional placeholder string for the inner input.

| TYPE | REQUIRED | DEFAULT |
| -------- | -------- | ----------- |
| `string` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" placeholder="Search" />;
```

### `isReadonly`

When true, the inner input is not editable.

| TYPE | REQUIRED | DEFAULT |
| --------- | -------- | ------- |
| `boolean` | No | `false` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" isReadonly placeholder="Read-only" />;
```

### `onFocus`

Optional handler when the inner input receives focus. TextField composes this with its own focus border behavior. Do not pass `onFocus` through `inputProps`; use this prop instead.

| TYPE | REQUIRED | DEFAULT |
| ---------- | -------- | ----------- |
| `function` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" placeholder="Focus" onFocus={() => {}} />;
```

### `onBlur`

Optional handler when the inner input loses focus. TextField composes this with its own focus border behavior. Do not pass `onBlur` through `inputProps`; use this prop instead.

| TYPE | REQUIRED | DEFAULT |
| ---------- | -------- | ----------- |
| `function` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" placeholder="Blur" onBlur={() => {}} />;
```

### `inputProps`

Additional props forwarded to the inner [Input](../Input/Input.tsx) / `TextInput`. Do not pass `placeholder`, `isReadonly`, `onFocus`, or `onBlur` here; use the TextField-level props above. `placeholderTextColor` is omitted from the type; the inner `Input` sets it from the theme. For a required field, use `inputProps.accessibilityState={{ required: true }}` (and related accessibility props as needed).

The field uses a fixed **48px** row height with a single-line inner input.
| TYPE | REQUIRED | DEFAULT |
| ------------------------------------------------------- | -------- | ----------- |
| `Omit<InputProps, …>` (see `TextFieldInputProps` types) | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField
value=""
onChangeText={(text) => {}}
placeholder="Search"
inputProps={{
autoCapitalize: 'none',
returnKeyType: 'search',
}}
/>;
```

### `isError`

Optional boolean to show the error state. Changes the border color to indicate an error.
When true, the field shows an error state (container border).

| TYPE | REQUIRED | DEFAULT |
| --------- | -------- | ------- |
| `boolean` | No | `false` |

```tsx
<TextField value="" isError placeholder="Error state" />
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" isError placeholder="Error state" />;
```

### `isDisabled`

Optional boolean to disable the TextField. Reduces opacity and prevents interaction.
When true, the field applies reduced opacity and forwards disabled state to the inner `Input` (non-editable).

| TYPE | REQUIRED | DEFAULT |
| --------- | -------- | ------- |
| `boolean` | No | `false` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" isDisabled placeholder="Disabled" />;
```

### `autoFocus`

When true, the inner input requests focus on mount.

| TYPE | REQUIRED | DEFAULT |
| --------- | -------- | ------- |
| `boolean` | No | `false` |

```tsx
<TextField value="" isDisabled placeholder="Disabled" />
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" autoFocus placeholder="Focused on mount" />;
```

### `startAccessory`

Optional content to display before the Input. For E2E, set `testID` on the accessory (or wrap it in your own `View`).
Optional content rendered before the inner input. For E2E, set `testID` on the accessory or wrap it in your own `View`.

| TYPE | REQUIRED | DEFAULT |
| ----------- | -------- | ----------- |
| `ReactNode` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';
import { Text } from 'react-native';

<TextField value="" startAccessory={<Text>🔍</Text>} placeholder="Search..." />;
```

### `endAccessory`

Optional content to display after the Input. For E2E, set `testID` on the accessory (or wrap it in your own `View`).
Optional content rendered after the inner input. For E2E, set `testID` on the accessory or wrap it in your own `View`.

| TYPE | REQUIRED | DEFAULT |
| ----------- | -------- | ----------- |
| `ReactNode` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';
import { Text } from 'react-native';

<TextField
Expand All @@ -82,21 +194,50 @@ import { Text } from 'react-native';

### `inputElement`

Optional prop to replace the default Input with a custom element.
Optional node that replaces the default `Input`. The forwarded ref still targets `TextInput` when the default input is used; with a custom `inputElement`, ensure your control is focusable if users need keyboard entry.

| TYPE | REQUIRED | DEFAULT |
| ----------- | -------- | ----------- |
| `ReactNode` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';
import { TextInput } from 'react-native';

<TextField value="" inputElement={<TextInput placeholder="Custom input" />} />;
```

### `testID`

Optional test id for the root `Box`.

| TYPE | REQUIRED | DEFAULT |
| -------- | -------- | ----------- |
| `string` | No | `undefined` |

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField value="" testID="my-text-field" placeholder="E2E" />;
```

### Layout and accessibility (`Box` / `View`)

Pass `BoxProps` and React Native `View` props at the top level for layout and accessibility on the root container (for example `accessibilityHint`, `pointerEvents`). Keys reserved by TextField (`style`, `twClassName`, `testID`, `children`, `accessible`, and all keys owned by `TextFieldBaseProps`) are not passed through from this intersection. Prefer either Tailwind via `twClassName` or explicit `Box` layout props, and avoid conflicting layout when mixing both.
Comment thread
brianacnguyen marked this conversation as resolved.
Outdated

```tsx
import { TextField } from '@metamask/design-system-react-native';

<TextField
value=""
placeholder="Search"
accessibilityHint="Searches tokens by name or address"
/>;
```

### `twClassName`

Use the `twClassName` prop to add Tailwind CSS classes to the container. These classes will be merged with the component's default classes using `twMerge`, allowing you to:
Use the `twClassName` prop to add Tailwind CSS classes to the component. These classes will be merged with the component's default classes using `twMerge`, allowing you to:

- Add new styles that don't exist in the default component
- Override the component's default styles when needed
Expand All @@ -108,16 +249,20 @@ Use the `twClassName` prop to add Tailwind CSS classes to the container. These c
```tsx
import { TextField } from '@metamask/design-system-react-native';

// Add additional styles (avoid layout/height changes without design system review)
// Add additional styles
<TextField value="" twClassName="rounded-lg" placeholder="With extra rounding" />

// Override default styles
<TextField value="" twClassName="bg-error-default" placeholder="Override background" />
<TextField
value=""
twClassName="bg-error-default"
placeholder="Override background"
/>
```

### `style`

Use the `style` prop to customize the container's appearance with React Native styles. For consistent styling, prefer using `twClassName` with Tailwind classes when possible. Use `style` with `tw.style()` for conditionals or dynamic values.
Use the `style` prop to customize the component's appearance with React Native styles. For consistent styling, prefer using `twClassName` with Tailwind classes when possible. Use `style` with `tw.style()` for conditionals or dynamic values.

| TYPE | REQUIRED | DEFAULT |
| ---------------------- | -------- | ----------- |
Expand Down
Loading
Loading