a11y: add screen reader support (#2040)

Tested with VoiceOver on iOS and macOS
This commit is contained in:
Huang Xin
2025-09-15 04:00:42 +08:00
committed by GitHub
parent b1fb477359
commit bb34b1ba59
55 changed files with 870 additions and 344 deletions
+50 -18
View File
@@ -1,10 +1,10 @@
import clsx from 'clsx';
import React, { useState, isValidElement, ReactElement, ReactNode } from 'react';
import { useTranslation } from '@/hooks/useTranslation';
import React, { useState, isValidElement, ReactElement, ReactNode, useRef } from 'react';
import { Overlay } from './Overlay';
import MenuItem from './MenuItem';
interface DropdownProps {
label: string;
className?: string;
menuClassName?: string;
buttonClassName?: string;
@@ -52,6 +52,7 @@ const enhanceMenuItems = (
};
const Dropdown: React.FC<DropdownProps> = ({
label,
className,
menuClassName,
buttonClassName,
@@ -59,14 +60,45 @@ const Dropdown: React.FC<DropdownProps> = ({
children,
onToggle,
}) => {
const _ = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [isPointerDown, setIsPointerDown] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const toggleDropdown = () => {
console.error('Toggling dropdown, current state:', isOpen);
const newIsOpen = !isOpen;
setIsDropdownOpen(newIsOpen);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
if (!isOpen) setIsDropdownOpen(true);
e.stopPropagation();
} else if (e.key === 'Escape' && isOpen) {
setIsDropdownOpen(false);
e.stopPropagation();
}
};
const handleFocus = () => {
if (!isOpen && !isPointerDown) {
setIsDropdownOpen(true);
}
setIsFocused(true);
};
const handleBlur = (e: React.FocusEvent) => {
if (!containerRef.current) return;
const relatedTarget = e.relatedTarget;
if (relatedTarget && !containerRef.current.contains(relatedTarget)) {
console.log('Dropdown lost focus, closing menu');
setIsFocused(false);
setIsDropdownOpen(false);
}
};
const setIsDropdownOpen = (isOpen: boolean) => {
setIsOpen(isOpen);
onToggle?.(isOpen);
@@ -86,28 +118,28 @@ const Dropdown: React.FC<DropdownProps> = ({
<div className='dropdown-container flex'>
{isOpen && <Overlay onDismiss={() => setIsDropdownOpen(false)} />}
<div
ref={containerRef}
role='menu'
tabIndex={0}
role='button'
aria-label={_('Menu')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
if (!isOpen) toggleDropdown();
} else if (e.key === 'Escape' && isOpen) {
toggleDropdown();
} else {
e.stopPropagation();
}
}}
className={clsx('dropdown', className)}
aria-label={label}
title={label}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onPointerDown={() => setIsPointerDown(true)}
onPointerUp={() => setIsPointerDown(false)}
className={clsx('dropdown flex flex-col', className)}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
role='none'
className={clsx('dropdown-toggle', buttonClassName, isFocused && 'bg-base-300/50')}
onClick={toggleDropdown}
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-300/50')}
>
{toggleButton}
</div>
{isOpen && childrenWithToggle}
<div role='none' className={clsx('flex items-center justify-center', !isOpen && 'hidden')}>
{isOpen && childrenWithToggle}
</div>
</div>
</div>
);