feat(a11y): always show page navigation buttons in reader page for screen readers, closes #3036 (#3167)

This commit is contained in:
Huang Xin
2026-02-04 17:07:37 +08:00
committed by GitHub
parent 9f261f12e9
commit 8dfab2c963
35 changed files with 242 additions and 38 deletions
@@ -1062,5 +1062,7 @@
"Manual": "يدوي",
"Group by...": "تجميع حسب...",
"Export as Plain Text": "تصدير كنص بسيط",
"Export as Markdown": "تصدير بصيغة Markdown"
"Export as Markdown": "تصدير بصيغة Markdown",
"Show Page Navigation Buttons": "أزرار التنقل",
"Page {{number}}": "صفحة {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "ম্যানুয়াল",
"Group by...": "গ্রুপ অনুযায়ী...",
"Export as Plain Text": "সাধারণ টেক্সট হিসেবে এক্সপোর্ট করুন",
"Export as Markdown": "Markdown হিসেবে এক্সপোর্ট করুন"
"Export as Markdown": "Markdown হিসেবে এক্সপোর্ট করুন",
"Show Page Navigation Buttons": "নেভিগেশন বোতাম",
"Page {{number}}": "পৃষ্ঠা {{number}}"
}
@@ -1002,5 +1002,7 @@
"Manual": "ལག་བཟོས།",
"Group by...": "དབྱེ་བ་འབྱེད་སྟངས།...",
"Export as Plain Text": "ཡིག་རྐྱང་དཀྱུས་མར་ཕྱིར་འདྲེན།",
"Export as Markdown": "Markdown དུ་ཕྱིར་འདྲེན།"
"Export as Markdown": "Markdown དུ་ཕྱིར་འདྲེན།",
"Show Page Navigation Buttons": "ཤོག་ངོས་མཐེབ་གནོན།",
"Page {{number}}": "ཤོག་ལྷེ། {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "Manuell",
"Group by...": "Gruppieren nach...",
"Export as Plain Text": "Als reinen Text exportieren",
"Export as Markdown": "Als Markdown exportieren"
"Export as Markdown": "Als Markdown exportieren",
"Show Page Navigation Buttons": "Navigationsschaltflächen",
"Page {{number}}": "Seite {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "Χειροκίνητα",
"Group by...": "Ομαδοποίηση κατά...",
"Export as Plain Text": "Αποστολή ως απλό κείμενο",
"Export as Markdown": "Αποστολή ως Markdown"
"Export as Markdown": "Αποστολή ως Markdown",
"Show Page Navigation Buttons": "Κουμπιά πλοήγησης",
"Page {{number}}": "Σελίδα {{number}}"
}
@@ -1026,5 +1026,7 @@
"Manual": "Manual",
"Group by...": "Agrupar por...",
"Export as Plain Text": "Exportar como texto sin formato",
"Export as Markdown": "Exportar como Markdown"
"Export as Markdown": "Exportar como Markdown",
"Show Page Navigation Buttons": "Botones de navegación",
"Page {{number}}": "Página {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "دستی",
"Group by...": "گروه‌بندی بر اساس...",
"Export as Plain Text": "خروجی به صورت متن ساده",
"Export as Markdown": "خروجی به صورت Markdown"
"Export as Markdown": "خروجی به صورت Markdown",
"Show Page Navigation Buttons": "دکمه‌های ناوبری",
"Page {{number}}": "صفحه {{number}}"
}
@@ -1026,5 +1026,7 @@
"Manual": "Manuel",
"Group by...": "Grouper par...",
"Export as Plain Text": "Exporter en texte brut",
"Export as Markdown": "Exporter en Markdown"
"Export as Markdown": "Exporter en Markdown",
"Show Page Navigation Buttons": "Boutons de navigation",
"Page {{number}}": "Page {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "मैनुअल",
"Group by...": "इसके द्वारा समूहबद्ध करें...",
"Export as Plain Text": "प्लेन टेक्स्ट के रूप में एक्सपोर्ट करें",
"Export as Markdown": "Markdown के रूप में एक्सपोर्ट करें"
"Export as Markdown": "Markdown के रूप में एक्सपोर्ट करें",
"Show Page Navigation Buttons": "नेविगेशन बटन",
"Page {{number}}": "पृष्ठ {{number}}"
}
@@ -1002,5 +1002,7 @@
"Manual": "Manual",
"Group by...": "Kelompokkan menurut...",
"Export as Plain Text": "Ekspor sebagai Teks Biasa",
"Export as Markdown": "Ekspor sebagai Markdown"
"Export as Markdown": "Ekspor sebagai Markdown",
"Show Page Navigation Buttons": "Tombol navigasi",
"Page {{number}}": "Halaman {{number}}"
}
@@ -1026,5 +1026,7 @@
"Manual": "Manuale",
"Group by...": "Raggruppa per...",
"Export as Plain Text": "Esporta come testo normale",
"Export as Markdown": "Esporta come Markdown"
"Export as Markdown": "Esporta come Markdown",
"Show Page Navigation Buttons": "Tasti navigazione",
"Page {{number}}": "Pagina {{number}}"
}
@@ -1002,5 +1002,7 @@
"Manual": "手動",
"Group by...": "グループ化...",
"Export as Plain Text": "プレーンテキストとして書き出し",
"Export as Markdown": "Markdownとして書き出し"
"Export as Markdown": "Markdownとして書き出し",
"Show Page Navigation Buttons": "ナビゲーションボタンを表示",
"Page {{number}}": "{{number}} ページ"
}
@@ -1002,5 +1002,7 @@
"Manual": "수동",
"Group by...": "그룹화 기준...",
"Export as Plain Text": "일반 텍스트로 내보내기",
"Export as Markdown": "Markdown으로 내보내기"
"Export as Markdown": "Markdown으로 내보내기",
"Show Page Navigation Buttons": "탐색 버튼 표시",
"Page {{number}}": "{{number}} 페이지"
}
@@ -1002,5 +1002,7 @@
"Manual": "Manual",
"Group by...": "Kumpulan mengikut...",
"Export as Plain Text": "Eksport sebagai Teks Biasa",
"Export as Markdown": "Eksport sebagai Markdown"
"Export as Markdown": "Eksport sebagai Markdown",
"Show Page Navigation Buttons": "Butang navigasi",
"Page {{number}}": "Halaman {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "Handmatig",
"Group by...": "Groeperen op...",
"Export as Plain Text": "Exporteren als platte tekst",
"Export as Markdown": "Exporteren als Markdown"
"Export as Markdown": "Exporteren als Markdown",
"Show Page Navigation Buttons": "Navigatieknoppen",
"Page {{number}}": "Pagina {{number}}"
}
@@ -1038,5 +1038,7 @@
"Manual": "Ręcznie",
"Group by...": "Grupuj według...",
"Export as Plain Text": "Eksportuj jako zwykły tekst",
"Export as Markdown": "Eksportuj jako Markdown"
"Export as Markdown": "Eksportuj jako Markdown",
"Show Page Navigation Buttons": "Przyciski nawigacji",
"Page {{number}}": "Strona {{number}}"
}
@@ -1026,5 +1026,7 @@
"Manual": "Manual",
"Group by...": "Agrupar por...",
"Export as Plain Text": "Exportar como texto simples",
"Export as Markdown": "Exportar como Markdown"
"Export as Markdown": "Exportar como Markdown",
"Show Page Navigation Buttons": "Botões de navegação",
"Page {{number}}": "Página {{number}}"
}
@@ -1038,5 +1038,7 @@
"Manual": "Вручную",
"Group by...": "Группировать по...",
"Export as Plain Text": "Экспортировать как обычный текст",
"Export as Markdown": "Экспортировать как Markdown"
"Export as Markdown": "Экспортировать как Markdown",
"Show Page Navigation Buttons": "Кнопки навигации",
"Page {{number}}": "Страница {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "අත්පොත",
"Group by...": "මගින් සමූහනය කරන්න...",
"Export as Plain Text": "සාමාන්‍ය පෙළ ලෙස අපනයනය කරන්න",
"Export as Markdown": "Markdown ලෙස අපනයනය කරන්න"
"Export as Markdown": "Markdown ලෙස අපනයනය කරන්න",
"Show Page Navigation Buttons": "සංචාලන බොත්තම්",
"Page {{number}}": "පිටුව {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "Manuellt",
"Group by...": "Gruppera efter...",
"Export as Plain Text": "Exportera som vanlig text",
"Export as Markdown": "Exportera som Markdown"
"Export as Markdown": "Exportera som Markdown",
"Show Page Navigation Buttons": "Navigeringsknappar",
"Page {{number}}": "Sida {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "கையேடு",
"Group by...": "இதன் படி குழுவாக்கு...",
"Export as Plain Text": "எளிய உரையாக ஏற்றுமதி செய்",
"Export as Markdown": "Markdown ஆக ஏற்றுமதி செய்"
"Export as Markdown": "Markdown ஆக ஏற்றுமதி செய்",
"Show Page Navigation Buttons": "வழிசெலுத்தல் பொத்தான்கள்",
"Page {{number}}": "பக்கம் {{number}}"
}
@@ -1002,5 +1002,7 @@
"Manual": "ทำเอง",
"Group by...": "จัดกลุ่มตาม...",
"Export as Plain Text": "ส่งออกเป็นข้อความธรรมดา",
"Export as Markdown": "ส่งออกเป็น Markdown"
"Export as Markdown": "ส่งออกเป็น Markdown",
"Show Page Navigation Buttons": "ปุ่มนำทาง",
"Page {{number}}": "หน้า {{number}}"
}
@@ -1014,5 +1014,7 @@
"Manual": "Manuel",
"Group by...": "Şuna göre grupla...",
"Export as Plain Text": "Düz Metin Olarak Dışa Aktar",
"Export as Markdown": "Markdown Olarak Dışa Aktar"
"Export as Markdown": "Markdown Olarak Dışa Aktar",
"Show Page Navigation Buttons": "Gezinme düğmeleri",
"Page {{number}}": "Sayfa {{number}}"
}
@@ -1038,5 +1038,7 @@
"Manual": "Вручну",
"Group by...": "Групувати за...",
"Export as Plain Text": "Експортувати як звичайний текст",
"Export as Markdown": "Експортувати як Markdown"
"Export as Markdown": "Експортувати як Markdown",
"Show Page Navigation Buttons": "Кнопки навігації",
"Page {{number}}": "Сторінка {{number}}"
}
@@ -1002,5 +1002,7 @@
"Manual": "Thủ công",
"Group by...": "Nhóm theo...",
"Export as Plain Text": "Xuất dưới dạng văn bản thuần túy",
"Export as Markdown": "Xuất dưới dạng Markdown"
"Export as Markdown": "Xuất dưới dạng Markdown",
"Show Page Navigation Buttons": "Nút điều hướng trang",
"Page {{number}}": "Trang {{number}}"
}
@@ -1002,5 +1002,7 @@
"Manual": "手动",
"Group by...": "分组方式...",
"Export as Plain Text": "导出为纯文本",
"Export as Markdown": "导出为 Markdown"
"Export as Markdown": "导出为 Markdown",
"Show Page Navigation Buttons": "显示翻页按钮",
"Page {{number}}": "第 {{number}} 页"
}
@@ -1002,5 +1002,7 @@
"Manual": "手動",
"Group by...": "分組方式...",
"Export as Plain Text": "匯出為純文字",
"Export as Markdown": "匯出為 Markdown"
"Export as Markdown": "匯出為 Markdown",
"Show Page Navigation Buttons": "顯示翻頁按鈕",
"Page {{number}}": "第 {{number}} 頁"
}
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
@@ -14,6 +14,7 @@ import BooknotesNav from './sidebar/BooknotesNav';
import FoliateViewer from './FoliateViewer';
import SectionInfo from './SectionInfo';
import HeaderBar from './HeaderBar';
import PageNavigationButtons from './PageNavigationButtons';
import FooterBar from './footerbar/FooterBar';
import ProgressInfoView from './ProgressInfo';
import Ribbon from './Ribbon';
@@ -35,6 +36,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { setGridInsets, hoveredBookKey } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
const [dropdownOpenBook, setDropdownOpenBook] = useState<string>('');
const { safeAreaInsets: screenInsets } = useThemeStore();
const aspectRatio = window.innerWidth / window.innerHeight;
@@ -90,7 +92,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { book, bookDoc } = bookData || {};
if (!book || !config || !bookDoc || !viewSettings || !viewState) return null;
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
const { section, pageinfo, sectionLabel } = progress || {};
const isBookmarked = viewState.ribbonVisible;
const viewerKey = viewState.viewerKey;
const horizontalGapPercent = viewSettings.gapPercent;
@@ -123,6 +125,11 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isHoveredAnim={bookKeys.length > 2}
onCloseBook={onCloseBook}
gridInsets={gridInsets}
onDropdownOpenChange={(isOpen) => setDropdownOpenBook(isOpen ? bookKey : '')}
/>
<PageNavigationButtons
bookKey={bookKey}
isDropdownOpen={dropdownOpenBook === bookKey}
/>
<FoliateViewer
key={viewerKey}
@@ -204,9 +211,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<ProgressInfoView
bookKey={bookKey}
toc={bookDoc.toc || []}
section={section}
pageinfo={pageinfo}
timeinfo={timeinfo}
horizontalGap={horizontalGapPercent}
contentInsets={contentInsets}
gridInsets={gridInsets}
@@ -34,6 +34,7 @@ interface HeaderBarProps {
isHoveredAnim: boolean;
gridInsets: Insets;
onCloseBook: (bookKey: string) => void;
onDropdownOpenChange?: (isOpen: boolean) => void;
}
const HeaderBar: React.FC<HeaderBarProps> = ({
@@ -43,6 +44,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
isHoveredAnim,
gridInsets,
onCloseBook,
onDropdownOpenChange,
}) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
@@ -77,6 +79,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
const handleToggleDropdown = (isOpen: boolean) => {
setIsDropdownOpen(isOpen);
onDropdownOpenChange?.(isOpen);
if (!isOpen) setHoveredBookKey('');
};
@@ -0,0 +1,115 @@
import clsx from 'clsx';
import React, { useCallback } from 'react';
import { IoChevronBack, IoChevronForward } from 'react-icons/io5';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { viewPagination } from '../hooks/usePagination';
import { useBookDataStore } from '@/store/bookDataStore';
interface PageNavigationButtonsProps {
bookKey: string;
isDropdownOpen: boolean;
}
const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
bookKey,
isDropdownOpen,
}) => {
const _ = useTranslation();
const { getBookData } = useBookDataStore();
const { getView, getProgress, getViewSettings, hoveredBookKey } = useReaderStore();
const bookData = getBookData(bookKey);
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey);
const progress = getProgress(bookKey);
const { section, pageinfo } = progress || {};
const pageInfo = bookData?.isFixedLayout ? section : pageinfo;
const currentPage = pageInfo?.current;
const isPageNavigationButtonsVisible =
(hoveredBookKey === bookKey || isDropdownOpen) && viewSettings?.showPaginationButtons;
const handleGoLeftPage = useCallback(() => {
viewPagination(view, viewSettings, 'left', 'page');
}, [view, viewSettings]);
const handleGoRightPage = useCallback(() => {
viewPagination(view, viewSettings, 'right', 'page');
}, [view, viewSettings]);
const getLeftPageLabel = () => {
const baseLabel = viewSettings?.rtl ? _('Next Page') : _('Previous Page');
if (currentPage !== undefined) {
return `${baseLabel}, ${_('Page {{number}}', { number: currentPage + 1 })}`;
}
return baseLabel;
};
const getRightPageLabel = () => {
const baseLabel = viewSettings?.rtl ? _('Previous Page') : _('Next Page');
if (currentPage !== undefined) {
return `${baseLabel}, ${_('Page {{number}}', { number: currentPage + 1 })}`;
}
return baseLabel;
};
return (
<>
{currentPage !== undefined && (
<div className='sr-only' role='status' aria-live='polite' aria-atomic='true'>
{_('Page {{number}}', { number: currentPage + 1 })}
</div>
)}
<button
onClick={handleGoLeftPage}
className={clsx(
'absolute left-2 top-1/2 z-10 -translate-y-1/2',
'flex h-20 w-20 items-center justify-center',
'transition-opacity duration-300',
'focus:opacity-100 focus:outline-none',
isPageNavigationButtonsVisible ? 'opacity-100' : 'opacity-0',
)}
aria-label={getLeftPageLabel()}
tabIndex={0}
>
<span
className={clsx(
'flex h-12 w-12 items-center justify-center rounded-full',
'bg-base-100/90 shadow-lg backdrop-blur-sm',
'eink:border eink:border-base-content not-eink:group-hover:bg-base-200',
'transition-transform active:scale-95',
)}
>
<IoChevronBack size={24} />
</span>
</button>
<button
onClick={handleGoRightPage}
className={clsx(
'absolute right-2 top-1/2 z-10 -translate-y-1/2',
'flex h-20 w-20 items-center justify-center',
'transition-opacity duration-300',
'focus:opacity-100 focus:outline-none',
isPageNavigationButtonsVisible ? 'opacity-100' : 'opacity-0',
)}
aria-label={getRightPageLabel()}
tabIndex={0}
>
<span
className={clsx(
'flex h-12 w-12 items-center justify-center rounded-full',
'bg-base-100/90 shadow-lg backdrop-blur-sm',
'eink:border eink:border-base-content not-eink:group-hover:bg-base-200',
'transition-transform active:scale-95',
)}
>
<IoChevronForward size={24} />
</span>
</button>
</>
);
};
export default PageNavigationButtons;
@@ -1,7 +1,6 @@
import clsx from 'clsx';
import React, { useEffect, useMemo, useState } from 'react';
import { Insets } from '@/types/misc';
import { PageInfo, TimeInfo } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -14,9 +13,6 @@ import { SIZE_PER_LOC, SIZE_PER_TIME_UNIT } from '@/services/constants';
interface PageInfoProps {
bookKey: string;
toc: TOCItem[];
section?: PageInfo;
pageinfo?: PageInfo;
timeinfo?: TimeInfo;
horizontalGap: number;
contentInsets: Insets;
gridInsets: Insets;
@@ -25,8 +21,6 @@ interface PageInfoProps {
const ProgressInfoView: React.FC<PageInfoProps> = ({
bookKey,
toc,
section,
pageinfo,
horizontalGap,
contentInsets,
gridInsets,
@@ -38,6 +32,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
const bookData = getBookData(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const progress = getProgress(bookKey);
const { section, pageinfo } = progress || {};
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
const isScrolled = viewSettings.scrolled;
@@ -30,6 +30,9 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
const [isContinuousScroll, setIsContinuousScroll] = useState(viewSettings.continuousScroll);
const [scrollingOverlap, setScrollingOverlap] = useState(viewSettings.scrollingOverlap);
const [volumeKeysToFlip, setVolumeKeysToFlip] = useState(viewSettings.volumeKeysToFlip);
const [showPaginationButtons, setShowPaginationButtons] = useState(
viewSettings.showPaginationButtons,
);
const [isDisableClick, setIsDisableClick] = useState(viewSettings.disableClick);
const [fullscreenClickArea, setFullscreenClickArea] = useState(viewSettings.fullscreenClickArea);
const [swapClickArea, setSwapClickArea] = useState(viewSettings.swapClickArea);
@@ -55,6 +58,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
continuousScroll: setIsContinuousScroll,
scrollingOverlap: setScrollingOverlap,
volumeKeysToFlip: setVolumeKeysToFlip,
showPaginationButtons: setShowPaginationButtons,
disableClick: setIsDisableClick,
swapClickArea: setSwapClickArea,
animated: setAnimated,
@@ -107,6 +111,18 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [volumeKeysToFlip]);
useEffect(() => {
saveViewSettings(
envConfig,
bookKey,
'showPaginationButtons',
showPaginationButtons,
false,
false,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showPaginationButtons]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'disableClick', isDisableClick, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -304,6 +320,15 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
/>
</div>
)}
<div className='config-item' data-setting-id='settings.control.showPaginationButtons'>
<span className=''>{_('Show Page Navigation Buttons')}</span>
<input
type='checkbox'
className='toggle'
checked={showPaginationButtons}
onChange={() => setShowPaginationButtons(!showPaginationButtons)}
/>
</div>
</div>
</div>
</div>
@@ -428,6 +428,12 @@ const controlPanelItems = [
keywords: ['disable', 'double', 'click', 'tap'],
section: 'Pagination',
},
{
id: 'settings.control.showPaginationButtons',
labelKey: _('Show Page Navigation Buttons'),
keywords: ['show', 'pagination', 'buttons', 'navigation', 'arrows', 'chevron', 'page', 'turn'],
section: 'Pagination',
},
{
id: 'settings.control.enableQuickActions',
labelKey: _('Enable Quick Actions'),
@@ -255,6 +255,7 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = {
showProgressInfo: true,
tapToToggleFooter: false,
showMarginsOnScroll: false,
showPaginationButtons: false,
progressStyle: 'fraction',
progressInfoMode: 'all',
+1
View File
@@ -204,6 +204,7 @@ export interface ViewConfig {
tapToToggleFooter: boolean;
showBarsOnScroll: boolean;
showMarginsOnScroll: boolean;
showPaginationButtons: boolean;
progressStyle: 'percentage' | 'fraction';
progressInfoMode: 'remaining' | 'progress' | 'all' | 'none';