Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ toolkit carry the same version; `sp-canvas start` says so when they drift.

Everything below is on `main` and reaches no install until a version is cut.

- **A two-finger pan over a board pans the canvas.** It used to send the browser
back a page instead, because a wheel event inside a board's iframe never
reaches the canvas and so never gets stopped.

## v1.1.0

2026-09-09. The canvas became something a review can point at, and the plugin
Expand Down
8 changes: 8 additions & 0 deletions canvas/src/canvasLibrary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,12 @@ describe('loadCanvasFileHtml', () => {
expect(await loadCanvasFileHtml(missing)).toBeUndefined()
expect(canvasFileHtml.has(missing)).toBe(false)
})

it('ends every board with the tag that stops the browser back gesture', async () => {
// A wheel inside an iframe never reaches tldraw, so a board that does not stop overscroll
// in its own document turns a two-finger pan over it into a back navigation.
const path = readCanvasLibrary()[1][0].path
const html = await loadCanvasFileHtml(path)
expect(html).toMatch(/<style>html\{overscroll-behavior:none\}<\/style>$/)
})
})
22 changes: 19 additions & 3 deletions canvas/src/canvasLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,22 @@ export async function cloneCanvas(slug: string, name: string) {
return JSON.parse(body).slug as string;
}

/** path -> raw HTML for every file fetched so far. Filled by loadCanvasFileHtml. */
/**
* Appended to every board. A wheel event whose target is inside an iframe never reaches the
* parent document, so tldraw's own fix for this — preventDefault on the wheel that reaches its
* container, in useGestureEvents — never runs, and the browser turns the horizontal part of a
* two-finger pan into a back navigation. The page's own `overscroll-behavior: none` in index.css
* cannot reach in: overscroll chains one frame at a time, so a board has to stop the chain in its
* own document. Every iframe in this app gets one, because the ones that can be panned over are
* not the same set in every browser and the fix has already been missed once per site.
*
* Appended rather than spliced: 37 of the repo's 180 boards emit no `</body>`, and a tag put
* before the doctype would drop the board into quirks mode. A trailing `<style>` is parsed into
* the body, and the inspector agent skips STYLE elements, so this adds no layer.
*/
const NO_OVERSCROLL = "<style>html{overscroll-behavior:none}</style>";

/** path -> the HTML every board iframe renders, for every file fetched so far. */
export const canvasFileHtml = new Map<string, string>();
const canvasFileLoads = new Map<string, Promise<string | undefined>>();

Expand All @@ -237,8 +252,9 @@ export function loadCanvasFileHtml(path: string): Promise<string | undefined> {
if (!load) {
load = loader()
.then((html) => {
canvasFileHtml.set(path, html);
return html;
const board = html + NO_OVERSCROLL;
canvasFileHtml.set(path, board);
return board;
})
// A chunk can fail to arrive — a board deleted between discovery and first render, a
// dev-server restart mid-flight. Without this the rejected promise stays in the map and
Expand Down
Loading