Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
31 changes: 31 additions & 0 deletions packages/mui-material/src/Modal/ModalManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,5 +438,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);
}
});
});
});
2 changes: 1 addition & 1 deletion packages/mui-material/src/Modal/ModalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,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