[{"data":1,"prerenderedAt":2569},["ShallowReactive",2],{"site-header-nav":3,"page-\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals\u002F":156,"content-navigation":2495},[4,66,70],{"title":5,"path":6,"stem":7,"children":8},"Core Accessibility Principles For Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks","core-accessibility-principles-for-modern-frameworks",[9,12,18,24,36,48,60],{"title":10,"path":6,"stem":11},"Core Accessibility Principles for Modern Frameworks","core-accessibility-principles-for-modern-frameworks\u002Findex",{"title":13,"path":14,"stem":15,"children":16},"Accessible Color Contrast & Theming","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming","core-accessibility-principles-for-modern-frameworks\u002Faccessible-color-contrast-theming\u002Findex",[17],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":22},"Accessible Form Validation & Error States in Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states","core-accessibility-principles-for-modern-frameworks\u002Faccessible-form-validation-error-states\u002Findex",[23],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":28},"Focus Management Strategies for SPAs","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Findex",[29,30],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":34},"Handling Focus Restoration After Dynamic Route Changes","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes","core-accessibility-principles-for-modern-frameworks\u002Ffocus-management-strategies-for-spas\u002Fhandling-focus-restoration-after-dynamic-route-changes\u002Findex",[35],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":40},"Keyboard Navigation Patterns for Modals","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Findex",[41,42],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":46},"Building Accessible Dropdowns Without External UI Kits","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits","core-accessibility-principles-for-modern-frameworks\u002Fkeyboard-navigation-patterns-for-modals\u002Fbuilding-accessible-dropdowns-without-external-ui-kits\u002Findex",[47],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":52},"Screen Reader Compatibility Testing for Modern Frameworks","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Findex",[53,54],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":58},"Testing ARIA Live Regions with Jest and Testing Library","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library","core-accessibility-principles-for-modern-frameworks\u002Fscreen-reader-compatibility-testing\u002Ftesting-aria-live-regions-with-jest-and-testing-library\u002Findex",[59],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":64},"Semantic HTML vs ARIA in Component Trees","\u002Fcore-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees","core-accessibility-principles-for-modern-frameworks\u002Fsemantic-html-vs-aria-in-component-trees\u002Findex",[65],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69},"Modern Framework Accessibility","\u002F","index",{"title":71,"path":72,"stem":73,"children":74},"React Nextjs Accessibility Patterns","\u002Freact-nextjs-accessibility-patterns","react-nextjs-accessibility-patterns",[75,78,90,102,108,126,144],{"title":76,"path":72,"stem":77},"React & Next.js Accessibility Patterns","react-nextjs-accessibility-patterns\u002Findex",{"title":79,"path":80,"stem":81,"children":82},"Accessible Component Libraries in React","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Findex",[83,84],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":88},"Building Accessible Tabs in React Without Radix UI","\u002Freact-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui","react-nextjs-accessibility-patterns\u002Faccessible-component-libraries-in-react\u002Fbuilding-accessible-tabs-in-react-without-radix-ui\u002Findex",[89],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":94},"Dynamic Content & State Announcements in React & Next.js","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Findex",[95,96],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":100},"Implementing React Context for Global Accessibility Preferences","\u002Freact-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences","react-nextjs-accessibility-patterns\u002Fdynamic-content-state-announcements\u002Freact-context-for-global-accessibility-preferences\u002Findex",[101],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":106},"Form Handling with React Hook Form & Accessibility","\u002Freact-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y","react-nextjs-accessibility-patterns\u002Fform-handling-with-react-hook-form-a11y\u002Findex",[107],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":112},"Next.js App Router & A11y: Implementation Guide for Modern Frameworks","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Findex",[113,114,120],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":118},"Implementing Skip Links in Next.js App Router: A Step-by-Step Guide","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fimplementing-skip-links-in-nextjs-app-router\u002Findex",[119],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":124},"Next.js Dynamic Imports and Keyboard Navigation: A Complete A11y Implementation Guide","\u002Freact-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation","react-nextjs-accessibility-patterns\u002Fnextjs-app-router-a11y\u002Fnextjs-dynamic-imports-and-keyboard-navigation\u002Findex",[125],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":130},"React Hooks for Accessibility: Implementation Patterns & State Management","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Findex",[131,132,138],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":136},"Fixing Focus Trap Issues in React Portals","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals\u002Findex",[137],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":142},"Making React useEffect Accessible for Screen Readers","\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers","react-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Fmaking-react-useeffect-accessible-for-screen-readers\u002Findex",[143],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":148},"Server Components & Client-Side Interactivity","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Findex",[149,150],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":154},"Handling Accessible Modals in Next.js 14 Server Components","\u002Freact-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components","react-nextjs-accessibility-patterns\u002Fserver-components-client-side-interactivity\u002Fhandling-accessible-modals-in-nextjs-14-server-components\u002Findex",[155],{"title":151,"path":152,"stem":153},{"id":157,"title":133,"body":158,"date":2488,"description":2489,"extension":2490,"image":2488,"meta":2491,"modifiedAt":2488,"navigation":395,"noindex":2492,"path":134,"publishedAt":2488,"seo":2493,"stem":135,"updatedAt":2488,"__hash__":2494},"content\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002Ffixing-focus-trap-issues-in-react-portals\u002Findex.md",{"type":159,"value":160,"toc":2474},"minimark",[161,165,179,185,207,212,234,239,242,282,303,310,318,323,358,967,991,1000,1003,1007,1043,1639,1646,1650,1653,1658,1667,1975,1979,1989,2124,2128,2131,2345,2354,2358,2415,2419,2431,2449,2470],[162,163,133],"h1",{"id":164},"fixing-focus-trap-issues-in-react-portals",[166,167,168,169,173,174,178],"p",{},"When rendering modals, dialogs, or dropdowns outside the primary DOM tree, implementing predictable keyboard navigation requires strict adherence to established ",[170,171,76],"a",{"href":172},"\u002Freact-nextjs-accessibility-patterns\u002F",". This guide addresses the architectural failure of focus escaping the viewport when using ",[175,176,177],"code",{},"createPortal",". We will implement a robust, WCAG-compliant focus trap that handles dynamic DOM mounting, native event delegation, and React's concurrent rendering lifecycle.",[166,180,181],{},[182,183,184],"strong",{},"WCAG Success Criteria Addressed",[186,187,188,195,201],"ul",{},[189,190,191,194],"li",{},[182,192,193],{},"2.4.3 Focus Order (Level A):"," Ensures logical navigation sequence remains intact when focus enters portaled content.",[189,196,197,200],{},[182,198,199],{},"2.4.7 Focus Visible (Level AA):"," Maintains visible focus indicators during trap cycling.",[189,202,203,206],{},[182,204,205],{},"1.3.2 Meaningful Sequence (Level A):"," Guarantees assistive technology reads content in the correct DOM order.",[166,208,209],{},[182,210,211],{},"Core Implementation Objectives",[186,213,214,217,224,231],{},[189,215,216],{},"Decouple React's synthetic event system from native DOM focus propagation",[189,218,219,220,223],{},"Implement a custom ",[175,221,222],{},"useFocusTrap"," hook tailored for dynamically mounted portaled content",[189,225,226,227,230],{},"Synchronize ",[175,228,229],{},"aria-modal"," state with inert background content suppression",[189,232,233],{},"Validate focus boundaries using screen readers and keyboard-only navigation workflows",[235,236,238],"h2",{"id":237},"why-focus-traps-fail-in-react-portals","Why Focus Traps Fail in React Portals",[166,240,241],{},"React portals render children into a detached DOM node while preserving component context. This architectural split creates a fundamental mismatch between React's synthetic event delegation and native browser focus management.",[243,244,245,263,273],"ol",{},[189,246,247,250,251,254,255,258,259,262],{},[182,248,249],{},"Event Bubbling Bypass:"," Naive ",[175,252,253],{},"onKeyDown"," listeners attached to React components rely on synthetic event propagation. When focus moves via ",[175,256,257],{},"Tab",", the browser dispatches native ",[175,260,261],{},"keydown"," events directly to the focused element, bypassing React's synthetic event tree entirely.",[189,264,265,268,269,272],{},[182,266,267],{},"Concurrent Rendering Race Conditions:"," React 18's concurrent features can interrupt mount\u002Funmount cycles. If a trap initializes before the portal DOM is fully committed, ",[175,270,271],{},"querySelectorAll"," returns stale or empty node lists, breaking boundary calculations.",[189,274,275,278,279,281],{},[182,276,277],{},"Focus Scope Leakage:"," Without explicit boundary enforcement, ",[175,280,257],{}," naturally progresses to the next focusable element in the document order, which often resides in the background application layer.",[166,283,284,287,288,291,292,294,295,298,299,302],{},[182,285,286],{},"Debugging Workflow:"," Open Chrome DevTools → Elements panel. Locate the portal container (",[175,289,290],{},"#modal-root","). Verify that ",[175,293,261],{}," listeners are attached directly to the portal node via ",[175,296,297],{},"Event Listeners"," tab, not to the React root. Use the ",[175,300,301],{},"focus()"," console method to manually trigger boundary checks during active development.",[235,304,306,307,309],{"id":305},"building-a-robust-usefocustrap-hook","Building a Robust ",[175,308,222],{}," Hook",[166,311,312,313,317],{},"To guarantee reliable focus containment, bypass React's synthetic event system and attach native listeners directly to the portal container. This pattern aligns with proven ",[170,314,316],{"href":315},"\u002Freact-nextjs-accessibility-patterns\u002Freact-hooks-for-accessibility\u002F","React Hooks for Accessibility"," architectures.",[166,319,320],{},[182,321,322],{},"Implementation Requirements",[186,324,325,332,348,355],{},[189,326,327,328,331],{},"Use ",[175,329,330],{},"useRef"," to maintain stable references to the portal container and the original trigger element.",[189,333,334,335,337,338,341,342,68,344,347],{},"Attach ",[175,336,261],{}," listeners to ",[175,339,340],{},"document"," or the container to capture ",[175,343,257],{},[175,345,346],{},"Shift+Tab"," before they propagate.",[189,349,350,351,354],{},"Implement explicit ",[175,352,353],{},"preventDefault()"," logic to override default browser tabbing behavior at boundaries.",[189,356,357],{},"Safely restore focus to the trigger element during unmount to prevent focus loss.",[359,360,365],"pre",{"className":361,"code":362,"language":363,"meta":364,"style":364},"language-typescript shiki shiki-themes github-light github-dark","import { useEffect, useRef, useCallback } from 'react';\n\nexport function useFocusTrap(isOpen: boolean, containerRef: React.RefObject\u003CHTMLElement>) {\n const triggerRef = useRef\u003CHTMLElement | null>(null);\n\n const handleKeyDown = useCallback((e: KeyboardEvent) => {\n if (!isOpen || !containerRef.current) return;\n \n const container = containerRef.current;\n const focusable = container.querySelectorAll\u003CHTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n );\n \n if (focusable.length === 0) return;\n \n const firstEl = focusable[0];\n const lastEl = focusable[focusable.length - 1];\n\n if (e.key === 'Tab') {\n if (e.shiftKey) {\n if (document.activeElement === firstEl) {\n e.preventDefault();\n lastEl.focus();\n }\n } else {\n if (document.activeElement === lastEl) {\n e.preventDefault();\n firstEl.focus();\n }\n }\n }\n }, [isOpen, containerRef]);\n\n useEffect(() => {\n if (isOpen) {\n triggerRef.current = document.activeElement as HTMLElement;\n document.addEventListener('keydown', handleKeyDown);\n \u002F\u002F Delay focus to ensure DOM commit in concurrent mode\n requestAnimationFrame(() => containerRef.current?.focus());\n }\n \n return () => {\n document.removeEventListener('keydown', handleKeyDown);\n triggerRef.current?.focus();\n };\n }, [isOpen, handleKeyDown, containerRef]);\n}\n","typescript","",[175,366,367,390,397,450,484,489,522,551,557,570,592,598,604,609,632,637,656,679,684,701,709,722,734,745,751,762,774,783,793,798,803,808,814,819,832,840,860,877,884,902,907,912,925,939,949,955,961],{"__ignoreMap":364},[368,369,372,376,380,383,387],"span",{"class":370,"line":371},"line",1,[368,373,375],{"class":374},"szBVR","import",[368,377,379],{"class":378},"sVt8B"," { useEffect, useRef, useCallback } ",[368,381,382],{"class":374},"from",[368,384,386],{"class":385},"sZZnC"," 'react'",[368,388,389],{"class":378},";\n",[368,391,393],{"class":370,"line":392},2,[368,394,396],{"emptyLinePlaceholder":395},true,"\n",[368,398,400,403,406,410,413,417,420,424,427,430,432,435,438,441,444,447],{"class":370,"line":399},3,[368,401,402],{"class":374},"export",[368,404,405],{"class":374}," function",[368,407,409],{"class":408},"sScJk"," useFocusTrap",[368,411,412],{"class":378},"(",[368,414,416],{"class":415},"s4XuR","isOpen",[368,418,419],{"class":374},":",[368,421,423],{"class":422},"sj4cs"," boolean",[368,425,426],{"class":378},", ",[368,428,429],{"class":415},"containerRef",[368,431,419],{"class":374},[368,433,434],{"class":408}," React",[368,436,437],{"class":378},".",[368,439,440],{"class":408},"RefObject",[368,442,443],{"class":378},"\u003C",[368,445,446],{"class":408},"HTMLElement",[368,448,449],{"class":378},">) {\n",[368,451,453,456,459,462,465,467,469,472,475,478,481],{"class":370,"line":452},4,[368,454,455],{"class":374}," const",[368,457,458],{"class":422}," triggerRef",[368,460,461],{"class":374}," =",[368,463,464],{"class":408}," useRef",[368,466,443],{"class":378},[368,468,446],{"class":408},[368,470,471],{"class":374}," |",[368,473,474],{"class":422}," null",[368,476,477],{"class":378},">(",[368,479,480],{"class":422},"null",[368,482,483],{"class":378},");\n",[368,485,487],{"class":370,"line":486},5,[368,488,396],{"emptyLinePlaceholder":395},[368,490,492,494,497,499,502,505,508,510,513,516,519],{"class":370,"line":491},6,[368,493,455],{"class":374},[368,495,496],{"class":422}," handleKeyDown",[368,498,461],{"class":374},[368,500,501],{"class":408}," useCallback",[368,503,504],{"class":378},"((",[368,506,507],{"class":415},"e",[368,509,419],{"class":374},[368,511,512],{"class":408}," KeyboardEvent",[368,514,515],{"class":378},") ",[368,517,518],{"class":374},"=>",[368,520,521],{"class":378}," {\n",[368,523,525,528,531,534,537,540,543,546,549],{"class":370,"line":524},7,[368,526,527],{"class":374}," if",[368,529,530],{"class":378}," (",[368,532,533],{"class":374},"!",[368,535,536],{"class":378},"isOpen ",[368,538,539],{"class":374},"||",[368,541,542],{"class":374}," !",[368,544,545],{"class":378},"containerRef.current) ",[368,547,548],{"class":374},"return",[368,550,389],{"class":378},[368,552,554],{"class":370,"line":553},8,[368,555,556],{"class":378}," \n",[368,558,560,562,565,567],{"class":370,"line":559},9,[368,561,455],{"class":374},[368,563,564],{"class":422}," container",[368,566,461],{"class":374},[368,568,569],{"class":378}," containerRef.current;\n",[368,571,573,575,578,580,583,585,587,589],{"class":370,"line":572},10,[368,574,455],{"class":374},[368,576,577],{"class":422}," focusable",[368,579,461],{"class":374},[368,581,582],{"class":378}," container.",[368,584,271],{"class":408},[368,586,443],{"class":378},[368,588,446],{"class":408},[368,590,591],{"class":378},">(\n",[368,593,595],{"class":370,"line":594},11,[368,596,597],{"class":385}," 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n",[368,599,601],{"class":370,"line":600},12,[368,602,603],{"class":378}," );\n",[368,605,607],{"class":370,"line":606},13,[368,608,556],{"class":378},[368,610,612,614,617,620,623,626,628,630],{"class":370,"line":611},14,[368,613,527],{"class":374},[368,615,616],{"class":378}," (focusable.",[368,618,619],{"class":422},"length",[368,621,622],{"class":374}," ===",[368,624,625],{"class":422}," 0",[368,627,515],{"class":378},[368,629,548],{"class":374},[368,631,389],{"class":378},[368,633,635],{"class":370,"line":634},15,[368,636,556],{"class":378},[368,638,640,642,645,647,650,653],{"class":370,"line":639},16,[368,641,455],{"class":374},[368,643,644],{"class":422}," firstEl",[368,646,461],{"class":374},[368,648,649],{"class":378}," focusable[",[368,651,652],{"class":422},"0",[368,654,655],{"class":378},"];\n",[368,657,659,661,664,666,669,671,674,677],{"class":370,"line":658},17,[368,660,455],{"class":374},[368,662,663],{"class":422}," lastEl",[368,665,461],{"class":374},[368,667,668],{"class":378}," focusable[focusable.",[368,670,619],{"class":422},[368,672,673],{"class":374}," -",[368,675,676],{"class":422}," 1",[368,678,655],{"class":378},[368,680,682],{"class":370,"line":681},18,[368,683,396],{"emptyLinePlaceholder":395},[368,685,687,689,692,695,698],{"class":370,"line":686},19,[368,688,527],{"class":374},[368,690,691],{"class":378}," (e.key ",[368,693,694],{"class":374},"===",[368,696,697],{"class":385}," 'Tab'",[368,699,700],{"class":378},") {\n",[368,702,704,706],{"class":370,"line":703},20,[368,705,527],{"class":374},[368,707,708],{"class":378}," (e.shiftKey) {\n",[368,710,712,714,717,719],{"class":370,"line":711},21,[368,713,527],{"class":374},[368,715,716],{"class":378}," (document.activeElement ",[368,718,694],{"class":374},[368,720,721],{"class":378}," firstEl) {\n",[368,723,725,728,731],{"class":370,"line":724},22,[368,726,727],{"class":378}," e.",[368,729,730],{"class":408},"preventDefault",[368,732,733],{"class":378},"();\n",[368,735,737,740,743],{"class":370,"line":736},23,[368,738,739],{"class":378}," lastEl.",[368,741,742],{"class":408},"focus",[368,744,733],{"class":378},[368,746,748],{"class":370,"line":747},24,[368,749,750],{"class":378}," }\n",[368,752,754,757,760],{"class":370,"line":753},25,[368,755,756],{"class":378}," } ",[368,758,759],{"class":374},"else",[368,761,521],{"class":378},[368,763,765,767,769,771],{"class":370,"line":764},26,[368,766,527],{"class":374},[368,768,716],{"class":378},[368,770,694],{"class":374},[368,772,773],{"class":378}," lastEl) {\n",[368,775,777,779,781],{"class":370,"line":776},27,[368,778,727],{"class":378},[368,780,730],{"class":408},[368,782,733],{"class":378},[368,784,786,789,791],{"class":370,"line":785},28,[368,787,788],{"class":378}," firstEl.",[368,790,742],{"class":408},[368,792,733],{"class":378},[368,794,796],{"class":370,"line":795},29,[368,797,750],{"class":378},[368,799,801],{"class":370,"line":800},30,[368,802,750],{"class":378},[368,804,806],{"class":370,"line":805},31,[368,807,750],{"class":378},[368,809,811],{"class":370,"line":810},32,[368,812,813],{"class":378}," }, [isOpen, containerRef]);\n",[368,815,817],{"class":370,"line":816},33,[368,818,396],{"emptyLinePlaceholder":395},[368,820,822,825,828,830],{"class":370,"line":821},34,[368,823,824],{"class":408}," useEffect",[368,826,827],{"class":378},"(() ",[368,829,518],{"class":374},[368,831,521],{"class":378},[368,833,835,837],{"class":370,"line":834},35,[368,836,527],{"class":374},[368,838,839],{"class":378}," (isOpen) {\n",[368,841,843,846,849,852,855,858],{"class":370,"line":842},36,[368,844,845],{"class":378}," triggerRef.current ",[368,847,848],{"class":374},"=",[368,850,851],{"class":378}," document.activeElement ",[368,853,854],{"class":374},"as",[368,856,857],{"class":408}," HTMLElement",[368,859,389],{"class":378},[368,861,863,866,869,871,874],{"class":370,"line":862},37,[368,864,865],{"class":378}," document.",[368,867,868],{"class":408},"addEventListener",[368,870,412],{"class":378},[368,872,873],{"class":385},"'keydown'",[368,875,876],{"class":378},", handleKeyDown);\n",[368,878,880],{"class":370,"line":879},38,[368,881,883],{"class":882},"sJ8bj"," \u002F\u002F Delay focus to ensure DOM commit in concurrent mode\n",[368,885,887,890,892,894,897,899],{"class":370,"line":886},39,[368,888,889],{"class":408}," requestAnimationFrame",[368,891,827],{"class":378},[368,893,518],{"class":374},[368,895,896],{"class":378}," containerRef.current?.",[368,898,742],{"class":408},[368,900,901],{"class":378},"());\n",[368,903,905],{"class":370,"line":904},40,[368,906,750],{"class":378},[368,908,910],{"class":370,"line":909},41,[368,911,556],{"class":378},[368,913,915,918,921,923],{"class":370,"line":914},42,[368,916,917],{"class":374}," return",[368,919,920],{"class":378}," () ",[368,922,518],{"class":374},[368,924,521],{"class":378},[368,926,928,930,933,935,937],{"class":370,"line":927},43,[368,929,865],{"class":378},[368,931,932],{"class":408},"removeEventListener",[368,934,412],{"class":378},[368,936,873],{"class":385},[368,938,876],{"class":378},[368,940,942,945,947],{"class":370,"line":941},44,[368,943,944],{"class":378}," triggerRef.current?.",[368,946,742],{"class":408},[368,948,733],{"class":378},[368,950,952],{"class":370,"line":951},45,[368,953,954],{"class":378}," };\n",[368,956,958],{"class":370,"line":957},46,[368,959,960],{"class":378}," }, [isOpen, handleKeyDown, containerRef]);\n",[368,962,964],{"class":370,"line":963},47,[368,965,966],{"class":378},"}\n",[166,968,969,972,973,975,976,978,979,982,983,986,987,990],{},[182,970,971],{},"Validation Step:"," Test with sequential ",[175,974,257],{},", reverse ",[175,977,346],{},", and ",[175,980,981],{},"Escape"," key presses. Verify that ",[175,984,985],{},"document.activeElement"," never resolves to ",[175,988,989],{},"document.body"," or any element outside the portal container.",[235,992,994,995,997,998],{"id":993},"integrating-with-createportal-and-aria-modal","Integrating with ",[175,996,177],{}," and ",[175,999,229],{},[166,1001,1002],{},"Correct component composition ensures screen readers announce the modal correctly while strictly trapping focus. The trap hook manages keyboard navigation; the component wrapper manages semantic state and background suppression.",[166,1004,1005],{},[182,1006,322],{},[186,1008,1009,1019,1026,1040],{},[189,1010,1011,1012,997,1015,1018],{},"Apply ",[175,1013,1014],{},"role=\"dialog\"",[175,1016,1017],{},"aria-modal=\"true\""," to the portal wrapper.",[189,1020,1021,1022,1025],{},"Set ",[175,1023,1024],{},"tabIndex={-1}"," to make the container programmatically focusable without adding it to the natural tab order.",[189,1027,1028,1029,1032,1033,1035,1036,1039],{},"Suppress background content using ",[175,1030,1031],{},"inert"," on the main application root. ",[175,1034,1031],{}," is preferred over ",[175,1037,1038],{},"aria-hidden"," as it natively prevents focus traversal and click events.",[189,1041,1042],{},"Enforce strict state cleanup on unmount to restore page scroll and interactivity.",[359,1044,1048],{"className":1045,"code":1046,"language":1047,"meta":364,"style":364},"language-tsx shiki shiki-themes github-light github-dark","import { useRef, useEffect } from 'react';\nimport { createPortal } from 'react-dom';\nimport { useFocusTrap } from '.\u002FuseFocusTrap';\n\ninterface AccessibleModalProps {\n isOpen: boolean;\n onClose: () => void;\n children: React.ReactNode;\n}\n\nexport function AccessibleModal({ isOpen, onClose, children }: AccessibleModalProps) {\n const portalRoot = typeof document !== 'undefined' ? document.getElementById('modal-root') : null;\n const containerRef = useRef\u003CHTMLDivElement>(null);\n const appRootRef = useRef\u003CHTMLElement | null>(typeof document !== 'undefined' ? document.getElementById('app-root') : null);\n\n useFocusTrap(isOpen, containerRef);\n\n useEffect(() => {\n if (isOpen) {\n document.body.style.overflow = 'hidden';\n appRootRef.current?.setAttribute('inert', '');\n } else {\n document.body.style.overflow = '';\n appRootRef.current?.removeAttribute('inert');\n }\n return () => { \n document.body.style.overflow = ''; \n appRootRef.current?.removeAttribute('inert');\n };\n }, [isOpen]);\n\n if (!isOpen || !portalRoot) return null;\n\n return createPortal(\n \u003Cdiv \n ref={containerRef} \n role=\"dialog\" \n aria-modal=\"true\" \n tabIndex={-1}\n style={{ position: 'fixed', inset: 0, zIndex: 9999 }}\n >\n \u003Cbutton onClick={onClose} aria-label=\"Close modal\">×\u003C\u002Fbutton>\n {children}\n \u003C\u002Fdiv>,\n portalRoot\n );\n}\n","tsx",[175,1049,1050,1063,1077,1091,1095,1105,1116,1132,1148,1152,1156,1189,1231,1253,1302,1306,1313,1317,1327,1333,1345,1365,1373,1384,1397,1401,1412,1423,1435,1439,1444,1448,1471,1475,1485,1496,1506,1518,1530,1548,1575,1580,1611,1616,1626,1631,1635],{"__ignoreMap":364},[368,1051,1052,1054,1057,1059,1061],{"class":370,"line":371},[368,1053,375],{"class":374},[368,1055,1056],{"class":378}," { useRef, useEffect } ",[368,1058,382],{"class":374},[368,1060,386],{"class":385},[368,1062,389],{"class":378},[368,1064,1065,1067,1070,1072,1075],{"class":370,"line":392},[368,1066,375],{"class":374},[368,1068,1069],{"class":378}," { createPortal } ",[368,1071,382],{"class":374},[368,1073,1074],{"class":385}," 'react-dom'",[368,1076,389],{"class":378},[368,1078,1079,1081,1084,1086,1089],{"class":370,"line":399},[368,1080,375],{"class":374},[368,1082,1083],{"class":378}," { useFocusTrap } ",[368,1085,382],{"class":374},[368,1087,1088],{"class":385}," '.\u002FuseFocusTrap'",[368,1090,389],{"class":378},[368,1092,1093],{"class":370,"line":452},[368,1094,396],{"emptyLinePlaceholder":395},[368,1096,1097,1100,1103],{"class":370,"line":486},[368,1098,1099],{"class":374},"interface",[368,1101,1102],{"class":408}," AccessibleModalProps",[368,1104,521],{"class":378},[368,1106,1107,1110,1112,1114],{"class":370,"line":491},[368,1108,1109],{"class":415}," isOpen",[368,1111,419],{"class":374},[368,1113,423],{"class":422},[368,1115,389],{"class":378},[368,1117,1118,1121,1123,1125,1127,1130],{"class":370,"line":524},[368,1119,1120],{"class":408}," onClose",[368,1122,419],{"class":374},[368,1124,920],{"class":378},[368,1126,518],{"class":374},[368,1128,1129],{"class":422}," void",[368,1131,389],{"class":378},[368,1133,1134,1137,1139,1141,1143,1146],{"class":370,"line":553},[368,1135,1136],{"class":415}," children",[368,1138,419],{"class":374},[368,1140,434],{"class":408},[368,1142,437],{"class":378},[368,1144,1145],{"class":408},"ReactNode",[368,1147,389],{"class":378},[368,1149,1150],{"class":370,"line":559},[368,1151,966],{"class":378},[368,1153,1154],{"class":370,"line":572},[368,1155,396],{"emptyLinePlaceholder":395},[368,1157,1158,1160,1162,1165,1168,1170,1172,1175,1177,1180,1183,1185,1187],{"class":370,"line":594},[368,1159,402],{"class":374},[368,1161,405],{"class":374},[368,1163,1164],{"class":408}," AccessibleModal",[368,1166,1167],{"class":378},"({ ",[368,1169,416],{"class":415},[368,1171,426],{"class":378},[368,1173,1174],{"class":415},"onClose",[368,1176,426],{"class":378},[368,1178,1179],{"class":415},"children",[368,1181,1182],{"class":378}," }",[368,1184,419],{"class":374},[368,1186,1102],{"class":408},[368,1188,700],{"class":378},[368,1190,1191,1193,1196,1198,1201,1204,1207,1210,1213,1215,1218,1220,1223,1225,1227,1229],{"class":370,"line":600},[368,1192,455],{"class":374},[368,1194,1195],{"class":422}," portalRoot",[368,1197,461],{"class":374},[368,1199,1200],{"class":374}," typeof",[368,1202,1203],{"class":378}," document ",[368,1205,1206],{"class":374},"!==",[368,1208,1209],{"class":385}," 'undefined'",[368,1211,1212],{"class":374}," ?",[368,1214,865],{"class":378},[368,1216,1217],{"class":408},"getElementById",[368,1219,412],{"class":378},[368,1221,1222],{"class":385},"'modal-root'",[368,1224,515],{"class":378},[368,1226,419],{"class":374},[368,1228,474],{"class":422},[368,1230,389],{"class":378},[368,1232,1233,1235,1238,1240,1242,1244,1247,1249,1251],{"class":370,"line":606},[368,1234,455],{"class":374},[368,1236,1237],{"class":422}," containerRef",[368,1239,461],{"class":374},[368,1241,464],{"class":408},[368,1243,443],{"class":378},[368,1245,1246],{"class":408},"HTMLDivElement",[368,1248,477],{"class":378},[368,1250,480],{"class":422},[368,1252,483],{"class":378},[368,1254,1255,1257,1260,1262,1264,1266,1268,1270,1272,1274,1277,1279,1281,1283,1285,1287,1289,1291,1294,1296,1298,1300],{"class":370,"line":611},[368,1256,455],{"class":374},[368,1258,1259],{"class":422}," appRootRef",[368,1261,461],{"class":374},[368,1263,464],{"class":408},[368,1265,443],{"class":378},[368,1267,446],{"class":408},[368,1269,471],{"class":374},[368,1271,474],{"class":422},[368,1273,477],{"class":378},[368,1275,1276],{"class":374},"typeof",[368,1278,1203],{"class":378},[368,1280,1206],{"class":374},[368,1282,1209],{"class":385},[368,1284,1212],{"class":374},[368,1286,865],{"class":378},[368,1288,1217],{"class":408},[368,1290,412],{"class":378},[368,1292,1293],{"class":385},"'app-root'",[368,1295,515],{"class":378},[368,1297,419],{"class":374},[368,1299,474],{"class":422},[368,1301,483],{"class":378},[368,1303,1304],{"class":370,"line":634},[368,1305,396],{"emptyLinePlaceholder":395},[368,1307,1308,1310],{"class":370,"line":639},[368,1309,409],{"class":408},[368,1311,1312],{"class":378},"(isOpen, containerRef);\n",[368,1314,1315],{"class":370,"line":658},[368,1316,396],{"emptyLinePlaceholder":395},[368,1318,1319,1321,1323,1325],{"class":370,"line":681},[368,1320,824],{"class":408},[368,1322,827],{"class":378},[368,1324,518],{"class":374},[368,1326,521],{"class":378},[368,1328,1329,1331],{"class":370,"line":686},[368,1330,527],{"class":374},[368,1332,839],{"class":378},[368,1334,1335,1338,1340,1343],{"class":370,"line":703},[368,1336,1337],{"class":378}," document.body.style.overflow ",[368,1339,848],{"class":374},[368,1341,1342],{"class":385}," 'hidden'",[368,1344,389],{"class":378},[368,1346,1347,1350,1353,1355,1358,1360,1363],{"class":370,"line":711},[368,1348,1349],{"class":378}," appRootRef.current?.",[368,1351,1352],{"class":408},"setAttribute",[368,1354,412],{"class":378},[368,1356,1357],{"class":385},"'inert'",[368,1359,426],{"class":378},[368,1361,1362],{"class":385},"''",[368,1364,483],{"class":378},[368,1366,1367,1369,1371],{"class":370,"line":724},[368,1368,756],{"class":378},[368,1370,759],{"class":374},[368,1372,521],{"class":378},[368,1374,1375,1377,1379,1382],{"class":370,"line":736},[368,1376,1337],{"class":378},[368,1378,848],{"class":374},[368,1380,1381],{"class":385}," ''",[368,1383,389],{"class":378},[368,1385,1386,1388,1391,1393,1395],{"class":370,"line":747},[368,1387,1349],{"class":378},[368,1389,1390],{"class":408},"removeAttribute",[368,1392,412],{"class":378},[368,1394,1357],{"class":385},[368,1396,483],{"class":378},[368,1398,1399],{"class":370,"line":753},[368,1400,750],{"class":378},[368,1402,1403,1405,1407,1409],{"class":370,"line":764},[368,1404,917],{"class":374},[368,1406,920],{"class":378},[368,1408,518],{"class":374},[368,1410,1411],{"class":378}," { \n",[368,1413,1414,1416,1418,1420],{"class":370,"line":776},[368,1415,1337],{"class":378},[368,1417,848],{"class":374},[368,1419,1381],{"class":385},[368,1421,1422],{"class":378},"; \n",[368,1424,1425,1427,1429,1431,1433],{"class":370,"line":785},[368,1426,1349],{"class":378},[368,1428,1390],{"class":408},[368,1430,412],{"class":378},[368,1432,1357],{"class":385},[368,1434,483],{"class":378},[368,1436,1437],{"class":370,"line":795},[368,1438,954],{"class":378},[368,1440,1441],{"class":370,"line":800},[368,1442,1443],{"class":378}," }, [isOpen]);\n",[368,1445,1446],{"class":370,"line":805},[368,1447,396],{"emptyLinePlaceholder":395},[368,1449,1450,1452,1454,1456,1458,1460,1462,1465,1467,1469],{"class":370,"line":810},[368,1451,527],{"class":374},[368,1453,530],{"class":378},[368,1455,533],{"class":374},[368,1457,536],{"class":378},[368,1459,539],{"class":374},[368,1461,542],{"class":374},[368,1463,1464],{"class":378},"portalRoot) ",[368,1466,548],{"class":374},[368,1468,474],{"class":422},[368,1470,389],{"class":378},[368,1472,1473],{"class":370,"line":816},[368,1474,396],{"emptyLinePlaceholder":395},[368,1476,1477,1479,1482],{"class":370,"line":821},[368,1478,917],{"class":374},[368,1480,1481],{"class":408}," createPortal",[368,1483,1484],{"class":378},"(\n",[368,1486,1487,1490,1494],{"class":370,"line":834},[368,1488,1489],{"class":378}," \u003C",[368,1491,1493],{"class":1492},"s9eBZ","div",[368,1495,556],{"class":378},[368,1497,1498,1501,1503],{"class":370,"line":842},[368,1499,1500],{"class":408}," ref",[368,1502,848],{"class":374},[368,1504,1505],{"class":378},"{containerRef} \n",[368,1507,1508,1511,1513,1516],{"class":370,"line":862},[368,1509,1510],{"class":408}," role",[368,1512,848],{"class":374},[368,1514,1515],{"class":385},"\"dialog\"",[368,1517,556],{"class":378},[368,1519,1520,1523,1525,1528],{"class":370,"line":879},[368,1521,1522],{"class":408}," aria-modal",[368,1524,848],{"class":374},[368,1526,1527],{"class":385},"\"true\"",[368,1529,556],{"class":378},[368,1531,1532,1535,1537,1540,1543,1546],{"class":370,"line":886},[368,1533,1534],{"class":408}," tabIndex",[368,1536,848],{"class":374},[368,1538,1539],{"class":378},"{",[368,1541,1542],{"class":374},"-",[368,1544,1545],{"class":422},"1",[368,1547,966],{"class":378},[368,1549,1550,1553,1555,1558,1561,1564,1566,1569,1572],{"class":370,"line":904},[368,1551,1552],{"class":408}," style",[368,1554,848],{"class":374},[368,1556,1557],{"class":378},"{{ position: ",[368,1559,1560],{"class":385},"'fixed'",[368,1562,1563],{"class":378},", inset: ",[368,1565,652],{"class":422},[368,1567,1568],{"class":378},", zIndex: ",[368,1570,1571],{"class":422},"9999",[368,1573,1574],{"class":378}," }}\n",[368,1576,1577],{"class":370,"line":909},[368,1578,1579],{"class":378}," >\n",[368,1581,1582,1584,1587,1590,1592,1595,1598,1600,1603,1606,1608],{"class":370,"line":914},[368,1583,1489],{"class":378},[368,1585,1586],{"class":1492},"button",[368,1588,1589],{"class":408}," onClick",[368,1591,848],{"class":374},[368,1593,1594],{"class":378},"{onClose} ",[368,1596,1597],{"class":408},"aria-label",[368,1599,848],{"class":374},[368,1601,1602],{"class":385},"\"Close modal\"",[368,1604,1605],{"class":378},">×\u003C\u002F",[368,1607,1586],{"class":1492},[368,1609,1610],{"class":378},">\n",[368,1612,1613],{"class":370,"line":927},[368,1614,1615],{"class":378}," {children}\n",[368,1617,1618,1621,1623],{"class":370,"line":941},[368,1619,1620],{"class":378}," \u003C\u002F",[368,1622,1493],{"class":1492},[368,1624,1625],{"class":378},">,\n",[368,1627,1628],{"class":370,"line":951},[368,1629,1630],{"class":378}," portalRoot\n",[368,1632,1633],{"class":370,"line":957},[368,1634,603],{"class":378},[368,1636,1637],{"class":370,"line":963},[368,1638,966],{"class":378},[166,1640,1641,1643,1644,437],{},[182,1642,971],{}," Execute VoiceOver (macOS) or NVDA (Windows). Verify the screen reader announces \"Dialog\" upon open, ignores background content, and correctly announces the close button. Confirm focus returns to the original trigger element on ",[175,1645,1174],{},[235,1647,1649],{"id":1648},"automated-and-manual-testing-strategies","Automated and Manual Testing Strategies",[166,1651,1652],{},"Reproducible testing workflows prevent regression in CI\u002FCD pipelines and guarantee compliance across framework updates.",[1654,1655,1657],"h3",{"id":1656},"unit-integration-testing","Unit & Integration Testing",[166,1659,327,1660,997,1663,1666],{},[175,1661,1662],{},"@testing-library\u002Freact",[175,1664,1665],{},"@testing-library\u002Fuser-event"," to simulate sequential keyboard navigation. Assert boundary containment programmatically.",[359,1668,1670],{"className":361,"code":1669,"language":363,"meta":364,"style":364},"import { render, screen } from '@testing-library\u002Freact';\nimport userEvent from '@testing-library\u002Fuser-event';\nimport { AccessibleModal } from '.\u002FAccessibleModal';\n\ntest('focus remains trapped within modal', async () => {\n const user = userEvent.setup();\n render(\u003CAccessibleModal isOpen={true} onClose={jest.fn()}>\u003Cbutton>Confirm\u003C\u002Fbutton>\u003C\u002FAccessibleModal>);\n \n const modal = screen.getByRole('dialog');\n expect(modal).toHaveFocus();\n \n await user.tab();\n expect(screen.getByRole('button', { name: 'Confirm' })).toHaveFocus();\n \n await user.tab();\n expect(screen.getByRole('button', { name: 'Close modal' })).toHaveFocus();\n \n await user.tab();\n expect(screen.getByRole('dialog')).toHaveFocus(); \u002F\u002F Wraps to first element\n});\n",[175,1671,1672,1686,1700,1714,1718,1739,1756,1813,1817,1839,1852,1856,1869,1896,1900,1910,1933,1937,1947,1970],{"__ignoreMap":364},[368,1673,1674,1676,1679,1681,1684],{"class":370,"line":371},[368,1675,375],{"class":374},[368,1677,1678],{"class":378}," { render, screen } ",[368,1680,382],{"class":374},[368,1682,1683],{"class":385}," '@testing-library\u002Freact'",[368,1685,389],{"class":378},[368,1687,1688,1690,1693,1695,1698],{"class":370,"line":392},[368,1689,375],{"class":374},[368,1691,1692],{"class":378}," userEvent ",[368,1694,382],{"class":374},[368,1696,1697],{"class":385}," '@testing-library\u002Fuser-event'",[368,1699,389],{"class":378},[368,1701,1702,1704,1707,1709,1712],{"class":370,"line":399},[368,1703,375],{"class":374},[368,1705,1706],{"class":378}," { AccessibleModal } ",[368,1708,382],{"class":374},[368,1710,1711],{"class":385}," '.\u002FAccessibleModal'",[368,1713,389],{"class":378},[368,1715,1716],{"class":370,"line":452},[368,1717,396],{"emptyLinePlaceholder":395},[368,1719,1720,1723,1725,1728,1730,1733,1735,1737],{"class":370,"line":486},[368,1721,1722],{"class":408},"test",[368,1724,412],{"class":378},[368,1726,1727],{"class":385},"'focus remains trapped within modal'",[368,1729,426],{"class":378},[368,1731,1732],{"class":374},"async",[368,1734,920],{"class":378},[368,1736,518],{"class":374},[368,1738,521],{"class":378},[368,1740,1741,1743,1746,1748,1751,1754],{"class":370,"line":491},[368,1742,455],{"class":374},[368,1744,1745],{"class":422}," user",[368,1747,461],{"class":374},[368,1749,1750],{"class":378}," userEvent.",[368,1752,1753],{"class":408},"setup",[368,1755,733],{"class":378},[368,1757,1758,1761,1764,1767,1769,1772,1775,1778,1780,1782,1785,1787,1790,1793,1795,1798,1801,1803,1806,1808,1810],{"class":370,"line":524},[368,1759,1760],{"class":408}," render",[368,1762,1763],{"class":378},"(\u003C",[368,1765,1766],{"class":408},"AccessibleModal",[368,1768,1109],{"class":408},[368,1770,1771],{"class":378},"={",[368,1773,1774],{"class":415},"true",[368,1776,1777],{"class":378},"} ",[368,1779,1174],{"class":408},[368,1781,1771],{"class":378},[368,1783,1784],{"class":408},"jest",[368,1786,437],{"class":378},[368,1788,1789],{"class":408},"fn",[368,1791,1792],{"class":378},"()}>\u003C",[368,1794,1586],{"class":408},[368,1796,1797],{"class":378},">Confirm",[368,1799,1800],{"class":374},"\u003C\u002F",[368,1802,1586],{"class":378},[368,1804,1805],{"class":374},">",[368,1807,1800],{"class":378},[368,1809,1766],{"class":408},[368,1811,1812],{"class":378},">);\n",[368,1814,1815],{"class":370,"line":553},[368,1816,556],{"class":378},[368,1818,1819,1821,1824,1826,1829,1832,1834,1837],{"class":370,"line":559},[368,1820,455],{"class":374},[368,1822,1823],{"class":422}," modal",[368,1825,461],{"class":374},[368,1827,1828],{"class":378}," screen.",[368,1830,1831],{"class":408},"getByRole",[368,1833,412],{"class":378},[368,1835,1836],{"class":385},"'dialog'",[368,1838,483],{"class":378},[368,1840,1841,1844,1847,1850],{"class":370,"line":572},[368,1842,1843],{"class":408}," expect",[368,1845,1846],{"class":378},"(modal).",[368,1848,1849],{"class":408},"toHaveFocus",[368,1851,733],{"class":378},[368,1853,1854],{"class":370,"line":594},[368,1855,556],{"class":378},[368,1857,1858,1861,1864,1867],{"class":370,"line":600},[368,1859,1860],{"class":374}," await",[368,1862,1863],{"class":378}," user.",[368,1865,1866],{"class":408},"tab",[368,1868,733],{"class":378},[368,1870,1871,1873,1876,1878,1880,1883,1886,1889,1892,1894],{"class":370,"line":606},[368,1872,1843],{"class":408},[368,1874,1875],{"class":378},"(screen.",[368,1877,1831],{"class":408},[368,1879,412],{"class":378},[368,1881,1882],{"class":385},"'button'",[368,1884,1885],{"class":378},", { name: ",[368,1887,1888],{"class":385},"'Confirm'",[368,1890,1891],{"class":378}," })).",[368,1893,1849],{"class":408},[368,1895,733],{"class":378},[368,1897,1898],{"class":370,"line":611},[368,1899,556],{"class":378},[368,1901,1902,1904,1906,1908],{"class":370,"line":634},[368,1903,1860],{"class":374},[368,1905,1863],{"class":378},[368,1907,1866],{"class":408},[368,1909,733],{"class":378},[368,1911,1912,1914,1916,1918,1920,1922,1924,1927,1929,1931],{"class":370,"line":639},[368,1913,1843],{"class":408},[368,1915,1875],{"class":378},[368,1917,1831],{"class":408},[368,1919,412],{"class":378},[368,1921,1882],{"class":385},[368,1923,1885],{"class":378},[368,1925,1926],{"class":385},"'Close modal'",[368,1928,1891],{"class":378},[368,1930,1849],{"class":408},[368,1932,733],{"class":378},[368,1934,1935],{"class":370,"line":658},[368,1936,556],{"class":378},[368,1938,1939,1941,1943,1945],{"class":370,"line":681},[368,1940,1860],{"class":374},[368,1942,1863],{"class":378},[368,1944,1866],{"class":408},[368,1946,733],{"class":378},[368,1948,1949,1951,1953,1955,1957,1959,1962,1964,1967],{"class":370,"line":686},[368,1950,1843],{"class":408},[368,1952,1875],{"class":378},[368,1954,1831],{"class":408},[368,1956,412],{"class":378},[368,1958,1836],{"class":385},[368,1960,1961],{"class":378},")).",[368,1963,1849],{"class":408},[368,1965,1966],{"class":378},"(); ",[368,1968,1969],{"class":882},"\u002F\u002F Wraps to first element\n",[368,1971,1972],{"class":370,"line":703},[368,1973,1974],{"class":378},"});\n",[1654,1976,1978],{"id":1977},"static-aria-validation","Static ARIA Validation",[166,1980,1981,1982,1985,1986,1988],{},"Integrate ",[175,1983,1984],{},"jest-axe"," into your test suite to catch missing roles, incorrect ",[175,1987,229],{}," states, and invalid focusable elements before deployment.",[359,1990,1992],{"className":361,"code":1991,"language":363,"meta":364,"style":364},"import { axe, toHaveNoViolations } from 'jest-axe';\nexpect.extend(toHaveNoViolations);\n\ntest('modal passes axe accessibility checks', async () => {\n const { container } = render(\u003CAccessibleModal isOpen={true} onClose={jest.fn()}>Content\u003C\u002FAccessibleModal>);\n const results = await axe(container);\n expect(results).toHaveNoViolations();\n});\n",[175,1993,1994,2008,2019,2023,2042,2091,2108,2120],{"__ignoreMap":364},[368,1995,1996,1998,2001,2003,2006],{"class":370,"line":371},[368,1997,375],{"class":374},[368,1999,2000],{"class":378}," { axe, toHaveNoViolations } ",[368,2002,382],{"class":374},[368,2004,2005],{"class":385}," 'jest-axe'",[368,2007,389],{"class":378},[368,2009,2010,2013,2016],{"class":370,"line":392},[368,2011,2012],{"class":378},"expect.",[368,2014,2015],{"class":408},"extend",[368,2017,2018],{"class":378},"(toHaveNoViolations);\n",[368,2020,2021],{"class":370,"line":399},[368,2022,396],{"emptyLinePlaceholder":395},[368,2024,2025,2027,2029,2032,2034,2036,2038,2040],{"class":370,"line":452},[368,2026,1722],{"class":408},[368,2028,412],{"class":378},[368,2030,2031],{"class":385},"'modal passes axe accessibility checks'",[368,2033,426],{"class":378},[368,2035,1732],{"class":374},[368,2037,920],{"class":378},[368,2039,518],{"class":374},[368,2041,521],{"class":378},[368,2043,2044,2046,2049,2052,2054,2056,2058,2060,2062,2064,2066,2068,2070,2072,2074,2076,2078,2080,2083,2085,2087,2089],{"class":370,"line":486},[368,2045,455],{"class":374},[368,2047,2048],{"class":378}," { ",[368,2050,2051],{"class":422},"container",[368,2053,756],{"class":378},[368,2055,848],{"class":374},[368,2057,1760],{"class":408},[368,2059,1763],{"class":378},[368,2061,1766],{"class":408},[368,2063,1109],{"class":408},[368,2065,1771],{"class":378},[368,2067,1774],{"class":415},[368,2069,1777],{"class":378},[368,2071,1174],{"class":408},[368,2073,1771],{"class":378},[368,2075,1784],{"class":408},[368,2077,437],{"class":378},[368,2079,1789],{"class":408},[368,2081,2082],{"class":378},"()}>Content",[368,2084,1800],{"class":374},[368,2086,1766],{"class":378},[368,2088,1805],{"class":374},[368,2090,483],{"class":378},[368,2092,2093,2095,2098,2100,2102,2105],{"class":370,"line":491},[368,2094,455],{"class":374},[368,2096,2097],{"class":422}," results",[368,2099,461],{"class":374},[368,2101,1860],{"class":374},[368,2103,2104],{"class":408}," axe",[368,2106,2107],{"class":378},"(container);\n",[368,2109,2110,2112,2115,2118],{"class":370,"line":524},[368,2111,1843],{"class":408},[368,2113,2114],{"class":378},"(results).",[368,2116,2117],{"class":408},"toHaveNoViolations",[368,2119,733],{"class":378},[368,2121,2122],{"class":370,"line":553},[368,2123,1974],{"class":378},[1654,2125,2127],{"id":2126},"cicd-pipeline-configuration","CI\u002FCD Pipeline Configuration",[166,2129,2130],{},"Add a dedicated accessibility validation step to your GitHub Actions or GitLab CI workflow.",[359,2132,2136],{"className":2133,"code":2134,"language":2135,"meta":364,"style":364},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fa11y-ci.yml\nname: Accessibility Validation\non: [push, pull_request]\njobs:\n a11y-check:\n runs-on: ubuntu-latest\n steps:\n - uses: actions\u002Fcheckout@v4\n - name: Setup Node\n uses: actions\u002Fsetup-node@v4\n with: { node-version: '20' }\n - run: npm ci\n - run: npm run test:unit -- --coverage\n - run: npx playwright test --config=playwright-a11y.config.ts\n - name: Upload Axe Reports\n if: always()\n uses: actions\u002Fupload-artifact@v4\n with: { name: a11y-reports, path: .\u002Ftest-results\u002F }\n","yaml",[175,2137,2138,2143,2154,2173,2181,2188,2198,2205,2218,2229,2239,2257,2269,2280,2291,2302,2311,2320],{"__ignoreMap":364},[368,2139,2140],{"class":370,"line":371},[368,2141,2142],{"class":882},"# .github\u002Fworkflows\u002Fa11y-ci.yml\n",[368,2144,2145,2148,2151],{"class":370,"line":392},[368,2146,2147],{"class":1492},"name",[368,2149,2150],{"class":378},": ",[368,2152,2153],{"class":385},"Accessibility Validation\n",[368,2155,2156,2159,2162,2165,2167,2170],{"class":370,"line":399},[368,2157,2158],{"class":422},"on",[368,2160,2161],{"class":378},": [",[368,2163,2164],{"class":385},"push",[368,2166,426],{"class":378},[368,2168,2169],{"class":385},"pull_request",[368,2171,2172],{"class":378},"]\n",[368,2174,2175,2178],{"class":370,"line":452},[368,2176,2177],{"class":1492},"jobs",[368,2179,2180],{"class":378},":\n",[368,2182,2183,2186],{"class":370,"line":486},[368,2184,2185],{"class":1492}," a11y-check",[368,2187,2180],{"class":378},[368,2189,2190,2193,2195],{"class":370,"line":491},[368,2191,2192],{"class":1492}," runs-on",[368,2194,2150],{"class":378},[368,2196,2197],{"class":385},"ubuntu-latest\n",[368,2199,2200,2203],{"class":370,"line":524},[368,2201,2202],{"class":1492}," steps",[368,2204,2180],{"class":378},[368,2206,2207,2210,2213,2215],{"class":370,"line":553},[368,2208,2209],{"class":378}," - ",[368,2211,2212],{"class":1492},"uses",[368,2214,2150],{"class":378},[368,2216,2217],{"class":385},"actions\u002Fcheckout@v4\n",[368,2219,2220,2222,2224,2226],{"class":370,"line":559},[368,2221,2209],{"class":378},[368,2223,2147],{"class":1492},[368,2225,2150],{"class":378},[368,2227,2228],{"class":385},"Setup Node\n",[368,2230,2231,2234,2236],{"class":370,"line":572},[368,2232,2233],{"class":1492}," uses",[368,2235,2150],{"class":378},[368,2237,2238],{"class":385},"actions\u002Fsetup-node@v4\n",[368,2240,2241,2244,2247,2250,2252,2255],{"class":370,"line":594},[368,2242,2243],{"class":1492}," with",[368,2245,2246],{"class":378},": { ",[368,2248,2249],{"class":1492},"node-version",[368,2251,2150],{"class":378},[368,2253,2254],{"class":385},"'20'",[368,2256,750],{"class":378},[368,2258,2259,2261,2264,2266],{"class":370,"line":600},[368,2260,2209],{"class":378},[368,2262,2263],{"class":1492},"run",[368,2265,2150],{"class":378},[368,2267,2268],{"class":385},"npm ci\n",[368,2270,2271,2273,2275,2277],{"class":370,"line":606},[368,2272,2209],{"class":378},[368,2274,2263],{"class":1492},[368,2276,2150],{"class":378},[368,2278,2279],{"class":385},"npm run test:unit -- --coverage\n",[368,2281,2282,2284,2286,2288],{"class":370,"line":611},[368,2283,2209],{"class":378},[368,2285,2263],{"class":1492},[368,2287,2150],{"class":378},[368,2289,2290],{"class":385},"npx playwright test --config=playwright-a11y.config.ts\n",[368,2292,2293,2295,2297,2299],{"class":370,"line":634},[368,2294,2209],{"class":378},[368,2296,2147],{"class":1492},[368,2298,2150],{"class":378},[368,2300,2301],{"class":385},"Upload Axe Reports\n",[368,2303,2304,2306,2308],{"class":370,"line":639},[368,2305,527],{"class":1492},[368,2307,2150],{"class":378},[368,2309,2310],{"class":385},"always()\n",[368,2312,2313,2315,2317],{"class":370,"line":658},[368,2314,2233],{"class":1492},[368,2316,2150],{"class":378},[368,2318,2319],{"class":385},"actions\u002Fupload-artifact@v4\n",[368,2321,2322,2324,2326,2328,2330,2333,2335,2338,2340,2343],{"class":370,"line":681},[368,2323,2243],{"class":1492},[368,2325,2246],{"class":378},[368,2327,2147],{"class":1492},[368,2329,2150],{"class":378},[368,2331,2332],{"class":385},"a11y-reports",[368,2334,426],{"class":378},[368,2336,2337],{"class":1492},"path",[368,2339,2150],{"class":378},[368,2341,2342],{"class":385},".\u002Ftest-results\u002F",[368,2344,750],{"class":378},[166,2346,2347,2349,2350,2353],{},[182,2348,971],{}," Run the CI pipeline locally using ",[175,2351,2352],{},"npm run test:unit",". Validate against WCAG 2.2 success criteria using automated tools and manual screen reader passes across Chrome, Firefox, and Safari.",[235,2355,2357],{"id":2356},"common-pitfalls","Common Pitfalls",[186,2359,2360,2375,2381,2391,2400],{},[189,2361,2362,2371,2372,2374],{},[182,2363,2364,2365,2367,2368,419],{},"Relying on React's ",[175,2366,253],{}," instead of native ",[175,2369,2370],{},"document.addEventListener"," Synthetic events do not intercept native ",[175,2373,257],{}," navigation, allowing focus to leak outside the portal.",[189,2376,2377,2380],{},[182,2378,2379],{},"Forgetting to restore focus to the original trigger element:"," Closing a modal without returning focus leaves keyboard users stranded at the top of the document or in an undefined state.",[189,2382,2383,2390],{},[182,2384,2385,2386,2389],{},"Using ",[175,2387,2388],{},"tabindex=\"0\""," on non-interactive elements:"," Forces non-focusable elements into the tab order, violating semantic HTML principles and confusing screen readers.",[189,2392,2393,2399],{},[182,2394,2395,2396,2398],{},"Neglecting ",[175,2397,346],{}," reverse navigation:"," Implementing only forward tabbing causes focus to jump to the browser chrome or background elements when navigating backwards.",[189,2401,2402,2408,2409,2411,2412,2414],{},[182,2403,2404,2405,2407],{},"Applying ",[175,2406,229],{}," without hiding background content:"," ",[175,2410,1017],{}," is a hint to assistive technology; it does not physically prevent focus traversal. Pair it with ",[175,2413,1031],{}," on the main app container for deterministic behavior.",[235,2416,2418],{"id":2417},"frequently-asked-questions","Frequently Asked Questions",[166,2420,2421,2427,2428,2430],{},[182,2422,2423,2424,2426],{},"Why does focus escape my React modal when using ",[175,2425,177],{},"?","\nReact portals render DOM nodes outside the parent component tree, which breaks standard synthetic event bubbling. Native focus events do not respect React's synthetic event system, requiring manual ",[175,2429,261],{}," listeners attached directly to the portal container to trap focus effectively.",[166,2432,2433,2442,2443,2445,2446,2448],{},[182,2434,2435,2436,2438,2439,2441],{},"Should I use ",[175,2437,229],{}," or ",[175,2440,1031],{}," for background content?","\nUse both for maximum compatibility. ",[175,2444,1017],{}," instructs screen readers to treat the dialog as the sole interactive context, while applying ",[175,2447,1031],{}," to the main app container physically prevents focus traversal and click events from reaching background elements. This dual approach ensures deterministic compliance across different assistive technologies.",[166,2450,2451,2454,2455,2457,2458,997,2460,2462,2463,2465,2466,2469],{},[182,2452,2453],{},"How do I test focus traps in automated CI\u002FCD pipelines?","\nUse ",[175,2456,1665],{}," to simulate ",[175,2459,257],{},[175,2461,346],{}," sequences, then assert that ",[175,2464,985],{}," remains within the expected container boundaries. Combine this with ",[175,2467,2468],{},"axe-core"," for static ARIA validation to catch missing labels, incorrect roles, or invalid focusable elements before deployment.",[2471,2472,2473],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":364,"searchDepth":392,"depth":392,"links":2475},[2476,2477,2479,2481,2486,2487],{"id":237,"depth":392,"text":238},{"id":305,"depth":392,"text":2478},"Building a Robust useFocusTrap Hook",{"id":993,"depth":392,"text":2480},"Integrating with createPortal and aria-modal",{"id":1648,"depth":392,"text":1649,"children":2482},[2483,2484,2485],{"id":1656,"depth":399,"text":1657},{"id":1977,"depth":399,"text":1978},{"id":2126,"depth":399,"text":2127},{"id":2356,"depth":392,"text":2357},{"id":2417,"depth":392,"text":2418},null,"Fix broken focus traps in React portals with reliable strategies for tab order, return focus, and assistive technology compatibility.","md",{},false,{"title":133,"description":2489},"FxJjxGBXykufXQtkj0OmJcGODMuCK7dGRXP_RCYXxXM",[2496,2526,2527],{"title":5,"path":6,"stem":7,"children":2497},[2498,2499,2502,2505,2511,2517,2523],{"title":10,"path":6,"stem":11},{"title":13,"path":14,"stem":15,"children":2500},[2501],{"title":13,"path":14,"stem":15},{"title":19,"path":20,"stem":21,"children":2503},[2504],{"title":19,"path":20,"stem":21},{"title":25,"path":26,"stem":27,"children":2506},[2507,2508],{"title":25,"path":26,"stem":27},{"title":31,"path":32,"stem":33,"children":2509},[2510],{"title":31,"path":32,"stem":33},{"title":37,"path":38,"stem":39,"children":2512},[2513,2514],{"title":37,"path":38,"stem":39},{"title":43,"path":44,"stem":45,"children":2515},[2516],{"title":43,"path":44,"stem":45},{"title":49,"path":50,"stem":51,"children":2518},[2519,2520],{"title":49,"path":50,"stem":51},{"title":55,"path":56,"stem":57,"children":2521},[2522],{"title":55,"path":56,"stem":57},{"title":61,"path":62,"stem":63,"children":2524},[2525],{"title":61,"path":62,"stem":63},{"title":67,"path":68,"stem":69},{"title":71,"path":72,"stem":73,"children":2528},[2529,2530,2536,2542,2545,2554,2563],{"title":76,"path":72,"stem":77},{"title":79,"path":80,"stem":81,"children":2531},[2532,2533],{"title":79,"path":80,"stem":81},{"title":85,"path":86,"stem":87,"children":2534},[2535],{"title":85,"path":86,"stem":87},{"title":91,"path":92,"stem":93,"children":2537},[2538,2539],{"title":91,"path":92,"stem":93},{"title":97,"path":98,"stem":99,"children":2540},[2541],{"title":97,"path":98,"stem":99},{"title":103,"path":104,"stem":105,"children":2543},[2544],{"title":103,"path":104,"stem":105},{"title":109,"path":110,"stem":111,"children":2546},[2547,2548,2551],{"title":109,"path":110,"stem":111},{"title":115,"path":116,"stem":117,"children":2549},[2550],{"title":115,"path":116,"stem":117},{"title":121,"path":122,"stem":123,"children":2552},[2553],{"title":121,"path":122,"stem":123},{"title":127,"path":128,"stem":129,"children":2555},[2556,2557,2560],{"title":127,"path":128,"stem":129},{"title":133,"path":134,"stem":135,"children":2558},[2559],{"title":133,"path":134,"stem":135},{"title":139,"path":140,"stem":141,"children":2561},[2562],{"title":139,"path":140,"stem":141},{"title":145,"path":146,"stem":147,"children":2564},[2565,2566],{"title":145,"path":146,"stem":147},{"title":151,"path":152,"stem":153,"children":2567},[2568],{"title":151,"path":152,"stem":153},1778094796327]