Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions src/EventLogExpert.UI/Database/DatabaseEntryRow.razor
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<div class="db-entry-row @(_isMouseRevealed ? "db-entry-row--revealed" : null)"
@onmouseleave="HandleRowMouseLeave">
<div class="db-entry-row @(IsSelected ? "db-entry-row--selected" : null)">
<Checkbox AriaLabel="@($"Select {Entry.FileName}")"
Value="IsSelected"
ValueChanged="@(_ => OnSelectionToggle.InvokeAsync())" />

<div class="db-entry-row-content">
<div class="db-entry-info">
<button class="db-entry-name" id="@_nameButtonId" @onclick="HandleNameClick" @ref="_nameButtonRef" type="button">@Entry.FileName</button>
<button class="db-entry-name" id="@_nameButtonId" @ref="_nameButtonRef" type="button">@Entry.FileName</button>
@if (ShouldShowBadge)
{
<span class="db-entry-badge" data-badge="@BadgeKind">@BadgeLabel</span>
Expand Down Expand Up @@ -111,14 +114,14 @@
</button>
break;
}

<button aria-label="@($"Remove database {Entry.FileName}")"
class="button button-red db-entry-remove-btn"
@onclick="OnRemoveClick"
@ref="_removeButtonRef"
type="button">
<i aria-hidden="true" class="bi bi-trash"></i>
</button>
</div>
</div>

<button aria-disabled="@(IsRemoveBlocked ? "true" : null)"
aria-label="@($"Remove database {Entry.FileName}")"
class="button button-red db-entry-remove-btn"
@onclick="OnRemoveClick"
type="button">
<i aria-hidden="true" class="bi bi-trash"></i>
</button>
</div>
20 changes: 8 additions & 12 deletions src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public sealed partial class DatabaseEntryRow : ComponentBase
private readonly string _nameButtonId = $"db-row-{Guid.NewGuid():N}-name";
private readonly string _pendingStatusId = $"db-row-{Guid.NewGuid():N}-pending";

private bool _isMouseRevealed;
private ElementReference _nameButtonRef;
private ElementReference _removeButtonRef;
private bool _shouldFocusNameAfterRender;

private enum ActionKind
Expand All @@ -37,6 +37,8 @@ private enum ActionKind

[Parameter] public bool IsClassificationPending { get; set; }

[Parameter] public bool IsSelected { get; set; }

[Parameter] public bool IsTogglePending { get; set; }

[Parameter] public bool IsUpgradeBlocked { get; set; }
Expand All @@ -49,6 +51,8 @@ private enum ActionKind

[Parameter] public EventCallback OnRetryClassification { get; set; }

[Parameter] public EventCallback OnSelectionToggle { get; set; }

[Parameter] public EventCallback OnToggle { get; set; }

[Parameter] public EventCallback OnUpgrade { get; set; }
Expand All @@ -59,8 +63,6 @@ private enum ActionKind

private string BadgeLabel => DatabaseStatusLabels.GetRowBadgeLabel(Entry);

private bool IsRemoveBlocked => IsUpgrading || UpgradeProgress is not null;

private bool IsRestoreBlocked => IsUpgradeBlocked || IsUpgrading || UpgradeProgress is not null;

private ActionKind PrimaryAction
Expand Down Expand Up @@ -96,6 +98,8 @@ UpgradeProgress is null &&

[Inject] private ITraceLogger TraceLogger { get; init; } = null!;

public ValueTask FocusRemoveButtonAsync() => _removeButtonRef.FocusAsync(preventScroll: true);

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_shouldFocusNameAfterRender) { return; }
Expand All @@ -115,10 +119,6 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
_ => "Upgrading"
};

private void HandleNameClick() => _isMouseRevealed = true;

private void HandleRowMouseLeave() => _isMouseRevealed = false;

private void OnCancelClick()
{
_shouldFocusNameAfterRender = true;
Expand All @@ -132,9 +132,5 @@ private void OnCancelClick()
}
}

private async Task OnRemoveClick()
{
if (IsRemoveBlocked) { return; }
await OnRemove.InvokeAsync();
}
private async Task OnRemoveClick() => await OnRemove.InvokeAsync();
}
78 changes: 17 additions & 61 deletions src/EventLogExpert.UI/Database/DatabaseEntryRow.razor.css
Original file line number Diff line number Diff line change
@@ -1,55 +1,35 @@
.db-entry-row {
--db-entry-trash-reveal: 2.5rem;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: .5rem;

position: relative;
overflow: hidden;
padding: .25rem .5rem;
background-color: var(--background-dark);

border-left: 3px solid transparent;

transition: background-color 180ms ease, border-left-color 180ms ease;
}

.db-entry-row-content {
position: relative;
.db-entry-row--selected {
border-left-color: var(--clr-lightblue);
}

.db-entry-row-content {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 1rem;

/* DOM order is now name-info → actions → trash-remove (last). The trash button is a
LATER sibling than .db-entry-row-content but must paint UNDER it at rest. z-index:1
promotes content into painting group 7 above trash's group 6 (where z-index:0/auto
siblings paint in tree order). Without this, the later-in-DOM trash would obscure
the leftmost 2.5rem of the row at rest. Background fill remains required to cover
the absolutely-positioned trash strip underneath; z-index alone wouldn't be enough
if the content were transparent. */
padding: .25rem .5rem .25rem 0;
background-color: var(--background-dark);
z-index: 1;

transition: transform 200ms ease;
}

/* Reveal logic:
- Mouse: clicking the name button sets the .db-entry-row--revealed flag in
code-behind, which keeps the slide open until the cursor leaves the row
(@onmouseleave clears the flag). Re-entering the row without re-clicking
does NOT re-open the slide, even if the name button still has DOM focus.
- Keyboard: :focus-visible on the name button (matched only when focus
came from a key press, not a mouse click) opens the slide independently
of hover/state, so keyboard users can see what's there.
- Trash :focus keeps the strip open during the click (mouse focus or
keyboard focus), so the click reliably lands. */
.db-entry-row.db-entry-row--revealed .db-entry-row-content,
.db-entry-row:has(.db-entry-name:focus-visible) .db-entry-row-content,
.db-entry-row:has(.db-entry-remove-btn:focus) .db-entry-row-content {
transform: translateX(var(--db-entry-trash-reveal));
padding: .25rem 0;
}

.db-entry-info {
display: flex;
align-items: center;
gap: .5rem;
min-width: 0;

padding-left: .5rem;
}

.db-entry-name {
Expand Down Expand Up @@ -155,34 +135,14 @@
}

.db-entry-remove-btn {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: var(--db-entry-trash-reveal);

flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;

/* Recessed-well treatment: darker fill plus a soft inner shadow makes the
exposed trash strip read as a depression cut into the row, rather than
a button floating on top. */
background-color: var(--surface-inset);
box-shadow: inset 0 0 .35rem rgba(0, 0, 0, .55);
}

@media (hover: none) {
.db-entry-row-content {
transform: translateX(var(--db-entry-trash-reveal));
}
}

@media (prefers-reduced-motion: reduce) {
.db-entry-row-content {
transition: none;
}

.db-entry-row,
.db-entry-upgrading,
.db-entry-upgrading-text,
.db-entry-cancel-btn {
Expand Down Expand Up @@ -211,10 +171,6 @@
border-radius: 4px;
}

.db-entry-actions--pending ::deep .option-select {
box-shadow: 0 0 0 1px var(--clr-lightblue);
}


.visually-hidden {
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
</div>
}

<span aria-atomic="true" aria-live="polite" class="visually-hidden" role="status">@_selectionAnnouncement</span>

<div class="manage-databases-import-row">
<button class="button"
id="manage-import-button"
@onclick="ImportDatabase"
@ref="_importButtonRef"
type="button">
<i aria-hidden="true" class="bi bi-plus-circle"></i> Import database&hellip;
</button>
Expand All @@ -26,15 +29,18 @@
<DatabaseEntryRow EffectiveEnabled="GetEffectiveEnabled(entry)"
Entry="@entry"
IsClassificationPending="@IsClassificationPending"
IsSelected="@_selectedForRemoval.Contains(entry.FileName)"
IsTogglePending="@_pendingToggles.ContainsKey(entry.FileName)"
IsUpgradeBlocked="@(IsUpgradeBlocked && !Coordinator.IsUpgradeInFlight(entry.FileName))"
IsUpgrading="@Coordinator.IsUpgradeInFlight(entry.FileName)"
@key="entry.FileName"
OnRemove="@(() => RemoveDatabase(entry))"
OnRestoreFromBackup="@(() => RestoreFromBackup(entry))"
OnRetryClassification="@(() => RetryClassification(entry))"
OnSelectionToggle="@(() => ToggleSelection(entry.FileName))"
OnToggle="@(() => ToggleDatabase(entry.FileName))"
OnUpgrade="@(() => UpgradeEntry(entry.FileName))"
@ref="_removeButtonRefs[entry.FileName]"
UpgradeProgress="@GetUpgradeProgressForEntry(entry)" />
}
}
Expand All @@ -44,6 +50,26 @@
}
</div>

@if (HasSelectedForRemoval)
{
<div aria-label="Bulk database actions" class="manage-databases-bulk-strip" role="group">
<span class="manage-databases-bulk-count">
@_selectedForRemoval.Count selected
</span>
<button class="button"
@onclick="ClearSelection"
type="button">
Clear
</button>
<button class="button button-red"
@onclick="OnBulkRemoveClickAsync"
@ref="_bulkRemoveButtonRef"
type="button">
Remove @_selectedForRemoval.Count
</button>
</div>
}

@if (HasPendingChanges)
{
<div aria-label="Pending database changes" class="manage-databases-save-strip" role="group">
Expand Down
Loading