diff --git a/src/features/github/diff/reviewDiff.test.tsx b/src/features/github/diff/reviewDiff.test.tsx index cbde3dbe..0026892f 100644 --- a/src/features/github/diff/reviewDiff.test.tsx +++ b/src/features/github/diff/reviewDiff.test.tsx @@ -216,7 +216,8 @@ describe("review threads in a pull request diff", () => { expect(right.contains(t1)).toBe(true); expect(t1.closest(".cm-lineInlay")!.previousElementSibling?.textContent).toBe("line three"); expect(right.querySelectorAll(".cm-reviewControl-toggle")).toHaveLength(2); - expect(inlay("T2")).not.toBeNull(); + // Each thread's inlay mounts in its own effect, so T2 may land a tick after T1. + await waitFor(() => expect(inlay("T2")).not.toBeNull()); act(() => setReviewDiffSettings({ threadsView: "unresolved" })); await waitFor(() => expect(inlay("T2")).toBeNull()); diff --git a/src/features/search/everywhere/SearchEverywhere.tsx b/src/features/search/everywhere/SearchEverywhere.tsx index 86b116fc..f5bd6c41 100644 --- a/src/features/search/everywhere/SearchEverywhere.tsx +++ b/src/features/search/everywhere/SearchEverywhere.tsx @@ -394,7 +394,7 @@ function SearchEverywhereDialog() { aria-label={tx("Search Everywhere")} className={styles.dialog} > -
+
{TABS.map((t) => ( +
+
rows
+ , + ); + const dialog = screen.getByRole("dialog"); + const handle = dialog.querySelector("[data-dialog-drag]")!; + const down = (el: Element, x: number) => fireEvent.pointerDown(el, { button: 0, clientX: x, clientY: 10 }); + down(handle.querySelector("button")!, 10); + down(dialog.querySelector("div:last-child")!, 10); + expect(dialog.style.translate).toBe(""); + down(handle, 10); + fireEvent.pointerMove(window, { clientX: 200, clientY: 40 }); + fireEvent.pointerUp(window); + expect(dialog.style.translate).toBe("190px 30px"); + }); + + it("still drags a window dialog by its title bar", () => { + render( + {}} title="Usages Detected" buttons={safeDelete()} platform="other"> +

Usages found

+
, + ); + const dialog = screen.getByRole("dialog"); + const bar = document.querySelector("[data-dialog-titlebar]") as HTMLElement; + fireEvent.pointerDown(bar, { button: 0, clientX: 10, clientY: 10 }); + fireEvent.pointerMove(window, { clientX: 60, clientY: 40 }); + fireEvent.pointerUp(window); + expect(dialog.style.translate).toBe("80px 30px"); + // The close button never starts a drag. + fireEvent.pointerDown(within(bar).getByRole("button", { name: "Close" }), { button: 0, clientX: 10, clientY: 10 }); + fireEvent.pointerMove(window, { clientX: 200, clientY: 40 }); + fireEvent.pointerUp(window); + expect(dialog.style.translate).toBe("80px 30px"); + }); + it("renders the help button and footer start on the left", () => { const onHelp = vi.fn(); render( diff --git a/src/ui/Dialog.tsx b/src/ui/Dialog.tsx index 4d0423d9..a7f148d9 100644 --- a/src/ui/Dialog.tsx +++ b/src/ui/Dialog.tsx @@ -117,7 +117,7 @@ export interface DialogProps { /** * `top` places a lightweight modal popup (choosers, Search Everywhere) near * the top. Popups (also any dialog without a title) get no window title bar - * and are not draggable. + * and drag only by a `[data-dialog-drag]` element, such as their title. */ position?: "center" | "top"; className?: string; @@ -287,8 +287,8 @@ function DialogInner({ const drag = (e: PointerEvent) => { if ( - popup || e.button !== 0 || + !(e.target as Element).closest("[data-dialog-drag]") || (e.target as Element).closest(`button, input, label, a, select, textarea, .${selectableText}`) ) return; @@ -362,6 +362,7 @@ function DialogInner({ data-alert={icon ? "" : undefined} data-inactive={active ? undefined : ""} className={clsx(styles.dialog, popup ? styles.popup : styles.window, className)} + onPointerDown={drag} style={{ width: size ? size.w : width, height: size?.h, @@ -370,12 +371,12 @@ function DialogInner({ > {icon ? null : popup ? ( title != null && ( -
+
{title}
) ) : platform === "mac" ? ( -
+
) : ( -
+
{title}
)} {icon ? ( -
+
{icon === "app" ? ( ) : ( @@ -419,7 +420,7 @@ function DialogInner({ )} {icon ? !!buttons?.length && ( -
+
)