Add component for ShapeIndicators (#4083)

This PR adds a component for `ShapeIndicators` to the UI component
overrides. It moves the "select tool" state logic up to the new
`TldrawShapeIndicators` component.

### Change type

- [ ] `bugfix`
- [x] `improvement`
- [ ] `feature`
- [x] `api`
- [ ] `other`

### Release notes

- Added new `ShapeIndicators` component to `components` object.
- Added new `TldrawShapeIndicators` component.
This commit is contained in:
Steve Ruiz 2024-07-05 11:41:03 +01:00 committed by GitHub
parent a466ffe92a
commit 1bf2820a3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 146 additions and 126 deletions

View file

@ -9,6 +9,7 @@ import {
TldrawScribble, TldrawScribble,
TldrawSelectionBackground, TldrawSelectionBackground,
TldrawSelectionForeground, TldrawSelectionForeground,
TldrawShapeIndicators,
TldrawUi, TldrawUi,
defaultBindingUtils, defaultBindingUtils,
defaultEditorAssetUrls, defaultEditorAssetUrls,
@ -24,6 +25,7 @@ import 'tldraw/tldraw.css'
// [1] // [1]
const defaultComponents = { const defaultComponents = {
Scribble: TldrawScribble, Scribble: TldrawScribble,
ShapeIndicators: TldrawShapeIndicators,
CollaboratorScribble: TldrawScribble, CollaboratorScribble: TldrawScribble,
SelectionForeground: TldrawSelectionForeground, SelectionForeground: TldrawSelectionForeground,
SelectionBackground: TldrawSelectionBackground, SelectionBackground: TldrawSelectionBackground,

View file

@ -653,6 +653,9 @@ export function DefaultSelectionForeground({ bounds, rotation }: TLSelectionFore
// @public (undocumented) // @public (undocumented)
export const DefaultShapeIndicator: NamedExoticComponent<TLShapeIndicatorProps>; export const DefaultShapeIndicator: NamedExoticComponent<TLShapeIndicatorProps>;
// @public (undocumented)
export const DefaultShapeIndicators: NamedExoticComponent<object>;
// @public (undocumented) // @public (undocumented)
export function DefaultSnapIndicator({ className, line, zoom }: TLSnapIndicatorProps): JSX_2.Element; export function DefaultSnapIndicator({ className, line, zoom }: TLSnapIndicatorProps): JSX_2.Element;
@ -1772,7 +1775,7 @@ export function openWindow(url: string, target?: string): void;
// @internal (undocumented) // @internal (undocumented)
export function OptionalErrorBoundary({ children, fallback, ...props }: Omit<TLErrorBoundaryProps, 'fallback'> & { export function OptionalErrorBoundary({ children, fallback, ...props }: Omit<TLErrorBoundaryProps, 'fallback'> & {
fallback: TLErrorFallbackComponent; fallback: TLErrorFallbackComponent;
}): JSX_2.Element; }): boolean | JSX_2.Element | Iterable<React_3.ReactNode> | null | number | string | undefined;
// @public (undocumented) // @public (undocumented)
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
@ -2669,6 +2672,8 @@ export interface TLEditorComponents {
// (undocumented) // (undocumented)
ShapeIndicatorErrorFallback?: TLShapeIndicatorErrorFallbackComponent; ShapeIndicatorErrorFallback?: TLShapeIndicatorErrorFallbackComponent;
// (undocumented) // (undocumented)
ShapeIndicators?: ComponentType | null;
// (undocumented)
SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null; SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null;
// (undocumented) // (undocumented)
Spinner?: ComponentType | null; Spinner?: ComponentType | null;

View file

@ -96,6 +96,7 @@ export {
type TLShapeIndicatorProps, type TLShapeIndicatorProps,
} from './lib/components/default-components/DefaultShapeIndicator' } from './lib/components/default-components/DefaultShapeIndicator'
export { type TLShapeIndicatorErrorFallbackComponent } from './lib/components/default-components/DefaultShapeIndicatorErrorFallback' export { type TLShapeIndicatorErrorFallbackComponent } from './lib/components/default-components/DefaultShapeIndicatorErrorFallback'
export { DefaultShapeIndicators } from './lib/components/default-components/DefaultShapeIndicators'
export { export {
DefaultSnapIndicator, DefaultSnapIndicator,
type TLSnapIndicatorProps, type TLSnapIndicatorProps,

View file

@ -453,7 +453,7 @@ function Layout({ children, onMount }: { children: ReactNode; onMount?: TLOnMoun
useForceUpdate() useForceUpdate()
useOnMount(onMount) useOnMount(onMount)
return <>{children}</> return children
} }
function Crash({ crashingError }: { crashingError: unknown }): null { function Crash({ crashingError }: { crashingError: unknown }): null {

View file

@ -46,7 +46,7 @@ export function OptionalErrorBoundary({
fallback: TLErrorFallbackComponent fallback: TLErrorFallbackComponent
}) { }) {
if (fallback === null) { if (fallback === null) {
return <>{children}</> return children
} }
return ( return (

View file

@ -9,13 +9,7 @@ import { usePresence } from '../hooks/usePresence'
export const LiveCollaborators = track(function Collaborators() { export const LiveCollaborators = track(function Collaborators() {
const peerIds = usePeerIds() const peerIds = usePeerIds()
return ( return peerIds.map((id) => <CollaboratorGuard key={id} collaboratorId={id} />)
<>
{peerIds.map((id) => (
<CollaboratorGuard key={id} collaboratorId={id} />
))}
</>
)
}) })
const CollaboratorGuard = track(function CollaboratorGuard({ const CollaboratorGuard = track(function CollaboratorGuard({

View file

@ -33,7 +33,7 @@ export interface TLCanvasComponentProps {
export function DefaultCanvas({ className }: TLCanvasComponentProps) { export function DefaultCanvas({ className }: TLCanvasComponentProps) {
const editor = useEditor() const editor = useEditor()
const { Background, SvgDefs } = useEditorComponents() const { Background, SvgDefs, ShapeIndicators } = useEditorComponents()
const rCanvas = useRef<HTMLDivElement>(null) const rCanvas = useRef<HTMLDivElement>(null)
const rHtmlLayer = useRef<HTMLDivElement>(null) const rHtmlLayer = useRef<HTMLDivElement>(null)
@ -161,7 +161,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
<BrushWrapper /> <BrushWrapper />
<ScribbleWrapper /> <ScribbleWrapper />
<ZoomBrushWrapper /> <ZoomBrushWrapper />
<ShapeIndicators /> {ShapeIndicators && <ShapeIndicators />}
<HintedShapeIndicator /> <HintedShapeIndicator />
<SnapIndicatorWrapper /> <SnapIndicatorWrapper />
<SelectionForegroundWrapper /> <SelectionForegroundWrapper />
@ -201,18 +201,9 @@ function ScribbleWrapper() {
if (!(Scribble && scribbles.length)) return null if (!(Scribble && scribbles.length)) return null
return ( return scribbles.map((scribble) => (
<> <Scribble key={scribble.id} className="tl-user-scribble" scribble={scribble} zoom={zoomLevel} />
{scribbles.map((scribble) => ( ))
<Scribble
key={scribble.id}
className="tl-user-scribble"
scribble={scribble}
zoom={zoomLevel}
/>
))}
</>
)
} }
function BrushWrapper() { function BrushWrapper() {
@ -243,13 +234,9 @@ function SnapIndicatorWrapper() {
if (!(SnapIndicator && lines.length > 0)) return null if (!(SnapIndicator && lines.length > 0)) return null
return ( return lines.map((line) => (
<> <SnapIndicator key={line.id} className="tl-user-snapline" line={line} zoom={zoomLevel} />
{lines.map((line) => ( ))
<SnapIndicator key={line.id} className="tl-user-snapline" line={line} zoom={zoomLevel} />
))}
</>
)
} }
function HandlesWrapper() { function HandlesWrapper() {
@ -388,16 +375,12 @@ function ShapesWithSVGs() {
[editor] [editor]
) )
return ( return renderingShapes.map((result) => (
<> <Fragment key={result.id + '_fragment'}>
{renderingShapes.map((result) => ( <Shape {...result} dprMultiple={dprMultiple} />
<Fragment key={result.id + '_fragment'}> <DebugSvgCopy id={result.id} />
<Shape {...result} dprMultiple={dprMultiple} /> </Fragment>
<DebugSvgCopy id={result.id} /> ))
</Fragment>
))}
</>
)
} }
function ReflowIfNeeded() { function ReflowIfNeeded() {
const editor = useEditor() const editor = useEditor()
@ -448,70 +431,6 @@ function ShapesToDisplay() {
) )
} }
function ShapeIndicators() {
const editor = useEditor()
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
const idsToDisplay = useValue(
'should display selected ids',
() => {
// todo: move to tldraw selected ids wrappe
const prev = rPreviousSelectedShapeIds.current
const next = new Set<TLShapeId>()
if (
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.editing_shape',
'select.pointing_shape',
'select.pointing_selection',
'select.pointing_handle'
) &&
!editor.getInstanceState().isChangingStyle
) {
const selected = editor.getSelectedShapeIds()
for (const id of selected) {
next.add(id)
}
if (editor.isInAny('select.idle', 'select.editing_shape')) {
const instanceState = editor.getInstanceState()
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
const hovered = editor.getHoveredShapeId()
if (hovered) next.add(hovered)
}
}
}
if (prev.size !== next.size) {
rPreviousSelectedShapeIds.current = next
return next
}
for (const id of next) {
if (!prev.has(id)) {
rPreviousSelectedShapeIds.current = next
return next
}
}
return prev
},
[editor]
)
const { ShapeIndicator } = useEditorComponents()
if (!ShapeIndicator) return null
return (
<>
{renderingShapes.map(({ id }) => (
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
))}
</>
)
}
function HintedShapeIndicator() { function HintedShapeIndicator() {
const editor = useEditor() const editor = useEditor()
const { ShapeIndicator } = useEditorComponents() const { ShapeIndicator } = useEditorComponents()
@ -521,13 +440,9 @@ function HintedShapeIndicator() {
if (!ids.length) return null if (!ids.length) return null
if (!ShapeIndicator) return null if (!ShapeIndicator) return null
return ( return ids.map((id) => (
<> <ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
{ids.map((id) => ( ))
<ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
))}
</>
)
} }
function CursorDef() { function CursorDef() {

View file

@ -0,0 +1,80 @@
import { useValue } from '@tldraw/state'
import { TLShapeId } from '@tldraw/tlschema'
import { memo, useRef } from 'react'
import { useEditor } from '../../hooks/useEditor'
import { useEditorComponents } from '../../hooks/useEditorComponents'
/** @public @react */
export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
const editor = useEditor()
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
const idsToDisplay = useValue(
'should display selected ids',
() => {
const prev = rPreviousSelectedShapeIds.current
const next = new Set<TLShapeId>()
if (
// We only show indicators when in the following states...
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.editing_shape',
'select.pointing_shape',
'select.pointing_selection',
'select.pointing_handle'
) &&
// ...but we hide indicators when we've just changed a style (so that the user can see the change)
!editor.getInstanceState().isChangingStyle
) {
// We always want to show indicators for the selected shapes, if any
const selected = editor.getSelectedShapeIds()
for (const id of selected) {
next.add(id)
}
// If we're idle or editing a shape, we want to also show an indicator for the hovered shape, if any
if (editor.isInAny('select.idle', 'select.editing_shape')) {
const instanceState = editor.getInstanceState()
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
const hovered = editor.getHoveredShapeId()
if (hovered) next.add(hovered)
}
}
}
// Ok, has anything changed?
// If the number of items in the set is different, then the selection has changed. This catches most changes.
if (prev.size !== next.size) {
rPreviousSelectedShapeIds.current = next
return next
}
// If any of the new ids are not in the previous set, then the selection has changed
for (const id of next) {
if (!prev.has(id)) {
rPreviousSelectedShapeIds.current = next
return next
}
}
// If nothing has changed, then return the previous value
return prev
},
[editor]
)
// Show indicators only for the shapes that are currently being rendered (ie that are on screen)
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
const { ShapeIndicator } = useEditorComponents()
if (!ShapeIndicator) return null
return renderingShapes.map(({ id }) => (
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
))
})

View file

@ -39,6 +39,7 @@ import {
DefaultShapeIndicatorErrorFallback, DefaultShapeIndicatorErrorFallback,
TLShapeIndicatorErrorFallbackComponent, TLShapeIndicatorErrorFallbackComponent,
} from '../components/default-components/DefaultShapeIndicatorErrorFallback' } from '../components/default-components/DefaultShapeIndicatorErrorFallback'
import { DefaultShapeIndicators } from '../components/default-components/DefaultShapeIndicators'
import { import {
DefaultSnapIndicator, DefaultSnapIndicator,
TLSnapIndicatorProps, TLSnapIndicatorProps,
@ -53,6 +54,7 @@ export interface TLEditorComponents {
SvgDefs?: ComponentType | null SvgDefs?: ComponentType | null
Brush?: ComponentType<TLBrushProps> | null Brush?: ComponentType<TLBrushProps> | null
ZoomBrush?: ComponentType<TLBrushProps> | null ZoomBrush?: ComponentType<TLBrushProps> | null
ShapeIndicators?: ComponentType | null
ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
Cursor?: ComponentType<TLCursorProps> | null Cursor?: ComponentType<TLCursorProps> | null
Canvas?: ComponentType<TLCanvasComponentProps> | null Canvas?: ComponentType<TLCanvasComponentProps> | null
@ -114,6 +116,7 @@ export function EditorComponentsProvider({
Spinner: DefaultSpinner, Spinner: DefaultSpinner,
SelectionBackground: DefaultSelectionBackground, SelectionBackground: DefaultSelectionBackground,
SelectionForeground: DefaultSelectionForeground, SelectionForeground: DefaultSelectionForeground,
ShapeIndicators: DefaultShapeIndicators,
ShapeIndicator: DefaultShapeIndicator, ShapeIndicator: DefaultShapeIndicator,
OnTheCanvas: null, OnTheCanvas: null,
InFrontOfTheCanvas: null, InFrontOfTheCanvas: null,

View file

@ -1686,6 +1686,9 @@ export const TldrawSelectionBackground: ({ bounds, rotation }: TLSelectionBackgr
// @public (undocumented) // @public (undocumented)
export const TldrawSelectionForeground: MemoExoticComponent<({ bounds, rotation, }: TLSelectionForegroundProps) => JSX_2.Element | null>; export const TldrawSelectionForeground: MemoExoticComponent<({ bounds, rotation, }: TLSelectionForegroundProps) => JSX_2.Element | null>;
// @public (undocumented)
export function TldrawShapeIndicators(): JSX_2.Element | null;
// @public (undocumented) // @public (undocumented)
export const TldrawUi: React_3.NamedExoticComponent<TldrawUiProps>; export const TldrawUi: React_3.NamedExoticComponent<TldrawUiProps>;

View file

@ -10,6 +10,7 @@ export { TldrawHandles } from './lib/canvas/TldrawHandles'
export { TldrawScribble } from './lib/canvas/TldrawScribble' export { TldrawScribble } from './lib/canvas/TldrawScribble'
export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground' export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground'
export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground' export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground'
export { TldrawShapeIndicators } from './lib/canvas/TldrawShapeIndicators'
export { defaultBindingUtils } from './lib/defaultBindingUtils' export { defaultBindingUtils } from './lib/defaultBindingUtils'
export { type TLExternalContentProps } from './lib/defaultExternalContentHandlers' export { type TLExternalContentProps } from './lib/defaultExternalContentHandlers'
export { defaultShapeTools } from './lib/defaultShapeTools' export { defaultShapeTools } from './lib/defaultShapeTools'

View file

@ -21,6 +21,7 @@ import { TldrawHandles } from './canvas/TldrawHandles'
import { TldrawScribble } from './canvas/TldrawScribble' import { TldrawScribble } from './canvas/TldrawScribble'
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground' import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground' import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
import { TldrawShapeIndicators } from './canvas/TldrawShapeIndicators'
import { defaultBindingUtils } from './defaultBindingUtils' import { defaultBindingUtils } from './defaultBindingUtils'
import { import {
TLExternalContentProps, TLExternalContentProps,
@ -72,6 +73,7 @@ export function Tldraw(props: TldrawProps) {
const componentsWithDefault = useMemo( const componentsWithDefault = useMemo(
() => ({ () => ({
Scribble: TldrawScribble, Scribble: TldrawScribble,
ShapeIndicators: TldrawShapeIndicators,
CollaboratorScribble: TldrawScribble, CollaboratorScribble: TldrawScribble,
SelectionForeground: TldrawSelectionForeground, SelectionForeground: TldrawSelectionForeground,
SelectionBackground: TldrawSelectionBackground, SelectionBackground: TldrawSelectionBackground,

View file

@ -0,0 +1,26 @@
import { DefaultShapeIndicators, useEditor, useValue } from '@tldraw/editor'
/** @public @react */
export function TldrawShapeIndicators() {
const editor = useEditor()
const isInSelectState = useValue(
'is in a valid select state',
() => {
return editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.editing_shape',
'select.pointing_shape',
'select.pointing_selection',
'select.pointing_handle'
)
},
[editor]
)
if (!isInSelectState) return null
return <DefaultShapeIndicators />
}

View file

@ -47,13 +47,7 @@ const Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
function _Dialogs() { function _Dialogs() {
const { dialogs } = useDialogs() const { dialogs } = useDialogs()
return ( return dialogs.map((dialog: TLUiDialog) => <Dialog key={dialog.id} {...dialog} />)
<>
{dialogs.map((dialog: TLUiDialog) => (
<Dialog key={dialog.id} {...dialog} />
))}
</>
)
} }
export const Dialogs = React.memo(_Dialogs) export const Dialogs = React.memo(_Dialogs)

View file

@ -83,13 +83,7 @@ function Toast({ toast }: { toast: TLUiToast }) {
function _Toasts() { function _Toasts() {
const { toasts } = useToasts() const { toasts } = useToasts()
return ( return toasts.map((toast) => <Toast key={toast.id} toast={toast} />)
<>
{toasts.map((toast) => (
<Toast key={toast.id} toast={toast} />
))}
</>
)
} }
export const Toasts = React.memo(_Toasts) export const Toasts = React.memo(_Toasts)