Merge pull request #309 from reuters-graphics/mf-svelte-scroller
Makes `ScrollerBase`
This commit is contained in:
commit
63e8de7966
10 changed files with 294 additions and 9 deletions
5
.changeset/khaki-points-pull.md
Normal file
5
.changeset/khaki-points-pull.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@reuters-graphics/graphics-components': patch
|
||||
---
|
||||
|
||||
Creates ScrollerBase component, which is used in Scroller and can be used to make custom scrollytelling components.
|
||||
|
|
@ -9,16 +9,25 @@
|
|||
}
|
||||
|
||||
let { index, steps, preload = 1, stackBackground = true }: Props = $props();
|
||||
|
||||
function showStep(i: number) {
|
||||
if (preload === 0) return true;
|
||||
if (stackBackground) return i >= 0;
|
||||
return i >= index - preload && i <= index + preload;
|
||||
}
|
||||
|
||||
function isVisible(i: number) {
|
||||
if (stackBackground) return i <= index;
|
||||
return i === index;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each steps as step, i}
|
||||
<!-- Load the step(s) before and after the active one, only -->
|
||||
<!-- Unless stackBackground is true. If so, keep all steps before the current one loaded. -->
|
||||
{#if preload === 0 || (i >= (stackBackground ? 0 : index - preload) && i <= index + preload)}
|
||||
{#if showStep(i)}
|
||||
<div
|
||||
class="step-background step-{i + 1} w-full absolute"
|
||||
class:visible={stackBackground ? i <= index : i === index}
|
||||
class:invisible={stackBackground ? i > index : i !== index}
|
||||
class={`step step-${i + 1} w-full absolute`}
|
||||
class:visible={isVisible(i)}
|
||||
class:invisible={!isVisible(i)}
|
||||
>
|
||||
<step.background {...step.backgroundProps || {}}></step.background>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import * as ScrollerStories from './Scroller.stories.svelte';
|
|||
|
||||
The `Scroller` component creates a basic scrollytelling graphic with layout options.
|
||||
|
||||
> This component is designed to handle most common layouts for scrollytelling. To make something more complex, customise [ScrollerBase](https://github.com/reuters-graphics/graphics-components/blob/main/src/components/Scroller/ScrollerBase/index.svelte), which is a Svelte 5 version of the [svelte-scroller](https://github.com/sveltejs/svelte-scroller).
|
||||
This component is designed to handle most common layouts for scrollytelling. To make something more complex, customise [ScrollerBase](?path=/story/components-graphics-scrollerbase--docs), which is a Svelte 5 version of the [svelte-scroller](https://github.com/sveltejs/svelte-scroller).
|
||||
|
||||
[Demo](?path=/story/components-graphics-scroller--demo)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<!-- @component `Scroller` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-scroller--docs) -->
|
||||
<script lang="ts">
|
||||
import ScrollerBase from './ScrollerBase/index.svelte';
|
||||
import ScrollerBase from '../ScrollerBase/ScrollerBase.svelte';
|
||||
import Background from './Background.svelte';
|
||||
import Foreground from './Foreground.svelte';
|
||||
import Embedded from './Embedded/index.svelte';
|
||||
|
|
|
|||
82
src/components/ScrollerBase/ScrollerBase.mdx
Normal file
82
src/components/ScrollerBase/ScrollerBase.mdx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
import * as ScrollerBaseStories from './ScrollerBase.stories.svelte';
|
||||
|
||||
<Meta of={ScrollerBaseStories} />
|
||||
|
||||
# ScrollerBase
|
||||
|
||||
The `ScrollerBase` component powers the [`Scroller` component](?path=/story/components-graphics-scroller--docs), which creates a basic storytelling graphic with preset layout options. `ScrollerBase` contains the bare minimum code necessary for a scrollytelling section, and allows for customisation beyond what the [`Scroller` component](?path=/story/components-graphics-scroller--docs) allows.
|
||||
|
||||
`ScrollerBase` is a Svelte 5 version of the [svelte-scroller](https://github.com/sveltejs/svelte-scroller).
|
||||
|
||||
> **Important❗:** Make sure the HTML element containing each foreground is a div with the class `step-foreground-container`. If you're modifying this to something else, pass the appropriate selector to the `query` prop.
|
||||
|
||||
[Demo](?path=/story/components-graphics-scrollerbase--demo)
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { ScrollerBase } from '@reuters-graphics/graphics-components';
|
||||
|
||||
// Optional: Bind your own variables to use them in your code.
|
||||
let count = $state(1);
|
||||
let index = $state(0);
|
||||
let offset = $state(0);
|
||||
let progress = $state(0);
|
||||
let top = $state(0.1);
|
||||
let threshold = $state(0.5);
|
||||
let bottom = $state(0.9);
|
||||
</script>
|
||||
|
||||
<ScrollerBase
|
||||
{top}
|
||||
{threshold}
|
||||
{bottom}
|
||||
bind:count
|
||||
bind:index
|
||||
bind:offset
|
||||
bind:progress
|
||||
query="div.step-foreground-container"
|
||||
>
|
||||
{#snippet backgroundSnippet()}
|
||||
<!-- Add custom background HTML or component -->
|
||||
<p class="mb-0">
|
||||
Current step: <strong>{index + 1}/{count}</strong>
|
||||
</p>
|
||||
<progress class="mb-4" value={(index + 1) / count}></progress>
|
||||
|
||||
<p class="mb-0">Offset in current step</p>
|
||||
<progress class="mb-4" value={offset}></progress>
|
||||
|
||||
<p class="mb-0">Total progress</p>
|
||||
<progress class="mb-4" value={progress}></progress>
|
||||
{/snippet}
|
||||
{#snippet foregroundSnippet()}
|
||||
<!-- Add custom foreground HTML or component -->
|
||||
<div class="step-foreground-container">Step 1</div>
|
||||
<div class="step-foreground-container">Step 2</div>
|
||||
<div class="step-foreground-container">Step 3</div>
|
||||
<div class="step-foreground-container">Step 4</div>
|
||||
<div class="step-foreground-container">Step 5</div>
|
||||
{/snippet}
|
||||
</ScrollerBase>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@reuters-graphics/graphics-components/dist/scss/mixins' as mixins;
|
||||
|
||||
.scroller-demo-container {
|
||||
width: mixins.$column-width-normal;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.step-foreground-container {
|
||||
height: 100vh;
|
||||
width: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: 1em;
|
||||
margin: 0 0 2em 0;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
12
src/components/ScrollerBase/ScrollerBase.stories.svelte
Normal file
12
src/components/ScrollerBase/ScrollerBase.stories.svelte
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import ScrollerBase from './ScrollerBase.svelte';
|
||||
import ScrollerDemo from './demo/ScrollerDemo.svelte';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Graphics/ScrollerBase',
|
||||
component: ScrollerBase,
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Demo"><ScrollerDemo /></Story>
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
top = 0,
|
||||
bottom = 1,
|
||||
threshold = 0.5,
|
||||
query = 'section',
|
||||
query = 'div.step-foreground-container',
|
||||
parallax = false,
|
||||
backgroundSnippet,
|
||||
foregroundSnippet,
|
||||
95
src/components/ScrollerBase/demo/DraggableLabel.svelte
Normal file
95
src/components/ScrollerBase/demo/DraggableLabel.svelte
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<script lang="ts">
|
||||
export let value = 0;
|
||||
export let label = 'label';
|
||||
|
||||
function drag(node: HTMLElement) {
|
||||
function handleMousedown() {
|
||||
function handleMousemove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
node.dispatchEvent(
|
||||
new CustomEvent('drag', {
|
||||
detail: {
|
||||
value: event.clientY / window.innerHeight,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function handleMouseup() {
|
||||
window.removeEventListener('mousemove', handleMousemove);
|
||||
window.removeEventListener('mouseup', handleMouseup);
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMousemove);
|
||||
window.addEventListener('mouseup', handleMouseup);
|
||||
}
|
||||
|
||||
node.addEventListener('mousedown', handleMousedown);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('mousedown', handleMousedown);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Round to 2 decimal places
|
||||
function round(value: number): number {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="label"
|
||||
style="top: {value * 100}%"
|
||||
use:drag
|
||||
ondrag={(event: DragEvent) => {
|
||||
const customEvent = event as unknown as { detail: { value: number } };
|
||||
value = customEvent.detail.value;
|
||||
}}
|
||||
role="slider"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="1"
|
||||
aria-valuenow={value}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="drag-target"></div>
|
||||
<hr />
|
||||
<p>{label}: {round(value)}</p>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../../scss/mixins' as mixins;
|
||||
|
||||
.label {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 150px;
|
||||
height: 0;
|
||||
cursor: ns-resize;
|
||||
|
||||
.drag-target {
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
hr {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: red;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
position: absolute;
|
||||
@include mixins.font-sans;
|
||||
@include mixins.font-medium;
|
||||
@include mixins.text-sm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
src/components/ScrollerBase/demo/ScrollerDemo.svelte
Normal file
81
src/components/ScrollerBase/demo/ScrollerDemo.svelte
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<script lang="ts">
|
||||
import ScrollerBase from '../ScrollerBase.svelte';
|
||||
import DraggableLabel from './DraggableLabel.svelte';
|
||||
import BodyText from '../../BodyText/BodyText.svelte';
|
||||
|
||||
let count = $state(1);
|
||||
let index = $state(0);
|
||||
let offset = $state(0);
|
||||
let progress = $state(0);
|
||||
let top = $state(0.1);
|
||||
let threshold = $state(0.5);
|
||||
let bottom = $state(0.9);
|
||||
|
||||
const text =
|
||||
'Read the documentation on the props `progress`, `top`, `threshold`, `bottom` under **Controls** to understand how they work. \n\nAdjust the red sliders on the right to see how changes in these values affect scrolling behaviour.';
|
||||
</script>
|
||||
|
||||
<BodyText {text} />
|
||||
|
||||
<div class="scroller-demo-container">
|
||||
<ScrollerBase
|
||||
{top}
|
||||
{threshold}
|
||||
{bottom}
|
||||
bind:count
|
||||
bind:index
|
||||
bind:offset
|
||||
bind:progress
|
||||
query="div.step-foreground-container"
|
||||
>
|
||||
{#snippet backgroundSnippet()}
|
||||
<p class="mb-0">
|
||||
Current step: <strong>{index + 1}/{count}</strong>
|
||||
</p>
|
||||
<progress class="mb-4" value={(index + 1) / count}></progress>
|
||||
|
||||
<p class="mb-0">Offset in current step</p>
|
||||
<progress class="mb-4" value={offset}></progress>
|
||||
|
||||
<p class="mb-0">Total progress</p>
|
||||
<progress class="mb-4" value={progress}></progress>
|
||||
{/snippet}
|
||||
{#snippet foregroundSnippet()}
|
||||
<div class="step-foreground-container font-medium">Step 1</div>
|
||||
<div class="step-foreground-container font-medium">Step 2</div>
|
||||
<div class="step-foreground-container font-medium">Step 3</div>
|
||||
<div class="step-foreground-container font-medium">Step 4</div>
|
||||
<div class="step-foreground-container font-medium">Step 5</div>
|
||||
{/snippet}
|
||||
</ScrollerBase>
|
||||
</div>
|
||||
|
||||
<BodyText
|
||||
text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
"
|
||||
/>
|
||||
|
||||
<DraggableLabel bind:value={top} label="top" />
|
||||
<DraggableLabel bind:value={threshold} label="threshold" />
|
||||
<DraggableLabel bind:value={bottom} label="bottom" />
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../../scss/mixins' as mixins;
|
||||
|
||||
.scroller-demo-container {
|
||||
width: mixins.$column-width-normal;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.step-foreground-container {
|
||||
height: 100vh;
|
||||
width: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: 1em;
|
||||
margin: 0 0 2em 0;
|
||||
|
||||
// Make it align to the right
|
||||
position: relative;
|
||||
left: 50%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -40,6 +40,7 @@ export { default as SiteFooter } from './components/SiteFooter/SiteFooter.svelte
|
|||
export { default as SiteHeader } from './components/SiteHeader/SiteHeader.svelte';
|
||||
export { default as SiteHeadline } from './components/SiteHeadline/SiteHeadline.svelte';
|
||||
export { default as Spinner } from './components/Spinner/Spinner.svelte';
|
||||
export { default as ScrollerBase } from './components/ScrollerBase/ScrollerBase.svelte';
|
||||
export { default as SponsorshipAd } from './components/AdSlot/SponsorshipAd.svelte';
|
||||
export { default as Table } from './components/Table/Table.svelte';
|
||||
export { default as Theme, themes } from './components/Theme/Theme.svelte';
|
||||
|
|
|
|||
Loading…
Reference in a new issue