making our own scroller base
This commit is contained in:
parent
713cadabc0
commit
4ffe0ebf99
5 changed files with 445 additions and 185 deletions
|
|
@ -16,4 +16,4 @@ The `Scroller` component TK
|
|||
<Scroller />
|
||||
```
|
||||
|
||||
<Canvas of={ScrollerStories.Demo} />
|
||||
<Canvas of={ScrollerStories.Test} />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import Scroller from './Scroller.svelte';
|
||||
import Scroller from './ScrollerStash.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Graphics/Scroller',
|
||||
|
|
@ -63,7 +63,9 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<Story
|
||||
<Story name="Test"></Story>
|
||||
|
||||
<!-- <Story
|
||||
name="Demo"
|
||||
args={{
|
||||
steps: [
|
||||
|
|
@ -153,4 +155,4 @@
|
|||
embeddedLayout: 'fb',
|
||||
embedded: false,
|
||||
}}
|
||||
/>
|
||||
/> -->
|
||||
|
|
|
|||
|
|
@ -1,189 +1,28 @@
|
|||
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
|
||||
<!-- @component `Scroller` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-scroller--docs) -->
|
||||
<script lang="ts">
|
||||
// @ts-ignore no types
|
||||
import SvelteScroller from '@sveltejs/svelte-scroller';
|
||||
import Background from './Background.svelte';
|
||||
import Foreground from './Foreground.svelte';
|
||||
import Embedded from './Embedded/index.svelte';
|
||||
import Block from '../Block/Block.svelte';
|
||||
<script>
|
||||
import ScrollerBase from './ScrollerBase/index.svelte';
|
||||
|
||||
// Types
|
||||
import type {
|
||||
ContainerWidth,
|
||||
ForegroundPosition,
|
||||
ScrollerStep,
|
||||
} from '../@types/global';
|
||||
|
||||
interface Props {
|
||||
/** ID of the scroller container */
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* An array of step objects that define the steps in your scroller.
|
||||
*
|
||||
* Each step object in the array can have:
|
||||
*
|
||||
* - `background` A background component. **REQUIRED**
|
||||
* - `backgroundProps` Optional props for background component.
|
||||
* - `foreground` A component or markdown-formatted string. **REQUIRED**
|
||||
* - `foregroundProps` Optional props for foreground component.
|
||||
* - `altText` Optional alt text for the background, read aloud after the foregroud text. You can add it to each step or just to the first step to describe the entire scroller graphic. **RECOMMENDED**
|
||||
*
|
||||
*/
|
||||
steps: ScrollerStep[];
|
||||
/** Width of the background */
|
||||
backgroundWidth?: ContainerWidth;
|
||||
/** Position of the foreground */
|
||||
foregroundPosition?: ForegroundPosition;
|
||||
/**
|
||||
* Whether previous background steps should stack below the current one.
|
||||
*
|
||||
* - `true` _default_ Background graphics from previous steps will remain visible below the active one, allowing you to stack graphics with transparent backgrounds.
|
||||
* - `false` Only the background graphic from the current step will show and backgrounds from previous steps are hidden.
|
||||
*/
|
||||
stackBackground?: boolean;
|
||||
/**
|
||||
* How many background steps to load before and after the currently active one, effectively lazy-loading them.
|
||||
*
|
||||
* Setting to `0` disables lazy-loading and loads all backgrounds at once.
|
||||
*/
|
||||
preload?: number;
|
||||
/** Setting to `true` will unroll the scroll experience into a flat layout */
|
||||
embedded?: boolean;
|
||||
/**
|
||||
* Layout order when `embedded` is `true`.
|
||||
*
|
||||
* - `fb` _default_ Foreground then background
|
||||
* - `bf` Background then foreground
|
||||
*
|
||||
*/
|
||||
embeddedLayout?: 'fb' | 'bf';
|
||||
/**
|
||||
* Threshold prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
threshold?: number;
|
||||
/**
|
||||
* Top prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
top?: number;
|
||||
/**
|
||||
* Bottom prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
bottom?: number;
|
||||
/**
|
||||
* Parallax prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
parallax?: boolean;
|
||||
/** Set a class to target with SCSS */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
id = '',
|
||||
steps,
|
||||
backgroundWidth = 'fluid',
|
||||
foregroundPosition = 'middle',
|
||||
stackBackground = true,
|
||||
preload = 1,
|
||||
embedded = false,
|
||||
embeddedLayout = 'fb',
|
||||
threshold = 0.5,
|
||||
top = 0,
|
||||
bottom = 1,
|
||||
parallax = false,
|
||||
class: cls = '',
|
||||
}: Props = $props();
|
||||
|
||||
let index = $state(0);
|
||||
let offset = $state(0);
|
||||
let progress = $state(0);
|
||||
let index, offset, progress;
|
||||
</script>
|
||||
|
||||
{#if !embedded}
|
||||
<Block width="fluid" class="scroller-container fmy-6 {cls}" {id}>
|
||||
<SvelteScroller
|
||||
bind:index
|
||||
bind:offset
|
||||
bind:progress
|
||||
{threshold}
|
||||
{top}
|
||||
{bottom}
|
||||
{parallax}
|
||||
query="div.step-foreground-container"
|
||||
>
|
||||
<div
|
||||
slot="background"
|
||||
class="background min-h-screen relative p-0 flex justify-center"
|
||||
class:right={foregroundPosition === 'left opposite'}
|
||||
class:left={foregroundPosition === 'right opposite'}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="scroller-graphic-well w-full">
|
||||
<Block
|
||||
width={backgroundWidth}
|
||||
class="background-container step-{index +
|
||||
1} my-0 min-h-screen flex justify-center items-center relative"
|
||||
>
|
||||
<Background {index} {steps} {preload} {stackBackground} />
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollerBase top={0.2} bottom={0.8} bind:index bind:offset bind:progress>
|
||||
<div slot="background">
|
||||
<p>
|
||||
This is the background content. It will stay fixed in place while the
|
||||
foreground scrolls over the top.
|
||||
</p>
|
||||
|
||||
<div slot="foreground" class="foreground {foregroundPosition} w-full">
|
||||
<Foreground {steps} />
|
||||
</div>
|
||||
</SvelteScroller>
|
||||
</Block>
|
||||
{:else}
|
||||
<Block width="widest" class="scroller-container embedded" {id}>
|
||||
<Embedded {steps} {embeddedLayout} {backgroundWidth} />
|
||||
</Block>
|
||||
{/if}
|
||||
<p>Section {index + 1} is currently active.</p>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.background {
|
||||
&.left {
|
||||
width: 50%;
|
||||
float: left;
|
||||
@media (max-width: 1200px) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
&.right {
|
||||
width: 50%;
|
||||
float: right;
|
||||
@media (max-width: 1200px) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
<div slot="foreground">
|
||||
<section>This is the first section.</section>
|
||||
<section>This is the second section.</section>
|
||||
<section>This is the third section.</section>
|
||||
</div>
|
||||
</ScrollerBase>
|
||||
|
||||
div.scroller-graphic-well {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
div.foreground {
|
||||
&.right {
|
||||
width: 50%;
|
||||
float: right;
|
||||
@media (max-width: 1200px) {
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
width: 50%;
|
||||
float: left;
|
||||
@media (max-width: 1200px) {
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
<style>
|
||||
section {
|
||||
height: 80vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
230
src/components/Scroller/ScrollerBase/index.svelte
Normal file
230
src/components/Scroller/ScrollerBase/index.svelte
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
<script context="module">
|
||||
const handlers = [];
|
||||
let manager;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const run_all = () => handlers.forEach((fn) => fn());
|
||||
|
||||
window.addEventListener('scroll', run_all);
|
||||
window.addEventListener('resize', run_all);
|
||||
}
|
||||
|
||||
if (typeof IntersectionObserver !== 'undefined') {
|
||||
const map = new Map();
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
const update = map.get(entry.target);
|
||||
const index = handlers.indexOf(update);
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
if (index === -1) handlers.push(update);
|
||||
} else {
|
||||
update();
|
||||
if (index !== -1) handlers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '400px 0px', // TODO why 400?
|
||||
}
|
||||
);
|
||||
|
||||
manager = {
|
||||
add: ({ outer, update }) => {
|
||||
const { top, bottom } = outer.getBoundingClientRect();
|
||||
|
||||
if (top < window.innerHeight && bottom > 0) handlers.push(update);
|
||||
|
||||
map.set(outer, update);
|
||||
observer.observe(outer);
|
||||
},
|
||||
|
||||
remove: ({ outer, update }) => {
|
||||
const index = handlers.indexOf(update);
|
||||
if (index !== -1) handlers.splice(index, 1);
|
||||
|
||||
map.delete(outer);
|
||||
observer.unobserve(outer);
|
||||
},
|
||||
};
|
||||
} else {
|
||||
manager = {
|
||||
add: ({ update }) => {
|
||||
handlers.push(update);
|
||||
},
|
||||
|
||||
remove: ({ update }) => {
|
||||
const index = handlers.indexOf(update);
|
||||
if (index !== -1) handlers.splice(index, 1);
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// config
|
||||
export let top = 0;
|
||||
export let bottom = 1;
|
||||
export let threshold = 0.5;
|
||||
export let query = 'section';
|
||||
export let parallax = false;
|
||||
|
||||
// bindings
|
||||
export let index = 0;
|
||||
export let count = 0;
|
||||
export let offset = 0;
|
||||
export let progress = 0;
|
||||
export let visible = false;
|
||||
|
||||
let outer;
|
||||
let foreground;
|
||||
let background;
|
||||
let left;
|
||||
let sections;
|
||||
let wh = 0;
|
||||
let fixed;
|
||||
let offset_top = 0;
|
||||
let width = 1;
|
||||
let height;
|
||||
let inverted;
|
||||
|
||||
$: top_px = Math.round(top * wh);
|
||||
$: bottom_px = Math.round(bottom * wh);
|
||||
$: threshold_px = Math.round(threshold * wh);
|
||||
|
||||
$: top, bottom, threshold, parallax, update();
|
||||
|
||||
$: style = `
|
||||
position: ${fixed ? 'fixed' : 'absolute'};
|
||||
top: 0;
|
||||
transform: translate(0, ${offset_top}px);
|
||||
z-index: ${inverted ? 3 : 1};
|
||||
`;
|
||||
|
||||
$: widthStyle = fixed ? `width:${width}px;` : '';
|
||||
|
||||
onMount(() => {
|
||||
sections = foreground.querySelectorAll(query);
|
||||
count = sections.length;
|
||||
|
||||
update();
|
||||
|
||||
const scroller = { outer, update };
|
||||
|
||||
manager.add(scroller);
|
||||
return () => manager.remove(scroller);
|
||||
});
|
||||
|
||||
function update() {
|
||||
if (!foreground) return;
|
||||
|
||||
// re-measure outer container
|
||||
const bcr = outer.getBoundingClientRect();
|
||||
left = bcr.left;
|
||||
width = bcr.right - left;
|
||||
|
||||
// determine fix state
|
||||
const fg = foreground.getBoundingClientRect();
|
||||
const bg = background.getBoundingClientRect();
|
||||
|
||||
visible = fg.top < wh && fg.bottom > 0;
|
||||
|
||||
const foreground_height = fg.bottom - fg.top;
|
||||
const background_height = bg.bottom - bg.top;
|
||||
|
||||
const available_space = bottom_px - top_px;
|
||||
progress = (top_px - fg.top) / (foreground_height - available_space);
|
||||
|
||||
if (progress <= 0) {
|
||||
offset_top = 0;
|
||||
fixed = false;
|
||||
} else if (progress >= 1) {
|
||||
offset_top =
|
||||
parallax ?
|
||||
foreground_height - background_height
|
||||
: foreground_height - available_space;
|
||||
fixed = false;
|
||||
} else {
|
||||
offset_top =
|
||||
parallax ?
|
||||
Math.round(top_px - progress * (background_height - available_space))
|
||||
: top_px;
|
||||
fixed = true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const { top } = section.getBoundingClientRect();
|
||||
|
||||
const next = sections[i + 1];
|
||||
const bottom = next ? next.getBoundingClientRect().top : fg.bottom;
|
||||
|
||||
offset = (threshold_px - top) / (bottom - top);
|
||||
if (bottom >= threshold_px) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={wh} />
|
||||
|
||||
<svelte-scroller-outer bind:this={outer}>
|
||||
<svelte-scroller-background-container
|
||||
class="background-container"
|
||||
style="{style}{widthStyle}"
|
||||
>
|
||||
<svelte-scroller-background bind:this={background}>
|
||||
<slot name="background"></slot>
|
||||
</svelte-scroller-background>
|
||||
</svelte-scroller-background-container>
|
||||
|
||||
<svelte-scroller-foreground bind:this={foreground}>
|
||||
<slot name="foreground"></slot>
|
||||
</svelte-scroller-foreground>
|
||||
</svelte-scroller-outer>
|
||||
|
||||
<style>
|
||||
svelte-scroller-outer {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
svelte-scroller-background {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
svelte-scroller-foreground {
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
svelte-scroller-foreground::after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
svelte-scroller-background-container {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
pointer-events: none;
|
||||
/* height: 100%; */
|
||||
|
||||
/* in theory this helps prevent jumping */
|
||||
will-change: transform;
|
||||
/* -webkit-transform: translate3d(0, 0, 0);
|
||||
-moz-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0); */
|
||||
}
|
||||
</style>
|
||||
189
src/components/Scroller/ScrollerStash.svelte
Normal file
189
src/components/Scroller/ScrollerStash.svelte
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
|
||||
<!-- @component `Scroller` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-scroller--docs) -->
|
||||
<script lang="ts">
|
||||
// @ts-ignore no types
|
||||
import SvelteScroller from '@sveltejs/svelte-scroller';
|
||||
import Background from './Background.svelte';
|
||||
import Foreground from './Foreground.svelte';
|
||||
import Embedded from './Embedded/index.svelte';
|
||||
import Block from '../Block/Block.svelte';
|
||||
|
||||
// Types
|
||||
import type {
|
||||
ContainerWidth,
|
||||
ForegroundPosition,
|
||||
ScrollerStep,
|
||||
} from '../@types/global';
|
||||
|
||||
interface Props {
|
||||
/** ID of the scroller container */
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* An array of step objects that define the steps in your scroller.
|
||||
*
|
||||
* Each step object in the array can have:
|
||||
*
|
||||
* - `background` A background component. **REQUIRED**
|
||||
* - `backgroundProps` Optional props for background component.
|
||||
* - `foreground` A component or markdown-formatted string. **REQUIRED**
|
||||
* - `foregroundProps` Optional props for foreground component.
|
||||
* - `altText` Optional alt text for the background, read aloud after the foregroud text. You can add it to each step or just to the first step to describe the entire scroller graphic. **RECOMMENDED**
|
||||
*
|
||||
*/
|
||||
steps: ScrollerStep[];
|
||||
/** Width of the background */
|
||||
backgroundWidth?: ContainerWidth;
|
||||
/** Position of the foreground */
|
||||
foregroundPosition?: ForegroundPosition;
|
||||
/**
|
||||
* Whether previous background steps should stack below the current one.
|
||||
*
|
||||
* - `true` _default_ Background graphics from previous steps will remain visible below the active one, allowing you to stack graphics with transparent backgrounds.
|
||||
* - `false` Only the background graphic from the current step will show and backgrounds from previous steps are hidden.
|
||||
*/
|
||||
stackBackground?: boolean;
|
||||
/**
|
||||
* How many background steps to load before and after the currently active one, effectively lazy-loading them.
|
||||
*
|
||||
* Setting to `0` disables lazy-loading and loads all backgrounds at once.
|
||||
*/
|
||||
preload?: number;
|
||||
/** Setting to `true` will unroll the scroll experience into a flat layout */
|
||||
embedded?: boolean;
|
||||
/**
|
||||
* Layout order when `embedded` is `true`.
|
||||
*
|
||||
* - `fb` _default_ Foreground then background
|
||||
* - `bf` Background then foreground
|
||||
*
|
||||
*/
|
||||
embeddedLayout?: 'fb' | 'bf';
|
||||
/**
|
||||
* Threshold prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
threshold?: number;
|
||||
/**
|
||||
* Top prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
top?: number;
|
||||
/**
|
||||
* Bottom prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
bottom?: number;
|
||||
/**
|
||||
* Parallax prop passed to [svelte-scroller](https://github.com/sveltejs/svelte-scroller#parameters)
|
||||
*/
|
||||
parallax?: boolean;
|
||||
/** Set a class to target with SCSS */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
id = '',
|
||||
steps,
|
||||
backgroundWidth = 'fluid',
|
||||
foregroundPosition = 'middle',
|
||||
stackBackground = true,
|
||||
preload = 1,
|
||||
embedded = false,
|
||||
embeddedLayout = 'fb',
|
||||
threshold = 0.5,
|
||||
top = 0,
|
||||
bottom = 1,
|
||||
parallax = false,
|
||||
class: cls = '',
|
||||
}: Props = $props();
|
||||
|
||||
let index = $state(0);
|
||||
let offset = $state(0);
|
||||
let progress = $state(0);
|
||||
</script>
|
||||
|
||||
{#if !embedded}
|
||||
<Block width="fluid" class="scroller-container fmy-6 {cls}" {id}>
|
||||
<SvelteScroller
|
||||
bind:index
|
||||
bind:offset
|
||||
bind:progress
|
||||
{threshold}
|
||||
{top}
|
||||
{bottom}
|
||||
{parallax}
|
||||
query="div.step-foreground-container"
|
||||
>
|
||||
<div
|
||||
slot="background"
|
||||
class="background min-h-screen relative p-0 flex justify-center"
|
||||
class:right={foregroundPosition === 'left opposite'}
|
||||
class:left={foregroundPosition === 'right opposite'}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="scroller-graphic-well w-full">
|
||||
<Block
|
||||
width={backgroundWidth}
|
||||
class="background-container step-{index +
|
||||
1} my-0 min-h-screen flex justify-center items-center relative"
|
||||
>
|
||||
<Background {index} {steps} {preload} {stackBackground} />
|
||||
</Block>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="foreground" class="foreground {foregroundPosition} w-full">
|
||||
<Foreground {steps} />
|
||||
</div>
|
||||
</SvelteScroller>
|
||||
</Block>
|
||||
{:else}
|
||||
<Block width="widest" class="scroller-container embedded" {id}>
|
||||
<Embedded {steps} {embeddedLayout} {backgroundWidth} />
|
||||
</Block>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
div.background {
|
||||
&.left {
|
||||
width: 50%;
|
||||
float: left;
|
||||
@media (max-width: 1200px) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
&.right {
|
||||
width: 50%;
|
||||
float: right;
|
||||
@media (max-width: 1200px) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
|
||||
div.scroller-graphic-well {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
div.foreground {
|
||||
&.right {
|
||||
width: 50%;
|
||||
float: right;
|
||||
@media (max-width: 1200px) {
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
width: 50%;
|
||||
float: left;
|
||||
@media (max-width: 1200px) {
|
||||
width: 100%;
|
||||
float: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue