Skip to content

Instantly share code, notes, and snippets.

@danethurber
Created February 7, 2019 17:35
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danethurber/a586dbc9097e2e5696719c390a00c683 to your computer and use it in GitHub Desktop.
Save danethurber/a586dbc9097e2e5696719c390a00c683 to your computer and use it in GitHub Desktop.
modal with react hooks
import React, { useEffect, useLayoutEffect, useRef } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import "./Modal.css";
const Modal = props => {
const ref = useRef();
useFocusLock(ref);
useKeyUp("Escape", props.onRequestClose);
useLockBodyScroll();
useOnClickOutside(ref, props.onRequestClose);
return ReactDOM.createPortal(
<>
<aside className="modal__cover" />
<div className="modal" ref={ref}>
<div className="modal__body">{props.children}</div>
<button className="modal__close" onClick={props.onRequestClose}>
<svg className="modal__close__icon" viewBox="0 0 40 40">
<path d="M 10,10 L 30,30 M 30,10 L 10,30" />
</svg>
</button>
</div>
</>,
document.body
);
};
Modal.propTypes = {
onRequestClose: PropTypes.func.isRequired
};
export default Modal;
// Focus lock hook
const FOCUSABLE_SELECTORS = [
'[contenteditable]:not([contenteditable="false"])',
"[tabindex]",
"a[href]",
"audio[controls]",
"button",
"iframe",
"input",
"select",
"textarea",
"video[controls]"
];
const hasNegativeTabIndex = el =>
el.getAttribute("tabindex") && el.getAttribute("tabindex") < 0;
const getFocusableChildNodes = el => {
const selectAll = FOCUSABLE_SELECTORS.join(",");
const nodelist = el.querySelectorAll(selectAll);
return Array.from(nodelist || []).filter(node => !hasNegativeTabIndex(node));
};
function useFocusLock(ref) {
useEffect(() => {
const prevFocusedElement = document.activeElement;
let focusableNodes = [];
if (ref && ref.current) {
focusableNodes = getFocusableChildNodes(ref.current);
const firstNode = focusableNodes[0];
if (firstNode) firstNode.focus();
}
const onKeyDown = event => {
const isTab = event.key === "Tab";
const withShiftKey = event.shiftKey;
if (!isTab) return;
const { activeElement } = document;
const first = focusableNodes[0];
const last = focusableNodes[focusableNodes.length - 1];
if (activeElement === first && withShiftKey) {
last.focus();
event.preventDefault();
event.stopPropagation();
} else if (activeElement === last && !withShiftKey) {
first.focus();
event.preventDefault();
event.stopPropagation();
}
};
window.addEventListener("keydown", onKeyDown);
return function cleanup() {
window.removeEventListener("keydown", onKeyDown);
if (prevFocusedElement) prevFocusedElement.focus();
};
});
}
// lock body scroll hook
function useLockBodyScroll() {
useLayoutEffect(() => {
document.body.style.overflow = "hidden";
return () => (document.body.style.overflow = "visible");
}, []);
}
// key up hook
function useKeyUp(targetKey, handler) {
const onKeyUp = ({ key }) => {
if (key === targetKey) handler();
};
useEffect(() => {
window.addEventListener("keyup", onKeyUp);
return () => {
window.removeEventListener("keyup", onKeyUp);
};
}, []);
}
// click outside hook
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = event => {
if (!ref.current || ref.current.contains(event.target)) return;
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, []);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment