Skip to content

feat(DSYS-488): Migrate Input to ADR-0003 and ADR-0004#1043

Merged
georgewrmarshall merged 8 commits intomainfrom
cursor/enum-shared-type-migration-6cf4
May 1, 2026
Merged

feat(DSYS-488): Migrate Input to ADR-0003 and ADR-0004#1043
georgewrmarshall merged 8 commits intomainfrom
cursor/enum-shared-type-migration-6cf4

Conversation

@cursor
Copy link
Copy Markdown
Contributor

@cursor cursor Bot commented Apr 6, 2026

Description

This PR migrates Input to align with ADR-0003 (String Unions) and ADR-0004 (Centralized Types Architecture) as part of epic DSYS-468.

It also folds in the breaking API alignment work we decided to land with this migration instead of deferring:

  • Input is now controlled-only on both React and React Native
  • isReadonly is renamed to isReadOnly
  • isStateStylesDisabled is available on both platforms

Changes

Shared types

In @metamask/design-system-shared:

  • TextVariant is centralized as the ADR-0003 const-object + string-union export used by both platforms
  • InputPropsShared is added and exported from the shared package root
  • InputPropsShared now contains the shared cross-platform Input contract:
    • value: string
    • textVariant
    • isDisabled
    • isReadOnly
    • isStateStylesDisabled

React Input

In @metamask/design-system-react:

  • Input.types.ts now extends InputPropsShared
  • defaultValue is removed from the public Input type surface
  • readOnly / isReadonly alignment is normalized to isReadOnly
  • isStateStylesDisabled now suppresses the input's own focus/disabled state classes
  • Storybook stories were updated so editable examples use a controlled wrapper instead of frozen value props
  • README and tests were updated for the controlled-only API

React Native Input

In @metamask/design-system-react-native:

  • Input.types.ts now extends InputPropsShared
  • Input remains controlled-only and now shares the same readonly/state-style prop names as web
  • Native focus handling was simplified so disabled/read-only behavior is owned by TextInput.editable, while local focused state is explicitly cleared when the input becomes disabled or read-only
  • README, migration docs, and tests were updated to match the final API and behavior

Breaking changes

Consumers should expect these Input API changes:

  • value is required on web Input; uncontrolled defaultValue usage is no longer part of the public type contract
  • isReadonly is renamed to isReadOnly
  • isStateStylesDisabled is now the shared prop name across both platforms

Related issues

Fixes: DSYS-488

Manual testing steps

  1. Run yarn build
  2. Run yarn lint
  3. Run yarn test
  4. Run yarn storybook
  5. Run yarn test:storybook
  6. Verify TextVariant is importable from @metamask/design-system-shared
  7. Verify InputPropsShared is importable from @metamask/design-system-shared
  8. Verify React Input stories remain editable where intended
  9. Verify native Input clears focused styling when it becomes disabled or read-only

Coverage note

The native Input branch coverage gap from the old disabled focus guards is resolved without istanbul ignore comments. Input.tsx was reworked so the disabled/read-only behavior is simpler and the resulting focused coverage run for that file is 100% for statements, branches, functions, and lines.

Screenshots/Recordings

After

after720.mov

Pre-merge author checklist

  • I've followed MetaMask Contributor Docs
  • I've completed the PR template to the best of my ability
  • I've included tests if applicable
  • I've documented my code using JSDoc format if applicable
  • I've applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Introduces breaking API changes to Input across web and native (controlled-only value, isReadonlyisReadOnly, new isStateStylesDisabled) and tweaks native focus handling, which can impact downstream form wrappers and styling.

Overview
Aligns Input across @metamask/design-system-react and @metamask/design-system-react-native to a new shared InputPropsShared contract exported from @metamask/design-system-shared.

Breaking API alignment: web Input is now controlled-only (public types drop defaultValue and require value), isReadonly is renamed to isReadOnly, and isStateStylesDisabled is supported on both platforms (suppresses focus/disabled styling).

Updates docs, stories, and tests to match the new contract, and adjusts React Native Input focus state handling to clear focused styling when the input becomes disabled or read-only.

Reviewed by Cursor Bugbot for commit 554ea6e. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 6, 2026

📖 Storybook Preview

@georgewrmarshall
Copy link
Copy Markdown
Contributor

@cursoragent rebase and resolve all conflicts now other components have moved to shared double check to make sure all imports are coming from design-system-shared and not ../../types

@cursor
Copy link
Copy Markdown
Contributor Author

cursor Bot commented Apr 28, 2026

  • Rebased this branch onto latest main and resolved all conflicts from recent shared-type migrations (Text, Icon centralization, AvatarAccount). Pushed the updated branch.

  • Audited and fixed imports so the migrated areas use shared types:

    • React + RN Text now import FontFamily, FontStyle, FontWeight, TextColor, and TextVariant from @metamask/design-system-shared (not ../../types).
    • React + RN Input types now consume InputPropsShared from shared.
    • Cleaned packages/design-system-shared/src/index.ts to remove duplicate TextVariant/InputPropsShared re-exports that caused TS duplicate identifier errors.
  • Verified repo health: enabled Corepack/Yarn 4, ran yarn install, yarn build (success), and yarn lint (clean).

Open in Web Open in Cursor 

@georgewrmarshall
Copy link
Copy Markdown
Contributor

@cursoragent well that was a massive fail. You've made a bad rebase and now have over 16,000 code changes in this PR

@cursor
Copy link
Copy Markdown
Contributor Author

cursor Bot commented Apr 28, 2026

Acknowledged. I corrected the bad rebase and reduced the PR surface:

  • Reset the work to align with latest main, then re-applied only the minimal Input migration:
    • Added InputPropsShared in @metamask/design-system-shared and exported it from packages/design-system-shared/src/index.ts.
    • Updated @metamask/design-system-react and @metamask/design-system-react-native Input.types.ts to consume InputPropsShared.
  • Verified builds using Yarn 4/Corepack; monorepo builds succeed.
  • Pushed a clean, minimal branch with just the Input changes: cursor/temp-reapply-input-shared-86ff (no unrelated churn).

If you want me to force-correct this PR branch to the minimal diff, I can either:

  • Hard-reset the existing PR branch to the minimal changes, or
  • Convert this temp branch into the updated PR target.
Open in Web Open in Cursor 

@georgewrmarshall georgewrmarshall force-pushed the cursor/enum-shared-type-migration-6cf4 branch from 73f8e67 to 3b2e5ea Compare April 30, 2026 20:00
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor Author

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 966c791. Configure here.

Comment thread packages/design-system-react/src/components/Input/Input.stories.tsx
@georgewrmarshall georgewrmarshall force-pushed the cursor/enum-shared-type-migration-6cf4 branch from 966c791 to 7e10637 Compare April 30, 2026 21:16
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall force-pushed the cursor/enum-shared-type-migration-6cf4 branch from 657178a to 11f4ac2 Compare April 30, 2026 21:43
@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

@georgewrmarshall georgewrmarshall self-assigned this Apr 30, 2026
@georgewrmarshall georgewrmarshall marked this pull request as ready for review April 30, 2026 22:22
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner April 30, 2026 22:22
expect(input).toHaveStyle(tw`border-primary-default`);
});

