From 68f928c8a2de6ef6a5503381079e48f007600b0b Mon Sep 17 00:00:00 2001 From: George Marshall Date: Wed, 22 Mar 2023 17:17:19 -0700 Subject: [PATCH] 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 --- .../component-library-components.scss | 1 + ui/components/component-library/index.js | 1 + .../modal-content/README.mdx | 115 ++++++++++++++ .../__snapshots__/modal-content.test.tsx.snap | 11 ++ .../component-library/modal-content/index.ts | 3 + .../modal-content/modal-content.scss | 17 +++ .../modal-content/modal-content.stories.tsx | 144 ++++++++++++++++++ .../modal-content/modal-content.test.tsx | 39 +++++ .../modal-content/modal-content.tsx | 37 +++++ .../modal-content/modal-content.types.ts | 40 +++++ 10 files changed, 408 insertions(+) create mode 100644 ui/components/component-library/modal-content/README.mdx create mode 100644 ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap create mode 100644 ui/components/component-library/modal-content/index.ts create mode 100644 ui/components/component-library/modal-content/modal-content.scss create mode 100644 ui/components/component-library/modal-content/modal-content.stories.tsx create mode 100644 ui/components/component-library/modal-content/modal-content.test.tsx create mode 100644 ui/components/component-library/modal-content/modal-content.tsx create mode 100644 ui/components/component-library/modal-content/modal-content.types.ts diff --git a/ui/components/component-library/component-library-components.scss b/ui/components/component-library/component-library-components.scss index 6a10d8f99..618b089ad 100644 --- a/ui/components/component-library/component-library-components.scss +++ b/ui/components/component-library/component-library-components.scss @@ -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'; diff --git a/ui/components/component-library/index.js b/ui/components/component-library/index.js index fbf0282f8..e1b2f1408 100644 --- a/ui/components/component-library/index.js +++ b/ui/components/component-library/index.js @@ -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'; diff --git a/ui/components/component-library/modal-content/README.mdx b/ui/components/component-library/modal-content/README.mdx new file mode 100644 index 000000000..3d755d954 --- /dev/null +++ b/ui/components/component-library/modal-content/README.mdx @@ -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 + + + + + +## Props + +The `ModalContent` accepts all props below as well as all [Box](/docs/components-ui-box--default-story#props) component props + + + +### Children + +Use the `children` prop to render the content of `ModalContent` + + + + + +```jsx +import { ModalContent, Text } from '../../component-library'; + + + + + 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. + +; +``` + +### 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. + + + + + +```jsx +import { BLOCK_SIZES } from '../../../helpers/constants/design-system'; +import { ModalContent,s Text } from '../../component-library'; + + + ModalContentSize.Sm default and only size 360px max-width + + + + Using width Box props and responsive array props
[ + BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF, + BLOCK_SIZES.ONE_THIRD, ] +
+
+ + Adding a className and setting a max width (max-width: 800px) + +``` + +### 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. + + + + + +```jsx +import React, { useEffect, useRef, useState } from 'react'; +import { ModalContent, Text } from '../../component-library'; + +const [show, setShow] = useState(false); +const modalContentRef = useRef(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); + }; +}, []); + + +{show && ( + + Click outside of this ModalContent to close + +)} +``` diff --git a/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap b/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap new file mode 100644 index 000000000..c8fa28c96 --- /dev/null +++ b/ui/components/component-library/modal-content/__snapshots__/modal-content.test.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ModalContent should match snapshot 1`] = ` +
+
+ test +
+
+`; diff --git a/ui/components/component-library/modal-content/index.ts b/ui/components/component-library/modal-content/index.ts new file mode 100644 index 000000000..ef6fb6595 --- /dev/null +++ b/ui/components/component-library/modal-content/index.ts @@ -0,0 +1,3 @@ +export { ModalContent } from './modal-content'; +export { ModalContentSize } from './modal-content.types'; +export type { ModalContentProps } from './modal-content.types'; diff --git a/ui/components/component-library/modal-content/modal-content.scss b/ui/components/component-library/modal-content/modal-content.scss new file mode 100644 index 000000000..3c3bc00fb --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.scss @@ -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; +} diff --git a/ui/components/component-library/modal-content/modal-content.stories.tsx b/ui/components/component-library/modal-content/modal-content.stories.tsx new file mode 100644 index 000000000..020e17026 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.stories.tsx @@ -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; + +const Template: ComponentStory = (args) => ( + +); + +export const DefaultStory = Template.bind({}); +DefaultStory.storyName = 'Default'; + +/* + * !!TODO: Replace with ModalHeader component + */ +const ModalHeader = () => ( + <> + + + + Modal Header + + + + +); + +export const Children: ComponentStory = (args) => ( + + + + 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. + + +); + +export const Size: ComponentStory = (args) => ( + <> + + ModalContentSize.Sm default and only size 360px max-width + + + + Using width Box props and responsive array props
[ + BLOCK_SIZES.FULL, BLOCK_SIZES.THREE_FOURTHS, BLOCK_SIZES.HALF, + BLOCK_SIZES.ONE_THIRD, ] +
+
+ + Adding a className and setting a max width (max-width: 800px) + + +); + +export const ModalContentRef: ComponentStory = (args) => { + const [show, setShow] = useState(false); + const modalContentRef = useRef(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 ( + <> + + {show && ( + + Click outside of this ModalContent to close + + )} + + ); +}; diff --git a/ui/components/component-library/modal-content/modal-content.test.tsx b/ui/components/component-library/modal-content/modal-content.test.tsx new file mode 100644 index 000000000..64b6a96df --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.test.tsx @@ -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(test); + expect(getByText('test')).toBeDefined(); + expect(getByText('test')).toHaveClass('mm-modal-content'); + }); + it('should match snapshot', () => { + const { container } = render(test); + expect(container).toMatchSnapshot(); + }); + it('should render with and additional className', () => { + const { getByText } = render( + test, + ); + expect(getByText('test')).toHaveClass('test-class'); + }); + it('should render with size sm', () => { + const { getByText } = render( + <> + default + sm + , + ); + 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(); + render(test); + expect(ref.current).toBeDefined(); + }); +}); diff --git a/ui/components/component-library/modal-content/modal-content.tsx b/ui/components/component-library/modal-content/modal-content.tsx new file mode 100644 index 000000000..064a4b4e7 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.tsx @@ -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) => ( + + {children} + +); diff --git a/ui/components/component-library/modal-content/modal-content.types.ts b/ui/components/component-library/modal-content/modal-content.types.ts new file mode 100644 index 000000000..801a04b36 --- /dev/null +++ b/ui/components/component-library/modal-content/modal-content.types.ts @@ -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; +}