Harden tables selection interactions

This commit is contained in:
2026-03-21 15:20:05 +01:00
parent fb6e5a4e86
commit 7a0ce00429
4 changed files with 69 additions and 4 deletions

View File

@@ -30,7 +30,7 @@ It is intentionally implementation-focused:
- Branch: `frontend/tables-overhaul` - Branch: `frontend/tables-overhaul`
- Last updated: `2026-03-21` - Last updated: `2026-03-21`
- Current focus: `Phase 3` - Current focus: `Phase 4`
- Document mode: living plan and progress log - Document mode: living plan and progress log
### Progress Log ### Progress Log
@@ -72,6 +72,7 @@ It is intentionally implementation-focused:
| 2026-03-21 | P3.9 | Completed | Pulled legend/help out of the always-on canvas component and turned it into an explicitly toggled secondary surface controlled from the context bar. | | 2026-03-21 | P3.9 | Completed | Pulled legend/help out of the always-on canvas component and turned it into an explicitly toggled secondary surface controlled from the context bar. |
| 2026-03-21 | P3.10 | Completed | Softened the default `Reference` mode by replacing repeated curation wording in resting cells with subtle status indicators and by simplifying the top-level guidance copy. | | 2026-03-21 | P3.10 | Completed | Softened the default `Reference` mode by replacing repeated curation wording in resting cells with subtle status indicators and by simplifying the top-level guidance copy. |
| 2026-03-21 | P3.11 | Completed | Moved the live editor and curation entry points into the shared inspector content and removed the last remaining grid-owned action buttons. | | 2026-03-21 | P3.11 | Completed | Moved the live editor and curation entry points into the shared inspector content and removed the last remaining grid-owned action buttons. |
| 2026-03-21 | P3.12 | Completed | Added keyboard-selectable cells, visible focus treatment, and selection normalization so changing filters or modes cannot leave the inspector pointing at hidden cells. |
### Lessons Learned ### Lessons Learned
@@ -467,7 +468,7 @@ Build the shared interaction infrastructure needed by multiple destinations befo
### Status ### Status
`In progress` `Completed`
### Task Progress ### Task Progress
@@ -484,7 +485,7 @@ Build the shared interaction infrastructure needed by multiple destinations befo
| `P3.9` | Completed | Legend/help is now on-demand and controlled from the context bar instead of always rendering below the canvas. | | `P3.9` | Completed | Legend/help is now on-demand and controlled from the context bar instead of always rendering below the canvas. |
| `P3.10` | Completed | Default `Reference` mode now uses quieter status indicators and calmer guidance copy so the page reads less like a maintenance surface. | | `P3.10` | Completed | Default `Reference` mode now uses quieter status indicators and calmer guidance copy so the page reads less like a maintenance surface. |
| `P3.11` | Completed | Full editor and curation entry points now live in the shared inspector content instead of the grid itself. | | `P3.11` | Completed | Full editor and curation entry points now live in the shared inspector content instead of the grid itself. |
| `P3.12` | Pending | Normalize click/tap/keyboard selection and close the phase with a hardening pass. | | `P3.12` | Completed | Cell selection now works by click, tap, and keyboard, and view changes clear stale selection instead of leaving the inspector out of sync with the visible grid. |
### Goal ### Goal

View File

