From 57af923b7df69b9b66ebdf67b0fa76e0884612a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 07:10:33 +0000 Subject: [PATCH] Fix scroll position not resetting when navigating between pages Adds a ScrollToTop component inside the main content area that resets scrollTop to 0 whenever the route pathname changes. The main GridItem already had overflowY="auto", meaning scroll state lived in that element rather than the window, so React Router's built-in scroll restoration didn't apply. Also exposes mainRef via SiteContext and adds a data-test attribute for testability. Fixes #2828, also addresses #2656 (activity viewer loading off-screen after navigating from scrolled Explore page). Adds e2e test to verify scroll position resets on navigation. https://claude.ai/code/session_01GkbCKnBr4VvMojBVuhULKd --- apps/app/src/paths/SiteHeader.tsx | 32 +++++++++++++-- .../e2e/Activities/scrollReset.cy.ts | 41 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 packages/e2e-tests/e2e/Activities/scrollReset.cy.ts diff --git a/apps/app/src/paths/SiteHeader.tsx b/apps/app/src/paths/SiteHeader.tsx index 75b134fbd..3f63129cb 100644 --- a/apps/app/src/paths/SiteHeader.tsx +++ b/apps/app/src/paths/SiteHeader.tsx @@ -1,6 +1,6 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Grid, GridItem, SkipNavLink, SkipNavContent } from "@chakra-ui/react"; -import { Outlet, useLoaderData } from "react-router"; +import { Outlet, useLoaderData, useLocation } from "react-router"; import axios from "axios"; import { ContentDescription, @@ -20,6 +20,7 @@ export type SiteContext = { setAddTo: (_: ContentDescription | null) => void; allLicenses: License[]; allDoenetmlVersions: DoenetmlVersion[]; + mainRef: React.RefObject; }; export async function loader() { @@ -44,6 +45,20 @@ export async function loader() { * and a hamburger menu with drill-down navigation on mobile. * Includes skip navigation link for accessibility. */ +function ScrollToTop({ + scrollRef, +}: { + scrollRef: React.RefObject; +}) { + const { pathname } = useLocation(); + + useEffect(() => { + scrollRef.current?.scrollTo({ top: 0 }); + }, [pathname, scrollRef]); + + return null; +} + export function SiteHeader() { const { user, allLicenses, allDoenetmlVersions } = useLoaderData() as { user?: UserInfoWithEmail; @@ -55,6 +70,8 @@ export function SiteHeader() { const [addTo, setAddTo] = useState(null); + const mainRef = useRef(null); + const siteContext: SiteContext = { user, exploreTab, @@ -63,6 +80,7 @@ export function SiteHeader() { setAddTo, allLicenses, allDoenetmlVersions, + mainRef, }; return ( @@ -84,8 +102,16 @@ export function SiteHeader() { > - + + diff --git a/packages/e2e-tests/e2e/Activities/scrollReset.cy.ts b/packages/e2e-tests/e2e/Activities/scrollReset.cy.ts new file mode 100644 index 000000000..10f2b69af --- /dev/null +++ b/packages/e2e-tests/e2e/Activities/scrollReset.cy.ts @@ -0,0 +1,41 @@ +describe("Scroll Reset on Navigation", { tags: ["@group2"] }, function () { + it("resets scroll position when navigating to a different page", () => { + cy.loginAsTestUser(); + + // Create content so the activities page has items to display + cy.createContent({ name: "Activity 1", doenetML: "Hello!" }); + cy.createContent({ name: "Activity 2", doenetML: "World!" }); + + cy.getUserInfo().then((user) => { + // Use a narrow viewport height so the page is scrollable with just a few items + cy.viewport(1000, 300); + + cy.visit(`/activities/${user.userId}`); + + // Wait for content to load + cy.get('[data-test="Activities"]').should("exist"); + + // Verify the main container is scrollable + cy.get('[data-test="Main Content"]').should(($el) => { + expect($el[0].scrollHeight).to.be.greaterThan($el[0].clientHeight); + }); + + // Scroll down in the main content area + cy.get('[data-test="Main Content"]').scrollTo("bottom"); + + // Verify we scrolled down + cy.get('[data-test="Main Content"]').should(($el) => { + expect($el[0].scrollTop).to.be.greaterThan(0); + }); + + // Navigate to Explore + cy.get('[data-test="Explore"]').click(); + cy.url().should("include", "/explore"); + + // Scroll position should be reset to top after navigation + cy.get('[data-test="Main Content"]').should(($el) => { + expect($el[0].scrollTop).to.equal(0); + }); + }); + }); +});