11import { useEffect , useRef , useState } from 'react' ;
2+ import { useTranslation } from 'react-i18next' ;
23import {
34 ImperativePanelHandle ,
45 Panel ,
@@ -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,58 @@ 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 (
126+ ( parseFloat ( value ) / 100 ) * window . innerWidth ,
127+ ) ;
128+ handle . setAttribute (
129+ 'aria-valuetext' ,
130+ t ( 'Sidebar width: {{widthPx}} pixels' , { widthPx } ) ,
131+ ) ;
132+ }
133+ } ;
134+ updateValueText ( ) ;
135+
136+ const observer = new MutationObserver ( updateValueText ) ;
137+ observer . observe ( handle , {
138+ attributes : true ,
139+ attributeFilter : [ 'aria-valuenow' ] ,
140+ } ) ;
141+
142+ return ( ) => {
143+ observer . disconnect ( ) ;
144+ } ;
145+ } , [ isPanelOpen , t ] ) ;
146+
99147 const handleResize = ( sizePercent : number ) => {
100148 const widthPx = ( sizePercent / 100 ) * window . innerWidth ;
101149 savedWidthPxRef . current = widthPx ;
102150 setPanelSizePercent ( sizePercent ) ;
103151 } ;
104152
105153 return (
106- < PanelGroup direction = "horizontal" >
154+ < PanelGroup direction = "horizontal" keyboardResizeBy = { 1 } >
107155 < Panel
108156 ref = { ref }
109157 className = "--docs--resizable-left-panel"
@@ -132,6 +180,7 @@ export const ResizableLeftPanel = ({
132180 </ Panel >
133181 { isPanelOpen && (
134182 < PanelResizeHandle
183+ id = { RESIZE_HANDLE_ID }
135184 style = { {
136185 borderRightWidth : '1px' ,
137186 borderRightStyle : 'solid' ,
0 commit comments