Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/core/src/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ class CurrentPage {

page.rememberedState ??= {}

const location = typeof window !== 'undefined' ? window.location : new URL(page.url)
const isServer = typeof window === 'undefined'
const location = !isServer ? window.location : new URL(page.url)
const scrollRegions = !isServer && preserveScroll ? history.getScrollRegions() : []
replace = replace || isSameUrlWithoutHash(hrefToUrl(page.url), location)

return new Promise((resolve) => {
Expand All @@ -89,7 +91,12 @@ class CurrentPage {
this.isFirstPageLoad = false

return this.swap({ component, page, preserveState }).then(() => {
if (!preserveScroll) {
if (preserveScroll) {
// Scroll regions must be explicitly restored since the DOM elements are destroyed
// and recreated during the component 'swap'. Document scroll is naturally
// preserved as the document element itself persists across navigations.
window.requestAnimationFrame(() => Scroll.restoreScrollRegions(scrollRegions))
} else {
Scroll.reset()
}

Expand Down
31 changes: 19 additions & 12 deletions packages/core/src/scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,28 @@ export class Scroll {

window.requestAnimationFrame(() => {
this.restoreDocument()
this.restoreScrollRegions(scrollRegions)
})
}

this.regions().forEach((region: Element, index: number) => {
const scrollPosition = scrollRegions[index]
public static restoreScrollRegions(scrollRegions: ScrollRegion[]): void {
if (typeof window === 'undefined') {
return
}

if (!scrollPosition) {
return
}
this.regions().forEach((region: Element, index: number) => {
const scrollPosition = scrollRegions[index]

if (typeof region.scrollTo === 'function') {
region.scrollTo(scrollPosition.left, scrollPosition.top)
} else {
region.scrollTop = scrollPosition.top
region.scrollLeft = scrollPosition.left
}
})
if (!scrollPosition) {
return
}

if (typeof region.scrollTo === 'function') {
region.scrollTo(scrollPosition.left, scrollPosition.top)
} else {
region.scrollTop = scrollPosition.top
region.scrollLeft = scrollPosition.left
}
})
}

Expand Down
31 changes: 31 additions & 0 deletions packages/react/test-app/Pages/Links/ScrollRegionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import WithScrollRegion from '@/Layouts/WithScrollRegion.jsx'
import { VisitHelperOptions } from '@inertiajs/core'
import { router } from '@inertiajs/react'

const ScrollRegionList = ({ user_id }: { user_id?: number }) => {
const navigate = (id: number, options: VisitHelperOptions = {}) => {
router.get(`/links/scroll-region-list/user/${id}`, {}, options)
}

const users = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, name: `User ${i + 1}` }))

return (
<div>
<span className="text">Scrollable list with scroll region</span>
<div className="user-text">Clicked user: {user_id || 'none'}</div>

{users.map((user) => (
<div key={user.id} style={{ padding: '20px', borderBottom: '1px solid #ccc' }}>
<div style={{ marginBottom: '10px', width: '500px' }}>{user.name}</div>
<button onClick={() => navigate(user.id)}>Default</button>
<button onClick={() => navigate(user.id, { preserveScroll: true })}>Preserve True</button>
<button onClick={() => navigate(user.id, { preserveScroll: false })}>Preserve False</button>
</div>
))}
</div>
)
}

ScrollRegionList.layout = (page: React.ReactNode) => <WithScrollRegion children={page} />

export default ScrollRegionList
30 changes: 30 additions & 0 deletions packages/svelte/test-app/Pages/Links/ScrollRegionList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script context="module" lang="ts">
export { default as layout } from '@/Layouts/WithScrollRegion.svelte'
</script>

<script lang="ts">
import { router } from '@inertiajs/svelte'
import type { VisitHelperOptions } from '@inertiajs/core'

export let user_id: number | undefined = undefined

const users = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, name: `User ${i + 1}` }))

const navigate = (id: number, options: VisitHelperOptions = {}) => {
router.get(`/links/scroll-region-list/user/${id}`, {}, options)
}
</script>

<div>
<span class="text">Scrollable list with scroll region</span>
<div class="user-text">Clicked user: {user_id || 'none'}</div>

{#each users as user (user.id)}
<div style="padding: 20px; border-bottom: 1px solid #ccc">
<div style="margin-bottom: 10px; width: 500px">{user.name}</div>
<button on:click={() => navigate(user.id)}>Default</button>
<button on:click={() => navigate(user.id, { preserveScroll: true })}> Preserve True </button>
<button on:click={() => navigate(user.id, { preserveScroll: false })}> Preserve False </button>
</div>
{/each}
</div>
31 changes: 31 additions & 0 deletions packages/vue3/test-app/Pages/Links/ScrollRegionList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import WithScrollRegion from '@/Layouts/WithScrollRegion.vue'
import { VisitHelperOptions } from '@inertiajs/core'
import { router } from '@inertiajs/vue3'

defineProps({
user_id: Number,
})

const navigate = (id: number, options: VisitHelperOptions = {}) => {
router.get(`/links/scroll-region-list/user/${id}`, {}, options)
}

const users = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, name: `User ${i + 1}` }))
</script>

