-
-
Notifications
You must be signed in to change notification settings - Fork 11
[1/N] feat: ModalOverlay migration (extension)
#1120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
13ac07b
99b6ee6
badcc46
61e057a
0b6252a
2fd397b
49c77bf
9dae9ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { Button, ButtonVariant } 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<HTMLDivElement>) => 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. blocking: This README links to |
||
|
|
||
| ## 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'; |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: I just merged #1034 so
ButtonVariantis a shared const, so per.cursor/rules/component-architecture.mdit should come from@metamask/design-system-sharedrather than through a sibling component export.