From 7a0ce00429bad4caf326e22aacdcdd98d6481497 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sat, 21 Mar 2026 15:20:05 +0100 Subject: [PATCH] Harden tables selection interactions --- ...s_frontend_overhaul_implementation_plan.md | 7 ++-- .../Components/Pages/Tables.razor | 41 +++++++++++++++++++ .../Components/Tables/TablesCanvas.razor | 20 ++++++++- src/RolemasterDb.App/wwwroot/app.css | 5 +++ 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md index 7d76ba9..708f979 100644 --- a/docs/tables_frontend_overhaul_implementation_plan.md +++ b/docs/tables_frontend_overhaul_implementation_plan.md @@ -30,7 +30,7 @@ It is intentionally implementation-focused: - Branch: `frontend/tables-overhaul` - Last updated: `2026-03-21` -- Current focus: `Phase 3` +- Current focus: `Phase 4` - Document mode: living plan and 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.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.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 @@ -467,7 +468,7 @@ Build the shared interaction infrastructure needed by multiple destinations befo ### Status -`In progress` +`Completed` ### 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.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.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 diff --git a/src/RolemasterDb.App/Components/Pages/Tables.razor b/src/RolemasterDb.App/Components/Pages/Tables.razor index a594edd..a4d4df9 100644 --- a/src/RolemasterDb.App/Components/Pages/Tables.razor +++ b/src/RolemasterDb.App/Components/Pages/Tables.razor @@ -677,18 +677,21 @@ private Task UpdateReferenceModeAsync(string mode) { referenceMode = NormalizeMode(mode); + NormalizeSelectedCellForCurrentView(); return Task.CompletedTask; } private Task UpdateSelectedGroupAsync(string groupKey) { selectedGroupKey = NormalizeOptionalFilter(groupKey); + NormalizeSelectedCellForCurrentView(); return Task.CompletedTask; } private Task UpdateSelectedColumnAsync(string columnKey) { selectedColumnKey = NormalizeOptionalFilter(columnKey); + NormalizeSelectedCellForCurrentView(); return Task.CompletedTask; } @@ -768,6 +771,8 @@ { selectedCell = null; } + + NormalizeSelectedCellForCurrentView(); } private static string NormalizeMode(string? mode) => @@ -796,4 +801,40 @@ var digitsOnly = new string(value.Where(char.IsDigit).ToArray()); 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 + }; + } } diff --git a/src/RolemasterDb.App/Components/Tables/TablesCanvas.razor b/src/RolemasterDb.App/Components/Tables/TablesCanvas.razor index d61fb9d..620afc5 100644 --- a/src/RolemasterDb.App/Components/Tables/TablesCanvas.razor +++ b/src/RolemasterDb.App/Components/Tables/TablesCanvas.razor @@ -33,7 +33,13 @@ @if (MatchesModeFilter(cell)) { -
+
@if (string.Equals(CurrentMode, TablesReferenceMode.Reference, StringComparison.Ordinal)) @@ -306,5 +312,17 @@ private bool IsSelectedCell(CriticalTableCellDetail cell) => 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};"; } diff --git a/src/RolemasterDb.App/wwwroot/app.css b/src/RolemasterDb.App/wwwroot/app.css index 255a8a0..0840d0c 100644 --- a/src/RolemasterDb.App/wwwroot/app.css +++ b/src/RolemasterDb.App/wwwroot/app.css @@ -1643,6 +1643,11 @@ pre, 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 { background: linear-gradient(135deg, rgba(102, 138, 83, 0.16), transparent 34%),