From fe40990f9d8dfed5f4113b7b92affca3527e1a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20B=C3=A4r?= Date: Tue, 2 Jun 2020 21:59:48 +0200 Subject: [PATCH 1/7] refactor(components): migrate ModalHeader to TypeScript feature/modal-hook --- ...dalHeader.spec.js => ModalHeader.spec.tsx} | 8 +-- .../{ModalHeader.js => ModalHeader.tsx} | 51 ++++++++----------- ...spec.js.snap => ModalHeader.spec.tsx.snap} | 31 ++++++++--- .../Modal/components/ModalHeader/index.js | 18 ------- .../Modal/components/{index.js => index.ts} | 2 +- 5 files changed, 52 insertions(+), 58 deletions(-) rename src/components/Modal/components/ModalHeader/{ModalHeader.spec.js => ModalHeader.spec.tsx} (79%) rename src/components/Modal/components/ModalHeader/{ModalHeader.js => ModalHeader.tsx} (67%) rename src/components/Modal/components/ModalHeader/__snapshots__/{ModalHeader.spec.js.snap => ModalHeader.spec.tsx.snap} (61%) delete mode 100644 src/components/Modal/components/ModalHeader/index.js rename src/components/Modal/components/{index.js => index.ts} (92%) diff --git a/src/components/Modal/components/ModalHeader/ModalHeader.spec.js b/src/components/Modal/components/ModalHeader/ModalHeader.spec.tsx similarity index 79% rename from src/components/Modal/components/ModalHeader/ModalHeader.spec.js rename to src/components/Modal/components/ModalHeader/ModalHeader.spec.tsx index 37e14598dc..ce653e3106 100644 --- a/src/components/Modal/components/ModalHeader/ModalHeader.spec.js +++ b/src/components/Modal/components/ModalHeader/ModalHeader.spec.tsx @@ -15,14 +15,16 @@ import React from 'react'; -import ModalHeader from '.'; +import { create, renderToHtml, axe } from '../../../../util/test-utils'; + +import { ModalHeader } from './ModalHeader'; describe('ModalHeader', () => { /** * Style tests. */ it('should render with default styles', () => { - const actual = create(); + const actual = create(); expect(actual).toMatchSnapshot(); }); @@ -30,7 +32,7 @@ describe('ModalHeader', () => { * Accessibility tests. */ it('should meet accessibility guidelines', async () => { - const wrapper = renderToHtml(); + const wrapper = renderToHtml(); const actual = await axe(wrapper); expect(actual).toHaveNoViolations(); }); diff --git a/src/components/Modal/components/ModalHeader/ModalHeader.js b/src/components/Modal/components/ModalHeader/ModalHeader.tsx similarity index 67% rename from src/components/Modal/components/ModalHeader/ModalHeader.js rename to src/components/Modal/components/ModalHeader/ModalHeader.tsx index 2c92192b1a..3a5d2ad785 100644 --- a/src/components/Modal/components/ModalHeader/ModalHeader.js +++ b/src/components/Modal/components/ModalHeader/ModalHeader.tsx @@ -13,13 +13,32 @@ * limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FC, MouseEvent } from 'react'; import { CardHeader } from '../../../Card'; import Heading from '../../../Heading'; -const ModalHeader = ({ title, onClose, labelCloseButton }) => ( +export interface ModalHeaderProps { + /** + * A concise, descriptive title for the modal. + */ + title: string; + /** + * Callback for the close button. If not specified, the button won't + * be shown. + */ + onClose?: (event: MouseEvent) => void; + /** + * Text label for the close button for screen reader users. + */ + labelCloseButton?: string; +} + +export const ModalHeader: FC = ({ + title, + onClose, + labelCloseButton +}) => ( {title && ( @@ -28,29 +47,3 @@ const ModalHeader = ({ title, onClose, labelCloseButton }) => ( )} ); - -ModalHeader.propTypes = { - /** - * Callback for the close button. If not specified, the button won't - * be shown. - */ - onClose: PropTypes.func, - /** - * The title for the Modal. - */ - title: PropTypes.string.isRequired, - /** - * Text label for the close button for screen readers. - * Important for accessibility. - */ - labelCloseButton: PropTypes.string -}; - -ModalHeader.defaultProps = { - onClose: null -}; - -/** - * @component - */ -export default ModalHeader; diff --git a/src/components/Modal/components/ModalHeader/__snapshots__/ModalHeader.spec.js.snap b/src/components/Modal/components/ModalHeader/__snapshots__/ModalHeader.spec.tsx.snap similarity index 61% rename from src/components/Modal/components/ModalHeader/__snapshots__/ModalHeader.spec.js.snap rename to src/components/Modal/components/ModalHeader/__snapshots__/ModalHeader.spec.tsx.snap index c384200aa4..6ad0afa41f 100644 --- a/src/components/Modal/components/ModalHeader/__snapshots__/ModalHeader.spec.js.snap +++ b/src/components/Modal/components/ModalHeader/__snapshots__/ModalHeader.spec.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ModalHeader should render with default styles 1`] = ` -.circuit-0 { +.circuit-1 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -15,13 +15,30 @@ exports[`ModalHeader should render with default styles 1`] = ` -ms-flex-pack: justify; justify-content: space-between; margin-bottom: 24px; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; +} + +.circuit-0 { + font-weight: 700; + margin-bottom: 24px; + font-size: 17px; + line-height: 24px; + margin-bottom: 0; +} + +@media (min-width:480px) { + .circuit-0 { + font-size: 17px; + line-height: 24px; + } }
+ class="circuit-1 circuit-2" +> +

+ Title +

+
`; diff --git a/src/components/Modal/components/ModalHeader/index.js b/src/components/Modal/components/ModalHeader/index.js deleted file mode 100644 index 7b273eb712..0000000000 --- a/src/components/Modal/components/ModalHeader/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ModalHeader from './ModalHeader'; - -export default ModalHeader; diff --git a/src/components/Modal/components/index.js b/src/components/Modal/components/index.ts similarity index 92% rename from src/components/Modal/components/index.js rename to src/components/Modal/components/index.ts index 3bae591aaf..abac38a969 100644 --- a/src/components/Modal/components/index.js +++ b/src/components/Modal/components/index.ts @@ -14,7 +14,7 @@ */ import ModalWrapper from './ModalWrapper'; -import ModalHeader from './ModalHeader'; +import { ModalHeader } from './ModalHeader/ModalHeader'; export { ModalWrapper, ModalHeader }; export { CardFooter as ModalFooter } from '../../Card'; From 55ddcbf5a9230f0ee818743e5c04344adb95ceef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20B=C3=A4r?= Date: Tue, 2 Jun 2020 22:09:25 +0200 Subject: [PATCH 2/7] refactor(components): migrate ModalWrapper to TypeScript --- src/__snapshots__/storyshots.spec.js.snap | 10 ++--- ...lWrapper.spec.js => ModalWrapper.spec.tsx} | 4 +- .../{ModalWrapper.js => ModalWrapper.tsx} | 42 +++++++++---------- ...pec.js.snap => ModalWrapper.spec.tsx.snap} | 2 +- .../Modal/components/ModalWrapper/index.js | 18 -------- src/components/Modal/components/index.ts | 2 +- src/components/Modal/index.js | 1 + 7 files changed, 31 insertions(+), 48 deletions(-) rename src/components/Modal/components/ModalWrapper/{ModalWrapper.spec.js => ModalWrapper.spec.tsx} (89%) rename src/components/Modal/components/ModalWrapper/{ModalWrapper.js => ModalWrapper.tsx} (55%) rename src/components/Modal/components/ModalWrapper/__snapshots__/{ModalWrapper.spec.js.snap => ModalWrapper.spec.tsx.snap} (96%) delete mode 100644 src/components/Modal/components/ModalWrapper/index.js diff --git a/src/__snapshots__/storyshots.spec.js.snap b/src/__snapshots__/storyshots.spec.js.snap index 53c1235d08..106131fb3d 100644 --- a/src/__snapshots__/storyshots.spec.js.snap +++ b/src/__snapshots__/storyshots.spec.js.snap @@ -6934,7 +6934,7 @@ exports[`Storyshots Components/Modal/Embedded Base 1`] = ` }
Hello World!
@@ -7282,7 +7282,7 @@ exports[`Storyshots Components/Modal/Embedded With Footer 1`] = ` }

{ /** diff --git a/src/components/Modal/components/ModalWrapper/ModalWrapper.js b/src/components/Modal/components/ModalWrapper/ModalWrapper.tsx similarity index 55% rename from src/components/Modal/components/ModalWrapper/ModalWrapper.js rename to src/components/Modal/components/ModalWrapper/ModalWrapper.tsx index c2b2a110ab..8b15573397 100644 --- a/src/components/Modal/components/ModalWrapper/ModalWrapper.js +++ b/src/components/Modal/components/ModalWrapper/ModalWrapper.tsx @@ -13,14 +13,25 @@ * limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from '@emotion/styled'; +import React, { FC } from 'react'; import { css } from '@emotion/core'; +import styled, { StyleProps } from '../../../../styles/styled'; import Card from '../../../Card'; -const baseStyles = ({ theme }) => css` +// TODO: Extend CardProps once the Card has been migrated to TypeScript. +export interface ModalWrapperProps { + /** + * The shadow depth of the Modal. + */ + shadow?: 'single' | 'double' | 'triple'; + /** + * The padding of the Modal. + */ + spacing?: 'mega' | 'giga'; +} + +const baseStyles = ({ theme }: StyleProps) => css` width: 100%; ${theme.mq.untilKilo} { @@ -31,22 +42,9 @@ const baseStyles = ({ theme }) => css` } `; -const Wrapper = styled(Card)` - ${baseStyles}; -`; - -Wrapper.defaultProps = Card.defaultProps; +// FIXME: Remove any typecast once the Card has been migrated to TypeScript. +const Wrapper = styled(Card as any)(baseStyles); -const ModalWrapper = ({ ...props }) => ; - -ModalWrapper.propTypes = { - /* - * Modal content - */ - children: PropTypes.node.isRequired -}; - -/** - * @component - */ -export default ModalWrapper; +export const ModalWrapper: FC = props => ( + +); diff --git a/src/components/Modal/components/ModalWrapper/__snapshots__/ModalWrapper.spec.js.snap b/src/components/Modal/components/ModalWrapper/__snapshots__/ModalWrapper.spec.tsx.snap similarity index 96% rename from src/components/Modal/components/ModalWrapper/__snapshots__/ModalWrapper.spec.js.snap rename to src/components/Modal/components/ModalWrapper/__snapshots__/ModalWrapper.spec.tsx.snap index 757680f58f..1c7872b740 100644 --- a/src/components/Modal/components/ModalWrapper/__snapshots__/ModalWrapper.spec.js.snap +++ b/src/components/Modal/components/ModalWrapper/__snapshots__/ModalWrapper.spec.tsx.snap @@ -30,6 +30,6 @@ exports[`ModalWrapper should render with default styles 1`] = ` }

`; diff --git a/src/components/Modal/components/ModalWrapper/index.js b/src/components/Modal/components/ModalWrapper/index.js deleted file mode 100644 index 938a78cc65..0000000000 --- a/src/components/Modal/components/ModalWrapper/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import ModalWrapper from './ModalWrapper'; - -export default ModalWrapper; diff --git a/src/components/Modal/components/index.ts b/src/components/Modal/components/index.ts index abac38a969..0ba579914c 100644 --- a/src/components/Modal/components/index.ts +++ b/src/components/Modal/components/index.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -import ModalWrapper from './ModalWrapper'; +import { ModalWrapper } from './ModalWrapper/ModalWrapper'; import { ModalHeader } from './ModalHeader/ModalHeader'; export { ModalWrapper, ModalHeader }; diff --git a/src/components/Modal/index.js b/src/components/Modal/index.js index ea6f47c7e6..4296ff5237 100644 --- a/src/components/Modal/index.js +++ b/src/components/Modal/index.js @@ -25,4 +25,5 @@ export { ModalHeader, ModalFooter }; + export default Modal; From 564094e76b0c45b269871ec7c902d549711bb836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20B=C3=A4r?= Date: Wed, 3 Jun 2020 00:28:14 +0200 Subject: [PATCH 3/7] refactor(components): migrate Modal and ModalProvider to TypeScript --- package.json | 3 + src/components/Button/Button.tsx | 8 +- src/components/Modal/Modal.docs.mdx | 7 +- ...l.embed.story.js => Modal.embed.story.tsx} | 4 +- src/components/Modal/Modal.js | 213 ------------------ .../Modal/{Modal.spec.js => Modal.spec.tsx} | 19 +- .../Modal/{Modal.story.js => Modal.story.tsx} | 173 +++++++------- src/components/Modal/Modal.tsx | 191 ++++++++++++++++ src/components/Modal/ModalProvider.js | 108 --------- src/components/Modal/ModalProvider.tsx | 93 ++++++++ src/components/Modal/{index.js => index.ts} | 5 +- src/index.js | 1 + src/util/{type-check.js => type-check.ts} | 10 +- yarn.lock | 19 ++ 14 files changed, 419 insertions(+), 435 deletions(-) rename src/components/Modal/{Modal.embed.story.js => Modal.embed.story.tsx} (97%) delete mode 100644 src/components/Modal/Modal.js rename src/components/Modal/{Modal.spec.js => Modal.spec.tsx} (83%) rename src/components/Modal/{Modal.story.js => Modal.story.tsx} (51%) create mode 100644 src/components/Modal/Modal.tsx delete mode 100644 src/components/Modal/ModalProvider.js create mode 100644 src/components/Modal/ModalProvider.tsx rename src/components/Modal/{index.js => index.ts} (84%) rename src/util/{type-check.js => type-check.ts} (73%) diff --git a/package.json b/package.json index b6b6b5e8cf..cc9b94b143 100644 --- a/package.json +++ b/package.json @@ -112,8 +112,11 @@ "@types/jest-axe": "^3.2.2", "@types/jscodeshift": "^0.7.1", "@types/lodash": "^4.14.149", + "@types/no-scroll": "^2.1.0", "@types/react": "^16.9.32", "@types/react-dom": "^16.9.6", + "@types/react-modal": "^3.10.5", + "@types/recompose": "^0.30.7", "audit-ci": "^2.1.0", "babel-eslint": "^10.0.1", "babel-jest": "^25.0.0", diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index ef3a9631c5..b496a3b193 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -52,10 +52,14 @@ export interface BaseProps { * The ref to the html dom element, it can be an anchor or a button */ ref?: React.Ref; + /** + * The HTML button type + */ + type?: 'button' | 'submit' | 'reset' | undefined; } -type LinkElProps = Omit, 'size' | 'type'>; -type ButtonElProps = Omit, 'size' | 'type'>; +type LinkElProps = Omit, 'size'>; +type ButtonElProps = Omit, 'size'>; export type ButtonProps = BaseProps & LinkElProps & ButtonElProps; diff --git a/src/components/Modal/Modal.docs.mdx b/src/components/Modal/Modal.docs.mdx index 83fa49e700..b6052891ed 100644 --- a/src/components/Modal/Modal.docs.mdx +++ b/src/components/Modal/Modal.docs.mdx @@ -1,9 +1,5 @@ import { Status, Props, Story } from '../../../.storybook/components'; -import { - ModalWrapper, - ModalHeader, - ModalFooter -} from '../../../src/components/Modal'; +import { ModalWrapper, ModalHeader, ModalFooter } from '.'; # Modal @@ -14,7 +10,6 @@ modal should be related to completing one single task. Modals are heavy UI elements which obscure the primary user interface — avoid them where possible. - diff --git a/src/components/Modal/Modal.embed.story.js b/src/components/Modal/Modal.embed.story.tsx similarity index 97% rename from src/components/Modal/Modal.embed.story.js rename to src/components/Modal/Modal.embed.story.tsx index db3302942b..6b34887db8 100644 --- a/src/components/Modal/Modal.embed.story.js +++ b/src/components/Modal/Modal.embed.story.tsx @@ -18,10 +18,10 @@ import styled from '@emotion/styled'; import { css } from '@emotion/core'; import { action } from '@storybook/addon-actions'; -import { ModalWrapper, ModalHeader, ModalFooter } from './components'; import Button from '../Button'; import ButtonGroup from '../ButtonGroup'; import Text from '../Text'; +import { ModalWrapper, ModalHeader, ModalFooter } from './components'; export default { title: 'Components/Modal/Embedded', @@ -58,7 +58,7 @@ export const withFooter = () => ( Some text in the modal body. - - - - - - ) - }} - /> + + {({ onClose }) => ( + + + Some text in the modal body. + + + + + + + + )} + ); export const withCustomStyles = () => { @@ -163,27 +152,29 @@ export const withCustomStyles = () => { url('https://source.unsplash.com/random'); `; - const cardClassName = css` - padding: 0; - height: 50vh; - `; return ( ( -
- - - A nice custom modal for special cases. - - - -
- ) - }} - /> + {...defaultModal} + css={css` + padding: 0; + height: 50vh; + `} + > + {() => ( +
+ + + A nice custom modal for special cases. + + + +
+ )} +
); }; diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx new file mode 100644 index 0000000000..4d8efd7ae6 --- /dev/null +++ b/src/components/Modal/Modal.tsx @@ -0,0 +1,191 @@ +/** + * Copyright 2019, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, MouseEvent, KeyboardEvent, ReactNode } from 'react'; +import ReactModal, { Props } from 'react-modal'; +import { ClassNames } from '@emotion/core'; +import { useTheme } from 'emotion-theming'; +import { Theme } from '@sumup/design-tokens'; +import noScroll from 'no-scroll'; + +import IS_IOS from '../../util/ios'; +import { isFunction } from '../../util/type-check'; + +type OnClose = (event: MouseEvent | KeyboardEvent) => void; + +export interface ModalProps extends Partial { + children: ReactNode | (({ onClose }: { onClose: OnClose }) => ReactNode); + /** + * Determines if the modal is visible or not. + */ + isOpen?: boolean; + /** + * Function to close the modal. Passed down to the children + * render prop. + */ + onClose: OnClose; + /** + * React Modal's accessibility string. + */ + contentLabel?: string; + /** + * The element that should be used as root for the + * React portal used to display the modal. See + * http://reactcommunity.org/react-modal/accessibility/#app-element + */ + appElement?: string | HTMLElement; +} + +export const TRANSITION_DURATION = 200; +export const DEFAULT_APP_ELEMENT = '#root'; + +const TOP_MARGIN = '10vh'; +const TRANSFORM_Y_FLOATING = '10vh'; +const FLOATING_TRANSITION = `${TRANSITION_DURATION}ms ease-in-out`; +// eslint-disable-next-line max-len +const FIXED_TRANSITION = `${TRANSITION_DURATION}ms cubic-bezier(0, 0.37, 0.64, 1)`; + +/** + * Circuit UI's wrapper component for ReactModal. Uses the Card component + * to wrap content passed as the children prop. Don't forget to set + * the aria prop when using this. + * http://reactcommunity.org/react-modal/accessibility/#aria + */ +export const Modal: FC = ({ + children, + onClose, + contentLabel = 'Modal', + appElement = DEFAULT_APP_ELEMENT, + isOpen = true, + ...props +}) => { + const theme: Theme = useTheme(); + ReactModal.setAppElement(appElement); + return ( + + {({ css }) => { + // React Modal styles + // https://reactcommunity.org/react-modal/styles/classes/ + + const className = { + base: css` + label: modal; + outline: none; + + ${theme.mq.untilKilo} { + bottom: 0; + max-height: 80vh; + -webkit-overflow-scrolling: touch; + overflow-y: auto; + position: fixed; + transform: translateY(100%); + transition: transform ${FIXED_TRANSITION}; + width: 100%; + width: 100vw; + } + + ${theme.mq.kilo} { + transition: transform ${FLOATING_TRANSITION}, + opacity ${FLOATING_TRANSITION}; + margin: ${TOP_MARGIN} auto auto; + max-height: 90vh; + max-width: 90%; + min-width: 450px; + opacity: 0; + position: relative; + transform: translateY(${TRANSFORM_Y_FLOATING}); + } + + ${theme.mq.mega} { + max-width: 720px; + } + + ${theme.mq.giga} { + max-width: 800px; + } + `, + afterOpen: css` + label: modal--after-open; + ${theme.mq.untilKilo} { + transform: translateY(0); + } + + ${theme.mq.kilo} { + opacity: 1; + transform: translateY(0); + } + `, + beforeClose: css` + label: modal--before-close; + ${theme.mq.untilKilo} { + transform: translateY(100%); + } + + ${theme.mq.kilo} { + opacity: 0; + transform: translateY(${TRANSFORM_Y_FLOATING}); + } + ` + }; + + const overlayClassName = { + base: css` + label: modal__overlay; + background: ${theme.colors.overlay}; + bottom: 0; + left: 0; + opacity: 0; + position: fixed; + right: 0; + top: 0; + transition: opacity 200ms ease-in-out; + z-index: ${theme.zIndex.modal}; + + ${theme.mq.kilo} { + -webkit-overflow-scrolling: touch; + overflow-y: auto; + } + `, + afterOpen: css` + label: modal__overlay--after-open; + opacity: 1; + `, + beforeClose: css` + label: modal__overlay--before-close; + opacity: 0; + ` + }; + + const reactModalProps = { + isOpen, + className, + overlayClassName, + htmlOpenClassName: 'ReactModal__Html--open', + contentLabel, + onAfterOpen: () => IS_IOS && noScroll.on(), + onAfterClose: () => IS_IOS && noScroll.off(), + onRequestClose: onClose, + closeTimeoutMS: TRANSITION_DURATION, + ...props + }; + return ( + + {isFunction(children) ? children({ onClose }) : children} + + ); + }} + + ); +}; diff --git a/src/components/Modal/ModalProvider.js b/src/components/Modal/ModalProvider.js deleted file mode 100644 index d6db153659..0000000000 --- a/src/components/Modal/ModalProvider.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { Component, createContext } from 'react'; -import { Global, css } from '@emotion/core'; - -import Modal, { TRANSITION_DURATION } from './Modal'; -import { childrenPropType } from '../../util/shared-prop-types'; - -const { Provider: ContextProvider, Consumer: ModalConsumer } = createContext({ - setModal: () => {}, - getModal: () => {} -}); - -export { ModalConsumer }; - -export class ModalProvider extends Component { - static propTypes = { - children: childrenPropType.isRequired - }; - - state = { - modal: null, - isOpen: false - }; - - componentDidUpdate(prevProps, { isOpen: prevIsOpen }) { - const { isOpen } = this.state; - if (!isOpen && prevIsOpen) { - setTimeout(() => { - this.setState(prevState => ({ ...prevState, modal: null })); - }, TRANSITION_DURATION); - } - } - - setModal = config => { - window.onpopstate = this.closeModal; - this.setState(prevState => ({ - ...prevState, - modal: { ...prevState.modal, ...config }, - isOpen: true - })); - }; - - closeModal = () => { - window.onpopstate = null; - this.setState(prevState => ({ - ...prevState, - isOpen: false - })); - }; - - // eslint-disable-next-line react/sort-comp - contextValue = { - setModal: this.setModal, - getModal: () => this.state.modal - }; - - render() { - const { modal, isOpen } = this.state; - // Cannot use noop from lodash here. Breaks tests on node 8 for - // some reason. - const { onClose = () => {}, children, ...otherProps } = modal || {}; - const handleClose = () => { - onClose(); - this.closeModal(); - }; - const modalProps = modal - ? { - isOpen, - ...otherProps, - children: () => children({ onClose: handleClose }), - onClose: handleClose - } - : { isOpen, onClose, children: () => null }; - - return ( - - {this.props.children} - {modal && } - {isOpen && ( - - )} - - ); - } -} diff --git a/src/components/Modal/ModalProvider.tsx b/src/components/Modal/ModalProvider.tsx new file mode 100644 index 0000000000..fb21567327 --- /dev/null +++ b/src/components/Modal/ModalProvider.tsx @@ -0,0 +1,93 @@ +/** + * Copyright 2019, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { + createContext, + FC, + useState, + MouseEvent, + KeyboardEvent +} from 'react'; +import { Global, css } from '@emotion/core'; + +import { Modal, ModalProps } from './Modal'; + +// eslint-disable-next-line no-spaced-func +export const ModalContext = createContext<{ + setModal: (modal: ModalProps) => void; + getModal: () => ModalProps | null; +}>({ + setModal: () => {}, + getModal: () => null +}); + +export const ModalConsumer = ModalContext.Consumer; + +export const ModalProvider: FC> = props => { + const [isOpen, setOpen] = useState(false); + const [modal, setModal] = useState(null); + + const closeModal = (): void => { + window.onpopstate = null; + setOpen(false); + }; + + const openModal = (newModal: ModalProps): void => { + window.onpopstate = closeModal; + setModal(newModal); + setOpen(true); + }; + + const { onClose, children, ...modalProps } = modal || {}; + + const handleClose = (event: MouseEvent | KeyboardEvent): void => { + if (onClose) { + onClose(event); + } + closeModal(); + }; + + return ( + modal }} + > + {props.children} + + {modal && ( + + {children} + + )} + + {isOpen && ( + + )} + + ); +}; diff --git a/src/components/Modal/index.js b/src/components/Modal/index.ts similarity index 84% rename from src/components/Modal/index.js rename to src/components/Modal/index.ts index 4296ff5237..62a18f3566 100644 --- a/src/components/Modal/index.js +++ b/src/components/Modal/index.ts @@ -13,12 +13,13 @@ * limitations under the License. */ -import { ModalProvider, ModalConsumer } from './ModalProvider'; -import Modal, { DEFAULT_APP_ELEMENT } from './Modal'; +import { ModalContext, ModalProvider, ModalConsumer } from './ModalProvider'; +import { Modal, DEFAULT_APP_ELEMENT } from './Modal'; import { ModalWrapper, ModalHeader, ModalFooter } from './components'; export { DEFAULT_APP_ELEMENT, + ModalContext, ModalConsumer, ModalProvider, ModalWrapper, diff --git a/src/index.js b/src/index.js index 1df67ccaf8..66e6a812af 100644 --- a/src/index.js +++ b/src/index.js @@ -86,6 +86,7 @@ export { default as BaseStyles } from './components/BaseStyles'; export { default as Modal, DEFAULT_APP_ELEMENT, + ModalContext, ModalConsumer, ModalProvider, ModalWrapper, diff --git a/src/util/type-check.js b/src/util/type-check.ts similarity index 73% rename from src/util/type-check.js rename to src/util/type-check.ts index 263ed4b565..80f36e2e13 100644 --- a/src/util/type-check.js +++ b/src/util/type-check.ts @@ -13,7 +13,11 @@ * limitations under the License. */ -export const isFunction = val => typeof val === 'function'; -export const isString = val => typeof val === 'string'; -export const isArray = value => +export const isFunction = (value: any): value is Function => + typeof value === 'function'; + +export const isString = (value: any): value is string => + typeof value === 'string'; + +export const isArray = (value: any): value is [] => value && typeof value === 'object' && value.constructor === Array; diff --git a/yarn.lock b/yarn.lock index 2df874be16..1b569f42f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2984,6 +2984,11 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/no-scroll@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/no-scroll/-/no-scroll-2.1.0.tgz#d50280440fe1bc85c7481f40e35dc51be7b54bb8" + integrity sha512-67oe0I3SZeEsCIcf5MT4yKrcJOIZn5g+ytPEn+JQQnM4IprztFAWg9T3ngVGbxkceJlLYBsqIfvHl9xEGTuPyA== + "@types/node@*", "@types/node@>= 8": version "13.7.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.1.tgz#238eb34a66431b71d2aaddeaa7db166f25971a0d" @@ -3048,6 +3053,13 @@ dependencies: "@types/react" "*" +"@types/react-modal@^3.10.5": + version "3.10.5" + resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.10.5.tgz#5aa40bcb71b59243126069330856eb430ed3adcc" + integrity sha512-iLL9afYbcgYlboW2J8mFNKH2tFgErIHR0q+JgHotKrFK99+d97v+V/dxL5LVoxt1OjlnWsuwj8sZuBMVsI1MtQ== + dependencies: + "@types/react" "*" + "@types/react-syntax-highlighter@11.0.2": version "11.0.2" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz#a2e3ff657d7c47813f80ca930f3d959c31ec51e3" @@ -3085,6 +3097,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/recompose@^0.30.7": + version "0.30.7" + resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.7.tgz#0d47f3da3bdf889a4f36d4ca7531fac1eee1c6bd" + integrity sha512-kEvD7XMguXgG7jJJS//cE1QTbkFj2qDtIPAg1FvXxE8D6jD1C0WabJjT7cVitC7TK0N5I3yp2955hqNFFZV0wg== + dependencies: + "@types/react" "*" + "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" From 7b9039c94df2a2f755830492f5e42e7b96a68b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20B=C3=A4r?= Date: Wed, 3 Jun 2020 00:52:02 +0200 Subject: [PATCH 4/7] feat(components): make ModalFooter sticky --- src/__snapshots__/storyshots.spec.js.snap | 91 ++++++++++--------- .../Card/components/Footer/Footer.js | 8 +- .../ModalFooter/ModalFooter.spec.tsx | 39 ++++++++ .../components/ModalFooter/ModalFooter.tsx | 32 +++++++ .../__snapshots__/ModalFooter.spec.tsx.snap | 43 +++++++++ src/components/Modal/components/index.ts | 8 +- 6 files changed, 167 insertions(+), 54 deletions(-) create mode 100644 src/components/Modal/components/ModalFooter/ModalFooter.spec.tsx create mode 100644 src/components/Modal/components/ModalFooter/ModalFooter.tsx create mode 100644 src/components/Modal/components/ModalFooter/__snapshots__/ModalFooter.spec.tsx.snap diff --git a/src/__snapshots__/storyshots.spec.js.snap b/src/__snapshots__/storyshots.spec.js.snap index 106131fb3d..8fa3868242 100644 --- a/src/__snapshots__/storyshots.spec.js.snap +++ b/src/__snapshots__/storyshots.spec.js.snap @@ -6955,13 +6955,7 @@ exports[`Storyshots Components/Modal/Embedded With Custom Styles 1`] = ` } } -.circuit-7 { - width: 100%; - padding: 0; - height: 50vh; -} - -.circuit-5 { +.circuit-3 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -6998,20 +6992,20 @@ exports[`Storyshots Components/Modal/Embedded With Custom Styles 1`] = ` padding: 24px 18px; } -.circuit-3 { +.circuit-2 { height: 100%; width: 50%; background: no-repeat center / cover url('https://source.unsplash.com/random'); }

