Canonicalize mobile row disclosure affordances

This commit is contained in:
rcourtman
2026-08-24 16:36:29 +01:00
parent b67ac28f25
commit 32b721503c
15 changed files with 72 additions and 28 deletions
@@ -1187,9 +1187,12 @@ not a replacement status card, CTA band, or page-local nested card.
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.
opt in only while their compact row remains the disclosure target. Every
disclosure-button consumer must declare that row-target relationship
explicitly through the required `hideWhenRowTappableOnMobile` boolean, so a
new table cannot silently restore a redundant mobile chevron. Rows whose
click action does something other than expand details must pass `false` and
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
@@ -2191,6 +2191,11 @@ forking separate table-only presentation logic. That component now also
consumes the shared `frontend-modern/src/utils/resourceChangePresentation.ts`
label helper for canonical change kinds, source types, and adapter provenance
so the chip wording stays consistent without adding extra hot-path branching.
Host, PBS, and PMG summary rows also declare themselves as the compact touch
disclosure target through `SummaryRowActionButton`; the shared primitive hides
the redundant visible mobile chevron while preserving accessible disclosure
semantics, so provider sections do not pay a separate cell-width or tap-target
cost for an action already owned by the whole row.
The same infrastructure hot path now also depends on the shared
`frontend-modern/src/components/shared/ProgressBar.tsx` primitive for metric
fill rendering. Performance-sensitive metric bars may vary by value and color,
@@ -2187,9 +2187,12 @@ files.
`frontend-modern/src/components/shared/SummaryRowActionButton.tsx`, but
storage group headers should pin through the row itself and must not add a
separate scope/pinned pill button beside the disclosure chevron. Touch
users still must not inherit synthetic hover branches, and storage must
not keep a special trailing expand column once the shared leading action
contract exists.
pool and physical-disk rows own expansion through the whole row, so they
must explicitly suppress the redundant visible mobile chevron; group
headers whose row click owns summary focus rather than expansion must keep
their disclosure chevron visible. Touch users still must not inherit
synthetic hover branches, and storage must not keep a special trailing
expand column once the shared leading action contract exists.
Static subgroup header emphasis for storage group rows and recovery history
day headers must also route through
`frontend-modern/src/components/shared/groupedTableRowPresentation.ts` and
@@ -1585,7 +1585,11 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and
`frontend-modern/src/components/shared/SummaryRowActionButton.tsx`, so the
unified-resource table does not fork mouse-only hover logic, focusable-row
button shims, touch-hostile synthetic hover, or provider-specific control
handling across host, PBS, and PMG sections.
handling across host, PBS, and PMG sections. Those three resource rows are
themselves the compact touch disclosure target and must explicitly enable
the shared mobile-chevron suppression; their accessible row/button state
remains available without spending a visible summary-cell slot on a
duplicate control.
15. Keep infrastructure search aligned with the governed display label. Shared
infrastructure filtering through
`frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts`
+34 -18
View File
@@ -1,25 +1,39 @@
{
"version": 1,
"base_sha": "714b26b69f8a39d906b041dbf1b64d5d623632c1",
"verified_at": "2026-08-24T15:30:00Z",
"base_sha": "b67ac28f25434cb9e6e393a0787e72d915ba479a",
"verified_at": "2026-08-24T15:34:13Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/TooltipPortal.tsx",
"frontend-modern/src/components/shared/hoverCapability.ts",
"frontend-modern/src/components/shared/useTooltipState.ts",
"frontend-modern/src/hooks/useTooltip.ts"
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx",
"frontend-modern/src/components/Storage/DiskList.tsx",
"frontend-modern/src/components/Storage/StorageGroupRow.tsx",
"frontend-modern/src/components/Storage/StoragePoolRow.tsx",
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx"
],
"content_sha256": {
"frontend-modern/src/components/shared/TooltipPortal.tsx": "dafea8136a75491438a9ab45de348a4307ada67799796f38e3e26087339ef445",
"frontend-modern/src/components/shared/hoverCapability.ts": "bae00548234580fe72e4fe93833c57ff237d7666d2ada00a0f673c676f9c39dc",
"frontend-modern/src/components/shared/useTooltipState.ts": "4959c6840e3c062fa31c2a0a7391f5c0314725ac8ef48deeed37551f9d48194c",
"frontend-modern/src/hooks/useTooltip.ts": "fa13e21a922954466a9cb09abaff9cd04b3caf0c43dbb025829e8bf251f8065f"
"frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx": "78588725f625e58325c2ddca6133d3e0b0d46a18f9d19e8946f7c0181c094442",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx": "15031095e5910c02bd47038213974de4b3d2aef53b1ff6cd030fee381a98fe91",
"frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx": "d9a5a6f209b97a02dfa40100f783d91e8202b0efc33af73415e05689251f6ec6",
"frontend-modern/src/components/Storage/DiskList.tsx": "5d600b9bfa8e34ee9ed13037cdbe87b931c10e6378440d41bf1d1ba68cab9526",
"frontend-modern/src/components/Storage/StorageGroupRow.tsx": "5ce92da0c45b4146c51eb52786adb245978b5f6b4adc95b40d2294d949720e99",
"frontend-modern/src/components/Storage/StoragePoolRow.tsx": "f389fef94c53a74cf69d6f165ae01f71f02a1e0143dc68c1318033aebe0001a4",
"frontend-modern/src/components/shared/SummaryRowActionButton.tsx": "dbd6a62b6dbf11cabfaf01bc7240ab3ba3a8230cef1ee20bc2f0bd8c19e4bb14"
},
"routes": ["/proxmox"],
"routes": [
"/proxmox/overview",
"/proxmox/storage",
"/docker/overview",
"/kubernetes/overview",
"/truenas/overview",
"/vmware/health",
"/standalone/machines"
],
"viewports": [
{
"width": 1280,
"height": 720
"height": 800
},
{
"width": 390,
@@ -27,13 +41,15 @@
}
],
"states": [
"Proxmox workload table with a fine hover-capable desktop pointer",
"Proxmox workload table with touch emulation reporting a coarse primary pointer",
"expanded artifact-cache-310 inline guest drawer on the narrow viewport"
"collapsed storage-pool and physical-disk rows at narrow width without visible disclosure chevrons",
"expanded and collapsed storage detail rows activated from the whole summary row",
"provider overview rows across Proxmox, Docker, Kubernetes, TrueNAS, vSphere, and standalone machines with accessible but visually suppressed mobile disclosure buttons",
"desktop storage rows retaining their explicit disclosure affordance"
],
"interactions": [
"tapped the artifact-cache-310 workload row at 390x844 and confirmed the inline guest drawer opened without a portal or singleton tooltip",
"hovered a workload CPU metric at 1280x720 and confirmed the CPU Details tooltip remained available for a fine pointer",
"confirmed the touch and desktop media capability queries selected the intended tooltip behavior before restoring the browser viewport"
"opened and closed a Proxmox storage-pool detail from the whole row at 390px",
"switched from Storage to Physical Disks and confirmed its whole-row disclosure buttons are visually clipped at 390px",
"audited seven loaded platform routes and found zero visible Expand or Collapse disclosure buttons at 390px while retaining 232 accessible controls",
"inspected the Proxmox storage table at 1280px and confirmed desktop disclosure layout and row density remain intact"
]
}
@@ -408,6 +408,7 @@ export const UnifiedResourceHostTableCard: Component<UnifiedResourceHostTableCar
subjectLabel={displayName()}
expanded={isExpanded()}
controlsId={detailControlsId()}
hideWhenRowTappableOnMobile
onAction={() => table.toggleExpand(resource.id)}
onPreviewClear={() => tableProps.onHoverChange?.(null)}
/>
@@ -184,6 +184,7 @@ export const UnifiedResourcePBSTableSection: Component<UnifiedResourcePBSTableSe
subjectLabel={displayName()}
expanded={isExpanded()}
controlsId={detailControlsId()}
hideWhenRowTappableOnMobile
onAction={() => table.toggleExpand(resource.id)}
onPreviewClear={() => tableProps.onHoverChange?.(null)}
/>
@@ -198,6 +198,7 @@ export const UnifiedResourcePMGTableSection: Component<UnifiedResourcePMGTableSe
subjectLabel={displayName()}
expanded={isExpanded()}
controlsId={detailControlsId()}
hideWhenRowTappableOnMobile
onAction={() => table.toggleExpand(resource.id)}
onPreviewClear={() => tableProps.onHoverChange?.(null)}
/>
@@ -821,6 +821,7 @@ describe('UnifiedResourceTable performance contract', () => {
expect(source).toContain('data-summary-row-active');
expect(source).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(source).toContain('SummaryRowActionButton');
expect(source).toContain('hideWhenRowTappableOnMobile');
expect(source).not.toContain('bg-blue-50 dark:bg-blue-900 ring-1 ring-blue-300');
expect(source).not.toContain('bg-blue-100 dark:bg-blue-800');
}
@@ -361,6 +361,7 @@ export const DiskList: Component<DiskListProps> = (props) => {
subjectLabel={data.model || 'disk'}
expanded={isSelected()}
controlsId={detailControlsId}
hideWhenRowTappableOnMobile
onAction={() => model.toggleSelectedDisk(disk)}
onPreviewClear={() => props.onHoverChange?.(null)}
/>
@@ -53,6 +53,7 @@ export const StorageGroupRow: Component<StorageGroupRowProps> = (props) => {
kind="disclosure"
subjectLabel={row().label}
expanded={props.expanded}
hideWhenRowTappableOnMobile={false}
onAction={props.onToggle}
onPreviewClear={() => props.onHoverChange?.(null)}
class="inline-flex items-center justify-center"
@@ -100,6 +100,7 @@ export const StoragePoolRow: Component<StoragePoolRowProps> = (props) => {
subjectLabel={props.record.name}
expanded={props.expanded}
controlsId={detailControlsId()}
hideWhenRowTappableOnMobile
onAction={props.onToggleExpand}
onPreviewClear={() => props.onHoverChange?.(null)}
/>
@@ -1555,7 +1555,7 @@ 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('hideWhenRowTappableOnMobile: boolean');
expect(summaryRowActionButtonSource).toContain('sm:not-sr-only');
expect(platformResourceDetailTableRowSource).toContain('PlatformResourceDetailToggleButton');
expect(platformResourceDetailTableRowSource).toContain('SummaryRowActionButton');
@@ -1563,7 +1563,6 @@ describe('shared primitive guardrails', () => {
for (const source of [
guestRowSource,
storageGroupRowSource,
storagePoolRowSource,
diskListSource,
unifiedResourceHostTableCardSource,
@@ -1572,8 +1571,13 @@ describe('shared primitive guardrails', () => {
]) {
expect(source).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(source).toContain('SummaryRowActionButton');
expect(source).toContain('hideWhenRowTappableOnMobile');
}
expect(storageGroupRowSource).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(storageGroupRowSource).toContain('SummaryRowActionButton');
expect(storageGroupRowSource).toContain('hideWhenRowTappableOnMobile={false}');
expect(workloadPanelSource).toContain('createSummaryInteractiveRowPreviewHandlers');
expect(workloadPanelSource).not.toContain('kind="scope"');
expect(storageGroupRowSource).not.toContain('kind="scope"');
@@ -11,7 +11,7 @@ type SummaryRowActionButtonProps =
subjectLabel: string;
controlsId?: string;
class?: string;
hideWhenRowTappableOnMobile?: boolean;
hideWhenRowTappableOnMobile: boolean;
onAction: () => void;
onPreviewClear?: () => void;
}
@@ -11,6 +11,7 @@ describe('SummaryRowActionButton', () => {
subjectLabel="alpha"
expanded={true}
controlsId="summary-row-detail-alpha"
hideWhenRowTappableOnMobile={false}
onAction={onAction}
/>
));
@@ -78,6 +79,7 @@ describe('SummaryRowActionButton', () => {
subjectLabel="alpha"
expanded={false}
controlsId="summary-row-detail-alpha"
hideWhenRowTappableOnMobile={false}
onAction={onAction}
/>
));