it('clears focused state when input becomes disabled', () => {
Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall Apr 30, 2026

Choose a reason for hiding this comment

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

This focused-then-disabled test captures the user-visible transition that justified the refactor. It verifies the field does not keep stale focused styling after becoming disabled.

);

/* istanbul ignore next: handler body covered by focus/blur tests */
useEffect(() => {
Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall Apr 30, 2026

Choose a reason for hiding this comment

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

This is an architectural improvement, not just a test fix. @brianacnguyen We previously relied on istanbul ignore around the old disabled focus guards, which made the file look fully covered without exercising the unreachable branch. I would rather lower coverage on an individual component in that situation so the reported coverage stays honest and leaves us room to come back and refactor the code into something genuinely reachable and testable, which is what this change does.

import type { InputPropsShared } from '@metamask/design-system-shared';
import type { TextInputProps } from 'react-native';

export type InputProps = Omit<
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.

This keeps the shared contract limited to cross-platform Input props and leaves RN-only TextInput behavior in the platform layer. The split is important because the event and base-prop surfaces still differ meaningfully between native and web.

# Input

Input is a light-weight, controlled-only borderless input used inside of TextField.
Input is a light-weight, controlled-only borderless input for use inside of `TextField`. Use `TextField` instead when you need labels, error states, or accessories.
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.

The README now frames Input as a controlled, foundation-level primitive rather than a full field abstraction. That should help first-time reviewers and consumers understand why TextField remains the recommended public entry point for labels, errors, and accessories.

* Input component shared props (ADR-0004)
* Platform-independent properties shared across React and React Native
*/
export type InputPropsShared = {
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.

The shared type is intentionally narrow: only the props that are semantically the same on both platforms moved here. Event handlers and platform base props stayed out of shared because that API convergence belongs at the future TextField layer, not in the low-level primitive.

import type { InputProps } from './Input.types';
import README from './README.mdx';

function ControlledInput(props: InputProps) {
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.

The local ControlledInput wrapper keeps editable stories interactive after making value part of the required public contract. Without it, the examples would render frozen inputs and hide the intended behavior from reviewers.

import type { InputPropsShared } from '@metamask/design-system-shared';
import type { ComponentPropsWithoutRef } from 'react';

export type InputProps = Omit<
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.

Removing defaultValue and value from the inherited DOM props is the type-level part of the controlled-only change on web. This keeps consumers from accidentally mixing the old uncontrolled browser surface back into the shared API.

// TextField types (ADR-0004)
export { type TextFieldPropsShared } from './types/TextField';

// Input types (ADR-0004)
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.

This root export matters for adoption more than implementation detail. It lets consumers and wrapper components import the shared Input contract the same way they already import other ADR-0004 shared types.

@@ -0,0 +1 @@
export { type InputPropsShared } from './Input.types';
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.

This barrel keeps Input aligned with the shared-type export pattern used throughout the package. It makes the new type discoverable from both the feature folder and the shared package root.


## Props

### `value`
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.

Calling out value as required in the README is important because this is a breaking change from the old web usage pattern. The docs now match the stories and tests, so first-time reviewers can see the controlled-only contract without reading the type definitions.

@@ -8,7 +8,7 @@ const TEST_ID = 'input';

describe('Input', () => {
it('renders with default props', () => {
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.

These test updates are mostly contract-locking for the breaking change: web Input is now treated as controlled-only, so every test passes value explicitly. That makes the new API surface visible in the spec instead of only in the type file.

@@ -23,11 +24,13 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
) => {
const mergedClassName = twMerge(
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.

isStateStylesDisabled is intentionally suppressing the input chrome for focus and disabled states on web so TextField can own those states without double styling. This is the same composition role the native implementation already relied on.

}

describe('Input', () => {
const tw = renderHook(() => useTailwind()).result.current;
Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall Apr 30, 2026

Choose a reason for hiding this comment

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

This rewrite is intentionally moving the native Input tests away from implementation-detail helpers and toward contract-level assertions. Using built-in matchers like toHaveStyle, toBeDisabled, and toBeOnTheScreen keeps the tests closer to real usage and makes the coverage signal more trustworthy.


## Version Updates

### From version 0.X.0 to 0.X.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.

This placeholder section is intentionally staging unreleased Input migration guidance without rewriting shipped 0.22.0 history. The explicit 0.X.0 marker should also make the release-prep version update easy to spot when this PR is included in a real package release.

@github-actions
Copy link
Copy Markdown
Contributor

📖 Storybook Preview

Copy link
Copy Markdown
Contributor

@brianacnguyen brianacnguyen left a comment

Choose a reason for hiding this comment

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

1 small change

onBlur?.(e);
}
setIsFocused(false);
onBlur?.(e);
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.

We probably still wait to not focus when it’s disabled no?

Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall May 1, 2026

Choose a reason for hiding this comment

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

I don’t think we need the extra isDisabled guard in the handlers because native TextInput already owns the interaction blocking through editable={!isDisabled && !isReadOnly}. The remaining responsibility here is just local state correctness: if the field is already focused and then becomes disabled or read-only, we still need to clear isFocused so we don’t keep stale focused styling after the native control stops being editable.

@georgewrmarshall georgewrmarshall enabled auto-merge (squash) May 1, 2026 22:17
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

📖 Storybook Preview

@georgewrmarshall georgewrmarshall merged commit 7267e40 into main May 1, 2026
44 checks passed
@georgewrmarshall georgewrmarshall deleted the cursor/enum-shared-type-migration-6cf4 branch May 1, 2026 22:21
georgewrmarshall added a commit that referenced this pull request May 5, 2026
## Release 38.0.0

This release updates the shared, web, native, tokens, and Tailwind
packages with new cross-platform input and avatar-group contracts, new
modal building blocks for React, a breaking React Native Toast API
redesign, and Tailwind CSS v4 support for web consumers.

### 📦 Package Versions

- `@metamask/design-system-shared`: **0.16.0**
- `@metamask/design-system-react`: **0.21.0**
- `@metamask/design-system-react-native`: **0.23.0**
- `@metamask/design-system-tailwind-preset`: **0.7.0**
- `@metamask/design-tokens`: **8.4.0**

### 🔄 Shared Type Updates (0.16.0)

#### Input and AvatarGroup shared contracts
([#1043](#1043),
[#1067](#1067))

**What Changed:**

- Added shared `Input` contracts for controlled `value`, `isReadOnly`,
and `isStateStylesDisabled`
- Added shared `AvatarGroup` size, variant, and prop contracts
- Added the shared `Merge` icon export
([#1155](#1155))

**Impact:**

- React and React Native consumers can build against one aligned input
and avatar-group API surface
- Cross-platform wrappers can depend on `@metamask/design-system-shared`
instead of maintaining platform-specific type assumptions

### 🌐 React Web Updates (0.21.0)

#### Added

- Added `ModalOverlay`, `ModalBody`, `ModalFocus`, and `ModalFooter` to
support Extension modal migrations into `@metamask/design-system-react`
([#1120](#1120),
[#1121](#1121),
[#1128](#1128),
[#1132](#1132))
- Added the `Merge` icon to the React icon set
([#1155](#1155))

#### Changed

- Updated `Input` to follow the shared controlled input API and use
`isReadOnly` as the public readonly prop name
([#1043](#1043))
- Updated `AvatarGroup` to use shared cross-platform size and variant
contracts
([#1067](#1067))

### 📱 React Native Updates (0.23.0)

#### Added

- Added the `Merge` icon to the React Native icon set
([#1155](#1155))

#### Changed

- **BREAKING:** Redesigned `Toast` to use a single mounted `<Toast />`
plus static `Toast.show(...)` and `Toast.hide()` methods for application
usage
([#1104](#1104))
- Removed `ToastContext`, `ToastContextWrapper`, and
`ToastContextParams` from the public export surface
- Renamed `ToastVariants` to `ToastVariant`, changed icon-only close
buttons to `ToastCloseButtonVariant.Icon`, and renamed
`customBottomOffset` to `bottomOffset`
- `Toast.show()` and `Toast.hide()` now throw a descriptive error if
called before `<Toast />` mounts
- See the [React Native Migration
Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0220-to-0230)
- Updated `Input` to follow the shared controlled input API and rename
`isReadonly` to `isReadOnly`
([#1043](#1043))
- Updated `AvatarGroup` to use shared cross-platform size and variant
contracts
([#1067](#1067))

### 🎨 Tokens and Tailwind Updates

#### `@metamask/design-tokens` 8.4.0

- Added `@metamask/design-tokens/tailwind/theme.css` for Tailwind CSS v4
consumers, providing a single import for token variables, theme values,
typography, fonts, and shadow utilities
([#1117](#1117))

#### `@metamask/design-system-tailwind-preset` 0.7.0

- Added a `fade-in` animation utility so consumers can use
`animate-fade-in` for simple opacity entrance transitions, including the
new `ModalOverlay` web migration path
([#1120](#1120))
- Clarified that Tailwind CSS v3 consumers should keep using this
preset, while Tailwind CSS v4 consumers should migrate to
`@metamask/design-tokens/tailwind/theme.css`
([#1117](#1117))

### ⚠️ Breaking Changes

#### Toast API redesign (React Native)

**What Changed:**

- `Toast` application usage moved from context/service patterns to
static `Toast.show(...)` and `Toast.hide()` methods
- `ToastContext`, `ToastContextWrapper`, and `ToastContextParams` were
removed from the public API
- `ToastVariants` was renamed to `ToastVariant`
- Icon-only close buttons now use `ToastCloseButtonVariant.Icon`
- `customBottomOffset` was renamed to `bottomOffset`

**Impact:**

- Existing `@metamask/design-system-react-native` consumers using the
old Toast context flow need import, root-mount, and call-site updates
- Existing app code must ensure `<Toast />` is mounted before invoking
`Toast.show()` / `Toast.hide()`

See migration guides for complete instructions:

- [React Native Migration
Guide](./packages/design-system-react-native/MIGRATION.md#from-version-0220-to-0230)

### ✅ Checklist

- [x] Changelogs updated with human-readable descriptions
- [x] Changelog validation passed (`yarn workspace <package>
changelog:validate`)
- [x] Version bumps follow semantic versioning
- [x] design-system-shared: minor (`0.15.0` → `0.16.0`) - new shared
`Input`, `AvatarGroup`, and icon exports
- [x] design-system-react: minor (`0.20.0` → `0.21.0`) - new modal
components, icon, and shared API alignment
- [x] design-system-react-native: minor (`0.22.0` → `0.23.0`) - breaking
Toast redesign, icon, and shared API alignment
- [x] design-system-tailwind-preset: minor (`0.6.1` → `0.7.0`) - new
`fade-in` utility and Tailwind CSS v4 migration guidance
- [x] design-tokens: minor (`8.3.0` → `8.4.0`) - Tailwind CSS v4
`theme.css` export
- [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**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs)
- [ ] 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**

- [ ] I've reviewed the [Reviewing Release
PRs](./docs/reviewing-release-prs.md) guide
- [ ] Package versions follow semantic versioning
- [ ] Changelog entries are consumer-facing (not commit message
regurgitation)
- [ ] Breaking changes are documented in MIGRATION.md with examples
- [ ] All unreleased changes are accounted for in changelogs
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