Mime Čuvalo 2024-05-09 14:40:02 +00:00 zatwierdzone przez GitHub
commit ccb69b8ff7
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
36 zmienionych plików z 252 dodań i 284 usunięć

Wyświetl plik

@ -67,7 +67,6 @@ export function BoardHistorySnapshot({
}}
overrides={[fileSystemUiOverrides]}
inferDarkMode
autoFocus
/>
</div>
<div className="board-history__restore">

Wyświetl plik

@ -9,7 +9,7 @@ import {
useRef,
useState,
} from 'react'
import { preventDefault, track, useContainer, useEditor, useTranslation } from 'tldraw'
import { preventDefault, track, useEditor, useTranslation } from 'tldraw'
// todo:
// - not cleaning up
@ -18,7 +18,6 @@ const CHAT_MESSAGE_TIMEOUT_CHATTING = 5000
export const CursorChatBubble = track(function CursorChatBubble() {
const editor = useEditor()
const container = useContainer()
const { isChatting, chatMessage } = editor.getInstanceState()
const rTimeout = useRef<any>(-1)
@ -31,14 +30,14 @@ export const CursorChatBubble = track(function CursorChatBubble() {
rTimeout.current = setTimeout(() => {
editor.updateInstanceState({ chatMessage: '', isChatting: false })
setValue('')
container.focus()
editor.focus()
}, duration)
}
return () => {
clearTimeout(rTimeout.current)
}
}, [container, editor, chatMessage, isChatting])
}, [editor, chatMessage, isChatting])
if (isChatting)
return <CursorChatInput value={value} setValue={setValue} chatMessage={chatMessage} />
@ -101,7 +100,6 @@ const CursorChatInput = track(function CursorChatInput({
}) {
const editor = useEditor()
const msg = useTranslation()
const container = useContainer()
const ref = useRef<HTMLInputElement>(null)
const placeholder = chatMessage || msg('cursor-chat.type-to-chat')
@ -126,11 +124,9 @@ const CursorChatInput = track(function CursorChatInput({
}, [editor, value, placeholder])
useLayoutEffect(() => {
// Focus the editor
let raf = requestAnimationFrame(() => {
raf = requestAnimationFrame(() => {
ref.current?.focus()
})
// Focus the input
const raf = requestAnimationFrame(() => {
ref.current?.focus()
})
return () => {
@ -140,8 +136,8 @@ const CursorChatInput = track(function CursorChatInput({
const stopChatting = useCallback(() => {
editor.updateInstanceState({ isChatting: false })
container.focus()
}, [editor, container])
editor.focus()
}, [editor])
// Update the chat message as the user types
const handleChange = useCallback(

Wyświetl plik

@ -102,7 +102,6 @@ export function LocalEditor() {
assetUrls={assetUrls}
persistenceKey={SCRATCH_PERSISTENCE_KEY}
onMount={handleMount}
autoFocus
overrides={[sharingUiOverrides, fileSystemUiOverrides]}
onUiEvent={handleUiEvent}
components={components}

Wyświetl plik

@ -158,7 +158,6 @@ export function MultiplayerEditor({
initialState={isReadonly ? 'hand' : 'select'}
onUiEvent={handleUiEvent}
components={components}
autoFocus
inferDarkMode
>
<UrlStateSync />

Wyświetl plik

@ -52,8 +52,8 @@ export function UserPresenceEditor() {
onCancel={toggleEditingName}
onBlur={handleBlur}
shouldManuallyMaintainScrollPositionWhenFocused
autofocus
autoselect
autoFocus
autoSelect
/>
) : (
<>

Wyświetl plik

@ -89,7 +89,6 @@ export function SnapshotsEditor(props: SnapshotEditorProps) {
}}
components={components}
renderDebugMenuItems={() => <DebugMenuItems />}
autoFocus
inferDarkMode
>
<UrlStateSync />

Wyświetl plik

@ -175,4 +175,53 @@ test.describe('Focus', () => {
null
)
})
test('still focuses text after clicking on style button', async ({ page }) => {
await page.goto('http://localhost:5420/end-to-end')
await page.waitForSelector('.tl-canvas')
const EditorA = (await page.$(`.tl-container`))!
expect(EditorA).toBeTruthy()
// Create a new note, text should be focused
await page.keyboard.press('n')
await (await page.$('body'))?.click()
await page.waitForSelector('.tl-shape')
const blueButton = await page.$('.tlui-button[data-testid="style.color.blue"]')
await blueButton?.dispatchEvent('pointerdown')
await blueButton?.click()
await blueButton?.dispatchEvent('pointerup')
// Text should still be focused.
expect(await page.evaluate(() => document.activeElement?.nodeName === 'TEXTAREA')).toBe(true)
})
test('edit->edit, focus stays in the text areas when going from shape-to-shape', async ({
page,
}) => {
await page.goto('http://localhost:5420/end-to-end')
await page.waitForSelector('.tl-canvas')
const EditorA = (await page.$(`.tl-container`))!
expect(EditorA).toBeTruthy()
// Create a new note, text should be focused
await page.keyboard.press('n')
await (await page.$('body'))?.click()
await page.waitForSelector('.tl-shape')
await page.keyboard.type('test')
// create new note next to it
await page.keyboard.press('Tab')
await (await page.$('body'))?.click()
// First note's textarea should be focused.
expect(
await EditorA.evaluate(
() => document.querySelector('.tl-shape textarea') === document.activeElement
)
).toBe(true)
})
})

Wyświetl plik

@ -67,7 +67,7 @@ export default function ExternalContentSourcesExample() {
return (
<div className="tldraw__editor">
<Tldraw autoFocus onMount={handleMount} shapeUtils={[DangerousHtmlExample]} />
<Tldraw onMount={handleMount} shapeUtils={[DangerousHtmlExample]} />
</div>
)
}

Wyświetl plik

@ -1,5 +1,5 @@
import { createContext, useCallback, useContext, useState } from 'react'
import { Tldraw } from 'tldraw'
import { Editor, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this page!
@ -7,24 +7,35 @@ import 'tldraw/tldraw.css'
// [1]
const focusedEditorContext = createContext(
{} as {
focusedEditor: string | null
setFocusedEditor: (id: string | null) => void
focusedEditor: Editor | null
setFocusedEditor: (id: Editor | null) => void
}
)
// [2]
export default function MultipleExample() {
const [focusedEditor, _setFocusedEditor] = useState<string | null>('A')
const [focusedEditor, _setFocusedEditor] = useState<Editor | null>(null)
const setFocusedEditor = useCallback(
(id: string | null) => {
if (focusedEditor !== id) {
_setFocusedEditor(id)
(editor: Editor | null) => {
if (focusedEditor !== editor) {
focusedEditor?.updateInstanceState({ isFocused: false })
_setFocusedEditor(editor)
editor?.updateInstanceState({ isFocused: true })
}
},
[focusedEditor]
)
const focusName =
focusedEditor === (window as any).EDITOR_A
? 'A'
: focusedEditor === (window as any).EDITOR_B
? 'B'
: focusedEditor === (window as any).EDITOR_C
? 'C'
: 'none'
return (
<div
style={{
@ -35,7 +46,7 @@ export default function MultipleExample() {
onPointerDown={() => setFocusedEditor(null)}
>
<focusedEditorContext.Provider value={{ focusedEditor, setFocusedEditor }}>
<h1>Focusing: {focusedEditor ?? 'none'}</h1>
<h1>Focusing: {focusName}</h1>
<EditorA />
<textarea data-testid="textarea" placeholder="type in me" style={{ margin: 10 }} />
<div
@ -61,19 +72,23 @@ export default function MultipleExample() {
// [3]
function EditorA() {
const { focusedEditor, setFocusedEditor } = useContext(focusedEditorContext)
const isFocused = focusedEditor === 'A'
const { setFocusedEditor } = useContext(focusedEditorContext)
return (
<div style={{ padding: 32 }}>
<h2>A</h2>
<div tabIndex={-1} onFocus={() => setFocusedEditor('A')} style={{ height: 600 }}>
<div
tabIndex={-1}
onFocus={() => setFocusedEditor((window as any).EDITOR_A)}
style={{ height: 600 }}
>
<Tldraw
persistenceKey="steve"
className="A"
autoFocus={isFocused}
autoFocus={false}
onMount={(editor) => {
;(window as any).EDITOR_A = editor
setFocusedEditor(editor)
}}
/>
</div>
@ -83,17 +98,20 @@ function EditorA() {
// [4]
function EditorB() {
const { focusedEditor, setFocusedEditor } = useContext(focusedEditorContext)
const isFocused = focusedEditor === 'B'
const { setFocusedEditor } = useContext(focusedEditorContext)
return (
<div>
<h2>B</h2>
<div tabIndex={-1} onFocus={() => setFocusedEditor('B')} style={{ height: 600 }}>
<div
tabIndex={-1}
onFocus={() => setFocusedEditor((window as any).EDITOR_B)}
style={{ height: 600 }}
>
<Tldraw
persistenceKey="david"
className="B"
autoFocus={isFocused}
autoFocus={false}
onMount={(editor) => {
;(window as any).EDITOR_B = editor
}}
@ -104,17 +122,20 @@ function EditorB() {
}
function EditorC() {
const { focusedEditor, setFocusedEditor } = useContext(focusedEditorContext)
const isFocused = focusedEditor === 'C'
const { setFocusedEditor } = useContext(focusedEditorContext)
return (
<div>
<h2>C</h2>
<div tabIndex={-1} onFocus={() => setFocusedEditor('C')} style={{ height: 600 }}>
<div
tabIndex={-1}
onFocus={() => setFocusedEditor((window as any).EDITOR_C)}
style={{ height: 600 }}
>
<Tldraw
persistenceKey="david"
className="C"
autoFocus={isFocused}
autoFocus={false}
onMount={(editor) => {
;(window as any).EDITOR_C = editor
}}

Wyświetl plik

@ -14,10 +14,7 @@ export default function ScrollExample() {
}}
>
<div style={{ width: '60vw', height: '80vh' }}>
<Tldraw
persistenceKey="scroll-example"
// autoFocus={false}
/>
<Tldraw persistenceKey="scroll-example" />
</div>
</div>
)

Wyświetl plik

@ -126,7 +126,6 @@ function TldrawInner({ uri, assetSrc, isDarkMode, fileContents }: TLDrawInnerPro
persistenceKey={uri}
onMount={handleMount}
components={components}
autoFocus
>
{/* <DarkModeHandler themeKind={themeKind} /> */}

Wyświetl plik

@ -663,7 +663,7 @@ export class Edge2d extends Geometry2d {
// @public (undocumented)
export class Editor extends EventEmitter<TLEventMap> {
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, cameraOptions, initialState, inferDarkMode, }: TLEditorOptions);
constructor({ store, user, shapeUtils, bindingUtils, tools, getContainer, cameraOptions, initialState, autoFocus, inferDarkMode, }: TLEditorOptions);
addOpenMenu(id: string): this;
alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'): this;
animateShape(partial: null | TLShapePartial | undefined, opts?: Partial<{
@ -771,6 +771,8 @@ export class Editor extends EventEmitter<TLEventMap> {
findCommonAncestor(shapes: TLShape[] | TLShapeId[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
findShapeAncestor(shape: TLShape | TLShapeId, predicate: (parent: TLShape) => boolean): TLShape | undefined;
flipShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
focus(): this;
readonly focusManager: FocusManager;
getAncestorPageId(shape?: TLShape | TLShapeId): TLPageId | undefined;
getAsset(asset: TLAsset | TLAssetId): TLAsset | undefined;
getAssetForExternalContent(info: TLExternalAssetContent): Promise<TLAsset | undefined>;
@ -1126,6 +1128,19 @@ export function extractSessionStateFromLegacySnapshot(store: Record<string, Unkn
// @internal (undocumented)
export const featureFlags: Record<string, DebugFlag<boolean>>;
// @public
export class FocusManager {
constructor(editor: Editor, autoFocus?: boolean);
// (undocumented)
blur(): void;
// (undocumented)
dispose(): void;
// (undocumented)
editor: Editor;
// (undocumented)
focus(): void;
}
// @public (undocumented)
export type GapsSnapIndicator = {
direction: 'horizontal' | 'vertical';
@ -2261,6 +2276,7 @@ export type TLEditorComponents = Partial<{
// @public (undocumented)
export interface TLEditorOptions {
autoFocus?: boolean;
bindingUtils: readonly TLBindingUtilConstructor<TLUnknownBinding>[];
cameraOptions?: Partial<TLCameraOptions>;
getContainer: () => HTMLElement;

Wyświetl plik

@ -132,6 +132,7 @@ export {
type BindingOnShapeDeleteOptions,
type TLBindingUtilConstructor,
} from './lib/editor/bindings/BindingUtil'
export type { FocusManager } from './lib/editor/managers/FocusManager'
export { HistoryManager } from './lib/editor/managers/HistoryManager'
export type {
SideEffectManager,

Wyświetl plik

@ -30,10 +30,8 @@ import {
useEditorComponents,
} from './hooks/useEditorComponents'
import { useEvent } from './hooks/useEvent'
import { useFocusEvents } from './hooks/useFocusEvents'
import { useForceUpdate } from './hooks/useForceUpdate'
import { useLocalStore } from './hooks/useLocalStore'
import { useSafariFocusOutFix } from './hooks/useSafariFocusOutFix'
import { useZoomCss } from './hooks/useZoomCss'
import { stopEventPropagation } from './utils/dom'
import { TLStoreWithStatus } from './utils/sync/StoreWithStatus'
@ -305,6 +303,7 @@ function TldrawEditorWithReadyStore({
const { ErrorFallback } = useEditorComponents()
const container = useContainer()
const [editor, setEditor] = useState<Editor | null>(null)
const [initialAutoFocus] = useState(autoFocus)
useLayoutEffect(() => {
const editor = new Editor({
@ -315,6 +314,7 @@ function TldrawEditorWithReadyStore({
getContainer: () => container,
user,
initialState,
autoFocus: initialAutoFocus,
inferDarkMode,
cameraOptions,
})
@ -331,6 +331,7 @@ function TldrawEditorWithReadyStore({
store,
user,
initialState,
initialAutoFocus,
inferDarkMode,
cameraOptions,
])
@ -374,30 +375,18 @@ function TldrawEditorWithReadyStore({
<Crash crashingError={crashingError} />
) : (
<EditorContext.Provider value={editor}>
<Layout autoFocus={autoFocus} onMount={onMount}>
{children ?? (Canvas ? <Canvas /> : null)}
</Layout>
<Layout onMount={onMount}>{children ?? (Canvas ? <Canvas /> : null)}</Layout>
</EditorContext.Provider>
)}
</OptionalErrorBoundary>
)
}
function Layout({
children,
onMount,
autoFocus,
}: {
children: ReactNode
autoFocus: boolean
onMount?: TLOnMountHandler
}) {
function Layout({ children, onMount }: { children: ReactNode; onMount?: TLOnMountHandler }) {
useZoomCss()
useCursor()
useDarkMode()
useSafariFocusOutFix()
useForceUpdate()
useFocusEvents(autoFocus)
useOnMount(onMount)
return (

Wyświetl plik

@ -125,6 +125,7 @@ import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage
import { getSvgJsx } from './getSvgJsx'
import { ClickManager } from './managers/ClickManager'
import { EnvironmentManager } from './managers/EnvironmentManager'
import { FocusManager } from './managers/FocusManager'
import { HistoryManager } from './managers/HistoryManager'
import { ScribbleManager } from './managers/ScribbleManager'
import { SideEffectManager } from './managers/SideEffectManager'
@ -198,6 +199,10 @@ export interface TLEditorOptions {
* The editor's initial active tool (or other state node id).
*/
initialState?: string
/**
* Whether to automatically focus the editor when it mounts.
*/
autoFocus?: boolean
/**
* Whether to infer dark mode from the user's system preferences. Defaults to false.
*/
@ -219,6 +224,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getContainer,
cameraOptions,
initialState,
autoFocus,
inferDarkMode,
}: TLEditorOptions) {
super()
@ -632,6 +638,9 @@ export class Editor extends EventEmitter<TLEventMap> {
this.root.enter(undefined, 'initial')
this.focusManager = new FocusManager(this, autoFocus)
this.disposables.add(this.focusManager.dispose.bind(this.focusManager))
if (this.getInstanceState().followingUserId) {
this.stopFollowingUser()
}
@ -711,6 +720,13 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
readonly sideEffects: SideEffectManager<this>
/**
* A manager for ensuring correct focus. See {@link FocusManager} for details.
*
* @public
*/
readonly focusManager: FocusManager
/**
* The current HTML element containing the editor.
*
@ -8021,6 +8037,21 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
/**
* Dispatch a focus event.
*
* @example
* ```ts
* editor.focus()
* ```
*
* @public
*/
focus(): this {
this.focusManager.focus()
return this
}
/**
* A manager for recording multiple click events.
*

Wyświetl plik

@ -0,0 +1,46 @@
import type { Editor } from '../Editor'
/**
* A manager for ensuring correct focus across the editor.
* It will listen for changes in the instance state to make sure the
* container is focused when the editor is focused.
* Also, it will make sure that the focus is on things like text
* labels when the editor is in editing mode.
*
* @public
*/
export class FocusManager {
private disposeSideEffectListener?: () => void
constructor(
public editor: Editor,
autoFocus?: boolean
) {
this.disposeSideEffectListener = editor.sideEffects.registerAfterChangeHandler(
'instance',
(prev, next) => {
if (prev.isFocused !== next.isFocused) {
next.isFocused ? this.focus() : this.blur()
}
}
)
const currentFocusState = editor.getInstanceState().isFocused
if (autoFocus !== currentFocusState) {
editor.updateInstanceState({ isFocused: !!autoFocus })
}
}
focus() {
this.editor.getContainer().focus()
}
blur() {
this.editor.complete() // stop any interaction
this.editor.getContainer().blur() // blur the container
}
dispose() {
this.disposeSideEffectListener?.()
}
}

Wyświetl plik

@ -40,15 +40,6 @@ export function useCanvasEvents() {
name: 'pointer_down',
...getPointerInfo(e),
})
if (editor.getOpenMenus().length > 0) {
editor.updateInstanceState({
openMenus: [],
})
document.body.click()
editor.getContainer().focus()
}
}
function onPointerMove(e: React.PointerEvent) {
@ -98,9 +89,6 @@ export function useCanvasEvents() {
function onTouchStart(e: React.TouchEvent) {
;(e as any).isKilled = true
// todo: investigate whether this effects keyboard shortcuts
// god damn it, but necessary for long presses to open the context menu
document.body.click()
preventDefault(e)
}

Wyświetl plik

@ -125,7 +125,7 @@ export function useDocumentEvents() {
// will break additional shortcuts. We need to
// refocus the container in order to keep these
// shortcuts working.
container.focus()
editor.focus()
}
return
}

Wyświetl plik

@ -1,32 +0,0 @@
import { useLayoutEffect } from 'react'
import { useContainer } from './useContainer'
import { useEditor } from './useEditor'
/** @internal */
export function useFocusEvents(autoFocus: boolean) {
const editor = useEditor()
const container = useContainer()
useLayoutEffect(() => {
if (autoFocus) {
// When autoFocus is true, update the editor state to be focused
// unless it's already focused
if (!editor.getInstanceState().isFocused) {
editor.updateInstanceState({ isFocused: true })
}
// 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.getInstanceState().isFocused) {
editor.updateInstanceState({ isFocused: false })
}
}
}, [editor, container, autoFocus])
}

Wyświetl plik

@ -1,32 +0,0 @@
import * as React from 'react'
import { useEditor } from './useEditor'
let isMobileSafari = false
if (typeof window !== 'undefined') {
const ua = window.navigator.userAgent
const iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i)
const webkit = !!ua.match(/WebKit/i)
isMobileSafari = iOS && webkit && !ua.match(/CriOS/i)
}
export function useSafariFocusOutFix(): void {
const editor = useEditor()
React.useEffect(() => {
if (!isMobileSafari) return
function handleFocusOut(e: FocusEvent) {
if (
(e.target instanceof HTMLInputElement && e.target.type === 'text') ||
e.target instanceof HTMLTextAreaElement
) {
editor.complete()
}
}
// Send event on iOS when a user presses the "Done" key while editing a text element.
document.addEventListener('focusout', handleFocusOut)
return () => document.removeEventListener('focusout', handleFocusOut)
}, [editor])
}

Wyświetl plik

@ -8,7 +8,7 @@ export type RecordTypeRecord<R extends RecordType<any, any>> = ReturnType<R['cre
/**
* Defines the scope of the record
*
* instance: The record belongs to a single instance of the store. It should not be synced, and any persistence logic should 'de-instance-ize' the record before persisting it, and apply the reverse when rehydrating.
* session: The record belongs to a single instance of the store. It should not be synced, and any persistence logic should 'de-instance-ize' the record before persisting it, and apply the reverse when rehydrating.
* document: The record is persisted and synced. It is available to all store instances.
* presence: The record belongs to a single instance of the store. It may be synced to other instances, but other instances should not make changes to it. It should not be persisted.
*

Wyświetl plik

@ -2227,9 +2227,9 @@ export type TLUiIconType = 'align-bottom' | 'align-center-horizontal' | 'align-c
// @public (undocumented)
export interface TLUiInputProps {
// (undocumented)
autofocus?: boolean;
autoFocus?: boolean;
// (undocumented)
autoselect?: boolean;
autoSelect?: boolean;
// (undocumented)
children?: React_3.ReactNode;
// (undocumented)
@ -2618,7 +2618,7 @@ export function useDialogs(): TLUiDialogsContextType;
// @public (undocumented)
export function useEditableText(id: TLShapeId, type: string, text: string): {
handleBlur: () => void;
handleBlur: typeof noop;
handleChange: (e: React_2.ChangeEvent<HTMLTextAreaElement>) => void;
handleDoubleClick: (e: any) => any;
handleFocus: typeof noop;

Wyświetl plik

@ -2,16 +2,6 @@ import { Editor } from '@tldraw/editor'
export function registerDefaultSideEffects(editor: Editor) {
return [
editor.sideEffects.registerAfterChangeHandler('instance', (prev, next) => {
if (prev.isFocused !== next.isFocused) {
if (next.isFocused) {
editor.getContainer().focus()
} else {
editor.complete() // stop any interaction
editor.getContainer().blur() // blur the container
}
}
}),
editor.sideEffects.registerAfterChangeHandler('instance_page_state', (prev, next) => {
if (prev.croppingShapeId !== next.croppingShapeId) {
const isInCroppingState = editor.isInAny(

Wyświetl plik

@ -58,14 +58,6 @@ export const FrameHeading = function FrameHeading({
// On iOS, we must focus here
el.focus()
el.select()
requestAnimationFrame(() => {
// On desktop, the input may have lost focus, so try try try again!
if (document.activeElement !== el) {
el.focus()
el.select()
}
})
}
}, [rInput, isEditing])

Wyświetl plik

@ -13,7 +13,6 @@ import { INDENT, TextHelpers } from './TextHelpers'
export function useEditableText(id: TLShapeId, type: string, text: string) {
const editor = useEditor()
const rInput = useRef<HTMLTextAreaElement>(null)
const rSelectionRanges = useRef<Range[] | null>()
const isEditing = useValue('isEditing', () => editor.getEditingShapeId() === id, [editor])
const isEditingAnything = useValue('isEditingAnything', () => !!editor.getEditingShapeId(), [
editor,
@ -21,98 +20,36 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
useEffect(() => {
function selectAllIfEditing({ shapeId }: { shapeId: TLShapeId }) {
// We wait a tick, because on iOS, the keyboard will not show if we focus immediately.
requestAnimationFrame(() => {
if (shapeId === id) {
const elm = rInput.current
if (elm) {
if (document.activeElement !== elm) {
elm.focus()
}
elm.select()
}
}
})
if (shapeId === id) {
rInput.current?.select()
}
}
editor.on('select-all-text', selectAllIfEditing)
return () => {
editor.off('select-all-text', selectAllIfEditing)
}
}, [editor, id])
}, [editor, id, isEditing])
useEffect(() => {
if (!isEditing) return
const elm = rInput.current
if (!elm) return
// Focus if we're not already focused
if (document.activeElement !== elm) {
elm.focus()
// On mobile etc, just select all the text when we start focusing
if (editor.getInstanceState().isCoarsePointer) {
elm.select()
}
} else {
// This fixes iOS not showing the cursor sometimes. This "shakes" the cursor
// awake.
if (editor.environment.isSafari) {
elm.blur()
elm.focus()
}
if (document.activeElement !== rInput.current) {
rInput.current?.focus()
}
// When the selection changes, save the selection ranges
function updateSelection() {
const selection = window.getSelection?.()
if (selection && selection.type !== 'None') {
const ranges: Range[] = []
for (let i = 0; i < selection.rangeCount; i++) {
ranges.push(selection.getRangeAt?.(i))
}
rSelectionRanges.current = ranges
}
if (editor.getInstanceState().isCoarsePointer) {
rInput.current?.select()
}
document.addEventListener('selectionchange', updateSelection)
return () => {
document.removeEventListener('selectionchange', updateSelection)
// XXX(mime): This fixes iOS not showing the cursor sometimes.
// This "shakes" the cursor awake.
if (editor.environment.isSafari) {
rInput.current?.blur()
rInput.current?.focus()
}
}, [editor, isEditing])
// 2. Restore the selection changes (and focus) if the element blurs
// When the label blurs, deselect all of the text and complete.
// This makes it so that the canvas does not have to be focused
// in order to exit the editing state and complete the editing state
const handleBlur = useCallback(() => {
const ranges = rSelectionRanges.current
requestAnimationFrame(() => {
const elm = rInput.current
const editingShapeId = editor.getEditingShapeId()
// Did we move to a different shape?
if (editingShapeId) {
// important! these ^v are two different things
// is that shape OUR shape?
if (elm && editingShapeId === id) {
elm.focus()
if (ranges && ranges.length) {
const selection = window.getSelection()
if (selection) {
ranges.forEach((range) => selection.addRange(range))
}
}
}
} else {
window.getSelection()?.removeAllRanges()
}
})
}, [editor, id])
// When the user presses ctrl / meta enter, complete the editing state.
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@ -186,7 +123,7 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
return {
rInput,
handleFocus: noop,
handleBlur,
handleBlur: noop,
handleKeyDown,
handleChange,
handleInputPointerDown,

Wyświetl plik

@ -36,7 +36,6 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
autoCapitalize="off"
autoCorrect="off"
autoSave="off"
// autoFocus
placeholder=""
spellCheck="true"
wrap="off"

Wyświetl plik

@ -217,7 +217,7 @@ export class PointingShape extends StateNode {
if (this.editor.getInstanceState().isReadonly) return
// Re-focus the editor, just in case the text label of the shape has stolen focus
this.editor.getContainer().focus()
this.editor.focus()
this.parent.transition('translating', info)
}

Wyświetl plik

@ -150,7 +150,7 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
ref={rInput}
className="tlui-edit-link-dialog__input"
label="edit-link-dialog.url"
autofocus
autoFocus
value={urlInputState.actual}
onValueChange={handleChange}
onComplete={handleComplete}

Wyświetl plik

@ -51,7 +51,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
className="tlui-embed-dialog__input"
label="embed-dialog.url"
placeholder="http://example.com"
autofocus
autoFocus
onValueChange={(value) => {
// Set the url that the user has typed into the input
setUrl(value)

Wyświetl plik

@ -34,8 +34,8 @@ export const PageItemInput = function PageItemInput({
onValueChange={handleChange}
onFocus={handleFocus}
shouldManuallyMaintainScrollPositionWhenFocused
autofocus={isCurrentPage}
autoselect
autoFocus={isCurrentPage}
autoSelect
/>
)
}

Wyświetl plik

@ -1,4 +1,3 @@
import { useEditor } from '@tldraw/editor'
import classnames from 'classnames'
import * as React from 'react'
@ -11,15 +10,6 @@ export interface TLUiButtonProps extends React.HTMLAttributes<HTMLButtonElement>
/** @public */
export const TldrawUiButton = React.forwardRef<HTMLButtonElement, TLUiButtonProps>(
function TldrawUiButton({ children, disabled, type, ...props }, ref) {
const editor = useEditor()
// If the button is getting disabled while it's focused, move focus to the editor
// so that the user can continue using keyboard shortcuts
const current = (ref as React.MutableRefObject<HTMLButtonElement | null>)?.current
if (disabled && current === document.activeElement) {
editor.getContainer().focus()
}
return (
<button
ref={ref}

Wyświetl plik

@ -40,6 +40,7 @@ function _TldrawUiButtonPicker<T extends string>(props: TLUiButtonPickerProps<T>
const msg = useTranslation()
const rPointing = useRef(false)
const rPointingOriginalActiveElement = useRef<HTMLElement | null>(null)
const {
handleButtonClick,
@ -50,6 +51,15 @@ function _TldrawUiButtonPicker<T extends string>(props: TLUiButtonPickerProps<T>
const handlePointerUp = () => {
rPointing.current = false
window.removeEventListener('pointerup', handlePointerUp)
// This is fun little micro-optimization to make sure that the focus
// is retained on a text label. That way, you can continue typing
// after selecting a style.
const origActiveEl = rPointingOriginalActiveElement.current
if (origActiveEl && ['TEXTAREA', 'INPUT'].includes(origActiveEl.nodeName)) {
origActiveEl.focus()
}
rPointingOriginalActiveElement.current = null
}
const handleButtonClick = (e: React.PointerEvent<HTMLButtonElement>) => {
@ -67,6 +77,7 @@ function _TldrawUiButtonPicker<T extends string>(props: TLUiButtonPickerProps<T>
onValueChange(style, id as T)
rPointing.current = true
rPointingOriginalActiveElement.current = document.activeElement as HTMLElement
window.addEventListener('pointerup', handlePointerUp) // see TLD-658
}

Wyświetl plik

@ -12,8 +12,8 @@ export interface TLUiInputProps {
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
icon?: TLUiIconType | Exclude<string, TLUiIconType>
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
autofocus?: boolean
autoselect?: boolean
autoFocus?: boolean
autoSelect?: boolean
children?: React.ReactNode
defaultValue?: string
placeholder?: string
@ -43,8 +43,8 @@ export const TldrawUiInput = React.forwardRef<HTMLInputElement, TLUiInputProps>(
label,
icon,
iconLeft,
autoselect = false,
autofocus = false,
autoSelect = false,
autoFocus = false,
defaultValue,
placeholder,
onComplete,
@ -75,13 +75,13 @@ export const TldrawUiInput = React.forwardRef<HTMLInputElement, TLUiInputProps>(
const elm = e.currentTarget as HTMLInputElement
rCurrentValue.current = elm.value
requestAnimationFrame(() => {
if (autoselect) {
if (autoSelect) {
elm.select()
}
})
onFocus?.()
},
[autoselect, onFocus]
[autoSelect, onFocus]
)
const handleChange = React.useCallback(
@ -159,7 +159,7 @@ export const TldrawUiInput = React.forwardRef<HTMLInputElement, TLUiInputProps>(
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus={autofocus}
autoFocus={autoFocus}
placeholder={placeholder}
value={value}
/>

Wyświetl plik

@ -26,8 +26,6 @@ export function useKeyboardShortcuts() {
useEffect(() => {
if (!isFocused) return
const container = editor.getContainer()
hotkeys.setScope(editor.store.id)
const hot = (keys: string, callback: (event: KeyboardEvent) => void) => {
@ -78,7 +76,7 @@ export function useKeyboardShortcuts() {
if (editor.inputs.keys.has('Comma')) return
preventDefault(e) // prevent whatever would normally happen
container.focus() // Focus if not already focused
editor.focus() // Focus if not already focused
editor.inputs.keys.add('Comma')

Wyświetl plik

@ -446,7 +446,9 @@ describe('isFocused', () => {
expect(editor.getInstanceState().isFocused).toBe(false)
})
it('becomes false when a child of the app container div receives a focusout event', () => {
it.skip('becomes false when a child of the app container div receives a focusout event', () => {
// This used to be true, but the focusout event doesn't actually bubble up anymore
// after we reworked to have the focus manager handle things.
const child = document.createElement('div')
editor.elm.appendChild(child)

Wyświetl plik

@ -20,10 +20,9 @@ function checkAllShapes(editor: Editor, shapes: string[]) {
describe('<TldrawEditor />', () => {
it('Renders without crashing', async () => {
await renderTldrawComponent(
<TldrawEditor tools={defaultTools} autoFocus initialState="select" />,
{ waitForPatterns: false }
)
await renderTldrawComponent(<TldrawEditor tools={defaultTools} initialState="select" />, {
waitForPatterns: false,
})
await screen.findByTestId('canvas')
})
@ -36,7 +35,6 @@ describe('<TldrawEditor />', () => {
}}
initialState="select"
tools={defaultTools}
autoFocus
/>,
{ waitForPatterns: false }
)
@ -53,7 +51,6 @@ describe('<TldrawEditor />', () => {
onMount={(e) => {
editor = e
}}
autoFocus
/>,
{ waitForPatterns: false }
)
@ -72,7 +69,6 @@ describe('<TldrawEditor />', () => {
onMount={(editor) => {
expect(editor.store).toBe(store)
}}
autoFocus
/>,
{ waitForPatterns: false }
)
@ -85,7 +81,6 @@ describe('<TldrawEditor />', () => {
// <TldrawEditor
// shapeUtils={[GroupShapeUtil]}
// store={createTLStore({ shapeUtils: [] })}
// autoFocus
// components={{
// ErrorFallback: ({ error }) => {
// throw error
@ -103,7 +98,6 @@ describe('<TldrawEditor />', () => {
// render(
// <TldrawEditor
// store={createTLStore({ shapeUtils: [GroupShapeUtil] })}
// autoFocus
// components={{
// ErrorFallback: ({ error }) => {
// throw error
@ -128,7 +122,6 @@ describe('<TldrawEditor />', () => {
tools={defaultTools}
store={initialStore}
onMount={onMount}
autoFocus
/>
)
const initialEditor = onMount.mock.lastCall[0]
@ -141,7 +134,6 @@ describe('<TldrawEditor />', () => {
initialState="select"
store={initialStore}
onMount={onMount}
autoFocus
/>
)
// not called again:
@ -149,13 +141,7 @@ describe('<TldrawEditor />', () => {
// re-render with a new store:
const newStore = createTLStore({ shapeUtils: [] })
rendered.rerender(
<TldrawEditor
tools={defaultTools}
initialState="select"
store={newStore}
onMount={onMount}
autoFocus
/>
<TldrawEditor tools={defaultTools} initialState="select" store={newStore} onMount={onMount} />
)
expect(initialEditor.dispose).toHaveBeenCalledTimes(1)
expect(onMount).toHaveBeenCalledTimes(2)
@ -169,7 +155,6 @@ describe('<TldrawEditor />', () => {
shapeUtils={[GeoShapeUtil]}
initialState="select"
tools={defaultTools}
autoFocus
onMount={(editorApp) => {
editor = editorApp
}}
@ -285,7 +270,6 @@ describe('Custom shapes', () => {
<TldrawEditor
shapeUtils={shapeUtils}
tools={[...defaultTools, ...tools]}
autoFocus
initialState="select"
onMount={(editorApp) => {
editor = editorApp