UNPKG

7.99 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.ModalManager = void 0;
7exports.ariaHidden = ariaHidden;
8var _utils = require("@mui/utils");
9// Is a vertical scrollbar displayed?
10function isOverflowing(container) {
11 const doc = (0, _utils.unstable_ownerDocument)(container);
12 if (doc.body === container) {
13 return (0, _utils.unstable_ownerWindow)(container).innerWidth > doc.documentElement.clientWidth;
14 }
15 return container.scrollHeight > container.clientHeight;
16}
17function ariaHidden(element, hide) {
18 if (hide) {
19 element.setAttribute('aria-hidden', 'true');
20 } else {
21 element.removeAttribute('aria-hidden');
22 }
23}
24function getPaddingRight(element) {
25 return parseInt((0, _utils.unstable_ownerWindow)(element).getComputedStyle(element).paddingRight, 10) || 0;
26}
27function isAriaHiddenForbiddenOnElement(element) {
28 // The forbidden HTML tags are the ones from ARIA specification that
29 // can be children of body and can't have aria-hidden attribute.
30 // cf. https://www.w3.org/TR/html-aria/#docconformance
31 const forbiddenTagNames = ['TEMPLATE', 'SCRIPT', 'STYLE', 'LINK', 'MAP', 'META', 'NOSCRIPT', 'PICTURE', 'COL', 'COLGROUP', 'PARAM', 'SLOT', 'SOURCE', 'TRACK'];
32 const isForbiddenTagName = forbiddenTagNames.includes(element.tagName);
33 const isInputHidden = element.tagName === 'INPUT' && element.getAttribute('type') === 'hidden';
34 return isForbiddenTagName || isInputHidden;
35}
36function ariaHiddenSiblings(container, mountElement, currentElement, elementsToExclude, hide) {
37 const blacklist = [mountElement, currentElement, ...elementsToExclude];
38 [].forEach.call(container.children, element => {
39 const isNotExcludedElement = !blacklist.includes(element);
40 const isNotForbiddenElement = !isAriaHiddenForbiddenOnElement(element);
41 if (isNotExcludedElement && isNotForbiddenElement) {
42 ariaHidden(element, hide);
43 }
44 });
45}
46function findIndexOf(items, callback) {
47 let idx = -1;
48 items.some((item, index) => {
49 if (callback(item)) {
50 idx = index;
51 return true;
52 }
53 return false;
54 });
55 return idx;
56}
57function handleContainer(containerInfo, props) {
58 const restoreStyle = [];
59 const container = containerInfo.container;
60 if (!props.disableScrollLock) {
61 if (isOverflowing(container)) {
62 // Compute the size before applying overflow hidden to avoid any scroll jumps.
63 const scrollbarSize = (0, _utils.unstable_getScrollbarSize)((0, _utils.unstable_ownerWindow)(container));
64 restoreStyle.push({
65 value: container.style.paddingRight,
66 property: 'padding-right',
67 el: container
68 });
69 // Use computed style, here to get the real padding to add our scrollbar width.
70 container.style.paddingRight = `${getPaddingRight(container) + scrollbarSize}px`;
71
72 // .mui-fixed is a global helper.
73 const fixedElements = (0, _utils.unstable_ownerDocument)(container).querySelectorAll('.mui-fixed');
74 [].forEach.call(fixedElements, element => {
75 restoreStyle.push({
76 value: element.style.paddingRight,
77 property: 'padding-right',
78 el: element
79 });
80 element.style.paddingRight = `${getPaddingRight(element) + scrollbarSize}px`;
81 });
82 }
83 let scrollContainer;
84 if (container.parentNode instanceof DocumentFragment) {
85 scrollContainer = (0, _utils.unstable_ownerDocument)(container).body;
86 } else {
87 // Support html overflow-y: auto for scroll stability between pages
88 // https://css-tricks.com/snippets/css/force-vertical-scrollbar/
89 const parent = container.parentElement;
90 const containerWindow = (0, _utils.unstable_ownerWindow)(container);
91 scrollContainer = parent?.nodeName === 'HTML' && containerWindow.getComputedStyle(parent).overflowY === 'scroll' ? parent : container;
92 }
93
94 // Block the scroll even if no scrollbar is visible to account for mobile keyboard
95 // screensize shrink.
96 restoreStyle.push({
97 value: scrollContainer.style.overflow,
98 property: 'overflow',
99 el: scrollContainer
100 }, {
101 value: scrollContainer.style.overflowX,
102 property: 'overflow-x',
103 el: scrollContainer
104 }, {
105 value: scrollContainer.style.overflowY,
106 property: 'overflow-y',
107 el: scrollContainer
108 });
109 scrollContainer.style.overflow = 'hidden';
110 }
111 const restore = () => {
112 restoreStyle.forEach(({
113 value,
114 el,
115 property
116 }) => {
117 if (value) {
118 el.style.setProperty(property, value);
119 } else {
120 el.style.removeProperty(property);
121 }
122 });
123 };
124 return restore;
125}
126function getHiddenSiblings(container) {
127 const hiddenSiblings = [];
128 [].forEach.call(container.children, element => {
129 if (element.getAttribute('aria-hidden') === 'true') {
130 hiddenSiblings.push(element);
131 }
132 });
133 return hiddenSiblings;
134}
135/**
136 * @ignore - do not document.
137 *
138 * Proper state management for containers and the modals in those containers.
139 * Simplified, but inspired by react-overlay's ModalManager class.
140 * Used by the Modal to ensure proper styling of containers.
141 */
142class ModalManager {
143 constructor() {
144 this.modals = [];
145 this.containers = [];
146 }
147 add(modal, container) {
148 let modalIndex = this.modals.indexOf(modal);
149 if (modalIndex !== -1) {
150 return modalIndex;
151 }
152 modalIndex = this.modals.length;
153 this.modals.push(modal);
154
155 // If the modal we are adding is already in the DOM.
156 if (modal.modalRef) {
157 ariaHidden(modal.modalRef, false);
158 }
159 const hiddenSiblings = getHiddenSiblings(container);
160 ariaHiddenSiblings(container, modal.mount, modal.modalRef, hiddenSiblings, true);
161 const containerIndex = findIndexOf(this.containers, item => item.container === container);
162 if (containerIndex !== -1) {
163 this.containers[containerIndex].modals.push(modal);
164 return modalIndex;
165 }
166 this.containers.push({
167 modals: [modal],
168 container,
169 restore: null,
170 hiddenSiblings
171 });
172 return modalIndex;
173 }
174 mount(modal, props) {
175 const containerIndex = findIndexOf(this.containers, item => item.modals.includes(modal));
176 const containerInfo = this.containers[containerIndex];
177 if (!containerInfo.restore) {
178 containerInfo.restore = handleContainer(containerInfo, props);
179 }
180 }
181 remove(modal, ariaHiddenState = true) {
182 const modalIndex = this.modals.indexOf(modal);
183 if (modalIndex === -1) {
184 return modalIndex;
185 }
186 const containerIndex = findIndexOf(this.containers, item => item.modals.includes(modal));
187 const containerInfo = this.containers[containerIndex];
188 containerInfo.modals.splice(containerInfo.modals.indexOf(modal), 1);
189 this.modals.splice(modalIndex, 1);
190
191 // If that was the last modal in a container, clean up the container.
192 if (containerInfo.modals.length === 0) {
193 // The modal might be closed before it had the chance to be mounted in the DOM.
194 if (containerInfo.restore) {
195 containerInfo.restore();
196 }
197 if (modal.modalRef) {
198 // In case the modal wasn't in the DOM yet.
199 ariaHidden(modal.modalRef, ariaHiddenState);
200 }
201 ariaHiddenSiblings(containerInfo.container, modal.mount, modal.modalRef, containerInfo.hiddenSiblings, false);
202 this.containers.splice(containerIndex, 1);
203 } else {
204 // Otherwise make sure the next top modal is visible to a screen reader.
205 const nextTop = containerInfo.modals[containerInfo.modals.length - 1];
206 // as soon as a modal is adding its modalRef is undefined. it can't set
207 // aria-hidden because the dom element doesn't exist either
208 // when modal was unmounted before modalRef gets null
209 if (nextTop.modalRef) {
210 ariaHidden(nextTop.modalRef, false);
211 }
212 }
213 return modalIndex;
214 }
215 isTopModal(modal) {
216 return this.modals.length > 0 && this.modals[this.modals.length - 1] === modal;
217 }
218}
219exports.ModalManager = ModalManager;
\No newline at end of file