Add support for custom cursor components (#994)
* Add support for custom cursor components * Add tests for the custom cursor props * Make the main tldraw app take the components prop * feat: add the ability to hide cursors * Update cursor props * Update imports Co-authored-by: Judicael <46365844+judicaelandria@users.noreply.github.com> Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
b7c968e2aa
commit
d721ae6a2f
16 changed files with 157 additions and 40 deletions
|
@ -1,4 +1,4 @@
|
|||
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
|
||||
import { Tldraw, TldrawProps, useFileSystem } from '@tldraw/tldraw'
|
||||
import * as React from 'react'
|
||||
import { useMultiplayerAssets } from '~hooks/useMultiplayerAssets'
|
||||
import { useMultiplayerState } from '~hooks/useMultiplayerState'
|
||||
|
|
|
@ -19,7 +19,7 @@ export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M
|
|||
abstract Indicator: (props: {
|
||||
shape: T
|
||||
meta: M
|
||||
user?: TLUser<T>
|
||||
user?: TLUser
|
||||
bounds: TLBounds
|
||||
isHovered: boolean
|
||||
isSelected: boolean
|
||||
|
|
|
@ -2,6 +2,10 @@ import * as React from 'react'
|
|||
import { mockDocument, renderWithContext } from '~test'
|
||||
import { Canvas } from './Canvas'
|
||||
|
||||
function TestCustomCursor() {
|
||||
return <div>Custom cursor</div>
|
||||
}
|
||||
|
||||
describe('page', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
expect(() =>
|
||||
|
@ -26,4 +30,29 @@ describe('page', () => {
|
|||
)
|
||||
).not.toThrowError()
|
||||
})
|
||||
|
||||
test('mounts component with custom cursors without crashing', () => {
|
||||
expect(() =>
|
||||
renderWithContext(
|
||||
<Canvas
|
||||
page={mockDocument.page}
|
||||
pageState={mockDocument.pageState}
|
||||
hideBounds={false}
|
||||
hideGrid={false}
|
||||
hideIndicators={false}
|
||||
hideHandles={false}
|
||||
hideBindingHandles={false}
|
||||
hideResizeHandles={false}
|
||||
hideCloneHandles={false}
|
||||
hideRotateHandle={false}
|
||||
showDashedBrush={false}
|
||||
onBoundsChange={() => {
|
||||
// noop
|
||||
}}
|
||||
assets={{}}
|
||||
components={{ Cursor: TestCustomCursor }}
|
||||
/>
|
||||
)
|
||||
).not.toThrowError()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { Brush } from '~components/Brush'
|
||||
import { Cursor, CursorComponent } from '~components/Cursor'
|
||||
import { EraseLine } from '~components/EraseLine'
|
||||
import { Grid } from '~components/Grid'
|
||||
import { Overlay } from '~components/Overlay'
|
||||
|
@ -36,7 +37,7 @@ export interface CanvasProps<T extends TLShape, M extends Record<string, unknown
|
|||
snapLines?: TLSnapLine[]
|
||||
eraseLine?: number[][]
|
||||
grid?: number
|
||||
users?: TLUsers<T>
|
||||
users?: TLUsers
|
||||
userId?: string
|
||||
hideBounds: boolean
|
||||
hideHandles: boolean
|
||||
|
@ -49,9 +50,13 @@ export interface CanvasProps<T extends TLShape, M extends Record<string, unknown
|
|||
showDashedBrush: boolean
|
||||
externalContainerRef?: React.RefObject<HTMLElement>
|
||||
performanceMode?: TLPerformanceMode
|
||||
components?: {
|
||||
Cursor?: CursorComponent
|
||||
}
|
||||
meta?: M
|
||||
id?: string
|
||||
onBoundsChange: (bounds: TLBounds) => void
|
||||
hideCursors?: boolean
|
||||
}
|
||||
|
||||
function _Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
||||
|
@ -64,9 +69,9 @@ function _Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
grid,
|
||||
users,
|
||||
userId,
|
||||
components = {},
|
||||
meta,
|
||||
performanceMode,
|
||||
externalContainerRef,
|
||||
showDashedBrush,
|
||||
hideHandles,
|
||||
hideBounds,
|
||||
|
@ -77,6 +82,7 @@ function _Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
hideRotateHandle,
|
||||
hideGrid,
|
||||
onBoundsChange,
|
||||
hideCursors,
|
||||
}: CanvasProps<T, M>) {
|
||||
const rCanvas = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
|
@ -128,7 +134,9 @@ function _Canvas<T extends TLShape, M extends Record<string, unknown>>({
|
|||
{pageState.brush && (
|
||||
<Brush brush={pageState.brush} dashed={showDashedBrush} zoom={pageState.camera.zoom} />
|
||||
)}
|
||||
{users && <Users userId={userId} users={users} />}
|
||||
{users && !hideCursors && (
|
||||
<Users userId={userId} users={users} Cursor={components?.Cursor ?? Cursor} />
|
||||
)}
|
||||
</div>
|
||||
<Overlay camera={pageState.camera}>
|
||||
{eraseLine && <EraseLine points={eraseLine} zoom={pageState.camera.zoom} />}
|
||||
|
|
27
packages/core/src/components/Cursor/Cursor.tsx
Normal file
27
packages/core/src/components/Cursor/Cursor.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import * as React from 'react'
|
||||
|
||||
interface CursorProps {
|
||||
id: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export type CursorComponent = (props: CursorProps) => any
|
||||
|
||||
export const Cursor: CursorComponent = React.memo(({ color }) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35 35" fill="none" fillRule="evenodd">
|
||||
<g fill="rgba(0,0,0,.2)" transform="translate(1,1)">
|
||||
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
||||
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
||||
</g>
|
||||
<g fill="white">
|
||||
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
||||
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
||||
</g>
|
||||
<g fill={color}>
|
||||
<path d="m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" />
|
||||
<path d="m13 10.814v11.188l2.969-2.866.428-.139h4.768z" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
})
|
1
packages/core/src/components/Cursor/index.ts
Normal file
1
packages/core/src/components/Cursor/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Cursor'
|
|
@ -7,6 +7,10 @@ import type { TLBounds, TLPage, TLPageState } from '~types'
|
|||
import Utils from '~utils'
|
||||
import { Renderer } from './Renderer'
|
||||
|
||||
function TestCustomCursor() {
|
||||
return <div>Custom cursor</div>
|
||||
}
|
||||
|
||||
describe('renderer', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
expect(() =>
|
||||
|
@ -19,6 +23,19 @@ describe('renderer', () => {
|
|||
)
|
||||
).not.toThrowError()
|
||||
})
|
||||
|
||||
test('mounts component with custom cursors without crashing', () => {
|
||||
expect(() =>
|
||||
render(
|
||||
<Renderer
|
||||
shapeUtils={mockUtils as any}
|
||||
page={mockDocument.page}
|
||||
pageState={mockDocument.pageState}
|
||||
components={{ Cursor: TestCustomCursor }}
|
||||
/>
|
||||
)
|
||||
).not.toThrowError()
|
||||
})
|
||||
})
|
||||
|
||||
// describe('When passing observables', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { Canvas } from '~/components/Canvas'
|
||||
import type { TLShapeUtilsMap } from '~TLShapeUtil'
|
||||
import { CursorComponent } from '~components/Cursor'
|
||||
import { TLContext, TLContextType, useTLTheme } from '~hooks'
|
||||
import { Inputs } from '~inputs'
|
||||
import type {
|
||||
|
@ -17,7 +17,7 @@ import type {
|
|||
TLUsers,
|
||||
} from '~types'
|
||||
|
||||
const EMPTY_OBJECT = {} as TLAssets
|
||||
const EMPTY_OBJECT = Object.freeze({}) as TLAssets
|
||||
|
||||
export type RendererProps<T extends TLShape, M = any> = Partial<TLCallbacks<T>> & {
|
||||
/**
|
||||
|
@ -46,6 +46,20 @@ export type RendererProps<T extends TLShape, M = any> = Partial<TLCallbacks<T>>
|
|||
* (optional) A ref for the renderer's container element, used for scoping event handlers.
|
||||
*/
|
||||
containerRef?: React.RefObject<HTMLElement>
|
||||
/**
|
||||
* (optional) Custom components to override parts of the default UI.
|
||||
*/
|
||||
components?: {
|
||||
/**
|
||||
* The component to render for multiplayer cursors.
|
||||
*/
|
||||
Cursor?: CursorComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* (optional) To hide cursors
|
||||
*/
|
||||
hideCursors?: boolean
|
||||
/**
|
||||
* (optional) An object of custom options that should be passed to rendered shapes.
|
||||
*/
|
||||
|
@ -53,7 +67,7 @@ export type RendererProps<T extends TLShape, M = any> = Partial<TLCallbacks<T>>
|
|||
/**
|
||||
* (optional) The current users to render.
|
||||
*/
|
||||
users?: TLUsers<T>
|
||||
users?: TLUsers
|
||||
/**
|
||||
* (optional) The current snap lines to render.
|
||||
*/
|
||||
|
@ -148,6 +162,7 @@ function _Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
grid,
|
||||
containerRef,
|
||||
performanceMode,
|
||||
components,
|
||||
hideHandles = false,
|
||||
hideIndicators = false,
|
||||
hideCloneHandles = false,
|
||||
|
@ -157,6 +172,7 @@ function _Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
hideBounds = false,
|
||||
hideGrid = true,
|
||||
showDashedBrush = false,
|
||||
hideCursors,
|
||||
...rest
|
||||
}: RendererProps<T, M>) {
|
||||
useTLTheme(theme, '#' + id)
|
||||
|
@ -216,7 +232,9 @@ function _Renderer<T extends TLShape, M extends Record<string, unknown>>({
|
|||
showDashedBrush={showDashedBrush}
|
||||
onBoundsChange={onBoundsChange}
|
||||
performanceMode={performanceMode}
|
||||
components={components}
|
||||
meta={meta}
|
||||
hideCursors={hideCursors}
|
||||
/>
|
||||
</TLContext.Provider>
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ export interface IndicatorProps<T extends TLShape, M = unknown> {
|
|||
isSelected?: boolean
|
||||
isHovered?: boolean
|
||||
isEditing?: boolean
|
||||
user?: TLUser<T>
|
||||
user?: TLUser
|
||||
}
|
||||
|
||||
function _ShapeIndicator<T extends TLShape, M>({
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import * as React from 'react'
|
||||
import type { TLShape, TLUser } from '~types'
|
||||
import { CursorComponent } from '~components/Cursor'
|
||||
import type { TLUser } from '~types'
|
||||
|
||||
interface UserProps {
|
||||
user: TLUser<TLShape>
|
||||
user: TLUser
|
||||
Cursor: CursorComponent
|
||||
}
|
||||
|
||||
export function User({ user }: UserProps) {
|
||||
const rCursor = React.useRef<SVGSVGElement>(null)
|
||||
export function User({ user, Cursor }: UserProps) {
|
||||
const rCursor = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
if (rCursor.current) {
|
||||
|
@ -15,26 +17,11 @@ export function User({ user }: UserProps) {
|
|||
}, [user.point])
|
||||
|
||||
return (
|
||||
<svg
|
||||
<div
|
||||
ref={rCursor}
|
||||
className={`tl-absolute tl-user tl-counter-scaled ${user.session ? '' : 'tl-animated'}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 35 35"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
>
|
||||
<g fill="rgba(0,0,0,.2)" transform="translate(1,1)">
|
||||
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
||||
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
||||
</g>
|
||||
<g fill="white">
|
||||
<path d="m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" />
|
||||
<path d="m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" />
|
||||
</g>
|
||||
<g fill={user.color}>
|
||||
<path d="m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z" />
|
||||
<path d="m13 10.814v11.188l2.969-2.866.428-.139h4.768z" />
|
||||
</g>
|
||||
</svg>
|
||||
<Cursor id={user.id} color={user.color} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import * as React from 'react'
|
||||
import { CursorComponent } from '~components/Cursor'
|
||||
import { User } from '~components/User/User'
|
||||
import type { TLShape, TLUsers } from '~types'
|
||||
import type { TLUsers } from '~types'
|
||||
|
||||
export interface UserProps {
|
||||
userId?: string
|
||||
users: TLUsers<TLShape>
|
||||
users: TLUsers
|
||||
Cursor: CursorComponent
|
||||
}
|
||||
|
||||
export function Users({ userId, users }: UserProps) {
|
||||
export function Users({ userId, users, Cursor }: UserProps) {
|
||||
return (
|
||||
<>
|
||||
{Object.values(users)
|
||||
.filter((user) => user && user.id !== userId)
|
||||
.map((user) => (
|
||||
<User key={user.id} user={user} />
|
||||
<User key={user.id} user={user} Cursor={Cursor} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import Utils from '~utils'
|
|||
interface UserIndicatorProps<T extends TLShape> {
|
||||
page: TLPage<any, any>
|
||||
userId: string
|
||||
users: TLUsers<T>
|
||||
users: TLUsers
|
||||
meta: any
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './Renderer'
|
||||
export * from './SVGContainer'
|
||||
export * from './HTMLContainer'
|
||||
export * from './Cursor'
|
||||
|
|
|
@ -44,7 +44,7 @@ export interface TLPageState {
|
|||
bindingId?: string | null
|
||||
}
|
||||
|
||||
export interface TLUser<T extends TLShape> {
|
||||
export interface TLUser {
|
||||
id: string
|
||||
color: string
|
||||
point: number[]
|
||||
|
@ -52,7 +52,7 @@ export interface TLUser<T extends TLShape> {
|
|||
session?: boolean
|
||||
}
|
||||
|
||||
export type TLUsers<T extends TLShape, U extends TLUser<T> = TLUser<T>> = Record<string, U>
|
||||
export type TLUsers = Record<string, TLUser>
|
||||
|
||||
export type TLSnapLine = number[][]
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Renderer } from '@tldraw/core'
|
||||
import { CursorComponent, Renderer } from '@tldraw/core'
|
||||
import * as React from 'react'
|
||||
import { ErrorBoundary as _Errorboundary } from 'react-error-boundary'
|
||||
import { IntlProvider } from 'react-intl'
|
||||
|
@ -102,6 +102,21 @@ export interface TldrawProps extends TDCallbacks {
|
|||
* bucket based solution will cause massive base64 string to be written to the liveblocks room.
|
||||
*/
|
||||
disableAssets?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Custom components to override parts of the default UI.
|
||||
*/
|
||||
components?: {
|
||||
/**
|
||||
* The component to render for multiplayer cursors.
|
||||
*/
|
||||
Cursor?: CursorComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* (optional) To hide cursors
|
||||
*/
|
||||
hideCursors?: boolean
|
||||
}
|
||||
|
||||
const isSystemDarkMode = window.matchMedia
|
||||
|
@ -123,6 +138,7 @@ export function Tldraw({
|
|||
readOnly = false,
|
||||
disableAssets = false,
|
||||
darkMode = isSystemDarkMode,
|
||||
components,
|
||||
onMount,
|
||||
onChange,
|
||||
onChangePresence,
|
||||
|
@ -143,6 +159,7 @@ export function Tldraw({
|
|||
onSessionStart,
|
||||
onSessionEnd,
|
||||
onExport,
|
||||
hideCursors,
|
||||
}: TldrawProps) {
|
||||
const [sId, setSId] = React.useState(id)
|
||||
|
||||
|
@ -336,6 +353,8 @@ export function Tldraw({
|
|||
showTools={showTools}
|
||||
showUI={showUI}
|
||||
readOnly={readOnly}
|
||||
components={components}
|
||||
hideCursors={hideCursors}
|
||||
/>
|
||||
</AlertDialogContext.Provider>
|
||||
</TldrawContext.Provider>
|
||||
|
@ -353,6 +372,10 @@ interface InnerTldrawProps {
|
|||
showStyles: boolean
|
||||
showUI: boolean
|
||||
showTools: boolean
|
||||
components?: {
|
||||
Cursor?: CursorComponent
|
||||
}
|
||||
hideCursors?: boolean
|
||||
}
|
||||
|
||||
const InnerTldraw = React.memo(function InnerTldraw({
|
||||
|
@ -366,6 +389,8 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
showTools,
|
||||
readOnly,
|
||||
showUI,
|
||||
components,
|
||||
hideCursors,
|
||||
}: InnerTldrawProps) {
|
||||
const app = useTldrawApp()
|
||||
const [dialogContainer, setDialogContainer] = React.useState<any>(null)
|
||||
|
@ -489,6 +514,8 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
|||
userId={room?.userId}
|
||||
theme={theme}
|
||||
meta={meta}
|
||||
components={components}
|
||||
hideCursors={hideCursors}
|
||||
hideBounds={hideBounds}
|
||||
hideHandles={hideHandles}
|
||||
hideResizeHandles={isHideResizeHandlesShape}
|
||||
|
|
|
@ -176,7 +176,7 @@ export enum TDUserStatus {
|
|||
}
|
||||
|
||||
// A TDUser, for multiplayer rooms
|
||||
export interface TDUser extends TLUser<TDShape> {
|
||||
export interface TDUser extends TLUser {
|
||||
activeShapes: TDShape[]
|
||||
status: TDUserStatus
|
||||
session?: boolean
|
||||
|
|
Loading…
Reference in a new issue