[fix] Focus events (actually) (#2015)
This PR restores the controlled nature of focus. Focus allows keyboard shortcuts and other interactions to occur. The editor's focus should always / entirely be controlled via the autoFocus prop or by manually setting `editor.instanceState.isFocused`. Design note: I'm starting to think that focus is the wrong abstraction, and that we should instead use a kind of "disabled" state for editors that the user isn't interacting with directly. In a page where multiple editors exit (e.g. a notion page), a developer could switch from disabled to enabled using a first interaction. ### Change Type - [x] `patch` — Bug fix ### Test Plan - [x] End to end tests
This commit is contained in:
parent
6b19d70a9e
commit
d715fa3a2e
11 changed files with 236 additions and 122 deletions
|
@ -1,6 +1,9 @@
|
|||
import test, { expect } from '@playwright/test'
|
||||
import { Editor } from '@tldraw/tldraw'
|
||||
|
||||
declare const __tldraw_editor_events: any[]
|
||||
declare const EDITOR_A: Editor
|
||||
declare const EDITOR_B: Editor
|
||||
declare const EDITOR_C: Editor
|
||||
|
||||
// We're just testing the events, not the actual results.
|
||||
|
||||
|
@ -9,42 +12,98 @@ test.describe('Focus', () => {
|
|||
await page.goto('http://localhost:5420/multiple')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
// Component A has autofocus
|
||||
// Component B does not
|
||||
|
||||
const EditorA = (await page.$(`.A`))!
|
||||
const EditorB = (await page.$(`.B`))!
|
||||
const EditorC = (await page.$(`.C`))!
|
||||
expect(EditorA).toBeTruthy()
|
||||
expect(EditorB).toBeTruthy()
|
||||
expect(EditorC).toBeTruthy()
|
||||
|
||||
async function isOnlyFocused(id: 'A' | 'B' | 'C' | null) {
|
||||
let activeElement: string | null = null
|
||||
const isA = await EditorA.evaluate(
|
||||
(node) => document.activeElement === node || node.contains(document.activeElement)
|
||||
)
|
||||
const isB = await EditorB.evaluate(
|
||||
(node) => document.activeElement === node || node.contains(document.activeElement)
|
||||
)
|
||||
|
||||
const isC = await EditorC.evaluate(
|
||||
(node) => document.activeElement === node || node.contains(document.activeElement)
|
||||
)
|
||||
|
||||
activeElement = isA ? 'A' : isB ? 'B' : isC ? 'C' : null
|
||||
|
||||
expect(
|
||||
activeElement,
|
||||
`Active element should have been ${id}, but was ${activeElement ?? 'null'} instead`
|
||||
).toBe(id)
|
||||
|
||||
await page.evaluate(
|
||||
({ id }) => {
|
||||
if (
|
||||
!(
|
||||
EDITOR_A.instanceState.isFocused === (id === 'A') &&
|
||||
EDITOR_B.instanceState.isFocused === (id === 'B') &&
|
||||
EDITOR_C.instanceState.isFocused === (id === 'C')
|
||||
)
|
||||
) {
|
||||
throw Error('isFocused is not correct')
|
||||
}
|
||||
},
|
||||
{ id }
|
||||
)
|
||||
}
|
||||
|
||||
// Component A has autofocus
|
||||
// Component B does not
|
||||
// Component C does not
|
||||
// Component B and C share persistence id
|
||||
|
||||
await (await page.$('body'))?.click()
|
||||
|
||||
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(true)
|
||||
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
|
||||
await (await page.$('body'))?.click()
|
||||
|
||||
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
await isOnlyFocused('A')
|
||||
|
||||
await EditorA.click()
|
||||
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(true)
|
||||
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
|
||||
await isOnlyFocused('A')
|
||||
|
||||
await EditorA.click()
|
||||
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
expect(await EditorA.evaluate((node) => node.contains(document.activeElement))).toBe(true)
|
||||
|
||||
await isOnlyFocused('A')
|
||||
|
||||
await EditorB.click()
|
||||
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(true)
|
||||
expect(await EditorA.evaluate((node) => node.contains(document.activeElement))).toBe(false)
|
||||
|
||||
await isOnlyFocused('B')
|
||||
|
||||
// Escape does not break focus
|
||||
await page.keyboard.press('Escape')
|
||||
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
expect(await EditorB.evaluate((node) => node.contains(document.activeElement))).toBe(true)
|
||||
|
||||
await isOnlyFocused('B')
|
||||
})
|
||||
|
||||
test('kbds when not focused', async ({ page }) => {
|
||||
await page.goto('http://localhost:5420/multiple')
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
|
||||
// Should not have any shapes on the page
|
||||
expect(await page.evaluate(() => EDITOR_A.currentPageShapes.length)).toBe(0)
|
||||
|
||||
const EditorA = (await page.$(`.A`))!
|
||||
await page.keyboard.press('r')
|
||||
await EditorA.click({ position: { x: 100, y: 100 } })
|
||||
|
||||
// Should not have created a shape
|
||||
expect(await page.evaluate(() => EDITOR_A.currentPageShapes.length)).toBe(1)
|
||||
|
||||
const TextArea = page.getByTestId(`textarea`)
|
||||
await TextArea.focus()
|
||||
await page.keyboard.type('hello world')
|
||||
await page.keyboard.press('Control+A')
|
||||
await page.keyboard.press('Delete')
|
||||
|
||||
// Should not have deleted the page
|
||||
expect(await page.evaluate(() => EDITOR_A.currentPageShapes.length)).toBe(1)
|
||||
})
|
||||
|
||||
test('kbds when focused', async ({ page }) => {
|
||||
|
@ -53,14 +112,13 @@ test.describe('Focus', () => {
|
|||
|
||||
const EditorA = (await page.$(`.A`))!
|
||||
const EditorB = (await page.$(`.B`))!
|
||||
const EditorC = (await page.$(`.C`))!
|
||||
expect(EditorA).toBeTruthy()
|
||||
expect(EditorB).toBeTruthy()
|
||||
expect(EditorC).toBeTruthy()
|
||||
|
||||
await (await page.$('body'))?.click()
|
||||
|
||||
expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(true)
|
||||
expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false)
|
||||
|
||||
expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe(
|
||||
null
|
||||
)
|
||||
|
|
|
@ -4,18 +4,18 @@ import { createContext, useCallback, useContext, useState } from 'react'
|
|||
|
||||
const focusedEditorContext = createContext(
|
||||
{} as {
|
||||
focusedEditor: string
|
||||
setFocusedEditor: (id: string) => void
|
||||
focusedEditor: string | null
|
||||
setFocusedEditor: (id: string | null) => void
|
||||
}
|
||||
)
|
||||
|
||||
export default function MultipleExample() {
|
||||
const [focusedEditor, focusedEditorSetter] = useState('first')
|
||||
const [focusedEditor, _setFocusedEditor] = useState<string | null>('A')
|
||||
|
||||
const setFocusedEditor = useCallback(
|
||||
(id: string) => {
|
||||
(id: string | null) => {
|
||||
if (focusedEditor !== id) {
|
||||
focusedEditorSetter(id)
|
||||
_setFocusedEditor(id)
|
||||
}
|
||||
},
|
||||
[focusedEditor]
|
||||
|
@ -29,47 +29,88 @@ export default function MultipleExample() {
|
|||
}}
|
||||
>
|
||||
<focusedEditorContext.Provider value={{ focusedEditor, setFocusedEditor }}>
|
||||
<h1>Focusing: "{focusedEditor}"</h1>
|
||||
<FirstEditor />
|
||||
<textarea defaultValue="type in me" style={{ margin: 10 }}></textarea>
|
||||
<SecondEditor />
|
||||
<h1>Focusing: {focusedEditor ?? 'none'}</h1>
|
||||
<EditorA />
|
||||
<textarea data-testid="textarea" placeholder="type in me" style={{ margin: 10 }} />
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(420px, 1fr))',
|
||||
gap: 64,
|
||||
}}
|
||||
>
|
||||
<EditorB />
|
||||
<EditorC />
|
||||
</div>
|
||||
<p>
|
||||
These two editors share the same persistence key so they will share a (locally)
|
||||
synchronized document.
|
||||
</p>
|
||||
<ABunchOfText />
|
||||
</focusedEditorContext.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FirstEditor() {
|
||||
function EditorA() {
|
||||
const { focusedEditor, setFocusedEditor } = useContext(focusedEditorContext)
|
||||
const isFocused = focusedEditor === 'A'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>First Example</h2>
|
||||
<p>This is the second example.</p>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onFocus={() => setFocusedEditor('first')}
|
||||
style={{ width: '100%', height: '600px', padding: 32 }}
|
||||
>
|
||||
<Tldraw persistenceKey="steve" className="A" autoFocus={focusedEditor === 'first'} />
|
||||
<div style={{ padding: 32 }}>
|
||||
<h2>A</h2>
|
||||
<div tabIndex={-1} onFocus={() => setFocusedEditor('A')} style={{ height: 600 }}>
|
||||
<Tldraw
|
||||
persistenceKey="steve"
|
||||
className="A"
|
||||
autoFocus={isFocused}
|
||||
onMount={(editor) => {
|
||||
;(window as any).EDITOR_A = editor
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SecondEditor() {
|
||||
function EditorB() {
|
||||
const { focusedEditor, setFocusedEditor } = useContext(focusedEditorContext)
|
||||
const isFocused = focusedEditor === 'B'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Second Example</h2>
|
||||
<p>This is the second example.</p>
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onFocus={() => setFocusedEditor('second')}
|
||||
style={{ width: '100%', height: '600px' }}
|
||||
>
|
||||
<Tldraw persistenceKey="david" className="B" autoFocus={focusedEditor === 'second'} />
|
||||
<h2>B</h2>
|
||||
<div tabIndex={-1} onFocus={() => setFocusedEditor('B')} style={{ height: 600 }}>
|
||||
<Tldraw
|
||||
persistenceKey="david"
|
||||
className="B"
|
||||
autoFocus={isFocused}
|
||||
onMount={(editor) => {
|
||||
;(window as any).EDITOR_B = editor
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EditorC() {
|
||||
const { focusedEditor, setFocusedEditor } = useContext(focusedEditorContext)
|
||||
const isFocused = focusedEditor === 'third'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>C</h2>
|
||||
<div tabIndex={-1} onFocus={() => setFocusedEditor('C')} style={{ height: 600 }}>
|
||||
<Tldraw
|
||||
persistenceKey="david"
|
||||
className="C"
|
||||
autoFocus={isFocused}
|
||||
onMount={(editor) => {
|
||||
;(window as any).EDITOR_C = editor
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -77,46 +118,44 @@ function SecondEditor() {
|
|||
|
||||
function ABunchOfText() {
|
||||
return (
|
||||
<div style={{ width: '100%', display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
||||
<article style={{ maxWidth: 600 }}>
|
||||
<h1>White Board</h1>
|
||||
<h2>Chapter 1: The First Strokes</h2>
|
||||
<p>
|
||||
The fluorescent lights flickered overhead as John sat hunched over his desk, his fingers
|
||||
tapping rhythmically on the keyboard. He was a software developer, and tonight, he had a
|
||||
peculiar mission. A mission that would take him deep into the labyrinthine world of web
|
||||
development. John had stumbled upon a new whiteboard library called "tldraw," a seemingly
|
||||
simple tool that promised to revolutionize collaborative drawing on the web. Little did he
|
||||
know that this discovery would set off a chain of events that would challenge his skills,
|
||||
test his perseverance, and blur the line between reality and imagination.
|
||||
</p>
|
||||
<p>
|
||||
With a newfound sense of excitement, John began integrating "tldraw" into his latest
|
||||
project. As lines of code danced across his screen, he imagined the possibilities that lay
|
||||
ahead. The potential to create virtual spaces where ideas could be shared, concepts could
|
||||
be visualized, and teams could collaborate seamlessly from different corners of the world.
|
||||
It was a dream that seemed within reach, a vision of a future where creativity and
|
||||
technology merged into a harmonious symphony.
|
||||
</p>
|
||||
<p>
|
||||
As the night wore on, John's mind became consumed with the whiteboard library. He couldn't
|
||||
help but marvel at its elegance and simplicity. With each stroke of his keyboard, he felt
|
||||
a surge of inspiration, a connection to something greater than himself. It was as if the
|
||||
lines of code he was writing were transforming into a digital canvas, waiting to be filled
|
||||
with the strokes of imagination. In that moment, John realized that he was not just
|
||||
building a tool, but breathing life into a new form of expression. The whiteboard was no
|
||||
longer just a blank slate; it had become a portal to a world where ideas could flourish
|
||||
and dreams could take shape.
|
||||
</p>
|
||||
<p>
|
||||
Little did John know, this integration of "tldraw" was only the beginning. It would lead
|
||||
him down a path filled with unforeseen challenges, where he would confront his own
|
||||
limitations and question the very nature of creation. The journey ahead would test his
|
||||
resolve, pushing him to the edge of his sanity. And as he embarked on this perilous
|
||||
adventure, he could not shake the feeling that the whiteboard held secrets far beyond his
|
||||
understanding. Secrets that would unfold before his eyes, one stroke at a time.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
<article style={{ maxWidth: 500 }}>
|
||||
<h1>White Board</h1>
|
||||
<h2>Chapter 1: The First Strokes</h2>
|
||||
<p>
|
||||
The fluorescent lights flickered overhead as John sat hunched over his desk, his fingers
|
||||
tapping rhythmically on the keyboard. He was a software developer, and tonight, he had a
|
||||
peculiar mission. A mission that would take him deep into the labyrinthine world of web
|
||||
development. John had stumbled upon a new whiteboard library called "tldraw," a seemingly
|
||||
simple tool that promised to revolutionize collaborative drawing on the web. Little did he
|
||||
know that this discovery would set off a chain of events that would challenge his skills,
|
||||
test his perseverance, and blur the line between reality and imagination.
|
||||
</p>
|
||||
<p>
|
||||
With a newfound sense of excitement, John began integrating "tldraw" into his latest
|
||||
project. As lines of code danced across his screen, he imagined the possibilities that lay
|
||||
ahead. The potential to create virtual spaces where ideas could be shared, concepts could be
|
||||
visualized, and teams could collaborate seamlessly from different corners of the world. It
|
||||
was a dream that seemed within reach, a vision of a future where creativity and technology
|
||||
merged into a harmonious symphony.
|
||||
</p>
|
||||
<p>
|
||||
As the night wore on, John's mind became consumed with the whiteboard library. He couldn't
|
||||
help but marvel at its elegance and simplicity. With each stroke of his keyboard, he felt a
|
||||
surge of inspiration, a connection to something greater than himself. It was as if the lines
|
||||
of code he was writing were transforming into a digital canvas, waiting to be filled with
|
||||
the strokes of imagination. In that moment, John realized that he was not just building a
|
||||
tool, but breathing life into a new form of expression. The whiteboard was no longer just a
|
||||
blank slate; it had become a portal to a world where ideas could flourish and dreams could
|
||||
take shape.
|
||||
</p>
|
||||
<p>
|
||||
Little did John know, this integration of "tldraw" was only the beginning. It would lead him
|
||||
down a path filled with unforeseen challenges, where he would confront his own limitations
|
||||
and question the very nature of creation. The journey ahead would test his resolve, pushing
|
||||
him to the edge of his sanity. And as he embarked on this perilous adventure, he could not
|
||||
shake the feeling that the whiteboard held secrets far beyond his understanding. Secrets
|
||||
that would unfold before his eyes, one stroke at a time.
|
||||
</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2296,7 +2296,7 @@ export type TLOnHandleChangeHandler<T extends TLShape> = (shape: T, info: {
|
|||
}) => TLShapePartial<T> | void;
|
||||
|
||||
// @public
|
||||
export type TLOnMountHandler = (editor: Editor) => (() => void) | undefined | void;
|
||||
export type TLOnMountHandler = (editor: Editor) => (() => undefined | void) | undefined | void;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLOnResizeEndHandler<T extends TLShape> = TLEventChangeHandler<T>;
|
||||
|
|
|
@ -123,7 +123,7 @@ export interface TldrawEditorBaseProps {
|
|||
*
|
||||
* @public
|
||||
*/
|
||||
export type TLOnMountHandler = (editor: Editor) => (() => void) | undefined | void
|
||||
export type TLOnMountHandler = (editor: Editor) => (() => void | undefined) | undefined | void
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -162,7 +162,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
|||
ref={rContainer}
|
||||
draggable={false}
|
||||
className={classNames('tl-container tl-theme__light', className)}
|
||||
tabIndex={0}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<OptionalErrorBoundary
|
||||
fallback={ErrorFallback}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import React, { useMemo } from 'react'
|
||||
import { preventDefault, releasePointerCapture, setPointerCapture } from '../utils/dom'
|
||||
import {
|
||||
preventDefault,
|
||||
releasePointerCapture,
|
||||
setPointerCapture,
|
||||
stopEventPropagation,
|
||||
} from '../utils/dom'
|
||||
import { getPointerInfo } from '../utils/getPointerInfo'
|
||||
import { useEditor } from './useEditor'
|
||||
|
||||
|
@ -12,6 +17,8 @@ export function useCanvasEvents() {
|
|||
let lastX: number, lastY: number
|
||||
|
||||
function onPointerDown(e: React.PointerEvent) {
|
||||
stopEventPropagation(e)
|
||||
|
||||
if ((e as any).isKilled) return
|
||||
|
||||
if (e.button === 2) {
|
||||
|
@ -103,6 +110,10 @@ export function useCanvasEvents() {
|
|||
})
|
||||
}
|
||||
|
||||
function onClick(e: React.MouseEvent) {
|
||||
stopEventPropagation(e)
|
||||
}
|
||||
|
||||
return {
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
|
@ -111,6 +122,7 @@ export function useCanvasEvents() {
|
|||
onDrop,
|
||||
onTouchStart,
|
||||
onTouchEnd,
|
||||
onClick,
|
||||
}
|
||||
},
|
||||
[editor]
|
||||
|
|
|
@ -7,25 +7,26 @@ export function useFocusEvents(autoFocus: boolean) {
|
|||
const editor = useEditor()
|
||||
const container = useContainer()
|
||||
useLayoutEffect(() => {
|
||||
function handleFocus() {
|
||||
if (autoFocus) {
|
||||
// When autoFocus is true, update the editor state to be focused
|
||||
// unless it's already focused
|
||||
if (!editor.instanceState.isFocused) {
|
||||
editor.updateInstanceState({ isFocused: true })
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener('focus', handleFocus)
|
||||
container.addEventListener('pointerdown', handleFocus)
|
||||
|
||||
if (autoFocus && !editor.instanceState.isFocused) {
|
||||
editor.updateInstanceState({ isFocused: true })
|
||||
container.focus()
|
||||
} else if (editor.instanceState.isFocused) {
|
||||
editor.updateInstanceState({ isFocused: false })
|
||||
}
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('focus', handleFocus)
|
||||
container.removeEventListener('pointerdown', handleFocus)
|
||||
// Note: Focus is also handled by the side effect manager in tldraw.
|
||||
// Importantly, if a user manually sets isFocused to true (or if it
|
||||
// changes for any reason from false to true), the side effect manager
|
||||
// in tldraw will also take care of the focus. However, it may be that
|
||||
// on first mount the editor already has isFocused: true in the model,
|
||||
// so we also need to focus it here just to be sure.
|
||||
editor.getContainer().focus()
|
||||
} else {
|
||||
// When autoFocus is false, update the editor state to be not focused
|
||||
// unless it's already not focused
|
||||
if (editor.instanceState.isFocused) {
|
||||
editor.updateInstanceState({ isFocused: false })
|
||||
}
|
||||
}
|
||||
}, [editor, container, autoFocus])
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { useMemo } from 'react'
|
||||
import { TLSelectionHandle } from '../editor/types/selection-types'
|
||||
import { loopToHtmlElement, releasePointerCapture, setPointerCapture } from '../utils/dom'
|
||||
import {
|
||||
loopToHtmlElement,
|
||||
releasePointerCapture,
|
||||
setPointerCapture,
|
||||
stopEventPropagation,
|
||||
} from '../utils/dom'
|
||||
import { getPointerInfo } from '../utils/getPointerInfo'
|
||||
import { useEditor } from './useEditor'
|
||||
|
||||
|
@ -48,7 +53,7 @@ export function useSelectionEvents(handle: TLSelectionHandle) {
|
|||
handle,
|
||||
...getPointerInfo(e),
|
||||
})
|
||||
e.stopPropagation()
|
||||
stopEventPropagation(e)
|
||||
}
|
||||
|
||||
// Track the last screen point
|
||||
|
|
|
@ -5,7 +5,6 @@ export function registerDefaultSideEffects(editor: Editor) {
|
|||
editor.sideEffects.registerAfterChangeHandler('instance', (prev, next) => {
|
||||
if (prev.isFocused !== next.isFocused) {
|
||||
if (next.isFocused) {
|
||||
editor.complete() // stop any interaction
|
||||
editor.getContainer().focus()
|
||||
editor.updateViewportScreenBounds()
|
||||
} else {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TLFrameShape, TLShapeId, useEditor } from '@tldraw/editor'
|
||||
import { TLFrameShape, TLShapeId, stopEventPropagation, useEditor } from '@tldraw/editor'
|
||||
import { forwardRef, useCallback } from 'react'
|
||||
import { defaultEmptyAs } from '../FrameShapeUtil'
|
||||
|
||||
|
@ -13,7 +13,7 @@ export const FrameLabelInput = forwardRef<
|
|||
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
|
||||
// need to prevent the enter keydown making it's way up to the Idle state
|
||||
// and sending us back into edit mode
|
||||
e.stopPropagation()
|
||||
stopEventPropagation(e)
|
||||
e.currentTarget.blur()
|
||||
editor.setEditingShape(null)
|
||||
}
|
||||
|
|
|
@ -131,7 +131,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
|||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<main
|
||||
<div
|
||||
className={classNames('tlui-layout', {
|
||||
'tlui-layout__mobile': breakpoint < 5,
|
||||
})}
|
||||
|
@ -180,7 +180,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
|
|||
<Dialogs />
|
||||
<ToastViewport />
|
||||
<FollowingIndicator />
|
||||
</main>
|
||||
</div>
|
||||
</ToastProvider>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEditor } from '@tldraw/editor'
|
||||
import { stopEventPropagation, useEditor } from '@tldraw/editor'
|
||||
import classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
|
||||
|
@ -94,14 +94,14 @@ export const Input = React.forwardRef<HTMLInputElement, TLUiInputProps>(function
|
|||
switch (e.key) {
|
||||
case 'Enter': {
|
||||
e.currentTarget.blur()
|
||||
e.stopPropagation()
|
||||
stopEventPropagation(e)
|
||||
onComplete?.(e.currentTarget.value)
|
||||
break
|
||||
}
|
||||
case 'Escape': {
|
||||
e.currentTarget.value = rInitialValue.current
|
||||
e.currentTarget.blur()
|
||||
e.stopPropagation()
|
||||
stopEventPropagation(e)
|
||||
onCancel?.(e.currentTarget.value)
|
||||
break
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue