Skip to content

Commit 3758de8

Browse files
authored
[Fix] indicators hideAll / showAll (tldraw#5654)
This PR adds `showAll` and `hideAll` props to the `TLShapeIndicators` component. ## Context As an optimization, we mount indicators for all shapes and hide or show them dynamically using CSS. This is faster than mounting or unmounting them dynamically. There are certain states where we want to hide all of the indicators. We allow customization of this logic by overriding a the `ShapeIndicators` component. In tldraw's `ShapeIndicators` component override, we check to see if we're in one of the select tool's "hide the indicators" states and return `null` instead of the default indicators component. However, this means the indicators are unmounted and remounted whenever they're hidden or shown; and on larger projects, this can be a performance hit. ## Solution This PR provides `hideAll` and `showAll` props to the ShapeIndicators component so that we can allow parent components to control visibility in a more performant way. ### For later It would be good to move _all_ of the "hide indicators when in these states" logic out of the DefaultIndicators component, though this would be a breaking change. ### Change type - [x] `bugfix` - [] `improvement` - [ ] `feature` - [ ] `api` - [ ] `other` ### Release notes - Improved performance on large projects when hiding / showing shape indicators. - Added `hideAll` and `showAll` props to the `ShapeIndicators` component props
1 parent e999671 commit 3758de8

File tree

8 files changed

+82
-48
lines changed

8 files changed

+82
-48
lines changed

apps/examples/src/examples/indicators-logic/IndicatorsLogicExample.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const components: TLComponents = {
1212
[editor]
1313
)
1414

15-
// [2
15+
// [2]
1616
const { ShapeIndicator } = useEditorComponents()
1717
if (!ShapeIndicator) return null
1818

@@ -24,6 +24,10 @@ const components: TLComponents = {
2424
</div>
2525
)
2626
},
27+
// [3]
28+
// ShapeIndicators: () => {
29+
// return <DefaultShapeIndicators showAll />
30+
// },
2731
}
2832

