From b47fb729ee1bcb5706598cd38cae1397ab46d288 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Thu, 2 Jun 2022 01:48:48 +0800 Subject: [PATCH] fix: Pointer position is incorrect if Tldraw is drawing in a scrolling g container (#706) * fix: Pointer position is incorrect if Tldraw is drawing in a scrolling container fix https://github.com/tldraw/tldraw/issues/661 * Add example for scrolling Co-authored-by: Steve Ruiz --- examples/tldraw-example/src/app.tsx | 124 +++++++------------ examples/tldraw-example/src/scroll.tsx | 22 ++++ packages/core/src/hooks/useResizeObserver.ts | 28 ++++- 3 files changed, 90 insertions(+), 84 deletions(-) create mode 100644 examples/tldraw-example/src/scroll.tsx diff --git a/examples/tldraw-example/src/app.tsx b/examples/tldraw-example/src/app.tsx index 21b68145a..610cd7be1 100644 --- a/examples/tldraw-example/src/app.tsx +++ b/examples/tldraw-example/src/app.tsx @@ -12,6 +12,7 @@ import ChangingId from './changing-id' import Persisted from './persisted' import Develop from './develop' import Api from './api' +import Scroll from './scroll' import FileSystem from './file-system' import UIOptions from './ui-options' import { Multiplayer } from './multiplayer' @@ -19,98 +20,59 @@ import { Multiplayer as MultiplayerWithImages } from './multiplayer-with-images' import './styles.css' import Export from '~export' +const pages: ({ path: string; component: any; title: string } | '---')[] = [ + { path: '/develop', component: Develop, title: 'Develop' }, + '---', + { path: '/basic', component: Basic, title: 'Basic' }, + { path: '/dark-mode', component: DarkMode, title: 'Dark mode' }, + { path: '/ui-options', component: UIOptions, title: 'Customizing user interfcace' }, + { path: '/persisted', component: Persisted, title: 'Persisting state with an ID' }, + { path: '/loading-files', component: LoadingFiles, title: 'Using the file system' }, + { path: '/file-system', component: FileSystem, title: 'Loading files' }, + { path: '/api', component: Api, title: 'Using the TldrawApp API' }, + { path: '/readonly', component: ReadOnly, title: 'Readonly mode' }, + { path: '/controlled', component: PropsControl, title: 'Controlled via props' }, + { path: '/imperative', component: ApiControl, title: 'Controlled via the TldrawApp API' }, + { path: '/changing-id', component: ChangingId, title: 'Changing ID' }, + { path: '/embedded', component: Embedded, title: 'Embedded' }, + { + path: '/no-size-embedded', + component: NoSizeEmbedded, + title: 'Embedded (without explicit size)', + }, + { path: '/export', component: Export, title: 'Export' }, + { path: '/scroll', component: Scroll, title: 'In a scrolling container' }, + { path: '/multiplayer', component: Multiplayer, title: 'Multiplayer' }, + { + path: '/multiplayer-with-images', + component: MultiplayerWithImages, + title: 'Multiplayer (with images)', + }, +] + export default function App() { return (
- } /> + {pages.map((page) => + page === '---' ? null : } /> + )} - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - -
    -
  • - Develop -
  • -
    -
  • - Basic -
  • -
  • - Dark Mode -
  • -
  • - UI Options -
  • -
  • - Persisting State with an ID -
  • -
  • - Using the File System -
  • -
  • - Readonly Mode -
  • -
  • - Loading Files -
  • -
  • - Controlled via Props -
  • -
  • - Using the TldrawApp API -
  • -
  • - Controlled via TldrawApp API -
  • -
  • - Changing ID -
  • -
  • - Embedded -
  • -
  • - Embedded (without explicit size) -
  • -
  • - Export -
  • -
  • - Multiplayer -
  • + {pages.map((page, i) => + page === '---' ? ( +
    + ) : ( +
  • + {page.title} +
  • + ) + )}
} diff --git a/examples/tldraw-example/src/scroll.tsx b/examples/tldraw-example/src/scroll.tsx new file mode 100644 index 000000000..d3c1fa3a9 --- /dev/null +++ b/examples/tldraw-example/src/scroll.tsx @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as React from 'react' +import { Tldraw, TldrawApp } from '@tldraw/tldraw' + +declare const window: Window & { app: TldrawApp } + +export default function Scroll() { + const rTldrawApp = React.useRef() + + const handleMount = React.useCallback((app: TldrawApp) => { + window.app = app + rTldrawApp.current = app + }, []) + + return ( +
+
+ +
+
+ ) +} diff --git a/packages/core/src/hooks/useResizeObserver.ts b/packages/core/src/hooks/useResizeObserver.ts index 1c6eb1ff6..8c010e2e2 100644 --- a/packages/core/src/hooks/useResizeObserver.ts +++ b/packages/core/src/hooks/useResizeObserver.ts @@ -4,7 +4,28 @@ import * as React from 'react' import { Utils } from '../utils' import type { TLBounds } from '../types' -export function useResizeObserver( +// Credits: from excalidraw +// https://github.com/excalidraw/excalidraw/blob/07ebd7c68ce6ff92ddbc22d1c3d215f2b21328d6/src/utils.ts#L542-L563 +const getNearestScrollableContainer = (element: HTMLElement): HTMLElement | Document => { + let parent = element.parentElement + while (parent) { + if (parent === document.body) { + return document + } + const { overflowY } = window.getComputedStyle(parent) + const hasScrollableContent = parent.scrollHeight > parent.clientHeight + if ( + hasScrollableContent && + (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') + ) { + return parent + } + parent = parent.parentElement + } + return document +} + +export function useResizeObserver( ref: React.RefObject, onBoundsChange: (bounds: TLBounds) => void ) { @@ -41,11 +62,12 @@ export function useResizeObserver( }, [ref, inputs, callbacks.onBoundsChange]) React.useEffect(() => { + const scrollingAnchor = ref.current ? getNearestScrollableContainer(ref.current) : document const debouncedupdateBounds = Utils.debounce(updateBounds, 100) - window.addEventListener('scroll', debouncedupdateBounds) + scrollingAnchor.addEventListener('scroll', debouncedupdateBounds) window.addEventListener('resize', debouncedupdateBounds) return () => { - window.removeEventListener('scroll', debouncedupdateBounds) + scrollingAnchor.removeEventListener('scroll', debouncedupdateBounds) window.removeEventListener('resize', debouncedupdateBounds) } }, [])