|
1 | 1 | import { AnimatePresence, motion } from "framer-motion"; |
2 | | -import React, { useCallback, useState } from "react"; |
| 2 | +import React, { useCallback, useEffect, useRef, useState } from "react"; |
3 | 3 | import { ToggleArrowIcon } from "~/assets/icons/ToggleArrowIcon"; |
4 | 4 |
|
5 | 5 | type Props = { |
@@ -27,13 +27,25 @@ export function SideMenuSection({ |
27 | 27 | headerAction, |
28 | 28 | }: Props) { |
29 | 29 | const [isCollapsed, setIsCollapsed] = useState(initialCollapsed); |
| 30 | + const contentRef = useRef<HTMLDivElement>(null); |
30 | 31 |
|
31 | 32 | const handleToggle = useCallback(() => { |
32 | 33 | const newIsCollapsed = !isCollapsed; |
33 | 34 | setIsCollapsed(newIsCollapsed); |
34 | 35 | onCollapseToggle?.(newIsCollapsed); |
35 | 36 | }, [isCollapsed, onCollapseToggle]); |
36 | 37 |
|
| 38 | + // When the section is collapsed its items are visually hidden (height 0) but stay in the DOM for |
| 39 | + // the animation, so remove them from the tab order and the accessibility tree with `inert` — |
| 40 | + // otherwise keyboard/screen-reader users can focus invisible items. `inert` doesn't affect |
| 41 | + // layout, so the height animation is unchanged. Set the DOM property directly since React 18's |
| 42 | + // handling of the `inert` prop is unreliable. |
| 43 | + useEffect(() => { |
| 44 | + if (contentRef.current) { |
| 45 | + contentRef.current.inert = isCollapsed; |
| 46 | + } |
| 47 | + }, [isCollapsed]); |
| 48 | + |
37 | 49 | return ( |
38 | 50 | <div className="w-full overflow-hidden"> |
39 | 51 | {/* Header container - stays in DOM to preserve height */} |
@@ -81,6 +93,7 @@ export function SideMenuSection({ |
81 | 93 | </div> |
82 | 94 | <AnimatePresence initial={false}> |
83 | 95 | <motion.div |
| 96 | + ref={contentRef} |
84 | 97 | className="w-full" |
85 | 98 | initial={isCollapsed ? "collapsed" : "expanded"} |
86 | 99 | animate={isCollapsed ? "collapsed" : "expanded"} |
|
0 commit comments