From 6d649d3d3fe5b9d3e63b492d4d8a1487e49e0498 Mon Sep 17 00:00:00 2001
From: Diego Rodrigues de Sa e Souza
<8016841+diegosouzapw@users.noreply.github.com>
Date: Tue, 7 Jul 2026 21:01:45 -0300
Subject: [PATCH] fix(dashboard): size the web-session cookie modal to fit on
1080p (#6265) (#6601)
---
.../[id]/components/modals/AddApiKeyModal.tsx | 4 +-
src/shared/components/Modal.tsx | 8 ++
src/shared/components/index.tsx | 2 +-
...eb-session-cookie-modal-size-6265.test.tsx | 81 +++++++++++++++++++
4 files changed, 92 insertions(+), 3 deletions(-)
create mode 100644 tests/unit/ui/web-session-cookie-modal-size-6265.test.tsx
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx
index 9802dafb35..43dc76981c 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx
+++ b/src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useTranslations } from "next-intl";
-import { Button, Badge, Input, Modal, Toggle } from "@/shared/components";
+import { Button, Badge, Input, Modal, Toggle, TALL_MODAL_PROPS } from "@/shared/components";
import {
providerAllowsOptionalApiKey,
supportsBulkApiKey,
@@ -445,7 +445,7 @@ export default function AddApiKeyModal({
title={getAddCredentialModalTitle(t, providerDisplayName, webSessionCredential)}
onClose={onClose}
size="lg"
- bodyClassName="p-6 max-h-[85vh] overflow-y-auto"
+ {...TALL_MODAL_PROPS}
>
{webProviderHostLink && (
diff --git a/src/shared/components/Modal.tsx b/src/shared/components/Modal.tsx
index dfff5de2f1..5e385ebf8d 100644
--- a/src/shared/components/Modal.tsx
+++ b/src/shared/components/Modal.tsx
@@ -4,6 +4,14 @@ import { useEffect, useRef, useId } from "react";
import { cn } from "@/shared/utils/cn";
import Button from "./Button";
+// #6265 — preset for content-heavy modals: caps height on the OUTERMOST dialog
+// wrapper only (single scroll owner) and keeps the inner body plain (no
+// independent max-h/overflow), avoiding a double height cap that clips content.
+export const TALL_MODAL_PROPS = {
+ className: "max-h-[90vh] overflow-y-auto",
+ bodyClassName: "p-6",
+};
+
interface ModalProps {
isOpen: boolean;
onClose: () => void;
diff --git a/src/shared/components/index.tsx b/src/shared/components/index.tsx
index a7ffdf4f36..774165b1ce 100644
--- a/src/shared/components/index.tsx
+++ b/src/shared/components/index.tsx
@@ -6,7 +6,7 @@ export { default as Checkbox } from "./Checkbox";
export { default as Textarea } from "./Textarea";
export { default as Card } from "./Card";
export { default as Collapsible } from "./Collapsible";
-export { default as Modal, ConfirmModal } from "./Modal";
+export { default as Modal, ConfirmModal, TALL_MODAL_PROPS } from "./Modal";
export { default as Loading, Spinner, PageLoading, Skeleton, CardSkeleton } from "./Loading";
export { default as Avatar } from "./Avatar";
export { default as Badge } from "./Badge";
diff --git a/tests/unit/ui/web-session-cookie-modal-size-6265.test.tsx b/tests/unit/ui/web-session-cookie-modal-size-6265.test.tsx
new file mode 100644
index 0000000000..6d569f9047
--- /dev/null
+++ b/tests/unit/ui/web-session-cookie-modal-size-6265.test.tsx
@@ -0,0 +1,81 @@
+// @vitest-environment jsdom
+// Issue #6265 — the "Add session cookie" modal (AddApiKeyModal, shared by every
+// `-web` cookie provider) was undersized on a 1920x1080 viewport: users had to
+// scroll *inside* the modal to reach Save, and the top of the cookie helper text
+// was clipped. Root cause: the height cap (`max-h-*` + `overflow-y-auto`) lived on
+// the INNER body div only, while the OUTERMOST dialog wrapper had no height bound
+// at all — so the whole box (header + body) could grow taller than the viewport
+// and get clipped by the centering flexbox, independent of the inner scrollbar.
+//
+// Fix: move the single height cap to the outermost dialog wrapper (`role="dialog"`)
+// and stop giving the inner body container its own independent `max-h-`/`overflow`
+// cap, so there is exactly one scroll owner for the whole modal.
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { describe, it, expect, vi, afterEach } from "vitest";
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+const { default: AddApiKeyModal } =
+ await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal");
+
+const containers: Array<{ root: ReturnType
; el: HTMLDivElement }> = [];
+
+function render(props: Record) {
+ const el = document.createElement("div");
+ document.body.appendChild(el);
+ const root = createRoot(el);
+ act(() => {
+ root.render(
+ undefined}
+ onClose={() => {}}
+ {...(props as any)}
+ />
+ );
+ });
+ containers.push({ root, el });
+ return el;
+}
+
+afterEach(() => {
+ for (const { root, el } of containers.splice(0)) {
+ act(() => root.unmount());
+ el.remove();
+ }
+});
+
+describe("AddApiKeyModal — cookie modal sizing (#6265)", () => {
+ it("caps height on the OUTERMOST dialog wrapper, not on an inner body div", () => {
+ // chatgpt-web is a `kind: "cookie"` web-session provider — same shared modal
+ // path lmarena/claude-web/gemini-web/kimi-web/z-ai all go through.
+ const el = render({ provider: "chatgpt-web", providerName: "ChatGPT (Web)" });
+
+ const dialog = el.querySelector('[role="dialog"]');
+ expect(dialog).toBeTruthy();
+
+ // The outermost wrapper must be the single owner of the height cap + scroll.
+ expect(dialog!.className).toMatch(/max-h-\[90vh\]/);
+ expect(dialog!.className).toMatch(/overflow-y-auto/);
+
+ // The body container (last child of the dialog — header is first, no footer
+ // prop is used by AddApiKeyModal) must NOT carry its own independent max-h/
+ // overflow cap — otherwise the outer cap and the inner cap fight (double cap),
+ // clipping content before the outer 90vh bound ever kicks in.
+ const bodyDiv = dialog!.children[dialog!.children.length - 1] as HTMLElement;
+ expect(bodyDiv).toBeTruthy();
+ expect(bodyDiv.className).not.toMatch(/max-h-/);
+ expect(bodyDiv.className).not.toMatch(/overflow-y-auto/);
+
+ // Sanity: the cookie helper text and Save button are both present in the DOM
+ // (this modal renders the full guide + form Save/Cancel inline in the body).
+ expect(el.textContent).toContain("How to get the session credential");
+ const saveBtn = Array.from(el.querySelectorAll("button")).find(
+ (b) => b.textContent?.trim() === "save"
+ );
+ expect(saveBtn).toBeTruthy();
+ });
+});