Skip to content

Commit 427ff9a

Browse files
author
Test
committed
🤖 fix: prevent mobile backdrop from intercepting review taps
Ensure taps inside the expanded mobile review panel do not register as backdrop clicks: - Forward ref SidebarContainer to measure actual rendered width - Track viewport and sidebar widths to size the backdrop hit area - Limit backdrop click target to the space outside the panel - Keep interior close controls and swipe gestures intact _Generated with `cmux`_ Change-Id: I5817e2a466875325839ca3059d47f43b8ce94ed8 Signed-off-by: Test <test@example.com>
1 parent cdf522c commit 427ff9a

File tree

1 file changed

+112
-40
lines changed

1 file changed

+112
-40
lines changed

src/components/RightSidebar.tsx

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface SidebarContainerProps {
2020
role: string;
2121
"aria-label": string;
2222
}
23+
const MOBILE_DEFAULT_WIDTH = 360;
2324

2425
/**
2526
* SidebarContainer - Main sidebar wrapper with dynamic width
@@ -30,42 +31,40 @@ interface SidebarContainerProps {
3031
* 3. wide - Auto-calculated max width for Review tab (when not resizing)
3132
* 4. default (300px) - Costs/Tools tabs
3233
*/
33-
const SidebarContainer: React.FC<SidebarContainerProps> = ({
34-
collapsed,
35-
wide,
36-
customWidth,
37-
children,
38-
role,
39-
"aria-label": ariaLabel,
40-
}) => {
41-
const width = collapsed
42-
? "20px"
43-
: customWidth
44-
? `${customWidth}px`
45-
: wide
46-
? "min(1200px, calc(100vw - 400px))"
47-
: "300px";
34+
const SidebarContainer = React.forwardRef<HTMLDivElement, SidebarContainerProps>(
35+
({ collapsed, wide, customWidth, children, role, "aria-label": ariaLabel }, ref) => {
36+
const width = collapsed
37+
? "20px"
38+
: customWidth
39+
? `${customWidth}px`
40+
: wide
41+
? "min(1200px, calc(100vw - 400px))"
42+
: "300px";
43+
44+
return (
45+
<div
46+
ref={ref}
47+
className={cn(
48+
"bg-separator border-l border-border-light flex flex-col overflow-hidden flex-shrink-0",
49+
customWidth ? "" : "transition-[width] duration-200",
50+
collapsed && "sticky right-0 z-10 shadow-[-2px_0_4px_rgba(0,0,0,0.2)]",
51+
// Mobile: slide in from right (similar to left sidebar pattern)
52+
"max-md:fixed max-md:right-0 max-md:top-0 max-md:h-screen max-md:transition-transform max-md:duration-300",
53+
collapsed && "max-md:translate-x-full max-md:shadow-none",
54+
!collapsed &&
55+
"max-md:translate-x-0 max-md:w-full max-md:max-w-md max-md:z-[999] max-md:shadow-[-2px_0_8px_rgba(0,0,0,0.5)] max-md:border-l max-md:border-border-light"
56+
)}
57+
style={{ width }}
58+
role={role}
59+
aria-label={ariaLabel}
60+
>
61+
{children}
62+
</div>
63+
);
64+
}
65+
);
4866

49-
return (
50-
<div
51-
className={cn(
52-
"bg-separator border-l border-border-light flex flex-col overflow-hidden flex-shrink-0",
53-
customWidth ? "" : "transition-[width] duration-200",
54-
collapsed && "sticky right-0 z-10 shadow-[-2px_0_4px_rgba(0,0,0,0.2)]",
55-
// Mobile: slide in from right (similar to left sidebar pattern)
56-
"max-md:fixed max-md:right-0 max-md:top-0 max-md:h-screen max-md:transition-transform max-md:duration-300",
57-
collapsed && "max-md:translate-x-full max-md:shadow-none",
58-
!collapsed &&
59-
"max-md:translate-x-0 max-md:w-full max-md:max-w-md max-md:z-[999] max-md:shadow-[-2px_0_8px_rgba(0,0,0,0.5)] max-md:border-l max-md:border-border-light"
60-
)}
61-
style={{ width }}
62-
role={role}
63-
aria-label={ariaLabel}
64-
>
65-
{children}
66-
</div>
67-
);
68-
};
67+
SidebarContainer.displayName = "SidebarContainer";
6968

7069
type TabType = "costs" | "review";
7170

