diff --git a/apps/readest-app/src/__tests__/components/ReadingRuler.test.tsx b/apps/readest-app/src/__tests__/components/ReadingRuler.test.tsx index 1db462bc..b2e86233 100644 --- a/apps/readest-app/src/__tests__/components/ReadingRuler.test.tsx +++ b/apps/readest-app/src/__tests__/components/ReadingRuler.test.tsx @@ -48,6 +48,25 @@ const makeLineRects = (count: number, pitch: number, height: number): RulerTestR height, })); +// A relocate range whose client rects are three vertical text columns (tall thin +// strips) in iframe-content coordinates. No frameElement -> rects map straight to +// overlay coordinates. Columns are listed right-to-left (reading order for +// vertical-rl), but the builder derives order from geometry + the rtl flag. +const makeVerticalColumnsRange = (): { + getClientRects: () => RulerTestRect[]; + startContainer: object; +} => { + const columnRects: RulerTestRect[] = [ + { top: 50, bottom: 950, left: 760, right: 776, width: 16, height: 900 }, // rightmost column + { top: 50, bottom: 950, left: 400, right: 416, width: 16, height: 900 }, // middle column + { top: 50, bottom: 950, left: 40, right: 56, width: 16, height: 900 }, // leftmost column + ]; + return { + startContainer: { ownerDocument: { defaultView: {} } }, + getClientRects: () => columnRects, + }; +}; + // A single visible section whose iframe is offset by `frameTop` along the scroll // axis (negative = scrolled down). `buildScrolledLineBoxes` walks these contents. const makeScrolledContents = ( @@ -299,6 +318,89 @@ describe('ReadingRuler', () => { expect(rulerTop()).toBeCloseTo(initialTop, 5); }); + // Regression: issue #4865 — for vertical-rl (Japanese vertical) books columns + // progress right-to-left, so advancing the ruler forward must move the band to + // the LEFT. It used to run backwards because rtl was false for vertical-rl. + it('advances the vertical-rl ruler band to the left on a forward move', async () => { + mockProgress = { range: makeVerticalColumnsRange(), pageinfo: { current: 0 } }; + const verticalSettings = { ...viewSettings } as ViewSettings; + + const { container } = render( + , + ); + const rulerLeft = () => + parseFloat((container.querySelector('.ruler') as HTMLDivElement).style.left); + + // Mount snaps the band onto the first (rightmost) column, near the right edge. + let before = 0; + await waitFor(() => { + before = rulerLeft(); + expect(before).toBeGreaterThan(80); + }); + + const consumed = eventDispatcher.dispatchSync('reading-ruler-move', { + bookKey: 'book-1', + direction: 'forward', + }); + + expect(consumed).toBe(true); + // Forward => next column to the left => band's left percentage decreases. + await waitFor(() => { + expect(rulerLeft()).toBeLessThan(before); + }); + }); + + // The vertical-lr (Mongolian) counterpart moves the other way: forward => right. + it('advances the vertical-lr ruler band to the right on a forward move', async () => { + mockProgress = { range: makeVerticalColumnsRange(), pageinfo: { current: 0 } }; + const verticalSettings = { ...viewSettings } as ViewSettings; + + const { container } = render( + , + ); + const rulerLeft = () => + parseFloat((container.querySelector('.ruler') as HTMLDivElement).style.left); + + // Mount snaps the band onto the first (leftmost) column, near the left edge. + let before = 100; + await waitFor(() => { + before = rulerLeft(); + expect(before).toBeLessThan(20); + }); + + const consumed = eventDispatcher.dispatchSync('reading-ruler-move', { + bookKey: 'book-1', + direction: 'forward', + }); + + expect(consumed).toBe(true); + await waitFor(() => { + expect(rulerLeft()).toBeGreaterThan(before); + }); + }); + it('still snaps the ruler to lines when advancing by click in scrolled mode', async () => { const lineRects = makeLineRects(50, 100, 40); mockProgress = { range: {}, pageinfo: { current: 0 } }; diff --git a/apps/readest-app/src/__tests__/libs/document.test.ts b/apps/readest-app/src/__tests__/libs/document.test.ts index 3f49af39..0fc4d949 100644 --- a/apps/readest-app/src/__tests__/libs/document.test.ts +++ b/apps/readest-app/src/__tests__/libs/document.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi } from 'vitest'; +import { afterEach, describe, it, expect, vi } from 'vitest'; import { readFileSync } from 'fs'; import { resolve } from 'path'; -import { DocumentLoader } from '@/libs/document'; +import { DocumentLoader, getDirection } from '@/libs/document'; if (typeof globalThis['CSS'] === 'undefined') { (globalThis as Record)['CSS'] = { @@ -94,3 +94,34 @@ describe('DocumentLoader.open', () => { expect(result.format).toBe('EPUB'); }, 15000); }); + +describe('getDirection', () => { + afterEach(() => { + document.body.removeAttribute('style'); + document.body.removeAttribute('dir'); + document.documentElement.removeAttribute('dir'); + document.body.innerHTML = ''; + }); + + it('treats vertical-rl (Japanese vertical) as RTL so columns advance right-to-left', () => { + // vertical-rl reads top-to-bottom with columns progressing right-to-left, but + // its computed CSS `direction` stays `ltr`; the writing mode alone marks it RTL. + document.body.style.writingMode = 'vertical-rl'; + expect(getDirection(document)).toEqual({ vertical: true, rtl: true }); + }); + + it('treats vertical-lr (Mongolian) as vertical but left-to-right', () => { + document.body.style.writingMode = 'vertical-lr'; + expect(getDirection(document)).toEqual({ vertical: true, rtl: false }); + }); + + it('leaves horizontal-tb as neither vertical nor RTL', () => { + document.body.style.writingMode = 'horizontal-tb'; + expect(getDirection(document)).toEqual({ vertical: false, rtl: false }); + }); + + it('still honors an explicit rtl dir for horizontal text', () => { + document.body.setAttribute('dir', 'rtl'); + expect(getDirection(document)).toEqual({ vertical: false, rtl: true }); + }); +}); diff --git a/apps/readest-app/src/libs/document.ts b/apps/readest-app/src/libs/document.ts index c09c8b11..f2df9d6e 100644 --- a/apps/readest-app/src/libs/document.ts +++ b/apps/readest-app/src/libs/document.ts @@ -452,7 +452,14 @@ export const getDirection = (doc: Document) => { } } const vertical = writingMode === 'vertical-rl' || writingMode === 'vertical-lr'; - const rtl = doc.body.dir === 'rtl' || direction === 'rtl' || doc.documentElement.dir === 'rtl'; + // `vertical-rl` (Japanese/Chinese vertical) advances columns right-to-left even + // though its computed `direction` stays `ltr`, so the writing mode itself marks + // it RTL. Without this the reading ruler and page turns run backwards (#4865). + const rtl = + writingMode === 'vertical-rl' || + doc.body.dir === 'rtl' || + direction === 'rtl' || + doc.documentElement.dir === 'rtl'; return { vertical, rtl }; };