+);
export const Modal = ({
autoFocus,
@@ -80,52 +52,21 @@ export const Modal = ({
closeButtonRef,
portalId,
position,
+ preventScrollUnderneath,
primaryButtonRef,
size,
...restProps
}) => {
const childrenWrapperRef = useRef();
- const keyPressHandler = (e) => {
- if (e.key === 'Escape' && closeButtonRef?.current != null) {
- closeButtonRef.current.click();
- }
-
- if (e.key === 'Enter' && e.target.nodeName !== 'BUTTON' && primaryButtonRef?.current != null) {
- primaryButtonRef.current.click();
- }
- };
-
- useEffect(() => {
- window.document.addEventListener('keydown', keyPressHandler, false);
- const removeKeyPressHandler = () => {
- window.document.removeEventListener('keydown', keyPressHandler, false);
- };
-
- // If `autoFocus` is set to `true`, following code finds first form field element
- // (input, textarea or select) or primary button and auto focuses it. This is necessary
- // to have focus on one of those elements to be able to submit form by pressing Enter key.
- if (autoFocus) {
- if (childrenWrapperRef?.current != null) {
- const childrenWrapperElement = childrenWrapperRef.current;
- const childrenElements = childrenWrapperElement.querySelectorAll('*');
- const formFieldEl = Array.from(childrenElements).find(
- (element) => ['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && !element.disabled,
- );
-
- if (formFieldEl) {
- formFieldEl.focus();
- return removeKeyPressHandler;
- }
- }
-
- if (primaryButtonRef?.current != null) {
- primaryButtonRef.current.focus();
- }
- }
+ useModalFocus(
+ autoFocus,
+ childrenWrapperRef,
+ primaryButtonRef,
+ closeButtonRef,
+ );
- return removeKeyPressHandler;
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
+ useModalScrollPrevention(preventScrollUnderneath);
if (portalId === null) {
return preRender(
@@ -157,14 +98,16 @@ Modal.defaultProps = {
closeButtonRef: null,
portalId: null,
position: 'center',
+ preventScrollUnderneath: 'default',
primaryButtonRef: null,
size: 'medium',
};
Modal.propTypes = {
/**
- * If `true`, focus the first input element in the modal or primary button (referenced by the `primaryButtonRef` prop)
- * when the modal is opened.
+ * If `true`, focus the first input element in the `Modal`, or primary button (referenced by the `primaryButtonRef`
+ * prop), or other focusable element when the `Modal` is opened. If there are none or `autoFocus` is set to `false`,
+ * focus the Modal itself.
*/
autoFocus: PropTypes.bool,
/**
@@ -192,6 +135,24 @@ Modal.propTypes = {
* Vertical position of the modal inside browser window.
*/
position: PropTypes.oneOf(['top', 'center']),
+ /**
+ * Mode in which Modal prevents scroll of elements bellow:
+ * * `default` - Modal prevents scroll on the `body` element
+ * * `off` - Modal does not prevent any scroll
+ * * object
+ * * * `reset` - method called on Modal's unmount to reset scroll prevention
+ * * * `start` - method called on Modal's mount to custom scroll prevention
+ */
+ preventScrollUnderneath: PropTypes.oneOfType([
+ PropTypes.oneOf([
+ 'default',
+ 'off',
+ ]),
+ PropTypes.shape({
+ reset: PropTypes.func,
+ start: PropTypes.func,
+ }),
+ ]),
/**
* Reference to primary button element. It is used to submit modal when Enter key is pressed and as fallback
* when `autoFocus` functionality does not find any input element to be focused.
diff --git a/src/lib/components/Modal/README.mdx b/src/lib/components/Modal/README.mdx
index 9fd25035..f100a27e 100644
--- a/src/lib/components/Modal/README.mdx
+++ b/src/lib/components/Modal/README.mdx
@@ -25,6 +25,7 @@ import {
ModalTitle,
Radio,
ScrollView,
+ TextArea,
TextField,
Toolbar,
ToolbarGroup,
@@ -116,8 +117,9 @@ See [API](#api) for all available options.
- Modal **automatically focuses the first non-disabled form field** by default
which allows users to confirm the modal by hitting the enter key. When no
- field is found then the primary button (in the footer) is focused. To turn
- this feature off, set the `autofocus` prop to `false`.
+ field is found then the primary button (in the footer) is focused. If there
+ are neither, it tries to focus any other focusable elements. In case there
+ are none, or [autoFocus](#autoFocus) is disabled, Modal itself is focused.
- **Avoid stacking** of modals. While it may technically work, the modal is just
not designed for that.
@@ -824,24 +826,147 @@ can be closed by pressing the `Escape` key.
To enable it, you just need to pass a reference to the buttons using
`primaryButtonRef` and `closeButtonRef` props on Modal. The advantage of passing
-a reference to the button is that if the button is disabled, the key press will
-not fire the event.
-
-👉 We strongly recommend using this feature together with
-[Autofocus](#autofocus) for a better user experience.
+the reference to the button is that if the button is disabled, the key press
+will not fire the event.
## Autofocus
Autofocus is implemented to enhance the user experience by automatically
-focussing an element within the modal.
+focusing an element within the Modal.
How does it work? It tries to find `input`, `textarea`, and `select` elements
inside of Modal and moves focus onto the first non-disabled one. If none is
found and the `primaryButtonRef` prop on Modal is set, then the primary button
-is focused.
+is focused. If there are neither, it tries to focus any other focusable elements.
+In case there are none or `autoFocus` is disabled, Modal itself is focused.
-Autofocus is enabled by default, so if you want to control the focus of
-elements manually, set the `autoFocus` prop on Modal to `false`.
+
+ {() => {
+ const [modalOpen, setModalOpen] = React.useState(null);
+ const modalPrimaryButtonRef = React.useRef();
+ const modalCloseButtonRef = React.useRef();
+ return (
+ <>
+
## Scrolling Long Content
@@ -961,7 +1086,7 @@ independent of the page itself. This can be done in three ways using the
{modalOpen && (
` element and accounts for the scrollbar
+width. If you choose `off`, there will be no scroll prevention. If you need more
+flexibility, define your methods `start` (called on Modal's mount) and `reset`
+(called on Modal unmount) wrapped by an object and handle scroll prevention
+yourself.
+
+
+ {() => {
+ const [modalOpen, setModalOpen] = React.useState(null);
+ const modalPrimaryButtonRef = React.useRef();
+ const modalCloseButtonRef = React.useRef();
+ const customScrollPreventionObject = {
+ start: () => {
+ // YOUR CUSTOM SCROLL PREVENTING LOGIC GOES HERE
+ window.document.body.style.overflowY = 'hidden'
+ },
+ reset: () => {
+ // YOUR CUSTOM SCROLL RE-ENABLING LOGIC GOES HERE
+ window.document.body.style.overflowY = 'auto'
+ },
+ };
+ return (
+ <>
+ setModalOpen(1)}
+ />
+ setModalOpen(2)}
+ />
+ setModalOpen(3)}
+ />
+