Skip to content

Commit

Permalink
Do scroll restoration in userland (#1550)
Browse files Browse the repository at this point in the history
* do scroll restoration in userland. lol. fucking lmao

* e2e test it baby

* persist scroll positions in sessionStorage

* see if it passes CI with the reload taken out
  • Loading branch information
david-crespo committed Jun 5, 2023
1 parent ef0a179 commit 4636fb7
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 123 deletions.
30 changes: 30 additions & 0 deletions app/hooks/use-scroll-restoration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect } from 'react'
import { useLocation, useNavigation } from 'react-router-dom'

function getScrollPosition(key: string) {
const pos = window.sessionStorage.getItem(key)
return pos && /^[0-9]+$/.test(pos) ? parseInt(pos, 10) : 0
}

function setScrollPosition(key: string, pos: number) {
window.sessionStorage.setItem(key, pos.toString())
}

/**
* Given a ref to a scrolling container element, keep track of its scroll
* position before navigation and restore it on return (e.g., back/forward nav).
* Note that `location.key` is used in the cache key, not `location.pathname`,
* so the same path navigated to at different points in the history stack will
* not share the same scroll position.
*/
export function useScrollRestoration(container: React.RefObject<HTMLElement>) {
const key = `scroll-position-${useLocation().key}`
const { state } = useNavigation()
useEffect(() => {
if (state === 'loading') {
setScrollPosition(key, container.current?.scrollTop ?? 0)
} else if (state === 'idle') {
container.current?.scrollTo(0, getScrollPosition(key))
}
}, [key, state, container])
}
7 changes: 4 additions & 3 deletions app/layouts/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { useRef } from 'react'
import { Outlet, ScrollRestoration } from 'react-router-dom'
import { Outlet } from 'react-router-dom'

import { apiQueryClient } from '@oxide/api'
import { Pagination } from '@oxide/pagination'
import { SkipLinkTarget } from '@oxide/ui'
import { classed } from '@oxide/util'

import { PageActionsTarget } from 'app/components/PageActions'
import { useScrollRestoration } from 'app/hooks/use-scroll-restoration'

export const PageContainer = classed.div`grid h-screen grid-cols-[14.25rem,1fr] grid-rows-[60px,1fr]`

export function ContentPane() {
const ref = useRef<HTMLDivElement>(null)
useScrollRestoration(ref)
return (
<div ref={ref} className="flex flex-col overflow-auto">
<ScrollRestoration elementRef={ref} />
<div ref={ref} className="flex flex-col overflow-auto" data-testid="scroll-container">
<div className="flex flex-grow flex-col pb-8">
<SkipLinkTarget />
<main className="[&>*]:gutter">
Expand Down
65 changes: 65 additions & 0 deletions app/test/e2e/scroll-restore.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { type Page, expect, test } from './utils'

async function expectScrollTop(page: Page, expected: number) {
const container = await page.getByTestId('scroll-container')
const getScrollTop = () => container.evaluate((el: HTMLElement) => el.scrollTop)
await expect.poll(getScrollTop).toBe(expected)
}

async function scrollTo(page: Page, to: number) {
const container = await page.getByTestId('scroll-container')
await container.evaluate((el: HTMLElement, to) => el.scrollTo(0, to), to)
}

test('scroll restore', async ({ page }) => {
// open small window to make scrolling easier
await page.setViewportSize({ width: 800, height: 500 })

// nav to disks and scroll it
await page.goto('/projects/mock-project/disks')
await expectScrollTop(page, 0)
await scrollTo(page, 143)

// nav to snapshots
await page.getByRole('link', { name: 'Snapshots' }).click()
await expectScrollTop(page, 0)

// go back to disks, scroll is restored, scroll it some more
await page.goBack()
await expect(page).toHaveURL('/projects/mock-project/disks')
await expectScrollTop(page, 143)
await scrollTo(page, 190)

// go forward to snapshots, now scroll it
await page.goForward()
await expect(page).toHaveURL('/projects/mock-project/snapshots')
await expectScrollTop(page, 0)
await scrollTo(page, 30)

// new nav to disks
await page.getByRole('link', { name: 'Disks' }).click()
await expectScrollTop(page, 0)

// this is too flaky so forget it for now

// random reload in there because we use sessionStorage. note we are
// deliberately on the disks page here because there's a quirk in playwright
// that seems to reset to the disks page on reload
// await page.reload()

// back to snapshots, scroll is restored
await page.goBack()
await expect(page).toHaveURL('/projects/mock-project/snapshots')
await expectScrollTop(page, 30)

// back again to disks, newer scroll value is restored
await page.goBack()
await expect(page).toHaveURL('/projects/mock-project/disks')
await expectScrollTop(page, 190)

// forward again to newest disks history entry, scroll remains 0
await page.goForward()
await page.goForward()
await expect(page).toHaveURL('/projects/mock-project/disks')
await expectScrollTop(page, 0)
})
120 changes: 0 additions & 120 deletions patches/react-router-dom+6.11.2.patch

This file was deleted.

0 comments on commit 4636fb7

Please sign in to comment.