From a2860e8fe61e0bb142edc7f962165704f3dc5e21 Mon Sep 17 00:00:00 2001 From: Cyril Date: Tue, 24 Mar 2026 15:17:44 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20fix=20sidebar=20?= =?UTF-8?q?resize=20handle=20for=20screen=20readers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the handle as a slider so arrow keys work with NVDA --- CHANGELOG.md | 1 + .../components/ResizableLeftPanel.tsx | 55 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 415bf128f..f9c2d365d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to - ♿️(frontend) make doc search result labels uniquely identifiable #2212 - ⬆️(backend) upgrade docspec to v3.0.x and adapt converter API #2220 - ✨(backend) make forward auth request uri header configurable #2241 +- ♿️(frontend) fix sidebar resize handle for screen readers #2122 ### Fixed diff --git a/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx index 03584d1de..cf89f7fbb 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ImperativePanelHandle, Panel, @@ -15,6 +16,27 @@ const pxToPercent = (px: number) => { return (px / window.innerWidth) * 100; }; +const RESIZE_HANDLE_ID = 'left-panel-resize-handle'; + +const getValueLabel = ( + current: number, + min: number, + max: number, + t: (key: string) => string, +): string => { + if (max <= min) { + return t('Sidebar width: medium'); + } + const ratio = (current - min) / (max - min); + if (ratio < 1 / 3) { + return t('Sidebar width: narrow'); + } + if (ratio < 2 / 3) { + return t('Sidebar width: medium'); + } + return t('Sidebar width: wide'); +}; + type ResizableLeftPanelProps = { leftPanel: React.ReactNode; children: React.ReactNode; @@ -28,6 +50,7 @@ export const ResizableLeftPanel = ({ minPanelSizePx = 300, maxPanelSizePx = 450, }: ResizableLeftPanelProps) => { + const { t } = useTranslation(); const { isDesktop } = useResponsiveStore(); const { isPanelOpen } = useLeftPanelStore(); const ref = useRef(null); @@ -96,6 +119,24 @@ export const ResizableLeftPanel = ({ }; }, [isDesktop]); + /** + * 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; + } + document.getElementById(RESIZE_HANDLE_ID)?.setAttribute('role', 'slider'); + }, [isPanelOpen]); + const handleResize = (sizePercent: number) => { const widthPx = (sizePercent / 100) * window.innerWidth; savedWidthPxRef.current = widthPx; @@ -103,7 +144,7 @@ export const ResizableLeftPanel = ({ }; return ( - + {isPanelOpen && (