UNPKG

8.04 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.ariaHidden = ariaHidden;
7exports.default = void 0;
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, show) {
18 if (show) {
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.indexOf(element.tagName) !== -1;
33 const isInputHidden = element.tagName === 'INPUT' && element.getAttribute('type') === 'hidden';
34 return isForbiddenTagName || isInputHidden;
35}
36function ariaHiddenSiblings(container, mountElement, currentElement, elementsToExclude, show) {
37 const blacklist = [mountElement, currentElement, ...elementsToExclude];
38 [].forEach.call(container.children, element => {
39 const isNotExcludedElement = blacklist.indexOf(element) === -1;
40 const isNotForbiddenElement = !isAriaHiddenForbiddenOnElement(element);
41 if (isNotExcludedElement && isNotForbiddenElement) {
42 ariaHidden(element, show);
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_ownerDocument)(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 // Improve Gatsby support
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 == null ? void 0 : 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.containers = void 0;
145 this.modals = void 0;
146 this.modals = [];
147 this.containers = [];
148 }
149 add(modal, container) {
150 let modalIndex = this.modals.indexOf(modal);
151 if (modalIndex !== -1) {
152 return modalIndex;
153 }
154 modalIndex = this.modals.length;
155 this.modals.push(modal);
156
157 // If the modal we are adding is already in the DOM.
158 if (modal.modalRef) {
159 ariaHidden(modal.modalRef, false);
160 }
161 const hiddenSiblings = getHiddenSiblings(container);
162 ariaHiddenSiblings(container, modal.mount, modal.modalRef, hiddenSiblings, true);
163 const containerIndex = findIndexOf(this.containers, item => item.container === container);
164 if (containerIndex !== -1) {
165 this.containers[containerIndex].modals.push(modal);
166 return modalIndex;
167 }
168 this.containers.push({
169 modals: [modal],
170 container,
171 restore: null,
172 hiddenSiblings
173 });
174 return modalIndex;
175 }
176 mount(modal, props) {
177 const containerIndex = findIndexOf(this.containers, item => item.modals.indexOf(modal) !== -1);
178 const containerInfo = this.containers[containerIndex];
179 if (!containerInfo.restore) {
180 containerInfo.restore = handleContainer(containerInfo, props);
181 }
182 }
183 remove(modal, ariaHiddenState = true) {
184 const modalIndex = this.modals.indexOf(modal);
185 if (modalIndex === -1) {
186 return modalIndex;
187 }
188 const containerIndex = findIndexOf(this.containers, item => item.modals.indexOf(modal) !== -1);
189 const containerInfo = this.containers[containerIndex];
190 containerInfo.modals.splice(containerInfo.modals.indexOf(modal), 1);
191 this.modals.splice(modalIndex, 1);
192
193 // If that was the last modal in a container, clean up the container.
194 if (containerInfo.modals.length === 0) {
195 // The modal might be closed before it had the chance to be mounted in the DOM.
196 if (containerInfo.restore) {
197 containerInfo.restore();
198 }
199 if (modal.modalRef) {
200 // In case the modal wasn't in the DOM yet.
201 ariaHidden(modal.modalRef, ariaHiddenState);
202 }
203 ariaHiddenSiblings(containerInfo.container, modal.mount, modal.modalRef, containerInfo.hiddenSiblings, false);
204 this.containers.splice(containerIndex, 1);
205 } else {
206 // Otherwise make sure the next top modal is visible to a screen reader.
207 const nextTop = containerInfo.modals[containerInfo.modals.length - 1];
208 // as soon as a modal is adding its modalRef is undefined. it can't set
209 // aria-hidden because the dom element doesn't exist either
210 // when modal was unmounted before modalRef gets null
211 if (nextTop.modalRef) {
212 ariaHidden(nextTop.modalRef, false);
213 }
214 }
215 return modalIndex;
216 }
217 isTopModal(modal) {
218 return this.modals.length > 0 && this.modals[this.modals.length - 1] === modal;
219 }
220}
221exports.default = ModalManager;
\No newline at end of file