authorgravatar for 32248437+ShpetimA@users.noreply.github.comShpetim <32248437+ShpetimA@users.noreply.github.com> 2026-09-04 21:37:26+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 12:37:26-07:00
log13427ecd8ba5bfa7892fc90d1e3f0ef59514870f
treeaa1096c8761e88d2166a20eb484ce003e8fddfbc
parentf6db4206258b0ef30e8dd8949627acfd209bf338
signature Signed by PGP key B5690EEEBB952194

fix(web): restore file comment focus in editable preview (#9061)


2 files changed, 55 insertions(+), 38 deletions(-)

apps/web/src/components/diffs/DiffCommentAnnotation.tsx+15-3
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { MessageCircle, Trash2 } from "lucide-react";1import { MessageCircle, Trash2 } from "lucide-react";
2import { useState, type ReactNode } from "react";2import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
33
4import { Button } from "~/components/ui/button";4import { Button } from "~/components/ui/button";
5import { Textarea } from "~/components/ui/textarea";5import { Textarea } from "~/components/ui/textarea";
...@@ -25,6 +25,7 @@ interface DiffCommentAnnotationProps {...@@ -25,6 +25,7 @@ interface DiffCommentAnnotationProps {
25 submitLabel?: string;25 submitLabel?: string;
26 pending?: boolean;26 pending?: boolean;
27 secondaryAction?: DiffCommentSecondaryAction;27 secondaryAction?: DiffCommentSecondaryAction;
28 focusOnMount?: boolean;
28}29}
2930
30/** The shared inline comment treatment for file previews, thread diffs, and pull-request diffs. */31/** The shared inline comment treatment for file previews, thread diffs, and pull-request diffs. */
...@@ -40,10 +41,20 @@ export function DiffCommentAnnotation({...@@ -40,10 +41,20 @@ export function DiffCommentAnnotation({
40 submitLabel = "Comment",41 submitLabel = "Comment",
41 pending = false,42 pending = false,
42 secondaryAction,43 secondaryAction,
44 focusOnMount = true,
43}: DiffCommentAnnotationProps) {45}: DiffCommentAnnotationProps) {
44 const [localDraftText, setLocalDraftText] = useState("");46 const [localDraftText, setLocalDraftText] = useState("");
45 const displayedText = kind === "draft" && !onTextChange ? localDraftText : text;47 const displayedText = kind === "draft" && !onTextChange ? localDraftText : text;
46 const trimmedText = displayedText.trim();48 const trimmedText = displayedText.trim();
49 const textareaRef = useRef<HTMLTextAreaElement | null>(null);
50
51 useLayoutEffect(() => {
52 if (kind !== "draft" || !focusOnMount) return;
53 const frame = window.requestAnimationFrame(() => {
54 textareaRef.current?.focus({ preventScroll: true });
55 });
56 return () => window.cancelAnimationFrame(frame);
57 }, [focusOnMount, kind]);
4758
48 if (kind === "comment") {59 if (kind === "comment") {
49 return (60 return (
...@@ -78,9 +89,10 @@ export function DiffCommentAnnotation({...@@ -78,9 +89,10 @@ export function DiffCommentAnnotation({
78 onPointerDown={(event) => event.stopPropagation()}89 onPointerDown={(event) => event.stopPropagation()}
79 >90 >
80 <Textarea91 <Textarea
81 autoFocus92 ref={textareaRef}
93 autoFocus={focusOnMount}
82 unstyled94 unstyled
83 className="relative inline-flex w-full rounded-md border border-border/50 bg-background/20 font-sans text-foreground transition-colors focus-within:border-border/70 [&_[data-slot=textarea]]:min-h-12 [&_[data-slot=textarea]]:cursor-text [&_[data-slot=textarea]]:px-2.5 [&_[data-slot=textarea]]:py-1.5 [&_[data-slot=textarea]]:font-sans [&_[data-slot=textarea]]:text-xs [&_[data-slot=textarea]]:leading-5 max-sm:[&_[data-slot=textarea]]:min-h-12"95 className="relative inline-flex w-full rounded-md border border-border/50 bg-background/20 font-sans text-foreground transition-colors focus-within:border-border/70 [&_[data-slot=textarea]]:min-h-12 [&_[data-slot=textarea]]:cursor-text [&_[data-slot=textarea]]:caret-foreground [&_[data-slot=textarea]]:px-2.5 [&_[data-slot=textarea]]:py-1.5 [&_[data-slot=textarea]]:font-sans [&_[data-slot=textarea]]:text-xs [&_[data-slot=textarea]]:leading-5 max-sm:[&_[data-slot=textarea]]:min-h-12"
84 size="sm"96 size="sm"
85 value={displayedText}97 value={displayedText}
86 placeholder={placeholder}98 placeholder={placeholder}
apps/web/src/components/files/FilePreviewPanel.tsx+40-35
...@@ -774,42 +774,47 @@ function EditableFileSurface({...@@ -774,42 +774,47 @@ function EditableFileSurface({
774 ],774 ],
775 );775 );
776776
777 const beginComment = useCallback((range: SelectedLineRange) => {777 const beginComment = useCallback(
778 const { startLine, endLine } = normalizeFileCommentRange(range);778 (range: SelectedLineRange) => {
779 const draftEntry: FileCommentAnnotationEntry = {779 editor.setSelections([]);
780 id: nextFileCommentId(),780 editor.blur();
781 kind: "draft",781 const { startLine, endLine } = normalizeFileCommentRange(range);
782 startLine,782 const draftEntry: FileCommentAnnotationEntry = {
783 endLine,783 id: nextFileCommentId(),
784 text: "",784 kind: "draft",
785 };785 startLine,
786 setLineAnnotations((current) => {786 endLine,
787 const withoutDraft = current.flatMap((annotation) => {787 text: "",
788 const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft");788 };
789 return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : [];789 setLineAnnotations((current) => {
790 const withoutDraft = current.flatMap((annotation) => {
791 const entries = annotation.metadata.entries.filter((entry) => entry.kind !== "draft");
792 return entries.length > 0 ? [{ ...annotation, metadata: { entries } }] : [];
793 });
794 const existingIndex = withoutDraft.findIndex(
795 (annotation) => annotation.lineNumber === endLine,
796 );
797 if (existingIndex < 0) {
798 return [
799 ...withoutDraft,
800 {
801 lineNumber: endLine,
802 metadata: { entries: [draftEntry] },
803 },
804 ];
805 }
806 return withoutDraft.map((annotation, index) =>
807 index === existingIndex
808 ? {
809 ...annotation,
810 metadata: { entries: [...annotation.metadata.entries, draftEntry] },
811 }
812 : annotation,
813 );
790 });814 });
791 const existingIndex = withoutDraft.findIndex(815 },
792 (annotation) => annotation.lineNumber === endLine,816 [editor],
793 );817 );
794 if (existingIndex < 0) {
795 return [
796 ...withoutDraft,
797 {
798 lineNumber: endLine,
799 metadata: { entries: [draftEntry] },
800 },
801 ];
802 }
803 return withoutDraft.map((annotation, index) =>
804 index === existingIndex
805 ? {
806 ...annotation,
807 metadata: { entries: [...annotation.metadata.entries, draftEntry] },
808 }
809 : annotation,
810 );
811 });
812 }, []);
813 const hasOpenCommentForm = lineAnnotations.some((annotation) =>818 const hasOpenCommentForm = lineAnnotations.some((annotation) =>
814 annotation.metadata.entries.some((entry) => entry.kind === "draft"),819 annotation.metadata.entries.some((entry) => entry.kind === "draft"),
815 );820 );