fix(overview): track pointer hover locally on the ribbon
The ribbon rendered its cursor and tip from the parent's hoveredSeq, which contextView throttles with a 140ms trailing update and a transition so pointer sweeps cannot re-render the whole view; during a sweep the readout froze at the first record and jumped once after the pointer rested. Keep the pointer's record in local state, as the bars face already does, and relay it upstream unchanged: the readout now follows every record change while the browser preview keeps its throttle. hoveredSeq's only producer is this pointer, so the local record wins while the pointer is inside; leaving drops it.
This commit is contained in:
1 parent
811422252e
commit
18075e6b66
2 files changed
+29
-4
No files matched your search
@@ -12,7 +12,7 @@
|
||||
* are illegible at ribbon scale) and Adaptive (the whole log IS the visible window).
|
||||
*/
|
||||
|
||||
import { Fragment, useLayoutEffect, useMemo, useRef, type ReactElement } from 'react'
|
||||
import { Fragment, useCallback, useLayoutEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
||||
import type { Category, ContextEventRecord, RequestRecord } from '../../shared/types'
|
||||
import { CATS, CAT_COLOR } from '../categories'
|
||||
import { RISE_CLASS, TURN_FILLS, preprocessTrend, trendMaxima, turnStepsOf, trendTipRows } from './trendChart'
|
||||
@@ -193,6 +193,15 @@ export function makeTrendOverview(kit: ViewKit): (props: TrendOverviewProps) =>
|
||||
const surfaceRef = useRef<HTMLDivElement | null>(null)
|
||||
const wrapRef = useRef<HTMLDivElement | null>(null)
|
||||
const lastIdxRef = useRef<number | null>(null)
|
||||
// Pointer hover is local to the ribbon (the bars' idiom): the tip and cursor must track the pointer, while the
|
||||
// parent's preview follows a throttle/transition and would otherwise freeze a sweep at its first record. The
|
||||
// pointer is hoveredSeq's only producer, so its record wins while inside; leaving drops the local override.
|
||||
const [localHover, setLocalHover] = useState<number | null>(null)
|
||||
const hoverSeq = localHover ?? props.hoveredSeq
|
||||
const handleHover = useCallback((seq: number | null): void => {
|
||||
setLocalHover(seq)
|
||||
props.onHover(seq)
|
||||
}, [props.onHover])
|
||||
if (n === 0) return null
|
||||
// The stretched-unit x scale: one unit per record; a single record closes its bands across the full width.
|
||||
const u = n > 1 ? n - 1 : 1
|
||||
@@ -242,19 +251,19 @@ export function makeTrendOverview(kit: ViewKit): (props: TrendOverviewProps) =>
|
||||
const idx = pick(e)
|
||||
if (lastIdxRef.current !== idx) {
|
||||
lastIdxRef.current = idx
|
||||
props.onHover(requests[idx].seq)
|
||||
handleHover(requests[idx].seq)
|
||||
}
|
||||
}
|
||||
const onLeave = (): void => {
|
||||
lastIdxRef.current = null
|
||||
props.onHover(null)
|
||||
handleHover(null)
|
||||
}
|
||||
const onClick = (e: { clientX: number; currentTarget: HTMLElement }): void => {
|
||||
const seq = requests[pick(e)].seq
|
||||
props.onSelect(props.selectedSeq === seq ? null : seq)
|
||||
}
|
||||
|
||||
const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex(r => r.seq === props.hoveredSeq) : -1
|
||||
const hoveredIdx = hoverSeq !== null ? requests.findIndex(r => r.seq === hoverSeq) : -1
|
||||
const selectedIdx = props.selectedSeq !== null ? requests.findIndex(r => r.seq === props.selectedSeq) : -1
|
||||
const stepsOf = useMemo(() => turnStepsOf(props.requests), [props.requests])
|
||||
// Re-position the tip after every commit (it mounts on hover changes); reads batch before paint — the
|
||||
|
||||
@@ -350,6 +350,22 @@ describe('TrendOverview pointer interaction', () => {
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('the pointer tracks locally without waiting for the parent hover', async () => {
|
||||
const m = await mount(h(TrendOverview, propsOf(reqs)))
|
||||
const surface = query(m.container, '.lc-chart')
|
||||
stubSurfaceRect(surface)
|
||||
// HoveredSeq never changes here: the local state alone must paint the cursor and the tip.
|
||||
await moveX(surface, 200)
|
||||
assert.equal(query(m.container, '.lc-fit-cursor').style.left, '50%')
|
||||
assert.equal(query(query(m.container, '.lc-chart-tip'), 'span').textContent, kit.t('tip.step', { t: 1, s: 2, n: 5 }))
|
||||
await moveX(surface, 120)
|
||||
assert.equal(query(m.container, '.lc-fit-cursor').style.left, '25%')
|
||||
await unhover(surface)
|
||||
assert.equal(queryAll(m.container, '.lc-fit-cursor').length, 0)
|
||||
assert.equal(queryAll(m.container, '.lc-chart-tip').length, 0)
|
||||
await m.unmount()
|
||||
})
|
||||
|
||||
test('a zero-width rect picks the first record', async () => {
|
||||
const spies = { hover: [] as (number | null)[] }
|
||||
const m = await mount(h(TrendOverview, propsOf(reqs, { onHover: (s) => { spies.hover.push(s) } })))
|
||||
|
||||
Reference in new issue
Block a user