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

View File

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

View File

@@ -6,6 +6,7 @@
import { getContext, onMount } from 'svelte'
import anime, { type AnimeTimelineInstance } from 'animejs'
import { cartOpen } from '$utils/stores/shop'
import { smoothScroll } from '$utils/functions'
// Components
import Image from '$components/atoms/Image.svelte'
import ButtonCart from '$components/atoms/ButtonCart.svelte'
@@ -106,7 +107,7 @@
<ul>
{#each shopLocations as { name, 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}
</a>
</li>

View File

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