Skip to content

Commit

Permalink
Add NotificationModal
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-baer committed Jul 29, 2021
1 parent 1869c21 commit ab7d85d
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 2 deletions.
4 changes: 3 additions & 1 deletion packages/circuit-ui/components/ButtonGroup/ButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export interface ButtonGroupProps {
/**
* Buttons to group.
*/
children: ReactElement<ButtonProps>[] | ReactElement<ButtonProps>;
children:
| (ReactElement<ButtonProps> | null | undefined)[]
| ReactElement<ButtonProps>;
/**
* Direction to align the content. Either left/right
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/circuit-ui/components/Image/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,6 +30,7 @@ export interface ImageProps
* user uses a screen reader.
*/
alt: string;
ref?: Ref<HTMLImageElement>;
}

const baseStyles = () => css`
Expand Down
1 change: 1 addition & 0 deletions packages/circuit-ui/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const TRANSITION_DURATION = Math.max(
TRANSITION_DURATION_MOBILE,
TRANSITION_DURATION_DESKTOP,
);

type PreventCloseProps =
| {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Button type="button" onClick={() => setModal(modal)}>
Open modal
</Button>
);
};
return (
<ModalProvider>
<ComponentWithModal />
</ModalProvider>
);
};

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'),
},
},
};
222 changes: 222 additions & 0 deletions packages/circuit-ui/components/NotificationModal/NotificationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/**
* 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 { css, ClassNames } from '@emotion/core';
import { ReactNode, MouseEvent, KeyboardEvent } from 'react';
import ReactModal from 'react-modal';
import { Theme } from '@sumup/design-tokens';

import { useClickEvent } from '../../hooks/useClickEvent';
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';
import CloseButton from '../CloseButton';

const TRANSITION_DURATION = 200;

type PreventCloseProps =
| {
/**
* Text label for the close button for screen readers.
* Important for accessibility.
*/
closeButtonLabel?: never;
/**
* Prevent users from closing the modal by clicking/tapping the overlay or
* pressing the escape key. Default `false`.
*/
preventClose: boolean;
}
| {
closeButtonLabel: string;
preventClose?: never;
};

const closeButtonStyles = (theme: Theme) => css`
position: absolute;
top: ${theme.spacings.byte};
right: ${theme.spacings.byte};
${theme.mq.kilo} {
top: ${theme.spacings.mega};
right: ${theme.spacings.mega};
}
`;

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 type NotificationModalProps = BaseModalProps &
PreventCloseProps & {
image: ImageProps;
headline: string;
body: string | ReactNode;
actions: {
primary: Omit<ButtonProps, 'variant'>;
secondary?: Omit<ButtonProps, 'variant'>;
};
};

/**
* Circuit UI's wrapper component for ReactModal.
* http://reactcommunity.org/react-modal/accessibility/#aria
*/
export const NotificationModal: ModalComponent<NotificationModalProps> = ({
image,
headline,
body,
actions,
onClose,
closeButtonLabel,
preventClose = 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 = useClickEvent(onClose, tracking, 'modal-close');
return (
<ClassNames<Theme>>
{({ css: cssString, theme }) => {
// React Modal styles
// https://reactcommunity.org/react-modal/styles/classes/
const styles = {
base: cssString`
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: ${theme.borderRadius.mega};
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`
opacity: 1;
`,
beforeClose: cssString`
opacity: 0;
`,
};

const overlayStyles = {
base: cssString`
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`
opacity: 1;
`,
beforeClose: cssString`
opacity: 0;
`,
};

const reactModalProps = {
className: styles,
overlayClassName: overlayStyles,
onRequestClose: handleClose,
closeTimeoutMS: TRANSITION_DURATION,
shouldCloseOnOverlayClick: !preventClose,
shouldCloseOnEsc: !preventClose,
...props,
};

function wrapOnClick(onClick?: ButtonProps['onClick']) {
return (event: MouseEvent | KeyboardEvent) => {
handleClose?.(event);
onClick?.(event);
};
}

return (
<ReactModal {...reactModalProps}>
{!preventClose && closeButtonLabel && (
<CloseButton
onClick={onClose}
label={closeButtonLabel}
css={closeButtonStyles}
/>
)}
<ModalImage {...image} />
<Headline as="h2" size="three" noMargin>
{headline}
</Headline>
<Body>{body}</Body>
<ButtonGroup align="center">
{actions.secondary && (
<Button
{...actions.secondary}
variant="secondary"
onClick={wrapOnClick(actions.secondary.onClick)}
/>
)}
<Button
{...actions.primary}
variant="primary"
onClick={wrapOnClick(actions.primary.onClick)}
/>
</ButtonGroup>
</ReactModal>
);
}}
</ClassNames>
);
};

NotificationModal.TIMEOUT = TRANSITION_DURATION;
17 changes: 17 additions & 0 deletions packages/circuit-ui/components/NotificationModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright 2021, 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.
*/

export { useNotificationModal } from './useNotificationModal';
export type { NotificationModalProps } from './NotificationModal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright 2021, 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 { createUseModal } from '../ModalContext';

import { NotificationModal } from './NotificationModal';

export const useNotificationModal = createUseModal(NotificationModal);
2 changes: 2 additions & 0 deletions packages/circuit-ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ export { ModalProvider } from './components/ModalContext';
export type { ModalProviderProps } from './components/ModalContext';
export { useModal } from './components/Modal';
export type { ModalProps } from './components/Modal';
export { useNotificationModal } from './components/NotificationModal';
export type { NotificationModalProps } from './components/NotificationModal';

export { default as Table } from './components/Table';
export type {
Expand Down

0 comments on commit ab7d85d

Please sign in to comment.