fix: isolate background interaction behind stacked modals

This commit is contained in:
2026-09-06 15:25:05 +03:00
parent abbf24078d
commit 69f687967b
2 changed files with 42 additions and 1 deletions
+34 -1
View File
@@ -1,5 +1,25 @@
import {useEffect,useRef} from 'react';
// Only the top modal remains interactive, including when it is portalled to body.
const modalStack=[];
const previousInert=new Map();
let isolationObserver=null;
function isolateTopModal() {
for(const [element,inert] of previousInert)element.inert=inert;
previousInert.clear();
const top=modalStack.at(-1);
if(!top?.isConnected)return;
for(let branch=top;branch.parentElement;branch=branch.parentElement){
const parent=branch.parentElement;
for(const sibling of parent.children){
if(sibling===branch)continue;
previousInert.set(sibling,sibling.inert);
sibling.inert=true;
}
if(parent===document.body)break;
}
}
function focusableElements(root) {
return [...root.querySelectorAll('button, a[href], input, select, textarea, summary, [tabindex]')].filter(el=>{
if(el.tabIndex<0 || el.matches(':disabled') || !el.getClientRects().length || el.closest('[inert]'))return false;
@@ -18,8 +38,21 @@ export function useModalFocus(open,onClose) {
useEffect(()=>{
if(!open || !ref.current)return;
const opener=document.activeElement,root=ref.current;
modalStack.push(root);
isolateTopModal();
if(!isolationObserver){
isolationObserver=new MutationObserver(isolateTopModal);
isolationObserver.observe(document.body,{childList:true,subtree:true});
}
(focusableElements(root)[0] || root).focus();
return ()=>{if(opener?.isConnected)opener.focus();};
return ()=>{
const wasTop=modalStack.at(-1)===root;
const index=modalStack.indexOf(root);
if(index>=0)modalStack.splice(index,1);
isolateTopModal();
if(!modalStack.length){isolationObserver?.disconnect();isolationObserver=null;}
if(wasTop && opener?.isConnected && !opener.closest('[inert]'))opener.focus();
};
},[open]);
const onKeyDown=e=>{
const root=ref.current;