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:
parent
a466ffe92a
commit
1bf2820a3f
15 changed files with 146 additions and 126 deletions
|
@ -9,6 +9,7 @@ import {
|
|||
TldrawScribble,
|
||||
TldrawSelectionBackground,
|
||||
TldrawSelectionForeground,
|
||||
TldrawShapeIndicators,
|
||||
TldrawUi,
|
||||
defaultBindingUtils,
|
||||
defaultEditorAssetUrls,
|
||||
|
@ -24,6 +25,7 @@ import 'tldraw/tldraw.css'
|
|||
// [1]
|
||||
const defaultComponents = {
|
||||
Scribble: TldrawScribble,
|
||||
ShapeIndicators: TldrawShapeIndicators,
|
||||
CollaboratorScribble: TldrawScribble,
|
||||
SelectionForeground: TldrawSelectionForeground,
|
||||
SelectionBackground: TldrawSelectionBackground,
|
||||
|
|
|
@ -653,6 +653,9 @@ export function DefaultSelectionForeground({ bounds, rotation }: TLSelectionFore
|
|||
// @public (undocumented)
|
||||
export const DefaultShapeIndicator: NamedExoticComponent<TLShapeIndicatorProps>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const DefaultShapeIndicators: NamedExoticComponent<object>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function DefaultSnapIndicator({ className, line, zoom }: TLSnapIndicatorProps): JSX_2.Element;
|
||||
|
||||
|
@ -1772,7 +1775,7 @@ export function openWindow(url: string, target?: string): void;
|
|||
// @internal (undocumented)
|
||||
export function OptionalErrorBoundary({ children, fallback, ...props }: Omit<TLErrorBoundaryProps, 'fallback'> & {
|
||||
fallback: TLErrorFallbackComponent;
|
||||
}): JSX_2.Element;
|
||||
}): boolean | JSX_2.Element | Iterable<React_3.ReactNode> | null | number | string | undefined;
|
||||
|
||||
// @public (undocumented)
|
||||
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
@ -2669,6 +2672,8 @@ export interface TLEditorComponents {
|
|||
// (undocumented)
|
||||
ShapeIndicatorErrorFallback?: TLShapeIndicatorErrorFallbackComponent;
|
||||
// (undocumented)
|
||||
ShapeIndicators?: ComponentType | null;
|
||||
// (undocumented)
|
||||
SnapIndicator?: ComponentType<TLSnapIndicatorProps> | null;
|
||||
// (undocumented)
|
||||
Spinner?: ComponentType | null;
|
||||
|
|
|
@ -96,6 +96,7 @@ export {
|
|||
type TLShapeIndicatorProps,
|
||||
} from './lib/components/default-components/DefaultShapeIndicator'
|
||||
export { type TLShapeIndicatorErrorFallbackComponent } from './lib/components/default-components/DefaultShapeIndicatorErrorFallback'
|
||||
export { DefaultShapeIndicators } from './lib/components/default-components/DefaultShapeIndicators'
|
||||
export {
|
||||
DefaultSnapIndicator,
|
||||
type TLSnapIndicatorProps,
|
||||
|
|
|
@ -453,7 +453,7 @@ function Layout({ children, onMount }: { children: ReactNode; onMount?: TLOnMoun
|
|||
useForceUpdate()
|
||||
useOnMount(onMount)
|
||||
|
||||
return <>{children}</>
|
||||
return children
|
||||
}
|
||||
|
||||
function Crash({ crashingError }: { crashingError: unknown }): null {
|
||||
|
|
|
@ -46,7 +46,7 @@ export function OptionalErrorBoundary({
|
|||
fallback: TLErrorFallbackComponent
|
||||
}) {
|
||||
if (fallback === null) {
|
||||
return <>{children}</>
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -9,13 +9,7 @@ import { usePresence } from '../hooks/usePresence'
|
|||
|
||||
export const LiveCollaborators = track(function Collaborators() {
|
||||
const peerIds = usePeerIds()
|
||||
return (
|
||||
<>
|
||||
{peerIds.map((id) => (
|
||||
<CollaboratorGuard key={id} collaboratorId={id} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
return peerIds.map((id) => <CollaboratorGuard key={id} collaboratorId={id} />)
|
||||
})
|
||||
|
||||
const CollaboratorGuard = track(function CollaboratorGuard({
|
||||
|
|
|
@ -33,7 +33,7 @@ export interface TLCanvasComponentProps {
|
|||
export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
||||
const editor = useEditor()
|
||||
|
||||
const { Background, SvgDefs } = useEditorComponents()
|
||||
const { Background, SvgDefs, ShapeIndicators } = useEditorComponents()
|
||||
|
||||
const rCanvas = useRef<HTMLDivElement>(null)
|
||||
const rHtmlLayer = useRef<HTMLDivElement>(null)
|
||||
|
@ -161,7 +161,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
<BrushWrapper />
|
||||
<ScribbleWrapper />
|
||||
<ZoomBrushWrapper />
|
||||
<ShapeIndicators />
|
||||
{ShapeIndicators && <ShapeIndicators />}
|
||||
<HintedShapeIndicator />
|
||||
<SnapIndicatorWrapper />
|
||||
<SelectionForegroundWrapper />
|
||||
|
@ -201,18 +201,9 @@ function ScribbleWrapper() {
|
|||
|
||||
if (!(Scribble && scribbles.length)) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{scribbles.map((scribble) => (
|
||||
<Scribble
|
||||
key={scribble.id}
|
||||
className="tl-user-scribble"
|
||||
scribble={scribble}
|
||||
zoom={zoomLevel}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
return scribbles.map((scribble) => (
|
||||
<Scribble key={scribble.id} className="tl-user-scribble" scribble={scribble} zoom={zoomLevel} />
|
||||
))
|
||||
}
|
||||
|
||||
function BrushWrapper() {
|
||||
|
@ -243,13 +234,9 @@ function SnapIndicatorWrapper() {
|
|||
|
||||
if (!(SnapIndicator && lines.length > 0)) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{lines.map((line) => (
|
||||
<SnapIndicator key={line.id} className="tl-user-snapline" line={line} zoom={zoomLevel} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
return lines.map((line) => (
|
||||
<SnapIndicator key={line.id} className="tl-user-snapline" line={line} zoom={zoomLevel} />
|
||||
))
|
||||
}
|
||||
|
||||
function HandlesWrapper() {
|
||||
|
@ -388,16 +375,12 @@ function ShapesWithSVGs() {
|
|||
[editor]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderingShapes.map((result) => (
|
||||
<Fragment key={result.id + '_fragment'}>
|
||||
<Shape {...result} dprMultiple={dprMultiple} />
|
||||
<DebugSvgCopy id={result.id} />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
return renderingShapes.map((result) => (
|
||||
<Fragment key={result.id + '_fragment'}>
|
||||
<Shape {...result} dprMultiple={dprMultiple} />
|
||||
<DebugSvgCopy id={result.id} />
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
function ReflowIfNeeded() {
|
||||
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() {
|
||||
const editor = useEditor()
|
||||
const { ShapeIndicator } = useEditorComponents()
|
||||
|
@ -521,13 +440,9 @@ function HintedShapeIndicator() {
|
|||
if (!ids.length) return null
|
||||
if (!ShapeIndicator) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{ids.map((id) => (
|
||||
<ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
return ids.map((id) => (
|
||||
<ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
|
||||
))
|
||||
}
|
||||
|
||||
function CursorDef() {
|
||||
|
|
|
@ -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)} />
|
||||
))
|
||||
})
|
|
@ -39,6 +39,7 @@ import {
|
|||
DefaultShapeIndicatorErrorFallback,
|
||||
TLShapeIndicatorErrorFallbackComponent,
|
||||
} from '../components/default-components/DefaultShapeIndicatorErrorFallback'
|
||||
import { DefaultShapeIndicators } from '../components/default-components/DefaultShapeIndicators'
|
||||
import {
|
||||
DefaultSnapIndicator,
|
||||
TLSnapIndicatorProps,
|
||||
|
@ -53,6 +54,7 @@ export interface TLEditorComponents {
|
|||
SvgDefs?: ComponentType | null
|
||||
Brush?: ComponentType<TLBrushProps> | null
|
||||
ZoomBrush?: ComponentType<TLBrushProps> | null
|
||||
ShapeIndicators?: ComponentType | null
|
||||
ShapeIndicator?: ComponentType<TLShapeIndicatorProps> | null
|
||||
Cursor?: ComponentType<TLCursorProps> | null
|
||||
Canvas?: ComponentType<TLCanvasComponentProps> | null
|
||||
|
@ -114,6 +116,7 @@ export function EditorComponentsProvider({
|
|||
Spinner: DefaultSpinner,
|
||||
SelectionBackground: DefaultSelectionBackground,
|
||||
SelectionForeground: DefaultSelectionForeground,
|
||||
ShapeIndicators: DefaultShapeIndicators,
|
||||
ShapeIndicator: DefaultShapeIndicator,
|
||||
OnTheCanvas: null,
|
||||
InFrontOfTheCanvas: null,
|
||||
|
|
|
@ -1686,6 +1686,9 @@ export const TldrawSelectionBackground: ({ bounds, rotation }: TLSelectionBackgr
|
|||
// @public (undocumented)
|
||||
export const TldrawSelectionForeground: MemoExoticComponent<({ bounds, rotation, }: TLSelectionForegroundProps) => JSX_2.Element | null>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function TldrawShapeIndicators(): JSX_2.Element | null;
|
||||
|
||||
// @public (undocumented)
|
||||
export const TldrawUi: React_3.NamedExoticComponent<TldrawUiProps>;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ export { TldrawHandles } from './lib/canvas/TldrawHandles'
|
|||
export { TldrawScribble } from './lib/canvas/TldrawScribble'
|
||||
export { TldrawSelectionBackground } from './lib/canvas/TldrawSelectionBackground'
|
||||
export { TldrawSelectionForeground } from './lib/canvas/TldrawSelectionForeground'
|
||||
export { TldrawShapeIndicators } from './lib/canvas/TldrawShapeIndicators'
|
||||
export { defaultBindingUtils } from './lib/defaultBindingUtils'
|
||||
export { type TLExternalContentProps } from './lib/defaultExternalContentHandlers'
|
||||
export { defaultShapeTools } from './lib/defaultShapeTools'
|
||||
|
|
|
@ -21,6 +21,7 @@ import { TldrawHandles } from './canvas/TldrawHandles'
|
|||
import { TldrawScribble } from './canvas/TldrawScribble'
|
||||
import { TldrawSelectionBackground } from './canvas/TldrawSelectionBackground'
|
||||
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
|
||||
import { TldrawShapeIndicators } from './canvas/TldrawShapeIndicators'
|
||||
import { defaultBindingUtils } from './defaultBindingUtils'
|
||||
import {
|
||||
TLExternalContentProps,
|
||||
|
@ -72,6 +73,7 @@ export function Tldraw(props: TldrawProps) {
|
|||
const componentsWithDefault = useMemo(
|
||||
() => ({
|
||||
Scribble: TldrawScribble,
|
||||
ShapeIndicators: TldrawShapeIndicators,
|
||||
CollaboratorScribble: TldrawScribble,
|
||||
SelectionForeground: TldrawSelectionForeground,
|
||||
SelectionBackground: TldrawSelectionBackground,
|
||||
|
|
26
packages/tldraw/src/lib/canvas/TldrawShapeIndicators.tsx
Normal file
26
packages/tldraw/src/lib/canvas/TldrawShapeIndicators.tsx
Normal 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 />
|
||||
}
|
|
@ -47,13 +47,7 @@ const Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
|
|||
function _Dialogs() {
|
||||
const { dialogs } = useDialogs()
|
||||
|
||||
return (
|
||||
<>
|
||||
{dialogs.map((dialog: TLUiDialog) => (
|
||||
<Dialog key={dialog.id} {...dialog} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
return dialogs.map((dialog: TLUiDialog) => <Dialog key={dialog.id} {...dialog} />)
|
||||
}
|
||||
|
||||
export const Dialogs = React.memo(_Dialogs)
|
||||
|
|
|
@ -83,13 +83,7 @@ function Toast({ toast }: { toast: TLUiToast }) {
|
|||
function _Toasts() {
|
||||
const { toasts } = useToasts()
|
||||
|
||||
return (
|
||||
<>
|
||||
{toasts.map((toast) => (
|
||||
<Toast key={toast.id} toast={toast} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
return toasts.map((toast) => <Toast key={toast.id} toast={toast} />)
|
||||
}
|
||||
|
||||
export const Toasts = React.memo(_Toasts)
|
||||
|
|
Loading…
Reference in a new issue