Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/mui-material/src/Modal/Modal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,29 @@ describe('<Modal />', () => {
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(
<Modal open>
<div data-testid="modal-content" />
</Modal>,
);

// 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);
Expand Down
45 changes: 45 additions & 0 deletions packages/mui-material/src/Modal/ModalManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
});
});
});
8 changes: 6 additions & 2 deletions packages/mui-material/src/Modal/ModalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
Expand Down Expand Up @@ -268,7 +272,7 @@ export class ModalManager {

ariaHiddenSiblings(
containerInfo.container,
modal.mount,
containerInfo.container,
modal.modalRef,
containerInfo.hiddenSiblings,
false,
Expand Down
13 changes: 12 additions & 1 deletion packages/mui-material/src/Modal/useModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function useModal(parameters: UseModalParameters): UseModalReturnValue {
const mountNodeRef = React.useRef<HTMLElement>(null);
const lastMountNodeRef = React.useRef<HTMLElement>(null);
const modalRef = React.useRef<HTMLDivElement>(null);
const registeredContainerRef = React.useRef<HTMLElement>(null);
const handleRef = useForkRef(modalRef, rootRef);
const [exited, setExited] = React.useState(!open);
const hasTransition = getHasTransition(children);
Expand Down Expand Up @@ -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) {
Expand All @@ -101,6 +111,7 @@ function useModal(parameters: UseModalParameters): UseModalReturnValue {
});

const handleClose = React.useCallback(() => {
registeredContainerRef.current = null;
manager.remove(getModal(), ariaHiddenProp);
}, [ariaHiddenProp]);

Expand All @@ -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);
Expand Down
Loading