@@ -174,6 +173,9 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
174173

175174
// Track manual expansion to prevent auto-collapse immediately after user opens sidebar
176175
const manualExpandRef = React.useRef(false);
176+
const sidebarRef = React.useRef<HTMLDivElement | null>(null);
177+
const [measuredSidebarWidth, setMeasuredSidebarWidth] = React.useState<number>(0);
178+
const [viewportWidth, setViewportWidth] = React.useState<number>(0);
177179

178180
const openSidebar = React.useCallback(
179181
(manual: boolean) => {
@@ -226,6 +228,68 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
226228
}, [chatAreaWidth, selectedTab, showCollapsed, closeSidebar, openSidebarAuto]);
227229

228230
// Swipe gesture detection for mobile - right-to-left swipe to open sidebar
231+
React.useEffect(() => {
232+
if (typeof window === "undefined") {
233+
return;
234+
}
235+
236+
const updateViewportWidth = () => {
237+
setViewportWidth(window.innerWidth);
238+
};
239+
240+
updateViewportWidth();
241+
window.addEventListener("resize", updateViewportWidth);
242+
return () => window.removeEventListener("resize", updateViewportWidth);
243+
}, []);
244+
245+
React.useEffect(() => {
246+
const element = sidebarRef.current;
247+
if (!element) {
248+
return;
249+
}
250+
251+
const updateMeasuredWidth = () => {
252+
setMeasuredSidebarWidth(element.getBoundingClientRect().width);
253+
};
254+
255+
updateMeasuredWidth();
256+
257+
if (typeof ResizeObserver === "undefined") {
258+
return;
259+
}
260+
261+
const observer = new ResizeObserver((entries) => {
262+
for (const entry of entries) {
263+
setMeasuredSidebarWidth(entry.contentRect.width);
264+
}
265+
});
266+
267+
observer.observe(element);
268+
return () => observer.disconnect();
269+
}, [showCollapsed, selectedTab, width]);
270+
271+
const effectiveSidebarWidth = React.useMemo(() => {
272+
if (!viewportWidth) {
273+
return null;
274+
}
275+
276+
const candidate =
277+
(measuredSidebarWidth > 0 ? measuredSidebarWidth : undefined) ??
278+
(typeof width === "number" && width > 0 ? width : undefined) ??
279+
MOBILE_DEFAULT_WIDTH;
280+
const sanitized = candidate > 0 ? candidate : MOBILE_DEFAULT_WIDTH;
281+
return Math.min(sanitized, viewportWidth);
282+
}, [measuredSidebarWidth, width, viewportWidth]);
283+
284+
const overlayClickableWidth = React.useMemo(() => {
285+
if (showCollapsed || !viewportWidth) {
286+
return 0;
287+
}
288+
289+
const sidebarWidthForCalc = effectiveSidebarWidth ?? MOBILE_DEFAULT_WIDTH;
290+
return Math.max(viewportWidth - sidebarWidthForCalc, 0);
291+
}, [effectiveSidebarWidth, showCollapsed, viewportWidth]);
292+
229293
React.useEffect(() => {
230294
// Only enable swipe on mobile when sidebar is collapsed
231295
if (typeof window === "undefined") return;
@@ -292,14 +356,22 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
292356
<>
293357
{/* Backdrop overlay - only on mobile when sidebar is expanded */}
294358
{!showCollapsed && (
295-
<div
296-
className="fixed inset-0 z-[998] hidden bg-black/50 backdrop-blur-sm max-md:block"
297-
onClick={closeSidebar}
298-
aria-hidden="true"
299-
/>
359+
<div className="fixed inset-0 z-[998] hidden max-md:block" aria-hidden="true">
360+
<div className="pointer-events-none absolute inset-0 bg-black/50 backdrop-blur-sm" />
361+
{overlayClickableWidth > 1 && (
362+
<button
363+
type="button"
364+
className="absolute top-0 left-0 h-full bg-transparent"
365+
style={{ width: overlayClickableWidth }}
366+
onClick={closeSidebar}
367+
aria-label="Close review panel"
368+
/>
369+
)}
370+
</div>
300371
)}
301372

302373
<SidebarContainer
374+
ref={sidebarRef}
303375
collapsed={showCollapsed}
304376
wide={selectedTab === "review" && !width} // Auto-wide only if not drag-resizing
305377
customWidth={width} // Drag-resized width from AIView (Review tab only)

0 commit comments

Comments
 (0)