feat: light a chapter when its heading crosses 60% of the viewport
This commit is contained in:
@@ -32,10 +32,17 @@ export interface ActivationLineInput {
|
|||||||
visualViewportHeight: number;
|
visualViewportHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A chapter becomes current when its heading's top crosses this fraction of
|
||||||
|
* the visual viewport, measured from the top. At 60% the chapter lights up as
|
||||||
|
* its heading settles into the reading zone, not after it has been read.
|
||||||
|
*/
|
||||||
|
export const ACTIVATION_LINE_VH = 0.6;
|
||||||
|
|
||||||
export const activationLineDocumentY = (input: ActivationLineInput): number =>
|
export const activationLineDocumentY = (input: ActivationLineInput): number =>
|
||||||
input.scrollY +
|
input.scrollY +
|
||||||
input.visualViewportOffsetTop +
|
input.visualViewportOffsetTop +
|
||||||
input.visualViewportHeight * 0.35;
|
input.visualViewportHeight * ACTIVATION_LINE_VH;
|
||||||
|
|
||||||
export interface DocumentEndInput {
|
export interface DocumentEndInput {
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ test("passive scrolling replaces a stable fragment without stealing focus", asyn
|
|||||||
if (heading === null) throw new Error("Missing experience heading");
|
if (heading === null) throw new Error("Missing experience heading");
|
||||||
const headingDocumentTop =
|
const headingDocumentTop =
|
||||||
heading.getBoundingClientRect().top + window.scrollY;
|
heading.getBoundingClientRect().top + window.scrollY;
|
||||||
// Place the heading just beyond the 35% activation line. Scroll offsets
|
// Place the heading just beyond the 60% activation line. Scroll offsets
|
||||||
// are integer-rounded in Chromium, so exact boundary equality is brittle.
|
// are integer-rounded in Chromium, so exact boundary equality is brittle.
|
||||||
window.scrollTo(0, headingDocumentTop - window.innerHeight * 0.34);
|
window.scrollTo(0, headingDocumentTop - window.innerHeight * 0.59);
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(page).toHaveURL(`${distUrl}/#experience`, { timeout: 2000 });
|
await expect(page).toHaveURL(`${distUrl}/#experience`, { timeout: 2000 });
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ const headings: readonly SectionHeadingGeometry[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe("active section selection", () => {
|
describe("active section selection", () => {
|
||||||
it("places the activation line at a literal 35 percent of the visual viewport", () => {
|
it("places the activation line at a literal 60 percent of the visual viewport", () => {
|
||||||
expect(
|
expect(
|
||||||
activationLineDocumentY({
|
activationLineDocumentY({
|
||||||
scrollY: 200,
|
scrollY: 200,
|
||||||
visualViewportOffsetTop: 25,
|
visualViewportOffsetTop: 25,
|
||||||
visualViewportHeight: 1000,
|
visualViewportHeight: 1000,
|
||||||
}),
|
}),
|
||||||
).toBe(575);
|
).toBe(825);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forces document end within exactly two CSS pixels using current geometry", () => {
|
it("forces document end within exactly two CSS pixels using current geometry", () => {
|
||||||
@@ -60,7 +60,7 @@ describe("active section selection", () => {
|
|||||||
).toBe("start");
|
).toBe("start");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selects the last heading at or above the literal 35 percent activation line", () => {
|
it("selects the last heading at or above the activation line", () => {
|
||||||
expect(
|
expect(
|
||||||
selectActiveSection({
|
selectActiveSection({
|
||||||
headings,
|
headings,
|
||||||
|
|||||||
Reference in New Issue
Block a user