From bede561f109449ada83134d68d6d563bce01c6e1 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 5 Jun 2026 21:49:54 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=8A=20fix:=20Contain=20Markdown=20Tabl?= =?UTF-8?q?e=20Overflow=20(#13543)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Contain Markdown table overflow * fix: Improve Markdown table scrollbar --- .../Chat/Messages/Content/Markdown.tsx | 3 +- .../Messages/Content/MarkdownComponents.tsx | 13 ++++++++ .../Content/MarkdownErrorBoundary.tsx | 3 +- .../Chat/Messages/Content/MarkdownLite.tsx | 3 +- .../Content/__tests__/Markdown.mcpui.test.tsx | 33 +++++++++++++++++++ client/src/style.css | 21 ++++++++++++ 6 files changed, 73 insertions(+), 3 deletions(-) diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index 1217869a2c..4daef8f80b 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -19,7 +19,7 @@ import { ArtifactProvider, CodeBlockProvider } from '~/Providers'; import MarkdownErrorBoundary from './MarkdownErrorBoundary'; import { langSubset, preprocessLaTeX } from '~/utils'; import { unicodeCitation } from '~/components/Web'; -import { code, a, p, img } from './MarkdownComponents'; +import { code, a, p, img, table } from './MarkdownComponents'; import store from '~/store'; type TContentProps = { @@ -88,6 +88,7 @@ const Markdown = memo(function Markdown({ content = '', isLatestMessage }: TCont a, p, img, + table, artifact: Artifact, citation: Citation, 'highlighted-text': HighlightedText, diff --git a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx index c38c40f1e1..5147dd2a9a 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx @@ -185,6 +185,19 @@ export const p: React.ElementType = memo(function MarkdownParagraph({ children } }); p.displayName = 'MarkdownParagraph'; +type TTableProps = { + children: React.ReactNode; +}; + +export const table: React.ElementType = memo(function MarkdownTable({ children }: TTableProps) { + return ( +
+ {children}
+
+ ); +}); +table.displayName = 'MarkdownTable'; + type TImageProps = { src?: string; alt?: string; diff --git a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx index 0342c60f8a..9862da2102 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx @@ -4,7 +4,7 @@ import supersub from 'remark-supersub'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import type { PluggableList } from 'unified'; -import { code, codeNoExecution, a, p } from './MarkdownComponents'; +import { code, codeNoExecution, a, p, table } from './MarkdownComponents'; import { CodeBlockProvider } from '~/Providers'; import { langSubset } from '~/utils'; @@ -72,6 +72,7 @@ class MarkdownErrorBoundary extends React.Component< code: codeExecution ? code : codeNoExecution, a, p, + table, } as { [nodeType: string]: React.ElementType; } diff --git a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx index 24980d8a90..44656ae77f 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx @@ -6,7 +6,7 @@ import supersub from 'remark-supersub'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import type { PluggableList } from 'unified'; -import { code, codeNoExecution, a, p, img } from './MarkdownComponents'; +import { code, codeNoExecution, a, p, img, table } from './MarkdownComponents'; import { CodeBlockProvider, ArtifactProvider } from '~/Providers'; import MarkdownErrorBoundary from './MarkdownErrorBoundary'; import { langSubset } from '~/utils'; @@ -44,6 +44,7 @@ const MarkdownLite = memo( a, p, img, + table, } as { [nodeType: string]: React.ElementType; } diff --git a/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx index 6ca06056fa..bde9614904 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import Markdown from '../Markdown'; +import MarkdownLite from '../MarkdownLite'; import { RecoilRoot } from 'recoil'; import { UI_RESOURCE_MARKER } from '~/components/MCPUIResource/plugin'; import { @@ -108,3 +109,35 @@ describe('Markdown with MCP UI markers (resource IDs)', () => { expect(renderers[1]).toHaveAttribute('data-resource-uri', 'ui://weather/nyc'); }); }); + +describe('Markdown table rendering', () => { + const tableMarkdown = [ + '| Alpha | Bravo | Charlie | Delta | Echo | Foxtrot | Golf | Hotel |', + '| --- | --- | --- | --- | --- | --- | --- | --- |', + '| one | two | three | four | five | six | seven | eight |', + ].join('\n'); + + it('wraps GFM tables in a horizontally scrollable container', () => { + render( + + + , + ); + + expect(screen.getByRole('table').parentElement).toHaveClass( + 'markdown-table-wrapper', + 'w-full', + 'max-w-full', + ); + }); + + it('wraps lightweight Markdown tables in a horizontally scrollable container', () => { + render(); + + expect(screen.getByRole('table').parentElement).toHaveClass( + 'markdown-table-wrapper', + 'w-full', + 'max-w-full', + ); + }); +}); diff --git a/client/src/style.css b/client/src/style.css index 902860ef52..35dee56a1d 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1496,6 +1496,27 @@ button { background-color: transparent; } +.markdown-table-wrapper { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + overscroll-behavior-x: contain; + padding-bottom: 0.25rem; + scrollbar-color: var(--border-medium) transparent; + scrollbar-width: thin; +} + +.markdown-table-wrapper::-webkit-scrollbar { + height: 0.5rem; +} + +.markdown-table-wrapper::-webkit-scrollbar-thumb { + background-color: var(--border-medium); +} + +.markdown-table-wrapper::-webkit-scrollbar-thumb:hover { + background-color: var(--border-heavy); +} + /* Show scrollbar only on hover */ .scrollbar-hover { scrollbar-width: thin;