diff --git a/packages/big-design/src/components/Modal/Modal.tsx b/packages/big-design/src/components/Modal/Modal.tsx index 430b161e8..006583309 100644 --- a/packages/big-design/src/components/Modal/Modal.tsx +++ b/packages/big-design/src/components/Modal/Modal.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { createPortal } from 'react-dom'; import { uniqueId } from '../../utils'; -import { Button } from '../Button'; -import { H2, HeadingProps } from '../Typography'; +import { Button, ButtonProps } from '../Button'; +import { H2 } from '../Typography'; import { StyledModal, @@ -16,10 +16,12 @@ import { } from './styled'; export interface ModalProps { + actions?: ModalAction[]; backdrop: boolean; - isOpen: boolean; closeOnClickOutside: boolean; closeOnEscKey: boolean; + header?: string; + isOpen: boolean; variant: 'modal' | 'dialog'; onClose(): void; } @@ -29,42 +31,20 @@ interface ModalState { modalContainer: HTMLDivElement | null; } -export interface ModalActionsProps { - withBorder?: boolean; -} - -export interface ModalHeaderProps { - withBorder?: boolean; +export interface ModalAction extends Omit { + text?: string; } -const ModalActions: React.FC = ({ children, withBorder }) => ( - - {children} - -); - -const ModalHeader: React.FC = ({ children, withBorder }) => { - return ( - - {typeof children === 'string' ?

{children}

: children} -
- ); -}; - export class Modal extends React.PureComponent { static defaultProps: Partial = { backdrop: true, - isOpen: false, closeOnClickOutside: false, closeOnEscKey: true, + isOpen: false, onClose: () => null, variant: 'modal', }; - static Actions = ModalActions; - static Body = StyledModalBody; - static Header = ModalHeader; - readonly state: ModalState = { initialBodyOverflowY: '', modalContainer: null, @@ -103,20 +83,22 @@ export class Modal extends React.PureComponent { } render() { - const { backdrop, isOpen, variant } = this.props; + const { backdrop, children, isOpen, variant } = this.props; const { modalContainer } = this.state; const modalContent = ( {this.renderClose()} - {this.renderChildren()} + {this.renderHeader()} + {children} + {this.renderActions()} ); @@ -136,18 +118,33 @@ export class Modal extends React.PureComponent { ); } - private renderChildren() { - const { children } = this.props; + private renderHeader() { + const { header } = this.props; + + return ( + header && + typeof header === 'string' && ( + +

{header}

+
+ ) + ); + } - return React.Children.map(children, child => { - if (React.isValidElement(child) && child.type === Modal.Header) { - return React.cloneElement(child as React.ReactElement, { - id: this.headerUniqueId, - }); - } + private renderActions() { + const { actions } = this.props; - return child; - }); + return ( + Array.isArray(actions) && ( + + {actions.map(({ text, ...props }, index) => ( + + ))} + + ) + ); } private autoFocus = () => { diff --git a/packages/big-design/src/components/Modal/__snapshots__/spec.tsx.snap b/packages/big-design/src/components/Modal/__snapshots__/spec.tsx.snap index 6c2beccd9..57d2ac88a 100644 --- a/packages/big-design/src/components/Modal/__snapshots__/spec.tsx.snap +++ b/packages/big-design/src/components/Modal/__snapshots__/spec.tsx.snap @@ -160,6 +160,15 @@ exports[`render open modal 1`] = ` right: 0.25rem; } +.c8 { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + padding: 0 1rem; + overflow-y: auto; +} + @media (min-width:720px) { .c5 + .bd-button { margin-top: 0; @@ -200,6 +209,12 @@ exports[`render open modal 1`] = ` } } +@media (min-width:720px) { + .c8 { + padding: 0 1.5rem; + } +} + @@ -249,7 +264,11 @@ exports[`render open modal 1`] = ` - This is a modal +
+ This is a modal +
@@ -410,6 +429,15 @@ exports[`render open modal without backdrop 1`] = ` right: 0.25rem; } +.c8 { + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + padding: 0 1rem; + overflow-y: auto; +} + @media (min-width:720px) { .c5 + .bd-button { margin-top: 0; @@ -450,6 +478,12 @@ exports[`render open modal without backdrop 1`] = ` } } +@media (min-width:720px) { + .c8 { + padding: 0 1.5rem; + } +} +
- This is a modal +
+ This is a modal +
`; + +exports[`renders destructive action button 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: #DB3643; + border-color: #DB3643; + font-weight: 600; +} + +.c0:focus { + outline: none; +} + +.c0[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c0 + .bd-button { + margin-top: 0.5rem; +} + +.c0:active { + background-color: #AD0000; +} + +.c0:focus { + box-shadow: 0 0 0 0.25rem #FEDBDE; +} + +.c0:hover:not(:active) { + background-color: #CC1F1F; +} + +.c0[disabled] { + background-color: #D9DCE9; +} + +.c1 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +@media (min-width:720px) { + .c0 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c0 { + width: auto; + } +} + + +`; + +exports[`renders secondary action button 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: 1px solid #D9DCE9; + border-radius: 0.25rem; + color: #FFFFFF; + cursor: pointer; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex: none; + -ms-flex: none; + flex: none; + font-size: 1rem; + font-weight: 400; + height: 2.25rem; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 2rem; + outline: none; + padding: 0 1rem; + pointer-events: auto; + position: relative; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + white-space: nowrap; + width: 100%; + background-color: transparent; + border-color: #3C64F4; + color: #3C64F4; +} + +.c0:focus { + outline: none; +} + +.c0[disabled] { + border-color: #D9DCE9; + pointer-events: none; +} + +.c0 + .bd-button { + margin-top: 0.5rem; +} + +.c0:active { + background-color: #DBE3FE; +} + +.c0:focus { + box-shadow: 0 0 0 0.25rem #DBE3FE; +} + +.c0:hover:not(:active) { + background-color: #F0F3FF; +} + +.c0[disabled] { + color: #D9DCE9; +} + +.c1 { + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: inline-grid; + grid-auto-flow: column; + grid-gap: 0.5rem; + visibility: visible; +} + +@media (min-width:720px) { + .c0 + .bd-button { + margin-top: 0; + margin-left: 0.5rem; + } +} + +@media (min-width:720px) { + .c0 { + width: auto; + } +} + + +`; diff --git a/packages/big-design/src/components/Modal/index.ts b/packages/big-design/src/components/Modal/index.ts index b37d3257e..b5a20a062 100644 --- a/packages/big-design/src/components/Modal/index.ts +++ b/packages/big-design/src/components/Modal/index.ts @@ -1 +1 @@ -export { Modal, ModalProps } from './Modal'; +export { Modal, ModalAction, ModalProps } from './Modal'; diff --git a/packages/big-design/src/components/Modal/spec.tsx b/packages/big-design/src/components/Modal/spec.tsx index ad081b27b..08944b99a 100644 --- a/packages/big-design/src/components/Modal/spec.tsx +++ b/packages/big-design/src/components/Modal/spec.tsx @@ -211,3 +211,47 @@ test('body has scroll locked on modal open', () => { expect(document.body.style.overflowY).toEqual(''); }); + +test('renders header', () => { + const { getByText } = render(); + + expect(getByText('Header Title')).toBeInTheDocument(); +}); + +test('header ignores components', () => { + // @ts-ignore bypassing type to test wrong use case + const { container } = render(Header Title} />); + + expect(container.querySelector('h2')).toBe(null); +}); + +test('renders actions', () => { + const { getAllByRole } = render(); + + expect(getAllByRole('button').length).toBe(3); +}); + +test('action button triggers onClick', () => { + const onClick = jest.fn(); + + const { getAllByRole } = render(); + const button = getAllByRole('button')[1]; + + fireEvent.click(button); + + expect(onClick).toHaveBeenCalled(); +}); + +test('renders secondary action button', () => { + const { getAllByRole } = render(); + const button = getAllByRole('button')[1]; + + expect(button).toMatchSnapshot(); +}); + +test('renders destructive action button', () => { + const { getAllByRole } = render(); + const button = getAllByRole('button')[1]; + + expect(button).toMatchSnapshot(); +}); diff --git a/packages/big-design/src/components/Modal/styled.tsx b/packages/big-design/src/components/Modal/styled.tsx index 8ee131b2a..06f439285 100644 --- a/packages/big-design/src/components/Modal/styled.tsx +++ b/packages/big-design/src/components/Modal/styled.tsx @@ -4,7 +4,7 @@ import styled, { css } from 'styled-components'; import { Flex } from '../Flex'; -import { ModalActionsProps, ModalHeaderProps, ModalProps } from './Modal'; +import { ModalProps } from './Modal'; export const StyledModal = styled.div.attrs({ 'aria-modal': true, @@ -67,34 +67,20 @@ export const StyledModalContent = styled(Flex)<{ variant: ModalProps['variant'] `} `; -export const StyledModalActions = styled(Flex)` +export const StyledModalActions = styled(Flex)` padding: ${({ theme }) => theme.spacing.medium}; ${({ theme }) => theme.breakpoints.tablet} { padding: ${({ theme }) => theme.spacing.xLarge}; } - - ${({ theme, withBorder }) => - withBorder && - css` - border-top: ${theme.border.box}; - margin-top: ${theme.spacing.medium}; - `}; `; -export const StyledModalHeader = styled.div` +export const StyledModalHeader = styled.div` padding: ${({ theme }) => theme.spacing.medium}; ${({ theme }) => theme.breakpoints.tablet} { padding: ${({ theme }) => theme.spacing.xLarge}; } - - ${({ theme, withBorder }) => - withBorder && - css` - border-bottom: ${theme.border.box}; - margin-bottom: ${theme.spacing.medium}; - `}; `; export const StyledModalClose = styled.div` diff --git a/packages/docs/PropTables/ModalPropTable.tsx b/packages/docs/PropTables/ModalPropTable.tsx index 067a79480..eae692740 100644 --- a/packages/docs/PropTables/ModalPropTable.tsx +++ b/packages/docs/PropTables/ModalPropTable.tsx @@ -1,14 +1,12 @@ import React from 'react'; -import { Code, PropTable } from '../components'; +import { Code, NextLink, PropTable } from '../components'; export const ModalPropTable: React.FC = () => ( - - Determines if the modal/dialog is open. - - - Function that will be called on close events. + + Accepts an array of objects with Button props and an additional{' '} + text prop. See example for usage. Determines if the backdrop is shown. @@ -19,24 +17,17 @@ export const ModalPropTable: React.FC = () => ( Controls whether onClose is called when pressing the ESC key. - - Determines the modal variant. + + Sets visible text that describes the content of the modal. - -); - -export const ModalActionsPropTable: React.FC = () => ( - - - Determines if the actions top border is shown. + + Determines if the modal/dialog is open. - -); - -export const ModalHeaderPropTable: React.FC = () => ( - - - Determines if the header bottom border is shown. + + Function that will be called on close events. + + + Determines the modal variant. ); diff --git a/packages/docs/pages/Modal/ModalPage.tsx b/packages/docs/pages/Modal/ModalPage.tsx index 477dd22be..5b6109334 100644 --- a/packages/docs/pages/Modal/ModalPage.tsx +++ b/packages/docs/pages/Modal/ModalPage.tsx @@ -2,7 +2,7 @@ import { Button, H0, H1, H2, Link, Modal, Text } from '@bigcommerce/big-design'; import React from 'react'; import { Code, CodePreview } from '../../components'; -import { ModalActionsPropTable, ModalHeaderPropTable, ModalPropTable } from '../../PropTables'; +import { ModalPropTable } from '../../PropTables'; export default () => ( <> @@ -26,29 +26,28 @@ export default () => ( <> - setIsOpen(false)} closeOnEscKey={true} closeOnClickOutside={false}> - Modal Title - - - - Ea tempor sunt amet labore proident dolor proident commodo in exercitation ea nulla sunt pariatur. - Nulla sunt ipsum do eu consectetur exercitation occaecat labore aliqua. Aute elit occaecat esse ea - fugiat esse. Reprehenderit sunt ea ea mollit commodo tempor amet fugiat. - - - Esse ipsum est consectetur nulla aute deserunt. Anim sint nisi consequat officia adipisicing irure. - Nulla ea reprehenderit elit eu nostrud sunt veniam dolore ex occaecat qui. Commodo ullamco ut sint - dolor quis cillum in et enim culpa esse exercitation ad. Eiusmod adipisicing nisi culpa esse laborum - cupidatat ad pariatur proident. Consectetur ex sint ullamco non ex. - - - - - - - + setIsOpen(false) }, + { text: 'Apply', onClick: () => setIsOpen(false) }, + ]} + header="Modal Title" + isOpen={isOpen} + onClose={() => setIsOpen(false)} + closeOnEscKey={true} + closeOnClickOutside={false} + > + + Ea tempor sunt amet labore proident dolor proident commodo in exercitation ea nulla sunt pariatur. Nulla + sunt ipsum do eu consectetur exercitation occaecat labore aliqua. Aute elit occaecat esse ea fugiat + esse. Reprehenderit sunt ea ea mollit commodo tempor amet fugiat. + + + Esse ipsum est consectetur nulla aute deserunt. Anim sint nisi consequat officia adipisicing irure. + Nulla ea reprehenderit elit eu nostrud sunt veniam dolore ex occaecat qui. Commodo ullamco ut sint dolor + quis cillum in et enim culpa esse exercitation ad. Eiusmod adipisicing nisi culpa esse laborum cupidatat + ad pariatur proident. Consectetur ex sint ullamco non ex. + ); @@ -77,26 +76,20 @@ export default () => ( setIsOpen(false) }, + { text: 'Apply', onClick: () => setIsOpen(false) }, + ]} + header="Dialog Title" isOpen={isOpen} onClose={() => setIsOpen(false)} closeOnEscKey={true} closeOnClickOutside={false} variant="dialog" > - Dialog Title - - - - Ea tempor sunt amet labore proident dolor proident commodo in exercitation ea nulla sunt pariatur. - - - - - - - + + Ea tempor sunt amet labore proident dolor proident commodo in exercitation ea nulla sunt pariatur. + ); @@ -109,13 +102,5 @@ export default () => (

Modal

- -

Modal.Header

- - - -

Modal.Actions

- - );