Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 61 additions & 0 deletions packages/design-system-react/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This guide provides detailed instructions for migrating your project from one ve
- [Text Component](#text-component)
- [Icon Component](#icon-component)
- [Checkbox Component](#checkbox-component)
- [ModalOverlay Component](#modaloverlay-component)
- [Version Updates](#version-updates)
- [From version 0.17.0 to 0.18.0](#from-version-0170-to-0180)
- [From version 0.16.0 to 0.17.0](#from-version-0160-to-0170)
Expand Down Expand Up @@ -1230,6 +1231,66 @@ import { Checkbox } from '@metamask/design-system-react';
- `inputProps` remains available and should be used for native input attributes such as `name`, `required`, and `title`.
- `isInvalid` is available for error-state visuals and is not part of the extension checkbox API.

### ModalOverlay Component

The extension `modal-overlay` component maps to `ModalOverlay` in the design system. The runtime API stays the same for typical usage — `<ModalOverlay />` with optional `onClick` and `className` — but the component drops the polymorphic Box surface and the legacy SCSS class hook in favor of Tailwind utilities and a token-driven fade-in animation.

Refer to [General Extension Migration Guidance](#general-extension-migration-guidance) for shared Box/style-utility migration patterns.

#### Breaking Changes

##### Import Path

| Extension Pattern | Design System Migration |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `import { ModalOverlay } from '../../component-library'` | `import { ModalOverlay } from '@metamask/design-system-react'` |
| `import type { ModalOverlayProps } from '../../component-library'` | `import type { ModalOverlayProps } from '@metamask/design-system-react'` |

##### Props and Behavior Mapping

| Extension API | Design System API | Change Type | Notes |
| ------------------------------------------------------------------- | ------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onClick?: (event: MouseEvent<HTMLDivElement>) => void` | `onClick?: (event: MouseEvent<HTMLDivElement>) => void` | unchanged | called when the overlay is clicked |
| `className?: string` | `className?: string` | unchanged | merged with default Tailwind classes via `twMerge` |
| Polymorphic `as` / `PolymorphicComponentPropWithRef<C, ...>` typing | removed | removed | always renders a `<div>`. If you need a different element, wrap or compose. |
| Box style-utility props (`backgroundColor`, `width`, `height`, …) | removed from public API | removed | the overlay renders a fixed full-viewport surface with `BoxBackgroundColor.OverlayDefault`. Override via `className` if a one-off tweak is needed. |
| `mm-modal-overlay` SCSS class hook | removed | removed | use `className` and Tailwind utilities to customize the overlay surface |
| `aria-hidden="true"` | `aria-hidden="true"` | unchanged | still applied by default |

##### Default and Behavior Changes

| Concern | Extension Behavior | Design System Behavior |
| --------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Positioning | `position: fixed; inset: 0; z-index: $modal-z-index (1050)` from SCSS | `fixed inset-0 z-[1050]` Tailwind utilities |
| Sizing | `width: 100%; height: 100%` via `BlockSize.Full` Box props | full viewport via `inset-0` (no separate width/height props) |
| Background | `BackgroundColor.overlayDefault` Box prop | `BoxBackgroundColor.OverlayDefault` applied internally; `className` to override |
| Mount animation | 250ms linear opacity fade-in via SCSS `@keyframes`, gated by `prefers-reduced-motion: no-preference` | 300ms linear opacity fade-in via the new `motion-safe:animate-fade-in` Tailwind utility (matches `AnimationDuration.Regularly` from `@metamask/design-tokens`); reduced-motion users get no animation |

#### Migration Example

##### Before (Extension)

```tsx
import { ModalOverlay } from '../../component-library';

<ModalOverlay onClick={handleClose} />;
```

##### After (Design System)

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

<ModalOverlay onClick={handleClose} />;
```

For typical call sites — for example `ui/components/multichain/network-list-menu/network-list-menu.tsx`, `ui/components/multichain/edit-accounts-modal/edit-accounts-modal.tsx`, and `ui/components/multichain/funding-method-modal/funding-method-modal.tsx` (verified via fresh grep) — the only change is the import path; the JSX stays identical.

#### API Differences

- `ModalOverlay` no longer composes Box's polymorphic API. It always renders a `<div>` and forwards arbitrary HTML attributes (`id`, `role`, `data-*`, `aria-*`, `ref`) to it.
- One-off styling that previously used Box utility props (e.g. `backgroundColor={BackgroundColor.overlayAlternative}`) should now use `className` with the equivalent Tailwind utility (e.g. `className="bg-overlay-alternative"`).

## Version Updates

This section covers version-to-version breaking changes within `@metamask/design-system-react`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ButtonVariant } from '@metamask/design-system-shared';
import type { Meta, StoryObj } from '@storybook/react-vite';
import React, { useState } from 'react';

import { Button } from '../Button';

import { ModalOverlay } from './ModalOverlay';
import type { ModalOverlayProps } from './ModalOverlay.types';
import README from './README.mdx';

const meta: Meta<ModalOverlayProps> = {
title: 'React Components/ModalOverlay',
component: ModalOverlay,
parameters: {
docs: {
page: README,
},
},
argTypes: {
className: {
control: 'text',
description:
'Optional prop for additional CSS classes to be applied to the ModalOverlay component',
},
onClick: {
action: 'onClick',
description:
'Optional click handler. Useful when used directly without Modal context to dismiss content rendered above the overlay.',
},
},
};

export default meta;

type Story = StoryObj<ModalOverlayProps>;

export const Default: Story = {
args: {},
render: (args) => <ModalOverlay {...args} />,
};

export const OnClick: Story = {
render: (args) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Button variant={ButtonVariant.Primary} onClick={() => setIsOpen(true)}>
Show modal overlay
</Button>
{isOpen && <ModalOverlay {...args} onClick={() => setIsOpen(false)} />}
</>
);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React, { createRef } from 'react';

import { ModalOverlay } from './ModalOverlay';

describe('ModalOverlay', () => {
it('renders without crashing', () => {
render(<ModalOverlay data-testid="modal-overlay" />);
expect(screen.getByTestId('modal-overlay')).toBeInTheDocument();
});

it('applies overlay positioning, z-index, and motion-safe fade-in classes', () => {
render(<ModalOverlay data-testid="modal-overlay" />);
expect(screen.getByTestId('modal-overlay')).toHaveClass(
'fixed',
'inset-0',
'z-[1050]',
'motion-safe:animate-fade-in',
);
});

it('applies the overlay-default background color', () => {
render(<ModalOverlay data-testid="modal-overlay" />);
expect(screen.getByTestId('modal-overlay')).toHaveClass(
'bg-overlay-default',
);
});

it('marks the overlay as decorative for assistive tech', () => {
render(<ModalOverlay data-testid="modal-overlay" />);
expect(screen.getByTestId('modal-overlay')).toHaveAttribute(
'aria-hidden',
'true',
);
});

it('merges custom className alongside default classes', () => {
render(<ModalOverlay data-testid="modal-overlay" className="opacity-50" />);
const overlay = screen.getByTestId('modal-overlay');
expect(overlay).toHaveClass('opacity-50');
expect(overlay).toHaveClass('fixed', 'inset-0');
});

it('fires the onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<ModalOverlay data-testid="modal-overlay" onClick={handleClick} />);
fireEvent.click(screen.getByTestId('modal-overlay'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('forwards ref to the underlying element', () => {
const ref = createRef<HTMLDivElement>();
render(<ModalOverlay ref={ref} data-testid="modal-overlay" />);
expect(ref.current).toBe(screen.getByTestId('modal-overlay'));
});

it('forwards arbitrary HTML attributes to the underlying element', () => {
render(
<ModalOverlay
data-testid="modal-overlay"
id="overlay"
role="presentation"
/>,
);
const overlay = screen.getByTestId('modal-overlay');
expect(overlay).toHaveAttribute('id', 'overlay');
expect(overlay).toHaveAttribute('role', 'presentation');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { forwardRef } from 'react';

import { twMerge } from '../../utils/tw-merge';
import { Box, BoxBackgroundColor } from '../Box';

import type { ModalOverlayProps } from './ModalOverlay.types';

export const ModalOverlay = forwardRef<HTMLDivElement, ModalOverlayProps>(
({ className, ...props }, ref) => (
<Box
ref={ref}
backgroundColor={BoxBackgroundColor.OverlayDefault}
aria-hidden="true"
className={twMerge(
'fixed inset-0 z-[1050] motion-safe:animate-fade-in',
className,
)}
{...props}
/>
),
);

ModalOverlay.displayName = 'ModalOverlay';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ComponentProps } from 'react';

export type ModalOverlayProps = ComponentProps<'div'> & {
/**
* Optional prop for additional CSS classes to be applied to the ModalOverlay component.
* These classes will be merged with the component's default classes using twMerge.
*/
className?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Controls, Canvas } from '@storybook/addon-docs/blocks';

import * as ModalOverlayStories from './ModalOverlay.stories';

# ModalOverlay

`ModalOverlay` is a transparent backdrop that covers the entire viewport. Use it to dim and isolate the page behind a modal or dialog. The component is purely presentational; pair it with `ModalContent` and the rest of the modal stack when building a full modal experience.

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

<ModalOverlay onClick={handleClose} />;
```

<Canvas of={ModalOverlayStories.Default} />

## Props

### `onClick`

Optional click handler invoked when the overlay is clicked. Useful for closing the modal when the user clicks outside its content. Not necessary when used inside `Modal` with `closeOnClickOutside` enabled.

<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: MouseEvent&lt;HTMLDivElement&gt;) =&gt; void</code>
</td>
<td align="left">No</td>
<td align="left">
<code>undefined</code>
</td>
</tr>
</tbody>
</table>

<Canvas of={ModalOverlayStories.OnClick} />

### `className`

Use the `className` prop to add custom 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

<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>

## Component API

<Controls of={ModalOverlayStories.Default} />

## Migration Guide

Migrating from `ui/components/component-library/modal-overlay` in MetaMask Extension? See the [ModalOverlay Migration Guide](../../../MIGRATION.md#modaloverlay-component) for prop mappings, before/after examples, and breaking changes.
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.

blocking: This README links to ../../../MIGRATION.md#modaloverlay-component, but packages/design-system-react/MIGRATION.md does not have a ModalOverlay section yet. Since this PR is part of the extension migration flow, can we add the ModalOverlay migration entry before merging so consumers have the actual prop mapping / before-after examples?


## References

[MetaMask Design System Guides](https://www.notion.so/MetaMask-Design-System-Guides-Design-f86ecc914d6b4eb6873a122b83c12940)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ModalOverlay } from './ModalOverlay';
export type { ModalOverlayProps } from './ModalOverlay.types';
3 changes: 3 additions & 0 deletions packages/design-system-react/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export type { JazziconProps } from './temp-components/Jazzicon';
export { Maskicon } from './temp-components/Maskicon';
export type { MaskiconProps } from './temp-components/Maskicon';

export { ModalOverlay } from './ModalOverlay';
export type { ModalOverlayProps } from './ModalOverlay';

export { Text } from './Text';
export {
TextVariant,
Expand Down
11 changes: 11 additions & 0 deletions packages/design-system-tailwind-preset/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ const tailwindConfig: Config = {
}),
...typography,
boxShadow: shadows,
keyframes: {
'fade-in': {
from: { opacity: '0' },
to: { opacity: '1' },
},
},
animation: {
// Duration matches `AnimationDuration.Regularly` from `@metamask/design-tokens`.
// Inlined to avoid a workspace dependency cycle (design-tokens → design-system-react → tailwind-preset).
'fade-in': 'fade-in 300ms linear forwards',
},
},
},
plugins: [
Expand Down
Loading