Use smooth scroll function to navigate to anchor

Using a eased RAF function to scroll to a specific target
Avoid using `scrollIntoView` or smooth behavior as it doesn't work on Safari and others.
This commit is contained in:
2022-07-11 16:42:28 +02:00
parent f81a468a04
commit ae4ea7f4fa
6 changed files with 73 additions and 7 deletions

View File

@@ -5,6 +5,7 @@
<script lang="ts"> <script lang="ts">
import { cartId } from '$utils/stores/shop' import { cartId } from '$utils/stores/shop'
import { addToCart } from '$utils/functions/shop' import { addToCart } from '$utils/functions/shop'
import { smoothScroll } from '$utils/functions'
// Components // Components
import Button from '$components/atoms/Button.svelte' import Button from '$components/atoms/Button.svelte'
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
@@ -16,7 +17,7 @@
<div class="poster"> <div class="poster">
{#if image} {#if image}
<a href="/shop/poster-{location.slug}#poster" sveltekit:noscroll sveltekit:prefetch> <a href="/shop/poster-{location.slug}" on:click={() => smoothScroll('poster', false)} sveltekit:noscroll sveltekit:prefetch>
<Image <Image
id={image.id} id={image.id}
sizeKey="product" sizeKey="product"
@@ -33,15 +34,16 @@
<div class="buttons"> <div class="buttons">
<Button <Button
size="xsmall" size="xsmall"
url="/shop/poster-{location.slug}#poster" url="/shop/poster-{location.slug}"
text="View" text="View"
on:click={() => setTimeout(() => smoothScroll('poster', false), 1000)}
/> />
<Button <Button
tag="button" tag="button"
size="xsmall" size="xsmall"
on:click={() => addToCart($cartId, product)}
text="Add to cart" text="Add to cart"
color="pink" color="pink"
on:click={() => addToCart($cartId, product)}
/> />
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { getContext } from 'svelte' import { getContext } from 'svelte'
import { shopCurrentProductSlug } from '$utils/stores/shop' import { shopCurrentProductSlug } from '$utils/stores/shop'
import { smoothScroll } from '$utils/functions'
export let isOver: boolean = false export let isOver: boolean = false
@@ -22,6 +23,8 @@
const quickLocationChange = ({ target: { value }}: any) => { const quickLocationChange = ({ target: { value }}: any) => {
const newPath = `/shop/poster-${value}` const newPath = `/shop/poster-${value}`
goto(newPath, { replaceState: true, noscroll: true, keepfocus: true }) goto(newPath, { replaceState: true, noscroll: true, keepfocus: true })
// Scroll to anchor
setTimeout(() => smoothScroll('poster'), 1000)
} }
</script> </script>

View File

@@ -6,6 +6,7 @@
import { getContext, onMount } from 'svelte' import { getContext, onMount } from 'svelte'
import anime, { type AnimeTimelineInstance } from 'animejs' import anime, { type AnimeTimelineInstance } from 'animejs'
import { cartOpen } from '$utils/stores/shop' import { cartOpen } from '$utils/stores/shop'
import { smoothScroll } from '$utils/functions'
// Components // Components
import Image from '$components/atoms/Image.svelte' import Image from '$components/atoms/Image.svelte'
import ButtonCart from '$components/atoms/ButtonCart.svelte' import ButtonCart from '$components/atoms/ButtonCart.svelte'
@@ -106,7 +107,7 @@
<ul> <ul>
{#each shopLocations as { name, slug }} {#each shopLocations as { name, slug }}
<li class:is-active={product && slug === product.location.slug}> <li class:is-active={product && slug === product.location.slug}>
<a href="/shop/poster-{slug}" sveltekit:prefetch sveltekit:noscroll> <a href="/shop/poster-{slug}" on:click={() => smoothScroll('poster')} sveltekit:prefetch sveltekit:noscroll>
{name} {name}
</a> </a>
</li> </li>

View File

@@ -7,7 +7,7 @@
import { getContext, onMount } from 'svelte' import { getContext, onMount } from 'svelte'
import anime, { type AnimeTimelineInstance } from 'animejs' import anime, { type AnimeTimelineInstance } from 'animejs'
import { DELAY } from '$utils/contants' import { DELAY } from '$utils/contants'
import { sleep } from '$utils/functions' import { sleep, smoothScroll } from '$utils/functions'
import { reveal, fade as animeFade } from '$animations/index' import { reveal, fade as animeFade } from '$animations/index'
// Components // Components
import Metas from '$components/Metas.svelte' import Metas from '$components/Metas.svelte'
@@ -95,7 +95,7 @@
{settings.description} {settings.description}
</p> </p>
<Button text="Explore locations" url="#locations"> <Button text="Explore locations" on:click={() => smoothScroll('locations')}>
<IconEarth animate={true} /> <IconEarth animate={true} />
</Button> </Button>
</div> </div>
@@ -111,10 +111,11 @@
<ListCTAs> <ListCTAs>
<li> <li>
<BoxCTA <BoxCTA
url="{$page.url.pathname}#locations" url="{$page.url.pathname}"
icon="globe" icon="globe"
label="Discover locations" label="Discover locations"
alt="Globe" alt="Globe"
on:click={() => smoothScroll('locations')}
/> />
</li> </li>
<li> <li>

View File

@@ -0,0 +1,9 @@
/**
* Ease: In Out Quart
*/
export const easeInOutQuart = (t: number, b: number, c: number, d: number) => {
t /= d/2
if (t < 1) return c/2 * t * t * t * t + b
t -= 2
return -c / 2 * (t * t * t * t - 2) + b
}

View File

@@ -1,3 +1,6 @@
import { easeInOutQuart } from './easing'
/** /**
* Throttle function * Throttle function
*/ */
@@ -149,4 +152,51 @@ export const scrollToTop = (delay?: number) => {
} else { } else {
scroll() scroll()
} }
}
/**
* Smooth Scroll to an element
* @description Promised based
* @url https://www.youtube.com/watch?v=oUSvlrDTLi4
*/
const smoothScrollPromise = (target: HTMLElement, duration: number = 1600): Promise<void> => {
const position = target.getBoundingClientRect().top + 1
const startPosition = window.scrollY
const distance = position - startPosition
let startTime: number = null
// Return Promise
return new Promise((resolve) => {
if (!(target instanceof Element)) throw new TypeError('Argument 1 must be an Element')
if (typeof window === 'undefined') return
// Scroll to animation
const animation = (currentTime: number) => {
if (startTime === null) startTime = currentTime
const timeElapsed = currentTime - startTime
// Create easing value
const easedYPosition = easeInOutQuart(timeElapsed, startPosition, distance, duration)
// Scroll to Y position
window.scrollTo(0, easedYPosition)
// Loop or end animation
if (timeElapsed < duration) {
requestAnimationFrame(animation)
} else {
return resolve()
}
}
requestAnimationFrame(animation)
})
}
export const smoothScroll = async (hash: string, changeHash: boolean = true, callback?: Function) => {
const target = document.getElementById(hash)
smoothScrollPromise(target).then(() => {
if (changeHash) {
location.hash = hash
}
callback && callback()
})
} }