From effcd5ba0c3bb160ebd8760aa1f831d3a0d6ec8a Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Sun, 8 Jun 2025 22:47:16 +0200 Subject: [PATCH 01/16] Implement horizontal scroll Signed-off-by: Philipp Daun --- src/index.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 417b0c0..9ed9ab4 100755 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,11 @@ type ScrollPositionsCache = Record; declare module 'swup' { export interface Swup { - scrollTo?: (offset: number, animate?: boolean, scrollContainer?: Element) => void; + scrollTo?: ( + position: number | ScrollPosition, + animate?: boolean, + scrollContainer?: Element + ) => void; } export interface VisitScroll { @@ -86,8 +90,7 @@ export default class SwupScrollPlugin extends Plugin { swup.hooks.create('scroll:end'); /* Add scrollTo method to swup instance */ - swup.scrollTo = (y: number, animate = true, element?: Element) => - this.scrollTo(y, animate, element); + swup.scrollTo = this.scrollTo.bind(this); /** * Disable browser scroll restoration for history visits @@ -291,10 +294,10 @@ export default class SwupScrollPlugin extends Plugin { // Finally, scroll to either the stored scroll position or to the very top of the page const scrollPositions = this.getCachedScrollPositions(visit.to.url); - const top = scrollPositions?.window?.top || 0; + const { top, left } = scrollPositions?.window || { top: 0, left: 0 }; // Give possible JavaScript time to execute before scrolling - requestAnimationFrame(() => this.swup.scrollTo?.(top, visit.scroll.animate)); + requestAnimationFrame(() => this.scrollTo({ top, left }, visit.scroll.animate)); visit.scroll.scrolledToContent = true; }; @@ -400,7 +403,9 @@ export default class SwupScrollPlugin extends Plugin { /** * Scroll to a specific offset, with optional animation. */ - scrollTo(y: number, animate = true, scrollContainer?: Element): void { + scrollTo(position: number | ScrollPosition, animate = true, scrollContainer?: Element): void { + const { top, left } = typeof position === 'number' ? { top: position } : position; + // Create dummy visit // @ts-expect-error: createVisit is currently private, need to make this semi-public somehow const visit = this.swup.createVisit({ to: this.swup.location.url }); @@ -438,7 +443,8 @@ export default class SwupScrollPlugin extends Plugin { this.swup.hooks.callSync('scroll:start', visit, undefined); scrollContainer.scrollTo({ - top: y, + top, + left, behavior: animate ? 'smooth' : 'instant' }); } @@ -454,9 +460,9 @@ export default class SwupScrollPlugin extends Plugin { inline: 'start' }); - scrollActions.forEach(({ el: scrollContainer, top }) => { + scrollActions.forEach(({ top, left, el: scrollContainer }) => { const offset = this.getOffset(scrollTarget, scrollContainer); - this.scrollTo(top - offset, animate, scrollContainer); + this.scrollTo({ top: top - offset, left: left - offset }, animate, scrollContainer); }); } } From b708d0cbfeed8253c8a634dc019276997b05bda3 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Sun, 8 Jun 2025 23:00:21 +0200 Subject: [PATCH 02/16] Scroll to top left Signed-off-by: Philipp Daun --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9ed9ab4..820295e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -193,10 +193,10 @@ export default class SwupScrollPlugin extends Plugin { }; /** - * Scroll to top on `scroll:top` hook + * Scroll to top/left on `scroll:top` hook */ handleScrollToTop: Handler<'scroll:top'> = (visit) => { - this.swup.scrollTo?.(0, visit.scroll.animate); + this.scrollTo({ top: 0, left: 0 }, visit.scroll.animate); return true; }; From 96caa6c1dab789111602978d2c573526b21f3db2 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 9 Jun 2025 18:56:05 +0200 Subject: [PATCH 03/16] Support vertical and horizontal offsets --- README.md | 11 ++++++++--- src/index.ts | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ce66b67..6c9c8f5 100755 --- a/README.md +++ b/README.md @@ -139,18 +139,23 @@ To highlight the current target element, use the `data-swup-scroll-target` attri ### Offset -Offset to substract from the final scroll position, to account for fixed headers. Can be either a number or a function that returns the offset. +Offset to substract from the final scroll position, to account for fixed headers. Can be either a +static number or a function that returns a value based on the scroll target. To apply differing +offsets for vertical and horizontal scrolling, return an object with `top` and `left` properties. ```javascript { // Number: fixed offset in px offset: 30, + // Object: fixed vertical and horizontal offset in px + offset: { top: 30, left: 10 }, + // Function: calculate offset before scrolling offset: () => document.querySelector('#header').offsetHeight, - // The scroll target element is passed into the function - offset: target => target.offsetHeight * 2, + // The scroll target and container are passed into the function + offset: (scrollTarget, scrollContainer) => target.offsetHeight * 2, } ``` diff --git a/src/index.ts b/src/index.ts index 820295e..1e18870 100755 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,10 @@ export type Options = { samePage: boolean; }; getAnchorElement?: (hash: string) => Element | null; - offset: number | ((scrollTarget?: Element, scrollContainer?: Element) => number); + offset: + | number + | ScrollPosition + | ((scrollTarget?: Element, scrollContainer?: Element) => number | ScrollPosition); scrollContainers: `[data-swup-scroll-container]`; shouldResetScrollPosition: (trigger: Element) => boolean; markScrollTarget?: boolean; @@ -173,16 +176,26 @@ export default class SwupScrollPlugin extends Plugin { /** * Get the offset for a scroll */ - getOffset = (scrollTarget?: Element, scrollContainer?: Element): number => { - if (!scrollTarget) return 0; + getOffset = (scrollTarget?: Element, scrollContainer?: Element): ScrollPosition => { + if (!scrollTarget) return { top: 0, left: 0 }; + + let offset: number | ScrollPosition; // If options.offset is a function, apply and return it + // Otherwise, use the actual offset value if (typeof this.options.offset === 'function') { - return parseInt(String(this.options.offset(scrollTarget, scrollContainer)), 10); + offset = this.options.offset(scrollTarget, scrollContainer); + } else { + offset = this.options.offset; } - // Otherwise, return the sanitized offset - return parseInt(String(this.options.offset), 10); + // Normalize offset to an object, sharing top value for both top and left if it's a number + if (typeof offset === 'object') { + return offset; + } else { + const top = parseInt(String(offset ?? ''), 10) || 0; + return { top, left: top }; + } }; /** @@ -461,8 +474,15 @@ export default class SwupScrollPlugin extends Plugin { }); scrollActions.forEach(({ top, left, el: scrollContainer }) => { - const offset = this.getOffset(scrollTarget, scrollContainer); - this.scrollTo({ top: top - offset, left: left - offset }, animate, scrollContainer); + const { top: topOffset, left: leftOffset } = this.getOffset( + scrollTarget, + scrollContainer + ); + this.scrollTo( + { top: top - topOffset, left: left - leftOffset }, + animate, + scrollContainer + ); }); } } From cd52ba218fb2b5f7bbcfa392662fdcd4715b2628 Mon Sep 17 00:00:00 2001 From: Philipp Daun Date: Mon, 9 Jun 2025 18:57:54 +0200 Subject: [PATCH 04/16] Ensure scroll position defaults --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1e18870..7739504 100755 --- a/src/index.ts +++ b/src/index.ts @@ -307,7 +307,7 @@ export default class SwupScrollPlugin extends Plugin { // Finally, scroll to either the stored scroll position or to the very top of the page const scrollPositions = this.getCachedScrollPositions(visit.to.url); - const { top, left } = scrollPositions?.window || { top: 0, left: 0 }; + const { top = 0, left = 0 } = scrollPositions?.window || { top: 0, left: 0 }; // Give possible JavaScript time to execute before scrolling requestAnimationFrame(() => this.scrollTo({ top, left }, visit.scroll.animate)); @@ -417,7 +417,7 @@ export default class SwupScrollPlugin extends Plugin { * Scroll to a specific offset, with optional animation. */ scrollTo(position: number | ScrollPosition, animate = true, scrollContainer?: Element): void { - const { top, left } = typeof position === 'number' ? { top: position } : position; + const { top = 0, left = 0 } = typeof position === 'number' ? { top: position } : position; // Create dummy visit // @ts-expect-error: createVisit is currently private, need to make this semi-public somehow @@ -474,7 +474,7 @@ export default class SwupScrollPlugin extends Plugin { }); scrollActions.forEach(({ top, left, el: scrollContainer }) => { - const { top: topOffset, left: leftOffset } = this.getOffset( + const { top: topOffset = 0, left: leftOffset = 0 } = this.getOffset( scrollTarget, scrollContainer ); From 93d691ee8d6b1bca1ed3e8e9eacee456ae335233 Mon Sep 17 00:00:00 2001 From: Rasso Hilber Date: Mon, 9 Jun 2025 20:55:49 +0200 Subject: [PATCH 05/16] Fixtures WIP --- .../e2e/fixtures/src/components/Header.astro | 43 +++++++++ tests/e2e/fixtures/src/components/Nav.astro | 22 +++++ .../e2e/fixtures/src/components/NavLink.astro | 19 ++++ .../components/examples/BothDirections.astro | 2 +- .../src/components/examples/Horizontal.astro | 2 +- .../src/components/examples/Tiles.astro | 87 ++++++++----------- .../src/components/examples/Vertical.astro | 2 +- tests/e2e/fixtures/src/layouts/Layout.astro | 37 ++++---- tests/e2e/fixtures/src/pages/index.astro | 24 ++++- tests/e2e/fixtures/src/styles/global.css | 9 +- 10 files changed, 171 insertions(+), 76 deletions(-) create mode 100644 tests/e2e/fixtures/src/components/Header.astro create mode 100644 tests/e2e/fixtures/src/components/Nav.astro create mode 100644 tests/e2e/fixtures/src/components/NavLink.astro diff --git a/tests/e2e/fixtures/src/components/Header.astro b/tests/e2e/fixtures/src/components/Header.astro new file mode 100644 index 0000000..13bc06f --- /dev/null +++ b/tests/e2e/fixtures/src/components/Header.astro @@ -0,0 +1,43 @@ +--- +import { ArrowUpRightIcon } from 'astro-feather'; + +const links = [ + { + href: 'https://swup.js.org', + title: 'Swup' + }, + { + href: 'https://swup.js.org/plugins/scroll-plugin', + title: 'ScrollPlugin' + } +]; +--- + +
+ + Swup Logo + swup + + +
diff --git a/tests/e2e/fixtures/src/components/Nav.astro b/tests/e2e/fixtures/src/components/Nav.astro new file mode 100644 index 0000000..4bc4e6b --- /dev/null +++ b/tests/e2e/fixtures/src/components/Nav.astro @@ -0,0 +1,22 @@ +--- +import NavLink from './NavLink.astro'; +import type { Props as NavLinkProps } from './NavLink.astro'; + +export type Props = { + links: NavLinkProps[]; +}; + +const { links } = Astro.props; +--- + + diff --git a/tests/e2e/fixtures/src/components/NavLink.astro b/tests/e2e/fixtures/src/components/NavLink.astro new file mode 100644 index 0000000..b2f5038 --- /dev/null +++ b/tests/e2e/fixtures/src/components/NavLink.astro @@ -0,0 +1,19 @@ +--- +import { ArrowRightIcon } from 'astro-feather'; + +export type Props = { + href: string; + title: string; + description: string; +}; + +const { href, title, description } = Astro.props; +--- + + + + {title} + + + {description} + diff --git a/tests/e2e/fixtures/src/components/examples/BothDirections.astro b/tests/e2e/fixtures/src/components/examples/BothDirections.astro index b9890b6..d9c8171 100644 --- a/tests/e2e/fixtures/src/components/examples/BothDirections.astro +++ b/tests/e2e/fixtures/src/components/examples/BothDirections.astro @@ -2,6 +2,6 @@ import Table from "./Table.astro"; --- -
+
diff --git a/tests/e2e/fixtures/src/components/examples/Horizontal.astro b/tests/e2e/fixtures/src/components/examples/Horizontal.astro index 696db3c..7e85e13 100644 --- a/tests/e2e/fixtures/src/components/examples/Horizontal.astro +++ b/tests/e2e/fixtures/src/components/examples/Horizontal.astro @@ -1,6 +1,6 @@ --- import Tiles from "./Tiles.astro"; --- -
+
diff --git a/tests/e2e/fixtures/src/components/examples/Tiles.astro b/tests/e2e/fixtures/src/components/examples/Tiles.astro index 6577d6a..c379ea1 100644 --- a/tests/e2e/fixtures/src/components/examples/Tiles.astro +++ b/tests/e2e/fixtures/src/components/examples/Tiles.astro @@ -1,67 +1,56 @@ --- interface Props { - amount: number; - direction: "vertical" | "horizontal"; - uniqueid: string; - startHue?: number; + amount: number; + direction: 'vertical' | 'horizontal'; + uniqueid: string; + startHue?: number; } const { amount, uniqueid, direction } = Astro.props; const startHue = Astro.props.startHue || 0; const getTestId = (index: number): string => { - if (index === 0) return `${uniqueid}_tile--first`; - if (index === amount - 1) return `${uniqueid}_tile--last`; - return `${uniqueid}_tile--${index + 1}`; + if (index === 0) return `${uniqueid}_tile--first`; + if (index === amount - 1) return `${uniqueid}_tile--last`; + return `${uniqueid}_tile--${index + 1}`; }; const getLabel = (index: number): string => { - if (index === 0) return "first"; - if (index === amount - 1) return "last"; - return String(index + 1); + if (index === 0) return 'first'; + if (index === amount - 1) return 'last'; + return String(index + 1); }; ---
    - { - [...Array(amount)].map((_data, index) => ( -
  • - {getLabel(index)} -
  • - )) - } + { + [...Array(amount)].map((_data, index) => ( +
  • + {getLabel(index)} +
  • + )) + }
diff --git a/tests/e2e/fixtures/src/components/examples/Vertical.astro b/tests/e2e/fixtures/src/components/examples/Vertical.astro index 8d08f16..8725c06 100644 --- a/tests/e2e/fixtures/src/components/examples/Vertical.astro +++ b/tests/e2e/fixtures/src/components/examples/Vertical.astro @@ -2,6 +2,6 @@ import Tiles from "./Tiles.astro"; --- -
+
diff --git a/tests/e2e/fixtures/src/layouts/Layout.astro b/tests/e2e/fixtures/src/layouts/Layout.astro index ffcca65..9f32a3d 100644 --- a/tests/e2e/fixtures/src/layouts/Layout.astro +++ b/tests/e2e/fixtures/src/layouts/Layout.astro @@ -1,10 +1,6 @@ --- import '../styles/global.css'; -import { ArrowLeftIcon } from 'astro-feather'; - -import Head from '../components/Head.astro'; -import Footer from '../components/Footer.astro'; interface Props { title?: string; description?: string; @@ -12,7 +8,12 @@ interface Props { noindex?: boolean; intro?: string; } + +import Head from '../components/Head.astro'; +import Footer from '../components/Footer.astro'; import { SITE_TITLE, SITE_DESCRIPTION } from '../consts'; +import Header from '../components/Header.astro'; + const { title, description, noindex, isFrontPage, intro } = Astro.props; --- @@ -23,27 +24,16 @@ const { title, description, noindex, isFrontPage, intro } = Astro.props; description={description ?? SITE_DESCRIPTION} noindex={noindex} /> - -
+ +
+

{SITE_TITLE}

-
+ +
@@ -51,9 +41,12 @@ const { title, description, noindex, isFrontPage, intro } = Astro.props; import Swup from 'swup'; import ScrollPlugin from '../../../../../src/index.js'; const swup = new Swup({ - plugins: [new ScrollPlugin()] + plugins: [ + new ScrollPlugin({ + markScrollTarget: true + }) + ] }); - console.log(swup); diff --git a/tests/e2e/fixtures/src/pages/index.astro b/tests/e2e/fixtures/src/pages/index.astro index ff1b070..6933130 100644 --- a/tests/e2e/fixtures/src/pages/index.astro +++ b/tests/e2e/fixtures/src/pages/index.astro @@ -5,11 +5,33 @@ import HorizontalExample from '../components/examples/Horizontal.astro'; import BothDirectionsExample from '../components/examples/BothDirections.astro'; import { ArrowRightIcon } from 'astro-feather'; import { SITE_DESCRIPTION } from '../consts.ts'; + +import Nav from '../components/Nav.astro'; ---

{SITE_DESCRIPTION}

+
- - { - [...Array(rows)].map((_data, rowIndex) => ( - - {[...Array(columns)].map((_data, colIndex) => ( - - ))} - - )) - } - +
-
- {getLabel(colIndex, rowIndex)} -
-
+ + { + [...Array(rows)].map((_data, rowIndex) => ( + + {[...Array(columns)].map((_data, colIndex) => ( + + ))} + + )) + } +
+
+ {getLabel(colIndex, rowIndex)} +
+
diff --git a/tests/e2e/fixtures/src/components/examples/Tiles.astro b/tests/e2e/fixtures/src/components/examples/Tiles.astro index c379ea1..2f779db 100644 --- a/tests/e2e/fixtures/src/components/examples/Tiles.astro +++ b/tests/e2e/fixtures/src/components/examples/Tiles.astro @@ -27,6 +27,7 @@ const getLabel = (index: number): string => {
  • {getLabel(index)} diff --git a/tests/e2e/fixtures/src/layouts/Layout.astro b/tests/e2e/fixtures/src/layouts/Layout.astro index 9f32a3d..bbd5d30 100644 --- a/tests/e2e/fixtures/src/layouts/Layout.astro +++ b/tests/e2e/fixtures/src/layouts/Layout.astro @@ -28,8 +28,8 @@ const { title, description, noindex, isFrontPage, intro } = Astro.props;
    -

    {SITE_TITLE}

    - +

    {SITE_TITLE}

    +
    @@ -43,7 +43,8 @@ const { title, description, noindex, isFrontPage, intro } = Astro.props; const swup = new Swup({ plugins: [ new ScrollPlugin({ - markScrollTarget: true + markScrollTarget: true, + offset: () => document.querySelector('#masthead')!.clientHeight + 20 }) ] }); diff --git a/tests/e2e/fixtures/src/pages/index.astro b/tests/e2e/fixtures/src/pages/index.astro index 6933130..a8b6c34 100644 --- a/tests/e2e/fixtures/src/pages/index.astro +++ b/tests/e2e/fixtures/src/pages/index.astro @@ -10,41 +10,46 @@ import Nav from '../components/Nav.astro'; --- -

    {SITE_DESCRIPTION}

    +

    {SITE_DESCRIPTION}