fix(deliverables): scroll the split review columns sideways together

This commit is contained in:
creatixchu committed 2026-09-16 16:53:44 +08:00
1 parent 142ca8b956
commit 26e2196a86
3 files changed
+23 -7

No files matched your search

@@ -1,4 +1,4 @@
/** Review tab: a file selector and icon tools in the document preview's header row, then numbered diff lines in the code font. Unwrapped split columns scroll sideways on their own; wrapped ones share rows so the sides stay aligned. */
/** Review tab: a file selector and icon tools in the document preview's header row, then numbered diff lines in the code font. Unwrapped split columns clip their lines and scroll sideways together; wrapped ones share rows so the sides stay aligned. */
.root { display: flex; flex-direction: column; box-sizing: border-box; width: 100%; height: 100%; min-height: 0; color: var(--dsw-alias-label-primary); }
.header { display: flex; flex: 0 0 auto; gap: 6px; align-items: center; box-sizing: border-box; height: 38px; padding: 0 6px 0 8px; border-bottom: 0.5px solid var(--dsw-alias-border-l3); }
.selector { flex: 0 1 auto; min-width: 0; }
@@ -3,8 +3,8 @@
* selected file's turn-start and turn-end comparison drawn unified or side by
* side, wrapped or scrolling, and controls to open the file itself.
*/
import { useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import type { ReactNode, UIEvent } from 'react'
import {
Button, IconChevronDownOutline14, IconCodeOutline16, IconPanelLeftOutline16, IconRightUpOutline16, IconWrapLinesOutline16, Menu, Tooltip,
} from '@deepseek-ai/dsh-client-ui-primitives'
@@ -271,16 +271,24 @@ function hunkHeader(hunk: WorkspaceDiffHunk): string {
}
/**
* The side-by-side view without wrapping: two columns that scroll sideways on
* their own, so a long line on one side never runs under the other. Every
* line is one fixed-height row, which keeps the sides aligned.
* The side-by-side view without wrapping: two columns that clip their long
* lines and scroll sideways together, so a long line on one side never runs
* under the other and both sides show the same columns of text. Every line is
* one fixed-height row, which keeps the sides aligned.
*/
function SplitColumns({ hunks }: { hunks: readonly WorkspaceDiffHunk[] }): ReactNode {
const paired = useMemo(() => hunks.map(hunk => ({ header: hunkHeader(hunk), rows: splitRows(hunk) })), [hunks])
const columns = useRef<Record<'left' | 'right', HTMLDivElement | null>>({ left: null, right: null })
// Mirror one side's horizontal offset onto the other; the mirrored side's own scroll event then finds nothing to change.
const follow = (side: 'left' | 'right') => (event: UIEvent<HTMLDivElement>): void => {
const other = columns.current[side === 'left' ? 'right' : 'left']
if (other !== null && other.scrollLeft !== event.currentTarget.scrollLeft) other.scrollLeft = event.currentTarget.scrollLeft
}
return (
<div className={css.columns}>
{(['left', 'right'] as const).map(side => (
<div key={side} className={css.column} data-diff-side={side}>
<div key={side} className={css.column} data-diff-side={side}
ref={(element) => { columns.current[side] = element }} onScroll={follow(side)}>
{paired.map((hunk, position) => (
<section key={position} className={css.hunk}>
<div className={css.hunkHeader}>{hunk.header}</div>
@@ -238,6 +238,14 @@ describe('ReviewTab', () => {
expect(side('left').map(line => line.getAttribute('data-diff-line'))).toEqual(['context', 'del', 'add', 'context', 'del', 'del'])
expect(side('left').map(line => line.textContent)).toEqual(['1a', '2b', '', '3d', '10x', '20z'])
expect(side('right').map(line => line.textContent)).toEqual(['1a', '2B', '3c', '4d', '11y', ''])
// The two sides scroll sideways together, whichever side the reader drags.
const [left, right] = ['left', 'right'].map(name => view.container.querySelector(`[data-diff-side="${name}"]`) as HTMLDivElement)
fireEvent.scroll(left!, { target: { scrollLeft: 40 } })
expect(right!.scrollLeft).toBe(40)
fireEvent.scroll(right!, { target: { scrollLeft: 15 } })
expect(left!.scrollLeft).toBe(15)
fireEvent.scroll(right!, { target: { scrollLeft: 15 } })
expect(left!.scrollLeft).toBe(15)
expect(view.getByRole('button', { name: en['review.splitAria'] }).getAttribute('aria-pressed')).toBe('true')
// Wrapped lines vary in height, so both sides share one row per pair.
fireEvent.click(view.getByRole('button', { name: en['review.wrapAria'] }))