fix(centralui): stabilize audit grid th nodes with @key; doc grid limitations

This commit is contained in:
Joseph Doherty
2026-05-21 06:33:20 -04:00
parent f1478c5a19
commit c503df4c4c
3 changed files with 31 additions and 0 deletions

View File

@@ -17,7 +17,13 @@
<tr>
@foreach (var col in OrderedColumns())
{
// @key keeps Blazor reusing one DOM node per column across
// re-renders (reorder/resize), so audit-grid.js binds drag
// listeners exactly once per <th> and never leaks them onto
// discarded nodes — the __auditGridCellBound guard relies on
// this node stability to be fully sound.
<th class="audit-grid-th"
@key="col.Key"
data-test="col-header-@col.Key"
data-col-key="@col.Key"
style="@ColumnWidthStyle(col.Key)">

View File

@@ -37,6 +37,16 @@ namespace ScadaLink.CentralUI.Components.Audit;
/// <see cref="PageSize"/> rows) — that's the conventional "we've reached the
/// end" signal for keyset paging without a count query.
/// </para>
///
/// <para>
/// <b>Accessibility.</b> Column resize and reorder are mouse/pointer-only —
/// they use a pointer-driven resize handle and native HTML5 drag-and-drop with
/// no keyboard equivalent and no ARIA for the reorder. This is a conscious
/// scope decision for an internal tool, not an oversight: only the column-
/// <i>customisation</i> gesture is mouse-only. The persisted layout itself
/// renders as plain HTML, so keyboard and assistive-technology users still get
/// a fully readable, navigable grid.
/// </para>
/// </summary>
public partial class AuditResultsGrid : IAsyncDisposable
{
@@ -99,6 +109,9 @@ public partial class AuditResultsGrid : IAsyncDisposable
/// <c>data-test</c> + the column-order parameter); the label is the user-facing
/// header text. Mirrors Component-AuditLog.md §10.
/// </summary>
// Label intentionally equals Key for every column today; the separate Label
// field is future-proofing for humanised headers (e.g. "Occurred (UTC)") —
// populating it is a deliberate later change, out of scope here.
private static readonly IReadOnlyList<(string Key, string Label)> AllColumns = new[]
{
("OccurredAtUtc", "OccurredAtUtc"),
@@ -251,6 +264,12 @@ public partial class AuditResultsGrid : IAsyncDisposable
// is idempotent — already-bound cells are skipped, and the .NET
// reference is refreshed — so a re-render after a reorder still leaves
// every header cell wired without leaking handlers.
//
// OnColumnResized/OnColumnReordered both call StateHasChanged(), which
// re-runs this method and calls init again. That repeat call is an
// intentional cheap no-op: the @key-stable <th> nodes plus the
// __auditGridCellBound guard mean init re-scans the header and rebinds
// nothing — so there is deliberately no gating logic here.
if (_selfRef is not null)
{
try