Fill the table's visible width with phone drawers (#2703)

Phone and tablet table drawers capped their sticky content at
`calc(100vw - 3.5rem)`, a guess at the surrounding page chrome. On pages
with less chrome than that, every drawer stopped short of the table and
left an empty strip on the right: 27px at 390px, measured on the
Availability checks, Proxmox node and guest, Docker host and container,
Kubernetes node and Machines drawers. At tablet widths a table wider
than its shell got a drawer wider than the visible area (712px in
695px).

The table scroll shell already declares `container-type: inline-size`
(the responsive column priorities use it), so `InlineDetailTableRow` now
caps the default content shell at `100cqi`, the shell's own visible
width. Measured live: content equals the visible table width at 320,
390, 768 and 1000px, with no right gap; desktop keeps the static
uncapped layout. The availability drawer's hostname now fits instead of
truncating. Callers passing their own `contentClass` are unaffected.

Not changed here: `.table-fixed td { overflow: hidden }` makes the
sticky offset inert, so a sideways-scrolled table still moves its drawer
(same before and after). Long unwrapped identity chips that were already
clipped are clipped less now; wrapping them is a separate follow-up.

Proof: `InlineDetailTableRow.test.tsx` and the shared-primitive
guardrail pin the new cap and reject the old one; the guardrail already
pins the scroll shell's `container-type`. Browser receipt covers five
routes at 320 to 1440px, before and after.
This commit is contained in:
rcourtman
2026-10-07 21:42:50 +01:00
committed by GitHub
5 changed files with 71 additions and 5 deletions
@@ -5770,7 +5770,15 @@ the surface-alt detail row shell locally. The content shell must clip
horizontal paint below the large breakpoint without becoming a scroll
container, reset the parent table's `whitespace-nowrap` inheritance, and allow
its descendants to shrink, then restore visible overflow for the static
desktop layout. Long operator-state copy must wrap inside the shared row border
desktop layout. Below that breakpoint the default content shell is capped at
the table scroll shell's content-box inline size (`max-w-[100cqi]`; the shell
is a size container), never a viewport estimate of the page chrome, so a phone
drawer does not leave an empty strip beside its content (the retired
`100vw-3.5rem` cap left 27px at 390px) and is never wider than the table's
visible area. Callers that pass their own `contentClass` own their width.
Fixed-layout cells clip overflow, so the sticky offset is inert and horizontal
scrolling still moves the drawer with its cell.
Long operator-state copy must wrap inside the shared row border
instead of painting beneath adjacent controls or disappearing at the clip edge.
When focused detail content is removed, `InlineDetailTableRow` restores focus
to its current `aria-controls` disclosure with `preventScroll`; live refresh,
@@ -0,0 +1,52 @@
{
"version": 1,
"base_sha": "7eb32f318100fef045d63414fd63304c62acfdef",
"verified_at": "2026-10-07T16:55:00Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx"
],
"content_sha256": {
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx": "e5b27178cb3268132595943058849b3f619da22fcbd7916dfc301d4e852ef244"
},
"routes": [
"/standalone/availability",
"/standalone/machines",
"/proxmox",
"/docker",
"/kubernetes"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 1000,
"height": 800
},
{
"width": 768,
"height": 1024
},
{
"width": 390,
"height": 844
},
{
"width": 320,
"height": 700
}
],
"states": [
"Private stacks served by Vite from this tree (Playwright 1.56.1 headless Chromium): a non-mock stack with one HTTP availability check, then a mock estate (3 Proxmox nodes, 2 Docker hosts, 1 Kubernetes cluster, 2 generic hosts).",
"Before (origin/main class max-w-[calc(100vw-3.5rem)]): at 390px every opened drawer (availability check, Proxmox node, Proxmox guest, Docker host, Docker container, Kubernetes node, Machines agent) was 334px inside a 361px visible table, leaving a 27px empty strip; at 768px a table forced wider than its shell got a 712px drawer in a 695px visible area.",
"After (max-w-[100cqi]): drawer content equals the scroll shell's visible width at 320 (291px), 390 (361px), 768 (695px) and 1000 (916px) with no right gap, including the forced-wider table; 1440px keeps the static lg layout (max-width none, 1320px). The availability drawer's hostname now shows in full instead of truncating.",
"Long unwrapped identity chips (e.g. proxmox-lxc-docker:Production West:pve1:107) were already clipped at the content edge before (Kubernetes 20px, Docker 8px and 83px) and are clipped less after (7px, 70px); chip wrapping is a separate follow-up.",
"Sticky positioning is inert in both versions because .table-fixed td sets overflow hidden; not changed here. This is not an installed-build, physical-phone or release acceptance."
],
"interactions": [
"Opened each drawer by its disclosure button (row tap at phone width where the button is hidden), measured the scroll shell, detail cell, content shell and every descendant's right edge, and captured screenshots at 390px before and after.",
"Forced a 900px table inside its shell at 390px and 768px and scrolled it 300px sideways to compare drawer width against the visible area for both classes."
]
}
@@ -5,9 +5,12 @@ import { TableCell, TableRow, type TableRowProps } from './Table';
export const INLINE_DETAIL_TABLE_CELL_CLASS = 'p-0 border-b border-border bg-surface-alt';
// overflow-x-clip keeps over-wide content from painting outside the row's
// border below the lg breakpoint (issue #1622) without creating a scroll
// container the way overflow-hidden would.
// container the way overflow-hidden would. Below lg the sticky content is as
// wide as the table's visible area: 100cqi resolves against the table scroll
// shell (a size container), so a guess at the page chrome cannot leave an
// empty strip beside the drawer or push it past the visible edge.
export const INLINE_DETAIL_TABLE_CONTENT_CLASS =
'sticky left-0 min-w-0 max-w-[calc(100vw-3.5rem)] whitespace-normal overflow-x-clip px-2 py-3 sm:px-4 sm:py-4 lg:static lg:max-w-none lg:overflow-x-visible';
'sticky left-0 min-w-0 max-w-[100cqi] whitespace-normal overflow-x-clip px-2 py-3 sm:px-4 sm:py-4 lg:static lg:max-w-none lg:overflow-x-visible';
export interface InlineDetailTableRowProps extends TableRowProps {
cellId?: string;
@@ -8757,7 +8757,10 @@ describe('shared primitive guardrails', () => {
expect(summaryRowActionButtonSource).toContain('h-9 w-9');
expect(searchFieldSource).toContain('min-h-11');
expect(filterToolbarSource).toContain('min-h-11');
expect(inlineDetailTableRowSource).toContain('max-w-[calc(100vw-3.5rem)]');
// Phone and tablet drawers fill the table's visible width (the scroll
// shell is a size container), not a viewport guess at the page chrome.
expect(inlineDetailTableRowSource).toContain('max-w-[100cqi]');
expect(inlineDetailTableRowSource).not.toContain('100vw-3.5rem');
});
it('keeps shared settings panels dense without forking their desktop framing', () => {
@@ -39,7 +39,7 @@ describe('InlineDetailTableRow', () => {
expect(detail.parentElement).toHaveClass('left-0');
expect(detail.parentElement).toHaveClass('min-w-0');
expect(detail.parentElement).toHaveClass('whitespace-normal');
expect(detail.parentElement).toHaveClass('max-w-[calc(100vw-3.5rem)]');
expect(detail.parentElement).toHaveClass('max-w-[100cqi]');
expect(detail.parentElement).toHaveClass('lg:static');
expect(detail.parentElement).toHaveClass('lg:max-w-none');
});