@dr.pogodin/react-utils
Version:
Collection of generic ReactJS components and utils
128 lines (125 loc) • 4.18 kB
JavaScript
import { useEffect, useMemo, useRef } from 'react';
import ReactDom from 'react-dom';
import themed from '@dr.pogodin/react-themes';
import baseTheme from "./base-theme.scss";
import S from "./styles.scss";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* The `<Modal>` component implements a simple themeable modal window, wrapped
* into the default theme. `<BaseModal>` exposes the base non-themed component.
* **Children:** Component children are rendered as the modal content.
* @param {object} props Component properties. Beside props documented below,
* [Other theming properties](https://www.npmjs.com/package/@dr.pogodin/react-themes#themed-component-properties) are supported as well.
* @param {function} [props.onCancel] The callback to trigger when user
* clicks outside the modal, or presses Escape. It is expected to hide the
* modal.
* @param {ModalTheme} [props.theme] _Ad hoc_ theme.
*/
const BaseModal = ({
cancelOnScrolling,
children,
containerStyle,
dontDisableScrolling,
onCancel,
overlayStyle,
style,
testId,
testIdForOverlay,
theme
}) => {
const containerRef = useRef(null);
const overlayRef = useRef(null);
// Sets up modal cancellation of scrolling, if opted-in.
useEffect(() => {
if (cancelOnScrolling && onCancel) {
window.addEventListener('scroll', onCancel);
window.addEventListener('wheel', onCancel);
}
return () => {
if (cancelOnScrolling && onCancel) {
window.removeEventListener('scroll', onCancel);
window.removeEventListener('wheel', onCancel);
}
};
}, [cancelOnScrolling, onCancel]);
// Disables window scrolling, if it is not opted-out.
useEffect(() => {
if (!dontDisableScrolling) {
document.body.classList.add(S.scrollingDisabledByModal);
}
return () => {
if (!dontDisableScrolling) {
document.body.classList.remove(S.scrollingDisabledByModal);
}
};
}, [dontDisableScrolling]);
const focusLast = useMemo(() => /*#__PURE__*/_jsx("div", {
onFocus: () => {
const elems = containerRef.current.querySelectorAll('*');
for (let i = elems.length - 1; i >= 0; --i) {
elems[i].focus();
if (document.activeElement === elems[i]) return;
}
overlayRef.current?.focus();
}
// TODO: Have a look at this later.
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
,
tabIndex: 0
}), []);
return /*#__PURE__*/ReactDom.createPortal(/*#__PURE__*/_jsxs("div", {
children: [focusLast, /*#__PURE__*/_jsx("div", {
"aria-label": "Cancel",
className: theme.overlay,
"data-testid": process.env.NODE_ENV === 'production' ? undefined : testIdForOverlay,
onClick: e => {
if (onCancel) {
onCancel();
e.stopPropagation();
}
},
onKeyDown: e => {
if (e.key === 'Escape' && onCancel) {
onCancel();
e.stopPropagation();
}
},
ref: node => {
if (node && node !== overlayRef.current) {
overlayRef.current = node;
node.focus();
}
},
role: "button",
style: overlayStyle,
tabIndex: 0
}), /*#__PURE__*/_jsx("div", {
// eslint-disable-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
"aria-modal": "true",
className: theme.container,
"data-testid": process.env.NODE_ENV === 'production' ? undefined : testId,
onClick: e => {
e.stopPropagation();
},
onWheel: event => {
event.stopPropagation();
},
ref: containerRef,
role: "dialog",
style: style ?? containerStyle,
children: children
}), /*#__PURE__*/_jsx("div", {
onFocus: () => {
overlayRef.current?.focus();
}
// TODO: Have a look at this later.
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
,
tabIndex: 0
}), focusLast]
}), document.body);
};
export default themed(BaseModal, 'Modal', baseTheme);
/* Non-themed version of the Modal. */
export { BaseModal };
//# sourceMappingURL=index.js.map