fixup! (frontend) make components accessible to screen readers

This commit is contained in:
Cyril
2025-09-04 12:21:28 +02:00
parent a88fa5a26c
commit 360b83b957
4 changed files with 41 additions and 25 deletions
@@ -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;
@@ -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<T> = NodeRendererProps<TreeDataItem<T>>['node'] & {
isFocused?: boolean;
focus?: () => void;
@@ -19,17 +21,13 @@ export const useActionableMode = <T>(
const actionsRef = useRef<HTMLDivElement>(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 = <T>(
e.preventDefault();
const focusables = actionsRef.current?.querySelectorAll<HTMLElement>(
'button, [role="button"], a[href], input, [tabindex]:not([tabindex="-1"])',
SELECTORS.FOCUSABLE,
);
const first = focusables?.[0];
@@ -67,9 +65,7 @@ export const useActionableMode = <T>(
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 = <T>(
e.stopPropagation();
const focusables = actionsRef.current?.querySelectorAll<HTMLElement>(
'button, [role="button"], a[href], input, [tabindex]:not([tabindex="-1"])',
SELECTORS.FOCUSABLE,
);
if (!focusables || focusables.length === 0) {
@@ -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<HTMLElement>(
'[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<HTMLElement>(
'[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<HTMLElement>(
'[data-testid^="doc-sub-page-item-"]',
SELECTORS.DATA_TESTID_DOC_SUB_PAGE_ITEM_PREFIX,
);
if (focusableElement) {
@@ -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();