2933
export default function IndicatorsLogicExample() {
@@ -71,5 +75,11 @@ you want to show the indicators for.
7175
[2]
7276
You could override the default ShapeIndicator component in this
7377
same TLComponents object, but the default (DefaultIndicator.tsx)
74-
has a lot of logic for where and how to display the indicator
78+
has a lot of logic for where and how to display the indicator.
79+
80+
[3]
81+
If all you want to do is show or hide all the indicators, you could
82+
create an override for the ShapeIndicators component that returns the
83+
DefaultShapeIndicators component with `hideAll` or `showAll` props
84+
set to true.
7585
*/

packages/editor/api-report.api.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ export function DefaultSelectionForeground({ bounds, rotation }: TLSelectionFore
689689
export const DefaultShapeIndicator: NamedExoticComponent<TLShapeIndicatorProps>;
690690

691691
// @public (undocumented)
692-
export const DefaultShapeIndicators: NamedExoticComponent<object>;
692+
export const DefaultShapeIndicators: NamedExoticComponent<TLShapeIndicatorsProps>;
693693

694694
// @public (undocumented)
695695
export function DefaultSnapIndicator({ className, line, zoom }: TLSnapIndicatorProps): JSX_2.Element;
@@ -3951,6 +3951,12 @@ export interface TLShapeIndicatorProps {
39513951
userId?: string;
39523952
}
39533953

3954+
// @public (undocumented)
3955+
export interface TLShapeIndicatorsProps {
3956+
hideAll?: boolean;
3957+
showAll?: boolean;
3958+
}
3959+
39543960
// @public
39553961
export interface TLShapeUtilCanBeLaidOutOpts {
39563962
shapes?: TLShape[];

packages/editor/src/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ export {
109109
type TLShapeIndicatorProps,
110110
} from './lib/components/default-components/DefaultShapeIndicator'
111111
export { type TLShapeIndicatorErrorFallbackComponent } from './lib/components/default-components/DefaultShapeIndicatorErrorFallback'
112-
export { DefaultShapeIndicators } from './lib/components/default-components/DefaultShapeIndicators'
112+
export {
113+
DefaultShapeIndicators,
114+
type TLShapeIndicatorsProps,
115+
} from './lib/components/default-components/DefaultShapeIndicators'
113116
export {
114117
DefaultSnapIndicator,
115118
type TLSnapIndicatorProps,

packages/editor/src/lib/components/default-components/DefaultShapeIndicator.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import { useEditorComponents } from '../../hooks/useEditorComponents'
99
import { OptionalErrorBoundary } from '../ErrorBoundary'
1010

1111
// need an extra layer of indirection here to allow hooks to be used inside the indicator render
12-
const EvenInnererIndicator = ({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
12+
const EvenInnererIndicator = memo(({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
1313
return useStateTracking('Indicator: ' + shape.type, () =>
1414
// always fetch the latest shape from the store even if the props/meta have not changed, to avoid
1515
// calling the render method with stale data.
1616
util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
1717
)
18-
}
18+
})
1919

20-
const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }) => {
20+
const InnerIndicator = memo(({ editor, id }: { editor: Editor; id: TLShapeId }) => {
2121
const shape = useValue('shape for indicator', () => editor.store.get(id), [editor, id])
2222

2323
const { ShapeIndicatorErrorFallback } = useEditorComponents()
@@ -34,7 +34,7 @@ const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }) => {
3434
<EvenInnererIndicator key={shape.id} shape={shape} util={editor.getShapeUtil(shape)} />
3535
</OptionalErrorBoundary>
3636
)
37-
}
37+
})
3838

3939
/** @public */
4040
export interface TLShapeIndicatorProps {

packages/editor/src/lib/components/default-components/DefaultShapeIndicators.tsx

+51-28
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,24 @@ import { memo, useRef } from 'react'
44
import { useEditor } from '../../hooks/useEditor'
55
import { useEditorComponents } from '../../hooks/useEditorComponents'
66

7+
/** @public */
8+
export interface TLShapeIndicatorsProps {
9+
/** Whether to hide all of the indicators */
10+
hideAll?: boolean
11+
/** Whether to show all of the indicators */
12+
showAll?: boolean
13+
}
14+
715
/** @public @react */
8-
export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
16+
export const DefaultShapeIndicators = memo(function DefaultShapeIndicators({
17+
hideAll,
18+
showAll,
19+
}: TLShapeIndicatorsProps) {
920
const editor = useEditor()
1021

22+
if (hideAll && showAll)
23+
throw Error('You cannot set both hideAll and showAll props to true, cmon now')
24+
1125
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
1226

1327
const idsToDisplay = useValue(
@@ -16,33 +30,38 @@ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
1630
const prev = rPreviousSelectedShapeIds.current
1731
const next = new Set<TLShapeId>()
1832

19-
if (
20-
// We only show indicators when in the following states...
21-
editor.isInAny(
22-
'select.idle',
23-
'select.brushing',
24-
'select.scribble_brushing',
25-
'select.editing_shape',
26-
'select.pointing_shape',
27-
'select.pointing_selection',
28-
'select.pointing_handle'
29-
) &&
30-
// ...but we hide indicators when we've just changed a style (so that the user can see the change)
31-
!editor.getInstanceState().isChangingStyle
32-
) {
33-
// We always want to show indicators for the selected shapes, if any
34-
const selected = editor.getSelectedShapeIds()
35-
for (const id of selected) {
36-
next.add(id)
37-
}
33+
const isChangingStyle = editor.getInstanceState().isChangingStyle
34+
35+
// todo: this is tldraw specific and is duplicated at the tldraw layer. What should we do here instead?
36+
const isInSelectState = editor.isInAny(
37+
'select.idle',
38+
'select.brushing',
39+
'select.scribble_brushing',
40+
'select.editing_shape',
41+
'select.pointing_shape',
42+
'select.pointing_selection',
43+
'select.pointing_handle'
44+
)
45+
46+
// We hide all indicators if we're changing style or in certain interactions
47+
// todo: move this to some kind of Tool.hideIndicators property
48+
if (isChangingStyle || !isInSelectState) {
49+
rPreviousSelectedShapeIds.current = next
50+
return next
51+
}
52+
53+
// We always want to show indicators for the selected shapes, if any
54+
const selected = editor.getSelectedShapeIds()
55+
for (const id of selected) {
56+
next.add(id)
57+
}
3858

39-
// If we're idle or editing a shape, we want to also show an indicator for the hovered shape, if any
40-
if (editor.isInAny('select.idle', 'select.editing_shape')) {
41-
const instanceState = editor.getInstanceState()
42-
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
43-
const hovered = editor.getHoveredShapeId()
44-
if (hovered) next.add(hovered)
45-
}
59+
// If we're idle or editing a shape, we want to also show an indicator for the hovered shape, if any
60+
if (editor.isInAny('select.idle', 'select.editing_shape')) {
61+
const instanceState = editor.getInstanceState()
62+
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
63+
const hovered = editor.getHoveredShapeId()
64+
if (hovered) next.add(hovered)
4665
}
4766
}
4867

@@ -75,6 +94,10 @@ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
7594
if (!ShapeIndicator) return null
7695

7796
return renderingShapes.map(({ id }) => (
78-
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
97+
<ShapeIndicator
98+
key={id + '_indicator'}
99+
shapeId={id}
100+
hidden={!showAll && (hideAll || !idsToDisplay.has(id))}
101+
/>
79102
))
80103
})

packages/tldraw/api-report.api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2415,7 +2415,7 @@ export const TldrawSelectionBackground: ({ bounds, rotation }: TLSelectionBackgr
24152415
export const TldrawSelectionForeground: NamedExoticComponent<TLSelectionForegroundProps>;
24162416

24172417
// @public (undocumented)
2418-
export function TldrawShapeIndicators(): JSX_2.Element | null;
2418+
export function TldrawShapeIndicators(): JSX_2.Element;
24192419

24202420
// @public (undocumented)
24212421
export const TldrawUi: React_3.NamedExoticComponent<TldrawUiProps>;

packages/tldraw/src/lib/canvas/TldrawShapeIndicators.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,5 @@ export function TldrawShapeIndicators() {
2020
[editor]
2121
)
2222

23-
if (!isInSelectState) return null
24-
25-
return <DefaultShapeIndicators />
23+
return <DefaultShapeIndicators hideAll={!isInSelectState} />
2624
}

packages/tldraw/src/lib/shapes/geo/GeoShapeUtil.tsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -430,18 +430,12 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
430430
const isOnlySelected = useValue(
431431
'isGeoOnlySelected',
432432
() => shape.id === editor.getOnlySelectedShapeId(),
433-
[]
433+
[editor]
434434
)
435435
const isEditingAnything = editor.getEditingShapeId() !== null
436436
const plaintext = renderPlaintextFromRichText(this.editor, shape.props.richText)
437437
const showHtmlContainer = isEditingAnything || !!plaintext.length
438-
const isForceSolid = useValue(
439-
'force solid',
440-
() => {
441-
return editor.getZoomLevel() < 0.2
442-
},
443-
[editor]
444-
)
438+
const isForceSolid = useValue('force solid', () => editor.getZoomLevel() < 0.2, [editor])
445439

446440
return (
447441
<>

0 commit comments

Comments
 (0)