forked from akai/readest
refactor(a11y): add basic lint for accessibility (#2021)
This commit is contained in:
@@ -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' ? (
|
||||
|
||||
Reference in New Issue
Block a user