Skip to content

Commit aa9cacf

Browse files
committed
♿️(frontend) fix sidebar resize handle for screen readers
Expose the handle as a slider so arrow keys work with NVDA
1 parent 0d09f76 commit aa9cacf

1 file changed

Lines changed: 48 additions & 1 deletion

File tree

src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
PanelGroup,
66
PanelResizeHandle,
77
} from 'react-resizable-panels';
8+
import { useTranslation } from 'react-i18next';
89

910
import { 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+
2528
export 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

Comments
 (0)