Repository navigation
fix(frontend): use skeletons for loading and add a slow-response note - #1333
Merged
Merged
Conversation
…fallback The app mixed shimmer skeletons with plain "Loading..." paragraphs, sometimes side by side (RI Exchange showed skeleton tables next to "Loading exchange history..."), and a slow or never-replaced skeleton gave no sign that anything was still happening. On Opportunities the skeleton was invisible altogether: the list table is display:block for horizontal scrolling, which collapsed the auto-sized skeleton cells to their padding and the 80%-wide shimmer boxes to 0px, so the panel read as an empty white card. - lib/skeleton.ts: every show* call adds a screen-reader-only "Loading..." status (the shimmer is aria-hidden) and arms a timer. If the skeleton is still on screen after 8 s (SKELETON_SLOW_MS) and its panel is not hidden, a visible "Still loading. This is taking longer than usual." note is appended. Success renders that replace the children, or teardownSkeleton, make the timer a no-op. New showSkeletonText helper. - Text loaders moved to skeletons: RI Exchange automation settings and exchange history, Admin > General settings (#settings-loading), account service-override panels, and the API key permissions list. - CSS: skeleton tables use fixed layout at full width with a minimum box width, the Opportunities skeleton table keeps display:table, and the slow note spans grid rows (Home KPI tiles, Plans cards). Refs LeanerCloud#186 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d loaders Jest (fake timers): showSkeletonText shape, one screen-reader status per skeleton kind, the note appears exactly at SKELETON_SLOW_MS, never after an innerHTML handoff, teardown or inside a hidden panel, and re-showing restarts the clock without stacking notes. RI Exchange automation settings render the skeleton instead of "Loading settings...". Playwright: on a held recommendations request the Opportunities skeleton is actually visible (non-zero shimmer width), the note appears after 8 s of page clock, and both go away once the data arrives. Refs LeanerCloud#186 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Conflicts: - frontend/src/__tests__/riexchange-automation-settings.test.ts: both sides appended a describe block at the end of the file (this branch's LeanerCloud#186 skeleton loading-state test, main's LeanerCloud#37 value-attribute escaping tests from LeanerCloud#1318). Kept both blocks, LeanerCloud#186 first. - frontend/src/settings.ts: both sides added an import after the ladder import (showSkeletonText here, buildLastUpdatedStamp from LeanerCloud#1314 on main). Kept both imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/lib/skeleton.ts: kept both accessibility layers. Main's aria-busy + MutationObserver (LeanerCloud#729/LeanerCloud#1370) and this branch's sr-only "Loading…" status and 8 s "Still loading" note both run from activateSkeleton; teardownSkeleton clears the slow timer and aria-busy. - frontend/src/__tests__/skeleton.test.ts: kept both suites (slow fallback and aria-busy); the aria-busy table also covers showSkeletonText. - frontend/src/riexchange.ts: kept main's titled "Exchange Automation" frame (LeanerCloud#190) with aria-busy and put this branch's showSkeletonText inside it, replacing the visible "Loading exchange automation settings..." line. Imports unioned (skeletonText no longer used). - frontend/src/__tests__/riexchange-automation-settings.test.ts: kept main's LeanerCloud#190 frame tests; folded this branch's assertions (skeleton, sr-only status, no loading paragraph) into the loading test. - frontend/src/apikeys.ts, frontend/src/settings.ts: import unions. - frontend/src/styles/responsive.css: kept this branch's skeleton-table display:table rule and main's scroll-affordance rules (LeanerCloud#185). Also updated tests-e2e/purchasing-sections.spec.ts (from main) to expect the skeleton inside the frame instead of the removed loading paragraph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
Collaborator
Author
|
Local verification at
|
IAmJSD
marked this pull request as ready for review
October 24, 2026 20:51
Collaborator
Author
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
IAmJSD
added a commit
to IAmJSD/cloud-commitments-platform
that referenced
this pull request
Oct 24, 2026
Conflicts: - frontend/src/riexchange.ts: import block. Main (LeanerCloud#1333) replaced the skeletonText import with showSkeletonText; this branch added the statusBadgeHtml import. Kept main's skeleton import and this branch's status-badge import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Loading states were inconsistent: most panels used shimmer skeletons, but several showed plain "Loading..." text (sometimes next to a skeleton), a slow response looked like a hang, and the Opportunities skeleton was invisible because its cells collapsed to 0px. This PR moves the remaining block-level text loaders to skeletons, fixes the Opportunities skeleton, and adds an app-wide "still loading" note after 8 s.
Refs #186
Partial: text loaders that live inside
<option>or<label>elements (where a shimmer cannot render) are left as text, and there is no request timeout or abort, only a visible note.What changed
frontend/src/lib/skeleton.ts: everyshow*helper now adds a screen-reader-only "Loading…" status (the shimmer staysaria-hidden) and arms an 8 s timer (SKELETON_SLOW_MS). If the skeleton is still on screen and its panel is not hidden when the timer fires, it appends "Still loading. This is taking longer than usual." withrole=status. A success render that replaces the children, orteardownSkeleton, turns the timer into a no-op. NewshowSkeletonText(container, lines)helper.riexchange.ts:loadAutomationSettings(4 lines) andloadExchangeHistory(3x7 rows).settings.ts:loadGlobalSettings(#settings-loading) andloadOverridesPanel.apikeys.ts: the create-key permissions list.styles/base.css:.skeleton-tableis full width with fixed layout and a minimum box width;.skeleton-slow-noteis centered, muted and spans grid rows (Home KPI tiles, Plans cards).styles/responsive.css:#recommendations-list > table.skeleton-tablekeepsdisplay: table, so the Opportunities skeleton cells no longer collapse to 0px.be1d55d2(the change),8aa8baa9(tests).Why this approach
Skeletons were already the dominant pattern (#344), so the remaining text loaders were moved to them rather than the reverse. The slow fallback lives in the shared helper, so all of the roughly 15 skeleton call sites get it without per-module code. It checks whether shimmer elements are still present rather than a data marker, because most success renders replace content via
innerHTMLand never call teardown. It only adds a note and never aborts the request, so a slow but valid response still lands. Empty responses are already handled by each renderer's own empty state; the remaining "looks like a hang" case was a skeleton left on screen, which the note covers.Screenshots
Before is main (
a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API. The API routes in these scenarios never resolve.Verification
Run on this branch's head
8aa8baa9on top of maina14dc8f2, fromfrontend/:--maxWorkers=2): 109 suites, 3497 passed, 1 skipped, 0 failed. The changed suites also pass with--randomize(32 tests).skeleton.test.ts+10 with fake timers (showSkeletonText; one screen-reader status per skeleton kind; note appears exactly at the threshold; no note after aninnerHTMLhandoff, after teardown, or in a hidden panel; re-show restarts the clock without stacking notes).riexchange-automation-settings.test.ts+1 (skeleton instead of "Loading settings...").e2e-local.sh): 158 passed, 0 failed, including the newtests-e2e/loading-fallback.spec.ts, which holds the recommendations request open and checks the skeleton has non-zero width, the note appears after 8 s of page clock, and both clear when data arrives.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Notes for review
<option>placeholder,federation.tsandladder-account-selector.tsselect/label text (a shimmer cannot sit inside an<option>or label), theladder.tsinitial template "Loading…", the ladder-timeline status text, and the Archera comparison live-region text (screen-reader only).AbortControllerplumbing inapi/client.ts, which is left out of this PR.Overlaps
lib/skeleton.ts(addsaria-busyplus aMutationObserver). It will conflict, and the two accessibility approaches overlap: keep one of them (aria-busywith the observer, or the sr-only status here) when combining.riexchange.tsloadAutomationSettingsandriexchange-automation-settings.test.ts; combine by usingshowSkeletonTextinside that branch's "Exchange Automation" frame.styles/base.css; feat(frontend): wide data tables clip right-edge columns under sticky header + action bar with no scroll affordance #185 and chore(frontend): low-severity table and CSS hygiene fixes #725 editstyles/responsive.css.loadOverridesPanelinsettings.ts; fix(frontend): raw transport and developer errors reach users instead of friendly messages #748 editssettings.ts/apikeys.ts; Sweep toISOString().split('T')[0] -> toLocalDateInputValue across frontend date inputs #51 and fix(frontend): confirm dialog focuses destructive button, lacks a11y, native dialogs remain #705 editapikeys.ts.riexchange.ts(refactor(frontend): consolidate column-filter popover plumbing into shared lib #18, fix(frontend): RI-exchange cost chips and totals hardcode "$" and discard currency_code #284, fix(frontend): RI exchange confirm lists offering UUIDs, no instance type or cost delta #709, fix(frontend): filter Clear hides all rows; user/group form validation toast-only #727, fix(frontend): low-severity accessibility fixes (focus, aria, target size) #729, fix(frontend): status badges use four shapes and conflicting colors per status #737, fix(frontend): RI exchange result is not announced and the exchange ID vanishes after 2s #738, fix(frontend): RI exchange target picker says type a UUID but offers no field #740); only fix(frontend): low-severity accessibility fixes (focus, aria, target size) #729 touches the same loader lines.Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code