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; if(['hidden','collapse'].includes(getComputedStyle(el).visibility))return false; // Closed details descendants can have rectangles but cannot receive focus. for(let parent=el.parentElement;parent && parent!==root;parent=parent.parentElement){ if(parent.tagName==='DETAILS' && !parent.open && !parent.querySelector(':scope > summary')?.contains(el))return false; } return true; }); } export function useModalFocus(open,onClose) { const ref=useRef(null),close=useRef(onClose); close.current=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 ()=>{ 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; if(e.defaultPrevented || e.target.closest('[role="dialog"]')!==root)return; if(e.key==='Escape'){e.preventDefault();e.stopPropagation();close.current();return;} if(e.key!=='Tab')return; const elements=focusableElements(root),first=elements[0],last=elements.at(-1); if(!first){e.preventDefault();root.focus();} else if(e.shiftKey && (document.activeElement===first || document.activeElement===root)){e.preventDefault();last.focus();} else if(!e.shiftKey && document.activeElement===last){e.preventDefault();first.focus();} }; return {ref,onKeyDown}; }