<template>
<WithScrollRegion>
<div>
<span class="text">Scrollable list with scroll region</span>
<div class="user-text">Clicked user: {{ user_id || 'none' }}</div>

<div v-for="user in users" :key="user.id" style="padding: 20px; border-bottom: 1px solid #ccc">
<div style="margin-bottom: 10px; width: 500px">{{ user.name }}</div>
<button @click="navigate(user.id)">Default</button>
<button @click="navigate(user.id, { preserveScroll: true })">Preserve True</button>
<button @click="navigate(user.id, { preserveScroll: false })">Preserve False</button>
</div>
</div>
</WithScrollRegion>
</template>
10 changes: 10 additions & 0 deletions tests/app/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ app.get('/links/cancel-sync-request/:page', (req, res) => {
page == 3 ? 500 : 0,
)
})
app.get('/links/scroll-region-list', (req, res) =>
inertia.render(req, res, {
component: 'Links/ScrollRegionList',
props: { user_id: req.query.user_id },
url: req.originalUrl,
}),
)
app.get('/links/scroll-region-list/user/:id', (req, res) =>
res.redirect(303, `/links/scroll-region-list?user_id=${req.params.id}`),
)

app.get('/client-side-visit', (req, res) =>
inertia.render(req, res, {
Expand Down
92 changes: 90 additions & 2 deletions tests/links.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from '@playwright/test'
import { expect, Page, test } from '@playwright/test'
import { consoleMessages, pageLoads, requests, scrollElementTo, shouldBeDumpPage } from './support'

declare const process: { env: { PACKAGE?: string } }
Expand Down Expand Up @@ -623,8 +623,8 @@ test.describe('enabled', () => {

test('restores all tracked scroll regions when pressing the back button', async ({ page }) => {
await page.getByTestId('preserve').click()

await expect(page).toHaveURL('/links/preserve-scroll-page-two')
await page.waitForTimeout(100)

await scrollElementTo(
page,
Expand Down Expand Up @@ -696,6 +696,94 @@ test.describe('enabled', () => {
})
})

test.describe('scroll region with scrollable list', () => {
const preparePage = async (page: Page, url: string) => {
await page.goto('/links/scroll-region-list')
await expect(page.getByText('Scrollable list with scroll region')).toBeVisible()
await expect(page.getByText('Clicked user: none')).toBeVisible()

await page.evaluate(() => {
// @ts-ignore
window.navigateEvents = window.navigateEvents || []

document.addEventListener('inertia:navigate', (e) => {
// @ts-ignore
window.navigateEvents.push(e)
})
})

await scrollElementTo(
page,
page.evaluate(() => window.scrollTo(5, 7)),
)

await scrollElementTo(
page,
page.evaluate(() => document.querySelector('#slot')?.scrollTo(10, 15)),
)

await page.getByRole('button', { exact: true, name: 'Update scroll positions' }).click()

await expect(page.getByText('Document scroll position is 5 & 7')).toBeVisible()
await expect(page.getByText('Slot scroll position is 10 & 15')).toBeVisible()
}

Object.entries({
'/links/scroll-region-list': 'navigate to another url',
'/links/scroll-region-list?user_id=1': 'stay on the same url',
}).forEach(([url, description]) => {
test.describe(`when visiting a URL that would ${description}`, () => {
test.beforeEach(async ({ page }) => await preparePage(page, url))

test('resets scroll regions when clicking default button', async ({ page }) => {
await page.waitForFunction(() => (window as any).navigateEvents.length === 0)
await page.getByRole('button', { exact: true, name: 'Default' }).first().click()

await expect(page).toHaveURL('/links/scroll-region-list?user_id=1')
await expect(page.getByText('Clicked user: 1')).toBeVisible()
await page.waitForFunction(() => (window as any).navigateEvents.length === 1)

await page.waitForTimeout(100)
await page.getByRole('button', { exact: true, name: 'Update scroll positions' }).click()
await page.waitForTimeout(100)

await expect(page.getByText('Document scroll position is 0 & 0')).toBeVisible()
await expect(page.getByText('Slot scroll position is 0 & 0')).toBeVisible()
})

test('resets scroll regions when clicking preserve false button', async ({ page }) => {
await page.waitForFunction(() => (window as any).navigateEvents.length === 0)
await page.getByRole('button', { exact: true, name: 'Preserve False' }).first().click()

await expect(page).toHaveURL('/links/scroll-region-list?user_id=1')
await expect(page.getByText('Clicked user: 1')).toBeVisible()
await page.waitForFunction(() => (window as any).navigateEvents.length === 1)

await page.waitForTimeout(100)
await page.getByRole('button', { exact: true, name: 'Update scroll positions' }).click()
await page.waitForTimeout(100)

await expect(page.getByText('Document scroll position is 0 & 0')).toBeVisible()
await expect(page.getByText('Slot scroll position is 0 & 0')).toBeVisible()
})

test('preserves scroll regions when clicking preserve true button', async ({ page }) => {
await page.waitForFunction(() => (window as any).navigateEvents.length === 0)
await page.getByRole('button', { exact: true, name: 'Preserve True' }).first().click()

await expect(page).toHaveURL('/links/scroll-region-list?user_id=1')
await expect(page.getByText('Clicked user: 1')).toBeVisible()
await page.waitForFunction(() => (window as any).navigateEvents.length === 1)

await page.getByRole('button', { exact: true, name: 'Update scroll positions' }).click()

await expect(page.getByText('Document scroll position is 5 & 7')).toBeVisible()
await expect(page.getByText('Slot scroll position is 10 & 15')).toBeVisible()
})
})
})
})

test.describe('URL fragment navigation (& automatic scrolling)', () => {
/** @see https://github.com/inertiajs/inertia/pull/257 */

Expand Down
4 changes: 2 additions & 2 deletions tests/manual-visits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,8 @@ test.describe('Preserve scroll', () => {

test('restores all tracked scroll regions when pressing the back button (visit method)', async ({ page }) => {
await page.getByRole('link', { exact: true, name: 'Preserve Scroll' }).click()

await expect(page).toHaveURL('/visits/preserve-scroll-page-two')
await page.waitForTimeout(100)

await scrollElementTo(
page,
Expand All @@ -711,8 +711,8 @@ test.describe('Preserve scroll', () => {

test('restores all tracked scroll regions when pressing the back button (GET method)', async ({ page }) => {
await page.getByRole('link', { exact: true, name: 'Preserve Scroll (GET)' }).click()

await expect(page).toHaveURL('/visits/preserve-scroll-page-two')
await page.waitForTimeout(100)

await scrollElementTo(
page,
Expand Down