Skip to content

Commit d4c0c36

Browse files
authored
🤖 fix: untracked popup overflow (#755)
_Generated with `mux`_ The UntrackedStatus popup was being clipped by parent containers with `overflow:hidden`. **Fix:** Use `createPortal` with fixed positioning and dynamic coordinates calculated from the trigger element's bounding rect. Also updated click-outside handler to account for the portal element.
1 parent 6182ee5 commit d4c0c36

File tree

1 file changed

+73
-31
lines changed

1 file changed

+73
-31
lines changed

src/browser/components/RightSidebar/CodeReview/UntrackedStatus.tsx

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* UntrackedStatus - Shows untracked files count with interactive tooltip
33
*/
44

5-
import React, { useState, useEffect, useRef } from "react";
5+
import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
6+
import { createPortal } from "react-dom";
67
import { cn } from "@/common/lib/utils";
78

89
interface UntrackedStatusProps {
@@ -22,10 +23,40 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
2223
const [isLoading, setIsLoading] = useState(false);
2324
const [showTooltip, setShowTooltip] = useState(false);
2425
const [isTracking, setIsTracking] = useState(false);
26+
const [popupPosition, setPopupPosition] = useState<{ top: number; right: number } | null>(null);
2527
const containerRef = useRef<HTMLDivElement>(null);
28+
const popupRef = useRef<HTMLDivElement>(null);
2629
const hasLoadedOnce = useRef(false);
2730
const loadingRef = useRef(false); // Prevent concurrent loads
2831

32+
// Calculate popup position when shown
33+
useLayoutEffect(() => {
34+
if (!showTooltip || !containerRef.current) {
35+
setPopupPosition(null);
36+
return;
37+
}
38+
39+
const updatePosition = () => {
40+
const container = containerRef.current;
41+
if (!container) return;
42+
43+
const rect = container.getBoundingClientRect();
44+
setPopupPosition({
45+
top: rect.bottom + 8, // 8px gap below anchor
46+
right: window.innerWidth - rect.right,
47+
});
48+
};
49+
50+
updatePosition();
51+
52+
window.addEventListener("resize", updatePosition);
53+
window.addEventListener("scroll", updatePosition, true);
54+
return () => {
55+
window.removeEventListener("resize", updatePosition);
56+
window.removeEventListener("scroll", updatePosition, true);
57+
};
58+
}, [showTooltip]);
59+
2960
// Load untracked files
3061
useEffect(() => {
3162
let cancelled = false;
@@ -78,7 +109,10 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
78109
if (!showTooltip) return;
79110

80111
const handleClickOutside = (e: MouseEvent) => {
81-
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
112+
const target = e.target as Node;
113+
const clickedContainer = containerRef.current?.contains(target);
114+
const clickedPopup = popupRef.current?.contains(target);
115+
if (!clickedContainer && !clickedPopup) {
82116
setShowTooltip(false);
83117
}
84118
};
@@ -121,7 +155,7 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
121155
const hasUntracked = count > 0;
122156

123157
return (
124-
<div ref={containerRef} className="relative inline-block">
158+
<div ref={containerRef} className="inline-block">
125159
<div
126160
className={cn(
127161
"py-1 px-2.5 rounded font-medium text-[11px] whitespace-nowrap transition-all duration-200",
@@ -134,35 +168,43 @@ export const UntrackedStatus: React.FC<UntrackedStatusProps> = ({
134168
{isLoading ? "..." : `${count} Untracked`}
135169
</div>
136170

137-
{showTooltip && hasUntracked && (
138-
<div className="bg-modal-bg border-bg-medium animate-in fade-in slide-in-from-top-1 absolute top-[calc(100%+8px)] right-0 z-[1000] max-w-96 min-w-48 rounded border p-2 shadow-[0_4px_12px_rgba(0,0,0,0.3)] duration-150">
139-
<div className="text-foreground border-border-light mb-2 border-b pb-1.5 text-[11px] font-semibold">
140-
Untracked Files ({count})
141-
</div>
142-
<div className="mb-2 max-h-[200px] overflow-y-auto">
143-
{untrackedFiles.map((file) => (
144-
<div
145-
key={file}
146-
className="text-label hover:bg-bg-subtle truncate px-1 py-0.5 font-mono text-[11px]"
147-
>
148-
{file}
149-
</div>
150-
))}
151-
</div>
152-
<button
153-
onClick={() => void handleTrackAll()}
154-
disabled={isTracking}
155-
className={cn(
156-
"w-full py-1 px-2 bg-transparent text-muted border border-border-medium rounded text-[11px] cursor-pointer transition-all duration-200 font-primary",
157-
"hover:bg-white-overlay-light hover:text-foreground hover:border-border-subtle",
158-
"active:bg-white-overlay",
159-
"disabled:text-border-darker disabled:border-border disabled:cursor-not-allowed disabled:bg-transparent"
160-
)}
171+
{showTooltip &&
172+
hasUntracked &&
173+
popupPosition &&
174+
createPortal(
175+
<div
176+
ref={popupRef}
177+
className="bg-modal-bg border-bg-medium animate-in fade-in slide-in-from-top-1 fixed z-[1000] max-w-96 min-w-48 rounded border p-2 shadow-[0_4px_12px_rgba(0,0,0,0.3)] duration-150"
178+
style={{ top: popupPosition.top, right: popupPosition.right }}
161179
>
162-
{isTracking ? "Tracking..." : "Track All"}
163-
</button>
164-
</div>
165-
)}
180+
<div className="text-foreground border-border-light mb-2 border-b pb-1.5 text-[11px] font-semibold">
181+
Untracked Files ({count})
182+
</div>
183+
<div className="mb-2 max-h-[200px] overflow-y-auto">
184+
{untrackedFiles.map((file) => (
185+
<div
186+
key={file}
187+
className="text-label hover:bg-bg-subtle truncate px-1 py-0.5 font-mono text-[11px]"
188+
>
189+
{file}
190+
</div>
191+
))}
192+
</div>
193+
<button
194+
onClick={() => void handleTrackAll()}
195+
disabled={isTracking}
196+
className={cn(
197+
"w-full py-1 px-2 bg-transparent text-muted border border-border-medium rounded text-[11px] cursor-pointer transition-all duration-200 font-primary",
198+
"hover:bg-white-overlay-light hover:text-foreground hover:border-border-subtle",
199+
"active:bg-white-overlay",
200+
"disabled:text-border-darker disabled:border-border disabled:cursor-not-allowed disabled:bg-transparent"
201+
)}
202+
>
203+
{isTracking ? "Tracking..." : "Track All"}
204+
</button>
205+
</div>,
206+
document.body
207+
)}
166208
</div>
167209
);
168210
};

0 commit comments

Comments
 (0)