import { tlenv, useContainer, useEditor, useReactor, useValue } from '@tldraw/editor' import classNames from 'classnames' import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react' import { TLUiAssetUrlOverrides } from './assetUrls' import { SkipToMainContent } from './components/A11y' import { TldrawUiButton } from './components/primitives/Button/TldrawUiButton' import { TldrawUiButtonIcon } from './components/primitives/Button/TldrawUiButtonIcon' import { PORTRAIT_BREAKPOINT, PORTRAIT_BREAKPOINTS } from './constants' import { useActions } from './context/actions' import { useBreakpoint } from './context/breakpoints' import { TLUiComponents, useTldrawUiComponents } from './context/components' import { TLUiContextProviderProps, TldrawUiContextProvider, } from './context/TldrawUiContextProvider' import { useNativeClipboardEvents } from './hooks/useClipboardEvents' import { useEditorEvents } from './hooks/useEditorEvents' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useReadonly } from './hooks/useReadonly' import { useDirection, useTranslation } from './hooks/useTranslation/useTranslation' /** @public */ export interface TldrawUiProps extends TLUiContextProviderProps { /** * The component's children. */ children?: ReactNode /** * Whether to hide the user interface and only display the canvas. */ hideUi?: boolean /** * Overrides for the UI components. */ components?: TLUiComponents /** * Additional items to add to the debug menu (will be deprecated) */ renderDebugMenuItems?(): React.ReactNode /** Asset URL override. */ assetUrls?: TLUiAssetUrlOverrides } /** * @public * @react */ export const TldrawUi = React.memo(function TldrawUi({ renderDebugMenuItems, children, hideUi, components, ...rest }: TldrawUiProps) { return ( {children} ) }) interface TldrawUiContentProps { hideUi?: boolean shareZone?: ReactNode topZone?: ReactNode renderDebugMenuItems?(): React.ReactNode } const TldrawUiInner = React.memo(function TldrawUiInner({ children, hideUi, ...rest }: TldrawUiContentProps & { children: ReactNode }) { // The hideUi prop should prevent the UI from mounting. // If we ever need want the UI to mount and preserve state, then // we should change this behavior and hide the UI via CSS instead. // Keyboard shortcuts and clipboard events should always be mounted, // even when the UI is hidden. useKeyboardShortcuts() useNativeClipboardEvents() return ( <> {children} {hideUi ? null : } ) }) const TldrawUiContent = React.memo(function TldrawUI() { const editor = useEditor() const msg = useTranslation() const breakpoint = useBreakpoint() const isReadonlyMode = useReadonly() const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [editor]) const isDebugMode = useValue('debug', () => editor.getInstanceState().isDebugMode, [editor]) const container = useContainer() const dir = useDirection() const locale = useValue('locale', () => editor.user.getLocale(), [editor]) useEffect(() => { container.dir = dir container.lang = locale }, [container, dir, locale]) const { SharePanel, TopPanel, MenuPanel, StylePanel, Toolbar, HelpMenu, NavigationPanel, HelperButtons, DebugPanel, Toasts, Dialogs, A11y, } = useTldrawUiComponents() useEditorEvents() const rIsEditingAnything = useRef(false) const rHidingTimeout = useRef(-1 as any) const [hideToolbarWhileEditing, setHideToolbarWhileEditing] = useState(false) useReactor( 'update hide toolbar while delayed', () => { const isMobileEnvironment = tlenv.isIos || tlenv.isAndroid if (!isMobileEnvironment) return const editingShape = editor.getEditingShapeId() if (editingShape === null) { if (rIsEditingAnything.current) { rIsEditingAnything.current = false clearTimeout(rHidingTimeout.current) if (tlenv.isAndroid) { // On Android, hide it after 150ms rHidingTimeout.current = editor.timers.setTimeout(() => { setHideToolbarWhileEditing(false) }, 150) } else { // On iOS, just hide it immediately setHideToolbarWhileEditing(false) } } return } if (!rIsEditingAnything.current) { rIsEditingAnything.current = true clearTimeout(rHidingTimeout.current) setHideToolbarWhileEditing(true) } }, [] ) const { 'toggle-focus-mode': toggleFocus } = useActions() const { breakpointsAbove, breakpointsBelow } = useMemo(() => { const breakpointsAbove = [] const breakpointsBelow = [] for (let bp = 0; bp < PORTRAIT_BREAKPOINTS.length; bp++) { if (bp <= breakpoint) { breakpointsAbove.push(bp) } else { breakpointsBelow.push(bp) } } return { breakpointsAbove, breakpointsBelow } }, [breakpoint]) return (
{isFocusMode ? (
toggleFocus.onSelect('menu')} >
) : ( <>
{MenuPanel && } {HelperButtons && }
{TopPanel && }
{SharePanel && } {StylePanel && breakpoint >= PORTRAIT_BREAKPOINT.TABLET_SM && !isReadonlyMode && ( )}
{NavigationPanel && } {Toolbar && } {HelpMenu && }
{isDebugMode && DebugPanel && } {A11y && }
)} {Toasts && } {Dialogs && }
) }) /** @public @react */ export function TldrawUiInFrontOfTheCanvas() { const { RichTextToolbar, ImageToolbar, VideoToolbar, CursorChatBubble, FollowingIndicator } = useTldrawUiComponents() return ( <> {RichTextToolbar && } {ImageToolbar && } {VideoToolbar && } {FollowingIndicator && } {CursorChatBubble && } ) }