Canonicalize mobile row disclosures

This commit is contained in:
rcourtman
2026-08-24 11:09:53 +01:00
parent d50f5c664e
commit a585eab712
8 changed files with 58 additions and 19 deletions
@@ -1167,6 +1167,14 @@ not a replacement status card, CTA band, or page-local nested card.
disclosure; a non-expandable identity cell must otherwise expose its
complete value without requiring hover. Default data rows stay single-line
so scan density and row rhythm are not traded for automatic text wrapping.
When a platform table row itself owns that keyboard- and touch-operable
disclosure, `SummaryRowActionButton` removes the redundant visible chevron
below the shared mobile breakpoint while preserving the button for screen
readers and revealing it on keyboard focus. `PlatformResourceDetailToggleButton`
applies that policy to provider-native platform tables, and workload rows
opt in only while their compact row remains the disclosure target. Rows
whose click action does something other than expand details must keep a
visible disclosure control.
They must not rely on a global rule that reveals
every hidden column: phone priority remains source-specific because state, capacity,
freshness, and recovery posture do not carry equal value for every resource
+21 -17
View File
@@ -1,15 +1,19 @@
{
"version": 1,
"base_sha": "791e29ad8d4cc991837ac43d0bf7c7a42d5fa54a",
"verified_at": "2026-08-24T09:50:53Z",
"base_sha": "d50f5c664e74e3a4d6a00abf76ef1e2c554e79a6",
"verified_at": "2026-08-24T10:04:07Z",
"result": "passed",
"changed_paths": ["frontend-modern/src/components/Workloads/GuestRow.tsx"],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestRow.tsx": "64776e4a32d5361a2ec541344178e9e7d1e401a3d09086e68ad4f923240bf715"
},
"routes": [
"/proxmox"
"changed_paths": [
"frontend-modern/src/components/Workloads/GuestRow.tsx",
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx",
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Workloads/GuestRow.tsx": "f8927883b484789cae4ca999845a77a7409aed800656d931be621a1f4d996ca8",
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx": "626741b5b725d19dd594a4c071888170d74b0e8352513cc2aa8557c86c986c88",
"frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx": "7907a58eeace6c2648f1e92e63a27bfd82fe305347e0100b76b59ab0e367ce61"
},
"routes": ["/proxmox"],
"viewports": [
{
"width": 390,
@@ -21,16 +25,16 @@
}
],
"states": [
"managed hot-dev mock estate with 50 nodes and 929 guests",
"phone Workloads table filtered to one guest with the compact header and collapsed row visible",
"phone Workloads table with the guest detail row expanded",
"desktop Workloads table with the same filtered guest"
"managed hot-dev mock estate with the canonical Proxmox Nodes and Guests tables visible",
"phone Nodes and Guests tables with their collapsed rows visible",
"phone Nodes and Guests tables after row-owned detail expansion",
"desktop Nodes and Guests tables after the responsive breakpoint transition"
],
"interactions": [
"confirmed the visible disclosure control is removed from compact rows while its screen-reader and keyboard-focus path remains available",
"confirmed the Type sort header renders without clipping at 390x844 and the availability column receives a larger compact-layout share when present",
"tapped the workload name to expand the filtered phone row and confirmed the accessible disclosure changes to Collapse",
"confirmed the expanded state survives the transition from 390x844 to 1440x900 and the desktop chevron remains visible",
"restored the browser viewport and real-node runtime after verification"
"confirmed provider node and workload disclosure controls remain in the accessibility tree while their redundant phone chevrons are visually removed",
"tapped the Analytics A node row and the artifact-cache-310 guest row and confirmed both detail regions expanded",
"confirmed both disclosure controls become visible again at the 1440x900 desktop breakpoint",
"confirmed Type, ID, CPU, Mem, Disk, and Age remain separated in the compact Workloads header",
"restored the browser viewport after verification"
]
}
@@ -221,7 +221,7 @@ export function GuestRow(props: GuestRowProps) {
subjectLabel={props.guest.name}
expanded={props.isExpanded === true}
controlsId={detailControlsId()}
class={usesCompactTableLayout() ? 'sr-only focus:not-sr-only' : undefined}
hideWhenRowTappableOnMobile={usesCompactTableLayout()}
onAction={() => props.onClick?.()}
onPreviewClear={() => props.onHoverChange?.(null)}
/>
@@ -924,6 +924,7 @@ describe('GuestRow', () => {
expect(disclosureButton.className).toContain('sr-only');
expect(disclosureButton.className).toContain('focus:not-sr-only');
expect(disclosureButton.className).toContain('sm:not-sr-only');
});
it('keeps the visible disclosure control in desktop layouts', () => {
@@ -1554,8 +1554,11 @@ describe('shared primitive guardrails', () => {
expect(summaryRowActionButtonSource).toContain('aria-expanded');
expect(summaryRowActionButtonSource).toContain('aria-pressed');
expect(summaryRowActionButtonSource).toContain('data-row-action="true"');
expect(summaryRowActionButtonSource).toContain('hideWhenRowTappableOnMobile');
expect(summaryRowActionButtonSource).toContain('sm:not-sr-only');
expect(platformResourceDetailTableRowSource).toContain('PlatformResourceDetailToggleButton');
expect(platformResourceDetailTableRowSource).toContain('SummaryRowActionButton');
expect(platformResourceDetailTableRowSource).toContain('hideWhenRowTappableOnMobile');
for (const source of [
guestRowSource,
@@ -11,6 +11,7 @@ type SummaryRowActionButtonProps =
subjectLabel: string;
controlsId?: string;
class?: string;
hideWhenRowTappableOnMobile?: boolean;
onAction: () => void;
onPreviewClear?: () => void;
}
@@ -46,12 +47,16 @@ export const SummaryRowActionButton: Component<SummaryRowActionButtonProps> = (p
scopeProps()?.pressed
? `Unpin summary scope for ${props.subjectLabel}`
: `Pin summary scope for ${props.subjectLabel}`;
const mobileRowTargetClass = () =>
disclosureProps()?.hideWhenRowTappableOnMobile
? 'sr-only focus:not-sr-only sm:not-sr-only'
: '';
return (
<button
type="button"
data-row-action="true"
class={`${props.kind === 'disclosure' ? DISCLOSURE_BUTTON_CLASS : SCOPE_BUTTON_CLASS} ${props.class ?? ''}`.trim()}
class={`${props.kind === 'disclosure' ? DISCLOSURE_BUTTON_CLASS : SCOPE_BUTTON_CLASS} ${mobileRowTargetClass()} ${props.class ?? ''}`.trim()}
aria-label={props.kind === 'disclosure' ? disclosureLabel() : scopeLabel()}
aria-expanded={disclosureProps()?.expanded}
aria-controls={disclosureProps()?.controlsId}
@@ -86,4 +86,21 @@ describe('SummaryRowActionButton', () => {
fireEvent.keyDown(button, { key: 'Space', code: 'Space' });
expect(onAction).toHaveBeenCalledTimes(1);
});
it('removes redundant mobile chrome when the whole row is tappable', () => {
render(() => (
<SummaryRowActionButton
kind="disclosure"
subjectLabel="alpha"
expanded={false}
hideWhenRowTappableOnMobile
onAction={vi.fn()}
/>
));
const button = screen.getByRole('button', { name: 'Expand alpha' });
expect(button).toHaveClass('sr-only');
expect(button).toHaveClass('focus:not-sr-only');
expect(button).toHaveClass('sm:not-sr-only');
});
});
@@ -39,6 +39,7 @@ export const PlatformResourceDetailToggleButton: Component<{
subjectLabel={`details for ${props.resourceLabel}`}
controlsId={props.controlsId}
class={props.class}
hideWhenRowTappableOnMobile
onAction={props.onToggle}
/>
);