From e72cdc8ad2a749f84f596962324d72731f7c2b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Connor=20B=C3=A4r?= Date: Mon, 14 Jun 2021 17:22:53 +0200 Subject: [PATCH] Add NotificationModal --- .../components/ButtonGroup/ButtonGroup.tsx | 4 +- .../circuit-ui/components/Image/Image.tsx | 3 +- .../NotificationModal.stories.tsx | 68 ++++++ .../NotificationModal/NotificationModal.tsx | 196 ++++++++++++++++++ .../components/NotificationModal/index.ts | 17 ++ .../NotificationModal/useNotificationModal.ts | 20 ++ packages/circuit-ui/index.ts | 2 + 7 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 packages/circuit-ui/components/NotificationModal/NotificationModal.stories.tsx create mode 100644 packages/circuit-ui/components/NotificationModal/NotificationModal.tsx create mode 100644 packages/circuit-ui/components/NotificationModal/index.ts create mode 100644 packages/circuit-ui/components/NotificationModal/useNotificationModal.ts diff --git a/packages/circuit-ui/components/ButtonGroup/ButtonGroup.tsx b/packages/circuit-ui/components/ButtonGroup/ButtonGroup.tsx index fcb233ece2..43396ae668 100644 --- a/packages/circuit-ui/components/ButtonGroup/ButtonGroup.tsx +++ b/packages/circuit-ui/components/ButtonGroup/ButtonGroup.tsx @@ -23,7 +23,9 @@ export interface ButtonGroupProps { /** * Buttons to group. */ - children: ReactElement[] | ReactElement; + children: + | (ReactElement | null | undefined)[] + | ReactElement; /** * Direction to align the content. Either left/right */ diff --git a/packages/circuit-ui/components/Image/Image.tsx b/packages/circuit-ui/components/Image/Image.tsx index 22b3796ec2..d5fa55e906 100644 --- a/packages/circuit-ui/components/Image/Image.tsx +++ b/packages/circuit-ui/components/Image/Image.tsx @@ -13,7 +13,7 @@ * limitations under the License. */ -import { HTMLProps } from 'react'; +import { HTMLProps, Ref } from 'react'; import { css } from '@emotion/core'; import styled from '../../styles/styled'; @@ -30,6 +30,7 @@ export interface ImageProps * user uses a screen reader. */ alt: string; + ref?: Ref; } const baseStyles = () => css` diff --git a/packages/circuit-ui/components/NotificationModal/NotificationModal.stories.tsx b/packages/circuit-ui/components/NotificationModal/NotificationModal.stories.tsx new file mode 100644 index 0000000000..fc02118e63 --- /dev/null +++ b/packages/circuit-ui/components/NotificationModal/NotificationModal.stories.tsx @@ -0,0 +1,68 @@ +/** + * 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 from 'react'; +import { action } from '@storybook/addon-actions'; + +import { ModalProvider } from '../ModalContext'; +import Button from '../Button'; + +// import docs from './Modal.docs.mdx'; +import { NotificationModal, NotificationModalProps } from './NotificationModal'; +import { useNotificationModal } from './useNotificationModal'; + +export default { + title: 'Components/NotificationModal', + component: NotificationModal, + // parameters: { + // docs: { page: docs }, + // }, +}; + +export const Base = (modal: NotificationModalProps): JSX.Element => { + const ComponentWithModal = () => { + const { setModal } = useNotificationModal(); + + return ( + + ); + }; + return ( + + + + ); +}; + +Base.args = { + image: { + src: 'https://source.unsplash.com/TpHmEoVSmfQ/1600x900', + alt: '', + }, + headline: 'Example modal', + body: 'Hello World!', + actions: { + primary: { + children: 'Primary', + onClick: action('primary'), + }, + secondary: { + children: 'Secondary', + onClick: action('secondary'), + }, + }, +}; diff --git a/packages/circuit-ui/components/NotificationModal/NotificationModal.tsx b/packages/circuit-ui/components/NotificationModal/NotificationModal.tsx new file mode 100644 index 0000000000..f89b20b6e5 --- /dev/null +++ b/packages/circuit-ui/components/NotificationModal/NotificationModal.tsx @@ -0,0 +1,196 @@ +/** + * 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. + */ + +/** @jsx jsx */ +import { jsx, css, ClassNames } from '@emotion/core'; +import { ReactNode, MouseEvent, KeyboardEvent } from 'react'; +import ReactModal from 'react-modal'; +import { Theme } from '@sumup/design-tokens'; + +import useClickHandler from '../../hooks/useClickHandler'; +import { ModalComponent, BaseModalProps } from '../ModalContext'; +import Image, { ImageProps } from '../Image'; +import Headline from '../Headline'; +import Body from '../Body'; +import Button, { ButtonProps } from '../Button'; +import ButtonGroup from '../ButtonGroup'; +import styled, { StyleProps } from '../../styles/styled'; + +const TRANSITION_DURATION = 200; + +const imageStyles = ({ theme }: StyleProps) => css` + max-width: 232px; + height: 160px; + object-fit: cover; + margin: 0 auto ${theme.spacings.mega}; +`; + +const ModalImage = styled(Image)(imageStyles); + +export interface NotificationModalProps extends BaseModalProps { + image: ImageProps; + headline: string; + body: string | ReactNode; + actions: { + primary: Omit; + secondary?: Omit; + }; + /** + * TODO: Add description. Default true. + */ + dismissible?: boolean; +} + +/** + * Circuit UI's wrapper component for ReactModal. + * http://reactcommunity.org/react-modal/accessibility/#aria + */ +export const NotificationModal: ModalComponent = ({ + image, + headline, + body, + actions, + onClose, + dismissible = false, + tracking = {}, + className, + ...props +}) => { + if (process.env.NODE_ENV !== 'production' && className) { + // eslint-disable-next-line no-console + console.warn( + [ + 'Custom styles are not supported by the NotificationModal component.', + 'If your use case requires custom styles, please open an issue at', + 'https://github.com/sumup-oss/circuit-ui.', + ].join(' '), + ); + } + + const handleClose = useClickHandler(onClose, tracking, 'modal-close'); + return ( + > + {({ css: cssString, theme }) => { + // React Modal styles + // https://reactcommunity.org/react-modal/styles/classes/ + + // FIXME: Replace border-radius with theme value in v3. + const styles = { + base: cssString` + label: notification-modal; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: calc(100vw - ${theme.spacings.peta} * 2); + max-width: 420px; + max-height: calc(100vh - ${theme.spacings.mega} * 2); + outline: none; + background-color: ${theme.colors.white}; + border-radius: 16px; + padding: ${theme.spacings.giga}; + text-align: center; + opacity: 0; + transition: opacity ${TRANSITION_DURATION}ms ease-in-out; + overflow-y: auto; + + ${theme.mq.untilKilo} { + -webkit-overflow-scrolling: touch; + } + `, + afterOpen: cssString` + label: notification-modal--after-open; + opacity: 1; + `, + beforeClose: cssString` + label: notification-modal--before-close; + opacity: 0; + `, + }; + + const overlayStyles = { + base: cssString` + label: notification-modal__overlay; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0; + transition: opacity ${TRANSITION_DURATION}ms ease-in-out; + background: ${theme.colors.overlay}; + z-index: ${theme.zIndex.modal}; + + ${theme.mq.kilo} { + -webkit-overflow-scrolling: touch; + overflow-y: auto; + } + `, + afterOpen: cssString` + label: notification-modal__overlay--after-open; + opacity: 1; + `, + beforeClose: cssString` + label: notification-modal__overlay--before-close; + opacity: 0; + `, + }; + + const reactModalProps = { + className: styles, + overlayClassName: overlayStyles, + onRequestClose: handleClose, + closeTimeoutMS: TRANSITION_DURATION, + shouldCloseOnOverlayClick: dismissible, + shouldCloseOnEsc: dismissible, + ...props, + }; + + function wrapOnClick(onClick?: ButtonProps['onClick']) { + return (event: MouseEvent | KeyboardEvent) => { + handleClose?.(event); + onClick?.(event); + }; + } + + return ( + + + + {headline} + + {body} + + {actions.secondary && ( +