Skip to content

Commit 932a4de

Browse files
var-ggclaude
andcommitted
fix(chip): keep the dropdown inside the window
The `align` prop only chooses which edge a dropdown anchors to; on the fixed 520px panel a rightmost chip's dropdown can still spill past the left window edge — visible once an active repo filter shortens the RepoChip label and shifts the chip row left. Measure the dropdown when it opens and nudge it back inside with a transform. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a9a269e commit 932a4de

1 file changed

Lines changed: 34 additions & 2 deletions

File tree

src/components/ChipDropdown.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { useEffect, useRef, type ReactNode } from "react";
1+
import {
2+
useEffect,
3+
useLayoutEffect,
4+
useRef,
5+
useState,
6+
type ReactNode,
7+
} from "react";
28

39
interface Props {
410
id: string;
@@ -24,6 +30,8 @@ export function ChipDropdown({
2430
align = "left",
2531
}: Props) {
2632
const ref = useRef<HTMLDivElement | null>(null);
33+
const dropdownRef = useRef<HTMLDivElement | null>(null);
34+
const [shift, setShift] = useState(0);
2735

2836
useEffect(() => {
2937
if (!open) return;
@@ -42,6 +50,24 @@ export function ChipDropdown({
4250
};
4351
}, [open, onClose]);
4452

53+
// `align` only picks which edge to anchor to; on the fixed-width panel a
54+
// dropdown can still spill past the opposite window edge. Once open,
55+
// measure it and nudge it back inside with a transform.
56+
useLayoutEffect(() => {
57+
if (!open) return;
58+
const el = dropdownRef.current;
59+
if (!el) return;
60+
const pad = 8;
61+
const rect = el.getBoundingClientRect();
62+
const baseLeft = rect.left - shift;
63+
const baseRight = rect.right - shift;
64+
let dx = 0;
65+
if (baseLeft < pad) dx = pad - baseLeft;
66+
else if (baseRight > window.innerWidth - pad)
67+
dx = window.innerWidth - pad - baseRight;
68+
if (dx !== shift) setShift(dx);
69+
}, [open, align, shift]);
70+
4571
// When the label itself is a string, surface it as the button title so
4672
// the truncated/ellipsised text still has a hover-reveal — caller can
4773
// still override with an explicit `title` prop.
@@ -63,7 +89,13 @@ export function ChipDropdown({
6389
<span className="chip-caret"></span>
6490
</button>
6591
{open && (
66-
<div className={"chip-dropdown chip-dropdown-" + align}>{children}</div>
92+
<div
93+
ref={dropdownRef}
94+
className={"chip-dropdown chip-dropdown-" + align}
95+
style={shift ? { transform: `translateX(${shift}px)` } : undefined}
96+
>
97+
{children}
98+
</div>
6799
)}
68100
</div>
69101
);

0 commit comments

Comments
 (0)