-
-
Notifications
You must be signed in to change notification settings - Fork 62
Description
Description
When using the temporary variant drawer and opening it, the browser warns:
Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
Element with focus: <button.MuiButtonBase-root MuiIconButton-root MuiIconButton-edgeEnd MuiIconButton-sizeMedium MuiIconButton-root MuiButtonBase-root css-5ki7wftn>
Ancestor with aria-hidden: <div#root> <div id="root" aria-hidden="true">…</div>
Reason
Unexpected aria-hidden
Based on my diagnosis, SUID incorrectly adds aria-hidden to the div used for mounting MuiModal.
<body style="overflow: hidden">
<div id="root" aria-hidden="true">...</div>
+ <div aria-hidden="true">
<div
class="MuiModal-root MuiDrawer-root MuiDrawer-modal MuiDrawer-root MuiModal-root css-o6o9lwg9"
role="presentation"
>
<div
aria-hidden="true"
class="MuiBackdrop-root MuiModal-backdrop MuiBackdrop-root css-bh4oe82h"
style="opacity: 1; transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1)"
></div>
<div
class="MuiPaper-root MuiPaper-elevation MuiPaper-elevation16 MuiDrawer-paper MuiDrawer-paperAnchorRight MuiDrawer-paper MuiPaper-root css-cslrtd3y_1"
style="transform: none; transition: transform 225ms cubic-bezier(0, 0, 0.2, 1)"
>
...
</div>
</div>
</div>
</body>
After reviewing the code, I found the issue originates here:
suid/packages/base/src/ModalUnstyled/ModalManager.ts
Lines 205 to 208 in d344de6
| ariaHidden(modal.ref, false); | |
| const hiddenSiblings = getHiddenSiblings(container); | |
| ariaHiddenSiblings(container, modal.ref, hiddenSiblings, true); |
SolidJS's Portal nests a div when passing modal.ref to the body instead of direct attachment. This causes the ariaHiddenSiblings function to consistently fail to exclude the current drawer element.
A similar issue exists here:
suid/packages/base/src/ModalUnstyled/ModalManager.ts
Lines 267 to 284 in d344de6
| ariaHidden(modal.ref, true); | |
| ariaHiddenSiblings( | |
| containerInfo.container, | |
| modal.ref, | |
| containerInfo.hiddenSiblings, | |
| false | |
| ); | |
| this.containers.splice(containerIndex, 1); | |
| } else { | |
| // Otherwise make sure the next top modal is visible to a screen reader. | |
| const nextTop = containerInfo.modals[containerInfo.modals.length - 1]; | |
| // as soon as a modal is adding its modalRef is undefined. it can't set | |
| // aria-hidden because the dom element doesn't exist either | |
| // when modal was unmounted before modalRef gets null | |
| ariaHidden(nextTop.ref, false); | |
| } |
However, replacing these model.ref with modal.ref.parentElement! still fails to resolve the error.
Unmanaged Focus
Further diagnosis reveals that SUID fails to handle focus management:
Mui Site:
> document.activeElement
<body dir="ltr" class="mode-light">…</body>
> document.querySelector('header .MuiIconButton-root').click()
undefined
> document.activeElement
<div class="MuiPaper-root MuiPaper-elevation MuiPaper-elevation16 MuiDrawer-paper MuiDrawer-paperAnchorLeft css-iqol3p" tabindex="-1" style="--Paper-shadow: var(--muidocs-shadows-16); --Paper-overlay: var(--muidocs-overlays-16); transform: none; transition: transform 225ms cubic-bezier(0, 0, 0.2, 1);">…</div>flexSuid Site:
> document.activeElement
<body>…</body>
> document.querySelector('.MuiToolbar-root .MuiIconButton-root').click()
undefined
> document.activeElement
<body style="overflow: hidden;">…</body>The problem occurs here:
suid/packages/base/src/ModalUnstyled/ModalUnstyled.tsx
Lines 187 to 239 in d344de6
| return ( | |
| <TransitionContext.Provider | |
| value={{ | |
| get in() { | |
| return !!props.transition && props.open; | |
| }, | |
| onEnter: () => { | |
| props.transition && setExited(false); | |
| }, | |
| onExited: () => { | |
| if (props.transition) { | |
| setExited(true); | |
| if (props.closeAfterTransition) handleClose(); | |
| } | |
| }, | |
| }} | |
| > | |
| <Show when={!noMount()}> | |
| <Portal container={props.container} disablePortal={props.disablePortal}> | |
| {/* | |
| * Marking an element with the role presentation indicates to assistive technology | |
| * that this element should be ignored; it exists to support the web application and | |
| * is not meant for humans to interact with directly. | |
| * https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md | |
| */} | |
| <Dynamic | |
| {...otherProps} | |
| component={Root()} | |
| role="presentation" | |
| {...rootProps()} | |
| {...(!isHostComponent(Root()) && { | |
| //component: baseProps.component, | |
| ownerState: allProps, | |
| })} | |
| onKeyDown={handleKeyDown} | |
| class={clsx(classes.root, rootProps().class, otherProps.class)} | |
| ref={element} | |
| > | |
| <Show when={!props.hideBackdrop && !!props.BackdropComponent}> | |
| <Dynamic | |
| component={props.BackdropComponent} | |
| open={props.open} | |
| onClick={handleBackdropClick} | |
| {...(props.BackdropProps ?? {})} | |
| /> | |
| </Show> | |
| {props.children} | |
| </Dynamic> | |
| </Portal> | |
| </Show> | |
| </TransitionContext.Provider> | |
| ); | |
| }); |
In Mui, a TrapFocus component handles focus management: