Merge pull request #309 from reuters-graphics/mf-svelte-scroller

Makes `ScrollerBase`
This commit is contained in:
MinamiFunakoshiTR 2025-05-27 10:04:40 -05:00 committed by GitHub
commit 63e8de7966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 294 additions and 9 deletions

View 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.

View file

@ -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>

View file

@ -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)

View file

@ -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';

View 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>
```

View 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>

View file

@ -117,7 +117,7 @@
top = 0,
bottom = 1,
threshold = 0.5,
query = 'section',
query = 'div.step-foreground-container',
parallax = false,
backgroundSnippet,
foregroundSnippet,

View 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>

View 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>

View file

@ -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';