Skip to content

Commit

Permalink
Feat(web-react): Introduce option to disable scrolling inside Modal #…
Browse files Browse the repository at this point in the history
…DS-732

Scrolling inside `ModalDialog` can now be turned off by setting the `isScrollable` prop to false.
  • Loading branch information
crishpeen committed Feb 6, 2024
1 parent 31f9964 commit 4bddca4
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 49 deletions.
3 changes: 2 additions & 1 deletion packages/web-react/src/components/Modal/ModalDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ const ModalDialog = <E extends ElementType = ModalDialogElementType>(
children,
isDockedOnMobile,
isExpandedOnMobile,
isScrollable,
maxHeightFromTabletUp,
preferredHeightOnMobile,
preferredHeightFromTabletUp,
...restProps
} = props;

const { classProps } = useModalStyleProps({ isDockedOnMobile, isExpandedOnMobile });
const { classProps } = useModalStyleProps({ isDockedOnMobile, isExpandedOnMobile, isScrollable });
const { styleProps, props: otherProps } = useStyleProps(restProps);

const customizedHeightStyle: CustomizedHeightCSSProperties = {
Expand Down
27 changes: 27 additions & 0 deletions packages/web-react/src/components/Modal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ This is useful for Modals with dynamic content, e.g. a list of items that can be
</ModalDialog>
```

👉 Please note the preferred height options are ignored when scrolling inside ModalDialog is
[turned off](#disable-scrolling-inside-modaldialog).

👉 Please note the custom height values are considered **preferred:** Modal will not expand beyond the viewport height.

### Custom Max Height
Expand All @@ -129,6 +132,8 @@ You can use the `maxHeightFromTabletUp` option to override the max height on tab
<ModalDialog maxHeightFromTabletUp="700px"></ModalDialog>
```

👉 Please note the max height is ignored when scrolling inside ModalDialog is [turned off](#disable-scrolling-inside-modaldialog).

👉 Please note the max height on mobile screens is currently not customizable. Let us know if you need this feature! 🙏

### API
Expand All @@ -139,6 +144,7 @@ You can use the `maxHeightFromTabletUp` option to override the max height on tab
| `elementType` | [`article` \| `form`] | `article` || ModalDialog element type |
| `isDockedOnMobile` | `bool` | `false` || [REQUIRES FEATURE FLAG](#feature-flag-uniform-appearance-on-all-breakpoints): Dock the ModalDialog to the bottom of the screen on mobile |
| `isExpandedOnMobile` | `bool` | `false` || ModalDialog shrinks to fit the height of its content |
| `isScrollable` | `bool` | `true` || If the ModalDialog should be scrollable. If set to `false`, the dialog will not scroll and will expand to fit the content. |
| `maxHeightFromTabletUp` | `string` | `null` || Max height of the modal. Accepts any valid CSS value. |
| `preferredHeightFromTabletUp` | `string` | `null` || Preferred height of the modal on tablet and larger. Accepts any valid CSS value. |
| `preferredHeightOnMobile` | `string` | `null` || Preferred height of the modal on mobile. Accepts any valid CSS value. |
Expand Down Expand Up @@ -284,6 +290,27 @@ takes over the responsibility for scrolling and provides visual overflow decorat
</ScrollView>
```
### Disable Scrolling Inside ModalDialog
Scrolling inside ModalDialog can be turned off by setting the `ModalDialog` prop `isScrollable` to `false`:
```jsx
<ModalDialog isScrollable="false">
<!---->
</ModalDialog>
```
This way, the ModalBody will expand to fit the height of its content and the whole ModalDialog will scroll in case the
content is longer than user's viewport.
👉 Please note that this modifier class can produce unexpected results when used in combination with ScrollView.
#### ⚠️ DEPRECATION NOTICE
The `isScrollable` prop will be set to `false` by default in the next major release and the ModalDialog will be made
non-scrollable by default. It will be possible to re-enable the inside scrolling by setting the
`isScrollable` boolean prop.
## Stacking Modals
Multiple Modals can be open at the same time. That means, you can open a Modal from another Modal, and they will display
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render } from '@testing-library/react';
import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
Expand All @@ -10,4 +12,54 @@ describe('ModalDialog', () => {
stylePropsTest(ModalDialog);

restPropsTest(ModalDialog, 'article');

it('should render children', () => {
const dom = render(
<ModalDialog>
<div>Test</div>
</ModalDialog>,
);

expect(dom.container).toHaveTextContent('Test');
});

it('should render with custom element type', () => {
const dom = render(
<ModalDialog elementType="section">
<div>Test</div>
</ModalDialog>,
);

expect(dom.container.querySelector('section')).toBeInTheDocument();
});

it('should render docked on mobile', () => {
const dom = render(
<ModalDialog isDockedOnMobile>
<div>Test</div>
</ModalDialog>,
);

expect(dom.container.querySelector('.ModalDialog')).toHaveClass('ModalDialog--dockOnMobile');
});

it('should render expanded on mobile', () => {
const dom = render(
<ModalDialog isExpandedOnMobile>
<div>Test</div>
</ModalDialog>,
);

expect(dom.container.querySelector('.ModalDialog')).toHaveClass('ModalDialog--expandOnMobile');
});

it('should render non scrollable', () => {
const dom = render(
<ModalDialog isScrollable={false}>
<div>Test</div>
</ModalDialog>,
);

expect(dom.container.querySelector('.ModalDialog')).toHaveClass('ModalDialog--nonScrollable');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { Button, Modal, ModalBody, ModalDialog, ModalFooter, ModalHeader, Scroll
const ModalScrollingLongContent = () => {
const [isFirstOpen, setFirstOpen] = useState(false);
const [isSecondOpen, setSecondOpen] = useState(false);
const [isThirdOpen, setThirdOpen] = useState(false);

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

const handleFirstClose = () => setFirstOpen(false);
const handleSecondClose = () => setSecondOpen(false);
const handleThirdClose = () => setThirdOpen(false);

return (
<>
Expand Down Expand Up @@ -131,6 +134,45 @@ const ModalScrollingLongContent = () => {
</ModalFooter>
</ModalDialog>
</Modal>

<Button onClick={toggleThirdModal}>Open Modal with Disabled Scrolling Inside</Button>

<Modal id="example-non-scrolling-modal" isOpen={isThirdOpen} onClose={handleThirdClose}>
<ModalDialog isScrollable={false}>
<ModalHeader>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam at excepturi laudantium magnam mollitia
perferendis reprehenderit, voluptate. Cum delectus dicta
</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>
<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>
<p style={{ marginBottom: '100vh' }}>
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>
<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>
</ModalBody>
<ModalFooter>
<Button onClick={handleThirdClose}>Primary action</Button>
<Button color="secondary" onClick={handleThirdClose}>
Secondary action
</Button>
</ModalFooter>
</ModalDialog>
</Modal>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,54 @@
import React, { ChangeEvent, useState } from 'react';
import { AlignmentX, AlignmentXDictionaryType, AlignmentY, AlignmentYDictionaryType } from '../../..';
import { Button, Modal, ModalBody, ModalDialog, ModalFooter, ModalHeader, Radio } from '../..';
import { Button, Checkbox, Modal, ModalBody, ModalDialog, ModalFooter, ModalHeader, Radio, Stack } from '../..';

const ModalDefault = () => {
const [isFirstOpen, setFirstOpen] = useState(false);
const [isSecondOpen, setSecondOpen] = useState(false);
const [isOpen, setOpen] = useState(false);
const [modalAlign, setModalAlign] = useState<AlignmentYDictionaryType>(AlignmentY.CENTER);
const [footerAlign, setFooterAlign] = useState<AlignmentXDictionaryType>(AlignmentX.RIGHT);
const [isDockedOnMobile, setIsDockedOnMobile] = useState(false);
const [isExpandedOnMobile, setIsExpandedOnMobile] = useState(false);
const [isScrollable, setIsScrollable] = useState(true);

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

const handleFirstClose = () => setFirstOpen(false);
const handleSecondClose = () => setSecondOpen(false);
const handleClose = () => setOpen(false);
const handleModalAlignChange = (event: ChangeEvent<HTMLInputElement>) => {
setModalAlign(event.target.value as AlignmentYDictionaryType);
};
const handleFooterAlignChange = (event: ChangeEvent<HTMLInputElement>) => {
setFooterAlign(event.target.value as AlignmentXDictionaryType);
};
const handleDockedOnMobileChange = (event: ChangeEvent<HTMLInputElement>) => {
setIsDockedOnMobile(event.target.checked);
};
const handleExpandedOnMobileChange = (event: ChangeEvent<HTMLInputElement>) => {
setIsExpandedOnMobile(event.target.checked);
};
const handleScrollableChange = (event: ChangeEvent<HTMLInputElement>) => {
setIsScrollable(event.target.checked);
};

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>
<Button onClick={toggleModal}>Open Modal</Button>

<Modal alignmentY={modalAlign} id="example-uniform" isOpen={isFirstOpen} onClose={handleFirstClose}>
<ModalDialog>
<Modal alignmentY={modalAlign} id="example-uniform" isOpen={isOpen} onClose={handleClose}>
<ModalDialog
isExpandedOnMobile={isExpandedOnMobile}
isDockedOnMobile={isDockedOnMobile}
isScrollable={isScrollable}
>
<ModalHeader>Modal Title</ModalHeader>
<ModalBody>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam at excepturi laudantium magnam
mollitia mollitia perferendis reprehenderit, voluptate. Cum delectus dicta ducimus eligendi excepturi
natus provident unde. Eveniet, iste, molestiae?
</p>
<form className="mb-tablet-600">
<form className="mb-600">
<div>Modal alignment:</div>
<Radio
id="modal-uniform-alignment-top"
Expand Down Expand Up @@ -68,7 +81,7 @@ const ModalDefault = () => {
onChange={handleModalAlignChange}
/>
</form>
<form className="d-none d-tablet-block">
<form className="d-none d-tablet-block mb-600">
<div>Footer alignment (from tablet up):</div>
<Radio
id="footer-uniform-alignment-left"
Expand Down Expand Up @@ -101,31 +114,37 @@ const ModalDefault = () => {
onChange={handleFooterAlignChange}
/>
</form>
<Stack hasSpacing elementType="form">
<Checkbox
autoComplete="off"
id="modal-uniform-docked"
label="Dock on mobile"
name="modal-uniform-docked"
isChecked={isDockedOnMobile}
onChange={handleDockedOnMobileChange}
/>
<Checkbox
autoComplete="off"
id="modal-uniform-expanded"
isDisabled={!isDockedOnMobile}
label="Expand on mobile (docked only)"
name="modal-uniform-expanded"
isChecked={isExpandedOnMobile}
onChange={handleExpandedOnMobileChange}
/>
<Checkbox
autoComplete="off"
id="modal-uniform-non-scrolling"
label="Scrolling inside"
name="modal-uniform-non-scrolling"
isChecked={isScrollable}
onChange={handleScrollableChange}
/>
</Stack>
</ModalBody>
<ModalFooter alignmentX={footerAlign} description="Optional description">
<Button onClick={handleFirstClose}>Primary action</Button>
<Button color="secondary" onClick={handleFirstClose}>
Secondary action
</Button>
</ModalFooter>
</ModalDialog>
</Modal>

<Button onClick={toggleSecondModal}>Open Docked Modal (mobile only)</Button>

<Modal id="example-docked" isOpen={isSecondOpen} onClose={handleSecondClose}>
<ModalDialog isDockedOnMobile isExpandedOnMobile>
<ModalHeader>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>
</ModalBody>
<ModalFooter description="Optional description">
<Button onClick={handleSecondClose}>Primary action</Button>
<Button color="secondary" onClick={handleSecondClose}>
<Button onClick={handleClose}>Primary action</Button>
<Button color="secondary" onClick={handleClose}>
Secondary action
</Button>
</ModalFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const meta: Meta<typeof ModalDialog> = {
isExpandedOnMobile: {
control: 'boolean',
},
isScrollable: {
control: 'boolean',
},
maxHeightFromTabletUp: {
control: 'text',
},
Expand All @@ -30,6 +33,7 @@ const meta: Meta<typeof ModalDialog> = {
args: {
isDockedOnMobile: false,
isExpandedOnMobile: false,
isScrollable: true,
maxHeightFromTabletUp: '',
preferredHeightOnMobile: '',
preferredHeightFromTabletUp: '',
Expand Down
23 changes: 10 additions & 13 deletions packages/web-react/src/components/Modal/useModalStyleProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface ModalStylesProps {
footerAlignment?: AlignmentXDictionaryType;
isDockedOnMobile?: boolean;
isExpandedOnMobile?: boolean;
isScrollable?: boolean;
modalAlignment?: AlignmentYDictionaryType;
}

Expand All @@ -26,19 +27,13 @@ export interface ModalStylesReturn {
};
}

export function useModalStyleProps(
{
footerAlignment = AlignmentX.RIGHT,
isDockedOnMobile,
isExpandedOnMobile,
modalAlignment = AlignmentY.CENTER,
}: ModalStylesProps = {
footerAlignment: AlignmentX.RIGHT,
isDockedOnMobile: false,
isExpandedOnMobile: false,
modalAlignment: AlignmentX.CENTER,
},
): ModalStylesReturn {
export function useModalStyleProps({
footerAlignment = AlignmentX.RIGHT,
isDockedOnMobile = false,
isExpandedOnMobile = false,
isScrollable = true,
modalAlignment = AlignmentY.CENTER,
}: ModalStylesProps = {}): ModalStylesReturn {
const modalClass = useClassNamePrefix('Modal');
const modalAlignClasses = {
top: `${modalClass}--top`,
Expand All @@ -48,6 +43,7 @@ export function useModalStyleProps(
const modalDialogClass = `${modalClass}Dialog`;
const modalDialogDockedOnMobileClass = `${modalDialogClass}--dockOnMobile`;
const modalDialogExpandedOnMobileClass = `${modalDialogClass}--expandOnMobile`;
const modalDialogNonScrollableClass = `${modalDialogClass}--nonScrollable`;
const modalHeaderClass = `${modalClass}Header`;
const modalTitleClass = `${modalHeaderClass}__title`;
const modalBodyClass = `${modalClass}Body`;
Expand All @@ -64,6 +60,7 @@ export function useModalStyleProps(
dialog: classNames(modalDialogClass, {
[modalDialogDockedOnMobileClass]: isDockedOnMobile,
[modalDialogExpandedOnMobileClass]: isExpandedOnMobile,
[modalDialogNonScrollableClass]: !isScrollable,
}),
title: modalTitleClass,
header: modalHeaderClass,
Expand Down
Loading

0 comments on commit 4bddca4

Please sign in to comment.