fixup! ️(frontend) fix sidebar resize handle for screen readers

This commit is contained in:
Cyril
2026-04-22 14:07:38 +02:00
parent b7d0ba7bdd
commit bfeeba5fbc
2 changed files with 39 additions and 36 deletions

View File

@@ -9,7 +9,7 @@ and this project adheres to
### Changed
- 💄(frontend) improve comments highlights #1961
♿️(frontend) fix sidebar resize handle for screen readers #2122
- ♿️(frontend) fix sidebar resize handle for screen readers #2122
## [v4.8.3] - 2026-03-23

View File

@@ -16,6 +16,23 @@ const pxToPercent = (px: number) => {
return (px / window.innerWidth) * 100;
};
const RESIZE_HANDLE_ID = 'left-panel-resize-handle';
const getSidebarWidthLabel = (
current: number,
min: number,
max: number,
): 'narrow' | 'medium' | 'wide' => {
const ratio = (current - min) / (max - min);
if (ratio < 1 / 3) {
return 'narrow';
}
if (ratio < 2 / 3) {
return 'medium';
}
return 'wide';
};
type ResizableLeftPanelProps = {
leftPanel: React.ReactNode;
children: React.ReactNode;
@@ -23,8 +40,6 @@ type ResizableLeftPanelProps = {
maxPanelSizePx?: number;
};
const RESIZE_HANDLE_ID = 'left-panel-resize-handle';
export const ResizableLeftPanel = ({
leftPanel,
children,
@@ -102,47 +117,21 @@ export const ResizableLeftPanel = ({
/**
* Workaround: NVDA does not enter focus mode for role="separator"
* (https://github.com/nvaccess/nvda/issues/11403), so arrow keys are
* intercepted by browse-mode navigation and never reach the handle.
* Changing the role to "slider" makes NVDA reliably switch to focus
* mode, restoring progressive keyboard resize with arrow keys.
*
* Note: PanelResizeHandle does not expose a ref (no RefAttributes in its
* type definition), so we use id + getElementById as the only viable option.
* Only role needs to be overridden here; aria-* props are passed directly.
*/
useEffect(() => {
if (!isPanelOpen) {
return;
}
const handle = document.getElementById(RESIZE_HANDLE_ID);
if (!handle) {
return;
}
handle.setAttribute('role', 'slider');
handle.setAttribute('aria-orientation', 'vertical');
handle.setAttribute('aria-label', t('Resize sidebar'));
const updateValueText = () => {
const value = handle.getAttribute('aria-valuenow');
if (value) {
const widthPx = Math.round(
(parseFloat(value) / 100) * window.innerWidth,
);
handle.setAttribute(
'aria-valuetext',
t('Sidebar width: {{widthPx}} pixels', { widthPx }),
);
}
};
updateValueText();
const observer = new MutationObserver(updateValueText);
observer.observe(handle, {
attributes: true,
attributeFilter: ['aria-valuenow'],
});
return () => {
observer.disconnect();
};
}, [isPanelOpen, t]);
document.getElementById(RESIZE_HANDLE_ID)?.setAttribute('role', 'slider');
}, [isPanelOpen]);
const handleResize = (sizePercent: number) => {
const widthPx = (sizePercent / 100) * window.innerWidth;
@@ -181,6 +170,20 @@ export const ResizableLeftPanel = ({
{isPanelOpen && (
<PanelResizeHandle
id={RESIZE_HANDLE_ID}
aria-label={t('Resize sidebar')}
aria-orientation="vertical"
aria-valuemin={Math.round(minPanelSizePercent)}
aria-valuemax={Math.round(maxPanelSizePercent)}
aria-valuenow={Math.round(panelSizePercent)}
aria-valuetext={t(`Sidebar width: {{label}}`, {
label: t(
getSidebarWidthLabel(
panelSizePercent,
minPanelSizePercent,
maxPanelSizePercent,
),
),
})}
style={{
borderRightWidth: '1px',
borderRightStyle: 'solid',