Skip to content

fix(frontend): stop pages scrolling horizontally at 1280, 768 and 390 - #767

Merged
cristim merged 2 commits into
mainfrom
fix/ux-horizontal-scroll
Oct 9, 2026
Merged

cristim merged 2 commits into
mainfrom
fix/ux-horizontal-scroll

Conversation

@cristim

@cristim cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member

What

Removes page-level horizontal scroll (#703). Independent of #763/#764/#765 (branched from main e5ac75d). CSS plus one e2e guard.

Measured offenders on main with the mock API (outermost element past the viewport), all fixed:

  • Tables: the display:block; overflow-x:auto wrap only existed below 768px. Now global (responsive.css), so Opportunities (+439px at 1280), Purchases (+153), Plans (+30) no longer widen the page.
  • <fieldset class="settings-category">: min-width: 0 (Admin accounts/users/purchasing, up to +444px at 390).
  • Hidden tooltips: .info-icon .tooltip-text used visibility:hidden, which still extends the scrollable area; now display:none, shown on hover and :focus-within, max-width: calc(100vw - 2rem).
  • Recommendations action box: flex-wrap and min-height instead of a fixed 80px height; filter-status bar wraps (Columns button no longer at x=395 on a 390px screen).
  • .settings-tabs / .inventory-subnav scroll within themselves; tab buttons don't shrink.
  • .setting-input select is capped at the row width (a select sizes to its longest option; +181px on Admin purchasing at 390).
  • .chart-section canvas { max-width: 100% }: the Home savings-by-service canvas drew 120px wider than its card in the e2e fixture.

Not changed: scroll affordance and clipped right-edge columns inside tables (#185).

Refs #703 (VIS-09).

Verification

  • New tests-e2e/page-overflow.spec.ts: 9 pages x 1280/768/390 assert scrollWidth - innerWidth <= 0. On a build of origin/main 10 of 27 fail (home x3, opportunities x3, admin general/purchasing/accounts/users at 390); on this head 27/27 pass.
  • Full Playwright e2e after a real build on a private port: 135/135. jest 3144 passed, tsc clean.
  • Rendered check with the larger review mock (9 pages x 3 widths, /tmp/claude/ux-fixes/overflow.js): main shows hscroll 439/153/30/94/66/60/94/51/376/444/438 on 11 page/width combinations; this head shows 0 on all 27.
  • Before/after shots, 7 pages x 3 widths: /tmp/claude/ux-fixes/shots/{home,opportunities,plans,purchases,inventory,admin-users,admin-general}-{main,pr3}-{1280,768,390}.png (e.g. opportunities-pr3-1280.png, admin-users-pr3-390.png). axe color-contrast node counts unchanged on every page.
  • Mock-based evidence, not a real backend.

🤖 Generated with Claude Code

Tables scroll inside their own box at every width (the rule only applied
below 768px), fieldsets get min-width: 0, hidden tooltips use display: none
instead of visibility: hidden so they no longer extend the scrollable area,
the recommendations action box and filter bar wrap, settings and inventory
sub-tab bars scroll within themselves, a select inside a settings row stays
within the row, and chart canvases are capped at their card width. A
Playwright guard compares scrollWidth to the viewport for nine pages at
three widths.

Refs #703

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added priority/p2 Backlog-worthy triaged Item has been triaged severity/high Significant harm urgency/this-quarter Within the quarter impact/many Affects most users effort/m Days type/bug Defect labels Oct 9, 2026
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 6 billable files and costs up to $1.50.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 18 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 85 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 4d38e78e-d9de-45b1-97b8-0a66cb0be4a6

📥 Commits

Reviewing files that changed from the base of the PR and between e5ac75d and 2b963b1.


📒 Files selected for processing (6)
  • frontend/src/styles/charts.css
  • frontend/src/styles/components.css
  • frontend/src/styles/responsive.css
  • frontend/src/styles/settings.css
  • frontend/src/users/permissionMatrix.ts
  • frontend/tests-e2e/page-overflow.spec.ts


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Independent review (rv-ux-visual): PR #767 at 0c56dfe711b15fbfc64495d11d69c58ba6c3e344

Verdict: changes requested. The overflow fix works on 32 of 33 page/width combinations. But the global table { display:block } visibly breaks every table narrower than its card at desktop, and Admin Users at 390 still scrolls sideways.

Base: origin/main e5ac75d. Both SHAs were built from tarballs (production webpack) and served through the same mock API (populated and empty). Playwright Chromium at 1280/768/390, axe-core 4.11 (WCAG A/AA), pixel diff base vs head. This is mock-based evidence. CI at this SHA: 26/26 green.

Overflow, measured (scrollWidth - innerWidth, px, populated mock)

Page 1280 base→head 768 base→head 390 base→head
Opportunities 435 → 0 94 → 0 94 → 0
Plans 120 → 0 0 → 0 0 → 0
Purchases 153 → 0 0 → 0 0 → 0
Admin General 0 → 0 0 → 0 51 → 0
Admin Purchasing 0 → 0 1 → 0 379 → 0
Admin Accounts 0 → 0 66 → 0 444 → 0
Admin Users 0 → 0 8 → 0 386 → 118

Home and the three Inventory sub-tabs are 0 on both. Empty mode: everything is 0 on head except Admin Users at 390 (118).

Findings

  1. Medium (visual regression at desktop): tables no longer fill their card (frontend/src/styles/responsive.css, new global table { display:block; max-width:100%; overflow-x:auto }).
    • With display:block, the table element becomes the scroll box. The rows go into an anonymous table that is only as wide as its content, while the box keeps width:100%, shadow and radius.
    • Seen at 1280:
      • Admin → Accounts: the header background and row dividers of all three provider tables stop at about x=937, inside a card box that runs to x=1207. An empty white slab sits on the right of every table.
      • Inventory → Coverage: the AWS rows end at about x=918 and the Azure rows at about x=900. The two tables no longer share column widths.
      • Admin Users and the Admin Purchasing ladder tables change the same way (pixel diff bands).
    • Before, these tables stretched edge to edge. This is the opposite of the "look top notch" goal, on every page with a short table.
    • Fix: keep tables as display: table; width: 100% and put the overflow on a wrapper: .table-wrap { overflow-x: auto; max-width: 100% } around each data table, or overflow-x:auto on the existing table containers (#recommendations-list, #history-list, the coverage and accounts cards). Then a narrow table fills its card and a wide one scrolls inside it.
  2. Medium: Admin Users at 390 still scrolls sideways by 118px, and the new guard cannot see it.
    • The outermost offender is div.group-card-actions (the "N members" badge plus Edit / Duplicate / Delete), 299px wide, with its right edge at x=508 on a 390 viewport. .group-card-actions { display:flex; flex-shrink:0 } and .group-card-header { display:flex; justify-content:space-between } (settings.css) never wrap.
    • Fix: .group-card-header { flex-wrap: wrap; gap: .5rem } and .group-card-actions { flex-wrap: wrap }.
    • Guard gap: tests-e2e/fixtures/recs.ts mockApi does not route /api/groups (nor /api/users), so /admin/users renders with no group cards in page-overflow.spec.ts, and the 27/27 pass does not cover this. Add populated users and groups to the fixture, or to the spec, so the guard runs against real content.
  3. Low (new axe violation): scrollable-region-focusable (serious) on .permission-matrix at 390.
    • The matrix is read-only (no focusable cells). It is now a scroll container, so keyboard users cannot scroll to the hidden columns.
    • The same rule already fires on base for the Inventory tables (from the old <768 rule), and this PR extends the pattern to all widths.
    • Fix: give the scroll wrapper from finding 1 tabindex="0", role="region" and an aria-label when it holds no focusable content. At minimum, do this for .permission-matrix.
  4. Nit: the tooltip's new :focus-within trigger is inert. None of the 10 .info-icon spans in index.html is focusable (no tabindex, no focusable child), so keyboard users still cannot open them. Add tabindex="0" and aria-describedby pointing at the tooltip text, or leave the selector out until the icons are focusable.

Verified good

  • The tooltip visibility:hidden → display:none change, the .settings-category { min-width:0 } fieldset fix, the sub-tab rows that scroll in place, the select cap, the wrapping Opportunities action bar and filter bar, and the canvas max-width all remove their offenders. The Opportunities action bar at 390 now wraps inside the viewport.
  • axe otherwise identical, or one node better on Opportunities target-size and Admin Purchasing color-contrast at 390. No console errors. Home, and Plans/Purchases at 768/390, are pixel-identical.

Evidence (local, review host): /tmp/claude/ux-pr/shots-767/ (<page>-{base,head,head-empty}-{1280,768,390}.png, *-diff-*.png, axe-{base,head}.json, crop-acc-{base,head}.png, crop-cov-head.png). Offender finder: /tmp/claude/ux-run/overflow.js (BASE=… CASES=/admin/users:390).

… pages

Tables keep display: table; the container that directly holds a table takes
overflow-x: auto instead (the recommendations list is excluded so its sticky
action box keeps sticking, its table scrolls itself). Group card actions and
headers wrap, member pills break long emails, the permission matrix is a
labelled focusable scroll region and is the containing block for its sr-only
cell labels, and the dead tooltip :focus-within rule is dropped. The
overflow guard now routes populated users and groups.

Refs #703

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim

cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Independent review, round 2 (rv-ux-visual): PR #767 at 2b963b121b313bab7e005b067df01d9b95f8dd3b

Verdict: approved (no blocking findings). Every round 1 finding is fixed and verified live. Two cosmetic side effects of the scroll containers are noted below for a follow-up.

Base origin/main e5ac75d. Both SHAs were built from tarballs (production webpack), served through the same populated mock API, and checked with Playwright Chromium at 1280/768/390 plus axe-core 4.11. Mock-based evidence.

Round 1 findings, re-verified

  1. Fixed: tables fill their cards again. Tables keep display:table; the overflow moved to the container (:where(div,section,fieldset,form,article):has(> table):not(#recommendations-list) { overflow-x:auto }). Only the recommendations table, whose sticky action box needs it, still scrolls itself. At 1280 the Admin → Accounts and Inventory → Coverage rows run edge to edge again, with no empty slab.
  2. Fixed: Admin Users at 390 no longer overflows (base 386 → round 1 118 → 0). .group-card-header and .group-card-actions wrap, and long member pills break. The guard now routes populated /api/users and /api/groups; CI Playwright at this SHA shows /admin/users does not scroll horizontally at 390px ✓ (135/135 passed).
  3. Fixed: scrollable-region-focusable on .permission-matrix is gone. The container gets tabindex="0", role="region" and aria-label="Permission matrix", and these are removed again in the empty state.
  4. Resolved: the inert :focus-within trigger was dropped, so the CSS no longer claims keyboard support it lacks. Keyboard tooltips remain a separate gap (the info icons are not focusable).

Overflow now (populated mock)

Every page at 1280/768/390 is 0 (33/33). Base had 12 overflowing combinations: Opportunities 435/94/94, Plans 120, Purchases 153, Admin Purchasing 1/379, Admin Users 8/386, Admin Accounts 66/444, Admin General 51.

Regression checks

  • axe: identical to base except two improvements (Opportunities target-size 15→14 at 1280, Admin Purchasing color-contrast 3→2 at 390). No new violations, no console errors.
  • Popovers: the column-filter popovers in both Purchases tables open fully unclipped, identically to base. No ancestor clips them.
  • Cosmetic side effects of the new scroll containers (follow-up, non-blocking):
    • A container with overflow-x:auto clips its child's box-shadow, so the tables' own soft shadow and rounded bottom edge no longer show (they look flatter).
    • The container becomes a block formatting context, so margins stop collapsing. Each table sits about 12px lower: Admin Accounts is 36px taller at 1280 (3 tables), Purchases 12px taller at every width.
    • If you want the old look back: move the shadow and radius onto the container (:has(> table)) or remove the table's top margin inside it.
  • Selector reach: :has(> table) applies to every div/section/fieldset/form/article that directly contains a table, including modal bodies. That is fine today (no clipping found). If a future table cell hosts an in-flow dropdown, it would be clipped; a .table-wrap class on the known containers would be narrower.

CI at this SHA: 26/26 green, including Build frontend (jest) and Playwright Chromium (135 passed, with page-overflow.spec.ts covering 9 pages x 3 widths on populated users/groups).

Evidence (local, review host): /tmp/claude/ux-pr/shots-767/ (<page>-{base2,head2}-{1280,768,390}.png, *-diff-*.png, axe-{base2,head2}.json, crop-admin-accounts-head2.png, popover-head2-*.png). Scripts: /tmp/claude/ux-run/{review-shoot.js,overflow.js,pr767-popover.js}.

@cristim
cristim merged commit 9a14691 into main Oct 9, 2026
27 checks passed
@cristim

cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Merged (squash) at reviewed head 2b963b1 -> 9a14691. Reviewer: rv-ux-visual, approved round 2 at this SHA (comment names it). CI: 27/27 checks pass. Local (frontend, head archive): npm ci 0, tsc --noEmit 0, npm test 0 (99 suites, 3144 passed, 1 skipped), npm run lint 0 (0 errors, 125 pre-existing-style warnings).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant