fix(app): prevent comment action clipping

This commit is contained in:
Brendan Allan
2026-09-29 14:54:24 +08:00
parent 7c50519264
commit c82d8bccd7
3 changed files with 67 additions and 34 deletions
@@ -64,7 +64,7 @@ for (const direction of ["ltr", "rtl"]) {
const box = await action.boundingBox()
const code = root.locator("[data-code]").first()
const gutterRight = await code.evaluate((element) => element.firstElementChild?.getBoundingClientRect().right)
expect((box?.x ?? 0) - (gutterRight ?? 0)).toBe(8)
expect((box?.x ?? 0) - (gutterRight ?? 0)).toBeCloseTo(8, 0)
expect(box?.x).toBeGreaterThanOrEqual(0)
expect((box?.x ?? 0) + (box?.width ?? 0)).toBeLessThanOrEqual(
await page.evaluate(() => document.documentElement.clientWidth),
@@ -115,7 +115,7 @@ for (const direction of ["up", "down"] as const) {
await expect
.poll(() =>
action.evaluate((button) => {
const host = button.closest('[data-component="file"]')?.querySelector("diffs-container")
const host = document.querySelector('[data-component="file"]')?.querySelector("diffs-container")
const root = host?.shadowRoot
if (!root) return NaN
const selection =
@@ -149,6 +149,24 @@ story("leaves a review code click as regular text interaction", async ({ mount }
await expect(root.getByRole("textbox")).not.toBeVisible()
})
story("keeps the text selection action visible past the end of a file", async ({ mount, page }) => {
const root = await mount("components-session-review--interactive-file-comments")
const action = page.getByRole("button", { name: "Add comment", exact: true })
await expect(async () => {
await root.getByText("export const last = 2", { exact: true }).selectText()
await expect(action).toBeVisible()
}).toPass()
const file = root.locator('[data-component="file"]')
await expect(action).toHaveCSS("position", "fixed")
expect(await action.evaluate((element) => element.closest('[data-component="file"]'))).toBeNull()
const actionBox = await action.boundingBox()
const fileBox = await file.boundingBox()
expect((actionBox?.y ?? 0) + (actionBox?.height ?? 0)).toBeGreaterThan(
(fileBox?.y ?? 0) + (fileBox?.height ?? 0),
)
})
story("keeps direct line-number range comments in the review panel", async ({ mount }) => {
const root = await mount("components-session-review--interactive-comments-panel")
await root.locator('[data-column-number="1"]').dragTo(root.locator('[data-column-number="3"]'))
+30 -32
View File
@@ -21,6 +21,7 @@ import { type PreloadFileDiffResult, type PreloadMultiFileDiffResult } from "@pi
import { createMediaQuery } from "@solid-primitives/media"
import { makeEventListener } from "@solid-primitives/event-listener"
import { ComponentProps, createEffect, createMemo, createSignal, onCleanup, onMount, Show, splitProps } from "solid-js"
import { Portal } from "solid-js/web"
import { Button } from "@opencode/ui/button"
import { createDefaultOptions, styleVariables } from "../pierre"
import { markCommentedDiffLines, markCommentedFileLines } from "../pierre/commented-lines"
@@ -252,8 +253,7 @@ function useFileViewer(config: ViewerConfig) {
const roomAbove = rect.top - TEXT_SELECTION_ACTION_HEIGHT - TEXT_SELECTION_ACTION_GAP >= 0
const preferBelow = selected.direction !== "up"
const below = preferBelow ? roomBelow || !roomAbove : !roomAbove && roomBelow
const gutterEdge =
(selected.gutterRight ?? wrapper.getBoundingClientRect().left) - wrapper.getBoundingClientRect().left
const gutterEdge = selected.gutterRight ?? wrapper.getBoundingClientRect().left
setTextSelection({ range: selected.range, rect, label: action.label, below, gutterEdge })
}
@@ -839,36 +839,34 @@ function ViewerShell(props: {
<div ref={(el) => (props.viewer.overlay = el)} class="pointer-events-none absolute inset-0 z-0" />
<Show when={props.viewer.textSelection()}>
{(selection) => (
<Button
data-slot="file-text-selection-action"
data-placement={selection().below ? "bottom" : "top"}
size="small"
variant="submit"
class="z-[110] whitespace-nowrap motion-safe:transition-transform duration-100 ease-out motion-reduce:transition-none"
style={{
position: "absolute",
"--line-comment-gutter-edge": `${selection().gutterEdge}px`,
left: `calc(var(--line-comment-gutter-edge) + ${LINE_COMMENT_ACTION_GAP}px)`,
top: `${
(selection().below ? selection().rect.bottom : selection().rect.top) -
props.viewer.wrapper.getBoundingClientRect().top
}px`,
transform: selection().below
? `translateY(${TEXT_SELECTION_ACTION_GAP}px)`
: `translateY(calc(-100% - ${TEXT_SELECTION_ACTION_GAP}px))`,
}}
onPointerDown={(event: PointerEvent) => {
event.preventDefault()
event.stopPropagation()
}}
onMouseDown={(event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
}}
onClick={props.viewer.activateTextSelection}
>
{selection().label}
</Button>
<Portal>
<Button
data-slot="file-text-selection-action"
data-placement={selection().below ? "bottom" : "top"}
size="small"
variant="submit"
class="z-[110] whitespace-nowrap motion-safe:transition-transform duration-100 ease-out motion-reduce:transition-none"
style={{
position: "fixed",
left: `${selection().gutterEdge + LINE_COMMENT_ACTION_GAP}px`,
top: `${selection().below ? selection().rect.bottom : selection().rect.top}px`,
transform: selection().below
? `translateY(${TEXT_SELECTION_ACTION_GAP}px)`
: `translateY(calc(-100% - ${TEXT_SELECTION_ACTION_GAP}px))`,
}}
onPointerDown={(event: PointerEvent) => {
event.preventDefault()
event.stopPropagation()
}}
onMouseDown={(event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
}}
onClick={props.viewer.activateTextSelection}
>
{selection().label}
</Button>
</Portal>
)}
</Show>
</div>
@@ -111,6 +111,23 @@ function InteractiveCommentsV2Story() {
export const InteractiveCommentsPanel = { render: () => <InteractiveCommentsV2Story /> }
export const InteractiveFileComments = {
render: () => (
<CurrentSessionProviders document={editThenTestDocument}>
<div class="h-screen overflow-auto bg-background-base">
<div class="relative overflow-hidden pb-40">
<File
mode="text"
file={{ name: "review.ts", contents: "export const first = 1\nexport const last = 2" }}
enableLineSelection
textSelectionAction={{ label: "Add comment" }}
/>
</div>
</div>
</CurrentSessionProviders>
),
}
const gitDiffs = [
{
// OpenCode 93e1f383dd79683af4fc5ad139cea0516603c838, unchanged git-show output.