Repository navigation
feat(frontend): add horizontal scroll affordances to wide data tables - #1332
Conversation
…llbar on screen Wide tables scroll inside their own box since LeanerCloud#703, but nothing showed that columns were cut off on the right. On Opportunities (16 columns at 1280px, the last five clipped) the table's only horizontal scrollbar sat at the bottom of a page-tall table, under the sticky action box, and a row reached with Tab could sit hidden under that box. lib/table-scroll.ts, wired from index.ts: - Any table scroller with hidden columns fades that edge out (.scroll-hint-left/right, a CSS mask, so row and cell backgrounds cannot cover it; the mask leaves scrollbars at full opacity). - The Opportunities table gets a scrollbar strip that sticks just above the action box and mirrors the table's scroll position both ways. The table's own bar is hidden so only one shows; touch, trackpad and Shift+wheel still scroll the table directly. - --action-box-offset tracks the action box's real height (it wraps to 136px at 390px), used by the list padding and the strip. - Keyboard focus inside the list scrolls the page so the focused row is clear of the topbar, strip and action box. Browsers do not do this themselves because a row under a sticky bar counts as on screen. Refs LeanerCloud#185 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jest covers the scroller detection, edge hints, strip sync and focus scrolling with stubbed layout (jsdom has none). The Playwright spec checks the real rendering at 1280px and 390px with 40 rows: the clipped edge fades, the strip sits on screen right above the action box and stays in step with the table both ways, a table that fits gets neither, and Tab onto a row under the action box scrolls it clear. Refs LeanerCloud#185 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CSS defaults The stylesheets read --scroll-hint-sb-w, --scroll-hint-sb-h and --action-box-offset, but only table-scroll.ts ever set them, so no stylesheet defined them and LeanerCloud#745's custom-property lint flags the reads as undefined. Declare :root defaults that equal the old var() fallbacks (0px, and --action-box-height). The inline values the module sets on the scroller and on <html> still win over :root, so rendering is unchanged. Refs LeanerCloud#185 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Independent review (IAmJSD's agent, fresh context) of the original head: APPROVE Blocking:
Follow-up commit |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Head |
|
Local verification at
|
|
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 |
No textual conflicts. Checked main's changes next to this branch's: LeanerCloud#1344 moved the YTD sparkline into loadYtdSparkline(), which looks up .kpi-tile-spark[data-spark-key="ytd"]; lib/kpi-tile.ts still renders that slot for the YTD tile. CSS added by LeanerCloud#1336/LeanerCloud#1332/LeanerCloud#1388 (sticky section nav z-index 5, table scroll shadows z-index 9, sort buttons) sits below the dialog z-index ladder and does not touch card or dialog surfaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Opportunities at 1280px the table is 1429px wide inside a 1000px card, so the last five columns are cut off and Monthly Savings is half clipped, with nothing on screen saying so: the table's only horizontal scrollbar sits at the bottom of a roughly 3000px-tall table, under the sticky action box. Tabbing onto a row under the action box also left the focused control hidden. This PR fades the clipped edge of any wide table, adds a scrollbar strip that sticks just above the Opportunities action box, and scrolls keyboard-focused rows clear of the sticky bars.
Closes #185
What changed
frontend/src/lib/table-scroll.ts(197 lines), started fromfrontend/src/index.tsonDOMContentLoaded:.scroll-hint-left/.scroll-hint-right, which fade that edge out with a CSS mask. This applies to every table in the app (including the purchase modal and history tables), because it detects whichever element actually scrolls.#recommendations-list > tableit inserts anaria-hidden.table-scroll-stripthat sticks just above the action box and stays in sync with the table'sscrollLeftin both directions. The table's own scrollbar is hidden so only one bar shows; touch, trackpad and Shift+wheel still scroll the table directly.--action-box-offsetto the action box's real height (80px on desktop, 136px at 390px), used by the list'spadding-bottom(components.css) and by the strip.focusinhandler (:focus-visible) scrolls the page so a focused control in the list clears the topbar, strip and action box.requestAnimationFrame), on resize and on every captured scroll.frontend/src/styles/responsive.css; two lines changed infrontend/src/styles/components.css. The three properties the module sets (--scroll-hint-sb-w,--scroll-hint-sb-h,--action-box-offset) also get:rootdefaults in CSS (0px,0px,var(--action-box-height)), so every custom property the stylesheets read is defined there. The module's inline values still override them.src/__tests__/table-scroll.test.ts(18 tests), one wiring test insrc/__tests__/index.test.ts, and a new Playwright spectests-e2e/table-scroll-affordance.spec.ts(4 tests).Why this approach
The issue lists several options. These are the ones that work without touching the 5755-line
recommendations.tsor changing which data is shown.background-attachmentshadow because hovered and highlighted rows paint over a background shadow.scroll-paddingbecause Chromium ignoresscroll-paddingwhen focusing an element it considers already visible (the e2e test failed withscroll-paddingalone).Screenshots
Before is main (
a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API, except where a narrower width is noted. The fixture has 40 recommendations, and Chromium's--hide-scrollbarsis disabled so real scrollbars render.Verification
Run on this branch's head
6f8dd286on top of maina14dc8f2:npm run lint: 0 errors (142 warnings, all pre-existing and none in the changed files). Typecheck clean, build OK (only the usual size warning).--randomize: 28/28.table-scroll.test.tscovers scroller detection, hint states, strip insertion, range and two-way sync, the action-box offset, focus scrolling (including mouse focus and focus outside the list), and that every property the module sets has a:rootdefault in the stylesheets. With fix(frontend): undefined CSS custom properties and inline-styled MFA panels #745'scss-custom-properties.test.tsapplied on top of this branch, it passes 111/111. Jest printed "a worker process has failed to exit gracefully"; the new suites start no timers, but whether this also happens on main was not checked.table-scroll-affordance.spec.tschecks the fade, strip position and sync at 1280px and 390px, that Tab onto a row under the action box scrolls it clear, and that a table that fits at 1920px gets no fade and no strip. Full suite: 161/161 passed, includingpage-overflow(fix(frontend): page-level horizontal scroll on Opportunities, Purchases and Admin #703).scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Notes for review
Design points:
responsive.css(--scroll-hint-fade/rgb(0 0 0 / 0.12)).scrollbar-width: none) in favor of the strip, which is styled with::-webkit-scrollbarso it stays visible with macOS overlay scrollbars. Firefox on macOS falls back toscrollbar-colorand may only show it on hover.scroll-behavior: smooth(already disabled under reduced motion).Not included: sticky first columns, a leaner default column set (a product decision; the Columns menu already lets users hide columns), and a card layout on narrow screens. The issue lists these as alternatives to a scroll affordance. Also unchanged from main: re-rendering the list (for example expanding a group) rebuilds the table and resets its horizontal scroll to the left.
Overlaps
responsive.csstable-overflow rules that fix(frontend): stop pages scrolling horizontally at 1280, 768 and 390 #767 added for fix(frontend): page-level horizontal scroll on Opportunities, Purchases and Admin #703 (already on main).frontend/src/styles/components.css, in different hunks; a clean merge is expected.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