Skip to content

Commit

Permalink
Consolidate and simplify the modal API (#949)
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-baer authored Jun 22, 2021
1 parent bd23429 commit 4e63620
Show file tree
Hide file tree
Showing 52 changed files with 1,290 additions and 1,165 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-lemons-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sumup/circuit-ui': major
---

Removed the exports of the `Modal`, `ModalWrapper`, `ModalHeader`, `ModalFooter`, `ModalContext`, and `ModalConsumer` components. Use the `useModal` hook instead.
24 changes: 13 additions & 11 deletions docs/introduction/getting-started.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,20 @@ npm install --save @sumup/collector @sumup/design-tokens @sumup/icons react reac

Finally, you need to make the theme available to Circuit UI. SumUp's default themes are part of the [@sumup/design-tokens](https://www.npmjs.com/package/@sumup/design-tokens) package. In most cases, they should cover your needs. Refer to the [Theme documentation](Features/Theme) to learn how to use and customize the theme.

At the root of your application, wrap the components in the `ThemeProvider` from Emotion:
At the root of your application, wrap the components in the `ThemeProvider` from Emotion and add the `BaseStyles` component:

```jsx
```tsx
// _app.tsx for Next.js or App.js for CRA
import { ThemeProvider } from 'emotion-theming';
import { light } from '@sumup/design-tokens';
import { Button } from '@sumup/circuit-ui';

const App = () => (
<ThemeProvider theme={light}>
<Button variant="primary">Click me</Button>
</ThemeProvider>
);

export default App;
import { BaseStyles } from '@sumup/circuit-ui';

export default function App() {
return (
<ThemeProvider theme={light}>
<BaseStyles />
{/* Your content here... */}
</ThemeProvider>
);
}
```
21 changes: 16 additions & 5 deletions packages/circuit-ui/components/Anchor/Anchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
* limitations under the License.
*/

import { forwardRef, HTMLProps, ReactNode, MouseEvent, Ref } from 'react';
import {
forwardRef,
HTMLProps,
ReactNode,
MouseEvent,
KeyboardEvent,
Ref,
} from 'react';
import { css } from '@emotion/core';
import { Dispatch as TrackingProps } from '@sumup/collector';
import { Theme } from '@sumup/design-tokens';
Expand All @@ -22,10 +29,14 @@ import { focusOutline } from '../../styles/style-mixins';
import { ReturnType } from '../../types/return-type';
import { Body, BodyProps } from '../Body/Body';
import { useComponents } from '../ComponentsContext';
import useClickHandler from '../../hooks/use-click-handler';
import { useClickHandler } from '../../hooks/useClickHandler';

export interface BaseProps extends BodyProps {
children: ReactNode;
/**
* Function that's called when the button is clicked.
*/
onClick?: (event: MouseEvent | KeyboardEvent) => void;
/**
* Additional data that is dispatched with the tracking event.
*/
Expand All @@ -35,8 +46,8 @@ export interface BaseProps extends BodyProps {
*/
ref?: Ref<HTMLButtonElement & HTMLAnchorElement>;
}
type LinkElProps = Omit<HTMLProps<HTMLAnchorElement>, 'size'>;
type ButtonElProps = Omit<HTMLProps<HTMLButtonElement>, 'size'>;
type LinkElProps = Omit<HTMLProps<HTMLAnchorElement>, 'size' | 'onClick'>;
type ButtonElProps = Omit<HTMLProps<HTMLButtonElement>, 'size' | 'onClick'>;

export type AnchorProps = BaseProps & LinkElProps & ButtonElProps;

Expand Down Expand Up @@ -94,7 +105,7 @@ export const Anchor = forwardRef(
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const Link = components.Link as any;

const handleClick = useClickHandler<MouseEvent<any>>(
const handleClick = useClickHandler<MouseEvent | KeyboardEvent>(
props.onClick,
tracking,
'anchor',
Expand Down
13 changes: 9 additions & 4 deletions packages/circuit-ui/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
FC,
SVGProps,
MouseEvent,
KeyboardEvent,
} from 'react';
import { css } from '@emotion/core';
import isPropValid from '@emotion/is-prop-valid';
Expand All @@ -35,7 +36,7 @@ import {
} from '../../styles/style-mixins';
import { ReturnType } from '../../types/return-type';
import { useComponents } from '../ComponentsContext';
import useClickHandler from '../../hooks/use-click-handler';
import { useClickHandler } from '../../hooks/useClickHandler';

export interface BaseProps {
'children': ReactNode;
Expand Down Expand Up @@ -68,6 +69,10 @@ export interface BaseProps {
* The HTML button type
*/
'type'?: 'button' | 'submit' | 'reset' | undefined;
/**
* Function that's called when the button is clicked.
*/
'onClick'?: (event: MouseEvent | KeyboardEvent) => void;
/**
* Additional data that is dispatched with the tracking event.
*/
Expand All @@ -79,8 +84,8 @@ export interface BaseProps {
'data-testid'?: string;
}

type LinkElProps = Omit<HTMLProps<HTMLAnchorElement>, 'size'>;
type ButtonElProps = Omit<HTMLProps<HTMLButtonElement>, 'size'>;
type LinkElProps = Omit<HTMLProps<HTMLAnchorElement>, 'size' | 'onClick'>;
type ButtonElProps = Omit<HTMLProps<HTMLButtonElement>, 'size' | 'onClick'>;

export type ButtonProps = BaseProps & LinkElProps & ButtonElProps;

Expand Down Expand Up @@ -290,7 +295,7 @@ export const Button = forwardRef(
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const Link = components.Link as any;

const handleClick = useClickHandler<MouseEvent<any>>(
const handleClick = useClickHandler<MouseEvent | KeyboardEvent>(
props.onClick,
tracking,
'button',
Expand Down
2 changes: 1 addition & 1 deletion packages/circuit-ui/components/Carousel/Carousel.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
childrenPropType,
childrenRenderPropType,
} from '../../util/shared-prop-types';
import useComponentSize from '../../hooks/use-component-size';
import { useComponentSize } from '../../hooks/useComponentSize';

import Container from './components/Container';
import Slides from './components/Slides';
Expand Down
2 changes: 1 addition & 1 deletion packages/circuit-ui/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
focusOutline,
} from '../../styles/style-mixins';
import { uniqueId } from '../../util/id';
import useClickHandler from '../../hooks/use-click-handler';
import { useClickHandler } from '../../hooks/useClickHandler';
import Tooltip from '../Tooltip';

export interface CheckboxProps extends HTMLProps<HTMLInputElement> {
Expand Down
124 changes: 34 additions & 90 deletions packages/circuit-ui/components/Modal/Modal.docs.mdx
Original file line number Diff line number Diff line change
@@ -1,122 +1,66 @@
import { Status, Props, Story } from '../../../../.storybook/components';
import { ModalWrapper, ModalHeader, ModalFooter } from '.';

# Modal

<Status.Stable />

Modals are floating cards which overlay the primary UI. All content in a single modal should be related to completing one single task. Modals are heavy UI elements which obscure the primary user interface — avoid them where possible.
The modal component displays self-contained tasks in a focused window that overlays the page content.

<Story id="components-modal--base" />
<Props of={ModalWrapper} />
<Props of={ModalHeader} />
<Props of={ModalFooter} />
<Props />

## When to use it

Use it when you want the user to focus on a single and perhaps more complex task.
Generally, use the modal component sparingly. Consider displaying more complex tasks and large amounts of information on a separate page instead.

## Usage guidelines
## Variants

#### General guidelines
<Story id="components-modal--variants" />

- **Do** use modals sparingly.
- **Do** use modals when you want to isolate an action from the primary UI.
- **Do not** draw a modal over another modal.
- **Do not** fill a modal with content which has multiple end results.
- **Do not** present a modal without a user prompting a modal (e.g. as a popup).
### Contextual

#### Header guidelines
Use this variant when the modal content requires the context of the page underneath to be understood. On small viewports, the modal component opens up from the bottom as a bottom sheet overlay on top of the page content, dimming the uncovered area while giving a visual context of the page underneath. The height of the bottom sheet can be manually adjusted depending on the use case and the amount of content needed to be displayed.

- **Do** use concise yet descriptive headings that label the function of the specific modal.
- **Do not** exclude headings from modals.
### Immersive

#### Content guidelines

- **Do** align text content to the left.
- **Do not** have more than two columns of content.

#### Footer guidelines

The modal footer contains CTA's which carry out an action on the entire modal.

- **Do** align modal CTA's to the right side of the footer.
- **Do not** have more than one "Primary - Major" CTA.
Use this variant to focus the user's attention on the modal content. On small viewports, the modal component opens up from the bottom as a fullscreen overlay on top of the page content and covers it entirely in favor of an immersive experience.

## Usage in code

There are a number of ways to use a modal in code. In some codebases, you
may opt to create a helper higher-order component that complements the
`ModalConsumer`.

### Using the ModalProvider
First, wrap your application in the `ModalProvider` which keeps track of the open modals, prevents scrolling of the page when a modal is open, and ensures the accessibility of the modal.

The benefit of using the ModalProvider is that it can be declared once at the application root, and you do not need to manage the open/closed state of the modal yourself.

```js
import {
useModal,
ModalProvider,
ModalWrapper,
ModalHeader,
Button,
} from '@sumup/circuit-ui';

const SayHello = ({ name }) => {
const { setModal } = useModal();
const showModal = () => {
setModal({
children: ({ onClose }) => (
<ModalWrapper>
<ModalHeader title="This is a modal" onClose={onClose} />
Hello {name}
</ModalWrapper>
),
});
};
return <Button onClick={showModal}>Say hello</Button>;
};
```tsx
// _app.tsx for Next.js or App.js for CRA
import { ThemeProvider } from 'emotion-theming';
import { light } from '@sumup/design-tokens';
import { ModalProvider, BaseStyles } from '@sumup/circuit-ui';

const Page = () => {
export default function App() {
return (
<ModalProvider>
<SayHello name="World" />
</ModalProvider>
<ThemeProvider theme={light}>
<BaseStyles />
<ModalProvider>{/* Your content here... */}</ModalProvider>
</ThemeProvider>
);
};
}
```

- `ModalWrapper` This is the wrapper for the body of a modal.
- `ModalHeader` This contains the title and the `X` close button.
- `ModalFooter` This component aligns its content.

### Embedding the modal in code
Then, use the `useModal` hook to open a modal from a component:

If you prefer to embed the code declaratively inside the component, you can do it as such:
```tsx
import { useModal, Button, Body } from '@sumup/circuit-ui';

```js
import { Modal, ModalWrapper, ModalHeader, Button } from '@sumup/circuit-ui';

const Page = () => {
const [isModalOpen, setModalOpen] = useState(false);
export function SayHello({ name }) {
const { setModal } = useModal();

const toggleModal = () => {
setModalOpen((prev) => !prev);
const handleClick = () => {
setModal({
children: <Body>Hello {name}</Body>,
variant: 'immersive',
closeButtonLabel: 'Close modal',
});
};

return (
<Fragment>
<Button onClick={toggleModal}>Open modal</Button>

<Modal isOpen={isModalOpen} onClose={toggleModal}>
{({ onClose }) => (
<ModalWrapper>
<ModalHeader onClose={onClose} title="Hello" />
The modal is open!
</ModalWrapper>
)}
</Modal>
</Fragment>
);
};
return <Button onClick={handleClick}>Say hello</Button>;
}
```
Loading

0 comments on commit 4e63620

Please sign in to comment.