fix(app): safety triangle for sidebar hover (#12179)
This commit is contained in:
138
packages/app/src/utils/aim.ts
Normal file
138
packages/app/src/utils/aim.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
type Point = { x: number; y: number }
|
||||
|
||||
export function createAim(props: {
|
||||
enabled: () => boolean
|
||||
active: () => string | undefined
|
||||
el: () => HTMLElement | undefined
|
||||
onActivate: (id: string) => void
|
||||
delay?: number
|
||||
max?: number
|
||||
tolerance?: number
|
||||
edge?: number
|
||||
}) {
|
||||
const state = {
|
||||
locs: [] as Point[],
|
||||
timer: undefined as number | undefined,
|
||||
pending: undefined as string | undefined,
|
||||
over: undefined as string | undefined,
|
||||
last: undefined as Point | undefined,
|
||||
}
|
||||
|
||||
const delay = props.delay ?? 250
|
||||
const max = props.max ?? 4
|
||||
const tolerance = props.tolerance ?? 80
|
||||
const edge = props.edge ?? 18
|
||||
|
||||
const cancel = () => {
|
||||
if (state.timer !== undefined) clearTimeout(state.timer)
|
||||
state.timer = undefined
|
||||
state.pending = undefined
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
cancel()
|
||||
state.over = undefined
|
||||
state.last = undefined
|
||||
state.locs.length = 0
|
||||
}
|
||||
|
||||
const move = (event: MouseEvent) => {
|
||||
if (!props.enabled()) return
|
||||
const el = props.el()
|
||||
if (!el) return
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) return
|
||||
|
||||
state.locs.push({ x, y })
|
||||
if (state.locs.length > max) state.locs.shift()
|
||||
}
|
||||
|
||||
const wait = () => {
|
||||
if (!props.enabled()) return 0
|
||||
if (!props.active()) return 0
|
||||
|
||||
const el = props.el()
|
||||
if (!el) return 0
|
||||
if (state.locs.length < 2) return 0
|
||||
|
||||
const rect = el.getBoundingClientRect()
|
||||
const loc = state.locs[state.locs.length - 1]
|
||||
if (!loc) return 0
|
||||
|
||||
const prev = state.locs[0] ?? loc
|
||||
if (prev.x < rect.left || prev.x > rect.right || prev.y < rect.top || prev.y > rect.bottom) return 0
|
||||
if (state.last && loc.x === state.last.x && loc.y === state.last.y) return 0
|
||||
|
||||
if (rect.right - loc.x <= edge) {
|
||||
state.last = loc
|
||||
return delay
|
||||
}
|
||||
|
||||
const upper = { x: rect.right, y: rect.top - tolerance }
|
||||
const lower = { x: rect.right, y: rect.bottom + tolerance }
|
||||
const slope = (a: Point, b: Point) => (b.y - a.y) / (b.x - a.x)
|
||||
|
||||
const decreasing = slope(loc, upper)
|
||||
const increasing = slope(loc, lower)
|
||||
const prevDecreasing = slope(prev, upper)
|
||||
const prevIncreasing = slope(prev, lower)
|
||||
|
||||
if (decreasing < prevDecreasing && increasing > prevIncreasing) {
|
||||
state.last = loc
|
||||
return delay
|
||||
}
|
||||
|
||||
state.last = undefined
|
||||
return 0
|
||||
}
|
||||
|
||||
const activate = (id: string) => {
|
||||
cancel()
|
||||
props.onActivate(id)
|
||||
}
|
||||
|
||||
const request = (id: string) => {
|
||||
if (!id) return
|
||||
if (props.active() === id) return
|
||||
|
||||
if (!props.active()) {
|
||||
activate(id)
|
||||
return
|
||||
}
|
||||
|
||||
const ms = wait()
|
||||
if (ms === 0) {
|
||||
activate(id)
|
||||
return
|
||||
}
|
||||
|
||||
cancel()
|
||||
state.pending = id
|
||||
state.timer = window.setTimeout(() => {
|
||||
state.timer = undefined
|
||||
if (state.pending !== id) return
|
||||
state.pending = undefined
|
||||
if (!props.enabled()) return
|
||||
if (!props.active()) return
|
||||
if (state.over !== id) return
|
||||
props.onActivate(id)
|
||||
}, ms)
|
||||
}
|
||||
|
||||
const enter = (id: string, event: MouseEvent) => {
|
||||
if (!props.enabled()) return
|
||||
state.over = id
|
||||
move(event)
|
||||
request(id)
|
||||
}
|
||||
|
||||
const leave = (id: string) => {
|
||||
if (state.over === id) state.over = undefined
|
||||
if (state.pending === id) cancel()
|
||||
}
|
||||
|
||||
return { move, enter, leave, activate, request, cancel, reset }
|
||||
}
|
||||
Reference in New Issue
Block a user