@mui/material
Version:
Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.
250 lines (241 loc) • 9.03 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ModalManager = void 0;
exports.ariaHidden = ariaHidden;
var _ownerWindow = _interopRequireDefault(require("@mui/utils/ownerWindow"));
var _ownerDocument = _interopRequireDefault(require("@mui/utils/ownerDocument"));
var _getScrollbarSize = _interopRequireDefault(require("@mui/utils/getScrollbarSize"));
// Is a vertical scrollbar displayed?
function isOverflowing(container) {
const doc = (0, _ownerDocument.default)(container);
if (container === doc.body || container === doc.documentElement) {
return (0, _ownerWindow.default)(container).innerWidth > doc.documentElement.clientWidth;
}
return container.scrollHeight > container.clientHeight;
}
function ariaHidden(element, hide) {
if (hide) {
element.setAttribute('aria-hidden', 'true');
} else {
element.removeAttribute('aria-hidden');
}
}
function getPaddingRight(element) {
return parseFloat((0, _ownerWindow.default)(element).getComputedStyle(element).paddingRight) || 0;
}
function isAriaHiddenForbiddenOnElement(element) {
// The forbidden HTML tags are the ones from ARIA specification that
// can be children of body and can't have aria-hidden attribute.
// cf. https://www.w3.org/TR/html-aria/#docconformance
const forbiddenTagNames = ['TEMPLATE', 'SCRIPT', 'STYLE', 'LINK', 'MAP', 'META', 'NOSCRIPT', 'PICTURE', 'COL', 'COLGROUP', 'PARAM', 'SLOT', 'SOURCE', 'TRACK'];
const isForbiddenTagName = forbiddenTagNames.includes(element.tagName);
const isInputHidden = element.tagName === 'INPUT' && element.getAttribute('type') === 'hidden';
return isForbiddenTagName || isInputHidden;
}
// The chain of elements from `node` up to (but not including) `container`
// that should not be aria-hidden.
function getKeepChain(node, container) {
const chain = new Set();
let current = node;
while (current && current !== container) {
chain.add(current);
current = current.parentElement;
}
return current === container ? chain : new Set();
}
// Walk down from `parent` collecting everything that should be aria-hidden.
// An element on the keep chain is stepped through rather than hidden, so the
// modal's own ancestors stay readable while their other children get hidden.
function collectHiddenTargets(parent, keep, keepChain, out) {
[].forEach.call(parent.children, element => {
if (element === keep || isAriaHiddenForbiddenOnElement(element)) {
return;
}
if (keepChain.has(element)) {
collectHiddenTargets(element, keep, keepChain, out);
return;
}
out.add(element);
});
}
/**
* Hides every element of the container from assistive technology except the
* topmost modal and the ancestors it sits inside, and unhides what no longer
* needs hiding. Elements that were already `aria-hidden` before the manager
* touched them are left untouched.
*/
function syncAriaHidden(containerInfo) {
const {
container,
modals
} = containerInfo;
const top = modals[modals.length - 1];
const keep = top.modalRef;
const next = new Set();
collectHiddenTargets(container, keep, getKeepChain(keep, container), next);
next.forEach(element => {
if (!containerInfo.hiddenSet.has(element) && element.getAttribute('aria-hidden') === 'true') {
next.delete(element);
}
});
// Hands the accessibility tree back to a parent dialog when a nested one closes.
containerInfo.hiddenSet.forEach(element => {
if (!next.has(element)) {
ariaHidden(element, false);
}
});
next.forEach(element => ariaHidden(element, true));
if (keep) {
ariaHidden(keep, false);
}
containerInfo.hiddenSet = next;
}
function handleContainer(containerInfo, props) {
const restoreStyle = [];
const container = containerInfo.container;
if (!props.disableScrollLock) {
let scrollContainer;
if (container.parentNode instanceof DocumentFragment) {
scrollContainer = (0, _ownerDocument.default)(container).body;
} else {
// Support html overflow-y: auto for scroll stability between pages
// https://css-tricks.com/snippets/css/force-vertical-scrollbar/
const parent = container.parentElement;
const containerWindow = (0, _ownerWindow.default)(container);
scrollContainer = parent?.nodeName === 'HTML' && containerWindow.getComputedStyle(parent).overflowY === 'scroll' ? parent : container;
}
if (isOverflowing(scrollContainer)) {
// Compute the size before applying overflow hidden to avoid any scroll jumps.
const scrollbarSize = (0, _getScrollbarSize.default)((0, _ownerWindow.default)(scrollContainer));
restoreStyle.push({
value: scrollContainer.style.paddingRight,
property: 'padding-right',
el: scrollContainer
});
// Use computed style, here to get the real padding to add our scrollbar width.
scrollContainer.style.paddingRight = `${getPaddingRight(scrollContainer) + scrollbarSize}px`;
// .mui-fixed is a global helper.
const fixedElements = (0, _ownerDocument.default)(container).querySelectorAll('.mui-fixed');
[].forEach.call(fixedElements, element => {
restoreStyle.push({
value: element.style.paddingRight,
property: 'padding-right',
el: element
});
element.style.paddingRight = `${getPaddingRight(element) + scrollbarSize}px`;
});
}
// Block the scroll even if no scrollbar is visible to account for mobile keyboard
// screensize shrink.
restoreStyle.push({
value: scrollContainer.style.overflow,
property: 'overflow',
el: scrollContainer
}, {
value: scrollContainer.style.overflowX,
property: 'overflow-x',
el: scrollContainer
}, {
value: scrollContainer.style.overflowY,
property: 'overflow-y',
el: scrollContainer
});
scrollContainer.style.overflow = 'hidden';
}
const restore = () => {
restoreStyle.forEach(({
value,
el,
property
}) => {
if (value) {
el.style.setProperty(property, value);
} else {
el.style.removeProperty(property);
}
});
};
return restore;
}
/**
* @ignore - do not document.
*
* Proper state management for containers and the modals in those containers.
* Simplified, but inspired by react-overlay's ModalManager class.
* Used by the Modal to ensure proper styling of containers.
*/
class ModalManager {
containers = [];
modals = [];
add(modal, container) {
let modalIndex = this.modals.indexOf(modal);
if (modalIndex !== -1) {
return modalIndex;
}
modalIndex = this.modals.length;
this.modals.push(modal);
// If the modal we are adding is already in the DOM.
if (modal.modalRef) {
ariaHidden(modal.modalRef, false);
}
const containerIndex = this.containers.findIndex(item => item.container === container);
if (containerIndex !== -1) {
this.containers[containerIndex].modals.push(modal);
syncAriaHidden(this.containers[containerIndex]);
return modalIndex;
}
const containerInfo = {
modals: [modal],
container,
restore: null,
hiddenSet: new Set()
};
this.containers.push(containerInfo);
syncAriaHidden(containerInfo);
return modalIndex;
}
mount(modal, props) {
const containerIndex = this.containers.findIndex(item => item.modals.includes(modal));
const containerInfo = this.containers[containerIndex];
if (!containerInfo.restore) {
containerInfo.restore = handleContainer(containerInfo, props);
}
}
remove(modal, ariaHiddenState = true) {
const modalIndex = this.modals.indexOf(modal);
if (modalIndex === -1) {
return modalIndex;
}
const containerIndex = this.containers.findIndex(item => item.modals.includes(modal));
const containerInfo = this.containers[containerIndex];
containerInfo.modals.splice(containerInfo.modals.indexOf(modal), 1);
this.modals.splice(modalIndex, 1);
// If that was the last modal in a container, clean up the container.
if (containerInfo.modals.length === 0) {
// The modal might be closed before it had the chance to be mounted in the DOM.
if (containerInfo.restore) {
containerInfo.restore();
}
if (modal.modalRef) {
// In case the modal wasn't in the DOM yet.
ariaHidden(modal.modalRef, ariaHiddenState);
}
containerInfo.hiddenSet.forEach(element => ariaHidden(element, false));
containerInfo.hiddenSet.clear();
this.containers.splice(containerIndex, 1);
} else {
syncAriaHidden(containerInfo);
if (modal.modalRef) {
ariaHidden(modal.modalRef, ariaHiddenState);
}
}
return modalIndex;
}
isTopModal(modal) {
return this.modals.length > 0 && this.modals[this.modals.length - 1] === modal;
}
}
exports.ModalManager = ModalManager;