prevent hover indicator from showing when pointer isn't over the canvas (#2023)

Before the geometry change, we'd rely on the browser to tell us which
element was hovered, which meant that when the pointer left the canvas
we'd automatically clear the hovered shape.

Currently, we don't know whether the pointer is over the canvas or not -
so we keep showing the hover indicator for the last shape you had your
pointer over.

This diff adds an `isHoveringCanvas` prop to the instance state (true,
false, or null if the current pointer doesn't support hovering) that we
can use to track this and disable the hover indicator appropriately.

![Kapture 2023-10-05 at 12 00
00](https://github.com/tldraw/tldraw/assets/1489520/236b9459-878b-47e2-bcaa-10d245581347)


### Change Type

- [x] `minor` — New feature

### Test Plan

1. Create some shapes that go below the UI
2. Move the mouse from the shape to the UI
3. Hover indicator should disappear
This commit is contained in:
alex 2023-10-09 07:34:48 +01:00 committed by GitHub
parent e2a6f3ed40
commit a007c66b78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 2 deletions

View file

@ -355,11 +355,16 @@ const HoveredShapeIndicator = function HoveredShapeIndicator() {
const isCoarsePointer = useValue('coarse pointer', () => editor.instanceState.isCoarsePointer, [
editor,
])
const isHoveringCanvas = useValue(
'hovering canvas',
() => editor.instanceState.isHoveringCanvas,
[editor]
)
const hoveredShapeId = useValue('hovered id', () => editor.currentPageState.hoveredShapeId, [
editor,
])
if (isCoarsePointer || !hoveredShapeId || !HoveredShapeIndicator) return null
if (isCoarsePointer || !isHoveringCanvas || !hoveredShapeId || !HoveredShapeIndicator) return null
return <HoveredShapeIndicator shapeId={hoveredShapeId} />
}

View file

@ -74,6 +74,20 @@ export function useCanvasEvents() {
})
}
function onPointerEnter(e: React.PointerEvent) {
if ((e as any).isKilled) return
if (editor.instanceState.isPenMode && e.pointerType !== 'pen') return
const canHover = e.pointerType === 'mouse' || e.pointerType === 'pen'
editor.updateInstanceState({ isHoveringCanvas: canHover ? true : null })
}
function onPointerLeave(e: React.PointerEvent) {
if ((e as any).isKilled) return
if (editor.instanceState.isPenMode && e.pointerType !== 'pen') return
const canHover = e.pointerType === 'mouse' || e.pointerType === 'pen'
editor.updateInstanceState({ isHoveringCanvas: canHover ? false : null })
}
function onTouchStart(e: React.TouchEvent) {
;(e as any).isKilled = true
// todo: investigate whether this effects keyboard shortcuts
@ -118,6 +132,8 @@ export function useCanvasEvents() {
onPointerDown,
onPointerMove,
onPointerUp,
onPointerEnter,
onPointerLeave,
onDragOver,
onDrop,
onTouchStart,

View file

@ -1033,6 +1033,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
isFocusMode: boolean;
// (undocumented)
isGridMode: boolean;
isHoveringCanvas: boolean | null;
// (undocumented)
isPenMode: boolean;
// (undocumented)

View file

@ -1535,6 +1535,18 @@ describe('Adding canSnap to line handles', () => {
})
})
describe('add isHoveringCanvas to TLInstance', () => {
const { up, down } = instanceMigrations.migrators[instanceVersions.AddHoveringCanvas]
test('up works as expected', () => {
expect(up({})).toEqual({ isHoveringCanvas: null })
})
test('down works as expected', () => {
expect(down({ isHoveringCanvas: null })).toEqual({})
})
})
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
for (const migrator of allMigrators) {

View file

@ -40,6 +40,11 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
isFocused: boolean
devicePixelRatio: number
isCoarsePointer: boolean
/**
* Will be null if the pointer doesn't support hovering (e.g. touch), but true or false
* otherwise
*/
isHoveringCanvas: boolean | null
openMenus: string[]
isChangingStyle: boolean
isReadonly: boolean
@ -85,6 +90,7 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
isFocused: T.boolean,
devicePixelRatio: T.number,
isCoarsePointer: T.boolean,
isHoveringCanvas: T.boolean.nullable(),
openMenus: T.arrayOf(T.string),
isChangingStyle: T.boolean,
isReadonly: T.boolean,
@ -122,6 +128,7 @@ export function createInstanceRecordType(stylesById: Map<string, StyleProp<unkno
isFocused: false,
devicePixelRatio: typeof window === 'undefined' ? 1 : window.devicePixelRatio,
isCoarsePointer: false,
isHoveringCanvas: null,
openMenus: [] as string[],
isChangingStyle: false,
isReadonly: false,
@ -152,11 +159,12 @@ export const instanceVersions = {
RemoveCursorColor: 18,
AddLonelyProperties: 19,
ReadOnlyReadonly: 20,
AddHoveringCanvas: 21,
} as const
/** @public */
export const instanceMigrations = defineMigrations({
currentVersion: instanceVersions.ReadOnlyReadonly,
currentVersion: instanceVersions.AddHoveringCanvas,
migrators: {
[instanceVersions.AddTransparentExportBgs]: {
up: (instance: TLInstance) => {
@ -453,6 +461,19 @@ export const instanceMigrations = defineMigrations({
}
},
},
[instanceVersions.AddHoveringCanvas]: {
up: (record) => {
return {
...record,
isHoveringCanvas: null,
}
},
down: ({ isHoveringCanvas: _, ...record }) => {
return {
...record,
}
},
},
},
})