1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Adding ModalContent component (#18175)

* Adding ModalContent component

* Using different component api for ref

* use imperative handle

* Updating size

* Updating stories and docs as well as component api

* Fixing import
This commit is contained in:
George Marshall 2023-03-22 17:17:19 -07:00 committed by GitHub
parent 349c9d4a03
commit 68f928c8a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 408 additions and 0 deletions

View File

@ -29,3 +29,4 @@
@import 'form-text-field/form-text-field';
@import 'banner-alert/banner-alert';
@import 'banner-tip/banner-tip';
@import 'modal-content/modal-content';

View File

@ -31,6 +31,7 @@ export { Text, TEXT_DIRECTIONS, INVISIBLE_CHARACTER } from './text';
export { Input, INPUT_TYPES } from './input';
export { TextField, TEXT_FIELD_TYPES, TEXT_FIELD_SIZES } from './text-field';
export { TextFieldSearch } from './text-field-search';
export { ModalContent, ModalContentSize } from './modal-content';
// Molecules
export { BannerBase } from './banner-base';

View File

@ -0,0 +1,115 @@
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { ModalContent } from './modal-content';
# ModalContent
`ModalContent` is the container for the modal dialog's content
<Canvas>
<Story id="components-componentlibrary-modalcontent--default-story" />
</Canvas>
## Props
The `ModalContent` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props
<ArgsTable of={ModalContent} />
### Children
Use the `children` prop to render the content of `ModalContent`
<Canvas>
<Story id="components-componentlibrary-modalcontent--children" />
</Canvas>
```jsx
import { ModalContent, Text } from '../../component-library';
<ModalContent>
<ModalHeader />
<Text>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio,
reiciendis assumenda dolorum mollitia saepe, optio at aliquam molestias
omnis quae corporis nesciunt natus, quas tempore ut ullam eaque fuga. Velit.
</Text>
</ModalContent>;
```
### Size
Currently the `ModalContent` supports a single size, this decision was made after we ran an audit on all modal sizes in the extension codebase and found that all modals could be made to fit the `ModalContentSize.Sm`(360px) size.
If you do require a larger modal size you can use the Box props or add a className to override the default size.
<Canvas>
<Story id="components-componentlibrary-modalcontent--size" />
</Canvas>
```jsx
import { BLOCK_SIZES } from '../../../helpers/constants/design-system';
import { ModalContent,s Text } from '../../component-library';
<ModalContent {...args} marginBottom={4}>
<Text>ModalContentSize.Sm default and only size 360px max-width</Text>
</ModalContent>
<ModalContent
{...args}
width={[
BLOCK_SIZES.FULL,
BLOCK_SIZES.THREE_FOURTHS,
BLOCK_SIZES.HALF,
BLOCK_SIZES.ONE_THIRD,
]}
marginBottom={4}
>
<Text>
Using width Box props and responsive array props <br /> [
BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF,
BLOCK_SIZES.ONE_THIRD, ]
</Text>
</ModalContent>
<ModalContent {...args} marginBottom={4} style={{ maxWidth: 800 }}>
Adding a className and setting a max width (max-width: 800px)
</ModalContent>
```
### Modal Content Ref
Use the `modalContentRef` prop to pass a ref to the `ModalContent` component. This is primarily used with the `closeOnOutsideClick` prop on the `Modal` component. It allows the `Modal` to close when the user clicks outside of the `ModalContent` component.
<Canvas>
<Story id="components-componentlibrary-modalcontent--modal-content-ref" />
</Canvas>
```jsx
import React, { useEffect, useRef, useState } from 'react';
import { ModalContent, Text } from '../../component-library';
const [show, setShow] = useState(false);
const modalContentRef = useRef<HTMLDivElement>(null);
const handleClickOutside = (event: MouseEvent) => {
if (
modalContentRef?.current &&
!modalContentRef.current.contains(event.target as Node)
) {
setShow(false);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
<Button onClick={() => setShow(true)}>Show ModalContent</Button>
{show && (
<ModalContent modalContentRef={modalContentRef}>
Click outside of this ModalContent to close
</ModalContent>
)}
```

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalContent should match snapshot 1`] = `
<div>
<div
class="box mm-modal-content mm-modal-content--size-sm box--padding-4 box--flex-direction-row box--width-full box--background-color-background-default box--rounded-lg"
>
test
</div>
</div>
`;

View File

@ -0,0 +1,3 @@
export { ModalContent } from './modal-content';
export { ModalContentSize } from './modal-content.types';
export type { ModalContentProps } from './modal-content.types';

View File

@ -0,0 +1,17 @@
.mm-modal-content {
--modal-content-size: var(--size, 360px);
// Currently there is only use case for one size of ModalContent in the extension
// See audit https://www.figma.com/file/hxYqloYgmVcgsoiVqmGZ8K/Modal-Audit?node-id=481%3A244&t=XITeuRB1pRc09hiG-1
// Not to say there won't be more in the future, but to prevent redundant code there is only one for now
&--size-sm {
--size: 360px;
max-width: var(--modal-content-size);
}
position: relative;
box-shadow: var(--shadow-size-lg) var(--color-shadow-default);
max-height: calc(100% - 32px); // allow for 16px padding on top and bottom
overflow: auto;
}

View File

@ -0,0 +1,144 @@
import React, { useEffect, useRef, useState } from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import {
DISPLAY,
JustifyContent,
AlignItems,
BLOCK_SIZES,
TextVariant,
TEXT_ALIGN,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box';
import { Button, Text } from '..';
import { ModalContent } from './modal-content';
import { ModalContentSize } from './modal-content.types';
import README from './README.mdx';
export default {
title: 'Components/ComponentLibrary/ModalContent',
component: ModalContent,
parameters: {
docs: {
page: README,
},
},
argTypes: {
className: {
control: 'text',
},
children: {
control: 'text',
},
size: {
control: 'select',
options: Object.values(ModalContentSize).map((value) =>
value.toLowerCase(),
),
},
},
args: {
children: 'Modal Content',
},
} as ComponentMeta<typeof ModalContent>;
const Template: ComponentStory<typeof ModalContent> = (args) => (
<ModalContent {...args} />
);
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';
/*
* !!TODO: Replace with ModalHeader component
*/
const ModalHeader = () => (
<>
<Box
className="mm-modal-header"
display={DISPLAY.FLEX}
justifyContent={JustifyContent.spaceBetween}
alignItems={AlignItems.flexStart}
width={BLOCK_SIZES.FULL}
marginBottom={4}
>
<button>Back</button>
<Text variant={TextVariant.headingSm} textAlign={TEXT_ALIGN.CENTER}>
Modal Header
</Text>
<button>Close</button>
</Box>
</>
);
export const Children: ComponentStory<typeof ModalContent> = (args) => (
<ModalContent {...args}>
<ModalHeader />
<Text>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Distinctio,
reiciendis assumenda dolorum mollitia saepe, optio at aliquam molestias
omnis quae corporis nesciunt natus, quas tempore ut ullam eaque fuga.
Velit.
</Text>
</ModalContent>
);
export const Size: ComponentStory<typeof ModalContent> = (args) => (
<>
<ModalContent {...args} marginBottom={4}>
<Text>ModalContentSize.Sm default and only size 360px max-width</Text>
</ModalContent>
<ModalContent
{...args}
width={[
BLOCK_SIZES.FULL,
BLOCK_SIZES.THREE_FOURTHS,
BLOCK_SIZES.HALF,
BLOCK_SIZES.ONE_THIRD,
]}
marginBottom={4}
>
<Text>
Using width Box props and responsive array props <br /> [
BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF,
BLOCK_SIZES.ONE_THIRD, ]
</Text>
</ModalContent>
<ModalContent {...args} marginBottom={4} style={{ maxWidth: 800 }}>
Adding a className and setting a max width (max-width: 800px)
</ModalContent>
</>
);
export const ModalContentRef: ComponentStory<typeof ModalContent> = (args) => {
const [show, setShow] = useState(false);
const modalContentRef = useRef<HTMLDivElement>(null);
const handleClickOutside = (event: MouseEvent) => {
if (
modalContentRef?.current &&
!modalContentRef.current.contains(event.target as Node)
) {
setShow(false);
}
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<>
<Button onClick={() => setShow(true)}>Show ModalContent</Button>
{show && (
<ModalContent {...args} modalContentRef={modalContentRef}>
Click outside of this ModalContent to close
</ModalContent>
)}
</>
);
};

View File

@ -0,0 +1,39 @@
/* eslint-disable jest/require-top-level-describe */
import { render } from '@testing-library/react';
import React from 'react';
import { ModalContent } from './modal-content';
import { ModalContentSize } from './modal-content.types';
describe('ModalContent', () => {
it('should render with text inside the ModalContent', () => {
const { getByText } = render(<ModalContent>test</ModalContent>);
expect(getByText('test')).toBeDefined();
expect(getByText('test')).toHaveClass('mm-modal-content');
});
it('should match snapshot', () => {
const { container } = render(<ModalContent>test</ModalContent>);
expect(container).toMatchSnapshot();
});
it('should render with and additional className', () => {
const { getByText } = render(
<ModalContent className="test-class">test</ModalContent>,
);
expect(getByText('test')).toHaveClass('test-class');
});
it('should render with size sm', () => {
const { getByText } = render(
<>
<ModalContent>default</ModalContent>
<ModalContent size={ModalContentSize.Sm}>sm</ModalContent>
</>,
);
expect(getByText('sm')).toHaveClass('mm-modal-content--size-sm');
expect(getByText('default')).toHaveClass('mm-modal-content--size-sm');
});
it('should render with a ref', () => {
const ref = React.createRef<HTMLDivElement>();
render(<ModalContent modalContentRef={ref}>test</ModalContent>);
expect(ref.current).toBeDefined();
});
});

View File

@ -0,0 +1,37 @@
import React from 'react';
import classnames from 'classnames';
import {
BackgroundColor,
BorderRadius,
BLOCK_SIZES,
} from '../../../helpers/constants/design-system';
import Box from '../../ui/box/box';
import { ModalContentProps, ModalContentSize } from './modal-content.types';
export const ModalContent = ({
className = '',
children,
size = ModalContentSize.Sm,
width,
modalContentRef, // Would have preferred to forwardRef but it's not trivial in TypeScript. Will update once we have an established pattern
...props
}: ModalContentProps) => (
<Box
className={classnames(
'mm-modal-content',
{ [`mm-modal-content--size-${size}`]: !width },
className,
)}
backgroundColor={BackgroundColor.backgroundDefault}
borderRadius={BorderRadius.LG}
width={width || BLOCK_SIZES.FULL}
padding={4}
ref={modalContentRef}
{...props}
>
{children}
</Box>
);

View File

@ -0,0 +1,40 @@
import React from 'react';
import type { BoxProps, BoxWidth, BoxWidthArray } from '../../ui/box/box.d';
import { Size } from '../../../helpers/constants/design-system';
/*
* ModalContent sizes
* Currently there is only use case for one size of ModalContent in the extension
* See audit https://www.figma.com/file/hxYqloYgmVcgsoiVqmGZ8K/Modal-Audit?node-id=481%3A244&t=XITeuRB1pRc09hiG-1
* Not to say there won't be more in the future, but to prevent redundant code there is only one for now
*/
export enum ModalContentSize {
Sm = Size.SM,
}
export interface ModalContentProps extends BoxProps {
/**
* The additional className of the ModalContent component
*/
className?: string;
/**
* The content of the ModalContent component
*/
children?: React.ReactNode;
/**
* The size of ModalContent
* Currently only one size is supported ModalContentSize.Sm 360px
* See docs for more info
*/
size?: ModalContentSize;
/**
* To override the default width of the ModalContent component
* Accepts all BLOCK_SIZES from design-system
*/
width?: BoxWidth | BoxWidthArray;
/**
* The ref of the ModalContent component
* Used with Modal and closeOnOutsideClick prop
*/
modalContentRef?: React.RefObject<HTMLElement>;
}