Sfoglia il codice sorgente

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

creatixchu 3 settimane fa
parent
commit
26e2196a86

+ 1 - 1
packages/client/ui-deliverables/src/client/ReviewTab.module.css

@@ -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; }

+ 14 - 6
packages/client/ui-deliverables/src/client/ReviewTab.tsx

@@ -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>

+ 8 - 0
packages/client/ui-deliverables/tests/review-tab.client.spec.tsx

@@ -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'] }))