fix(ui): dismiss annotation toolbars on outside click and Escape (#182)

* fix: close annotation toolbars on outside click and escape

* refactor: share dismiss logic for annotation toolbars
This commit is contained in:
김영준E
2026-02-25 23:38:17 +09:00
committed by GitHub
parent e8ad0d5c38
commit 39b63c99c0
5 changed files with 63 additions and 4 deletions
+5 -3
View File
@@ -1,10 +1,12 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "plannotator",
"dependencies": {
"@pierre/diffs": "^1.0.4",
"diff": "^8.0.3",
},
"devDependencies": {
"happy-dom": "^20.5.0",
@@ -52,7 +54,7 @@
},
"apps/opencode-plugin": {
"name": "@plannotator/opencode",
"version": "0.8.5",
"version": "0.9.2",
"dependencies": {
"@opencode-ai/plugin": "^1.1.10",
},
@@ -65,7 +67,7 @@
},
"apps/pi-extension": {
"name": "@plannotator/pi-extension",
"version": "0.8.5",
"version": "0.9.2",
"peerDependencies": {
"@mariozechner/pi-coding-agent": ">=0.53.0",
},
@@ -132,7 +134,7 @@
},
"packages/server": {
"name": "@plannotator/server",
"version": "0.8.5",
"version": "0.9.2",
"peerDependencies": {
"bun": ">=1.0.0",
},
+2 -1
View File
@@ -29,7 +29,8 @@
"test": "bun test"
},
"dependencies": {
"@pierre/diffs": "^1.0.4"
"@pierre/diffs": "^1.0.4",
"diff": "^8.0.3"
},
"devDependencies": {
"happy-dom": "^20.5.0"
@@ -1,6 +1,7 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import { PatchDiff } from '@pierre/diffs/react';
import { CodeAnnotation, CodeAnnotationType, SelectedLineRange, DiffAnnotationMetadata } from '@plannotator/ui/types';
import { useDismissOnOutsideAndEscape } from '@plannotator/ui/hooks/useDismissOnOutsideAndEscape';
import { useTheme } from '@plannotator/ui/components/ThemeProvider';
interface DiffViewerProps {
@@ -39,6 +40,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
}) => {
const { theme } = useTheme();
const containerRef = useRef<HTMLDivElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const [toolbarState, setToolbarState] = useState<ToolbarState | null>(null);
const [commentText, setCommentText] = useState('');
const [suggestedCode, setSuggestedCode] = useState('');
@@ -140,6 +142,12 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
onLineSelection(null);
}, [onLineSelection]);
useDismissOnOutsideAndEscape({
enabled: !!toolbarState,
ref: toolbarRef,
onDismiss: handleCancel,
});
// Render annotation in diff - returns React element
const renderAnnotation = useCallback((annotation: { side: string; lineNumber: number; metadata?: DiffAnnotationMetadata }) => {
if (!annotation.metadata) return null;
@@ -294,6 +302,7 @@ export const DiffViewer: React.FC<DiffViewerProps> = ({
{/* Annotation toolbar - single-step comment input */}
{toolbarState && (
<div
ref={toolbarRef}
className="review-toolbar"
style={{
position: 'fixed',
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import { AnnotationType, type ImageAttachment } from "../types";
import { createPortal } from "react-dom";
import { AttachmentsButton } from "./AttachmentsButton";
import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
type PositionMode = 'center-above' | 'top-right';
@@ -54,6 +55,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
const [position, setPosition] = useState<{ top: number; left?: number; right?: number } | null>(null);
const [copied, setCopied] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const handleCopy = async () => {
// Use provided copyText, or fall back to code element / element text
@@ -158,6 +160,12 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
return () => window.removeEventListener("keydown", handleKeyDown);
}, [step, onClose]);
useDismissOnOutsideAndEscape({
enabled: true,
ref: toolbarRef,
onDismiss: onClose,
});
if (!position) return null;
const handleTypeSelect = (type: AnnotationType) => {
@@ -191,6 +199,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
return createPortal(
<div
ref={toolbarRef}
className="annotation-toolbar fixed z-[100] bg-popover border border-border rounded-lg shadow-2xl"
style={style}
onMouseDown={(e) => e.stopPropagation()}
@@ -0,0 +1,38 @@
import { useEffect } from "react";
export function useDismissOnOutsideAndEscape({
enabled,
ref,
onDismiss,
}: {
enabled: boolean;
ref: React.RefObject<HTMLElement>;
onDismiss: () => void;
}) {
useEffect(() => {
if (!enabled) return;
const handlePointerDown = (event: PointerEvent) => {
const target = event.target as Node | null;
if (!target) return;
if (ref.current && ref.current.contains(target)) {
return;
}
onDismiss();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onDismiss();
}
};
document.addEventListener("pointerdown", handlePointerDown, true);
window.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("pointerdown", handlePointerDown, true);
window.removeEventListener("keydown", handleKeyDown);
};
}, [enabled, ref, onDismiss]);
}