From b22ea7cd4e6c27dcebd6615daa07116ecacbf554 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 19 Jul 2023 11:52:21 +0100 Subject: [PATCH] More cleanup, focus bug fixes (#1749) This PR is another grab bag: - renames `readOnly` to `readonly` throughout editor - fixes a regression related to focus and keyboard shortcuts - adds a small outline for focused editors ### Change Type - [x] `major` ### Test Plan - [x] End to end tests --- apps/examples/e2e/tests/test-focus.spec.ts | 119 ++++++++++++++++++ .../src/7-multiple/MultipleExample.tsx | 4 +- assets/translations/pt-br.json | 2 +- packages/editor/api-report.md | 1 + packages/editor/editor.css | 4 + packages/editor/src/lib/TldrawEditor.tsx | 16 ++- packages/editor/src/lib/components/Canvas.tsx | 4 +- packages/editor/src/lib/editor/Editor.ts | 117 +++++++++-------- .../src/lib/editor/managers/SnapManager.ts | 18 +-- .../src/lib/editor/types/editor-state.ts | 21 ---- .../editor/src/lib/hooks/useCanvasEvents.ts | 4 +- .../editor/src/lib/hooks/useDocumentEvents.ts | 25 ++-- packages/tldraw/api-report.md | 2 +- packages/tldraw/src/index.ts | 2 +- .../lib/canvas/TldrawSelectionForeground.tsx | 4 +- .../src/lib/shapes/arrow/ArrowShapeUtil.tsx | 2 +- .../src/lib/tools/SelectTool/children/Idle.ts | 12 +- .../SelectTool/children/PointingSelection.ts | 2 +- .../SelectTool/children/PointingShape.ts | 2 +- packages/tldraw/src/lib/ui/TldrawUi.tsx | 5 +- .../src/lib/ui/components/ActionsMenu.tsx | 4 +- .../src/lib/ui/components/ContextMenu.tsx | 6 +- .../tldraw/src/lib/ui/components/HelpMenu.tsx | 4 +- .../ui/components/KeyboardShortcutsDialog.tsx | 4 +- .../tldraw/src/lib/ui/components/Menu.tsx | 4 +- .../tldraw/src/lib/ui/components/MenuZone.tsx | 4 +- .../lib/ui/components/PageMenu/PageMenu.tsx | 4 +- .../ui/components/StylePanel/StylePanel.tsx | 6 +- .../src/lib/ui/components/Toolbar/Toolbar.tsx | 14 +-- .../src/lib/ui/components/TrashButton.tsx | 4 +- .../src/lib/ui/hooks/useKeyboardShortcuts.ts | 19 +-- .../tldraw/src/lib/ui/hooks/useReadOnly.ts | 7 -- .../tldraw/src/lib/ui/hooks/useReadonly.ts | 7 ++ packages/tldraw/src/test/Editor.test.tsx | 22 ++-- packages/tldraw/src/test/SelectTool.test.ts | 6 +- packages/tldraw/src/test/groups.test.ts | 4 +- packages/tlschema/api-report.md | 2 +- packages/tlschema/src/migrations.test.ts | 16 +++ packages/tlschema/src/records/TLInstance.ts | 23 +++- 39 files changed, 350 insertions(+), 176 deletions(-) create mode 100644 apps/examples/e2e/tests/test-focus.spec.ts delete mode 100644 packages/editor/src/lib/editor/types/editor-state.ts delete mode 100644 packages/tldraw/src/lib/ui/hooks/useReadOnly.ts create mode 100644 packages/tldraw/src/lib/ui/hooks/useReadonly.ts diff --git a/apps/examples/e2e/tests/test-focus.spec.ts b/apps/examples/e2e/tests/test-focus.spec.ts new file mode 100644 index 000000000..3e91fe29d --- /dev/null +++ b/apps/examples/e2e/tests/test-focus.spec.ts @@ -0,0 +1,119 @@ +import test, { expect } from '@playwright/test' + +declare const __tldraw_editor_events: any[] + +// We're just testing the events, not the actual results. + +test.describe('Focus', () => { + test('focus events', async ({ page }) => { + 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`))! + expect(EditorA).toBeTruthy() + expect(EditorB).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) + + 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 EditorA.click() + expect(await EditorA.evaluate((node) => document.activeElement === node)).toBe(true) + expect(await EditorB.evaluate((node) => document.activeElement === node)).toBe(false) + + 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 EditorB.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) + + // Escape does not break focus + await page.keyboard.press('Escape') + expect(await EditorA.evaluate((node) => node.contains(document.activeElement))).toBe(true) + }) + + test('kbds when focused', async ({ page }) => { + await page.goto('http://localhost:5420/multiple') + await page.waitForSelector('.tl-canvas') + + const EditorA = (await page.$(`.A`))! + const EditorB = (await page.$(`.B`))! + expect(EditorA).toBeTruthy() + expect(EditorB).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 + ) + expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe( + null + ) + + await page.keyboard.press('d') + + expect( + await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]') + ).not.toBe(null) + expect(await EditorB.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe( + null + ) + + await EditorB.click() + await page.waitForTimeout(100) // takes 30ms or so to focus + await page.keyboard.press('d') + + expect( + await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]') + ).not.toBe(null) + expect( + await EditorB.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]') + ).not.toBe(null) + }) + + test('kbds after clicking on ui elements', 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() + + const drawButton = await EditorA.$('.tlui-button[data-testid="tools.draw"]') + + // select button should be selected, not the draw button + expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe( + null + ) + + await drawButton?.click() + + // draw button should be selected now + expect( + await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]') + ).not.toBe(null) + + await page.keyboard.press('v') + + // select button should be selected again + expect(await EditorA.$('.tlui-button[data-testid="tools.draw"][data-state="selected"]')).toBe( + null + ) + }) +}) diff --git a/apps/examples/src/7-multiple/MultipleExample.tsx b/apps/examples/src/7-multiple/MultipleExample.tsx index 39047d06c..ec22e30d2 100644 --- a/apps/examples/src/7-multiple/MultipleExample.tsx +++ b/apps/examples/src/7-multiple/MultipleExample.tsx @@ -12,7 +12,7 @@ export default function MultipleExample() {

First Example

This is the second example.

- +
@@ -20,7 +20,7 @@ export default function MultipleExample() {

Second Example

This is the second example.

- +
; export interface TldrawEditorBaseProps { autoFocus?: boolean; children?: any; + className?: string; components?: Partial; initialState?: string; onMount?: TLOnMountHandler; diff --git a/packages/editor/editor.css b/packages/editor/editor.css index 5f1a65f6c..209ac46fc 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -191,6 +191,10 @@ -webkit-touch-callout: initial; } +.tl-container:focus-within { + outline: 1px solid var(--color-low); +} + input, *[contenteditable], *[contenteditable] * { diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 8b5514885..d0f126519 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -10,6 +10,7 @@ import React, { useSyncExternalStore, } from 'react' +import classNames from 'classnames' import { Canvas } from './components/Canvas' import { OptionalErrorBoundary } from './components/ErrorBoundary' import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback' @@ -92,6 +93,11 @@ export interface TldrawEditorBaseProps { * The editor's initial state (usually the id of the first active tool). */ initialState?: string + + /** + * A classname to pass to the editor's container. + */ + className?: string } /** @@ -119,9 +125,10 @@ const EMPTY_TOOLS_ARRAY = [] as const export const TldrawEditor = memo(function TldrawEditor({ store, components, + className, ...rest }: TldrawEditorProps) { - const [container, setContainer] = React.useState(null) + const [container, rContainer] = React.useState(null) const user = useMemo(() => createTLUser(), []) const ErrorFallback = @@ -137,7 +144,12 @@ export const TldrawEditor = memo(function TldrawEditor({ } return ( -
+
annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })} diff --git a/packages/editor/src/lib/components/Canvas.tsx b/packages/editor/src/lib/components/Canvas.tsx index efea6fe99..dfa78653a 100644 --- a/packages/editor/src/lib/components/Canvas.tsx +++ b/packages/editor/src/lib/components/Canvas.tsx @@ -188,9 +188,9 @@ function HandlesWrapper() { const isChangingStyle = useValue('isChangingStyle', () => editor.instanceState.isChangingStyle, [ editor, ]) - const isReadOnly = useValue('isChangingStyle', () => editor.instanceState.isReadOnly, [editor]) + const isReadonly = useValue('isChangingStyle', () => editor.instanceState.isReadonly, [editor]) - if (!Handles || !onlySelectedShape || isChangingStyle || isReadOnly) return null + if (!Handles || !onlySelectedShape || isChangingStyle || isReadonly) return null const handles = editor.getHandles(onlySelectedShape) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index fa7d8d3fb..6ae7da32a 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -43,6 +43,7 @@ import { annotateError, assert, compact, + debounce, dedupe, deepCopy, getOwnProperty, @@ -306,19 +307,32 @@ export class Editor extends EventEmitter { const container = this.getContainer() - const handleFocus = () => this.updateInstanceState({ isFocused: true }) - const handleBlur = () => this.updateInstanceState({ isFocused: false }) + // We need to debounce this because when focus changes, the body + // becomes focused for a brief moment. Debouncing means that we + // check only when focus stops changing: when it settles, what + // has it settled on? If it's settled on the container or something + // inside of the container, then focus or preserve the current focus; + // if not, then turn off focus. Turning off focus is a trigger to + // also turn off keyboard shortcuts and other things. + const updateFocus = debounce(() => { + const { activeElement } = document + const { isFocused } = this.instanceState + const hasFocus = container === activeElement || container.contains(activeElement) + if ((!isFocused && hasFocus) || (isFocused && !hasFocus)) { + this.updateInstanceState({ isFocused: hasFocus }) + } + }, 32) - container.addEventListener('focusin', handleFocus) - container.addEventListener('focus', handleFocus) - container.addEventListener('focusout', handleBlur) - container.addEventListener('blur', handleBlur) + container.addEventListener('focusin', updateFocus) + container.addEventListener('focus', updateFocus) + container.addEventListener('focusout', updateFocus) + container.addEventListener('blur', updateFocus) this.disposables.add(() => { - container.removeEventListener('focusin', handleFocus) - container.removeEventListener('focus', handleFocus) - container.removeEventListener('focusout', handleBlur) - container.removeEventListener('blur', handleBlur) + container.removeEventListener('focusin', updateFocus) + container.removeEventListener('focus', updateFocus) + container.removeEventListener('focusout', updateFocus) + container.removeEventListener('blur', updateFocus) }) this.store.ensureStoreIsUsable() @@ -1046,26 +1060,6 @@ export class Editor extends EventEmitter { return paths.some((path) => this.isIn(path)) } - /** - * The id of the current selected tool. - * - * @public - */ - @computed get currentToolId(): string { - const { currentTool } = this - if (!currentTool) return '' - return currentTool.currentToolIdMask ?? currentTool.id - } - - /** - * The current selected tool. - * - * @public - */ - @computed get currentTool(): StateNode | undefined { - return this.root.current.value - } - /** * Set the selected tool. * @@ -1084,6 +1078,25 @@ export class Editor extends EventEmitter { this.root.transition(id, info) return this } + /** + * The current selected tool. + * + * @public + */ + @computed get currentTool(): StateNode | undefined { + return this.root.current.value + } + + /** + * The id of the current selected tool. + * + * @public + */ + @computed get currentToolId(): string { + const { currentTool } = this + if (!currentTool) return '' + return currentTool.currentToolIdMask ?? currentTool.id + } /** * Get a descendant by its path. @@ -3276,7 +3289,7 @@ export class Editor extends EventEmitter { private _updatePage = this.history.createCommand( 'updatePage', (partial: RequiredKeys, squashing = false) => { - if (this.instanceState.isReadOnly) return null + if (this.instanceState.isReadonly) return null const prev = this.getPageById(partial.id) @@ -3326,7 +3339,7 @@ export class Editor extends EventEmitter { private _createPage = this.history.createCommand( 'createPage', (title: string, id: TLPageId = PageRecordType.createId(), belowPageIndex?: string) => { - if (this.instanceState.isReadOnly) return null + if (this.instanceState.isReadonly) return null if (this.pages.length >= MAX_PAGES) return null const pageInfo = this.pages const topIndex = belowPageIndex ?? pageInfo[pageInfo.length - 1]?.index ?? 'a1' @@ -3408,7 +3421,7 @@ export class Editor extends EventEmitter { private _deletePage = this.history.createCommand( 'delete_page', (id: TLPageId) => { - if (this.instanceState.isReadOnly) return null + if (this.instanceState.isReadonly) return null const { pages } = this if (pages.length === 1) return null @@ -3492,7 +3505,7 @@ export class Editor extends EventEmitter { * @public */ renamePage(id: TLPageId, name: string, squashing = false) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this this.updatePage({ id, name }, squashing) return this } @@ -3533,7 +3546,7 @@ export class Editor extends EventEmitter { private _createAssets = this.history.createCommand( 'createAssets', (assets: TLAsset[]) => { - if (this.instanceState.isReadOnly) return null + if (this.instanceState.isReadonly) return null if (assets.length <= 0) return null return { data: { assets } } @@ -3569,7 +3582,7 @@ export class Editor extends EventEmitter { private _updateAssets = this.history.createCommand( 'updateAssets', (assets: TLAssetPartial[]) => { - if (this.instanceState.isReadOnly) return + if (this.instanceState.isReadonly) return if (assets.length <= 0) return const snapshots: Record = {} @@ -3616,7 +3629,7 @@ export class Editor extends EventEmitter { private _deleteAssets = this.history.createCommand( 'deleteAssets', (ids: TLAssetId[]) => { - if (this.instanceState.isReadOnly) return + if (this.instanceState.isReadonly) return if (ids.length <= 0) return const prev = compact(ids.map((id) => this.store.get(id))) @@ -5236,7 +5249,7 @@ export class Editor extends EventEmitter { */ moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this { if (ids.length === 0) return this - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this const { currentPageId } = this @@ -5293,7 +5306,7 @@ export class Editor extends EventEmitter { * @public */ toggleLock(ids: TLShapeId[] = this.selectedIds): this { - if (this.instanceState.isReadOnly || ids.length === 0) return this + if (this.instanceState.isReadonly || ids.length === 0) return this let allLocked = true, allUnlocked = true @@ -5414,7 +5427,7 @@ export class Editor extends EventEmitter { * @public */ flipShapes(operation: 'horizontal' | 'vertical', ids: TLShapeId[] = this.selectedIds) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this let shapes = compact(ids.map((id) => this.getShapeById(id))) @@ -5478,7 +5491,7 @@ export class Editor extends EventEmitter { ids: TLShapeId[] = this.currentPageState.selectedIds, gap?: number ) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this const shapes = compact(ids.map((id) => this.getShapeById(id))).filter((shape) => { if (!shape) return false @@ -5605,7 +5618,7 @@ export class Editor extends EventEmitter { * @param padding - The padding to apply to the packed shapes. */ packShapes(ids: TLShapeId[] = this.currentPageState.selectedIds, padding = 16) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (ids.length < 2) return this const shapes = compact( @@ -5764,7 +5777,7 @@ export class Editor extends EventEmitter { operation: 'left' | 'center-horizontal' | 'right' | 'top' | 'center-vertical' | 'bottom', ids: TLShapeId[] = this.currentPageState.selectedIds ) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (ids.length < 2) return this const shapes = compact(ids.map((id) => this.getShapeById(id))) @@ -5851,7 +5864,7 @@ export class Editor extends EventEmitter { operation: 'horizontal' | 'vertical', ids: TLShapeId[] = this.currentPageState.selectedIds ) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (ids.length < 3) return this const len = ids.length @@ -5936,7 +5949,7 @@ export class Editor extends EventEmitter { operation: 'horizontal' | 'vertical', ids: TLShapeId[] = this.currentPageState.selectedIds ) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (ids.length < 2) return this const shapes = compact(ids.map((id) => this.getShapeById(id))) @@ -6024,7 +6037,7 @@ export class Editor extends EventEmitter { mode?: TLResizeMode } = {} ) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (!Number.isFinite(scale.x)) scale = new Vec2d(1, scale.y) if (!Number.isFinite(scale.y)) scale = new Vec2d(scale.x, 1) @@ -6296,7 +6309,7 @@ export class Editor extends EventEmitter { private _createShapes = this.history.createCommand( 'createShapes', (partials: TLShapePartial[], select = false) => { - if (this.instanceState.isReadOnly) return null + if (this.instanceState.isReadonly) return null if (partials.length <= 0) return null const { currentPageShapeIds: shapeIds, selectedIds } = this @@ -6584,7 +6597,7 @@ export class Editor extends EventEmitter { * @public */ groupShapes(ids: TLShapeId[] = this.selectedIds, groupId = createShapeId()) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (ids.length <= 1) return this @@ -6639,7 +6652,7 @@ export class Editor extends EventEmitter { * @public */ ungroupShapes(ids: TLShapeId[] = this.selectedIds) { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (ids.length === 0) return this // Only ungroup when the select tool is active @@ -6730,7 +6743,7 @@ export class Editor extends EventEmitter { private _updateShapes = this.history.createCommand( 'updateShapes', (_partials: (TLShapePartial | null | undefined)[], squashing = false) => { - if (this.instanceState.isReadOnly) return null + if (this.instanceState.isReadonly) return null const partials = compact(_partials) @@ -6853,7 +6866,7 @@ export class Editor extends EventEmitter { private _deleteShapes = this.history.createCommand( 'delete_shapes', (ids: TLShapeId[]) => { - if (this.instanceState.isReadOnly) return null + if (this.instanceState.isReadonly) return null if (ids.length === 0) return null const prevSelectedIds = [...this.currentPageState.selectedIds] @@ -7444,7 +7457,7 @@ export class Editor extends EventEmitter { preserveIds?: boolean } = {} ): this { - if (this.instanceState.isReadOnly) return this + if (this.instanceState.isReadonly) return this if (!content.schema) { throw Error('Could not put content:\ncontent is missing a schema.') diff --git a/packages/editor/src/lib/editor/managers/SnapManager.ts b/packages/editor/src/lib/editor/managers/SnapManager.ts index 8ffe03fdd..06fb828e4 100644 --- a/packages/editor/src/lib/editor/managers/SnapManager.ts +++ b/packages/editor/src/lib/editor/managers/SnapManager.ts @@ -233,15 +233,17 @@ export class SnapManager { constructor(public readonly editor: Editor) {} @computed get snapPointsCache() { - return this.editor.store.createComputedCache('snapPoints', (shape) => { - const pageTransfrorm = this.editor.getPageTransformById(shape.id) + const { editor } = this + return editor.store.createComputedCache('snapPoints', (shape) => { + const pageTransfrorm = editor.getPageTransformById(shape.id) if (!pageTransfrorm) return undefined - const util = this.editor.getShapeUtil(shape) - const snapPoints = util.snapPoints(shape) - return snapPoints.map((point, i) => { - const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point) - return { x, y, id: `${shape.id}:${i}` } - }) + return editor + .getShapeUtil(shape) + .snapPoints(shape) + .map((point, i) => { + const { x, y } = Matrix2d.applyToPoint(pageTransfrorm, point) + return { x, y, id: `${shape.id}:${i}` } + }) }) } diff --git a/packages/editor/src/lib/editor/types/editor-state.ts b/packages/editor/src/lib/editor/types/editor-state.ts deleted file mode 100644 index 05c9d9dd2..000000000 --- a/packages/editor/src/lib/editor/types/editor-state.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { T } from '@tldraw/validate' - -export type TLinstanceState = { - canMoveCamera: boolean - isFocused: boolean - devicePixelRatio: number - isCoarsePointer: boolean - openMenus: string[] - isChangingStyle: boolean - isReadOnly: boolean -} - -export const instanceStateValidator = T.object({ - canMoveCamera: T.boolean, - isFocused: T.boolean, - devicePixelRatio: T.number, - isCoarsePointer: T.boolean, - openMenus: T.arrayOf(T.string), - isChangingStyle: T.boolean, - isReadOnly: T.boolean, -}) diff --git a/packages/editor/src/lib/hooks/useCanvasEvents.ts b/packages/editor/src/lib/hooks/useCanvasEvents.ts index f860fec5d..5051f22d1 100644 --- a/packages/editor/src/lib/hooks/useCanvasEvents.ts +++ b/packages/editor/src/lib/hooks/useCanvasEvents.ts @@ -80,7 +80,9 @@ export function useCanvasEvents() { function onTouchStart(e: React.TouchEvent) { ;(e as any).isKilled = true - document.body.click() // god damn it, but necessary for long presses to open the context menu + // 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) } diff --git a/packages/editor/src/lib/hooks/useDocumentEvents.ts b/packages/editor/src/lib/hooks/useDocumentEvents.ts index f2e0aed04..5a58b5c8c 100644 --- a/packages/editor/src/lib/hooks/useDocumentEvents.ts +++ b/packages/editor/src/lib/hooks/useDocumentEvents.ts @@ -32,6 +32,7 @@ export function useDocumentEvents() { const handleKeyDown = (e: KeyboardEvent) => { if ( e.altKey && + // todo: When should we allow the alt key to be used? Perhaps states should declare which keys matter to them? (editor.isIn('zoom') || !editor.root.path.value.endsWith('.idle')) && !isFocusingInput() ) { @@ -45,21 +46,14 @@ export function useDocumentEvents() { ;(e as any).isKilled = true switch (e.key) { - case '=': { - if (e.metaKey || e.ctrlKey) { - preventDefault(e) - return - } - break - } - case '-': { - if (e.metaKey || e.ctrlKey) { - preventDefault(e) - return - } - break - } + case '=': + case '-': case '0': { + // These keys are used for zooming. Technically we only use + // the + - and 0 keys, however it's common for them to be + // paired with modifier keys (command / control) so we need + // to prevent the browser's regular actions (i.e. zooming + // the page). A user can zoom by unfocusing the editor. if (e.metaKey || e.ctrlKey) { preventDefault(e) return @@ -73,6 +67,9 @@ export function useDocumentEvents() { break } case ',': { + // todo: extract to extension + // This seems very fragile; the comma key here is used to send pointer events, + // but that means it also needs to know about pen mode, hovered ids, etc. if (!isFocusingInput()) { preventDefault(e) if (!editor.inputs.keys.has('Comma')) { diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 3377d7100..65834a469 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -796,7 +796,7 @@ export function useMenuSchema(): TLUiMenuSchema; export function useNativeClipboardEvents(): void; // @public (undocumented) -export function useReadOnly(): boolean; +export function useReadonly(): boolean; // @public (undocumented) export function useToasts(): TLUiToastsContextType; diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index bb9ec47fe..3d8b01b82 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -83,7 +83,7 @@ export { type TLUiMenuSchemaContextType, type TLUiMenuSchemaProviderProps, } from './lib/ui/hooks/useMenuSchema' -export { useReadOnly } from './lib/ui/hooks/useReadOnly' +export { useReadonly } from './lib/ui/hooks/useReadonly' export { useToasts, type TLUiToast, diff --git a/packages/tldraw/src/lib/canvas/TldrawSelectionForeground.tsx b/packages/tldraw/src/lib/canvas/TldrawSelectionForeground.tsx index 0912fdd89..f2d66176f 100644 --- a/packages/tldraw/src/lib/canvas/TldrawSelectionForeground.tsx +++ b/packages/tldraw/src/lib/canvas/TldrawSelectionForeground.tsx @@ -12,7 +12,7 @@ import { } from '@tldraw/editor' import classNames from 'classnames' import { useRef } from 'react' -import { useReadOnly } from '../ui/hooks/useReadOnly' +import { useReadonly } from '../ui/hooks/useReadonly' import { CropHandles } from './CropHandles' const IS_FIREFOX = @@ -25,7 +25,7 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track( const editor = useEditor() const rSvg = useRef(null) - const isReadonlyMode = useReadOnly() + const isReadonlyMode = useReadonly() const topEvents = useSelectionEvents('top') const rightEvents = useSelectionEvents('right') const bottomEvents = useSelectionEvents('bottom') diff --git a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx index 6fdb7d29f..e77d203c0 100644 --- a/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.tsx @@ -557,7 +557,7 @@ export class ArrowShapeUtil extends ShapeUtil { 'select.pointing_handle', 'select.dragging_handle', 'arrow.dragging' - ) && !this.editor.instanceState.isReadOnly + ) && !this.editor.instanceState.isReadonly const info = this.editor.getArrowInfo(shape) const bounds = this.editor.getBounds(shape) diff --git a/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts b/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts index fe2939b96..33fa7c6ae 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/children/Idle.ts @@ -87,7 +87,7 @@ export class Idle extends StateNode { break } case 'handle': { - if (this.editor.instanceState.isReadOnly) break + if (this.editor.instanceState.isReadonly) break if (this.editor.inputs.altKey) { this.parent.transition('pointing_shape', info) } else { @@ -142,12 +142,12 @@ export class Idle extends StateNode { switch (info.target) { case 'canvas': { // Create text shape and transition to editing_shape - if (this.editor.instanceState.isReadOnly) break + if (this.editor.instanceState.isReadonly) break this.handleDoubleClickOnCanvas(info) break } case 'selection': { - if (this.editor.instanceState.isReadOnly) break + if (this.editor.instanceState.isReadonly) break const { onlySelectedShape } = this.editor if (onlySelectedShape) { @@ -191,7 +191,7 @@ export class Idle extends StateNode { if ( shape.type !== 'video' && shape.type !== 'embed' && - this.editor.instanceState.isReadOnly + this.editor.instanceState.isReadonly ) break @@ -221,7 +221,7 @@ export class Idle extends StateNode { break } case 'handle': { - if (this.editor.instanceState.isReadOnly) break + if (this.editor.instanceState.isReadonly) break const { shape, handle } = info const util = this.editor.getShapeUtil(shape) @@ -307,7 +307,7 @@ export class Idle extends StateNode { } override onKeyUp = (info: TLKeyboardEventInfo) => { - if (this.editor.instanceState.isReadOnly) { + if (this.editor.instanceState.isReadonly) { switch (info.code) { case 'Enter': { if (this.shouldStartEditingShape() && this.editor.onlySelectedShape) { diff --git a/packages/tldraw/src/lib/tools/SelectTool/children/PointingSelection.ts b/packages/tldraw/src/lib/tools/SelectTool/children/PointingSelection.ts index 9f7ab7f0e..f4d7bca32 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/children/PointingSelection.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/children/PointingSelection.ts @@ -18,7 +18,7 @@ export class PointingSelection extends StateNode { override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => { if (this.editor.inputs.isDragging) { - if (this.editor.instanceState.isReadOnly) return + if (this.editor.instanceState.isReadonly) return this.parent.transition('translating', info) } } diff --git a/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts b/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts index 416a09ff8..8ad75f403 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/children/PointingShape.ts @@ -121,7 +121,7 @@ export class PointingShape extends StateNode { override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => { if (this.editor.inputs.isDragging) { - if (this.editor.instanceState.isReadOnly) return + if (this.editor.instanceState.isReadonly) return this.parent.transition('translating', info) } } diff --git a/packages/tldraw/src/lib/ui/TldrawUi.tsx b/packages/tldraw/src/lib/ui/TldrawUi.tsx index ab86d36f4..ab90149b1 100644 --- a/packages/tldraw/src/lib/ui/TldrawUi.tsx +++ b/packages/tldraw/src/lib/ui/TldrawUi.tsx @@ -1,5 +1,5 @@ import { ToastProvider } from '@radix-ui/react-toast' -import { useEditor, useValue } from '@tldraw/editor' +import { preventDefault, useEditor, useValue } from '@tldraw/editor' import classNames from 'classnames' import React, { ReactNode } from 'react' import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider' @@ -119,7 +119,7 @@ const TldrawUiContent = React.memo(function TldrawUI({ const editor = useEditor() const msg = useTranslation() const breakpoint = useBreakpoint() - const isReadonlyMode = useValue('isReadOnlyMode', () => editor.instanceState.isReadOnly, [editor]) + const isReadonlyMode = useValue('isReadonlyMode', () => editor.instanceState.isReadonly, [editor]) const isFocusMode = useValue('focus', () => editor.instanceState.isFocusMode, [editor]) const isDebugMode = useValue('debug', () => editor.instanceState.isDebugMode, [editor]) @@ -135,6 +135,7 @@ const TldrawUiContent = React.memo(function TldrawUI({ className={classNames('tlui-layout', { 'tlui-layout__mobile': breakpoint < 5, })} + onPointerDown={preventDefault} > {isFocusMode ? (
diff --git a/packages/tldraw/src/lib/ui/components/ActionsMenu.tsx b/packages/tldraw/src/lib/ui/components/ActionsMenu.tsx index 608cd35a3..14582acb1 100644 --- a/packages/tldraw/src/lib/ui/components/ActionsMenu.tsx +++ b/packages/tldraw/src/lib/ui/components/ActionsMenu.tsx @@ -3,7 +3,7 @@ import { useContainer } from '@tldraw/editor' import { memo } from 'react' import { TLUiMenuChild } from '../hooks/menuHelpers' import { useActionsMenuSchema } from '../hooks/useActionsMenuSchema' -import { useReadOnly } from '../hooks/useReadOnly' +import { useReadonly } from '../hooks/useReadonly' import { useTranslation } from '../hooks/useTranslation/useTranslation' import { Button } from './primitives/Button' import { Popover, PopoverTrigger } from './primitives/Popover' @@ -13,7 +13,7 @@ export const ActionsMenu = memo(function ActionsMenu() { const msg = useTranslation() const container = useContainer() const menuSchema = useActionsMenuSchema() - const isReadonly = useReadOnly() + const isReadonly = useReadonly() function getActionMenuItem(item: TLUiMenuChild) { if (isReadonly && !item.readonlyOk) return null diff --git a/packages/tldraw/src/lib/ui/components/ContextMenu.tsx b/packages/tldraw/src/lib/ui/components/ContextMenu.tsx index 5382d2619..afa09029d 100644 --- a/packages/tldraw/src/lib/ui/components/ContextMenu.tsx +++ b/packages/tldraw/src/lib/ui/components/ContextMenu.tsx @@ -6,7 +6,7 @@ import { TLUiMenuChild } from '../hooks/menuHelpers' import { useBreakpoint } from '../hooks/useBreakpoint' import { useContextMenuSchema } from '../hooks/useContextMenuSchema' import { useMenuIsOpen } from '../hooks/useMenuIsOpen' -import { useReadOnly } from '../hooks/useReadOnly' +import { useReadonly } from '../hooks/useReadonly' import { useTranslation } from '../hooks/useTranslation/useTranslation' import { MoveToPageMenu } from './MoveToPageMenu' import { Button } from './primitives/Button' @@ -66,7 +66,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any }) const [_, handleOpenChange] = useMenuIsOpen('context menu', cb) // If every item in the menu is readonly, then we don't want to show the menu - const isReadonly = useReadOnly() + const isReadonly = useReadonly() const noItemsToShow = contextTLUiMenuSchema.length === 0 || @@ -98,7 +98,7 @@ function ContextMenuContent() { const menuSchema = useContextMenuSchema() const [_, handleSubOpenChange] = useMenuIsOpen('context menu sub') - const isReadonly = useReadOnly() + const isReadonly = useReadonly() const breakpoint = useBreakpoint() const container = useContainer() diff --git a/packages/tldraw/src/lib/ui/components/HelpMenu.tsx b/packages/tldraw/src/lib/ui/components/HelpMenu.tsx index 6cf77b603..f3c6edb0d 100644 --- a/packages/tldraw/src/lib/ui/components/HelpMenu.tsx +++ b/packages/tldraw/src/lib/ui/components/HelpMenu.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import { TLUiMenuChild } from '../hooks/menuHelpers' import { useHelpMenuSchema } from '../hooks/useHelpMenuSchema' import { useMenuIsOpen } from '../hooks/useMenuIsOpen' -import { useReadOnly } from '../hooks/useReadOnly' +import { useReadonly } from '../hooks/useReadonly' import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey' import { useTranslation } from '../hooks/useTranslation/useTranslation' import { TLUiIconType } from '../icon-types' @@ -59,7 +59,7 @@ export const HelpMenu = React.memo(function HelpMenu() { function HelpMenuContent() { const menuSchema = useHelpMenuSchema() - const isReadonly = useReadOnly() + const isReadonly = useReadonly() function getHelpMenuItem(item: TLUiMenuChild) { if (isReadonly && !item.readonlyOk) return null diff --git a/packages/tldraw/src/lib/ui/components/KeyboardShortcutsDialog.tsx b/packages/tldraw/src/lib/ui/components/KeyboardShortcutsDialog.tsx index 9a454f8e4..98ea0116c 100644 --- a/packages/tldraw/src/lib/ui/components/KeyboardShortcutsDialog.tsx +++ b/packages/tldraw/src/lib/ui/components/KeyboardShortcutsDialog.tsx @@ -1,6 +1,6 @@ import { TLUiMenuChild } from '../hooks/menuHelpers' import { useKeyboardShortcutsSchema } from '../hooks/useKeyboardShortcutsSchema' -import { useReadOnly } from '../hooks/useReadOnly' +import { useReadonly } from '../hooks/useReadonly' import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey' import { useTranslation } from '../hooks/useTranslation/useTranslation' import * as Dialog from './primitives/Dialog' @@ -8,7 +8,7 @@ import { Kbd } from './primitives/Kbd' export const KeyboardShortcutsDialog = () => { const msg = useTranslation() - const isReadonly = useReadOnly() + const isReadonly = useReadonly() const shortcutsItems = useKeyboardShortcutsSchema() function getKeyboardShortcutItem(item: TLUiMenuChild) { diff --git a/packages/tldraw/src/lib/ui/components/Menu.tsx b/packages/tldraw/src/lib/ui/components/Menu.tsx index 995cb2c74..64d089247 100644 --- a/packages/tldraw/src/lib/ui/components/Menu.tsx +++ b/packages/tldraw/src/lib/ui/components/Menu.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { TLUiMenuChild } from '../hooks/menuHelpers' import { useBreakpoint } from '../hooks/useBreakpoint' import { useMenuSchema } from '../hooks/useMenuSchema' -import { useReadOnly } from '../hooks/useReadOnly' +import { useReadonly } from '../hooks/useReadonly' import { useTranslation } from '../hooks/useTranslation/useTranslation' import { LanguageMenu } from './LanguageMenu' import { Button } from './primitives/Button' @@ -35,7 +35,7 @@ function MenuContent() { const msg = useTranslation() const menuSchema = useMenuSchema() const breakpoint = useBreakpoint() - const isReadonly = useReadOnly() + const isReadonly = useReadonly() function getMenuItem( editor: Editor, diff --git a/packages/tldraw/src/lib/ui/components/MenuZone.tsx b/packages/tldraw/src/lib/ui/components/MenuZone.tsx index eb5129107..acf74b9bd 100644 --- a/packages/tldraw/src/lib/ui/components/MenuZone.tsx +++ b/packages/tldraw/src/lib/ui/components/MenuZone.tsx @@ -1,6 +1,6 @@ import { track, useEditor } from '@tldraw/editor' import { useBreakpoint } from '../hooks/useBreakpoint' -import { useReadOnly } from '../hooks/useReadOnly' +import { useReadonly } from '../hooks/useReadonly' import { ActionsMenu } from './ActionsMenu' import { DuplicateButton } from './DuplicateButton' import { Menu } from './Menu' @@ -13,7 +13,7 @@ export const MenuZone = track(function MenuZone() { const editor = useEditor() const breakpoint = useBreakpoint() - const isReadonly = useReadOnly() + const isReadonly = useReadonly() const showQuickActions = !isReadonly && !editor.isInAny('hand', 'zoom', 'eraser') diff --git a/packages/tldraw/src/lib/ui/components/PageMenu/PageMenu.tsx b/packages/tldraw/src/lib/ui/components/PageMenu/PageMenu.tsx index aa35c48cf..bb8b052b6 100644 --- a/packages/tldraw/src/lib/ui/components/PageMenu/PageMenu.tsx +++ b/packages/tldraw/src/lib/ui/components/PageMenu/PageMenu.tsx @@ -10,7 +10,7 @@ import { import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { useBreakpoint } from '../../hooks/useBreakpoint' import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' -import { useReadOnly } from '../../hooks/useReadOnly' +import { useReadonly } from '../../hooks/useReadonly' import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { Button } from '../primitives/Button' import { Icon } from '../primitives/Icon' @@ -36,7 +36,7 @@ export const PageMenu = function PageMenu() { const currentPage = useValue('currentPage', () => editor.currentPage, [editor]) // When in readonly mode, we don't allow a user to edit the pages - const isReadonlyMode = useReadOnly() + const isReadonlyMode = useReadonly() // If the user has reached the max page count, we disable the "add page" button const maxPageCountReached = useValue( diff --git a/packages/tldraw/src/lib/ui/components/StylePanel/StylePanel.tsx b/packages/tldraw/src/lib/ui/components/StylePanel/StylePanel.tsx index f80a04ca1..ec4f05607 100644 --- a/packages/tldraw/src/lib/ui/components/StylePanel/StylePanel.tsx +++ b/packages/tldraw/src/lib/ui/components/StylePanel/StylePanel.tsx @@ -142,7 +142,11 @@ function CommonStylePickerSet({ return ( <> -
+
{color === undefined ? null : ( (undefined) - const isReadOnly = useReadOnly() + const isReadonly = useReadonly() const toolbarItems = useToolbarSchema() const laserTool = toolbarItems.find((item) => item.toolItem.id === 'laser') @@ -36,8 +36,8 @@ export const Toolbar = memo(function Toolbar() { editor, ]) - const showEditingTools = !isReadOnly - const showExtraActions = !(isReadOnly || isHandTool) + const showEditingTools = !isReadonly + const showExtraActions = !(isReadonly || isHandTool) const getTitle = (item: TLUiToolItem) => item.label ? `${msg(item.label)} ${item.kbd ? kbdStr(item.kbd) : ''}` : '' @@ -110,7 +110,7 @@ export const Toolbar = memo(function Toolbar() {
- {!isReadOnly && ( + {!isReadonly && (
) })} - {isReadOnly && laserTool && ( + {isReadonly && laserTool && (
- {breakpoint < 5 && !isReadOnly && ( + {breakpoint < 5 && !isReadonly && (
diff --git a/packages/tldraw/src/lib/ui/components/TrashButton.tsx b/packages/tldraw/src/lib/ui/components/TrashButton.tsx index b6e94c118..7c3702124 100644 --- a/packages/tldraw/src/lib/ui/components/TrashButton.tsx +++ b/packages/tldraw/src/lib/ui/components/TrashButton.tsx @@ -1,6 +1,6 @@ import { track, useEditor } from '@tldraw/editor' import { useActions } from '../hooks/useActions' -import { useReadOnly } from '../hooks/useReadOnly' +import { useReadonly } from '../hooks/useReadonly' import { useTranslation } from '../hooks/useTranslation/useTranslation' import { Button } from './primitives/Button' import { kbdStr } from './primitives/shared' @@ -11,7 +11,7 @@ export const TrashButton = track(function TrashButton() { const msg = useTranslation() const action = actions['delete'] - const isReadonly = useReadOnly() + const isReadonly = useReadonly() if (isReadonly) return null diff --git a/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts b/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts index 581e533ca..42b5ecfb4 100644 --- a/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts +++ b/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts @@ -1,8 +1,8 @@ -import { preventDefault, useEditor } from '@tldraw/editor' +import { preventDefault, useEditor, useValue } from '@tldraw/editor' import hotkeys from 'hotkeys-js' import { useEffect } from 'react' import { useActions } from './useActions' -import { useReadOnly } from './useReadOnly' +import { useReadonly } from './useReadonly' import { useTools } from './useTools' const SKIP_KBDS = [ @@ -18,11 +18,14 @@ const SKIP_KBDS = [ export function useKeyboardShortcuts() { const editor = useEditor() - const isReadonly = useReadOnly() + const isReadonly = useReadonly() const actions = useActions() const tools = useTools() + const isFocused = useValue('is focused', () => editor.instanceState.isFocused, [editor]) useEffect(() => { + if (!isFocused) return + const container = editor.getContainer() hotkeys.setScope(editor.store.id) @@ -34,9 +37,7 @@ export function useKeyboardShortcuts() { // Add hotkeys for actions and tools. // Except those that in SKIP_KBDS! const areShortcutsDisabled = () => - (editor.instanceState.isFocused && editor.isMenuOpen) || - editor.editingId !== null || - editor.crashingError + editor.isMenuOpen || editor.editingId !== null || editor.crashingError for (const action of Object.values(actions)) { if (!action.kbd) continue @@ -51,7 +52,9 @@ export function useKeyboardShortcuts() { } for (const tool of Object.values(tools)) { - if (!tool.kbd || (!tool.readonlyOk && editor.instanceState.isReadOnly)) continue + if (!tool.kbd || (!tool.readonlyOk && editor.instanceState.isReadonly)) { + continue + } if (SKIP_KBDS.includes(tool.id)) continue @@ -65,7 +68,7 @@ export function useKeyboardShortcuts() { return () => { hotkeys.deleteScope(editor.store.id) } - }, [actions, tools, isReadonly, editor]) + }, [actions, tools, isReadonly, editor, isFocused]) } function getHotkeysStringFromKbd(kbd: string) { diff --git a/packages/tldraw/src/lib/ui/hooks/useReadOnly.ts b/packages/tldraw/src/lib/ui/hooks/useReadOnly.ts deleted file mode 100644 index 4dbcf5c1c..000000000 --- a/packages/tldraw/src/lib/ui/hooks/useReadOnly.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useEditor, useValue } from '@tldraw/editor' - -/** @public */ -export function useReadOnly() { - const editor = useEditor() - return useValue('isReadOnlyMode', () => editor.instanceState.isReadOnly, [editor]) -} diff --git a/packages/tldraw/src/lib/ui/hooks/useReadonly.ts b/packages/tldraw/src/lib/ui/hooks/useReadonly.ts new file mode 100644 index 000000000..f6aeaf798 --- /dev/null +++ b/packages/tldraw/src/lib/ui/hooks/useReadonly.ts @@ -0,0 +1,7 @@ +import { useEditor, useValue } from '@tldraw/editor' + +/** @public */ +export function useReadonly() { + const editor = useEditor() + return useValue('isReadonlyMode', () => editor.instanceState.isReadonly, [editor]) +} diff --git a/packages/tldraw/src/test/Editor.test.tsx b/packages/tldraw/src/test/Editor.test.tsx index b1f7a3148..09b19b15b 100644 --- a/packages/tldraw/src/test/Editor.test.tsx +++ b/packages/tldraw/src/test/Editor.test.tsx @@ -285,7 +285,7 @@ describe("App's default tool", () => { }) it('Is hand for readonly mode', () => { editor = new TestEditor() - editor.updateInstanceState({ isReadOnly: true }) + editor.updateInstanceState({ isReadonly: true }) editor.setCurrentTool('hand') expect(editor.currentToolId).toBe('hand') }) @@ -369,37 +369,42 @@ describe('isFocused', () => { }) it('becomes true when the container div receives a focus event', () => { + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(false) editor.elm.focus() + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(true) }) it('becomes false when the container div receives a blur event', () => { editor.elm.focus() + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(true) editor.elm.blur() + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(false) }) - it('becomes true when a child of the app container div receives a focusin event', () => { + it.skip('becomes true when a child of the app container div receives a focusin event', () => { + // We need to skip this one because it's not actually true: the focusin event will bubble + // to the document.body, resulting in that being the active element. In reality, the editor's + // container would also have received a focus event, and after the editor's debounce ends, + // the container (or one of its descendants) will be the focused element. editor.elm.blur() - const child = document.createElement('div') editor.elm.appendChild(child) - + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(false) - child.dispatchEvent(new FocusEvent('focusin', { bubbles: true })) - + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(true) - child.dispatchEvent(new FocusEvent('focusout', { bubbles: true })) - + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(false) }) @@ -413,6 +418,7 @@ describe('isFocused', () => { child.dispatchEvent(new FocusEvent('focusout', { bubbles: true })) + jest.advanceTimersByTime(100) expect(editor.instanceState.isFocused).toBe(false) }) diff --git a/packages/tldraw/src/test/SelectTool.test.ts b/packages/tldraw/src/test/SelectTool.test.ts index 0484d89f9..ceb386a3b 100644 --- a/packages/tldraw/src/test/SelectTool.test.ts +++ b/packages/tldraw/src/test/SelectTool.test.ts @@ -412,7 +412,7 @@ describe('When in readonly mode', () => { props: { w: 100, h: 100, url: '' }, }, ]) - editor.updateInstanceState({ isReadOnly: true }) + editor.updateInstanceState({ isReadonly: true }) editor.setCurrentTool('hand') editor.setCurrentTool('select') }) @@ -420,7 +420,7 @@ describe('When in readonly mode', () => { it('Begins editing embed when double clicked', () => { expect(editor.editingId).toBe(null) expect(editor.selectedIds.length).toBe(0) - expect(editor.instanceState.isReadOnly).toBe(true) + expect(editor.instanceState.isReadonly).toBe(true) const shape = editor.getShapeById(ids.embed1) editor.doubleClick(100, 100, { target: 'shape', shape }) @@ -430,7 +430,7 @@ describe('When in readonly mode', () => { it('Begins editing embed when pressing Enter on a selected embed', () => { expect(editor.editingId).toBe(null) expect(editor.selectedIds.length).toBe(0) - expect(editor.instanceState.isReadOnly).toBe(true) + expect(editor.instanceState.isReadonly).toBe(true) editor.setSelectedIds([ids.embed1]) expect(editor.selectedIds.length).toBe(1) diff --git a/packages/tldraw/src/test/groups.test.ts b/packages/tldraw/src/test/groups.test.ts index 7487f8c25..5607bcd53 100644 --- a/packages/tldraw/src/test/groups.test.ts +++ b/packages/tldraw/src/test/groups.test.ts @@ -279,7 +279,7 @@ describe('creating groups', () => { // │ A │ │ B │ │ C │ // └───┘ └───┘ └───┘ editor.createShapes([box(ids.boxA, 0, 0), box(ids.boxB, 20, 0), box(ids.boxC, 40, 0)]) - editor.updateInstanceState({ isReadOnly: true }) + editor.updateInstanceState({ isReadonly: true }) editor.setCurrentTool('hand') editor.selectAll() expect(editor.selectedIds.length).toBe(3) @@ -491,7 +491,7 @@ describe('ungrouping shapes', () => { expect(editor.selectedIds.length).toBe(3) editor.groupShapes() expect(editor.selectedIds.length).toBe(1) - editor.updateInstanceState({ isReadOnly: true }) + editor.updateInstanceState({ isReadonly: true }) editor.setCurrentTool('hand') editor.ungroupShapes() diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 12ad0ed3a..ee3c9f3b2 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -1020,7 +1020,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> { // (undocumented) isPenMode: boolean; // (undocumented) - isReadOnly: boolean; + isReadonly: boolean; // (undocumented) isToolLocked: boolean; // (undocumented) diff --git a/packages/tlschema/src/migrations.test.ts b/packages/tlschema/src/migrations.test.ts index a0e8d1397..697da7439 100644 --- a/packages/tlschema/src/migrations.test.ts +++ b/packages/tlschema/src/migrations.test.ts @@ -1341,6 +1341,22 @@ describe('adds lonely properties', () => { }) }) +describe('rename isReadOnly to isReadonly', () => { + const { up, down } = instanceMigrations.migrators[instanceVersions.ReadOnlyReadonly] + + test('up works as expected', () => { + expect(up({ isReadOnly: false })).toStrictEqual({ + isReadonly: false, + }) + }) + + test('down works as expected', () => { + expect(down({ isReadonly: false })).toStrictEqual({ + isReadOnly: false, + }) + }) +}) + /* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */ for (const migrator of allMigrators) { diff --git a/packages/tlschema/src/records/TLInstance.ts b/packages/tlschema/src/records/TLInstance.ts index 5f9730b0a..6d294869f 100644 --- a/packages/tlschema/src/records/TLInstance.ts +++ b/packages/tlschema/src/records/TLInstance.ts @@ -42,7 +42,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> { isCoarsePointer: boolean openMenus: string[] isChangingStyle: boolean - isReadOnly: boolean + isReadonly: boolean meta: JsonObject } @@ -87,7 +87,7 @@ export function createInstanceRecordType(stylesById: Map, }) ) @@ -124,7 +124,7 @@ export function createInstanceRecordType(stylesById: Map { @@ -438,6 +439,20 @@ export const instanceMigrations = defineMigrations({ } }, }, + [instanceVersions.ReadOnlyReadonly]: { + up: ({ isReadOnly: _isReadOnly, ...record }) => { + return { + ...record, + isReadonly: _isReadOnly, + } + }, + down: ({ isReadonly: _isReadonly, ...record }) => { + return { + ...record, + isReadOnly: _isReadonly, + } + }, + }, }, })