From c9ce61136d3665c3d9e8591281c33d4600093c9f Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Thu, 20 Aug 2026 14:45:15 +0800 Subject: [PATCH] feat(web): reveal the wide-table scrollbar on hover instead of painting it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The themed WebKit scrollbar skin keeps a wide table's horizontal bar permanently painted. Chromium never repaints state-conditioned scrollbar styles (hover-conditioned ::-webkit-scrollbar* rules and :hover scrollbar-color changes both compute but never reach the painted bar, measured headed and headless), so the hover reveal toggles overflow-x itself: hidden at rest with a padding-bottom matching the themed bar height, auto on hover or keyboard focus with the padding released — the appearing bar exactly replaces the padding and nothing below shifts. Resting hidden overflow drops Chromium's implicit scroller focusability, so wide wrappers carry an explicit tabindex for arrow-key scrolling. --- ...-19-web-markdown-wide-table-view.i18n.yaml | 4 +-- ...2026-08-19-web-markdown-wide-table-view.md | 2 +- ...6-08-19-web-markdown-wide-table-view.zh.md | 2 +- apps/web/tests/markdown-wide-table.e2e.ts | 28 +++++++++++++++++++ .../client/ui-primitives/README.i18n.yaml | 4 +-- packages/client/ui-primitives/README.md | 2 +- packages/client/ui-primitives/README.zh.md | 2 +- .../src/markdown/MarkdownText.module.css | 20 +++++++++++++ .../ui-primitives/src/markdown/render.tsx | 12 ++++++-- .../table-wide-and-blockquote.settled.txt | 2 +- .../table-wide-and-blockquote.streaming.txt | 2 +- .../table-with-alignment.settled.txt | 2 +- .../table-with-alignment.streaming.txt | 2 +- 13 files changed, 70 insertions(+), 14 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.i18n.yaml b/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.i18n.yaml index 19561bb6f0..e4b5654510 100644 --- a/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md -2026-08-19-web-markdown-wide-table-view.md: 847b3f055edd29eeb0b6f3f8bec5b97252aca4f4 -2026-08-19-web-markdown-wide-table-view.zh.md: d2fcc18f883f71a3fdbfbab6476c87b1c9840b21 +2026-08-19-web-markdown-wide-table-view.md: fc025f6755caa2ca160b02f0c03ae80fd04cc1c7 +2026-08-19-web-markdown-wide-table-view.zh.md: 4d13a29f69fac31f9796dae064726d4d3a779ffd diff --git a/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md b/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md index 847b3f055e..fc025f6755 100644 --- a/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md +++ b/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md @@ -26,7 +26,7 @@ English | [中文](2026-08-19-web-markdown-wide-table-view.zh.md) ## Consequences -An ordinary wide table reads in place with wrapped cells; a many-column table keeps its readable natural width, spans the whole transcript where the layout has spare width, and scrolls for the remainder — with no interaction required and nothing to restore. Chromium keyboard-focuses scrollable containers by default, so the wrapper stays keyboard-scrollable without added attributes (a `:focus-visible` ring marks it). Two knowledge edges: `container-type: inline-size` on ChatView's `.scroll` makes it the nearest query container for anything inside the transcript that later uses container units, and sub-four-column tables now always stretch to the full column width (deepsuite chat behavior) rather than shrink-wrapping short content. +An ordinary wide table reads in place with wrapped cells; a many-column table keeps its readable natural width, spans the whole transcript where the layout has spare width, and scrolls for the remainder — with no interaction required and nothing to restore. A wide table's horizontal bar reveals on hover instead of staying painted: Chromium never repaints state-conditioned scrollbar styles (neither hover-conditioned `::-webkit-scrollbar*` rules nor a `:hover` `scrollbar-color` change reaches the painted bar — measured headed and headless), so the reveal toggles `overflow-x` itself (`hidden` at rest, `auto` on hover or focus), with resting `padding-bottom` matching the themed bar height so the appearing bar replaces it without moving content below. Resting `overflow-x: hidden` drops Chromium's implicit scroller focusability, so wide wrappers carry an explicit `tabindex="0"` (a `:focus-visible` ring marks them, and focus restores scrolling for arrow keys). Two knowledge edges: `container-type: inline-size` on ChatView's `.scroll` makes it the nearest query container for anything inside the transcript that later uses container units, and sub-four-column tables now always stretch to the full column width (deepsuite chat behavior) rather than shrink-wrapping short content. ## Testing diff --git a/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.zh.md b/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.zh.md index d2fcc18f88..4d13a29f69 100644 --- a/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.zh.md +++ b/.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.zh.md @@ -26,7 +26,7 @@ Status: implemented ## 后果 -普通宽表原地换行阅读;多列表格保持可读的自然宽度,在布局有空余处横跨整个转录区,剩余部分滚动——无需任何交互,也没有状态要恢复。Chromium 默认让可滚动容器可键盘聚焦,因此包裹层无需附加属性即可键盘滚动(`:focus-visible` 有焦点圈)。两个需要知道的点:ChatView `.scroll` 上的 `container-type: inline-size` 使它成为转录区内后续使用容器单位的最近查询容器;不足四列的表格现在总是拉伸到整列宽(deepsuite chat 行为),而不是按内容收缩。 +普通宽表原地换行阅读;多列表格保持可读的自然宽度,在布局有空余处横跨整个转录区,剩余部分滚动——无需任何交互,也没有状态要恢复。宽表的横向滚动条悬停才出现、不再常驻:Chromium 从不重绘状态条件化的滚动条样式(悬停条件化的 `::-webkit-scrollbar*` 规则和 `:hover` 下的 `scrollbar-color` 变化都到不了已绘制的滚动条——有头与无头模式均已实测),因此显隐切换的是 `overflow-x` 本身(静止 `hidden`,悬停或聚焦 `auto`),静止时的 `padding-bottom` 与主题滚动条高度一致,出现的滚动条恰好顶替它、下方内容不动。静止的 `overflow-x: hidden` 会失去 Chromium 对滚动容器的隐式可聚焦性,因此宽表包裹层带显式 `tabindex="0"`(`:focus-visible` 有焦点圈,聚焦后方向键可滚)。两个需要知道的点:ChatView `.scroll` 上的 `container-type: inline-size` 使它成为转录区内后续使用容器单位的最近查询容器;不足四列的表格现在总是拉伸到整列宽(deepsuite chat 行为),而不是按内容收缩。 ## 测试 diff --git a/apps/web/tests/markdown-wide-table.e2e.ts b/apps/web/tests/markdown-wide-table.e2e.ts index da2c371085..bb897ea364 100644 --- a/apps/web/tests/markdown-wide-table.e2e.ts +++ b/apps/web/tests/markdown-wide-table.e2e.ts @@ -370,6 +370,34 @@ describe('web e2e: markdown tables fill the column, wide ones break out and scro expect(tripwire.pageErrors).toEqual([]) }, 120_000) + it('reveals the wide table scrollbar on hover only', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-wide-table-scrollbar')) + await sweep() + await settleAt(1680) + const wide = page.locator('[class*="tableScroll"]', { hasText: WIDE_MARKER }) + // Chromium never repaints state-conditioned scrollbar STYLES, so the + // hover reveal toggles overflow-x itself; the resting padding matches + // the bar height so the swap does not move anything below. Both are + // ordinary properties whose computed values follow :hover. + const overflowState = () => wide.evaluate(element => [ + getComputedStyle(element).overflowX, + getComputedStyle(element).paddingBottom, + ].join(' ')) + // Park the pointer away and drop focus: the keyboard case above leaves + // the wrapper focused, and focus-visible also reveals the bar. + await page.mouse.move(4, 4) + await wide.evaluate((element) => { element.blur() }) + await expect.poll(overflowState, { timeout: 5_000 }).toBe('hidden 8px') + // Resting hidden overflow keeps the scroll position reachable and intact. + expect(await wide.evaluate(element => element.scrollLeft)).toBeGreaterThanOrEqual(0) + await wide.hover() + await expect.poll(overflowState, { timeout: 5_000 }).toBe('auto 0px') + // Pointer leaves: the bar rests hidden again. + await page.mouse.move(4, 4) + await expect.poll(overflowState, { timeout: 5_000 }).toBe('hidden 8px') + expect(tripwire.pageErrors).toEqual([]) + }, 120_000) + it('keeps the fill/scroll relations under page zoom', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-markdown-wide-table-zoom')) await sweep() diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 6856283fcc..e6ed8300ac 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md -README.md: 0d7848112b650a322965ad52a38d23e0145e7921 -README.zh.md: 475af21d5a7bc1bc48115b171d92b9a0d44df309 +README.md: 7822a5d41e8125752b8bc28fea3db2232323fd81 +README.zh.md: 5a78e683f3bb94f8b9d3a64b0d4ba1f3f3df9d85 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 0d7848112b..7822a5d41e 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -14,7 +14,7 @@ Pure React atoms (zero cordis): StateDot, DisclosureRow, ic_ds_* icons, Button/P ## Markdown rendering -`MarkdownText` renders GFM and `$…$`, `$$…$$`, `\(…\)`, and `\[…\]` TeX math from untrusted assistant output through React elements, with math typeset by KaTeX and trusted commands disabled; block-level same-line `$$…$$` is display math, including `\tag{}`. A narrow micromark extension lets asterisk strong emphasis ending in punctuation close before adjacent CJK text, where prose normally omits the whitespace CommonMark requires; single-asterisk emphasis, non-CJK adjacency, escapes, code, and math retain upstream parsing. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders absolute HTTP(S) images without a referrer; relative paths, absolute local paths, `file:` URLs, and unsupported schemes retain their alt text. Inline code whose complete value is an absolute HTTP(S) URL keeps its code styling and gains the same safe external anchor; commands, partial URLs, other schemes, and fenced code remain inert. An optional `fileMentions` resolver lets the owning view link inline code that names a real file: the token keeps code styling and gains a button wired to the resolved opener, with the resolver's accessible label and full-path `title`. The renderer never guesses at what looks like a path — an unresolved token stays inert, mentions apply to settled renders only (the streaming cache must not bake in handlers that could go stale), and a token inside an anchor stays inert because a button cannot nest there. While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail behind them re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply ([mechanism and DOM-parity contract](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md)). Tables size by column count (deepsuite chat parity): under four columns — or inside a blockquote — a table fills its column and wraps cell text down to the cells' minimum readable width, while four-or-more-column tables keep their natural width, scroll horizontally inside their wrapper, and carry the stable `md-table-wide` class so a hosting layout can widen the wrapper past its column (the chat transcript's container-query breakout in `dsh-client-ui-conversation`); Chromium keyboard-focuses the scrollable wrapper by default ([decision record](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md)). `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, responsive images, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars). +`MarkdownText` renders GFM and `$…$`, `$$…$$`, `\(…\)`, and `\[…\]` TeX math from untrusted assistant output through React elements, with math typeset by KaTeX and trusted commands disabled; block-level same-line `$$…$$` is display math, including `\tag{}`. A narrow micromark extension lets asterisk strong emphasis ending in punctuation close before adjacent CJK text, where prose normally omits the whitespace CommonMark requires; single-asterisk emphasis, non-CJK adjacency, escapes, code, and math retain upstream parsing. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders absolute HTTP(S) images without a referrer; relative paths, absolute local paths, `file:` URLs, and unsupported schemes retain their alt text. Inline code whose complete value is an absolute HTTP(S) URL keeps its code styling and gains the same safe external anchor; commands, partial URLs, other schemes, and fenced code remain inert. An optional `fileMentions` resolver lets the owning view link inline code that names a real file: the token keeps code styling and gains a button wired to the resolved opener, with the resolver's accessible label and full-path `title`. The renderer never guesses at what looks like a path — an unresolved token stays inert, mentions apply to settled renders only (the streaming cache must not bake in handlers that could go stale), and a token inside an anchor stays inert because a button cannot nest there. While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail behind them re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply ([mechanism and DOM-parity contract](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md)). Tables size by column count (deepsuite chat parity): under four columns — or inside a blockquote — a table fills its column and wraps cell text down to the cells' minimum readable width, while four-or-more-column tables keep their natural width, scroll horizontally inside their wrapper, and carry the stable `md-table-wide` class so a hosting layout can widen the wrapper past its column (the chat transcript's container-query breakout in `dsh-client-ui-conversation`); a wide table's horizontal bar reveals on hover or keyboard focus (the wrapper carries `tabindex="0"`) instead of staying painted ([decision record](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md)). `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, responsive images, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars). ## Terminal output diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 475af21d5a..5a78e683f3 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -14,7 +14,7 @@ ## Markdown 渲染 -`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM 与 `$…$`、`$$…$$`、`\(…\)` 和 `\[…\]` TeX 公式,公式由 KaTeX 排版并禁用受信任命令;块级同一行 `$$…$$` 是显示公式并支持 `\tag{}`。一个小范围的 micromark 扩展允许由星号标记、以标点结尾的粗体在紧邻的 CJK 文本前闭合,以适应 CJK 文本通常省略 CommonMark 所要求空格的写法;单星号强调、紧邻非 CJK 文本的情况、转义、代码与数学公式仍沿用上游解析行为。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并在不发送 referrer 的情况下渲染采用绝对 HTTP(S) URL 的图片;相对路径、绝对本地路径、`file:` URL 与不受支持的 scheme 会保留其 alt 文本。完整内容为绝对 HTTP(S) URL 的行内代码会保留代码样式,并获得同样安全的外部链接;命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。可选的 `fileMentions` 解析器让持有该组件的视图为命名真实文件的行内代码添加可点击入口:token 保留代码样式,并获得一个连接到解析所得 opener 的按钮,按钮带有解析器提供的无障碍标签和以完整路径为值的 `title`。渲染器绝不猜测哪些内容像路径:未解析的 token 保持不可交互;文件提及仅应用于已定稿的渲染(流式缓存不得固化可能过期的 handler);锚点内的 token 也保持不可交互,因为按钮不能嵌套其中。回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复([机制与 DOM 一致性约定](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md))。表格按列数决定尺寸(对齐 deepsuite chat):不足四列——或位于 blockquote 内——的表格填满所在列,单元格文本换行收缩至最小可读列宽;四列及以上的表格保持自然宽度、在包裹层内横向滚动,并携带稳定的 `md-table-wide` 类,供宿主布局把包裹层加宽到所在列之外(`dsh-client-ui-conversation` 中聊天转录区的容器查询突破样式);Chromium 默认让可滚动包裹层可键盘聚焦([决策记录](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md))。`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、响应式图片、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。 +`MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM 与 `$…$`、`$$…$$`、`\(…\)` 和 `\[…\]` TeX 公式,公式由 KaTeX 排版并禁用受信任命令;块级同一行 `$$…$$` 是显示公式并支持 `\tag{}`。一个小范围的 micromark 扩展允许由星号标记、以标点结尾的粗体在紧邻的 CJK 文本前闭合,以适应 CJK 文本通常省略 CommonMark 所要求空格的写法;单星号强调、紧邻非 CJK 文本的情况、转义、代码与数学公式仍沿用上游解析行为。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并在不发送 referrer 的情况下渲染采用绝对 HTTP(S) URL 的图片;相对路径、绝对本地路径、`file:` URL 与不受支持的 scheme 会保留其 alt 文本。完整内容为绝对 HTTP(S) URL 的行内代码会保留代码样式,并获得同样安全的外部链接;命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。可选的 `fileMentions` 解析器让持有该组件的视图为命名真实文件的行内代码添加可点击入口:token 保留代码样式,并获得一个连接到解析所得 opener 的按钮,按钮带有解析器提供的无障碍标签和以完整路径为值的 `title`。渲染器绝不猜测哪些内容像路径:未解析的 token 保持不可交互;文件提及仅应用于已定稿的渲染(流式缓存不得固化可能过期的 handler);锚点内的 token 也保持不可交互,因为按钮不能嵌套其中。回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复([机制与 DOM 一致性约定](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md))。表格按列数决定尺寸(对齐 deepsuite chat):不足四列——或位于 blockquote 内——的表格填满所在列,单元格文本换行收缩至最小可读列宽;四列及以上的表格保持自然宽度、在包裹层内横向滚动,并携带稳定的 `md-table-wide` 类,供宿主布局把包裹层加宽到所在列之外(`dsh-client-ui-conversation` 中聊天转录区的容器查询突破样式);宽表的横向滚动条在悬停或键盘聚焦(包裹层带 `tabindex="0"`)时才出现、不再常驻([决策记录](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md))。`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、响应式图片、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。 ## 终端输出 diff --git a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css index 969cf41342..f8d45cd4f6 100644 --- a/packages/client/ui-primitives/src/markdown/MarkdownText.module.css +++ b/packages/client/ui-primitives/src/markdown/MarkdownText.module.css @@ -177,6 +177,26 @@ overscroll-behavior-x: contain; } +/* Wide tables reveal their horizontal bar on hover (or keyboard focus) + instead of keeping it painted. Chromium never repaints state-conditioned + scrollbar STYLES (neither hover-conditioned `::-webkit-scrollbar*` rules + nor a :hover `scrollbar-color` change reaches the painted bar), so the + toggle is `overflow-x` itself — a layout change repaints reliably. The + resting padding matches the themed bar's height, so on an overflowing + table the appearing bar exactly replaces it and nothing below shifts. + Wheel and trackpad scrolling need the pointer over the table, which is + already the hover that re-enables `auto`. */ +.tableScroll:global(.md-table-wide) { + overflow-x: hidden; + padding-bottom: var(--dsh-scrollbar-width, 8px); +} + +.tableScroll:global(.md-table-wide):hover, +.tableScroll:global(.md-table-wide):focus-visible { + overflow-x: auto; + padding-bottom: 0; +} + /* Chromium keyboard-focuses scrollable containers by default; the ring uses the sheet's link focus color. */ .tableScroll:focus-visible { diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx index 12d73bb44b..de713858cf 100644 --- a/packages/client/ui-primitives/src/markdown/render.tsx +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -400,14 +400,22 @@ function renderTable(node: Md.Table, key: Key, context: MarkdownRenderContext): const align = node.align ?? null const [headRow, ...bodyRows] = node.children const columns = align === null ? headRow?.children.length ?? 0 : align.length - // Four or more columns read as a comparison matrix: the wrapper keeps the + // Four or more columns read as a comparison matrix: the block keeps the // table at natural width and exposes the stable `md-table-wide` hook so a // hosting layout (the chat transcript) can widen it past the message // column. Narrower tables — and any table inside a blockquote — fill the // column and wrap instead (deepsuite chat TableWrapper parity). const wide = columns >= 4 && context.inBlockquote !== true return ( -
+ // Wide tables rest with overflow-x hidden (the hover-revealed bar in + // MarkdownText.module.css), which drops Chromium's implicit scroller + // focusability — the explicit tabindex keeps them keyboard-reachable, + // and :focus-visible restores scrolling. +
{headRow !== undefined && {renderTableRow(headRow, 'th', align, 0, context)}} {bodyRows.length > 0 && ( diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.settled.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.settled.txt index 9b805440be..c105766edf 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.settled.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.settled.txt @@ -1,5 +1,5 @@
-
+
diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.streaming.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.streaming.txt index 9b805440be..c105766edf 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.streaming.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-wide-and-blockquote.streaming.txt @@ -1,5 +1,5 @@
-
+
diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.settled.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.settled.txt index 2ce936ddcc..99a45c4e77 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.settled.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.settled.txt @@ -1,5 +1,5 @@
-
+
diff --git a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.streaming.txt b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.streaming.txt index 2ce936ddcc..99a45c4e77 100644 --- a/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.streaming.txt +++ b/packages/client/ui-primitives/tests/fixtures/markdown-dom/table-with-alignment.streaming.txt @@ -1,5 +1,5 @@
-
+