From 360b83b9578986ca60bdac019eb57f898cbe3cd8 Mon Sep 17 00:00:00 2001 From: Cyril Date: Wed, 13 Aug 2025 18:40:50 +0200 Subject: [PATCH] =?UTF-8?q?fixup!=20=E2=9C=A8(frontend)=20make=20component?= =?UTF-8?q?s=20accessible=20to=20screen=20readers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/docs/doc-tree/dom-selectors.ts | 16 +++++++++++ .../docs/doc-tree/hooks/useActionableMode.ts | 18 +++++------- .../hooks/useDropdownFocusManagement.ts | 28 ++++++++++--------- .../doc-tree/hooks/useKeyboardActivation.tsx | 4 ++- 4 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-tree/dom-selectors.ts diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/dom-selectors.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/dom-selectors.ts new file mode 100644 index 00000000..0b166b73 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/dom-selectors.ts @@ -0,0 +1,16 @@ +// Centralized selector constants for doc-tree hooks + +export const SELECTORS = { + MODAL: + '[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content', + MODAL_SCROLLER: '.c__modal__scroller', + ACTIONS_CONTAINER: '.--docs--doc-tree-item-actions', + DOC_SUB_PAGE_ITEM: '.--docs-sub-page-item', + ROLE_MENU: '[role="menu"]', + ROLE_MENUITEM_OR_BUTTON: + '[role="menuitem"], button, [tabindex]:not([tabindex="-1"])', + FOCUSABLE: + 'button, [role="button"], a[href], input, [tabindex]:not([tabindex="-1"])', + DATA_TESTID_DOC_SUB_PAGE_ITEM: 'doc-sub-page-item-', + DATA_TESTID_DOC_SUB_PAGE_ITEM_PREFIX: '[data-testid^="doc-sub-page-item-"]', +} as const; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useActionableMode.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useActionableMode.ts index 7c807e4c..c88f04ed 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useActionableMode.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useActionableMode.ts @@ -2,6 +2,8 @@ import { TreeDataItem } from '@gouvfr-lasuite/ui-kit'; import { useEffect, useRef } from 'react'; import type { NodeRendererProps } from 'react-arborist'; +import { SELECTORS } from '../dom-selectors'; + type FocusableNode = NodeRendererProps>['node'] & { isFocused?: boolean; focus?: () => void; @@ -19,17 +21,13 @@ export const useActionableMode = ( const actionsRef = useRef(null); useEffect(() => { - const modalOpen = document.querySelector( - '[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content', - ); + const modalOpen = document.querySelector(SELECTORS.MODAL); if (!node?.isFocused || modalOpen) { return; } const toActions = (e: KeyboardEvent) => { - const modalOpen = document.querySelector( - '[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content', - ); + const modalOpen = document.querySelector(SELECTORS.MODAL); if (modalOpen) { return; } @@ -46,7 +44,7 @@ export const useActionableMode = ( e.preventDefault(); const focusables = actionsRef.current?.querySelectorAll( - 'button, [role="button"], a[href], input, [tabindex]:not([tabindex="-1"])', + SELECTORS.FOCUSABLE, ); const first = focusables?.[0]; @@ -67,9 +65,7 @@ export const useActionableMode = ( return; } - const modal = document.querySelector( - '[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content', - ); + const modal = document.querySelector(SELECTORS.MODAL); if (modal) { return; } @@ -84,7 +80,7 @@ export const useActionableMode = ( e.stopPropagation(); const focusables = actionsRef.current?.querySelectorAll( - 'button, [role="button"], a[href], input, [tabindex]:not([tabindex="-1"])', + SELECTORS.FOCUSABLE, ); if (!focusables || focusables.length === 0) { diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownFocusManagement.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownFocusManagement.ts index ec87a497..0dd0b80e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownFocusManagement.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useDropdownFocusManagement.ts @@ -1,5 +1,7 @@ import { useEffect } from 'react'; +import { SELECTORS } from '../dom-selectors'; + interface UseDropdownFocusManagementProps { isOpen: boolean; docId: string; @@ -20,12 +22,12 @@ export const useDropdownFocusManagement = ({ const timer = setTimeout(() => { // Try to find menu in actions container first const menuElement = actionsRef?.current - ?.closest('.--docs--doc-tree-item-actions') - ?.querySelector('[role="menu"]'); + ?.closest(SELECTORS.ACTIONS_CONTAINER) + ?.querySelector(SELECTORS.ROLE_MENU); if (menuElement) { const firstMenuItem = menuElement.querySelector( - '[role="menuitem"], button, [tabindex]:not([tabindex="-1"])', + SELECTORS.ROLE_MENUITEM_OR_BUTTON, ); if (firstMenuItem) { firstMenuItem.focus(); @@ -34,11 +36,11 @@ export const useDropdownFocusManagement = ({ } // Fallback: find any menu in document - const allMenus = document.querySelectorAll('[role="menu"]'); + const allMenus = document.querySelectorAll(SELECTORS.ROLE_MENU); const lastMenu = allMenus[allMenus.length - 1]; if (lastMenu) { const firstMenuItem = lastMenu.querySelector( - '[role="menuitem"], button, [tabindex]:not([tabindex="-1"])', + SELECTORS.ROLE_MENUITEM_OR_BUTTON, ); if (firstMenuItem) { firstMenuItem.focus(); @@ -56,30 +58,30 @@ export const useDropdownFocusManagement = ({ } const timer = setTimeout(() => { - const modal = document.querySelector( - '[role="dialog"], .c__modal, [data-modal], .c__modal__overlay, .ReactModal_Content', - ); + const modal = document.querySelector(SELECTORS.MODAL); if (modal) { return; } // Only handle focus return if no modal is open - let subPageItem = actionsRef?.current?.closest('.--docs-sub-page-item'); + let subPageItem = actionsRef?.current?.closest( + SELECTORS.DOC_SUB_PAGE_ITEM, + ); // If not found, try to find by data-testid if (!subPageItem) { const testIdElement = document.querySelector( - `[data-testid="doc-sub-page-item-${docId}"]`, + `[data-testid="${SELECTORS.DATA_TESTID_DOC_SUB_PAGE_ITEM}${docId}"]`, ); subPageItem = - testIdElement?.closest('.--docs-sub-page-item') || - testIdElement?.parentElement?.closest('.--docs-sub-page-item'); + testIdElement?.closest(SELECTORS.DOC_SUB_PAGE_ITEM) || + testIdElement?.parentElement?.closest(SELECTORS.DOC_SUB_PAGE_ITEM); } // Focus the sub-document if found if (subPageItem) { const focusableElement = subPageItem.querySelector( - '[data-testid^="doc-sub-page-item-"]', + SELECTORS.DATA_TESTID_DOC_SUB_PAGE_ITEM_PREFIX, ); if (focusableElement) { diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx index 6d81e9dc..a0137936 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useKeyboardActivation.tsx @@ -1,5 +1,7 @@ import { useEffect } from 'react'; +import { SELECTORS } from '../dom-selectors'; + export const useKeyboardActivation = ( keys: string[], enabled: boolean, @@ -12,7 +14,7 @@ export const useKeyboardActivation = ( } const onKeyDown = (e: KeyboardEvent) => { - const modal = document.querySelector('.c__modal__scroller'); + const modal = document.querySelector(SELECTORS.MODAL_SCROLLER); if (modal) { e.stopPropagation();