From 4e0f374b935cd5971589ae278e93db894ea4a3db Mon Sep 17 00:00:00 2001 From: Kyle Alwyn Date: Wed, 13 Mar 2019 09:44:22 -0700 Subject: [PATCH] feat(Modal): Use Transition to simplify animation; Add close button; Convert to hooks (#24) --- .eslintrc | 5 +- package-lock.json | 32 +++--- src/Modal/Modal.js | 257 +++++++++++++++++++------------------------- src/Modal/Modal.mdx | 2 +- 4 files changed, 130 insertions(+), 166 deletions(-) diff --git a/.eslintrc b/.eslintrc index f6ef999..46406fc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,3 @@ { - "extends": [ - "sappira", - "sappira/react" - ] + "extends": ["sappira/react"] } diff --git a/package-lock.json b/package-lock.json index a92d3ef..dcaf307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3463,7 +3463,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -3504,7 +3504,7 @@ "dependencies": { "@babel/code-frame": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz", "integrity": "sha512-cuAuTTIQ9RqcFRJ/Y8PvTh+paepNcaGxwQwjIDRWPXmzzyAeCO4KqS9ikMvq0MCbRk6GlYKwfzStrcP3/jSL8g==", "dev": true, "requires": { @@ -3513,7 +3513,7 @@ }, "@babel/generator": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.44.tgz", "integrity": "sha512-5xVb7hlhjGcdkKpMXgicAVgx8syK5VJz193k0i/0sLP6DzE6lRrU1K3B/rFefgdo9LPGMAOOOAWW4jycj07ShQ==", "dev": true, "requires": { @@ -3526,7 +3526,7 @@ }, "@babel/helper-function-name": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz", "integrity": "sha512-MHRG2qZMKMFaBavX0LWpfZ2e+hLloT++N7rfM3DYOMUOGCD8cVjqZpwiL8a0bOX3IYcQev1ruciT0gdFFRTxzg==", "dev": true, "requires": { @@ -3537,7 +3537,7 @@ }, "@babel/helper-get-function-arity": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz", "integrity": "sha512-w0YjWVwrM2HwP6/H3sEgrSQdkCaxppqFeJtAnB23pRiJB5E/O9Yp7JAAeWBl+gGEgmBFinnTyOv2RN7rcSmMiw==", "dev": true, "requires": { @@ -3546,7 +3546,7 @@ }, "@babel/helper-split-export-declaration": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz", "integrity": "sha512-aQ7QowtkgKKzPGf0j6u77kBMdUFVBKNHw2p/3HX/POt5/oz8ec5cs0GwlgM8Hz7ui5EwJnzyfRmkNF1Nx1N7aA==", "dev": true, "requires": { @@ -3555,7 +3555,7 @@ }, "@babel/highlight": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.44.tgz", "integrity": "sha512-Il19yJvy7vMFm8AVAh6OZzaFoAd0hbkeMZiX3P5HGD+z7dyI7RzndHB0dg6Urh/VAFfHtpOIzDUSxmY6coyZWQ==", "dev": true, "requires": { @@ -3566,7 +3566,7 @@ }, "@babel/template": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz", "integrity": "sha512-w750Sloq0UNifLx1rUqwfbnC6uSUk0mfwwgGRfdLiaUzfAOiH0tHJE6ILQIUi3KYkjiCDTskoIsnfqZvWLBDng==", "dev": true, "requires": { @@ -3578,7 +3578,7 @@ }, "@babel/traverse": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.44.tgz", "integrity": "sha512-UHuDz8ukQkJCDASKHf+oDt3FVUzFd+QYfuBIsiNu/4+/ix6pP/C+uQZJ6K1oEfbCMv/IKWbgDEh7fcsnIE5AtA==", "dev": true, "requires": { @@ -3596,7 +3596,7 @@ }, "@babel/types": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.44.tgz", "integrity": "sha512-5eTV4WRmqbaFM3v9gHAIljEQJU4Ssc6fxL61JN+Oe2ga/BwyjzjamwkCVVAQjHGuAX8i0BWo42dshL8eO5KfLQ==", "dev": true, "requires": { @@ -3607,7 +3607,7 @@ }, "babylon": { "version": "7.0.0-beta.44", - "resolved": "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", "dev": true }, @@ -8339,7 +8339,7 @@ }, "eslint": { "version": "4.19.1", - "resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", "dev": true, "requires": { @@ -8568,7 +8568,7 @@ }, "load-json-file": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { @@ -9017,7 +9017,7 @@ }, "external-editor": { "version": "2.2.0", - "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", "dev": true, "requires": { @@ -15442,7 +15442,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -18544,7 +18544,7 @@ }, "regexpp": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", "dev": true }, diff --git a/src/Modal/Modal.js b/src/Modal/Modal.js index 2104737..2c47c97 100644 --- a/src/Modal/Modal.js +++ b/src/Modal/Modal.js @@ -1,15 +1,21 @@ -import React from 'react'; +import React, { createContext, useContext, useEffect, useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { keyframes, css } from 'styled-components'; import * as animations from 'react-animations'; +import { Transition } from 'react-transition-group'; import Portal from '../Portal'; +import Flex from '../Flex'; +import Box from '../Box'; +import Icon from '../Icon'; import { createComponent, themeGet } from '../utils'; +const ModalContext = createContext({}); + const getAnimation = name => keyframes`${animations[name]}`; const Backdrop = createComponent({ name: 'ModalBackdrop', - style: ({ opening, closing }) => css` + style: ({ transitionState }) => css` top: 0; left: 0; right: 0; @@ -24,12 +30,17 @@ const Backdrop = createComponent({ background: rgba(0, 0, 0, 0.2); justify-content: center; - ${opening && + ${transitionState === 'exited' && + css` + display: none; + `} + + ${transitionState === 'entering' && css` animation: 0.35s ${getAnimation('fadeIn')}; `}; - ${closing && + ${transitionState === 'exiting' && css` animation: 0.35s ${getAnimation('fadeOut')}; `}; @@ -38,163 +49,116 @@ const Backdrop = createComponent({ const ModalContent = createComponent({ name: 'ModalContent', - style: ({ minWidth, maxWidth, opening, closing, animationIn, animationOut }) => css` + style: ({ minWidth, maxWidth, transitionState, animationIn, animationOut }) => css` position: relative; margin: auto; - min-width: ${minWidth || 250}px; - max-width: ${maxWidth || 768}px; + min-width: ${minWidth}px; + max-width: ${maxWidth}px; background: #ffffff; background-clip: padding-box; box-shadow: 0 8px 30px rgba(0, 29, 54, 0.1); border-radius: ${themeGet('radius')}px; - ${opening && + ${transitionState === 'entering' && css` animation: 0.75s ${getAnimation(animationIn)}; `}; - ${closing && + ${transitionState === 'exiting' && css` animation: 0.75s ${getAnimation(animationOut)}; `}; `, }); -class Modal extends React.Component { - static propTypes = { - open: PropTypes.bool, - closeOnBackdropClick: PropTypes.bool, - closeOnEscape: PropTypes.bool, - minWidth: PropTypes.number, - maxWidth: PropTypes.number, - animationIn: PropTypes.string, - animationOut: PropTypes.string, - onClose: PropTypes.func, - }; +function Modal({ children, title, animationDuration, showClose, onClose, open, ...props }) { + const [isOpen, setOpen] = useState(open); - static defaultProps = { - open: false, - closeOnBackdropClick: true, - closeOnEscape: true, - animationIn: 'zoomIn', - animationOut: 'zoomOut', - onClose: () => {}, + const handleClose = () => { + setOpen(false); + onClose(); }; - state = { - open: this.props.open || false, - opening: false, - closing: false, - }; + const handleContentClick = event => event.stopPropagation(); - componentDidUpdate(oldProps) { - if (!oldProps.open && this.props.open) { - this.open(); - } else if (oldProps.open && !this.props.open) { - this.close(); - } - } - - get isOpen() { - return !this.closed; - } - - get isClosed() { - const { open, opening, closing } = this.state; - return !open && !opening && !closing; - } - - get isControlled() { - return 'open' in this.props; - } - - open() { - this.setState( - { - opening: true, - }, - () => { - setTimeout(() => { - document.addEventListener('keydown', this.handleEscapeKey); - - this.setState({ - open: true, - opening: false, - }); - }, 250); - } - ); - } - - close() { - this.setState( - { - closing: true, - }, - () => { - setTimeout(() => { - document.removeEventListener('keydown', this.handleEscapeKey); - - this.setState({ - open: false, - closing: false, - }); - }, 250); - } - ); - } - - closeInternal() { - if (this.isControlled) { - this.props.onClose(); - } else { - this.close(); - } - } - - handleEscapeKey = event => { - if (!this.props.closeOnEscape) { - return; - } + const handleBackdropClick = () => { + if (!props.closeOnBackdropClick) return; - if (event.keyCode === 27) { - this.closeInternal(); - } + handleClose(); }; - handleBackdropClick = () => { - if (!this.props.closeOnBackdropClick) { - return; - } - - this.closeInternal(); - }; + const handleKeyDown = useCallback(event => { + if (!isOpen || !props.closeOnEscape) return; - handleContentClick = event => { - event.stopPropagation(); - }; + if (event.keyCode === 27) { + handleClose(); + } + }); - render() { - if (this.isClosed) { - return null; + useEffect(() => { + if (open !== isOpen) { + setOpen(open); } + }, [open]); - const { opening, closing } = this.state; - const { children, title, ...props } = this.props; + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }); - return ( + return ( + - - - {title && } - {children} - - + + {state => ( + + + {title && } + {children} + + + )} + - ); - } + + ); } +Modal.propTypes = { + open: PropTypes.bool, + showClose: PropTypes.bool, + closeOnBackdropClick: PropTypes.bool, + closeOnEscape: PropTypes.bool, + minWidth: PropTypes.number, + maxWidth: PropTypes.number, + animationIn: PropTypes.string, + animationOut: PropTypes.string, + animationDuration: PropTypes.number, + onClose: PropTypes.func, +}; + +Modal.defaultProps = { + open: false, + showClose: true, + closeOnBackdropClick: true, + closeOnEscape: true, + minWidth: 250, + maxWidth: 768, + animationIn: 'zoomIn', + animationOut: 'zoomOut', + animationDuration: 175, + onClose: () => {}, +}; + +Modal.Title = createComponent({ + name: 'ModalTitle', + tag: 'h2', + style: css` + font-size: 1.25rem; + margin: 0; + `, +}); + const ModalHeader = createComponent({ name: 'ModalHeader', style: css` @@ -213,23 +177,26 @@ const ModalHeaderInner = createComponent({ `, }); -Modal.Header = ({ title, children }) => ( - - - {title && {title}} - {children} - - -); - -Modal.Title = createComponent({ - name: 'ModalTitle', - tag: 'h2', - style: css` - font-size: 1.25rem; - margin: 0; - `, -}); +Modal.Header = ({ title, children, showClose = true }) => { + const { handleClose } = useContext(ModalContext); + + return ( + + + + {title && {title}} + {children} + + {showClose && ( + + + + )} + + + + ); +}; Modal.Body = createComponent({ name: 'ModalBody', diff --git a/src/Modal/Modal.mdx b/src/Modal/Modal.mdx index d7809e8..56d3b5a 100644 --- a/src/Modal/Modal.mdx +++ b/src/Modal/Modal.mdx @@ -27,5 +27,5 @@ Toggle a working modal demo by clicking the button below. It will slide up and f When modals become too long for the user’s viewport or device, they scroll independent of the page itself. - {new Array(50).fill(null).map(() =>

I'm really long annoying content.

)}}/ > + {new Array(50).fill(null).map((_, i) =>

I'm really long annoying content.

)}} />