fix(activity-log): no spinner flash on instant filtering
- re-query keeps current rows visible instead of clearing + showing the full 'Loading' spinner - loading affordance is delayed ~180ms: instant responses show nothing; slow ones get a subtle dim (aria-busy) - full spinner reserved for the genuine first load; append (load-more) shows no indicator
This commit is contained in:
@@ -577,6 +577,16 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Subtle busy state while a slow re-query is in flight: the current rows stay
|
||||
visible (no spinner flash) but dim slightly and stop accepting clicks until
|
||||
the fresh results swap in. Only applied after a short delay, so instant
|
||||
filtering shows nothing. */
|
||||
.al-list-container.al-busy {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
/* ── Tabular-nums utility ────────────────────────────────────────────────── */
|
||||
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
|
||||
@@ -74,6 +74,13 @@ let _total = 0;
|
||||
let _expandedIds = new Set<string>();
|
||||
let _debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let _liveEventListener: ((e: Event) => void) | null = null;
|
||||
// Loading UX: `_showSpinner` gates the full-panel spinner so it only appears
|
||||
// after a short delay (slow requests), never flashing on instant filtering.
|
||||
// `_hasLoadedOnce` distinguishes the genuine first load (spinner immediately)
|
||||
// from re-queries (keep current rows, subtle delayed busy hint).
|
||||
let _loadingDelayTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let _showSpinner = false;
|
||||
let _hasLoadedOnce = false;
|
||||
|
||||
const _filters: ActiveFilters = {
|
||||
categories: [],
|
||||
@@ -353,7 +360,7 @@ function _renderFilterToolbar(): string {
|
||||
// ─── List and state rendering ────────────────────────────────
|
||||
|
||||
function _renderList(): string {
|
||||
if (_loading && _entries.length === 0) {
|
||||
if (_showSpinner && _entries.length === 0) {
|
||||
return `<div class="al-state al-loading" role="status" aria-live="polite">
|
||||
<div class="al-spinner"></div>
|
||||
<span>${escapeHtml(t('activity_log.loading'))}</span>
|
||||
@@ -496,15 +503,58 @@ function _updateListContainer(): void {
|
||||
|
||||
// ─── Data fetching ───────────────────────────────────────────
|
||||
|
||||
/** Surface a loading affordance only when a request is slow enough to notice. */
|
||||
function _showDelayedBusy(): void {
|
||||
if (!_loading) return;
|
||||
if (_entries.length === 0) {
|
||||
// Nothing to keep on screen — fall back to the full spinner.
|
||||
_showSpinner = true;
|
||||
_updateListContainer();
|
||||
} else {
|
||||
// Re-query of a populated list: keep the current rows, just dim them.
|
||||
const c = document.getElementById('al-list-container');
|
||||
c?.classList.add('al-busy');
|
||||
c?.setAttribute('aria-busy', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear all loading affordances (timer, spinner flag, busy dim). Idempotent. */
|
||||
function _clearBusy(): void {
|
||||
if (_loadingDelayTimer) {
|
||||
clearTimeout(_loadingDelayTimer);
|
||||
_loadingDelayTimer = null;
|
||||
}
|
||||
_showSpinner = false;
|
||||
const c = document.getElementById('al-list-container');
|
||||
c?.classList.remove('al-busy');
|
||||
c?.removeAttribute('aria-busy');
|
||||
}
|
||||
|
||||
async function _fetchPage(beforeSeq: number | null = null, append = false): Promise<void> {
|
||||
if (_loading) return;
|
||||
_loading = true;
|
||||
if (!append) {
|
||||
_entries = [];
|
||||
// Reset the cursor for a fresh query, but DON'T clear `_entries` — keep
|
||||
// the current rows on screen so filtering an already-populated list
|
||||
// never flashes the full "Loading" state (the new results replace them
|
||||
// on arrival).
|
||||
_nextBeforeSeq = null;
|
||||
_hasMore = false;
|
||||
}
|
||||
_updateListContainer();
|
||||
|
||||
if (!_hasLoadedOnce && !append) {
|
||||
// Genuine first load — there's nothing to show yet, so the spinner is
|
||||
// the correct (and expected) initial state. Show it immediately.
|
||||
_showSpinner = true;
|
||||
_updateListContainer();
|
||||
} else if (!append) {
|
||||
// Re-query (filter change / language change): defer any loading hint so
|
||||
// near-instant responses show nothing at all; a slow request gets a
|
||||
// subtle dim after the delay.
|
||||
if (_loadingDelayTimer) clearTimeout(_loadingDelayTimer);
|
||||
_loadingDelayTimer = setTimeout(_showDelayedBusy, 180);
|
||||
}
|
||||
// append (load-more): keep existing rows, no loading indicator.
|
||||
|
||||
try {
|
||||
const qs = _buildQuery(beforeSeq);
|
||||
@@ -524,15 +574,16 @@ async function _fetchPage(beforeSeq: number | null = null, append = false): Prom
|
||||
_nextBeforeSeq = page.next_before_seq;
|
||||
_hasMore = page.has_more;
|
||||
_total = page.total;
|
||||
// Reset the loading flag BEFORE rendering: _renderList() shows the
|
||||
// spinner whenever (_loading && _entries.length === 0), so a zero-result
|
||||
// page (e.g. an unmatched entity-type filter, or a fresh install) would
|
||||
// otherwise render the spinner here and spin forever — the finally below
|
||||
// clears _loading but does not re-render.
|
||||
_hasLoadedOnce = true;
|
||||
// Clear loading affordances BEFORE rendering so a zero-result page
|
||||
// renders the empty state (not the spinner) and a re-query swaps in the
|
||||
// fresh, undimmed rows.
|
||||
_clearBusy();
|
||||
_loading = false;
|
||||
_updateListContainer();
|
||||
} catch (e: unknown) {
|
||||
if (e && typeof e === 'object' && 'isAuth' in e) return;
|
||||
_clearBusy();
|
||||
const container = document.getElementById('al-list-container');
|
||||
if (container) {
|
||||
container.innerHTML = `<div class="al-state al-error" role="alert">
|
||||
@@ -542,6 +593,7 @@ async function _fetchPage(beforeSeq: number | null = null, append = false): Prom
|
||||
}
|
||||
} finally {
|
||||
_loading = false;
|
||||
_clearBusy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user