diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index b1db5ae7..11494fa3 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -14,6 +14,8 @@ "build-web:vinext": "dotenv -e .env.web -- vinext build", "start-web:vinext": "dotenv -e .env.web -- vinext start", "build-tauri": "dotenv -e .env.tauri -- next build", + "dev-android": "tauri android build -t aarch64 -- --features devtools && adb install -r src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk", + "dev-ios": "tauri ios build -- --features devtools && ideviceinstaller -i src-tauri/gen/apple/build/arm64/Readest.ipa", "i18n:extract": "i18next-scanner --config i18next-scanner.config.cjs", "lint": "tsgo --noEmit && biome check .", "test": "dotenv -e .env -e .env.test.local -- vitest", diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 4ed008b7..6e9fc690 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1243,5 +1243,6 @@ "Remove": "إزالة", "Edit OPDS Catalog": "تعديل كتالوج OPDS", "Save Changes": "حفظ التغييرات", - "Use Book Layout": "استخدام تخطيط الكتاب" + "Use Book Layout": "استخدام تخطيط الكتاب", + "End of this section. Continue to the next.": "نهاية هذا القسم. تابع إلى القسم التالي." } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 6d02a895..09f57365 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1191,5 +1191,6 @@ "Remove": "সরান", "Edit OPDS Catalog": "OPDS ক্যাটালগ সম্পাদনা", "Save Changes": "পরিবর্তন সংরক্ষণ", - "Use Book Layout": "বইয়ের লেআউট ব্যবহার করুন" + "Use Book Layout": "বইয়ের লেআউট ব্যবহার করুন", + "End of this section. Continue to the next.": "এই বিভাগের শেষ। পরবর্তী বিভাগে চালিয়ে যান।" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index a06cb59d..91d8e9c8 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1178,5 +1178,6 @@ "Remove": "བསུབ།", "Edit OPDS Catalog": "OPDS དཀར་ཆག་རྩོམ་སྒྲིག", "Save Changes": "བསྒྱུར་བཅོས་ཉར་ཚགས།", - "Use Book Layout": "དཔེ་ཆའི་བཀོད་སྒྲིག་སྤྱོད་པ" + "Use Book Layout": "དཔེ་ཆའི་བཀོད་སྒྲིག་སྤྱོད་པ", + "End of this section. Continue to the next.": "སྡེ་ཚན་འདིའི་མཇུག་རེད། རྗེས་མའི་སྡེ་ཚན་དུ་མུ་མཐུད་རོགས།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 3fe803f7..862e9f89 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1191,5 +1191,6 @@ "Remove": "Entfernen", "Edit OPDS Catalog": "OPDS-Katalog bearbeiten", "Save Changes": "Änderungen speichern", - "Use Book Layout": "Buchlayout verwenden" + "Use Book Layout": "Buchlayout verwenden", + "End of this section. Continue to the next.": "Ende dieses Abschnitts. Weiter zum nächsten." } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index b24757f6..7c6bff7f 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1191,5 +1191,6 @@ "Remove": "Αφαίρεση", "Edit OPDS Catalog": "Επεξεργασία καταλόγου OPDS", "Save Changes": "Αποθήκευση αλλαγών", - "Use Book Layout": "Χρήση διάταξης βιβλίου" + "Use Book Layout": "Χρήση διάταξης βιβλίου", + "End of this section. Continue to the next.": "Τέλος αυτής της ενότητας. Συνεχίστε στην επόμενη." } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index ce9597fb..07570191 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1204,5 +1204,6 @@ "Remove": "Eliminar", "Edit OPDS Catalog": "Editar catálogo OPDS", "Save Changes": "Guardar cambios", - "Use Book Layout": "Usar el diseño del libro" + "Use Book Layout": "Usar el diseño del libro", + "End of this section. Continue to the next.": "Fin de esta sección. Continuar a la siguiente." } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 57a09d5e..bbcbc7d2 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1191,5 +1191,6 @@ "Remove": "حذف", "Edit OPDS Catalog": "ویرایش کاتالوگ OPDS", "Save Changes": "ذخیره تغییرات", - "Use Book Layout": "استفاده از چیدمان کتاب" + "Use Book Layout": "استفاده از چیدمان کتاب", + "End of this section. Continue to the next.": "پایان این بخش. به بخش بعدی ادامه دهید." } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 185aaf77..df46d260 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1204,5 +1204,6 @@ "Remove": "Supprimer", "Edit OPDS Catalog": "Modifier le catalogue OPDS", "Save Changes": "Enregistrer les modifications", - "Use Book Layout": "Utiliser la mise en page du livre" + "Use Book Layout": "Utiliser la mise en page du livre", + "End of this section. Continue to the next.": "Fin de cette section. Continuer à la suivante." } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 7c771f85..54a0ecae 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1204,5 +1204,6 @@ "Remove": "הסר", "Edit OPDS Catalog": "עריכת קטלוג OPDS", "Save Changes": "שמור שינויים", - "Use Book Layout": "השתמש בפריסת הספר" + "Use Book Layout": "השתמש בפריסת הספר", + "End of this section. Continue to the next.": "סוף הסעיף הזה. המשך לסעיף הבא." } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 137d3db9..9f723244 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1191,5 +1191,6 @@ "Remove": "हटाएँ", "Edit OPDS Catalog": "OPDS कैटलॉग संपादित करें", "Save Changes": "परिवर्तन सहेजें", - "Use Book Layout": "पुस्तक का लेआउट उपयोग करें" + "Use Book Layout": "पुस्तक का लेआउट उपयोग करें", + "End of this section. Continue to the next.": "इस अनुभाग का अंत। अगले पर जारी रखें।" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index c4614b6e..806a2023 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1191,5 +1191,6 @@ "Remove": "Eltávolítás", "Edit OPDS Catalog": "OPDS katalógus szerkesztése", "Save Changes": "Módosítások mentése", - "Use Book Layout": "Könyv elrendezésének használata" + "Use Book Layout": "Könyv elrendezésének használata", + "End of this section. Continue to the next.": "Ennek a szakasznak a vége. Folytatás a következővel." } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index aff1a571..02c87e97 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1178,5 +1178,6 @@ "Remove": "Hapus", "Edit OPDS Catalog": "Edit Katalog OPDS", "Save Changes": "Simpan Perubahan", - "Use Book Layout": "Gunakan tata letak buku" + "Use Book Layout": "Gunakan tata letak buku", + "End of this section. Continue to the next.": "Akhir bagian ini. Lanjutkan ke bagian berikutnya." } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 151c6338..8da64db4 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1204,5 +1204,6 @@ "Remove": "Rimuovi", "Edit OPDS Catalog": "Modifica catalogo OPDS", "Save Changes": "Salva modifiche", - "Use Book Layout": "Usa layout del libro" + "Use Book Layout": "Usa layout del libro", + "End of this section. Continue to the next.": "Fine di questa sezione. Continua alla successiva." } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 55cbf0f7..096b3b94 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1178,5 +1178,6 @@ "Remove": "削除", "Edit OPDS Catalog": "OPDSカタログを編集", "Save Changes": "変更を保存", - "Use Book Layout": "書籍のレイアウトを使用" + "Use Book Layout": "書籍のレイアウトを使用", + "End of this section. Continue to the next.": "このセクションの終わりです。次のセクションへ進みます。" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 1ab0ddfe..2ca78228 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1178,5 +1178,6 @@ "Remove": "삭제", "Edit OPDS Catalog": "OPDS 카탈로그 편집", "Save Changes": "변경 사항 저장", - "Use Book Layout": "책 레이아웃 사용" + "Use Book Layout": "책 레이아웃 사용", + "End of this section. Continue to the next.": "이 섹션의 끝입니다. 다음으로 계속하세요." } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index a0fecf94..094241cc 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1178,5 +1178,6 @@ "Remove": "Alih keluar", "Edit OPDS Catalog": "Edit Katalog OPDS", "Save Changes": "Simpan Perubahan", - "Use Book Layout": "Guna susun atur buku" + "Use Book Layout": "Guna susun atur buku", + "End of this section. Continue to the next.": "Akhir bahagian ini. Teruskan ke bahagian seterusnya." } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 28a8ec7b..5ae4db66 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1191,5 +1191,6 @@ "Remove": "Verwijderen", "Edit OPDS Catalog": "OPDS-catalogus bewerken", "Save Changes": "Wijzigingen opslaan", - "Use Book Layout": "Boekindeling gebruiken" + "Use Book Layout": "Boekindeling gebruiken", + "End of this section. Continue to the next.": "Einde van deze sectie. Ga verder naar de volgende." } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index ac59fe7d..4e301453 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1217,5 +1217,6 @@ "Remove": "Usuń", "Edit OPDS Catalog": "Edytuj katalog OPDS", "Save Changes": "Zapisz zmiany", - "Use Book Layout": "Użyj układu książki" + "Use Book Layout": "Użyj układu książki", + "End of this section. Continue to the next.": "Koniec tej sekcji. Przejdź do następnej." } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index fcea0c18..3b750875 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1204,5 +1204,6 @@ "Remove": "Remover", "Edit OPDS Catalog": "Editar catálogo OPDS", "Save Changes": "Salvar alterações", - "Use Book Layout": "Usar layout do livro" + "Use Book Layout": "Usar layout do livro", + "End of this section. Continue to the next.": "Fim desta secção. Continuar para a próxima." } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index 5b7a8a54..03b3467d 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1204,5 +1204,6 @@ "Remove": "Elimină", "Edit OPDS Catalog": "Editare catalog OPDS", "Save Changes": "Salvare modificări", - "Use Book Layout": "Folosește aspectul cărții" + "Use Book Layout": "Folosește aspectul cărții", + "End of this section. Continue to the next.": "Sfârșitul acestei secțiuni. Continuați la următoarea." } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 21ee0699..e39755ec 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1217,5 +1217,6 @@ "Remove": "Удалить", "Edit OPDS Catalog": "Редактировать каталог OPDS", "Save Changes": "Сохранить изменения", - "Use Book Layout": "Использовать макет книги" + "Use Book Layout": "Использовать макет книги", + "End of this section. Continue to the next.": "Конец этого раздела. Перейти к следующему." } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 1628dc43..27874988 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1191,5 +1191,6 @@ "Remove": "ඉවත් කරන්න", "Edit OPDS Catalog": "OPDS නාමාවලිය සංස්කරණය", "Save Changes": "වෙනස්කම් සුරකින්න", - "Use Book Layout": "පොතේ සැකැස්ම භාවිත කරන්න" + "Use Book Layout": "පොතේ සැකැස්ම භාවිත කරන්න", + "End of this section. Continue to the next.": "මෙම කොටසේ අවසානයයි. ඊළඟ කොටසට ඉදිරියට යන්න." } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 6d763405..d902b384 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1217,5 +1217,6 @@ "Remove": "Odstrani", "Edit OPDS Catalog": "Uredi katalog OPDS", "Save Changes": "Shrani spremembe", - "Use Book Layout": "Uporabi postavitev knjige" + "Use Book Layout": "Uporabi postavitev knjige", + "End of this section. Continue to the next.": "Konec tega razdelka. Nadaljujte z naslednjim." } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index 40d4dc2a..aae8d60c 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1191,5 +1191,6 @@ "Remove": "Ta bort", "Edit OPDS Catalog": "Redigera OPDS-katalog", "Save Changes": "Spara ändringar", - "Use Book Layout": "Använd bokens layout" + "Use Book Layout": "Använd bokens layout", + "End of this section. Continue to the next.": "Slut på det här avsnittet. Fortsätt till nästa." } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index ca4bdde2..75aaab04 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1191,5 +1191,6 @@ "Remove": "நீக்கு", "Edit OPDS Catalog": "OPDS பட்டியலைத் திருத்து", "Save Changes": "மாற்றங்களைச் சேமி", - "Use Book Layout": "புத்தகத்தின் அமைப்பைப் பயன்படுத்து" + "Use Book Layout": "புத்தகத்தின் அமைப்பைப் பயன்படுத்து", + "End of this section. Continue to the next.": "இந்தப் பிரிவின் முடிவு. அடுத்ததற்குத் தொடரவும்." } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index cfa9851d..cfcabd23 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1178,5 +1178,6 @@ "Remove": "ลบ", "Edit OPDS Catalog": "แก้ไขแคตตาล็อก OPDS", "Save Changes": "บันทึกการเปลี่ยนแปลง", - "Use Book Layout": "ใช้เลย์เอาต์ของหนังสือ" + "Use Book Layout": "ใช้เลย์เอาต์ของหนังสือ", + "End of this section. Continue to the next.": "สิ้นสุดส่วนนี้ ดำเนินการไปยังส่วนถัดไป" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 9ec557b1..cece5861 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1191,5 +1191,6 @@ "Remove": "Kaldır", "Edit OPDS Catalog": "OPDS Kataloğunu Düzenle", "Save Changes": "Değişiklikleri Kaydet", - "Use Book Layout": "Kitap düzenini kullan" + "Use Book Layout": "Kitap düzenini kullan", + "End of this section. Continue to the next.": "Bu bölümün sonu. Sonrakine devam edin." } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index c09f1ca8..1dfa7a99 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1217,5 +1217,6 @@ "Remove": "Видалити", "Edit OPDS Catalog": "Редагувати каталог OPDS", "Save Changes": "Зберегти зміни", - "Use Book Layout": "Використовувати макет книги" + "Use Book Layout": "Використовувати макет книги", + "End of this section. Continue to the next.": "Кінець цього розділу. Продовжте до наступного." } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 0ebf2533..8e783107 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1178,5 +1178,6 @@ "Remove": "Xóa", "Edit OPDS Catalog": "Chỉnh sửa danh mục OPDS", "Save Changes": "Lưu thay đổi", - "Use Book Layout": "Sử dụng bố cục sách" + "Use Book Layout": "Sử dụng bố cục sách", + "End of this section. Continue to the next.": "Kết thúc phần này. Tiếp tục đến phần tiếp theo." } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index c85f7d58..d9c0f7de 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1178,5 +1178,6 @@ "Remove": "移除", "Edit OPDS Catalog": "编辑 OPDS 目录", "Save Changes": "保存更改", - "Use Book Layout": "使用书籍排版" + "Use Book Layout": "使用书籍排版", + "End of this section. Continue to the next.": "本节结束。继续到下一节。" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index b3e4d5dd..0266d872 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1178,5 +1178,6 @@ "Remove": "移除", "Edit OPDS Catalog": "編輯 OPDS 目錄", "Save Changes": "儲存變更", - "Use Book Layout": "使用書籍排版" + "Use Book Layout": "使用書籍排版", + "End of this section. Continue to the next.": "本節結束。繼續到下一節。" } diff --git a/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts b/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts index a0c001d0..56074cf1 100644 --- a/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts +++ b/apps/readest-app/src/__tests__/document/paginator-multiview.browser.test.ts @@ -200,6 +200,75 @@ describe('Paginator multi-view architecture (browser)', () => { }); }); + describe('Accessibility: non-primary views are hidden from AT', () => { + const getViewWrappers = (el: Renderer) => { + const container = el.shadowRoot?.getElementById('container'); + return container ? (Array.from(container.children) as HTMLElement[]) : []; + }; + const wrapperContainsIframeForIndex = (wrapper: HTMLElement, doc: Document) => + wrapper.querySelector('iframe')?.contentDocument === doc; + + it('should mark non-primary view wrappers inert + aria-hidden', async () => { + const firstLinear = book.sections!.findIndex((s) => s.linear !== 'no'); + await setupAt(firstLinear); + await waitForViews(paginator, 2); + await waitForFillComplete(paginator); + + const contents = paginator.getContents(); + const primary = contents.find((c) => c.index === paginator.primaryIndex); + const nonPrimary = contents.filter((c) => c.index !== paginator.primaryIndex); + expect(primary).toBeDefined(); + expect(nonPrimary.length).toBeGreaterThan(0); + + const wrappers = getViewWrappers(paginator); + const primaryWrapper = wrappers.find((w) => wrapperContainsIframeForIndex(w, primary!.doc)); + expect(primaryWrapper).toBeDefined(); + expect(primaryWrapper!.hasAttribute('inert')).toBe(false); + expect(primaryWrapper!.getAttribute('aria-hidden')).not.toBe('true'); + + for (const np of nonPrimary) { + const wrapper = wrappers.find((w) => wrapperContainsIframeForIndex(w, np.doc)); + expect(wrapper).toBeDefined(); + expect(wrapper!.hasAttribute('inert')).toBe(true); + expect(wrapper!.getAttribute('aria-hidden')).toBe('true'); + } + }); + + it('should move inert + aria-hidden when the primary section changes', async () => { + const linearSections = book + .sections!.map((s, i) => ({ s, i })) + .filter(({ s }) => s.linear !== 'no'); + expect(linearSections.length).toBeGreaterThan(1); + + const first = linearSections[0]!.i; + const second = linearSections[1]!.i; + await setupAt(first); + await waitForViews(paginator, 2); + await waitForFillComplete(paginator); + + await paginator.goTo({ index: second }); + await new Promise((r) => setTimeout(r, 300)); + + expect(paginator.primaryIndex).toBe(second); + + const contents = paginator.getContents(); + const primary = contents.find((c) => c.index === second); + const wrappers = getViewWrappers(paginator); + const primaryWrapper = wrappers.find((w) => wrapperContainsIframeForIndex(w, primary!.doc)); + expect(primaryWrapper).toBeDefined(); + expect(primaryWrapper!.hasAttribute('inert')).toBe(false); + expect(primaryWrapper!.getAttribute('aria-hidden')).not.toBe('true'); + + const nonPrimary = contents.filter((c) => c.index !== second); + for (const np of nonPrimary) { + const wrapper = wrappers.find((w) => wrapperContainsIframeForIndex(w, np.doc)); + expect(wrapper).toBeDefined(); + expect(wrapper!.hasAttribute('inert')).toBe(true); + expect(wrapper!.getAttribute('aria-hidden')).toBe('true'); + } + }); + }); + describe('Navigation between sections', () => { it('should update primaryIndex when navigating to a different section', async () => { const linearSections = book diff --git a/apps/readest-app/src/__tests__/utils/a11y.test.ts b/apps/readest-app/src/__tests__/utils/a11y.test.ts index ecf0a4fd..1c191e65 100644 --- a/apps/readest-app/src/__tests__/utils/a11y.test.ts +++ b/apps/readest-app/src/__tests__/utils/a11y.test.ts @@ -1,61 +1,39 @@ -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, test, expect, vi, afterEach } from 'vitest'; import type { FoliateView } from '@/types/view'; - -vi.mock('@/utils/throttle', () => ({ - throttle: vi.fn((fn: (...args: unknown[]) => unknown) => fn), -})); -vi.mock('@/utils/debounce', () => ({ - debounce: vi.fn((fn: (...args: unknown[]) => unknown) => fn), -})); - -const mockObserve = vi.fn(); -const mockDisconnect = vi.fn(); -vi.stubGlobal( - 'IntersectionObserver', - class { - constructor( - public callback: IntersectionObserverCallback, - public options?: IntersectionObserverInit, - ) {} - observe = mockObserve; - disconnect = mockDisconnect; - unobserve = vi.fn(); - }, -); - import { handleA11yNavigation } from '@/utils/a11y'; function createMockView() { - return { - renderer: { - addEventListener: vi.fn(), - }, - getCFI: vi.fn().mockReturnValue('epubcfi(/6/4)'), - resolveNavigation: vi.fn().mockReturnValue({ index: 0 }), - }; + return {} as FoliateView; } +const LAST_POS_ID = 'readest-skip-link-last-pos'; +const NEXT_SECTION_ID = 'readest-skip-link-next-section'; + +const cleanupSkipLinks = () => { + document.getElementById(LAST_POS_ID)?.remove(); + document.getElementById(NEXT_SECTION_ID)?.remove(); +}; + +const makeOptions = (overrides: Partial[2]> = {}) => ({ + skipToLastPosCallback: vi.fn(), + skipToLastPosLabel: 'last', + skipToNextSectionCallback: vi.fn(), + skipToNextSectionLabel: 'next', + ...overrides, +}); + describe('handleA11yNavigation', () => { - let consoleSpy: ReturnType; - - beforeEach(() => { - consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - mockObserve.mockClear(); - mockDisconnect.mockClear(); - }); - afterEach(() => { - consoleSpy.mockRestore(); + cleanupSkipLinks(); vi.restoreAllMocks(); - // Clean up skip link from document body between tests - const existing = document.getElementById('readest-skip-link'); - if (existing) existing.remove(); }); test('returns early when view is null', () => { expect(() => { - handleA11yNavigation(null, document, 0); + handleA11yNavigation(null, document); }).not.toThrow(); + expect(document.getElementById(LAST_POS_ID)).toBeNull(); + expect(document.getElementById(NEXT_SECTION_ID)).toBeNull(); }); test('sets tabindex="-1" on anchor elements', () => { @@ -64,8 +42,7 @@ describe('handleA11yNavigation', () => { document.body.appendChild(a1); document.body.appendChild(a2); - const view = createMockView(); - handleA11yNavigation(view as unknown as FoliateView, document, 0); + handleA11yNavigation(createMockView(), document); expect(a1.getAttribute('tabindex')).toBe('-1'); expect(a2.getAttribute('tabindex')).toBe('-1'); @@ -74,106 +51,88 @@ describe('handleA11yNavigation', () => { a2.remove(); }); - test('creates skip link with correct attributes', () => { - const callback = vi.fn(); - const view = createMockView(); + test('creates last-pos skip link with correct attributes as first child', () => { + handleA11yNavigation( + createMockView(), + document, + makeOptions({ skipToLastPosLabel: 'Skip to reading position' }), + ); - handleA11yNavigation(view as unknown as FoliateView, document, 0, { - skipToLastPosCallback: callback, - skipToLastPosLabel: 'Skip to reading position', - }); - - const skipLink = document.getElementById('readest-skip-link'); + const skipLink = document.getElementById(LAST_POS_ID); expect(skipLink).not.toBeNull(); expect(skipLink!.getAttribute('cfi-inert')).toBe(''); expect(skipLink!.getAttribute('tabindex')).toBe('0'); expect(skipLink!.getAttribute('aria-hidden')).toBe('false'); expect(skipLink!.getAttribute('aria-label')).toBe('Skip to reading position'); - // Should be first child of body expect(document.body.firstElementChild).toBe(skipLink); }); - test('skip link click calls callback', () => { - const callback = vi.fn(); - const view = createMockView(); + test('creates next-section skip link with correct attributes as last child', () => { + handleA11yNavigation( + createMockView(), + document, + makeOptions({ skipToNextSectionLabel: 'Skip to next section' }), + ); - handleA11yNavigation(view as unknown as FoliateView, document, 0, { - skipToLastPosCallback: callback, - skipToLastPosLabel: 'Skip', - }); - - const skipLink = document.getElementById('readest-skip-link'); + const skipLink = document.getElementById(NEXT_SECTION_ID); expect(skipLink).not.toBeNull(); - - const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true }); - skipLink!.dispatchEvent(clickEvent); - - expect(callback).toHaveBeenCalledOnce(); + expect(skipLink!.getAttribute('cfi-inert')).toBe(''); + expect(skipLink!.getAttribute('tabindex')).toBe('0'); + expect(skipLink!.getAttribute('aria-hidden')).toBe('false'); + expect(skipLink!.getAttribute('aria-label')).toBe('Skip to next section'); + expect(document.body.lastElementChild).toBe(skipLink); }); - test('does not duplicate skip link if already exists', () => { - const view = createMockView(); + test('last-pos skip link click calls skipToLastPosCallback', () => { + const options = makeOptions(); + handleA11yNavigation(createMockView(), document, options); - // First call creates the skip link - handleA11yNavigation(view as unknown as FoliateView, document, 0, { - skipToLastPosCallback: vi.fn(), - skipToLastPosLabel: 'First', - }); - - // Second call should not create another - handleA11yNavigation(view as unknown as FoliateView, document, 0, { - skipToLastPosCallback: vi.fn(), - skipToLastPosLabel: 'Second', - }); - - const skipLinks = document.querySelectorAll('#readest-skip-link'); - expect(skipLinks.length).toBe(1); - // Label should still be from the first call - expect(skipLinks[0]!.getAttribute('aria-label')).toBe('First'); - }); - - test('observes paragraph elements', () => { - const p1 = document.createElement('p'); - const p2 = document.createElement('p'); - const p3 = document.createElement('p'); - document.body.appendChild(p1); - document.body.appendChild(p2); - document.body.appendChild(p3); - - const view = createMockView(); - handleA11yNavigation(view as unknown as FoliateView, document, 0); - - expect(mockObserve).toHaveBeenCalledTimes(3); - expect(mockObserve).toHaveBeenCalledWith(p1); - expect(mockObserve).toHaveBeenCalledWith(p2); - expect(mockObserve).toHaveBeenCalledWith(p3); - - p1.remove(); - p2.remove(); - p3.remove(); - }); - - test('registers scroll and relocate listeners on renderer', () => { - const view = createMockView(); - handleA11yNavigation(view as unknown as FoliateView, document, 0); - - expect(view.renderer.addEventListener).toHaveBeenCalledTimes(2); - - const calls = view.renderer.addEventListener.mock.calls; - expect(calls[0]![0]).toBe('scroll'); - expect(typeof calls[0]![1]).toBe('function'); - expect(calls[0]![2]).toEqual({ passive: true }); - - expect(calls[1]![0]).toBe('relocate'); - expect(typeof calls[1]![1]).toBe('function'); - }); - - test('skip link aria-label defaults to empty string when no options', () => { - const view = createMockView(); - handleA11yNavigation(view as unknown as FoliateView, document, 0); - - const skipLink = document.getElementById('readest-skip-link'); + const skipLink = document.getElementById(LAST_POS_ID); expect(skipLink).not.toBeNull(); - expect(skipLink!.getAttribute('aria-label')).toBe(''); + skipLink!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + + expect(options.skipToLastPosCallback).toHaveBeenCalledOnce(); + expect(options.skipToNextSectionCallback).not.toHaveBeenCalled(); + }); + + test('next-section skip link click calls skipToNextSectionCallback', () => { + const options = makeOptions(); + handleA11yNavigation(createMockView(), document, options); + + const skipLink = document.getElementById(NEXT_SECTION_ID); + expect(skipLink).not.toBeNull(); + skipLink!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + + expect(options.skipToNextSectionCallback).toHaveBeenCalledOnce(); + expect(options.skipToLastPosCallback).not.toHaveBeenCalled(); + }); + + test('does not duplicate skip links if already exist', () => { + handleA11yNavigation( + createMockView(), + document, + makeOptions({ skipToLastPosLabel: 'First-last', skipToNextSectionLabel: 'First-next' }), + ); + handleA11yNavigation( + createMockView(), + document, + makeOptions({ skipToLastPosLabel: 'Second-last', skipToNextSectionLabel: 'Second-next' }), + ); + + expect(document.querySelectorAll(`#${LAST_POS_ID}`).length).toBe(1); + expect(document.querySelectorAll(`#${NEXT_SECTION_ID}`).length).toBe(1); + expect(document.getElementById(LAST_POS_ID)!.getAttribute('aria-label')).toBe('First-last'); + expect(document.getElementById(NEXT_SECTION_ID)!.getAttribute('aria-label')).toBe('First-next'); + }); + + test('skip link aria-labels default to empty string when no options', () => { + handleA11yNavigation(createMockView(), document); + + const lastPos = document.getElementById(LAST_POS_ID); + const nextSection = document.getElementById(NEXT_SECTION_ID); + expect(lastPos).not.toBeNull(); + expect(nextSection).not.toBeNull(); + expect(lastPos!.getAttribute('aria-label')).toBe(''); + expect(nextSection!.getAttribute('aria-label')).toBe(''); }); }); diff --git a/apps/readest-app/src/app/reader/components/BooksGrid.tsx b/apps/readest-app/src/app/reader/components/BooksGrid.tsx index 11a66f55..3d714d3e 100644 --- a/apps/readest-app/src/app/reader/components/BooksGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BooksGrid.tsx @@ -130,10 +130,6 @@ const BooksGrid: React.FC = ({ bookKeys, onCloseBook, onGoToLibr onGoToLibrary={onGoToLibrary} onDropdownOpenChange={(isOpen) => setDropdownOpenBook(isOpen ? bookKey : '')} /> - = ({ bookKeys, onCloseBook, onGoToLibr gridInsets={gridInsets} /> )} + diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index 9b174bde..5a705621 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -12,7 +12,7 @@ import { useSettingsStore } from '@/store/settingsStore'; import { useCustomFontStore } from '@/store/customFontStore'; import { useParallelViewStore } from '@/store/parallelViewStore'; import { useMouseEvent, useTouchEvent, useLongPressEvent } from '../hooks/useIframeEvents'; -import { usePagination } from '../hooks/usePagination'; +import { usePagination, viewPagination } from '../hooks/usePagination'; import { useFoliateEvents } from '../hooks/useFoliateEvents'; import { useProgressSync } from '../hooks/useProgressSync'; import { useProgressAutoSave } from '../hooks/useProgressAutoSave'; @@ -196,6 +196,12 @@ const FoliateViewer: React.FC<{ } }, [getView, getProgress, bookKey]); + const skipToNextSection = useCallback(() => { + const view = getView(bookKey); + const viewSettings = getViewSettings(bookKey); + viewPagination(view, viewSettings, 'down', 'section'); + }, [bookKey]); + const docLoadHandler = (event: Event) => { docLoaded.current = true; if (bookDoc.rendition?.layout === 'pre-paginated') { @@ -249,9 +255,11 @@ const FoliateViewer: React.FC<{ applyScrollModeClass(detail.doc, viewSettings.scrolled || false); applyScrollbarStyle(document, viewSettings.hideScrollbar || false); keepTextAlignment(detail.doc); - handleA11yNavigation(viewRef.current, detail.doc, detail.index, { + handleA11yNavigation(viewRef.current, detail.doc, { skipToLastPosCallback: skipToReadingPosition, skipToLastPosLabel: _('Skip to last reading position'), + skipToNextSectionCallback: skipToNextSection, + skipToNextSectionLabel: _('End of this section. Continue to the next.'), }); // Inline scripts in tauri platforms are not executed by default diff --git a/apps/readest-app/src/app/reader/components/PageNavigationButtons.tsx b/apps/readest-app/src/app/reader/components/PageNavigationButtons.tsx index 4dd2f43f..2f0b8b08 100644 --- a/apps/readest-app/src/app/reader/components/PageNavigationButtons.tsx +++ b/apps/readest-app/src/app/reader/components/PageNavigationButtons.tsx @@ -84,15 +84,16 @@ const PageNavigationButtons: React.FC = ({ className={clsx( 'absolute left-2 -translate-y-1/2', 'flex items-center gap-1', - 'transition-opacity duration-300', - isPageNavigationButtonsVisible - ? 'top-1/2 z-10 opacity-100' - : `${appService?.isAndroidApp ? 'bottom-2' : 'pointer-events-none bottom-12'} opacity-0`, + isPageNavigationButtonsVisible ? 'top-1/2 opacity-100' : 'bottom-2 opacity-0', + !isPageNavigationButtonsVisible && !appService?.isAndroidApp ? 'pointer-events-none' : '', )} >