@tldraw/editor 5.3.0-next.dec36c5c2930 → 5.3.0-next.df310c6196c0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cjs/index.js CHANGED
@@ -389,7 +389,7 @@ var import_uniq = require("./lib/utils/uniq");
389
389
  var import_defaultThemes2 = require("./lib/editor/managers/ThemeManager/defaultThemes");
390
390
  (0, import_utils.registerTldrawLibraryVersion)(
391
391
  "@tldraw/editor",
392
- "5.3.0-next.dec36c5c2930",
392
+ "5.3.0-next.df310c6196c0",
393
393
  "cjs"
394
394
  );
395
395
  //# sourceMappingURL=index.js.map
@@ -25,6 +25,7 @@ var import_react = require("react");
25
25
  var import_dom = require("../utils/dom");
26
26
  var import_useContainer = require("./useContainer");
27
27
  var import_useEditor = require("./useEditor");
28
+ var import_useEvent = require("./useEvent");
28
29
  function isScrollableOverflow(overflow) {
29
30
  return overflow === "auto" || overflow === "scroll" || overflow === "overlay";
30
31
  }
@@ -53,27 +54,33 @@ function usePassThroughWheelEvents(ref) {
53
54
  if (!ref) throw Error("usePassThroughWheelEvents must be passed a ref");
54
55
  const container = (0, import_useContainer.useContainer)();
55
56
  const editor = (0, import_useEditor.useMaybeEditor)();
56
- (0, import_react.useEffect)(() => {
57
- function onWheel(e) {
58
- if (!editor?.getInstanceState().isFocused) return;
59
- if (e.isSpecialRedispatchedEvent) return;
60
- const elm2 = ref.current;
61
- if (elm2 && hasScrollableElement(e.target, elm2)) {
62
- return;
63
- }
64
- (0, import_dom.preventDefault)(e);
65
- const cvs = container.querySelector(".tl-canvas");
66
- if (!cvs) return;
67
- const newEvent = new WheelEvent("wheel", e);
68
- newEvent.isSpecialRedispatchedEvent = true;
69
- cvs.dispatchEvent(newEvent);
57
+ const onWheel = (0, import_useEvent.useEvent)((e) => {
58
+ if (!editor?.getInstanceState().isFocused) return;
59
+ if (e.isSpecialRedispatchedEvent) return;
60
+ const elm = ref.current;
61
+ if (elm && hasScrollableElement(e.target, elm)) {
62
+ return;
70
63
  }
64
+ (0, import_dom.preventDefault)(e);
65
+ const cvs = container.querySelector(".tl-canvas");
66
+ if (!cvs) return;
67
+ const newEvent = new WheelEvent("wheel", e);
68
+ newEvent.isSpecialRedispatchedEvent = true;
69
+ cvs.dispatchEvent(newEvent);
70
+ });
71
+ const attached = (0, import_react.useRef)(null);
72
+ (0, import_react.useEffect)(() => {
71
73
  const elm = ref.current;
72
- if (!elm) return;
73
- elm.addEventListener("wheel", onWheel, { passive: false });
74
+ if (elm === attached.current) return;
75
+ attached.current?.removeEventListener("wheel", onWheel);
76
+ attached.current = elm;
77
+ elm?.addEventListener("wheel", onWheel, { passive: false });
78
+ });
79
+ (0, import_react.useEffect)(() => {
74
80
  return () => {
75
- elm.removeEventListener("wheel", onWheel);
81
+ attached.current?.removeEventListener("wheel", onWheel);
82
+ attached.current = null;
76
83
  };
77
- }, [container, editor, ref]);
84
+ }, [onWheel]);
78
85
  }
79
86
  //# sourceMappingURL=usePassThroughWheelEvents.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/usePassThroughWheelEvents.ts"],
4
- "sourcesContent": ["import { RefObject, useEffect } from 'react'\nimport { preventDefault } from '../utils/dom'\nimport { useContainer } from './useContainer'\nimport { useMaybeEditor } from './useEditor'\n\nfunction isScrollableOverflow(overflow: string) {\n\treturn overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay'\n}\n\n// An element only consumes a wheel event when it's a real scroll container: its content overflows\n// AND its computed overflow is scrollable. Checking scrollHeight/scrollWidth alone is not enough \u2014\n// an `overflow: visible` element whose content merely overflows (e.g. a comment pin whose hover\n// `transform: scale()` inflates scrollHeight past clientHeight) never scrolls, so the wheel must\n// still pass through to the canvas.\nfunction isScrollable(elm: HTMLElement) {\n\t// Cheap first: if nothing overflows, it can't scroll \u2014 skip the getComputedStyle read. This runs\n\t// per ancestor on every wheel event, so avoid resolving styles for the common (non-overflowing) case.\n\tconst overflowsY = elm.scrollHeight > elm.clientHeight\n\tconst overflowsX = elm.scrollWidth > elm.clientWidth\n\tif (!overflowsY && !overflowsX) return false\n\n\tconst style = getComputedStyle(elm)\n\treturn (\n\t\t(overflowsY && isScrollableOverflow(style.overflowY)) ||\n\t\t(overflowsX && isScrollableOverflow(style.overflowX))\n\t)\n}\n\n// Walk from the wheeled element up to (and including) the pass-through root, looking for a scroll\n// container. If one is found, the wheel belongs to it \u2014 don't redispatch it to the canvas.\nfunction hasScrollableElement(target: EventTarget | null, root: HTMLElement) {\n\tif (!(target instanceof Node)) {\n\t\treturn isScrollable(root)\n\t}\n\n\tlet elm: Element | null = target instanceof Element ? target : target.parentElement\n\twhile (elm) {\n\t\tif (elm instanceof HTMLElement && isScrollable(elm)) {\n\t\t\treturn true\n\t\t}\n\t\tif (elm === root) return false\n\t\telm = elm.parentElement\n\t}\n\n\treturn false\n}\n\n/** @public */\nexport function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {\n\tif (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')\n\tconst container = useContainer()\n\tconst editor = useMaybeEditor()\n\n\tuseEffect(() => {\n\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t// Only pass through wheel events if the editor is focused\n\t\t\tif (!editor?.getInstanceState().isFocused) return\n\n\t\t\tif ((e as any).isSpecialRedispatchedEvent) return\n\n\t\t\t// If the wheel is over a scrollable element, let it scroll instead of redispatching.\n\t\t\tconst elm = ref.current\n\t\t\tif (elm && hasScrollableElement(e.target, elm)) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpreventDefault(e)\n\t\t\tconst cvs = container.querySelector('.tl-canvas')\n\t\t\tif (!cvs) return\n\t\t\tconst newEvent = new WheelEvent('wheel', e as any)\n\t\t\t;(newEvent as any).isSpecialRedispatchedEvent = true\n\t\t\tcvs.dispatchEvent(newEvent)\n\t\t}\n\n\t\tconst elm = ref.current\n\t\tif (!elm) return\n\n\t\telm.addEventListener('wheel', onWheel, { passive: false })\n\t\treturn () => {\n\t\t\telm.removeEventListener('wheel', onWheel)\n\t\t}\n\t}, [container, editor, ref])\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAqC;AACrC,iBAA+B;AAC/B,0BAA6B;AAC7B,uBAA+B;AAE/B,SAAS,qBAAqB,UAAkB;AAC/C,SAAO,aAAa,UAAU,aAAa,YAAY,aAAa;AACrE;AAOA,SAAS,aAAa,KAAkB;AAGvC,QAAM,aAAa,IAAI,eAAe,IAAI;AAC1C,QAAM,aAAa,IAAI,cAAc,IAAI;AACzC,MAAI,CAAC,cAAc,CAAC,WAAY,QAAO;AAEvC,QAAM,QAAQ,iBAAiB,GAAG;AAClC,SACE,cAAc,qBAAqB,MAAM,SAAS,KAClD,cAAc,qBAAqB,MAAM,SAAS;AAErD;AAIA,SAAS,qBAAqB,QAA4B,MAAmB;AAC5E,MAAI,EAAE,kBAAkB,OAAO;AAC9B,WAAO,aAAa,IAAI;AAAA,EACzB;AAEA,MAAI,MAAsB,kBAAkB,UAAU,SAAS,OAAO;AACtE,SAAO,KAAK;AACX,QAAI,eAAe,eAAe,aAAa,GAAG,GAAG;AACpD,aAAO;AAAA,IACR;AACA,QAAI,QAAQ,KAAM,QAAO;AACzB,UAAM,IAAI;AAAA,EACX;AAEA,SAAO;AACR;AAGO,SAAS,0BAA0B,KAAoC;AAC7E,MAAI,CAAC,IAAK,OAAM,MAAM,gDAAgD;AACtE,QAAM,gBAAY,kCAAa;AAC/B,QAAM,aAAS,iCAAe;AAE9B,8BAAU,MAAM;AACf,aAAS,QAAQ,GAAe;AAE/B,UAAI,CAAC,QAAQ,iBAAiB,EAAE,UAAW;AAE3C,UAAK,EAAU,2BAA4B;AAG3C,YAAMA,OAAM,IAAI;AAChB,UAAIA,QAAO,qBAAqB,EAAE,QAAQA,IAAG,GAAG;AAC/C;AAAA,MACD;AAEA,qCAAe,CAAC;AAChB,YAAM,MAAM,UAAU,cAAc,YAAY;AAChD,UAAI,CAAC,IAAK;AACV,YAAM,WAAW,IAAI,WAAW,SAAS,CAAQ;AAChD,MAAC,SAAiB,6BAA6B;AAChD,UAAI,cAAc,QAAQ;AAAA,IAC3B;AAEA,UAAM,MAAM,IAAI;AAChB,QAAI,CAAC,IAAK;AAEV,QAAI,iBAAiB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;AACzD,WAAO,MAAM;AACZ,UAAI,oBAAoB,SAAS,OAAO;AAAA,IACzC;AAAA,EACD,GAAG,CAAC,WAAW,QAAQ,GAAG,CAAC;AAC5B;",
6
- "names": ["elm"]
4
+ "sourcesContent": ["import { RefObject, useEffect, useRef } from 'react'\nimport { preventDefault } from '../utils/dom'\nimport { useContainer } from './useContainer'\nimport { useMaybeEditor } from './useEditor'\nimport { useEvent } from './useEvent'\n\nfunction isScrollableOverflow(overflow: string) {\n\treturn overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay'\n}\n\n// An element only consumes a wheel event when it's a real scroll container: its content overflows\n// AND its computed overflow is scrollable. Checking scrollHeight/scrollWidth alone is not enough \u2014\n// an `overflow: visible` element whose content merely overflows (e.g. a comment pin whose hover\n// `transform: scale()` inflates scrollHeight past clientHeight) never scrolls, so the wheel must\n// still pass through to the canvas.\nfunction isScrollable(elm: HTMLElement) {\n\t// Cheap first: if nothing overflows, it can't scroll \u2014 skip the getComputedStyle read. This runs\n\t// per ancestor on every wheel event, so avoid resolving styles for the common (non-overflowing) case.\n\tconst overflowsY = elm.scrollHeight > elm.clientHeight\n\tconst overflowsX = elm.scrollWidth > elm.clientWidth\n\tif (!overflowsY && !overflowsX) return false\n\n\tconst style = getComputedStyle(elm)\n\treturn (\n\t\t(overflowsY && isScrollableOverflow(style.overflowY)) ||\n\t\t(overflowsX && isScrollableOverflow(style.overflowX))\n\t)\n}\n\n// Walk from the wheeled element up to (and including) the pass-through root, looking for a scroll\n// container. If one is found, the wheel belongs to it \u2014 don't redispatch it to the canvas.\nfunction hasScrollableElement(target: EventTarget | null, root: HTMLElement) {\n\tif (!(target instanceof Node)) {\n\t\treturn isScrollable(root)\n\t}\n\n\tlet elm: Element | null = target instanceof Element ? target : target.parentElement\n\twhile (elm) {\n\t\tif (elm instanceof HTMLElement && isScrollable(elm)) {\n\t\t\treturn true\n\t\t}\n\t\tif (elm === root) return false\n\t\telm = elm.parentElement\n\t}\n\n\treturn false\n}\n\n/** @public */\nexport function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {\n\tif (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')\n\tconst container = useContainer()\n\tconst editor = useMaybeEditor()\n\n\tconst onWheel = useEvent((e: WheelEvent) => {\n\t\t// Only pass through wheel events if the editor is focused\n\t\tif (!editor?.getInstanceState().isFocused) return\n\n\t\tif ((e as any).isSpecialRedispatchedEvent) return\n\n\t\t// If the wheel is over a scrollable element, let it scroll instead of redispatching.\n\t\tconst elm = ref.current\n\t\tif (elm && hasScrollableElement(e.target, elm)) {\n\t\t\treturn\n\t\t}\n\n\t\tpreventDefault(e)\n\t\tconst cvs = container.querySelector('.tl-canvas')\n\t\tif (!cvs) return\n\t\tconst newEvent = new WheelEvent('wheel', e as any)\n\t\t;(newEvent as any).isSpecialRedispatchedEvent = true\n\t\tcvs.dispatchEvent(newEvent)\n\t})\n\n\t// The element the listener is currently on, which isn't necessarily `ref.current`: a `RefObject`\n\t// gives no notification when its element changes.\n\tconst attached = useRef<HTMLElement | null>(null)\n\n\t// Deliberately no dependency array \u2014 this re-checks the ref after every render of the component\n\t// that owns it, and re-attaches when the element has been swapped. A component that keeps the\n\t// hook mounted while unmounting and remounting the element it points at (a comment pin culled\n\t// while its anchor is off screen, say) would otherwise leave the listener on the discarded node\n\t// and silently lose pass-through once the element comes back.\n\tuseEffect(() => {\n\t\tconst elm = ref.current\n\t\tif (elm === attached.current) return\n\t\tattached.current?.removeEventListener('wheel', onWheel)\n\t\tattached.current = elm\n\t\telm?.addEventListener('wheel', onWheel, { passive: false })\n\t})\n\n\t// Teardown is its own effect so the re-attach check above can skip returning a cleanup that\n\t// would tear the listener down and rebuild it on every render.\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tattached.current?.removeEventListener('wheel', onWheel)\n\t\t\tattached.current = null\n\t\t}\n\t}, [onWheel])\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,iBAA+B;AAC/B,0BAA6B;AAC7B,uBAA+B;AAC/B,sBAAyB;AAEzB,SAAS,qBAAqB,UAAkB;AAC/C,SAAO,aAAa,UAAU,aAAa,YAAY,aAAa;AACrE;AAOA,SAAS,aAAa,KAAkB;AAGvC,QAAM,aAAa,IAAI,eAAe,IAAI;AAC1C,QAAM,aAAa,IAAI,cAAc,IAAI;AACzC,MAAI,CAAC,cAAc,CAAC,WAAY,QAAO;AAEvC,QAAM,QAAQ,iBAAiB,GAAG;AAClC,SACE,cAAc,qBAAqB,MAAM,SAAS,KAClD,cAAc,qBAAqB,MAAM,SAAS;AAErD;AAIA,SAAS,qBAAqB,QAA4B,MAAmB;AAC5E,MAAI,EAAE,kBAAkB,OAAO;AAC9B,WAAO,aAAa,IAAI;AAAA,EACzB;AAEA,MAAI,MAAsB,kBAAkB,UAAU,SAAS,OAAO;AACtE,SAAO,KAAK;AACX,QAAI,eAAe,eAAe,aAAa,GAAG,GAAG;AACpD,aAAO;AAAA,IACR;AACA,QAAI,QAAQ,KAAM,QAAO;AACzB,UAAM,IAAI;AAAA,EACX;AAEA,SAAO;AACR;AAGO,SAAS,0BAA0B,KAAoC;AAC7E,MAAI,CAAC,IAAK,OAAM,MAAM,gDAAgD;AACtE,QAAM,gBAAY,kCAAa;AAC/B,QAAM,aAAS,iCAAe;AAE9B,QAAM,cAAU,0BAAS,CAAC,MAAkB;AAE3C,QAAI,CAAC,QAAQ,iBAAiB,EAAE,UAAW;AAE3C,QAAK,EAAU,2BAA4B;AAG3C,UAAM,MAAM,IAAI;AAChB,QAAI,OAAO,qBAAqB,EAAE,QAAQ,GAAG,GAAG;AAC/C;AAAA,IACD;AAEA,mCAAe,CAAC;AAChB,UAAM,MAAM,UAAU,cAAc,YAAY;AAChD,QAAI,CAAC,IAAK;AACV,UAAM,WAAW,IAAI,WAAW,SAAS,CAAQ;AAChD,IAAC,SAAiB,6BAA6B;AAChD,QAAI,cAAc,QAAQ;AAAA,EAC3B,CAAC;AAID,QAAM,eAAW,qBAA2B,IAAI;AAOhD,8BAAU,MAAM;AACf,UAAM,MAAM,IAAI;AAChB,QAAI,QAAQ,SAAS,QAAS;AAC9B,aAAS,SAAS,oBAAoB,SAAS,OAAO;AACtD,aAAS,UAAU;AACnB,SAAK,iBAAiB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;AAAA,EAC3D,CAAC;AAID,8BAAU,MAAM;AACf,WAAO,MAAM;AACZ,eAAS,SAAS,oBAAoB,SAAS,OAAO;AACtD,eAAS,UAAU;AAAA,IACpB;AAAA,EACD,GAAG,CAAC,OAAO,CAAC;AACb;",
6
+ "names": []
7
7
  }
@@ -22,10 +22,10 @@ __export(version_exports, {
22
22
  version: () => version
23
23
  });
24
24
  module.exports = __toCommonJS(version_exports);
25
- const version = "5.3.0-next.dec36c5c2930";
25
+ const version = "5.3.0-next.df310c6196c0";
26
26
  const publishDates = {
27
27
  major: "2026-05-06T16:28:18.473Z",
28
- minor: "2026-08-05T10:19:16.457Z",
29
- patch: "2026-08-05T10:19:16.457Z"
28
+ minor: "2026-08-05T11:28:43.788Z",
29
+ patch: "2026-08-05T11:28:43.788Z"
30
30
  };
31
31
  //# sourceMappingURL=version.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.3.0-next.dec36c5c2930'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-05T10:19:16.457Z',\n\tpatch: '2026-08-05T10:19:16.457Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.3.0-next.df310c6196c0'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-05T11:28:43.788Z',\n\tpatch: '2026-08-05T11:28:43.788Z',\n}\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
@@ -305,7 +305,7 @@ import { LocalIndexedDb, Table } from "./lib/utils/sync/LocalIndexedDb.mjs";
305
305
  import { uniq } from "./lib/utils/uniq.mjs";
306
306
  registerTldrawLibraryVersion(
307
307
  "@tldraw/editor",
308
- "5.3.0-next.dec36c5c2930",
308
+ "5.3.0-next.df310c6196c0",
309
309
  "esm"
310
310
  );
311
311
  import { getColorValue } from "./lib/editor/managers/ThemeManager/defaultThemes.mjs";
@@ -1,7 +1,8 @@
1
- import { useEffect } from "react";
1
+ import { useEffect, useRef } from "react";
2
2
  import { preventDefault } from "../utils/dom.mjs";
3
3
  import { useContainer } from "./useContainer.mjs";
4
4
  import { useMaybeEditor } from "./useEditor.mjs";
5
+ import { useEvent } from "./useEvent.mjs";
5
6
  function isScrollableOverflow(overflow) {
6
7
  return overflow === "auto" || overflow === "scroll" || overflow === "overlay";
7
8
  }
@@ -30,28 +31,34 @@ function usePassThroughWheelEvents(ref) {
30
31
  if (!ref) throw Error("usePassThroughWheelEvents must be passed a ref");
31
32
  const container = useContainer();
32
33
  const editor = useMaybeEditor();
33
- useEffect(() => {
34
- function onWheel(e) {
35
- if (!editor?.getInstanceState().isFocused) return;
36
- if (e.isSpecialRedispatchedEvent) return;
37
- const elm2 = ref.current;
38
- if (elm2 && hasScrollableElement(e.target, elm2)) {
39
- return;
40
- }
41
- preventDefault(e);
42
- const cvs = container.querySelector(".tl-canvas");
43
- if (!cvs) return;
44
- const newEvent = new WheelEvent("wheel", e);
45
- newEvent.isSpecialRedispatchedEvent = true;
46
- cvs.dispatchEvent(newEvent);
34
+ const onWheel = useEvent((e) => {
35
+ if (!editor?.getInstanceState().isFocused) return;
36
+ if (e.isSpecialRedispatchedEvent) return;
37
+ const elm = ref.current;
38
+ if (elm && hasScrollableElement(e.target, elm)) {
39
+ return;
47
40
  }
41
+ preventDefault(e);
42
+ const cvs = container.querySelector(".tl-canvas");
43
+ if (!cvs) return;
44
+ const newEvent = new WheelEvent("wheel", e);
45
+ newEvent.isSpecialRedispatchedEvent = true;
46
+ cvs.dispatchEvent(newEvent);
47
+ });
48
+ const attached = useRef(null);
49
+ useEffect(() => {
48
50
  const elm = ref.current;
49
- if (!elm) return;
50
- elm.addEventListener("wheel", onWheel, { passive: false });
51
+ if (elm === attached.current) return;
52
+ attached.current?.removeEventListener("wheel", onWheel);
53
+ attached.current = elm;
54
+ elm?.addEventListener("wheel", onWheel, { passive: false });
55
+ });
56
+ useEffect(() => {
51
57
  return () => {
52
- elm.removeEventListener("wheel", onWheel);
58
+ attached.current?.removeEventListener("wheel", onWheel);
59
+ attached.current = null;
53
60
  };
54
- }, [container, editor, ref]);
61
+ }, [onWheel]);
55
62
  }
56
63
  export {
57
64
  usePassThroughWheelEvents
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/hooks/usePassThroughWheelEvents.ts"],
4
- "sourcesContent": ["import { RefObject, useEffect } from 'react'\nimport { preventDefault } from '../utils/dom'\nimport { useContainer } from './useContainer'\nimport { useMaybeEditor } from './useEditor'\n\nfunction isScrollableOverflow(overflow: string) {\n\treturn overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay'\n}\n\n// An element only consumes a wheel event when it's a real scroll container: its content overflows\n// AND its computed overflow is scrollable. Checking scrollHeight/scrollWidth alone is not enough \u2014\n// an `overflow: visible` element whose content merely overflows (e.g. a comment pin whose hover\n// `transform: scale()` inflates scrollHeight past clientHeight) never scrolls, so the wheel must\n// still pass through to the canvas.\nfunction isScrollable(elm: HTMLElement) {\n\t// Cheap first: if nothing overflows, it can't scroll \u2014 skip the getComputedStyle read. This runs\n\t// per ancestor on every wheel event, so avoid resolving styles for the common (non-overflowing) case.\n\tconst overflowsY = elm.scrollHeight > elm.clientHeight\n\tconst overflowsX = elm.scrollWidth > elm.clientWidth\n\tif (!overflowsY && !overflowsX) return false\n\n\tconst style = getComputedStyle(elm)\n\treturn (\n\t\t(overflowsY && isScrollableOverflow(style.overflowY)) ||\n\t\t(overflowsX && isScrollableOverflow(style.overflowX))\n\t)\n}\n\n// Walk from the wheeled element up to (and including) the pass-through root, looking for a scroll\n// container. If one is found, the wheel belongs to it \u2014 don't redispatch it to the canvas.\nfunction hasScrollableElement(target: EventTarget | null, root: HTMLElement) {\n\tif (!(target instanceof Node)) {\n\t\treturn isScrollable(root)\n\t}\n\n\tlet elm: Element | null = target instanceof Element ? target : target.parentElement\n\twhile (elm) {\n\t\tif (elm instanceof HTMLElement && isScrollable(elm)) {\n\t\t\treturn true\n\t\t}\n\t\tif (elm === root) return false\n\t\telm = elm.parentElement\n\t}\n\n\treturn false\n}\n\n/** @public */\nexport function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {\n\tif (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')\n\tconst container = useContainer()\n\tconst editor = useMaybeEditor()\n\n\tuseEffect(() => {\n\t\tfunction onWheel(e: WheelEvent) {\n\t\t\t// Only pass through wheel events if the editor is focused\n\t\t\tif (!editor?.getInstanceState().isFocused) return\n\n\t\t\tif ((e as any).isSpecialRedispatchedEvent) return\n\n\t\t\t// If the wheel is over a scrollable element, let it scroll instead of redispatching.\n\t\t\tconst elm = ref.current\n\t\t\tif (elm && hasScrollableElement(e.target, elm)) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpreventDefault(e)\n\t\t\tconst cvs = container.querySelector('.tl-canvas')\n\t\t\tif (!cvs) return\n\t\t\tconst newEvent = new WheelEvent('wheel', e as any)\n\t\t\t;(newEvent as any).isSpecialRedispatchedEvent = true\n\t\t\tcvs.dispatchEvent(newEvent)\n\t\t}\n\n\t\tconst elm = ref.current\n\t\tif (!elm) return\n\n\t\telm.addEventListener('wheel', onWheel, { passive: false })\n\t\treturn () => {\n\t\t\telm.removeEventListener('wheel', onWheel)\n\t\t}\n\t}, [container, editor, ref])\n}\n"],
5
- "mappings": "AAAA,SAAoB,iBAAiB;AACrC,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAE/B,SAAS,qBAAqB,UAAkB;AAC/C,SAAO,aAAa,UAAU,aAAa,YAAY,aAAa;AACrE;AAOA,SAAS,aAAa,KAAkB;AAGvC,QAAM,aAAa,IAAI,eAAe,IAAI;AAC1C,QAAM,aAAa,IAAI,cAAc,IAAI;AACzC,MAAI,CAAC,cAAc,CAAC,WAAY,QAAO;AAEvC,QAAM,QAAQ,iBAAiB,GAAG;AAClC,SACE,cAAc,qBAAqB,MAAM,SAAS,KAClD,cAAc,qBAAqB,MAAM,SAAS;AAErD;AAIA,SAAS,qBAAqB,QAA4B,MAAmB;AAC5E,MAAI,EAAE,kBAAkB,OAAO;AAC9B,WAAO,aAAa,IAAI;AAAA,EACzB;AAEA,MAAI,MAAsB,kBAAkB,UAAU,SAAS,OAAO;AACtE,SAAO,KAAK;AACX,QAAI,eAAe,eAAe,aAAa,GAAG,GAAG;AACpD,aAAO;AAAA,IACR;AACA,QAAI,QAAQ,KAAM,QAAO;AACzB,UAAM,IAAI;AAAA,EACX;AAEA,SAAO;AACR;AAGO,SAAS,0BAA0B,KAAoC;AAC7E,MAAI,CAAC,IAAK,OAAM,MAAM,gDAAgD;AACtE,QAAM,YAAY,aAAa;AAC/B,QAAM,SAAS,eAAe;AAE9B,YAAU,MAAM;AACf,aAAS,QAAQ,GAAe;AAE/B,UAAI,CAAC,QAAQ,iBAAiB,EAAE,UAAW;AAE3C,UAAK,EAAU,2BAA4B;AAG3C,YAAMA,OAAM,IAAI;AAChB,UAAIA,QAAO,qBAAqB,EAAE,QAAQA,IAAG,GAAG;AAC/C;AAAA,MACD;AAEA,qBAAe,CAAC;AAChB,YAAM,MAAM,UAAU,cAAc,YAAY;AAChD,UAAI,CAAC,IAAK;AACV,YAAM,WAAW,IAAI,WAAW,SAAS,CAAQ;AAChD,MAAC,SAAiB,6BAA6B;AAChD,UAAI,cAAc,QAAQ;AAAA,IAC3B;AAEA,UAAM,MAAM,IAAI;AAChB,QAAI,CAAC,IAAK;AAEV,QAAI,iBAAiB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;AACzD,WAAO,MAAM;AACZ,UAAI,oBAAoB,SAAS,OAAO;AAAA,IACzC;AAAA,EACD,GAAG,CAAC,WAAW,QAAQ,GAAG,CAAC;AAC5B;",
6
- "names": ["elm"]
4
+ "sourcesContent": ["import { RefObject, useEffect, useRef } from 'react'\nimport { preventDefault } from '../utils/dom'\nimport { useContainer } from './useContainer'\nimport { useMaybeEditor } from './useEditor'\nimport { useEvent } from './useEvent'\n\nfunction isScrollableOverflow(overflow: string) {\n\treturn overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay'\n}\n\n// An element only consumes a wheel event when it's a real scroll container: its content overflows\n// AND its computed overflow is scrollable. Checking scrollHeight/scrollWidth alone is not enough \u2014\n// an `overflow: visible` element whose content merely overflows (e.g. a comment pin whose hover\n// `transform: scale()` inflates scrollHeight past clientHeight) never scrolls, so the wheel must\n// still pass through to the canvas.\nfunction isScrollable(elm: HTMLElement) {\n\t// Cheap first: if nothing overflows, it can't scroll \u2014 skip the getComputedStyle read. This runs\n\t// per ancestor on every wheel event, so avoid resolving styles for the common (non-overflowing) case.\n\tconst overflowsY = elm.scrollHeight > elm.clientHeight\n\tconst overflowsX = elm.scrollWidth > elm.clientWidth\n\tif (!overflowsY && !overflowsX) return false\n\n\tconst style = getComputedStyle(elm)\n\treturn (\n\t\t(overflowsY && isScrollableOverflow(style.overflowY)) ||\n\t\t(overflowsX && isScrollableOverflow(style.overflowX))\n\t)\n}\n\n// Walk from the wheeled element up to (and including) the pass-through root, looking for a scroll\n// container. If one is found, the wheel belongs to it \u2014 don't redispatch it to the canvas.\nfunction hasScrollableElement(target: EventTarget | null, root: HTMLElement) {\n\tif (!(target instanceof Node)) {\n\t\treturn isScrollable(root)\n\t}\n\n\tlet elm: Element | null = target instanceof Element ? target : target.parentElement\n\twhile (elm) {\n\t\tif (elm instanceof HTMLElement && isScrollable(elm)) {\n\t\t\treturn true\n\t\t}\n\t\tif (elm === root) return false\n\t\telm = elm.parentElement\n\t}\n\n\treturn false\n}\n\n/** @public */\nexport function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {\n\tif (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')\n\tconst container = useContainer()\n\tconst editor = useMaybeEditor()\n\n\tconst onWheel = useEvent((e: WheelEvent) => {\n\t\t// Only pass through wheel events if the editor is focused\n\t\tif (!editor?.getInstanceState().isFocused) return\n\n\t\tif ((e as any).isSpecialRedispatchedEvent) return\n\n\t\t// If the wheel is over a scrollable element, let it scroll instead of redispatching.\n\t\tconst elm = ref.current\n\t\tif (elm && hasScrollableElement(e.target, elm)) {\n\t\t\treturn\n\t\t}\n\n\t\tpreventDefault(e)\n\t\tconst cvs = container.querySelector('.tl-canvas')\n\t\tif (!cvs) return\n\t\tconst newEvent = new WheelEvent('wheel', e as any)\n\t\t;(newEvent as any).isSpecialRedispatchedEvent = true\n\t\tcvs.dispatchEvent(newEvent)\n\t})\n\n\t// The element the listener is currently on, which isn't necessarily `ref.current`: a `RefObject`\n\t// gives no notification when its element changes.\n\tconst attached = useRef<HTMLElement | null>(null)\n\n\t// Deliberately no dependency array \u2014 this re-checks the ref after every render of the component\n\t// that owns it, and re-attaches when the element has been swapped. A component that keeps the\n\t// hook mounted while unmounting and remounting the element it points at (a comment pin culled\n\t// while its anchor is off screen, say) would otherwise leave the listener on the discarded node\n\t// and silently lose pass-through once the element comes back.\n\tuseEffect(() => {\n\t\tconst elm = ref.current\n\t\tif (elm === attached.current) return\n\t\tattached.current?.removeEventListener('wheel', onWheel)\n\t\tattached.current = elm\n\t\telm?.addEventListener('wheel', onWheel, { passive: false })\n\t})\n\n\t// Teardown is its own effect so the re-attach check above can skip returning a cleanup that\n\t// would tear the listener down and rebuild it on every render.\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tattached.current?.removeEventListener('wheel', onWheel)\n\t\t\tattached.current = null\n\t\t}\n\t}, [onWheel])\n}\n"],
5
+ "mappings": "AAAA,SAAoB,WAAW,cAAc;AAC7C,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,gBAAgB;AAEzB,SAAS,qBAAqB,UAAkB;AAC/C,SAAO,aAAa,UAAU,aAAa,YAAY,aAAa;AACrE;AAOA,SAAS,aAAa,KAAkB;AAGvC,QAAM,aAAa,IAAI,eAAe,IAAI;AAC1C,QAAM,aAAa,IAAI,cAAc,IAAI;AACzC,MAAI,CAAC,cAAc,CAAC,WAAY,QAAO;AAEvC,QAAM,QAAQ,iBAAiB,GAAG;AAClC,SACE,cAAc,qBAAqB,MAAM,SAAS,KAClD,cAAc,qBAAqB,MAAM,SAAS;AAErD;AAIA,SAAS,qBAAqB,QAA4B,MAAmB;AAC5E,MAAI,EAAE,kBAAkB,OAAO;AAC9B,WAAO,aAAa,IAAI;AAAA,EACzB;AAEA,MAAI,MAAsB,kBAAkB,UAAU,SAAS,OAAO;AACtE,SAAO,KAAK;AACX,QAAI,eAAe,eAAe,aAAa,GAAG,GAAG;AACpD,aAAO;AAAA,IACR;AACA,QAAI,QAAQ,KAAM,QAAO;AACzB,UAAM,IAAI;AAAA,EACX;AAEA,SAAO;AACR;AAGO,SAAS,0BAA0B,KAAoC;AAC7E,MAAI,CAAC,IAAK,OAAM,MAAM,gDAAgD;AACtE,QAAM,YAAY,aAAa;AAC/B,QAAM,SAAS,eAAe;AAE9B,QAAM,UAAU,SAAS,CAAC,MAAkB;AAE3C,QAAI,CAAC,QAAQ,iBAAiB,EAAE,UAAW;AAE3C,QAAK,EAAU,2BAA4B;AAG3C,UAAM,MAAM,IAAI;AAChB,QAAI,OAAO,qBAAqB,EAAE,QAAQ,GAAG,GAAG;AAC/C;AAAA,IACD;AAEA,mBAAe,CAAC;AAChB,UAAM,MAAM,UAAU,cAAc,YAAY;AAChD,QAAI,CAAC,IAAK;AACV,UAAM,WAAW,IAAI,WAAW,SAAS,CAAQ;AAChD,IAAC,SAAiB,6BAA6B;AAChD,QAAI,cAAc,QAAQ;AAAA,EAC3B,CAAC;AAID,QAAM,WAAW,OAA2B,IAAI;AAOhD,YAAU,MAAM;AACf,UAAM,MAAM,IAAI;AAChB,QAAI,QAAQ,SAAS,QAAS;AAC9B,aAAS,SAAS,oBAAoB,SAAS,OAAO;AACtD,aAAS,UAAU;AACnB,SAAK,iBAAiB,SAAS,SAAS,EAAE,SAAS,MAAM,CAAC;AAAA,EAC3D,CAAC;AAID,YAAU,MAAM;AACf,WAAO,MAAM;AACZ,eAAS,SAAS,oBAAoB,SAAS,OAAO;AACtD,eAAS,UAAU;AAAA,IACpB;AAAA,EACD,GAAG,CAAC,OAAO,CAAC;AACb;",
6
+ "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
- const version = "5.3.0-next.dec36c5c2930";
1
+ const version = "5.3.0-next.df310c6196c0";
2
2
  const publishDates = {
3
3
  major: "2026-05-06T16:28:18.473Z",
4
- minor: "2026-08-05T10:19:16.457Z",
5
- patch: "2026-08-05T10:19:16.457Z"
4
+ minor: "2026-08-05T11:28:43.788Z",
5
+ patch: "2026-08-05T11:28:43.788Z"
6
6
  };
7
7
  export {
8
8
  publishDates,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/version.ts"],
4
- "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.3.0-next.dec36c5c2930'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-05T10:19:16.457Z',\n\tpatch: '2026-08-05T10:19:16.457Z',\n}\n"],
4
+ "sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.3.0-next.df310c6196c0'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-05T11:28:43.788Z',\n\tpatch: '2026-08-05T11:28:43.788Z',\n}\n"],
5
5
  "mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "tldraw infinite canvas SDK (editor).",
4
- "version": "5.3.0-next.dec36c5c2930",
4
+ "version": "5.3.0-next.df310c6196c0",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -49,12 +49,12 @@
49
49
  "@tiptap/core": "^3.12.1",
50
50
  "@tiptap/pm": "^3.12.1",
51
51
  "@tiptap/react": "^3.12.1",
52
- "@tldraw/state": "5.3.0-next.dec36c5c2930",
53
- "@tldraw/state-react": "5.3.0-next.dec36c5c2930",
54
- "@tldraw/store": "5.3.0-next.dec36c5c2930",
55
- "@tldraw/tlschema": "5.3.0-next.dec36c5c2930",
56
- "@tldraw/utils": "5.3.0-next.dec36c5c2930",
57
- "@tldraw/validate": "5.3.0-next.dec36c5c2930",
52
+ "@tldraw/state": "5.3.0-next.df310c6196c0",
53
+ "@tldraw/state-react": "5.3.0-next.df310c6196c0",
54
+ "@tldraw/store": "5.3.0-next.df310c6196c0",
55
+ "@tldraw/tlschema": "5.3.0-next.df310c6196c0",
56
+ "@tldraw/utils": "5.3.0-next.df310c6196c0",
57
+ "@tldraw/validate": "5.3.0-next.df310c6196c0",
58
58
  "classnames": "^2.5.1",
59
59
  "eventemitter3": "^4.0.7",
60
60
  "idb": "^7.1.1",
@@ -1,7 +1,8 @@
1
- import { RefObject, useEffect } from 'react'
1
+ import { RefObject, useEffect, useRef } from 'react'
2
2
  import { preventDefault } from '../utils/dom'
3
3
  import { useContainer } from './useContainer'
4
4
  import { useMaybeEditor } from './useEditor'
5
+ import { useEvent } from './useEvent'
5
6
 
6
7
  function isScrollableOverflow(overflow: string) {
7
8
  return overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay'
@@ -51,33 +52,49 @@ export function usePassThroughWheelEvents(ref: RefObject<HTMLElement | null>) {
51
52
  const container = useContainer()
52
53
  const editor = useMaybeEditor()
53
54
 
54
- useEffect(() => {
55
- function onWheel(e: WheelEvent) {
56
- // Only pass through wheel events if the editor is focused
57
- if (!editor?.getInstanceState().isFocused) return
58
-
59
- if ((e as any).isSpecialRedispatchedEvent) return
55
+ const onWheel = useEvent((e: WheelEvent) => {
56
+ // Only pass through wheel events if the editor is focused
57
+ if (!editor?.getInstanceState().isFocused) return
60
58
 
61
- // If the wheel is over a scrollable element, let it scroll instead of redispatching.
62
- const elm = ref.current
63
- if (elm && hasScrollableElement(e.target, elm)) {
64
- return
65
- }
59
+ if ((e as any).isSpecialRedispatchedEvent) return
66
60
 
67
- preventDefault(e)
68
- const cvs = container.querySelector('.tl-canvas')
69
- if (!cvs) return
70
- const newEvent = new WheelEvent('wheel', e as any)
71
- ;(newEvent as any).isSpecialRedispatchedEvent = true
72
- cvs.dispatchEvent(newEvent)
61
+ // If the wheel is over a scrollable element, let it scroll instead of redispatching.
62
+ const elm = ref.current
63
+ if (elm && hasScrollableElement(e.target, elm)) {
64
+ return
73
65
  }
74
66
 
67
+ preventDefault(e)
68
+ const cvs = container.querySelector('.tl-canvas')
69
+ if (!cvs) return
70
+ const newEvent = new WheelEvent('wheel', e as any)
71
+ ;(newEvent as any).isSpecialRedispatchedEvent = true
72
+ cvs.dispatchEvent(newEvent)
73
+ })
74
+
75
+ // The element the listener is currently on, which isn't necessarily `ref.current`: a `RefObject`
76
+ // gives no notification when its element changes.
77
+ const attached = useRef<HTMLElement | null>(null)
78
+
79
+ // Deliberately no dependency array — this re-checks the ref after every render of the component
80
+ // that owns it, and re-attaches when the element has been swapped. A component that keeps the
81
+ // hook mounted while unmounting and remounting the element it points at (a comment pin culled
82
+ // while its anchor is off screen, say) would otherwise leave the listener on the discarded node
83
+ // and silently lose pass-through once the element comes back.
84
+ useEffect(() => {
75
85
  const elm = ref.current
76
- if (!elm) return
86
+ if (elm === attached.current) return
87
+ attached.current?.removeEventListener('wheel', onWheel)
88
+ attached.current = elm
89
+ elm?.addEventListener('wheel', onWheel, { passive: false })
90
+ })
77
91
 
78
- elm.addEventListener('wheel', onWheel, { passive: false })
92
+ // Teardown is its own effect so the re-attach check above can skip returning a cleanup that
93
+ // would tear the listener down and rebuild it on every render.
94
+ useEffect(() => {
79
95
  return () => {
80
- elm.removeEventListener('wheel', onWheel)
96
+ attached.current?.removeEventListener('wheel', onWheel)
97
+ attached.current = null
81
98
  }
82
- }, [container, editor, ref])
99
+ }, [onWheel])
83
100
  }
@@ -0,0 +1,77 @@
1
+ import { act, cleanup, render } from '@testing-library/react'
2
+ import { useRef, useState } from 'react'
3
+ import { createTLStore } from '../config/createTLStore'
4
+ import { usePassThroughWheelEvents } from '../hooks/usePassThroughWheelEvents'
5
+ import { TL_CONTAINER_CLASS, TldrawEditor } from '../TldrawEditor'
6
+
7
+ let setVisible: (visible: boolean) => void
8
+ let rerender: () => void
9
+
10
+ /**
11
+ * The shape a comment pin has: the hook's owner stays mounted while the element the ref points at
12
+ * is culled and later remounted (the pin returns null when its anchor leaves the viewport).
13
+ */
14
+ function Culled() {
15
+ const ref = useRef<HTMLButtonElement>(null)
16
+ usePassThroughWheelEvents(ref)
17
+ const [visible, _setVisible] = useState(true)
18
+ const [, setTick] = useState(0)
19
+ setVisible = _setVisible
20
+ rerender = () => setTick((t) => t + 1)
21
+ if (!visible) return null
22
+ return <button ref={ref} data-testid="marker" />
23
+ }
24
+
25
+ describe('usePassThroughWheelEvents', () => {
26
+ async function renderEditor() {
27
+ const store = createTLStore({ shapeUtils: [], bindingUtils: [] })
28
+ await act(async () => {
29
+ render(<TldrawEditor store={store} components={{ InFrontOfTheCanvas: Culled }} />)
30
+ })
31
+ return document.querySelector(`.${TL_CONTAINER_CLASS}`)!
32
+ }
33
+
34
+ function wheelOverMarker(container: Element) {
35
+ const canvas = container.querySelector('.tl-canvas')!
36
+ const received: Event[] = []
37
+ canvas.addEventListener('wheel', (e) => received.push(e))
38
+ const marker = container.querySelector('[data-testid="marker"]')!
39
+ marker.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaY: 10 }))
40
+ return received
41
+ }
42
+
43
+ it('redispatches wheel events over the element to the canvas', async () => {
44
+ const container = await renderEditor()
45
+ expect(wheelOverMarker(container)).toHaveLength(1)
46
+ })
47
+
48
+ // The regression: the owner stays mounted across the cull, so an effect keyed on its deps never
49
+ // re-runs and the listener is left on the discarded element.
50
+ it('still redispatches after the element unmounts and remounts', async () => {
51
+ const container = await renderEditor()
52
+ await act(async () => setVisible(false))
53
+ await act(async () => setVisible(true))
54
+ expect(wheelOverMarker(container)).toHaveLength(1)
55
+ })
56
+
57
+ // The re-attach check runs on every render, so it has to be a no-op when the element hasn't
58
+ // changed — otherwise each render stacks another listener and one wheel redispatches many.
59
+ it('does not stack listeners across renders that leave the element in place', async () => {
60
+ const container = await renderEditor()
61
+ for (let i = 0; i < 3; i++) {
62
+ await act(async () => rerender())
63
+ }
64
+ expect(wheelOverMarker(container)).toHaveLength(1)
65
+ })
66
+
67
+ it('stops redispatching once the owner unmounts', async () => {
68
+ const container = await renderEditor()
69
+ const marker = container.querySelector('[data-testid="marker"]')!
70
+ const canvas = container.querySelector('.tl-canvas')!
71
+ const received: Event[] = []
72
+ canvas.addEventListener('wheel', (e) => received.push(e))
73
+ await act(async () => cleanup())
74
+ marker.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaY: 10 }))
75
+ expect(received).toHaveLength(0)
76
+ })
77
+ })
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '5.3.0-next.dec36c5c2930'
4
+ export const version = '5.3.0-next.df310c6196c0'
5
5
  export const publishDates = {
6
6
  major: '2026-05-06T16:28:18.473Z',
7
- minor: '2026-08-05T10:19:16.457Z',
8
- patch: '2026-08-05T10:19:16.457Z',
7
+ minor: '2026-08-05T11:28:43.788Z',
8
+ patch: '2026-08-05T11:28:43.788Z',
9
9
  }