@bianic-ui/modal
Version:
An accessible dialog (modal) component for React & Bianic UI
159 lines (143 loc) • 4.75 kB
JavaScript
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { useIds } from "@bianic-ui/hooks";
import { callAllHandlers, mergeRefs } from "@bianic-ui/utils";
import { hideOthers } from "aria-hidden";
import { useCallback, useEffect, useRef, useState } from "react";
import { manager, useModalManager } from "./modal-manager";
/**
* Modal hook that manages all the logic for the modal dialog widget
* and returns prop getters, state and actions.
*
* @param props
*/
export function useModal(props) {
var {
isOpen,
onClose,
id,
closeOnOverlayClick = true,
closeOnEsc = true,
useInert = true,
onOverlayClick: onOverlayClickProp,
onEsc
} = props;
var dialogRef = useRef(null);
var overlayRef = useRef(null);
var [dialogId, headerId, bodyId] = useIds(id, "bianic-modal", "bianic-modal--header", "bianic-modal--body");
/**
* Hook used to polyfill `aria-modal` for older browsers.
* It uses `aria-hidden` to all other nodes.
*
* @see https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/
*/
useAriaHidden(dialogRef, isOpen && useInert);
/**
* Hook use to manage multiple or nested modals
*/
useModalManager(dialogRef, isOpen);
var mouseDownTarget = useRef(null);
var onMouseDown = useCallback(event => {
mouseDownTarget.current = event.target;
}, []);
var onKeyDown = useCallback(event => {
if (event.key === "Escape") {
event.stopPropagation();
if (closeOnEsc) {
onClose == null ? void 0 : onClose();
}
onEsc == null ? void 0 : onEsc();
}
}, [closeOnEsc, onClose, onEsc]);
var onOverlayClick = useCallback(event => {
event.stopPropagation();
/**
* Make sure the event starts and ends on the same DOM element.
*
* This is used to prevent the modal from closing when you
* start dragging from the content, and release drag outside the content.
*
* We prevent this because it's technically not a considered "click outside"
*/
if (mouseDownTarget.current !== event.target) return;
/**
* When you click on the overlay, we want to remove only the topmost modal
*/
if (manager.isTopModal(dialogRef)) {
if (closeOnOverlayClick) {
onClose == null ? void 0 : onClose();
}
onOverlayClickProp == null ? void 0 : onOverlayClickProp();
}
}, [onClose, closeOnOverlayClick, onOverlayClickProp]);
var [headerMounted, setHeaderMounted] = useState(false);
var [bodyMounted, setBodyMounted] = useState(false);
var getContentProps = useCallback(function (props, ref) {
if (props === void 0) {
props = {};
}
if (ref === void 0) {
ref = null;
}
return _extends({
role: "dialog"
}, props, {
ref: mergeRefs(ref, dialogRef),
id: dialogId,
tabIndex: -1,
"aria-modal": true,
"aria-labelledby": headerMounted ? headerId : undefined,
"aria-describedby": bodyMounted ? bodyId : undefined,
onClick: callAllHandlers(props.onClick, event => event.stopPropagation())
});
}, [bodyId, bodyMounted, dialogId, headerId, headerMounted]);
var getOverlayProps = useCallback(function (props, ref) {
if (props === void 0) {
props = {};
}
if (ref === void 0) {
ref = null;
}
return _extends({}, props, {
ref: mergeRefs(ref, overlayRef),
onClick: callAllHandlers(props.onClick, onOverlayClick),
onKeyDown: callAllHandlers(props.onKeyDown, onKeyDown),
onMouseDown: callAllHandlers(props.onMouseDown, onMouseDown)
});
}, [onKeyDown, onMouseDown, onOverlayClick]);
return {
isOpen,
onClose,
headerId,
bodyId,
setBodyMounted,
setHeaderMounted,
dialogRef,
overlayRef,
getContentProps,
getOverlayProps
};
}
/**
* Modal hook to polyfill `aria-modal`.
*
* It applies `aria-hidden` to elements behind the modal
* to indicate that they're `inert`.
*
* @param ref React ref of the node
* @param shouldHide whether `aria-hidden` should be applied
*/
export function useAriaHidden(ref, shouldHide) {
useEffect(() => {
if (!ref.current) return;
var undo = null;
if (shouldHide && ref.current) {
undo = hideOthers(ref.current);
}
return () => {
if (shouldHide) {
undo == null ? void 0 : undo();
}
};
}, [shouldHide, ref]);
}
//# sourceMappingURL=use-modal.js.map