feat(usability): label booking actions and enlarge row targets (U09)
This commit is contained in:
@@ -63,3 +63,7 @@ U08: gemeinsame native Modaldialoge mit Fokus/Escape/Verlustschutz, semantische
|
||||
## U08 – Tastatur und Modaldialoge
|
||||
|
||||
Gemeinsame native dialog-Komponente für Erfassung, Bestätigungen, Konten, Namen, Übertrag, Jahr und Über. showModal sperrt den Hintergrund und hält den Tastaturfokus im Dialog; Fokus geht ins sinnvolle Feld und nach Schließen zum Auslöser zurück. Escape schließt unveränderte Dialoge; geänderte Eingaben bieten Weiterbearbeiten/Verwerfen. Während Speichern kein Schließen. Unsichere Bestätigungen fokussieren Abbrechen und verwenden keine implizite Formularbestätigung. Kontokarten sind semantische Links. Labels sind technisch verbunden. Toasts bleiben auch innerhalb nativer Modaldialoge zugänglich. Prüfung: 44/44 Debug-Tests, warnungsfreier Build; Computer Use beobachtete Fokus im Beschreibungsfeld, ausschließlich modalen Accessibility-Baum und Escape-Schließen. Kein vollständiger Screenreader-Nutzertest.
|
||||
|
||||
## U09 – Zeilenaktionen
|
||||
|
||||
Beschriftetes Aktionsmenü je Buchung, sichtbares Bearbeiten/Löschen und Wiederherstellen als Wort. Zugängliche Namen nennen Nummer und Beschreibung, Trefferbereiche mindestens 44 CSS-Pixel. Auf schmalen Fenstern wechseln Zeilen in beschriftete Karten, damit die Aktionsspalte nicht die Beschreibung verdrängt. Gerenderter Test prüft Zuordnung und Aktion.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<tr class="@GetRowClass()">
|
||||
<td>@Entry.DisplayId</td>
|
||||
<td>@Entry.Date.ToString("dd.MM.yyyy")</td>
|
||||
<td>
|
||||
<td data-label="Nummer">@Entry.DisplayId</td>
|
||||
<td data-label="Datum">@Entry.Date.ToString("dd.MM.yyyy")</td>
|
||||
<td data-label="Beschreibung" class="entry-description">
|
||||
@Entry.Title
|
||||
@if (Entry.IsTransfer && !string.IsNullOrEmpty(Entry.LinkedAccountName))
|
||||
{
|
||||
@@ -10,26 +10,27 @@
|
||||
</small>
|
||||
}
|
||||
</td>
|
||||
<td class="text-end @(Entry.Type == EntryType.Income ? "amount-positive" : "amount-negative")">
|
||||
<td data-label="Betrag" class="text-end @(Entry.Type == EntryType.Income ? "amount-positive" : "amount-negative")">
|
||||
@(Entry.Type == EntryType.Income ? "+" : "\u2212")@($"{Entry.Amount:N2} €")
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Aktionen" class="entry-actions">
|
||||
@if (Entry.IsDeleted)
|
||||
{
|
||||
<button class="btn btn-outline-success btn-sm" @onclick="() => OnRestore.InvokeAsync(Entry.Id)" title="Wiederherstellen">
|
||||
<i class="bi bi-arrow-counterclockwise"></i>
|
||||
<button type="button" class="btn btn-outline-success" @onclick="() => OnRestore.InvokeAsync(Entry.Id)" aria-label="@($"{Entry.DisplayId} {Entry.Title} wiederherstellen")">
|
||||
<i class="bi bi-arrow-counterclockwise" aria-hidden="true"></i> Wiederherstellen
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-outline-secondary btn-sm" @onclick="() => OnEdit.InvokeAsync(Entry.Id)" title="Bearbeiten">
|
||||
<i class="bi bi-pencil"></i>
|
||||
<details class="entry-action-menu">
|
||||
<summary aria-label="@($"Aktionen für {Entry.DisplayId} {Entry.Title}")">Aktionen</summary>
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="() => OnEdit.InvokeAsync(Entry.Id)" aria-label="@($"{Entry.DisplayId} {Entry.Title} bearbeiten")">
|
||||
<i class="bi bi-pencil" aria-hidden="true"></i> Bearbeiten
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" @onclick="() => OnDelete.InvokeAsync(Entry.Id)" title="Löschen">
|
||||
<i class="bi bi-trash"></i>
|
||||
<button type="button" class="btn btn-outline-danger" @onclick="() => OnDelete.InvokeAsync(Entry.Id)" aria-label="@($"{Entry.DisplayId} {Entry.Title} löschen")">
|
||||
<i class="bi bi-trash" aria-hidden="true"></i> Löschen
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive entry-list">
|
||||
<table class="table table-hover table-sm align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
@@ -6,7 +6,7 @@
|
||||
<th style="width: 120px;">Datum</th>
|
||||
<th>Bezeichnung</th>
|
||||
<th style="width: 140px;" class="text-end">Betrag</th>
|
||||
<th style="width: 80px;"></th>
|
||||
<th style="width: 11rem;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -641,3 +641,16 @@ fieldset { border: 0; margin: 0; padding: 0; }
|
||||
body:has(dialog[open]) .toast-stack:not(.modal-toasts) { display: none; }
|
||||
|
||||
.visually-hidden { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; }
|
||||
|
||||
.entry-actions button, .entry-action-menu summary { min-height: 44px; padding: .6rem; }
|
||||
.entry-action-menu summary { cursor: pointer; border: 1px solid var(--color-border); border-radius: .4rem; }
|
||||
.entry-action-menu button { display: block; width: 100%; margin-top: .5rem; }
|
||||
.entry-description { overflow-wrap: anywhere; }
|
||||
@media (max-width: 900px) {
|
||||
.entry-list thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
|
||||
.entry-list table, .entry-list tbody { display: block; width: 100%; }
|
||||
.entry-list tbody tr { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--color-border); border-radius: .5rem; padding: .6rem; margin-bottom: .75rem; }
|
||||
.entry-list td { display: block; border: 0; min-width: 0; padding: .3rem; text-align: left !important; }
|
||||
.entry-list td::before { content: attr(data-label); display: block; color: var(--color-text-muted); font-size: .85rem; }
|
||||
.entry-list .entry-description, .entry-list .entry-actions { grid-column: 1 / -1; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Bunit;
|
||||
using Duempelkas.App.Components.Accounts;
|
||||
using Duempelkas.App.Services.Models;
|
||||
using Duempelkas.Domain.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Duempelkas.Tests;
|
||||
|
||||
public class EntryPresentationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Actions_HaveVisibleTextAndBookingSpecificNames()
|
||||
{
|
||||
using var context = new BunitContext();
|
||||
var entry = new EntryDto(1, 1, "2026-001", EntryType.Expense, DateTime.Today, "Porto", 4.20m, false, false, null, null, null);
|
||||
var edited = 0;
|
||||
var table = context.Render<EntryTable>(p => p.Add(x => x.Entries, [entry]).Add(x => x.OnEditEntry, id => edited = id));
|
||||
Assert.Contains("2026-001 Porto", table.Find("summary").GetAttribute("aria-label"));
|
||||
var edit = table.FindAll("button").Single(b => b.TextContent.Contains("Bearbeiten"));
|
||||
Assert.Contains("Porto", edit.GetAttribute("aria-label"));
|
||||
edit.Click();
|
||||
Assert.Equal(1, edited);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user