Skip to content

Commit 2beefa0

Browse files
salevineclaude
andcommitted
fix: keep the dragged eval popup position across window blurs
Minimizing Chrome (or switching apps) blurs the focused editor, which closes the popup; the close handler then discarded the hand-dragged position, so the popup teleported back to its anchor the moment the window was restored. Only re-anchor when the popup closes while the document still has focus (the user deliberately moved elsewhere in the app). document.hasFocus() is false during window-level blurs, so those keep the dragged position. Adds two tests that simulate the full drag in jsdom and toggle isOpen with document.hasFocus() mocked each way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011K3yPfQMWxQK15jxkFsfQW
1 parent 097d80b commit 2beefa0

2 files changed

Lines changed: 100 additions & 6 deletions

File tree

app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.test.tsx

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,77 @@ import { theme } from "constants/DefaultTheme";
1111
import { EditorTheme } from "./EditorConfig";
1212
import { LOCAL_STORAGE_KEYS } from "utils/localStorage";
1313

14-
function renderPopup(props?: {
14+
function popupJsx(props?: {
1515
hasError?: boolean;
1616
hideEvaluatedValue?: boolean;
17+
isOpen?: boolean;
1718
}) {
18-
return render(
19+
return (
1920
<Provider store={store}>
2021
<ThemeProvider theme={theme}>
2122
<EvaluatedValuePopup
2223
errors={[]}
2324
hasError={props?.hasError || false}
2425
hideEvaluatedValue={props?.hideEvaluatedValue || false}
25-
isOpen
26+
isOpen={props?.isOpen ?? true}
2627
theme={EditorTheme.LIGHT}
2728
>
2829
<div>children</div>
2930
</EvaluatedValuePopup>
3031
</ThemeProvider>
31-
</Provider>,
32+
</Provider>
3233
);
3334
}
3435

36+
function renderPopup(props?: {
37+
hasError?: boolean;
38+
hideEvaluatedValue?: boolean;
39+
isOpen?: boolean;
40+
}) {
41+
return render(popupJsx(props));
42+
}
43+
44+
// Drags the popup by its handle so its internal dragged-position state is
45+
// set, exactly as a user drag would. jsdom elements measure 0x0, and a
46+
// zero-size popup is treated as closed on mouseup, so give the popper
47+
// element a real box first.
48+
function dragPopupTo(clientX: number, clientY: number) {
49+
const popperElement = document.querySelector(".t--CodeEditor-evaluatedValue")
50+
?.parentElement as HTMLElement;
51+
52+
popperElement.getBoundingClientRect = () =>
53+
({
54+
left: 100,
55+
top: 100,
56+
width: 300,
57+
height: 118,
58+
right: 400,
59+
bottom: 218,
60+
x: 100,
61+
y: 100,
62+
toJSON: () => ({}),
63+
}) as DOMRect;
64+
65+
const handle = document.querySelector(
66+
"[id$='-popper-draghandler']",
67+
) as HTMLElement;
68+
69+
fireEvent.mouseDown(handle, { clientX: 10, clientY: 10 });
70+
fireEvent.mouseMove(document, { clientX, clientY });
71+
fireEvent.mouseUp(document, { clientX, clientY });
72+
73+
return popperElement;
74+
}
75+
3576
describe("EvaluatedValuePopup", () => {
3677
beforeEach(() => {
3778
localStorage.removeItem(LOCAL_STORAGE_KEYS.EVALUATED_VALUE_POPUP_COLLAPSED);
3879
});
3980

81+
afterEach(() => {
82+
jest.restoreAllMocks();
83+
});
84+
4085
it("should render evaluated popup when hideEvaluatedValue is false", () => {
4186
renderPopup();
4287
const input = screen.queryByTestId("evaluated-value-popup-title");
@@ -125,4 +170,47 @@ describe("EvaluatedValuePopup", () => {
125170
screen.getByTestId("t--evaluated-popup-error-indicator"),
126171
).toBeTruthy();
127172
});
173+
174+
it("should keep the dragged position across a window-level blur (minimize / app switch)", () => {
175+
jest.spyOn(document, "hasFocus").mockReturnValue(false);
176+
177+
const { rerender } = renderPopup();
178+
179+
// the anchor ref is null on the very first render; a second render lets
180+
// the popper attach and create its drag handle
181+
rerender(popupJsx({ isOpen: true }));
182+
dragPopupTo(60, 60);
183+
184+
// window loses focus -> editor blurs -> popup closes
185+
rerender(popupJsx({ isOpen: false }));
186+
// window restored, editor refocuses -> popup reopens
187+
rerender(popupJsx({ isOpen: true }));
188+
189+
const popperElement = document.querySelector(
190+
".t--CodeEditor-evaluatedValue",
191+
)?.parentElement as HTMLElement;
192+
193+
expect(popperElement.style.top).toBe("100px");
194+
expect(popperElement.style.left).toBe("100px");
195+
});
196+
197+
it("should re-anchor after an in-app blur discards the dragged position", () => {
198+
jest.spyOn(document, "hasFocus").mockReturnValue(true);
199+
200+
const { rerender } = renderPopup();
201+
202+
rerender(popupJsx({ isOpen: true }));
203+
dragPopupTo(60, 60);
204+
205+
// the user moves on inside the app: close and reopen
206+
rerender(popupJsx({ isOpen: false }));
207+
rerender(popupJsx({ isOpen: true }));
208+
209+
const popperElement = document.querySelector(
210+
".t--CodeEditor-evaluatedValue",
211+
)?.parentElement as HTMLElement;
212+
213+
expect(popperElement.style.top).not.toBe("100px");
214+
expect(popperElement.style.left).not.toBe("100px");
215+
});
128216
});

app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,9 +764,15 @@ function EvaluatedValuePopup(props: Props) {
764764

765765
// A hand-dragged position used to stick for the lifetime of the component,
766766
// so the only way back to the anchored spot was to drag it there by hand.
767-
// The popup already hides on blur, so reopening it re-anchors to the field.
767+
// The popup already hides on blur, so reopening it re-anchors to the field —
768+
// but only for blurs inside the app. When the whole window loses focus
769+
// (minimize, switching apps), the editor blurs too; discarding the dragged
770+
// position then would teleport the popup back to its anchor the moment the
771+
// user returns. document.hasFocus() separates the two cases.
768772
useEffect(() => {
769-
if (!isPopupOpen && position !== undefined) setPosition(undefined);
773+
if (!isPopupOpen && position !== undefined && document.hasFocus()) {
774+
setPosition(undefined);
775+
}
770776
}, [isPopupOpen, position]);
771777

772778
const wrapperRef = useRef<HTMLDivElement>(null);

0 commit comments

Comments
 (0)