Skip to content

Commit bfeeba5

Browse files
committed
fixup! ♿️(frontend) fix sidebar resize handle for screen readers
1 parent b7d0ba7 commit bfeeba5

2 files changed

Lines changed: 39 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to
99
### Changed
1010

1111
- 💄(frontend) improve comments highlights #1961
12-
♿️(frontend) fix sidebar resize handle for screen readers #2122
12+
- ♿️(frontend) fix sidebar resize handle for screen readers #2122
1313

1414
## [v4.8.3] - 2026-03-23
1515

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

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,30 @@ const pxToPercent = (px: number) => {
1616
return (px / window.innerWidth) * 100;
1717
};
1818

19+
const RESIZE_HANDLE_ID = 'left-panel-resize-handle';
20+
21+
const getSidebarWidthLabel = (
22+
current: number,
23+
min: number,
24+
max: number,
25+
): 'narrow' | 'medium' | 'wide' => {
26+
const ratio = (current - min) / (max - min);
27+
if (ratio < 1 / 3) {
28+
return 'narrow';
29+
}
30+
if (ratio < 2 / 3) {
31+
return 'medium';
32+
}
33+
return 'wide';
34+
};
35+
1936
type ResizableLeftPanelProps = {
2037
leftPanel: React.ReactNode;
2138
children: React.ReactNode;
2239
minPanelSizePx?: number;
2340
maxPanelSizePx?: number;
2441
};
2542

26-
const RESIZE_HANDLE_ID = 'left-panel-resize-handle';
27-
2843
export const ResizableLeftPanel = ({
2944
leftPanel,
3045
children,
@@ -102,47 +117,21 @@ export const ResizableLeftPanel = ({
102117

103118
/**
104119
* Workaround: NVDA does not enter focus mode for role="separator"
120+
* (https://github.com/nvaccess/nvda/issues/11403), so arrow keys are
105121
* intercepted by browse-mode navigation and never reach the handle.
106122
* Changing the role to "slider" makes NVDA reliably switch to focus
107123
* mode, restoring progressive keyboard resize with arrow keys.
124+
*
125+
* Note: PanelResizeHandle does not expose a ref (no RefAttributes in its
126+
* type definition), so we use id + getElementById as the only viable option.
127+
* Only role needs to be overridden here; aria-* props are passed directly.
108128
*/
109129
useEffect(() => {
110130
if (!isPanelOpen) {
111131
return;
112132
}
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]);
133+
document.getElementById(RESIZE_HANDLE_ID)?.setAttribute('role', 'slider');
134+
}, [isPanelOpen]);
146135

147136
const handleResize = (sizePercent: number) => {
148137
const widthPx = (sizePercent / 100) * window.innerWidth;
@@ -181,6 +170,20 @@ export const ResizableLeftPanel = ({
181170
{isPanelOpen && (
182171
<PanelResizeHandle
183172
id={RESIZE_HANDLE_ID}
173+
aria-label={t('Resize sidebar')}
174+
aria-orientation="vertical"
175+
aria-valuemin={Math.round(minPanelSizePercent)}
176+
aria-valuemax={Math.round(maxPanelSizePercent)}
177+
aria-valuenow={Math.round(panelSizePercent)}
178+
aria-valuetext={t(`Sidebar width: {{label}}`, {
179+
label: t(
180+
getSidebarWidthLabel(
181+
panelSizePercent,
182+
minPanelSizePercent,
183+
maxPanelSizePercent,
184+
),
185+
),
186+
})}
184187
style={{
185188
borderRightWidth: '1px',
186189
borderRightStyle: 'solid',

0 commit comments

Comments
 (0)