tldraw
tldraw copied to clipboard
[Feature]: Improve layout composition flexibility between UI and Editor
What's the feature?
Context: I am writing a custom UI and it inevitably needs to useEditor to interact with Editor state. However, it seems that the EditorContext is not exposed in any way, so we need to structure the toolbar as being a child of TldrawEditor —the component in which EditorContext.Provider is declared.
What I'm trying to achieve is decouple the Editor's Canvas and the Toolbar, and rely on the browser's layout engine to place them as I see fit (e.g using flexbox, relative positioning, etc), instead of trying to stub things with absolutes.
One way to work around this is to try to layout things within TldrawEditor:
globalCss`
.tl-container {
position: relative;
display: flex;
}
`
const CustomEditor = () => {
<TldrawEditor> {/* .tl-container */}
<CustomUI>
<Canvas>
</TldrawEditor>
}
However, it seems that tools rely on the TldrawEditor's ref-element (the one tagged with .tl-container) positioning to draw shapes, so if I try to move the canvas around within the TldrawEditor, shapes are displayed with this unexpected offset.
Proposed solution
However, it'd be best if consumers were not bound by this implicit layout expectation from tools, as this greatly limits how one can layout the canvas and a custom UI. Ideally, tldraw should export EditorContext so that consumers can layout however they see fit.
For instance, this is what I'm doing to get the job done:
diff --git a/node_modules/@tldraw/editor/dist-cjs/index.d.ts b/node_modules/@tldraw/editor/dist-cjs/index.d.ts
index 6c05c8f..110b6de 100644
--- a/node_modules/@tldraw/editor/dist-cjs/index.d.ts
+++ b/node_modules/@tldraw/editor/dist-cjs/index.d.ts
@@ -5571,6 +5571,9 @@ export declare function useContainer(): HTMLDivElement;
/** @public */
export declare const useEditor: () => Editor;
+/** @public */
+export declare const EditorContext: React.Context<Editor | null>;
+
/** @public */
export declare function useIsCropping(shapeId: TLShapeId): boolean;
diff --git a/node_modules/@tldraw/editor/dist-cjs/index.js b/node_modules/@tldraw/editor/dist-cjs/index.js
index aa1e3bb..f369f9a 100644
--- a/node_modules/@tldraw/editor/dist-cjs/index.js
+++ b/node_modules/@tldraw/editor/dist-cjs/index.js
@@ -54,6 +54,7 @@ __export(src_exports, {
EVENT_NAME_MAP: () => import_event_types.EVENT_NAME_MAP,
Edge2d: () => import_Edge2d.Edge2d,
Editor: () => import_Editor.Editor,
+ EditorContext: () => import_useEditor.EditorContext,
Ellipse2d: () => import_Ellipse2d.Ellipse2d,
ErrorBoundary: () => import_ErrorBoundary.ErrorBoundary,
ErrorScreen: () => import_TldrawEditor.ErrorScreen,
diff --git a/node_modules/@tldraw/editor/dist-esm/index.d.mts b/node_modules/@tldraw/editor/dist-esm/index.d.mts
index 6c05c8f..110b6de 100644
--- a/node_modules/@tldraw/editor/dist-esm/index.d.mts
+++ b/node_modules/@tldraw/editor/dist-esm/index.d.mts
@@ -5571,6 +5571,9 @@ export declare function useContainer(): HTMLDivElement;
/** @public */
export declare const useEditor: () => Editor;
+/** @public */
+export declare const EditorContext: React.Context<Editor | null>;
+
/** @public */
export declare function useIsCropping(shapeId: TLShapeId): boolean;
diff --git a/node_modules/@tldraw/editor/dist-esm/index.mjs b/node_modules/@tldraw/editor/dist-esm/index.mjs
index 68c95c9..57ac5bb 100644
--- a/node_modules/@tldraw/editor/dist-esm/index.mjs
+++ b/node_modules/@tldraw/editor/dist-esm/index.mjs
@@ -136,7 +136,7 @@ import {
} from "./lib/editor/types/event-types.mjs";
import { useContainer } from "./lib/hooks/useContainer.mjs";
import { getCursor } from "./lib/hooks/useCursor.mjs";
-import { useEditor } from "./lib/hooks/useEditor.mjs";
+import { useEditor, EditorContext } from "./lib/hooks/useEditor.mjs";
import { useIsCropping } from "./lib/hooks/useIsCropping.mjs";
import { useIsDarkMode } from "./lib/hooks/useIsDarkMode.mjs";
import { useIsEditing } from "./lib/hooks/useIsEditing.mjs";
@@ -295,6 +295,7 @@ export {
EVENT_NAME_MAP,
Edge2d,
Editor,
+ EditorContext,
Ellipse2d,
ErrorBoundary,
ErrorScreen,
Contact Details
Code of Conduct
- [X] I agree to follow this project's Code of Conduct
I think this is easy enough and would bring amazing customization
Hey!
You can definitely move the UI out of the TldrawEditor component. To make that work just use the onMount prop, which should give you the editor once it's mounted.
Created a quick example (based on the CustomUiExample from the repo):
https://github.com/tldraw/tldraw/blob/16f9054f71b421c8904bf724caa78ba7b3e7c06f/apps/examples/src/examples/CustomUiExample/CustomUiExample.tsx#L11
Would this work for your use case?
Hey! You can definitely move the UI out of the
TldrawEditorcomponent. To make that work just use theonMountprop, which should give you the editor once it's mounted. Created a quick example (based on theCustomUiExamplefrom the repo):https://github.com/tldraw/tldraw/blob/16f9054f71b421c8904bf724caa78ba7b3e7c06f/apps/examples/src/examples/CustomUiExample/CustomUiExample.tsx#L11
Would this work for your use case?
That is a good point. We currently do this dance of picking up the Editor instance onMount, and broadcast it over as an EditorContext.Provider value, so it can be referenced outside of TldrawEditor. We do this because we still want to leverage other key functionality in a decoupled toolbar that relies on EditorContext:
useActions, for undo, redo and others:
https://github.com/tldraw/tldraw/blob/431ce73476f6116f3234b4d667fd3752f140ff89/packages/tldraw/src/lib/ui/hooks/useActions.tsx#L73-L74
useKeyboardShortcuts and useNativeClipboardEvents, both of which need to be on the same sub-tree as the toolbar, because it depends on ActionContextProvider and UiEventsProvider:
https://github.com/tldraw/tldraw/blob/431ce73476f6116f3234b4d667fd3752f140ff89/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts#L18-L20
https://github.com/tldraw/tldraw/blob/431ce73476f6116f3234b4d667fd3752f140ff89/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts#L624-L626
Personally and maintenance-wise, I'd love to not have to do this dance of capturing the Editor instance on a onMount callback. It adds unnecessary complexity as this is me trying to work around Tldraw and React so I can layout components the way I need. However, this could be solved by exposing the Editor state dependency via parameters rather than React contexts.
Folding this up into a newer issue https://github.com/tldraw/tldraw/issues/3860