forked from akai/readest
Vertical-rl (Japanese/Chinese vertical) books read top-to-bottom with columns progressing right-to-left, but getDirection only derived rtl from the horizontal dir/direction, which stays ltr for these books. As a result viewSettings.rtl was false and the reading ruler laid columns out left-to-right, advancing the band the wrong way (reverse reading order). Treat writing-mode: vertical-rl as RTL in getDirection so vertical-rl runs through the same rtl paths that horizontal-rtl already uses: the reading ruler coordinate mapping, page-turn tap mapping, footer navigation, and the progress bar. Page-turn taps for these books now follow the vertical-rl convention (tap left to go forward), matching horizontal-rtl behavior. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
<ReadingRuler
|
||||
bookKey='book-1'
|
||||
isVertical={true}
|
||||
rtl={true}
|
||||
lines={1}
|
||||
position={4}
|
||||
opacity={0.5}
|
||||
color='transparent'
|
||||
bookFormat='EPUB'
|
||||
viewSettings={verticalSettings}
|
||||
gridInsets={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<ReadingRuler
|
||||
bookKey='book-1'
|
||||
isVertical={true}
|
||||
rtl={false}
|
||||
lines={1}
|
||||
position={4}
|
||||
opacity={0.5}
|
||||
color='transparent'
|
||||
bookFormat='EPUB'
|
||||
viewSettings={verticalSettings}
|
||||
gridInsets={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
||||
/>,
|
||||
);
|
||||
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 } };
|
||||
|
||||
@@ -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<string, unknown>)['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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user