forked from akai/readest
refactor(a11y): add basic lint for accessibility (#2021)
This commit is contained in:
@@ -85,67 +85,71 @@ export const AboutWindow = () => {
|
||||
onClose={handleClose}
|
||||
boxClassName='sm:!w-96 sm:h-auto'
|
||||
>
|
||||
<div className='about-content flex h-full flex-col items-center justify-center'>
|
||||
<div className='flex flex-col items-center gap-2 px-8'>
|
||||
<div className='mb-2 mt-8'>
|
||||
<Image src='/icon.png' alt='App Logo' className='h-20 w-20' width={64} height={64} />
|
||||
{isOpen && (
|
||||
<div className='about-content flex h-full flex-col items-center justify-center'>
|
||||
<div className='flex flex-col items-center gap-2 px-8'>
|
||||
<div className='mb-2 mt-8'>
|
||||
<Image src='/icon.png' alt='App Logo' className='h-20 w-20' width={64} height={64} />
|
||||
</div>
|
||||
<div className='flex select-text flex-col items-center'>
|
||||
<h2 className='mb-2 text-2xl font-bold'>Readest</h2>
|
||||
<p className='text-neutral-content text-center text-sm'>
|
||||
{_('Version {{version}}', { version: getAppVersion() })} {`(${browserInfo})`}
|
||||
</p>
|
||||
</div>
|
||||
<div className='h-5'>
|
||||
{!updateStatus && (
|
||||
<button
|
||||
className='badge badge-primary cursor-pointer p-2'
|
||||
onClick={appService?.hasUpdater ? handleCheckUpdate : handleShowRecentUpdates}
|
||||
>
|
||||
{_('Check Update')}
|
||||
</button>
|
||||
)}
|
||||
{updateStatus === 'updated' && (
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
{_('Already the latest version')}
|
||||
</p>
|
||||
)}
|
||||
{updateStatus === 'checking' && (
|
||||
<p className='text-neutral-content mt-2 text-xs'>{_('Checking for updates...')}</p>
|
||||
)}
|
||||
{updateStatus === 'error' && (
|
||||
<p className='text-error mt-2 text-xs'>{_('Error checking for updates')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex select-text flex-col items-center'>
|
||||
<h2 className='mb-2 text-2xl font-bold'>Readest</h2>
|
||||
<p className='text-neutral-content text-center text-sm'>
|
||||
{_('Version {{version}}', { version: getAppVersion() })} {`(${browserInfo})`}
|
||||
|
||||
<div className='divider py-16 sm:py-2'></div>
|
||||
|
||||
<div className='flex flex-col items-center px-4 text-center' dir='ltr'>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
<div className='h-5'>
|
||||
{!updateStatus && (
|
||||
<span
|
||||
className='badge badge-primary cursor-pointer p-2'
|
||||
onClick={appService?.hasUpdater ? handleCheckUpdate : handleShowRecentUpdates}
|
||||
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
This software is licensed under the{' '}
|
||||
<Link
|
||||
href='https://www.gnu.org/licenses/agpl-3.0.html'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
{_('Check Update')}
|
||||
</span>
|
||||
)}
|
||||
{updateStatus === 'updated' && (
|
||||
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
|
||||
)}
|
||||
{updateStatus === 'checking' && (
|
||||
<p className='text-neutral-content mt-2 text-xs'>{_('Checking for updates...')}</p>
|
||||
)}
|
||||
{updateStatus === 'error' && (
|
||||
<p className='text-error mt-2 text-xs'>{_('Error checking for updates')}</p>
|
||||
)}
|
||||
GNU Affero General Public License v3.0
|
||||
</Link>
|
||||
. You are free to use, modify, and distribute this software under the terms of the
|
||||
AGPL v3 license. Please see the license for more details.
|
||||
</p>
|
||||
<p className='text-neutral-content my-2 text-xs'>
|
||||
Source code is available at{' '}
|
||||
<Link href='https://github.com/readest/readest' className='text-blue-500 underline'>
|
||||
GitHub
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<LegalLinks />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='divider py-16 sm:py-2'></div>
|
||||
|
||||
<div className='flex flex-col items-center px-4 text-center' dir='ltr'>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
|
||||
</p>
|
||||
|
||||
<p className='text-neutral-content mt-2 text-xs'>
|
||||
This software is licensed under the{' '}
|
||||
<Link
|
||||
href='https://www.gnu.org/licenses/agpl-3.0.html'
|
||||
className='text-blue-500 underline'
|
||||
>
|
||||
GNU Affero General Public License v3.0
|
||||
</Link>
|
||||
. You are free to use, modify, and distribute this software under the terms of the AGPL
|
||||
v3 license. Please see the license for more details.
|
||||
</p>
|
||||
<p className='text-neutral-content my-2 text-xs'>
|
||||
Source code is available at{' '}
|
||||
<Link href='https://github.com/readest/readest' className='text-blue-500 underline'>
|
||||
GitHub
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<LegalLinks />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { Overlay } from './Overlay';
|
||||
|
||||
const VELOCITY_THRESHOLD = 0.5;
|
||||
const SNAP_THRESHOLD = 0.2;
|
||||
@@ -146,7 +147,9 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { handleDragStart } = useDrag(handleDragMove, handleDragEnd);
|
||||
const handleDragKeyDown = () => {};
|
||||
|
||||
const { handleDragStart } = useDrag(handleDragMove, handleDragKeyDown, handleDragEnd);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
@@ -159,13 +162,13 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
)}
|
||||
dir={isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
<div
|
||||
<Overlay
|
||||
className={clsx(
|
||||
'overlay fixed inset-0 z-10 bg-black/50 sm:bg-black/50',
|
||||
'z-10 bg-black/50 sm:bg-black/50',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
bgClassName,
|
||||
)}
|
||||
onClick={onClose}
|
||||
onDismiss={onClose}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -184,6 +187,7 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
...(isMobile ? { height: snapHeight ? `${snapHeight * 100}%` : '100%', bottom: 0 } : {}),
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={clsx(
|
||||
'drag-handle h-10 max-h-10 min-h-10 w-full cursor-row-resize items-center justify-center',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, isValidElement, ReactElement, ReactNode } from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { Overlay } from './Overlay';
|
||||
import MenuItem from './MenuItem';
|
||||
|
||||
interface DropdownProps {
|
||||
@@ -57,12 +59,12 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
children,
|
||||
onToggle,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
const newIsOpen = !isOpen;
|
||||
setIsOpen(newIsOpen);
|
||||
onToggle?.(newIsOpen);
|
||||
setIsDropdownOpen(newIsOpen);
|
||||
};
|
||||
|
||||
const setIsDropdownOpen = (isOpen: boolean) => {
|
||||
@@ -82,12 +84,24 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
|
||||
return (
|
||||
<div className='dropdown-container flex'>
|
||||
{isOpen && (
|
||||
<div className='fixed inset-0 bg-transparent' onClick={() => setIsDropdownOpen(false)} />
|
||||
)}
|
||||
<div className={clsx('dropdown', className)}>
|
||||
{isOpen && <Overlay onDismiss={() => setIsDropdownOpen(false)} />}
|
||||
<div
|
||||
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)}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onClick={toggleDropdown}
|
||||
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-300/50')}
|
||||
>
|
||||
|
||||
@@ -37,6 +37,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
const iconSize = useResponsiveSize(16);
|
||||
const menuButton = (
|
||||
<button
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content flex w-full flex-col items-center justify-center rounded-md p-1 py-[10px]',
|
||||
disabled && 'btn-disabled text-gray-400',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
interface OverlayProps {
|
||||
onDismiss: () => void;
|
||||
dismissLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Overlay: React.FC<OverlayProps> = ({ onDismiss, dismissLabel, className }) => {
|
||||
const _ = useTranslation();
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onDismiss();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('overlay fixed inset-0 cursor-default', className)}
|
||||
role='button'
|
||||
tabIndex={-1}
|
||||
aria-label={dismissLabel || _('Dismiss')}
|
||||
onClick={onDismiss}
|
||||
onContextMenu={onDismiss}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -6,9 +6,11 @@ import { AuthProvider } from '@/context/AuthContext';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { CSPostHogProvider } from '@/context/PHContext';
|
||||
import { SyncProvider } from '@/context/SyncContext';
|
||||
import { initSystemThemeListener } from '@/store/themeStore';
|
||||
import { initSystemThemeListener, loadDataTheme } from '@/store/themeStore';
|
||||
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
|
||||
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
import i18n from '@/i18n/i18n';
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
const { appService } = useEnv();
|
||||
@@ -16,6 +18,20 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
useSafeAreaInsets(); // Initialize safe area insets
|
||||
|
||||
useEffect(() => {
|
||||
const handlerLanguageChanged = (lng: string) => {
|
||||
document.documentElement.lang = lng;
|
||||
};
|
||||
|
||||
const locale = getLocale();
|
||||
handlerLanguageChanged(locale);
|
||||
i18n.on('languageChanged', handlerLanguageChanged);
|
||||
return () => {
|
||||
i18n.off('languageChanged', handlerLanguageChanged);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadDataTheme();
|
||||
if (appService) {
|
||||
initSystemThemeListener(appService);
|
||||
}
|
||||
|
||||
@@ -47,9 +47,9 @@ const Quota: React.FC<QuotaProps> = ({ quotas, showProgress, className, labelCla
|
||||
labelClassName,
|
||||
)}
|
||||
>
|
||||
<div className='lg:tooltip lg:tooltip-right' data-tip={quota.tooltip}>
|
||||
<span className='truncate'>{quota.name}</span>
|
||||
</div>
|
||||
<span className='truncate' title={quota.tooltip}>
|
||||
{quota.name}
|
||||
</span>
|
||||
<div className='text-right text-sm'>
|
||||
{quota.used} / {quota.total} {quota.unit}
|
||||
</div>
|
||||
|
||||
@@ -25,11 +25,8 @@ export default function Select({
|
||||
<select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={clsx(
|
||||
'select h-8 min-h-8 rounded-md border-none text-sm',
|
||||
'bg-base-200 focus:outline-none focus:ring-0',
|
||||
className,
|
||||
)}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
className={clsx('select bg-base-200 h-8 min-h-8 rounded-md border-none text-sm', className)}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
textAlignLast: 'end',
|
||||
|
||||
@@ -171,7 +171,7 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
<div className='bg-base-100 relative w-full rounded-lg'>
|
||||
<div className='mb-6 flex items-start gap-4'>
|
||||
<div className='flex w-[30%] max-w-32 flex-col gap-2'>
|
||||
<div
|
||||
<button
|
||||
className='aspect-[28/41] h-full shadow-md'
|
||||
onClick={!isCoverLocked ? handleSelectLocalImage : undefined}
|
||||
>
|
||||
@@ -186,7 +186,7 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
...(newCoverImageUrl ? { coverImageUrl: newCoverImageUrl } : {}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div className='flex w-full gap-1'>
|
||||
<button
|
||||
onClick={handleSelectLocalImage}
|
||||
|
||||
@@ -232,12 +232,14 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
</Dialog>
|
||||
|
||||
{/* Source Selection Modal */}
|
||||
<SourceSelector
|
||||
sources={availableSources}
|
||||
isOpen={showSourceSelection}
|
||||
onSelect={handleSourceSelection}
|
||||
onClose={handleCloseSourceSelection}
|
||||
/>
|
||||
{showSourceSelection && (
|
||||
<SourceSelector
|
||||
sources={availableSources}
|
||||
isOpen={showSourceSelection}
|
||||
onSelect={handleSourceSelection}
|
||||
onClose={handleCloseSourceSelection}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDeleteAction && currentDeleteConfig && (
|
||||
<div
|
||||
|
||||
@@ -75,10 +75,9 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
|
||||
toggleButton={<MdOutlineDelete className='fill-red-500' />}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
className={clsx(
|
||||
'delete-menu dropdown-content dropdown-center no-triangle',
|
||||
'border-base-200 z-20 mt-1 max-w-[90vw] shadow-2xl',
|
||||
'border-base-300 !bg-base-200 z-20 mt-1 max-w-[90vw] shadow-2xl',
|
||||
)}
|
||||
>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { MdOutlineCheck, MdOutlineEdit } from 'react-icons/md';
|
||||
|
||||
import { BookMetadata } from '@/libs/document';
|
||||
@@ -24,6 +24,13 @@ interface SourceSelectorProps {
|
||||
|
||||
const SourceSelector: React.FC<SourceSelectorProps> = ({ sources, isOpen, onSelect, onClose }) => {
|
||||
const _ = useTranslation();
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && modalRef.current) {
|
||||
modalRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const getConfidenceIcon = (confidence: number) => {
|
||||
if (confidence >= 90) return <MdOutlineCheck className='text-green-500' />;
|
||||
@@ -34,16 +41,23 @@ const SourceSelector: React.FC<SourceSelectorProps> = ({ sources, isOpen, onSele
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black/50'>
|
||||
<div className='bg-base-100 mx-4 max-h-[80vh] w-full max-w-md overflow-y-auto rounded-lg p-6'>
|
||||
<div className='source-selector fixed inset-0 z-[60] flex items-center justify-center bg-black/50'>
|
||||
<div
|
||||
ref={modalRef}
|
||||
tabIndex={-1}
|
||||
role='dialog'
|
||||
aria-modal='true'
|
||||
className='bg-base-100 mx-4 max-h-[80vh] w-full max-w-md overflow-y-auto rounded-lg p-6'
|
||||
>
|
||||
<h3 className='mb-4 text-lg font-semibold'>{_('Select Metadata Source')}</h3>
|
||||
|
||||
<div className='space-y-3'>
|
||||
{sources.map((source, index) => (
|
||||
<div
|
||||
<button
|
||||
tabIndex={0}
|
||||
key={index}
|
||||
onClick={() => onSelect(source)}
|
||||
className='hover:bg-base-200 cursor-pointer rounded-md border p-3 transition-colors'
|
||||
className='hover:bg-base-300/75 bg-base-200 border-base-200 cursor-pointer rounded-md border p-3 transition-colors'
|
||||
>
|
||||
<div className='flex items-start gap-4'>
|
||||
<div className='aspect-[28/41] h-full w-[40%] max-w-32 shadow-md'>
|
||||
@@ -86,18 +100,19 @@ const SourceSelector: React.FC<SourceSelectorProps> = ({ sources, isOpen, onSele
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div
|
||||
<button
|
||||
tabIndex={0}
|
||||
onClick={onClose}
|
||||
className='hover:bg-base-200 cursor-pointer rounded-md border p-3 transition-colors'
|
||||
className='hover:bg-base-300/75 border-base-200 bg-base-200 cursor-pointer rounded-md border p-3 transition-colors'
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<MdOutlineEdit className='h-4 w-4' />
|
||||
<span className='font-medium'>{_('Keep manual input')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-end gap-2'>
|
||||
|
||||
Reference in New Issue
Block a user