Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(web-react): Introduce vertical alignment options for Modal #DS… #1243

Merged
merged 1 commit into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/web-react/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';
import classNames from 'classnames';
import { AlignmentY } from '../../constants';
import { useStyleProps, useLastActiveFocus } from '../../hooks';
import { SpiritModalProps } from '../../types';
import { useModalStyleProps } from './useModalStyleProps';
import { ModalProvider } from './ModalContext';
import Dialog from '../Dialog/Dialog';

const Modal = (props: SpiritModalProps) => {
const { children, isOpen, onClose, id, ...restProps } = props;
const { classProps } = useModalStyleProps();
const { children, alignmentY = AlignmentY.CENTER, isOpen, onClose, id, ...restProps } = props;
const { classProps } = useModalStyleProps({ modalAlignment: alignmentY });
const { styleProps, props: otherProps } = useStyleProps(restProps);

const contextValue = {
Expand Down
3 changes: 2 additions & 1 deletion packages/web-react/src/components/Modal/ModalFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import classNames from 'classnames';
import { AlignmentX } from '../../constants';
import { useStyleProps } from '../../hooks';
import { ModalFooterProps } from '../../types';
import { useModalStyleProps } from './useModalStyleProps';

const ModalFooter = (props: ModalFooterProps) => {
const { children, alignmentX = 'right', description, ...restProps } = props;
const { children, alignmentX = AlignmentX.RIGHT, description, ...restProps } = props;

const { classProps } = useModalStyleProps({ footerAlignment: alignmentX });
const { styleProps, props: otherProps } = useStyleProps(restProps);
Expand Down
40 changes: 30 additions & 10 deletions packages/web-react/src/components/Modal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Modal is a composition of several subcomponents:
- [Opening the Modal](#opening-the-modal)
- [Scrolling Long Content](#scrolling-long-content)
- [Scrolling with ScrollView](#scrolling-with-scrollview)
- [Stacking Modals](#stacking-modals)
- [Full Example](#full-example)

## Modal
Expand All @@ -36,17 +37,35 @@ provides several accessibility advantages.
<Modal id="modal-example">…</Modal>
```

### Vertical Alignment

Modal can be aligned to the center (default), top, or bottom. These values come from the
[alignment dictionary][dictionary-alignment]. Using a corresponding alignment option will align the modal accordingly:

- `top`
- `center` (default)
- `bottom`

Example:

```jsx
<Modal alignmentY="top" id="modal-example">
</Modal>
```

### API

| Name | Type | Default | Required | Description |
| ---------------------- | ---------------------------------------------- | ------- | -------- | ----------------------------------------------------- |
| `children` | `ReactNode` | — | ✕ | Children node |
| `closeOnBackdropClick` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked |
| `id` | `string` | — | ✔ | Modal ID |
| `isOpen` | `bool` | `false` | ✔ | Open state |
| `onClose` | `(event: ClickEvent or KeyboardEvent) => void` | — | ✔ | Callback on dialog closed |
| `UNSAFE_className` | `string` | — | ✕ | Modal custom class name |
| `UNSAFE_style` | `CSSProperties` | — | ✕ | Modal custom style |
| Name | Type | Default | Required | Description |
| ---------------------- | ---------------------------------------------- | -------- | -------- | ----------------------------------------------------- |
| `alignmentY` | [AlignmentY dictionary][dictionary-alignment] | `center` | ✕ | Vertical alignment of modal |
| `children` | `ReactNode` | — | ✕ | Children node |
| `closeOnBackdropClick` | `bool` | `true` | ✕ | Whether the modal will close when backdrop is clicked |
| `id` | `string` | — | ✔ | Modal ID |
| `isOpen` | `bool` | `false` | ✔ | Open state |
| `onClose` | `(event: ClickEvent or KeyboardEvent) => void` | — | ✔ | Callback on dialog closed |
| `UNSAFE_className` | `string` | — | ✕ | Modal custom class name |
| `UNSAFE_style` | `CSSProperties` | — | ✕ | Modal custom style |

Also, all properties of the [`<dialog>` element][mdn-dialog] are supported.

Expand Down Expand Up @@ -215,7 +234,8 @@ Optionally, you can add a description to the footer:
### Footer Alignment

ModalFooter can be aligned to the right (default), center, or left. These values come from the
[dictionary][dictionary-alignment]. Using a corresponding alignment option will align the footer actions accordingly:
[alignment dictionary][dictionary-alignment]. Using a corresponding alignment option will align the footer actions
accordingly:

- `right` (default)
- `center`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('useModalStyleProps', () => {
it('should return defaults', () => {
const { result } = renderHook(() => useModalStyleProps({}));

expect(result.current.classProps.root).toBe('Modal');
expect(result.current.classProps.root).toBe('Modal Modal--center');
expect(result.current.classProps.dialog).toBe('ModalDialog');
expect(result.current.classProps.title).toBe('ModalHeader__title');
expect(result.current.classProps.header).toBe('ModalHeader');
Expand Down
73 changes: 55 additions & 18 deletions packages/web-react/src/components/Modal/demo/ModalDefault.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { ChangeEvent, useState } from 'react';
import { AlignmentXDictionaryType } from '../../..';
import { AlignmentX, AlignmentXDictionaryType, AlignmentY, AlignmentYDictionaryType } from '../../..';
import { Button, Checkbox, Modal, ModalBody, ModalDialog, ModalFooter, ModalHeader, Radio, TextField } from '../..';

const ModalDefault = () => {
const [isFirstOpen, setFirstOpen] = useState(false);
const [isSecondOpen, setSecondOpen] = useState(false);
const [isThirdOpen, setThirdOpen] = useState(false);
const [footerAlign, setFooterAlign] = useState<AlignmentXDictionaryType>('right');
const [modalAlign, setModalAlign] = useState<AlignmentYDictionaryType>(AlignmentY.CENTER);
const [footerAlign, setFooterAlign] = useState<AlignmentXDictionaryType>(AlignmentX.RIGHT);
const [isExpanded, setIsExpanded] = useState(true);

const toggleFirstModal = () => setFirstOpen(!isFirstOpen);
Expand All @@ -17,59 +18,95 @@ const ModalDefault = () => {
const handleFirstClose = () => setFirstOpen(false);
const handleSecondClose = () => setSecondOpen(false);
const handleThirdClose = () => setThirdOpen(false);
const handleFooterAlignChange = (e: ChangeEvent<HTMLInputElement>) => {
setFooterAlign(e.target.value as AlignmentXDictionaryType);
const handleModalAlignChange = (event: ChangeEvent<HTMLInputElement>) => {
setModalAlign(event.target.value as AlignmentYDictionaryType);
};
const handleFooterAlignChange = (event: ChangeEvent<HTMLInputElement>) => {
setFooterAlign(event.target.value as AlignmentXDictionaryType);
};

return (
<>
<Button onClick={toggleFirstModal}>Open Modal</Button>

<Modal id="example_basic" isOpen={isFirstOpen} onClose={handleFirstClose}>
<Modal alignmentY={modalAlign} id="example-basic" isOpen={isFirstOpen} onClose={handleFirstClose}>
<ModalDialog isExpandedOnMobile={isExpanded}>
<ModalHeader id="example_basic">Modal Title</ModalHeader>
<ModalHeader id="example-basic">Modal Title</ModalHeader>
<ModalBody>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam at excepturi laudantium magnam mollitia
perferendis reprehenderit, voluptate. Cum delectus dicta ducimus eligendi excepturi natus perferendis
provident unde. Eveniet, iste, molestiae?
</p>
<form className="d-none d-tablet-block mb-600">
<div>Modal alignment (from tablet up):</div>
<Radio
id="modal-alignment-top"
UNSAFE_className="mr-600"
label="Top"
value="top"
name="modal_alignment"
autoComplete="off"
isChecked={modalAlign === AlignmentY.TOP}
onChange={handleModalAlignChange}
/>{' '}
<Radio
id="modal-alignment-center"
UNSAFE_className="mr-600"
label="Center"
value="center"
name="modal_alignment"
autoComplete="off"
isChecked={modalAlign === AlignmentY.CENTER}
onChange={handleModalAlignChange}
/>{' '}
<Radio
id="modal-alignment-bottom"
UNSAFE_className="mr-600"
label="Bottom"
value="bottom"
name="modal_alignment"
autoComplete="off"
isChecked={modalAlign === AlignmentY.BOTTOM}
onChange={handleModalAlignChange}
/>
</form>
<form className="d-none d-tablet-block">
<div>Footer alignment (from tablet up):</div>
<Radio
id="footer_alignment_left"
id="footer-alignment-left"
UNSAFE_className="mr-600"
label="Left"
value="left"
name="footer_alignment"
autoComplete="off"
isChecked={footerAlign === 'left'}
isChecked={footerAlign === AlignmentX.LEFT}
onChange={handleFooterAlignChange}
/>
/>{' '}
<Radio
id="footer_alignment_center"
id="footer-alignment-center"
UNSAFE_className="mr-600"
label="Center"
value="center"
name="footer_alignment"
autoComplete="off"
isChecked={footerAlign === 'center'}
isChecked={footerAlign === AlignmentX.CENTER}
onChange={handleFooterAlignChange}
/>
/>{' '}
<Radio
id="footer_alignment_right"
id="footer-alignment-right"
UNSAFE_className="mr-600"
label="Right"
value="right"
name="footer_alignment"
autoComplete="off"
isChecked={footerAlign === 'right'}
isChecked={footerAlign === AlignmentX.RIGHT}
onChange={handleFooterAlignChange}
/>
</form>
<form className="d-tablet-none">
<Checkbox
id="expand_on_mobile"
id="expand-on-mobile"
label="Expand on mobile"
value="right"
autoComplete="off"
Expand All @@ -78,7 +115,7 @@ const ModalDefault = () => {
/>
</form>
</ModalBody>
<ModalFooter description="Optional description" alignmentX={footerAlign}>
<ModalFooter alignmentX={footerAlign} description="Optional description">
<Button onClick={handleFirstClose}>Primary action</Button>
<Button color="secondary" onClick={handleFirstClose}>
Secondary action
Expand All @@ -89,7 +126,7 @@ const ModalDefault = () => {

<Button onClick={toggleSecondModal}>Open Modal with a Form</Button>

<Modal id="example_form" isOpen={isSecondOpen} onClose={handleSecondClose}>
<Modal id="example-form" isOpen={isSecondOpen} onClose={handleSecondClose}>
<ModalDialog elementType="form" method="dialog">
<ModalHeader>Modal with a Form</ModalHeader>
<ModalBody>
Expand All @@ -110,7 +147,7 @@ const ModalDefault = () => {

<Button onClick={toggleThirdModal}>Open Modal with Custom Height</Button>

<Modal id="example_custom_height" isOpen={isThirdOpen} onClose={handleThirdClose}>
<Modal id="example-custom-height" isOpen={isThirdOpen} onClose={handleThirdClose}>
<ModalDialog
elementType="form"
isExpandedOnMobile={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import React, { useState } from 'react';
import { Button, Modal, ModalBody, ModalDialog, ModalFooter, ModalHeader } from '../..';
import React, { ChangeEvent, useState } from 'react';
import { AlignmentX, AlignmentXDictionaryType, AlignmentY, AlignmentYDictionaryType } from '../../..';
import { Button, Modal, ModalBody, ModalDialog, ModalFooter, ModalHeader, Radio } from '../..';

const ModalDefault = () => {
const [isFirstOpen, setFirstOpen] = useState(false);
const [isSecondOpen, setSecondOpen] = useState(false);
const [modalAlign, setModalAlign] = useState<AlignmentYDictionaryType>(AlignmentY.CENTER);
const [footerAlign, setFooterAlign] = useState<AlignmentXDictionaryType>(AlignmentX.RIGHT);

const toggleFirstModal = () => setFirstOpen(!isFirstOpen);
const toggleSecondModal = () => setSecondOpen(!isSecondOpen);

const handleFirstClose = () => setFirstOpen(false);
const handleSecondClose = () => setSecondOpen(false);
const handleModalAlignChange = (event: ChangeEvent<HTMLInputElement>) => {
setModalAlign(event.target.value as AlignmentYDictionaryType);
};
const handleFooterAlignChange = (event: ChangeEvent<HTMLInputElement>) => {
setFooterAlign(event.target.value as AlignmentXDictionaryType);
};

return (
<>
{/* Set `display: contents` to enable parent stack layout. */}
<div className="spirit-feature-modal-enable-uniform-dialog" style={{ display: 'contents' }}>
<Button onClick={toggleFirstModal}>Open Modal</Button>

<Modal id="example-uniform" isOpen={isFirstOpen} onClose={handleFirstClose}>
<Modal alignmentY={modalAlign} id="example-uniform" isOpen={isFirstOpen} onClose={handleFirstClose}>
<ModalDialog>
<ModalHeader>Modal Title</ModalHeader>
<ModalBody>
Expand All @@ -26,8 +35,74 @@ const ModalDefault = () => {
mollitia mollitia perferendis reprehenderit, voluptate. Cum delectus dicta ducimus eligendi excepturi
natus provident unde. Eveniet, iste, molestiae?
</p>
<form className="mb-tablet-600">
<div>Modal alignment:</div>
<Radio
id="modal-uniform-alignment-top"
UNSAFE_className="mr-600"
label="Top"
value="top"
name="modal_uniform_alignment"
autoComplete="off"
isChecked={modalAlign === AlignmentY.TOP}
onChange={handleModalAlignChange}
/>{' '}
<Radio
id="modal-uniform-alignment-center"
UNSAFE_className="mr-600"
label="Center"
value="center"
name="modal_uniform_alignment"
autoComplete="off"
isChecked={modalAlign === AlignmentY.CENTER}
onChange={handleModalAlignChange}
/>{' '}
<Radio
id="modal-uniform-alignment-bottom"
UNSAFE_className="mr-600"
label="Bottom"
value="bottom"
name="modal_uniform_alignment"
autoComplete="off"
isChecked={modalAlign === AlignmentY.BOTTOM}
onChange={handleModalAlignChange}
/>
</form>
<form className="d-none d-tablet-block">
<div>Footer alignment (from tablet up):</div>
<Radio
id="footer-uniform-alignment-left"
UNSAFE_className="mr-600"
label="Left"
value="left"
name="footer_uniform_alignment"
autoComplete="off"
isChecked={footerAlign === AlignmentX.LEFT}
onChange={handleFooterAlignChange}
/>{' '}
<Radio
id="footer-uniform-alignment-center"
UNSAFE_className="mr-600"
label="Center"
value="center"
name="footer_uniform_alignment"
autoComplete="off"
isChecked={footerAlign === AlignmentX.CENTER}
onChange={handleFooterAlignChange}
/>{' '}
<Radio
id="footer-uniform-alignment-right"
UNSAFE_className="mr-600"
label="Right"
value="right"
name="footer_uniform_alignment"
autoComplete="off"
isChecked={footerAlign === AlignmentX.RIGHT}
onChange={handleFooterAlignChange}
/>
</form>
</ModalBody>
<ModalFooter description="Optional description">
<ModalFooter alignmentX={footerAlign} description="Optional description">
<Button onClick={handleFirstClose}>Primary action</Button>
<Button color="secondary" onClick={handleFirstClose}>
Secondary action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Markdown } from '@storybook/blocks';
import type { Meta, StoryObj } from '@storybook/react';

import { AlignmentY } from '../../../constants';
import { SpiritModalProps } from '../../../types';
import { Button } from '../../Button';
import ReadMe from '../README.md';
Expand All @@ -16,6 +17,13 @@ const meta: Meta<typeof Modal> = {
},
},
argTypes: {
alignmentY: {
control: 'select',
options: [...Object.values(AlignmentY)],
table: {
defaultValue: { summary: AlignmentY.CENTER },
},
},
id: {
control: 'text',
},
Expand All @@ -30,6 +38,7 @@ const meta: Meta<typeof Modal> = {
},
},
args: {
alignmentY: AlignmentY.CENTER,
adamkudrna marked this conversation as resolved.
Show resolved Hide resolved
id: 'modal',
isOpen: false,
},
Expand Down
Loading