feat(ui): granularity switches re-anchor the history at the newest bars
Returning from turn to step mode kept the stale left edge (the narrow turn chart had clamped the scroll position), so the newest bars were off screen. The layout effect now detects a granularity change and re-runs the first-mount anchor — both directions land on the right edge. Plain re-renders (polls) still respect a scrolled-away position.
This commit is contained in:
1 parent
7eaec6d70b
commit
895ebbee1a
3 files changed
+36
-5
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
+22
-1
@@ -140,8 +140,9 @@ const statefulReact = {
|
||||
},
|
||||
useLayoutEffect(fn) {
|
||||
// Store the callback in a hook slot so tests can drive the layout effect.
|
||||
// Always overwrite: real React runs the LATEST render's closure.
|
||||
const i = hookCursor++
|
||||
if (currentHooks[i] === undefined) currentHooks[i] = { effect: fn }
|
||||
currentHooks[i] = { effect: fn }
|
||||
return currentHooks[i]
|
||||
},
|
||||
}
|
||||
@@ -456,6 +457,26 @@ tr = renderView()
|
||||
assert.equal(byClass(tr, 'lc-chart-fade-l').length, 1, 'left fade shown once anchored at the newest bars')
|
||||
assert.equal(byClass(tr, 'lc-chart-fade-r').length, 0, 'no right fade at the end')
|
||||
|
||||
// ---- granularity switches re-anchor at the newest bars ----
|
||||
// (the turn->step report: returning to step must show the right edge)
|
||||
const latestEffect = () => hookStates.get(trendKey).find(s => s && typeof s.effect === 'function')
|
||||
scrollEl.scrollLeft = 0 // stale left edge from the narrow turn chart
|
||||
const granTurnBtn = byClass(tr, 'lc-gran-btn')[1]
|
||||
granTurnBtn.args[1].onClick() // step -> turn
|
||||
tr = renderView()
|
||||
latestEffect().effect()
|
||||
assert.equal(scrollEl.scrollLeft, 800, 'switching to turn re-anchors at the newest bars')
|
||||
scrollEl.scrollLeft = 0
|
||||
const granStepBtn = byClass(tr, 'lc-gran-btn')[0]
|
||||
granStepBtn.args[1].onClick() // turn -> step
|
||||
tr = renderView()
|
||||
latestEffect().effect()
|
||||
assert.equal(scrollEl.scrollLeft, 800, 'switching back to step re-anchors at the newest bars')
|
||||
// a plain re-render (poll) without a switch must NOT yank a scrolled-away view
|
||||
scrollEl.scrollLeft = 200
|
||||
latestEffect().effect()
|
||||
assert.equal(scrollEl.scrollLeft, 200, 'plain re-renders still respect the scroll position')
|
||||
|
||||
// ---- message list: newest first, with timestamps when available ----
|
||||
const nodeRows = byClass(tr, 'lc-node')
|
||||
assert.equal(nodeRows.length, 2, 'message rows rendered')
|
||||
|
||||
+14
-4
@@ -288,6 +288,8 @@ interface TrendChartProps {
|
||||
hoveredSeq: number | null
|
||||
/** The turn currently highlighted (from the turn strip or a hovered bar). */
|
||||
activeTurn: number | null
|
||||
/** Display granularity; a switch re-anchors the chart at the newest bars. */
|
||||
granularity: 'step' | 'turn'
|
||||
onSelect: (seq: number | null) => void
|
||||
onHover: (seq: number | null) => void
|
||||
onHoverTurn: (turn: number | null) => void
|
||||
@@ -457,12 +459,15 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
|
||||
}
|
||||
|
||||
// Default anchor: the newest bars at the RIGHT edge. The first layout
|
||||
// after mount scrolls unconditionally; afterwards the chart only sticks
|
||||
// to the end while the user is already near it (scrolling away is
|
||||
// respected). useLayoutEffect avoids a first-paint flash at the left.
|
||||
// Edge fades stay in sync with the scroll position.
|
||||
// after mount scrolls unconditionally; a GRANULARITY SWITCH re-anchors
|
||||
// the same way (returning to step mode must show the newest bars, not
|
||||
// the stale left edge from the narrow turn chart); otherwise the chart
|
||||
// only sticks to the end while the user is already near it (scrolling
|
||||
// away is respected). useLayoutEffect avoids a first-paint flash at the
|
||||
// left. Edge fades stay in sync with the scroll position.
|
||||
const scrollRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const scrolledOnce = React.useRef(false)
|
||||
const lastGranRef = React.useRef(props.granularity)
|
||||
const [edges, setEdges] = React.useState<{ left: boolean; right: boolean }>({ left: false, right: false })
|
||||
const updateEdges = (el: HTMLDivElement): void => {
|
||||
const left = el.scrollLeft > 4
|
||||
@@ -472,6 +477,10 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
|
||||
React.useLayoutEffect(() => {
|
||||
const el = scrollRef.current
|
||||
if (el === null) return
|
||||
if (props.granularity !== lastGranRef.current) {
|
||||
lastGranRef.current = props.granularity
|
||||
scrolledOnce.current = false // re-anchor on every granularity switch
|
||||
}
|
||||
if (!scrolledOnce.current) {
|
||||
scrolledOnce.current = true
|
||||
el.scrollLeft = el.scrollWidth
|
||||
@@ -763,6 +772,7 @@ function makeView(ctx: ClientCtx, t: Translate): (props: ContextViewProps) => Re
|
||||
selectedSeq: pinnedReq ? pinnedReq.seq : null,
|
||||
hoveredSeq,
|
||||
activeTurn,
|
||||
granularity,
|
||||
onSelect: setSelectedSeq,
|
||||
onHover: setHoveredSeq,
|
||||
onHoverTurn: setHoverTurn,
|
||||
|
||||
Reference in new issue
Block a user