1
0
mirror of synced 2026-10-23 07:08:55 +03:00

Fix modal dialogs being hidden from assistive technology by aria-hidden on the application root. #9865 (#10587)

This commit is contained in:
Dave Page
2026-10-15 19:40:21 +01:00
parent 733cd5ba5e
commit f73b51f912
2 changed files with 44 additions and 1 deletions

View File

@@ -366,9 +366,11 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose
}
};
const [isFullScreen, setIsFullScreen] = useState(fullScreen);
const dialogRef = useRef(null);
return (
<StyleDialog
ref={dialogRef}
open={true}
onClose={closeModal}
PaperComponent={PaperComponent}
@@ -380,6 +382,14 @@ function ModalContainer({ id, title, content, dialogHeight, dialogWidth, onClose
fullScreen={isFullScreen}
fullWidth={isFullWidth}
disablePortal
// The dialog is rendered in place (disablePortal), so it sits inside
// the application root rather than directly under <body>. MUI's modal
// manager sets aria-hidden on the children of its container, which
// defaults to <body>, so it would hide the application root and the
// dialog along with it, whilst focus is inside the dialog. Use the
// dialog's own parent as the container so that only its siblings, the
// background content, are hidden.
container={() => dialogRef.current?.parentElement}
{...props}
>
{ showTitle &&

View File

@@ -66,7 +66,6 @@ describe('ModalProvider AlertContent', () => {
});
});
describe('ModalProvider keeps focus in an open modal (#9784)', () => {
let iframe;
@@ -166,3 +165,37 @@ describe('ModalProvider keeps focus in an open modal (#9784)', () => {
expect(document.activeElement).toBe(input);
});
});
describe('ModalProvider aria-hidden handling', () => {
let modalApi;
function CaptureModal() {
modalApi = useModal();
return <button>Background action</button>;
}
const ThemedProvider = withTheme(ModalProvider);
it('hides the background content but not the dialog itself',
async () => {
const ctrl = await act(async () => render(
<ThemedProvider><CaptureModal /></ThemedProvider>
));
const background = screen.getByText('Background action');
background.focus();
await act(async () => {
modalApi.confirm('Reset statistics', 'Are you sure?', () => {});
});
const dialog = screen.getByRole('dialog');
expect(dialog.closest('[aria-hidden="true"]')).toBeNull();
expect(dialog.contains(document.activeElement)).toBe(true);
expect(background.closest('[aria-hidden="true"]')).not.toBeNull();
expect(ctrl.container.closest('[aria-hidden="true"]')).toBeNull();
await act(async () => {
screen.getByText('No').click();
});
expect(screen.queryByRole('dialog')).toBeNull();
expect(background.closest('[aria-hidden="true"]')).toBeNull();
});
});