Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ describe("ExpandedImageDialog", () => {
await userEvent.keyboard("{Tab}");
await userEvent.keyboard("{Enter}");
}
await expect.element(page.getByRole("button", { name: "Close image preview" })).toHaveFocus();
await expect
.element(page.getByRole("button", { name: "Back, close image preview" }))
.toHaveFocus();
await userEvent.keyboard("{Escape}");
await expect.element(opener).toHaveFocus();
expect(opener.element().matches(":focus-visible")).toBe(c.focusVisible);
Expand Down Expand Up @@ -114,7 +116,9 @@ describe("ExpandedImageDialog", () => {
expect(onClose).not.toHaveBeenCalled();

// Focus returns to the popup's trigger and opens its tooltip; leave it for a plain control.
(screen.getByRole("button", { name: "Close image preview" }).element() as HTMLElement).focus();
(
screen.getByRole("button", { name: "Back, close image preview" }).element() as HTMLElement
).focus();
await userEvent.keyboard("{Escape}");
await vi.waitFor(() => expect(onClose).toHaveBeenCalledOnce());
});
Expand Down
18 changes: 13 additions & 5 deletions apps/web/src/components/chat/ExpandedImageDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { memo, useRef, useState, type ReactNode, type KeyboardEvent } from "react";
import { flushSync } from "react-dom";
import { ChevronLeftIcon, ChevronRightIcon, XIcon, ImageIcon, TextIcon } from "lucide-react";
import {
ArrowLeftIcon,
ChevronLeftIcon,
ChevronRightIcon,
ImageIcon,
TextIcon,
} from "lucide-react";
import { Button } from "../ui/button";
import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog";
import type { ExpandedImageItem, ExpandedImagePreview } from "./ExpandedImagePreview";
Expand Down Expand Up @@ -174,6 +180,7 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({
<DialogPopup
{...composerFloatingLayerProps}
variant="media"
mediaControlsPlacement="inside"
showCloseButton={false}
bottomStickOnMobile={false}
className="row-start-1 h-full max-h-[92vh] w-[92vw] max-w-[92vw] items-center justify-center overflow-visible [--media-width:92vw] [--media-height:min(calc((100vh-var(--native-titlebar-height,0px))*0.86),calc(100vh-var(--native-titlebar-height,0px)-160px))] sm:[--media-width:calc(92vw-96px)]"
Expand All @@ -188,13 +195,14 @@ export const ExpandedImageDialog = memo(function ExpandedImageDialog({
<Button
type="button"
ref={closeButtonRef}
size="icon-sm"
size="sm"
variant="media-close"
className="absolute right-0 top-0 z-20"
className="absolute left-0 top-0 z-20 max-[8rem]:left-1/2 max-[8rem]:-translate-x-1/2"
onClick={close}
aria-label={`Close ${mediaLabel} preview`}
aria-label={`Back, close ${mediaLabel} preview`}
>
<XIcon />
<ArrowLeftIcon aria-hidden="true" />
Back
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
</Button>
{preview.images.length > 1 && (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ it.each([false, true])(
await expect.element(page.getByText("second.png (2/2)", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Previous media", exact: true }).click();
await expect.element(page.getByText("first.png (1/2)", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Close image preview", exact: true }).click();
await page.getByRole("button", { name: "Back, close image preview", exact: true }).click();
await expect.element(page.getByRole("dialog")).not.toBeInTheDocument();
await expect.element(page.getByText("Later", { exact: true })).toBeVisible();
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1477,7 +1477,7 @@ it("toggles a Mermaid attachment between diagram and source through the lazy pre
expect(image?.alt).toBe("Mermaid diagram");
expect(image?.naturalWidth).toBeGreaterThan(0);
});
await page.getByRole("button", { name: "Close image preview" }).click();
await page.getByRole("button", { name: "Back, close image preview" }).click();
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
await screen.getByRole("button", { name: "Show Mermaid source" }).click();
await vi.waitFor(() => expect(screen.container.querySelector("pre")?.textContent).toBe(text));
Expand Down
25 changes: 23 additions & 2 deletions apps/web/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
import { useRender } from "@base-ui/react/use-render";
import { XIcon } from "lucide-react";
import { type ComponentProps, createContext, type ReactNode, use, useRef, useState } from "react";
import {
type ComponentProps,
type CSSProperties,
createContext,
type ReactNode,
use,
useRef,
useState,
} from "react";
import { cn } from "~/lib/utils";
import { useNativeTitlebarHeight } from "~/lib/windowControlsOverlay";
import { Button, type ButtonVariant } from "~/components/ui/button";
import {
DIALOG_BACKDROP_CLASS,
Expand Down Expand Up @@ -103,11 +112,13 @@ function DialogPopup({
showCloseButton = true,
bottomStickOnMobile = true,
variant = "default",
mediaControlsPlacement = "outside",
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean;
bottomStickOnMobile?: boolean;
variant?: "default" | "media";
mediaControlsPlacement?: "inside" | "outside";
}) {
const { popupRef, initialFocus } = useDialogActionInitialFocus();
const popup = useRender({
Expand Down Expand Up @@ -139,6 +150,8 @@ function DialogPopup({
</DialogPrimitive.Popup>
),
});
// macOS has no CSS window-controls overlay, so use the platform titlebar height.
const nativeTitlebarHeight = useNativeTitlebarHeight();
return (
<DialogPortal>
{/* Media opens from inside other overlays (a composer chip, a popover), so it sits above them. */}
Expand All @@ -147,8 +160,16 @@ function DialogPopup({
className={cn(
bottomStickOnMobile && "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
variant === "media" &&
"z-[60] grid-rows-1 place-items-center px-4 pb-6 pt-[calc(var(--native-titlebar-height,0px)+1.5rem)]",
"z-[60] grid-rows-1 place-items-center px-4 max-[8rem]:px-0 pb-6 pt-[calc(var(--media-dialog-titlebar-height,0px)+1.5rem)] [-webkit-app-region:no-drag]",
variant === "media" &&
mediaControlsPlacement === "inside" &&
"pt-[min(calc(var(--media-dialog-titlebar-height,0px)+1.5rem),50dvh)]",
)}
style={
variant === "media"
? ({ "--media-dialog-titlebar-height": `${nativeTitlebarHeight}px` } as CSSProperties)
: undefined
}
>
{popup}
</DialogViewport>
Expand Down
Loading