403 lines
13 KiB
Text
403 lines
13 KiB
Text
import { Meta } from '@storybook/blocks';
|
|
|
|
import * as HorizontalScrollerStories from './HorizontalScroller.stories.svelte';
|
|
|
|
import IllustratorScreenshot from './assets/illustrator.png';
|
|
|
|
<Meta of={HorizontalScrollerStories} />
|
|
|
|
# HorizontalScroller
|
|
|
|
The `HorizontalScroller` component is helpful in making horizontal scrolling sections that respond to vertical scroll input. It is flexible in a way that it can horizontally scroll any children content wider than 100vw from one end to the other.
|
|
|
|
To scroll any DOM layout wider than the viewport, wrap the content inside the `HorizontalScroller` component. The component will take care of the rest.
|
|
|
|
## Basic demo
|
|
|
|
To use the `HorizontalScroller` component, import it and provide the children content to scroll. The scroll height defaults to `200lvh`, but you can adjust this to any valid CSS height value such as `1200px` or `200lvh` with the `height` prop.
|
|
|
|
> 💡TIP: Use `lvh` or `svh` units instead of `vh` unit for the height, as [these units](https://www.w3.org/TR/css-values-4/#large-viewport-size) are more reliable on mobile or other devices where elements such as the address bar toggle between being shown and hidden.
|
|
|
|
[Demo](?path=/story/components-graphics-horizontalscroller--demo)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
|
</script>
|
|
|
|
<!-- Optionally set `height` to adjust scroll height -->
|
|
<HorizontalScroller height="400lvh">
|
|
<div style="width: 200vw; height: 100lvh;">
|
|
<!-- Content wider than 100vw -->
|
|
<!-- Only the top 100lvh will be visible -->
|
|
<img
|
|
src="path/to/wide-image.jpg"
|
|
alt="alt text"
|
|
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
|
|
/>
|
|
</div>
|
|
</HorizontalScroller>
|
|
```
|
|
|
|
## With stops
|
|
|
|
The `HorizontalScroller` also allows you to define a set of points to stop or slow down the scrolling at specific intervals using the `stops` prop. This is useful for creating step-based horizontal scrolling experiences.
|
|
|
|
The `scrubbed` prop can be used to define whether the scrolling experience should be smooth or tied directly to the scroll position. Setting `scrubbed` to `true` will make the horizontal scroll position directly correspond to the vertical scroll position, while setting it to `false` will create a smooth scrolling effect.
|
|
|
|
If `scrubbed` is set to `false` and `stops` are defined, the scroller will transition smoothly to the next stop when the `Mapped Progress` reaches the midpoint between the two stops. The transition speed is controlled by the `duration` prop (in milliseconds) and the `easing` prop (which accepts any easing function from `svelte/easing` or a custom function based on signature `(t: number) => number`).
|
|
|
|
If `scrubbed` is set to `true` and `stops` are defined, all the stops are traversed at equal distance but based on the easing function provided.
|
|
|
|
Use `showDebugInfo` prop to visualize the scroll progress and other useful debug information. The `Progress` indicates the vertical progress with values in the range 0...1 indicating the content being locked or a user-fed value to control the horizontal scroll position. The `Mapped Progress` value indicates the vertical progress mapped to mappedStart and mappedEnd values. By default these are 0 and 1 respectively. Finally, the `Eased Progress` value indicates the horizontal scroll progress after applying stops and easing (if any). `Eased Progress` accurately reflects the transition of horizontal scroll position.
|
|
|
|
Feel free to toggle `scrubbed` prop here to see the difference.
|
|
|
|
[Demo](?path=/story/components-graphics-horizontalscroller--demo)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
|
import { quartInOut } from 'svelte/easing';
|
|
</script>
|
|
|
|
<HorizontalScroller
|
|
height="200lvh"
|
|
stops={[0.2, 0.5, 0.6, 0.7]}
|
|
duration={400}
|
|
scrubbed={false}
|
|
easing={quartInOut}
|
|
showDebugInfo
|
|
direction="right"
|
|
>
|
|
<div style="width: 200vw; height: 100lvh;">
|
|
<!-- Content wider than 100vw -->
|
|
<!-- Only the top 100lvh will be visible -->
|
|
<img
|
|
src="path/to/wide-image.jpg"
|
|
alt="alt text"
|
|
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
|
|
/>
|
|
</div>
|
|
</HorizontalScroller>
|
|
```
|
|
|
|
## Extended boundary
|
|
|
|
`HorizontalScroller` also provides `mappedStart` and `mappedEnd` props to extend the horizontal scroll boundaries beyond the default 0 to 1 range. This is useful when you want to create an overscroll effect or have more control over the horizontal scroll range. By default these values are set to 0 and 1 respectively.
|
|
|
|
[Demo](?path=/story/components-graphics-horizontalscroller--extended-boundary)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
|
import { quartInOut } from 'svelte/easing';
|
|
</script>
|
|
|
|
<HorizontalScroller
|
|
height="200lvh"
|
|
mappedStart={-0.5}
|
|
mappedEnd={1.5}
|
|
stops={[0, 1]}
|
|
scrubbed={true}
|
|
easing={quartInOut}
|
|
direction="right"
|
|
showDebugInfo
|
|
>
|
|
<div style="width: 200vw; height: 100lvh;">
|
|
<!-- Content wider than 100vw -->
|
|
<!-- Only the top 100lvh will be visible -->
|
|
<img
|
|
src="path/to/wide-image.jpg"
|
|
alt="alt text"
|
|
style="width: 100%; height: 100%; object-fit: cover; padding: 0; margin: 0;"
|
|
/>
|
|
</div>
|
|
</HorizontalScroller>
|
|
```
|
|
|
|
## With custom child components
|
|
|
|
You can create a horizontal stack of any components and pass it as children to the `HorizontalScroller`. Here's an example of using `DatawrapperChart`, `Headline` and ai2svelte components inside the scroller.
|
|
|
|
[Demo](?path=/story/components-graphics-horizontalscroller--custom-children)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import {
|
|
Block,
|
|
DatawrapperChart,
|
|
Headline,
|
|
HorizontalScroller,
|
|
} from '@reuters-graphics/graphics-components';
|
|
import AiChart from './ai2svelte/ai-chart.svelte';
|
|
import { quartInOut } from 'svelte/easing';
|
|
</script>
|
|
|
|
<HorizontalScroller
|
|
height="200lvh"
|
|
stops={[0.2, 0.5, 0.6, 0.7]}
|
|
duration={400}
|
|
scrubbed={false}
|
|
easing={quartInOut}
|
|
direction="right"
|
|
showDebugInfo
|
|
>
|
|
<div id="horizontal-stack">
|
|
<div style="width: 100vw;">
|
|
<DatawrapperChart
|
|
title="Global abortion access"
|
|
ariaLabel="map"
|
|
id="abortion-rights-map"
|
|
src="https://graphics.reuters.com/USA-ABORTION/lgpdwggnwvo/media-embed.html"
|
|
frameTitle=""
|
|
scrolling="no"
|
|
textWidth="normal"
|
|
width="wider"
|
|
/>
|
|
</div>
|
|
<div style="width: 100vw;">
|
|
<Headline
|
|
hed="Reuters Graphics Interactive"
|
|
dek="The beginning of a beautiful page"
|
|
section="World News"
|
|
/>
|
|
</div>
|
|
<div style="width: 100vw;">
|
|
<Block width="normal">
|
|
<AiChart />
|
|
</Block>
|
|
</div>
|
|
</div>
|
|
</HorizontalScroller>
|
|
|
|
<style lang="scss">
|
|
#horizontal-stack {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-around;
|
|
gap: 10vw;
|
|
height: 100%;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## With ai2svelte components
|
|
|
|
With ai2svelte v1.0.3 onwards, you can export your ai2svelte graphic with a wider-than-viewport layout and use it directly inside the `HorizontalScroller` component to create horizontal scrolling graphics.
|
|
|
|
To do that, follow these steps:
|
|
|
|
1. In Illustrator, rename your artboard with a tag indicating breakpoint width for that artboard to be visible on page. For example, to make the XL artboard visible on viewports wider than 1200px, rename the artboard to `xl:1200`. You can have more than one artboard with different breakpoint widths.
|
|
2. In ai2svelte settings, set these properties and run ai2svelte to export the component.
|
|
|
|
```yaml
|
|
include_resizer_css: false
|
|
respect_height: true
|
|
allow_overflow: true
|
|
```
|
|
|
|
<img
|
|
src={IllustratorScreenshot}
|
|
alt="Screenshot showing Illustrator document with artboard panel"
|
|
/>
|
|
|
|
[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
|
import AiGraphic from './ai2svelte/ai-graphic.svelte';
|
|
import { sineInOut } from 'svelte/easing';
|
|
// If using with the graphics kit
|
|
import { assets } from '$app/paths';
|
|
</script>
|
|
|
|
<HorizontalScroller
|
|
width="fluid"
|
|
height="800lvh"
|
|
direction="right"
|
|
easing={sineInOut}
|
|
showDebugInfo
|
|
>
|
|
<AiGraphic assetsPath={assets} />
|
|
</HorizontalScroller>
|
|
```
|
|
|
|
## With ai2svelte components (advanced)
|
|
|
|
Binding the `progress` can be useful to even transition tagged content inside the ai2svelte graphic as part of the horizontal scrolling experience. For example, caption boxes exported as `htext` tagged layers can be animated to fade in/out or move in/out of view based on the scroll progress. Or one could even use tagged `png` layers to create parallax effects.
|
|
|
|
[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte-advanced)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import { HorizontalScroller } from '@reuters-graphics/graphics-components';
|
|
import AiGraphic from './ai2svelte/ai-graphic.svelte';
|
|
import { sineInOut } from 'svelte/easing';
|
|
// If using with the graphics kit
|
|
import { assets } from '$app/paths';
|
|
|
|
// bind progress for advanced interactivity
|
|
let progress: number = $state(0);
|
|
let pngLayer: HTMLElement | null;
|
|
let captions: HTMLElement[] | null;
|
|
let threshold = 0.8;
|
|
let screenWidth: number = $state(0);
|
|
|
|
function handleScroll() {
|
|
// to create the parallax movement
|
|
if (pngLayer) {
|
|
pngLayer.style.transform = `translateX(${map(progress, 0, 1, -400, 400)}px)`;
|
|
}
|
|
|
|
// for each caption, checks if position of the caption < 80vw
|
|
// if it is, show it
|
|
// if not, hide it
|
|
if (captions?.length) {
|
|
captions.forEach((caption) => {
|
|
let captionWidth = caption.getBoundingClientRect().width;
|
|
let captionMidpoint =
|
|
caption.getBoundingClientRect().left + captionWidth / 2;
|
|
|
|
if (
|
|
captionMidpoint < screenWidth * threshold &&
|
|
caption.style.opacity !== '1'
|
|
) {
|
|
caption.style.opacity = '1';
|
|
} else if (
|
|
captionMidpoint > screenWidth * threshold &&
|
|
caption.style.opacity !== '0'
|
|
) {
|
|
caption.style.opacity = '0';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// refetch new captions and png image
|
|
// every time the artboard changes
|
|
function onArtboardChange(artboard: HTMLElement) {
|
|
pngLayer = artboard.querySelector('.g-png-layer-overlay');
|
|
captions = Array.from(artboard.querySelectorAll('.g-captions'));
|
|
|
|
if (pngLayer) {
|
|
window.removeEventListener('scroll', handleScroll);
|
|
window.addEventListener('scroll', handleScroll, {
|
|
passive: true,
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<HorizontalScroller
|
|
width="fluid"
|
|
height="800lvh"
|
|
direction="right"
|
|
bind:progress
|
|
easing={sineInOut}
|
|
showDebugInfo
|
|
>
|
|
<AiGraphic assetsPath={assets} {onArtboardChange} />
|
|
</HorizontalScroller>
|
|
|
|
<style lang="scss">
|
|
:global(.scroller-caption) {
|
|
padding: 1rem;
|
|
margin: 0;
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
border-radius: 8px;
|
|
filter: drop-shadow(0px 2px 16px rgba(0, 0, 0, 0.2));
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## With ScrollerBase
|
|
|
|
You can also integrate HorizontalScroller with `ScrollerBase` for a horizontal scroll with vertical captions experience.
|
|
|
|
[Demo](?path=/story/components-graphics-horizontalscroller--scrollable-ai-2-svelte)
|
|
|
|
```svelte
|
|
<script lang="ts">
|
|
import {
|
|
HorizontalScroller,
|
|
ScrollerBase,
|
|
} from '@reuters-graphics/graphics-components';
|
|
import AiGraphic from './ai2svelte/ai-graphic.svelte';
|
|
import { circInOut } from 'svelte/easing';
|
|
import { circInOut } from 'svelte/easing';
|
|
|
|
// 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);
|
|
let threshold = $state(0.5);
|
|
let bottom = $state(1);
|
|
</script>
|
|
|
|
<ScrollerBase
|
|
{top}
|
|
{threshold}
|
|
{bottom}
|
|
bind:count
|
|
bind:index
|
|
bind:offset
|
|
bind:progress
|
|
query="div.step-foreground-container"
|
|
>
|
|
{#snippet backgroundSnippet()}
|
|
<!-- Make sure to set height to `100lvh` -->
|
|
<!-- and handleScroll to false to avoid scroll conflicts -->
|
|
<HorizontalScroller
|
|
width="fluid"
|
|
height="100lvh"
|
|
direction="right"
|
|
bind:progress
|
|
scrubbed
|
|
stops={[0.5]}
|
|
handleScroll={false}
|
|
easing={circInOut}
|
|
showDebugInfo
|
|
>
|
|
<Demo />
|
|
</HorizontalScroller>
|
|
{/snippet}
|
|
{#snippet foregroundSnippet()}
|
|
<!-- Add custom foreground HTML or component -->
|
|
<div class="step-foreground-container"><p>Step 1</p></div>
|
|
<div class="step-foreground-container"><p>Step 2</p></div>
|
|
<div class="step-foreground-container"><p>Step 3</p></div>
|
|
<div class="step-foreground-container"><p>Step 4</p></div>
|
|
<div class="step-foreground-container"><p>Step 5</p></div>
|
|
{/snippet}
|
|
</ScrollerBase>
|
|
|
|
<style lang="scss">
|
|
.step-foreground-container {
|
|
height: 100vh;
|
|
width: 50%;
|
|
background-color: rgba(0, 0, 0, 0);
|
|
padding: 1em;
|
|
margin: 0 auto 10px 0;
|
|
position: relative;
|
|
left: 50%;
|
|
transform: translate(-50%, 0);
|
|
|
|
p {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 100%;
|
|
padding: 1rem;
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
border-radius: 4px;
|
|
color: white;
|
|
}
|
|
}
|
|
</style>
|
|
```
|