@@ -677,18 +677,21 @@
private Task UpdateReferenceModeAsync(string mode) private Task UpdateReferenceModeAsync(string mode)
{ {
referenceMode = NormalizeMode(mode); referenceMode = NormalizeMode(mode);
NormalizeSelectedCellForCurrentView();
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task UpdateSelectedGroupAsync(string groupKey) private Task UpdateSelectedGroupAsync(string groupKey)
{ {
selectedGroupKey = NormalizeOptionalFilter(groupKey); selectedGroupKey = NormalizeOptionalFilter(groupKey);
NormalizeSelectedCellForCurrentView();
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task UpdateSelectedColumnAsync(string columnKey) private Task UpdateSelectedColumnAsync(string columnKey)
{ {
selectedColumnKey = NormalizeOptionalFilter(columnKey); selectedColumnKey = NormalizeOptionalFilter(columnKey);
NormalizeSelectedCellForCurrentView();
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -768,6 +771,8 @@
{ {
selectedCell = null; selectedCell = null;
} }
NormalizeSelectedCellForCurrentView();
} }
private static string NormalizeMode(string? mode) => private static string NormalizeMode(string? mode) =>
@@ -796,4 +801,40 @@
var digitsOnly = new string(value.Where(char.IsDigit).ToArray()); var digitsOnly = new string(value.Where(char.IsDigit).ToArray());
return digitsOnly.Length == 0 ? string.Empty : digitsOnly; return digitsOnly.Length == 0 ? string.Empty : digitsOnly;
} }
private void NormalizeSelectedCellForCurrentView()
{
if (selectedCell is null || tableDetail is null)
{
return;
}
var cell = tableDetail.Cells.FirstOrDefault(item => item.ResultId == selectedCell.ResultId);
if (cell is null || !MatchesCurrentView(cell))
{
selectedCell = null;
}
}
private bool MatchesCurrentView(CriticalTableCellDetail cell)
{
if (!string.IsNullOrWhiteSpace(selectedGroupKey) &&
!string.Equals(cell.GroupKey, selectedGroupKey, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!string.IsNullOrWhiteSpace(selectedColumnKey) &&
!string.Equals(cell.ColumnKey, selectedColumnKey, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return referenceMode switch
{
TablesReferenceMode.NeedsCuration => !cell.IsCurated,
TablesReferenceMode.Curated => cell.IsCurated,
_ => true
};
}
} }

View File

@@ -33,7 +33,13 @@
@if (MatchesModeFilter(cell)) @if (MatchesModeFilter(cell))
{ {
<div class="@GetCellCssClass(cell, displayColumn.GroupKey)" @onclick="() => SelectCell(cell)"> <div
class="@GetCellCssClass(cell, displayColumn.GroupKey)"
role="button"
tabindex="0"
aria-pressed="@isSelectedCell"
@onclick="() => SelectCell(cell)"
@onkeydown="args => HandleCellKeyDown(args, cell)">
<div class="critical-table-cell-shell"> <div class="critical-table-cell-shell">
<div class="critical-table-cell-actions"> <div class="critical-table-cell-actions">
@if (string.Equals(CurrentMode, TablesReferenceMode.Reference, StringComparison.Ordinal)) @if (string.Equals(CurrentMode, TablesReferenceMode.Reference, StringComparison.Ordinal))
@@ -306,5 +312,17 @@
private bool IsSelectedCell(CriticalTableCellDetail cell) => private bool IsSelectedCell(CriticalTableCellDetail cell) =>
SelectedCell is not null && cell.ResultId == SelectedCell.ResultId; SelectedCell is not null && cell.ResultId == SelectedCell.ResultId;
private Task HandleCellKeyDown(KeyboardEventArgs args, CriticalTableCellDetail cell)
{
if (string.Equals(args.Key, "Enter", StringComparison.Ordinal) ||
string.Equals(args.Key, " ", StringComparison.Ordinal) ||
string.Equals(args.Key, "Spacebar", StringComparison.Ordinal))
{
return SelectCell(cell);
}
return Task.CompletedTask;
}
private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};"; private static string BuildColumnSpanStyle(int span) => $"grid-column: span {span};";
} }

View File

@@ -1643,6 +1643,11 @@ pre,
transition: box-shadow 0.16s ease, background-color 0.16s ease, transform 0.16s ease; transition: box-shadow 0.16s ease, background-color 0.16s ease, transform 0.16s ease;
} }
.critical-table-cell:focus-visible {
outline: 2px solid rgba(13, 148, 136, 0.45);
outline-offset: -2px;
}
.critical-table-cell.is-curated { .critical-table-cell.is-curated {
background: background:
linear-gradient(135deg, rgba(102, 138, 83, 0.16), transparent 34%), linear-gradient(135deg, rgba(102, 138, 83, 0.16), transparent 34%),