55 PanelGroup ,
66 PanelResizeHandle ,
77} from 'react-resizable-panels' ;
8+ import { useTranslation } from 'react-i18next' ;
89
910import { useResponsiveStore } from '@/stores' ;
1011
@@ -22,12 +23,15 @@ type ResizableLeftPanelProps = {
2223 maxPanelSizePx ?: number ;
2324} ;
2425
26+ const RESIZE_HANDLE_ID = 'left-panel-resize-handle' ;
27+
2528export const ResizableLeftPanel = ( {
2629 leftPanel,
2730 children,
2831 minPanelSizePx = 300 ,
2932 maxPanelSizePx = 450 ,
3033} : ResizableLeftPanelProps ) => {
34+ const { t } = useTranslation ( ) ;
3135 const { isDesktop } = useResponsiveStore ( ) ;
3236 const { isPanelOpen } = useLeftPanelStore ( ) ;
3337 const ref = useRef < ImperativePanelHandle > ( null ) ;
@@ -96,14 +100,56 @@ export const ResizableLeftPanel = ({
96100 } ;
97101 } , [ isDesktop ] ) ;
98102
103+ /**
104+ * Workaround: NVDA does not enter focus mode for role="separator"
105+ * intercepted by browse-mode navigation and never reach the handle.
106+ * Changing the role to "slider" makes NVDA reliably switch to focus
107+ * mode, restoring progressive keyboard resize with arrow keys.
108+ */
109+ useEffect ( ( ) => {
110+ if ( ! isPanelOpen ) {
111+ return ;
112+ }
113+ const handle = document . getElementById ( RESIZE_HANDLE_ID ) ;
114+ if ( ! handle ) {
115+ return ;
116+ }
117+
118+ handle . setAttribute ( 'role' , 'slider' ) ;
119+ handle . setAttribute ( 'aria-orientation' , 'vertical' ) ;
120+ handle . setAttribute ( 'aria-label' , t ( 'Resize sidebar' ) ) ;
121+
122+ const updateValueText = ( ) => {
123+ const value = handle . getAttribute ( 'aria-valuenow' ) ;
124+ if ( value ) {
125+ const widthPx = Math . round ( ( parseFloat ( value ) / 100 ) * window . innerWidth ) ;
126+ handle . setAttribute (
127+ 'aria-valuetext' ,
128+ t ( 'Sidebar width: {{widthPx}} pixels' , { widthPx } ) ,
129+ ) ;
130+ }
131+ } ;
132+ updateValueText ( ) ;
133+
134+ const observer = new MutationObserver ( updateValueText ) ;
135+ observer . observe ( handle , {
136+ attributes : true ,
137+ attributeFilter : [ 'aria-valuenow' ] ,
138+ } ) ;
139+
140+ return ( ) => {
141+ observer . disconnect ( ) ;
142+ } ;
143+ } , [ isPanelOpen , t ] ) ;
144+
99145 const handleResize = ( sizePercent : number ) => {
100146 const widthPx = ( sizePercent / 100 ) * window . innerWidth ;
101147 savedWidthPxRef . current = widthPx ;
102148 setPanelSizePercent ( sizePercent ) ;
103149 } ;
104150
105151 return (
106- < PanelGroup direction = "horizontal" >
152+ < PanelGroup direction = "horizontal" keyboardResizeBy = { 1 } >
107153 < Panel
108154 ref = { ref }
109155 className = "--docs--resizable-left-panel"
@@ -132,6 +178,7 @@ export const ResizableLeftPanel = ({
132178 </ Panel >
133179 { isPanelOpen && (
134180 < PanelResizeHandle
181+ id = { RESIZE_HANDLE_ID }
135182 style = { {
136183 borderRightWidth : '1px' ,
137184 borderRightStyle : 'solid' ,
0 commit comments