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:
parent
349c9d4a03
commit
68f928c8a2
@ -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';
|
||||
|
@ -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';
|
||||
|
115
ui/components/component-library/modal-content/README.mdx
Normal file
115
ui/components/component-library/modal-content/README.mdx
Normal 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>
|
||||
)}
|
||||
```
|
@ -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>
|
||||
`;
|
3
ui/components/component-library/modal-content/index.ts
Normal file
3
ui/components/component-library/modal-content/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { ModalContent } from './modal-content';
|
||||
export { ModalContentSize } from './modal-content.types';
|
||||
export type { ModalContentProps } from './modal-content.types';
|
@ -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;
|
||||
}
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
@ -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>
|
||||
);
|
@ -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>;
|
||||
}
|
Loading…
Reference in New Issue
Block a user