refactor(a11y): add basic lint for accessibility (#2021)

This commit is contained in:
Huang Xin
2025-09-12 00:57:04 +08:00
committed by GitHub
parent 2571ceede4
commit 9fd152d727
56 changed files with 511 additions and 314 deletions
@@ -41,8 +41,9 @@ const BookCard = ({ book }: { book: Book }) => {
<button
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
aria-label={_('More Info')}
onClick={showBookDetails}
>
<MdInfoOutline size={iconSize18} className='fill-base-content' onClick={showBookDetails} />
<MdInfoOutline size={iconSize18} className='fill-base-content' />
</button>
</div>
);
@@ -85,11 +85,8 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
setIsDropdownOpen?.(false);
};
const isWebApp = isWebAppPlatform();
return (
<div
tabIndex={0}
className={clsx('book-menu dropdown-content border-base-100 z-20 shadow-2xl', menuClassName)}
>
<MenuItem
@@ -149,7 +146,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
/>
<MenuItem label={_('Reload Page')} shortcut='Shift+R' onClick={handleReloadPage} />
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
</div>
);
@@ -38,7 +38,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
const progress = getProgress(bookKey);
const { isCurrent, viewRef } = useScrollToItem(cfi, progress);
const handleClickItem = (event: React.MouseEvent) => {
const handleClickItem = (event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
eventDispatcher.dispatch('navigate', { bookKey, cfi });
@@ -108,7 +108,6 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
onChange={(value) => (editorDraftRef.current = value)}
onSave={handleSaveBookmark}
onEscape={() => setInlineEditMode(false)}
autoFocus={true}
spellCheck={false}
/>
</div>
@@ -124,14 +123,25 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
return (
<li
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role='button'
ref={viewRef}
className={clsx(
'border-base-300 content group relative my-2 cursor-pointer rounded-lg p-2',
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/55 bg-base-100',
isCurrent
? 'bg-base-300/85 hover:bg-base-300 focus:bg-base-300'
: 'hover:bg-base-300/55 focus:bg-base-300/55 bg-base-100',
'transition-all duration-300 ease-in-out',
)}
tabIndex={0}
onClick={handleClickItem}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClickItem(e);
} else {
e.stopPropagation();
}
}}
>
<div
className={clsx('min-h-4 p-0 transition-all duration-300 ease-in-out')}
@@ -175,17 +185,20 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
</div>
</div>
</div>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={clsx(
'max-h-0 overflow-hidden p-0',
'transition-[max-height] duration-300 ease-in-out',
'group-hover:max-h-8 group-hover:overflow-visible',
'group-focus-within:max-h-8 group-focus-within:overflow-visible',
)}
style={
{
'--bottom-override': 0,
} as React.CSSProperties
}
// This is needed to prevent the parent onClick from being triggered
onClick={(e) => e.stopPropagation()}
>
<div className='flex cursor-default items-center justify-between'>
@@ -199,7 +212,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
<TextButton
onClick={item.type === 'bookmark' ? editBookmark : editNote.bind(null, item)}
variant='primary'
className='opacity-0 transition duration-300 ease-in-out group-hover:opacity-100'
className='opacity-0 transition duration-300 ease-in-out group-focus-within:opacity-100 group-hover:opacity-100'
>
{_('Edit')}
</TextButton>
@@ -208,7 +221,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
<TextButton
onClick={deleteNote.bind(null, item)}
variant='danger'
className='opacity-0 transition duration-300 ease-in-out group-hover:opacity-100'
className='opacity-0 transition duration-300 ease-in-out group-focus-within:opacity-100 group-hover:opacity-100'
>
{_('Delete')}
</TextButton>
@@ -46,7 +46,6 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
return (
<div
tabIndex={0}
className={clsx(
'book-menu dropdown-content dropdown-center border-base-200 z-20 w-56 border shadow-2xl',
menuClassName,
@@ -23,12 +23,22 @@ const SearchResultItem: React.FC<SearchResultItemProps> = ({
return (
<li
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role='button'
ref={viewRef}
className={clsx(
'my-2 cursor-pointer rounded-lg p-2 text-sm',
isCurrent ? 'bg-base-300 hover:bg-gray-300/70' : 'hover:bg-base-300 bg-base-100',
)}
tabIndex={0}
onClick={() => onSelectResult(cfi)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onSelectResult(cfi);
} else {
e.stopPropagation();
}
}}
>
<div className='line-clamp-3'>
<span className=''>{excerpt.pre}</span>
@@ -1,24 +1,26 @@
import clsx from 'clsx';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { impactFeedback } from '@tauri-apps/plugin-haptics';
import { useSettingsStore } from '@/store/settingsStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { BookSearchResult } from '@/types/book';
import { eventDispatcher } from '@/utils/event';
import { getBookDirFromLanguage } from '@/utils/book';
import { useEnv } from '@/context/EnvContext';
import { useDrag } from '@/hooks/useDrag';
import { DragKey, useDrag } from '@/hooks/useDrag';
import { useThemeStore } from '@/store/themeStore';
import { Overlay } from '@/components/Overlay';
import useShortcuts from '@/hooks/useShortcuts';
import SidebarHeader from './Header';
import SidebarContent from './Content';
import BookCard from './BookCard';
import useSidebar from '../../hooks/useSidebar';
import SearchBar from './SearchBar';
import SearchResults from './SearchResults';
import useShortcuts from '@/hooks/useShortcuts';
const MIN_SIDEBAR_WIDTH = 0.05;
const MAX_SIDEBAR_WIDTH = 0.45;
@@ -28,6 +30,7 @@ const VELOCITY_THRESHOLD = 0.5;
const SideBar: React.FC<{
onGoToLibrary: () => void;
}> = ({ onGoToLibrary }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { updateAppTheme, safeAreaInsets } = useThemeStore();
const { settings } = useSettingsStore();
@@ -37,11 +40,13 @@ const SideBar: React.FC<{
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const sidebarHeight = useRef(1.0);
const isMobile = window.innerWidth < 640;
const {
sideBarWidth,
isSideBarPinned,
isSideBarVisible,
getSideBarWidth,
setSideBarVisible,
handleSideBarResize,
handleSideBarTogglePin,
@@ -89,6 +94,7 @@ const SideBar: React.FC<{
const heightFraction = data.clientY / window.innerHeight;
const newTop = Math.max(0.0, Math.min(1, heightFraction));
sidebarHeight.current = newTop;
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
const overlay = document.querySelector('.overlay') as HTMLElement;
@@ -135,11 +141,29 @@ const SideBar: React.FC<{
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
};
const handleHorizontalDragKeyDown = (data: { key: DragKey; step: number }) => {
const currentWidth = parseFloat(getSideBarWidth()) / 100;
let newWidth = currentWidth;
if (data.key === 'ArrowLeft') {
newWidth = Math.max(MIN_SIDEBAR_WIDTH, currentWidth - data.step);
} else if (data.key === 'ArrowRight') {
newWidth = Math.min(MAX_SIDEBAR_WIDTH, currentWidth + data.step);
}
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
};
const handleVerticalDragKeyDown = () => {};
const { handleDragStart: handleVerticalDragStart } = useDrag(
handleVerticalDragMove,
handleVerticalDragKeyDown,
handleVerticalDragEnd,
);
const { handleDragStart: handleHorizontalDragStart } = useDrag(handleHorizontalDragMove);
const { handleDragStart: handleHorizontalDragStart, handleDragKeyDown } = useDrag(
handleHorizontalDragMove,
handleHorizontalDragKeyDown,
);
const handleClickOverlay = () => {
setSideBarVisible(false);
@@ -182,10 +206,7 @@ const SideBar: React.FC<{
return isSideBarVisible ? (
<>
{!isSideBarPinned && (
<div
className='overlay fixed inset-0 z-[45] bg-black/50 sm:bg-black/20'
onClick={handleClickOverlay}
/>
<Overlay className='z-[45] bg-black/50 sm:bg-black/20' onDismiss={handleClickOverlay} />
)}
<div
className={clsx(
@@ -222,6 +243,11 @@ const SideBar: React.FC<{
<div className='flex-shrink-0'>
{isMobile && (
<div
role='slider'
tabIndex={0}
aria-label={_('Resize Sidebar')}
aria-orientation='vertical'
aria-valuenow={sidebarHeight.current}
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
onMouseDown={handleVerticalDragStart}
onTouchStart={handleVerticalDragStart}
@@ -265,11 +291,17 @@ const SideBar: React.FC<{
)}
<div
className={clsx(
'drag-bar absolute right-0 top-0 -m-2 h-full w-0.5 cursor-col-resize bg-transparent p-2',
'drag-bar absolute -right-2 top-0 h-full w-0.5 cursor-col-resize bg-transparent p-2',
isMobile && 'hidden',
)}
role='slider'
tabIndex={0}
aria-label={_('Resize Sidebar')}
aria-orientation='horizontal'
aria-valuenow={parseFloat(sideBarWidth)}
onMouseDown={handleHorizontalDragStart}
onTouchStart={handleHorizontalDragStart}
onKeyDown={handleDragKeyDown}
></div>
</div>
</>
@@ -49,7 +49,7 @@ const TOCItemView = React.memo<{
);
const handleClickItem = useCallback(
(event: React.MouseEvent) => {
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
onItemClick(item);
},
@@ -58,9 +58,10 @@ const TOCItemView = React.memo<{
return (
<div
tabIndex={0}
role='treeitem'
tabIndex={-1}
onClick={item.href ? handleClickItem : undefined}
onKeyDown={item.href ? (e) => e.key === 'Enter' && handleClickItem(e) : undefined}
aria-expanded={flatItem.isExpanded ? 'true' : 'false'}
aria-selected={isActive ? 'true' : 'false'}
data-href={item.href ? getContentMd5(item.href) : undefined}
@@ -76,8 +77,11 @@ const TOCItemView = React.memo<{
}}
>
{item.subitems && (
<div
<button
onClick={handleToggleExpand}
onKeyDown={(e) => {
e.stopPropagation();
}}
className='inline-block cursor-pointer'
style={{
padding: '12px',
@@ -85,7 +89,7 @@ const TOCItemView = React.memo<{
}}
>
{createExpanderIcon(flatItem.isExpanded || false)}
</div>
</button>
)}
<div
className='ms-2 truncate text-ellipsis'
@@ -36,12 +36,22 @@ const TabNavigation: React.FC<{
{tabs.map((tab) => (
<div
key={tab}
className='lg:tooltip lg:tooltip-top z-20 m-1.5 flex-1 cursor-pointer rounded-md p-2'
data-tip={
tabIndex={0}
role='button'
className='z-20 m-1.5 flex-1 cursor-pointer rounded-md p-2'
onClick={() => onTabChange(tab)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onTabChange(tab);
}
}}
title={tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark')}
aria-label={
tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark')
}
>
<div className={clsx('flex h-6 items-center')} onClick={() => onTabChange(tab)}>
<div className={clsx('m-0 flex h-6 items-center p-0')}>
{tab === 'toc' ? (
<TOCIcon className='mx-auto' />
) : tab === 'annotations' ? (