@@ -7210,35 +7204,6 @@ exports[`Storyshots Components/Modal/Embedded With Footer 1`] = ` } } -.circuit-8 { - display: block; - width: 100%; - margin-top: 24px; -} - -@media (min-width:480px) { - .circuit-8 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - margin-top: 16px; - } -} - -@media (min-width:480px) { - .circuit-8 { - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; - } -} - .circuit-3 { font-weight: 400; margin-bottom: 16px; @@ -7253,7 +7218,7 @@ exports[`Storyshots Components/Modal/Embedded With Footer 1`] = ` } } -.circuit-10 { +.circuit-9 { background-color: #FFFFFF; border-radius: 4px; display: -webkit-box; @@ -7273,7 +7238,7 @@ exports[`Storyshots Components/Modal/Embedded With Footer 1`] = ` } @media (max-width:479px) { - .circuit-10 { + .circuit-9 { border-bottom-left-radius: 0; border-bottom-right-radius: 0; min-width: initial; @@ -7281,8 +7246,44 @@ exports[`Storyshots Components/Modal/Embedded With Footer 1`] = ` } } +.circuit-8 { + display: block; + width: 100%; + margin-top: 24px; + position: -webkit-sticky; + position: sticky; + bottom: 0; + margin-top: 0; + padding-top: 24px; + padding-bottom: 12px; + background: #FFFFFF; +} + +@media (min-width:480px) { + .circuit-8 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + margin-top: 16px; + } +} + +@media (min-width:480px) { + .circuit-8 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + } +} +