fix(ui): keep long Markdown previews scrollable (#113465)

* [AI] fix(ui): add min-height:0 to flex scroll containers for Firefox

Firefox enforces the CSS Flexbox automatic minimum size more strictly
than Chromium, preventing .sidebar-content and .md-preview-dialog__body
from shrinking below their content height. This blocked overflow:auto
from creating a scrollable area when Markdown content exceeded the
available panel height.

Add min-height:0 to both flex children so Firefox allows them to shrink
and become the inner scroll container.

Related to #107571

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* [AI] fix(ui): bound Markdown sidebar previews

* [AI] fix(ui): bound file sidebar editors

* test(ui): cover scroll repair in sidebar column layout

---------

Co-authored-by: 杨爱文 <yang.aiwen@xydigit.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Thiago Pontes AI <thiagopontesai@Hefestos-Mac-Mini.local>
This commit is contained in:
Thiago Pontes
2026-07-27 04:54:03 -03:00
committed by GitHub
parent 3b519cecb8
commit bb687fa2de
4 changed files with 98 additions and 1 deletions
@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import "../../../styles.css";
import "../../../styles/chat.css";
import type { SidebarContent } from "./chat-sidebar.ts";
import "./chat-sidebar.ts";
const browserMode = "__vitest_browser__" in globalThis;
type DetailPanel = HTMLElement & {
content: SidebarContent;
updateComplete: Promise<unknown>;
};
describe.runIf(browserMode)("chat sidebar layout", () => {
it("keeps long markdown scrollable inside a bounded sidebar", async () => {
const container = document.createElement("div");
container.className = "sidebar-column__panel";
container.style.cssText = "display:flex;width:480px;height:320px;";
const panel = document.createElement("openclaw-chat-detail-panel") as DetailPanel;
panel.className = "chat-sidebar";
panel.content = {
kind: "markdown",
content: Array.from(
{ length: 40 },
(_, index) => `## Section ${index + 1}\n\nLong preview content for scrolling.`,
).join("\n\n"),
};
container.append(panel);
document.body.append(container);
try {
await panel.updateComplete;
const content = panel.querySelector<HTMLElement>(".sidebar-content");
expect(content).not.toBeNull();
expect(content!.clientHeight).toBeLessThan(content!.scrollHeight);
content!.scrollTop = content!.scrollHeight;
await new Promise(requestAnimationFrame);
expect(content!.scrollTop).toBeGreaterThan(0);
} finally {
container.remove();
}
});
it("keeps long files scrollable inside CodeMirror", async () => {
const container = document.createElement("div");
container.className = "sidebar-column__panel";
container.style.cssText = "display:flex;width:480px;height:320px;";
const panel = document.createElement("openclaw-chat-detail-panel") as DetailPanel;
panel.className = "chat-sidebar";
panel.content = {
kind: "file",
path: "src/long-example.ts",
name: "long-example.ts",
language: "typescript",
content: Array.from(
{ length: 200 },
(_, index) => `export const value${index + 1} = ${index + 1};`,
).join("\n"),
};
container.append(panel);
document.body.append(container);
try {
await panel.updateComplete;
await expect
.poll(() => panel.querySelector<HTMLElement>(".cm-scroller"), { timeout: 5_000 })
.not.toBeNull();
const scroller = panel.querySelector<HTMLElement>(".cm-scroller");
expect(scroller).not.toBeNull();
expect(scroller!.clientHeight).toBeLessThan(scroller!.scrollHeight);
scroller!.scrollTop = scroller!.scrollHeight;
await new Promise(requestAnimationFrame);
expect(scroller!.scrollTop).toBeGreaterThan(0);
} finally {
container.remove();
}
});
});
+5 -1
View File
@@ -1280,8 +1280,12 @@ class ChatDetailPanel extends OpenClawLightDomElement {
const currentMatchIndex = matches.length
? Math.min(this.fileSearchMatchIndex, matches.length - 1)
: 0;
// Markdown previews and file editors need a bounded host wrapper so their
// inner content can shrink and scroll. Content-sized kinds keep auto height.
const fillHost =
this.visibleContent?.kind === "file" || this.visibleContent?.kind === "markdown";
return html`
<div @click=${this.handlePanelClick}>
<div class=${fillHost ? "sidebar-panel-host--fill" : ""} @click=${this.handlePanelClick}>
${renderMarkdownSidebar({
content: this.visibleContent,
error: this.error,
+10
View File
@@ -1347,10 +1347,20 @@ openclaw-chat-sidebar-region,
.sidebar-content {
flex: 1;
min-height: 0;
overflow: auto;
padding: 16px;
}
/* Full-height panel kinds need a bounded wrapper so their inner content can
shrink and scroll instead of expanding past the rail. */
.sidebar-panel-host--fill {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
openclaw-session-discussion {
display: flex;
flex: 1 1 0;
+1
View File
@@ -4040,6 +4040,7 @@ td.data-table-key-col {
.md-preview-dialog__body {
flex: 1;
min-height: 0;
overflow: auto;
overscroll-behavior: contain;
padding: clamp(18px, 3vw, 28px);