UNPKG

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