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 <steveruizok@gmail.com>
This commit is contained in:
Peng Xiao 2022-06-02 01:48:48 +08:00 committed by GitHub
parent b3ad319518
commit b47fb729ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 84 deletions

View file

@ -12,6 +12,7 @@ import ChangingId from './changing-id'
import Persisted from './persisted' import Persisted from './persisted'
import Develop from './develop' import Develop from './develop'
import Api from './api' import Api from './api'
import Scroll from './scroll'
import FileSystem from './file-system' import FileSystem from './file-system'
import UIOptions from './ui-options' import UIOptions from './ui-options'
import { Multiplayer } from './multiplayer' import { Multiplayer } from './multiplayer'
@ -19,98 +20,59 @@ import { Multiplayer as MultiplayerWithImages } from './multiplayer-with-images'
import './styles.css' import './styles.css'
import Export from '~export' 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() { export default function App() {
return ( return (
<main> <main>
<Routes> <Routes>
<Route path="/develop" element={<Develop />} /> {pages.map((page) =>
page === '---' ? null : <Route path={page.path} element={<page.component />} />
)}
<Route path="/basic" element={<Basic />} />
<Route path="/dark-mode" element={<DarkMode />} />
<Route path="/ui-options" element={<UIOptions />} />
<Route path="/persisted" element={<Persisted />} />
<Route path="/loading-files" element={<LoadingFiles />} />
<Route path="/file-system" element={<FileSystem />} />
<Route path="/api" element={<Api />} />
<Route path="/readonly" element={<ReadOnly />} />
<Route path="/controlled" element={<PropsControl />} />
<Route path="/imperative" element={<ApiControl />} />
<Route path="/changing-id" element={<ChangingId />} />
<Route path="/embedded" element={<Embedded />} />
<Route path="/no-size-embedded" element={<NoSizeEmbedded />} />
<Route path="/export" element={<Export />} />
<Route path="/multiplayer" element={<Multiplayer />} />
<Route path="/multiplayer-with-images" element={MultiplayerWithImages} />
<Route <Route
path="/" path="/"
element={ element={
<div> <div>
<img className="hero" src="./card-repo.png" /> <img className="hero" src="./card-repo.png" />
<ul className="links"> <ul className="links">
<li> {pages.map((page, i) =>
<Link to="/develop">Develop</Link> page === '---' ? (
</li> <hr key={i} />
<hr /> ) : (
<li> <li key={i}>
<Link to="/basic">Basic</Link> <Link to={page.path}>{page.title}</Link>
</li> </li>
<li> )
<Link to="/dark-mode">Dark Mode</Link> )}
</li>
<li>
<Link to="/ui-options">UI Options</Link>
</li>
<li>
<Link to="/persisted">Persisting State with an ID</Link>
</li>
<li>
<Link to="/file-system">Using the File System</Link>
</li>
<li>
<Link to="/readonly">Readonly Mode</Link>
</li>
<li>
<Link to="/loading-files">Loading Files</Link>
</li>
<li>
<Link to="/controlled">Controlled via Props</Link>
</li>
<li>
<Link to="/api">Using the TldrawApp API</Link>
</li>
<li>
<Link to="/imperative">Controlled via TldrawApp API</Link>
</li>
<li>
<Link to="/changing-id">Changing ID</Link>
</li>
<li>
<Link to="/embedded">Embedded</Link>
</li>
<li>
<Link to="/no-size-embedded">Embedded (without explicit size)</Link>
</li>
<li>
<Link to="/export">Export</Link>
</li>
<li>
<Link to="/multiplayer">Multiplayer</Link>
</li>
</ul> </ul>
</div> </div>
} }

View file

@ -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<TldrawApp>()
const handleMount = React.useCallback((app: TldrawApp) => {
window.app = app
rTldrawApp.current = app
}, [])
return (
<div style={{ height: 1600, width: 1600, padding: 200 }}>
<div style={{ width: '100%', height: '100%', position: 'relative' }}>
<Tldraw id="develop" onMount={handleMount} showSponsorLink={true} />
</div>
</div>
)
}

View file

@ -4,7 +4,28 @@ import * as React from 'react'
import { Utils } from '../utils' import { Utils } from '../utils'
import type { TLBounds } from '../types' import type { TLBounds } from '../types'
export function useResizeObserver<T extends Element>( // 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<T extends HTMLElement>(
ref: React.RefObject<T>, ref: React.RefObject<T>,
onBoundsChange: (bounds: TLBounds) => void onBoundsChange: (bounds: TLBounds) => void
) { ) {
@ -41,11 +62,12 @@ export function useResizeObserver<T extends Element>(
}, [ref, inputs, callbacks.onBoundsChange]) }, [ref, inputs, callbacks.onBoundsChange])
React.useEffect(() => { React.useEffect(() => {
const scrollingAnchor = ref.current ? getNearestScrollableContainer(ref.current) : document
const debouncedupdateBounds = Utils.debounce(updateBounds, 100) const debouncedupdateBounds = Utils.debounce(updateBounds, 100)
window.addEventListener('scroll', debouncedupdateBounds) scrollingAnchor.addEventListener('scroll', debouncedupdateBounds)
window.addEventListener('resize', debouncedupdateBounds) window.addEventListener('resize', debouncedupdateBounds)
return () => { return () => {
window.removeEventListener('scroll', debouncedupdateBounds) scrollingAnchor.removeEventListener('scroll', debouncedupdateBounds)
window.removeEventListener('resize', debouncedupdateBounds) window.removeEventListener('resize', debouncedupdateBounds)
} }
}, []) }, [])