diff --git a/site/e2e/touch.spec.ts b/site/e2e/touch.spec.ts index 9283f87..0968fce 100644 --- a/site/e2e/touch.spec.ts +++ b/site/e2e/touch.spec.ts @@ -145,22 +145,44 @@ test("holding Enter sinks the focused building until release", async ({ page }) await expect(page.locator(".block.pressed")).toHaveCount(0); }); +/** Pixels the name sits from its expected place. */ +const drift = (page: Page, name: string) => + page.evaluate(([n, unit]) => { + const svg = document.querySelector(".tour .stage")!; + const scale = svg.getBoundingClientRect().width / svg.viewBox.baseVal.width; + const rects = [...document.querySelectorAll(`.hit-face[data-part="${n}"]`)].map((f) => f.getBoundingClientRect()); + const centre = (Math.min(...rects.map((r) => r.left)) + Math.max(...rects.map((r) => r.right))) / 2; + const bottom = Math.min(...rects.map((r) => r.top)) - 2 * unit * scale; + const box = document.querySelector(".tour .caption.shown")!.getBoundingClientRect(); + return Math.max(Math.abs(box.left + box.width / 2 - centre), Math.abs(box.bottom - bottom)); + }, [name, CELL] as const); + test("a focused name follows the model when the window narrows", async ({ page }) => { await page.setViewportSize({ width: 760, height: 1000 }); await page.locator(".doors a").nth(3).focus(); await page.setViewportSize({ width: 390, height: 844 }); - await expect - .poll(() => page.evaluate(() => document.documentElement.scrollWidth - innerWidth)) - .toBe(0); - const where = () => page.evaluate(() => { - const name = document.querySelector(".tour .caption.shown")!.getBoundingClientRect(); - const tops = [...document.querySelectorAll('.block[data-part="ElyPDF"] .outline')].map((o) => o.getBoundingClientRect()); - const left = Math.min(...tops.map((r) => r.left)); - const right = Math.max(...tops.map((r) => r.right)); - return { centre: name.left + name.width / 2, left, right, inside: name.left >= 0 && name.right <= innerWidth }; + await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBe(0); + await expect.poll(() => drift(page, "ElyPDF")).toBeLessThan(1); +}); + +test("a focused name follows the model when only the model resizes", async ({ page }) => { + await page.locator(".doors a").nth(3).focus(); + await page.evaluate(() => { + const svg = document.querySelector(".tour .stage")!; + svg.style.width = `${svg.getBoundingClientRect().width * 0.8}px`; }); - await expect.poll(async () => (await where()).inside).toBe(true); - const placed = await where(); - expect(placed.centre).toBeGreaterThanOrEqual(placed.left); - expect(placed.centre).toBeLessThanOrEqual(placed.right); + await expect.poll(() => drift(page, "ElyPDF")).toBeLessThan(1); +}); + +test.describe("with motion", () => { + test.use({ reducedMotion: "no-preference" }); + + test("the name sits one cell above the building once it has lifted", async ({ page }) => { + await page.waitForFunction(() => document.getAnimations().every((a) => a.playState !== "running"), null, { + timeout: 20_000, + }); + await page.locator(".doors a").first().focus(); + await page.waitForTimeout(400); + expect(await drift(page, "ElyDoc")).toBeLessThan(1); + }); }); diff --git a/site/src/touch.ts b/site/src/touch.ts index b617fca..25a6784 100644 --- a/site/src/touch.ts +++ b/site/src/touch.ts @@ -23,7 +23,8 @@ export function touch(host: HTMLElement, svg: SVGSVGElement, parts: Map place(caption, host, svg, blocks); + const follow = () => place(caption, host, svg, hit); + const sized = new ResizeObserver(follow); const show = () => { const up = state.hover || state.focus; for (const b of blocks) { @@ -32,9 +33,14 @@ export function touch(host: HTMLElement, svg: SVGSVGElement, parts: Map) => { Object.assign(state, change); @@ -82,14 +88,15 @@ function still(svg: SVGSVGElement, owner: Map): SVGPolygonE return hits; } -/** Puts the caption one step above the building's top. */ -function place(caption: HTMLElement, host: HTMLElement, svg: SVGSVGElement, blocks: SVGGElement[]) { +/** Puts the caption one cell above the lifted top. */ +function place(caption: HTMLElement, host: HTMLElement, svg: SVGSVGElement, faces: SVGPolygonElement[]) { const origin = host.getBoundingClientRect(); const scale = svg.getBoundingClientRect().width / svg.viewBox.baseVal.width; - const rects = blocks.map((b) => b.querySelector(".outline")!.getBoundingClientRect()); + // Hit faces never lift, so they give the rest pose. + const rects = faces.map((f) => f.getBoundingClientRect()); const left = Math.min(...rects.map((r) => r.left)); const right = Math.max(...rects.map((r) => r.right)); const top = Math.min(...rects.map((r) => r.top)); caption.style.left = `${(left + right) / 2 - origin.left}px`; - caption.style.top = `${top - origin.top - UNIT * scale}px`; + caption.style.top = `${top - origin.top - 2 * UNIT * scale}px`; }