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:
parent
b3ad319518
commit
b47fb729ee
3 changed files with 90 additions and 84 deletions
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
22
examples/tldraw-example/src/scroll.tsx
Normal file
22
examples/tldraw-example/src/scroll.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
Loading…
Reference in a new issue