Skip to content

Commit

Permalink
Feat(web-react): Introduce vertical alignment options for Modal #DS…
Browse files Browse the repository at this point in the history
…-940

New `alignmentY` option can be set to `top`, `center` (default), or `bottom`.
  • Loading branch information
adamkudrna committed Jan 22, 2024
1 parent e7b9317 commit 67e2594
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 43 deletions.
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,
id: 'modal',
isOpen: false,
},
Expand Down
Loading

0 comments on commit 67e2594

Please sign in to comment.