diff --git a/packages/mui-material/src/Modal/Modal.test.js b/packages/mui-material/src/Modal/Modal.test.js
index f04146da17e7aa..77d7a4faa427fe 100644
--- a/packages/mui-material/src/Modal/Modal.test.js
+++ b/packages/mui-material/src/Modal/Modal.test.js
@@ -799,6 +799,29 @@ describe('', () => {
setProps({ anchorEl: document.body });
});
+ it('should remove aria-hidden from the previous container when the container changes', () => {
+ const root = document.createElement('div');
+ root.setAttribute('id', 'root');
+ document.body.appendChild(root);
+
+ try {
+ const { setProps } = render(
+
+
+ ,
+ );
+
+ // The modal is mounted in the body, making #root an aria-hidden sibling.
+ expect(root).toBeInaccessible();
+
+ // Moving the modal into #root must remove the stale aria-hidden from it.
+ setProps({ container: root });
+ expect(root).not.toBeInaccessible();
+ } finally {
+ document.body.removeChild(root);
+ }
+ });
+
it('should finish closing when the container changes during the exit transition', () => {
function TestCase(props) {
const firstContainerRef = React.useRef(null);
diff --git a/packages/mui-material/src/Modal/ModalManager.test.ts b/packages/mui-material/src/Modal/ModalManager.test.ts
index 2d57307cdb651e..a649ad6ff13e57 100644
--- a/packages/mui-material/src/Modal/ModalManager.test.ts
+++ b/packages/mui-material/src/Modal/ModalManager.test.ts
@@ -350,6 +350,20 @@ describe('ModalManager', () => {
expect(modal2).not.toBeInaccessible();
});
+ it('should not add aria-hidden to container siblings that contain the modal', () => {
+ // Simulates a non-portaled modal (disablePortal) whose DOM node is nested
+ // inside one of the container's children. Hiding that child would make the
+ // modal itself inaccessible.
+ const nestedParent = document.createElement('div');
+ const nestedModalRef = document.createElement('div');
+ nestedParent.appendChild(nestedModalRef);
+ container2.appendChild(nestedParent);
+
+ modalManager.add({ ...getDummyModal(), modalRef: nestedModalRef }, container2);
+
+ expect(nestedParent).not.toBeInaccessible();
+ });
+
it('should add aria-hidden to container siblings', () => {
const secondSibling = document.createElement('input');
container2.appendChild(secondSibling);
@@ -438,5 +452,36 @@ describe('ModalManager', () => {
expect(container2.children[1]).toBeInaccessible();
expect(container2.children[2]).not.toBeInaccessible();
});
+
+ it('should restore aria-hidden when a modal is re-registered to a nested container', () => {
+ const containerA = document.createElement('div');
+ const containerB = document.createElement('div');
+ const sibling = document.createElement('div');
+ const modalRef = document.createElement('div');
+ containerA.appendChild(sibling);
+ containerA.appendChild(containerB);
+ containerA.appendChild(modalRef);
+ document.body.appendChild(containerA);
+
+ try {
+ const modal = { mount: containerA, modalRef };
+ modalManager.add(modal, containerA);
+ modalManager.mount(modal, {});
+ expect(containerB).toBeInaccessible();
+ expect(sibling).toBeInaccessible();
+
+ // Simulates the portal having moved into containerB before the manager is notified.
+ modal.mount = containerB;
+
+ modalManager.remove(modal);
+ modalManager.add(modal, containerB);
+
+ expect(containerB).not.toBeInaccessible();
+ expect(sibling).not.toBeInaccessible();
+ expect(modalRef).not.toBeInaccessible();
+ } finally {
+ document.body.removeChild(containerA);
+ }
+ });
});
});
diff --git a/packages/mui-material/src/Modal/ModalManager.ts b/packages/mui-material/src/Modal/ModalManager.ts
index 54a976e7bd4cf8..4fcfa49a0f9d3b 100644
--- a/packages/mui-material/src/Modal/ModalManager.ts
+++ b/packages/mui-material/src/Modal/ModalManager.ts
@@ -66,7 +66,11 @@ function ariaHiddenSiblings(
[].forEach.call(container.children, (element: Element) => {
const isNotExcludedElement = !blacklist.includes(element);
const isNotForbiddenElement = !isAriaHiddenForbiddenOnElement(element);
- if (isNotExcludedElement && isNotForbiddenElement) {
+ // Never hide an element that contains the modal, otherwise the modal itself
+ // becomes inaccessible. This happens when the modal is not portaled into the
+ // container, for example when `disablePortal` is used.
+ const isNotModalAncestor = currentElement ? !element.contains(currentElement) : true;
+ if (isNotExcludedElement && isNotForbiddenElement && isNotModalAncestor) {
ariaHidden(element, hide);
}
});
@@ -268,7 +272,7 @@ export class ModalManager {
ariaHiddenSiblings(
containerInfo.container,
- modal.mount,
+ containerInfo.container,
modal.modalRef,
containerInfo.hiddenSiblings,
false,
diff --git a/packages/mui-material/src/Modal/useModal.ts b/packages/mui-material/src/Modal/useModal.ts
index 2c0a3a94d08b9a..8b6218b31ab1fb 100644
--- a/packages/mui-material/src/Modal/useModal.ts
+++ b/packages/mui-material/src/Modal/useModal.ts
@@ -46,6 +46,7 @@ function useModal(parameters: UseModalParameters): UseModalReturnValue {
const mountNodeRef = React.useRef(null);
const lastMountNodeRef = React.useRef(null);
const modalRef = React.useRef(null);
+ const registeredContainerRef = React.useRef(null);
const handleRef = useForkRef(modalRef, rootRef);
const [exited, setExited] = React.useState(!open);
const hasTransition = getHasTransition(children);
@@ -74,7 +75,16 @@ function useModal(parameters: UseModalParameters): UseModalReturnValue {
const handleOpen = useEventCallback(() => {
const resolvedContainer = getContainer(container) || getDoc().body;
+ // When the container changes while the modal stays open, the previous container
+ // keeps the `aria-hidden` state computed for the previous sibling set.
+ // Unregister the modal first so the new container is evaluated against its own siblings.
+ if (registeredContainerRef.current && registeredContainerRef.current !== resolvedContainer) {
+ manager.remove(getModal(), ariaHiddenProp);
+ registeredContainerRef.current = null;
+ }
+
manager.add(getModal(), resolvedContainer as HTMLElement);
+ registeredContainerRef.current = resolvedContainer as HTMLElement;
// The element was already mounted.
if (modalRef.current) {
@@ -101,6 +111,7 @@ function useModal(parameters: UseModalParameters): UseModalReturnValue {
});
const handleClose = React.useCallback(() => {
+ registeredContainerRef.current = null;
manager.remove(getModal(), ariaHiddenProp);
}, [ariaHiddenProp]);
@@ -116,7 +127,7 @@ function useModal(parameters: UseModalParameters): UseModalReturnValue {
} else if (!hasTransition || !closeAfterTransition) {
handleClose();
}
- }, [open, handleClose, hasTransition, closeAfterTransition, handleOpen]);
+ }, [open, handleClose, hasTransition, closeAfterTransition, handleOpen, container]);
const createHandleKeyDown = (otherHandlers: EventHandlers) => (event: React.KeyboardEvent) => {
otherHandlers.onKeyDown?.(event);