Merge pull request #235 from reuters-graphics/mf-before-after
Updates BeforeAfter
This commit is contained in:
commit
8ad8024987
10 changed files with 319 additions and 308 deletions
111
src/components/BeforeAfter/BeforeAfter.mdx
Normal file
111
src/components/BeforeAfter/BeforeAfter.mdx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { Meta, Canvas } from '@storybook/blocks';
|
||||
|
||||
import * as BeforeAfterStories from './BeforeAfter.stories.svelte';
|
||||
|
||||
<Meta of={BeforeAfterStories} />
|
||||
|
||||
# BeforeAfter
|
||||
|
||||
The `BeforeAfter` component shows a before-and-after comparison of an image.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { BeforeAfter } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths'; // If using in the Graphics Kit
|
||||
</script>
|
||||
|
||||
<BeforeAfter
|
||||
beforeSrc={`${assets}/images/before-after/myrne-before.jpg`}
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc={`${assets}/images/before-after/myrne-after.jpg`}
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={BeforeAfterStories.Demo} />
|
||||
|
||||
## Using with ArchieML docs
|
||||
|
||||
With the Graphics Kit, you'll likely get your text value from an ArchieML doc...
|
||||
|
||||
```yaml
|
||||
# ArchieML doc
|
||||
[blocks]
|
||||
|
||||
type: before-after
|
||||
beforeSrc: images/before-after/myrne-before.jpg
|
||||
beforeAlt: Satellite image of Russian base at Myrne taken on July 7, 2020.
|
||||
afterSrc: images/before-after/myrne-after.jpg
|
||||
afterAlt: Satellite image of Russian base at Myrne taken on Oct. 20, 2020.
|
||||
|
||||
[]
|
||||
```
|
||||
|
||||
... which you'll parse out of a ArchieML block object before passing to the `BeforeAfter` component.
|
||||
|
||||
```svelte
|
||||
<!-- App.svelte -->
|
||||
{#each content.blocks as block}
|
||||
{#if block.type === 'before-after'}
|
||||
<BeforeAfter
|
||||
beforeSrc={`${assets}/${block.beforeSrc}`}
|
||||
beforeAlt={block.beforeAlt}
|
||||
afterSrc={`${assets}/${block.afterSrc}`}
|
||||
afterAlt={block.afterAlt}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
<Canvas of={BeforeAfterStories.Demo} />
|
||||
|
||||
## Adding text
|
||||
|
||||
To add text overlays and captions, use [snippets](https://svelte.dev/docs/svelte/snippet) for `beforeOverlay`, `afterOverlay` and `caption`. You can style the snippets to match your page design, like in [this demo](./?path=/story/components-multimedia-beforeafter--with-overlays).
|
||||
|
||||
> 💡**NOTE:** The text in the overlays are used as [ARIA descriptions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) for your before and after images. You must always use the `beforeAlt` / `afterAlt` props to label your image for visually impaired readers, but these ARIA descriptions provide additional information or context that the reader might need.
|
||||
|
||||
```svelte
|
||||
<BeforeAfter
|
||||
beforeSrc={beforeImg}
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc={afterImg}
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
>
|
||||
<!-- Optional custom text overlay for the before image -->
|
||||
{#snippet beforeOverlay()}
|
||||
<div class="overlay p-3 before text-left">
|
||||
<p class="h4 font-bold">July 7, 2020</p>
|
||||
<p class="body-note">Initially, this site was far smaller.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Optional custom text overlay for the after image -->
|
||||
{#snippet afterOverlay()}
|
||||
<div class="overlay p-3 after text-right">
|
||||
<p class="h4 font-bold">Oct. 20, 2020</p>
|
||||
<p class="body-note">But then forces built up.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Optional custom caption for both images -->
|
||||
{#snippet caption()}
|
||||
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
|
||||
{/snippet}
|
||||
</BeforeAfter>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
max-width: 350px;
|
||||
&.after {
|
||||
text-align: right;
|
||||
}
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<Canvas of={BeforeAfterStories.WithText} />
|
||||
|
|
@ -1,19 +1,10 @@
|
|||
<!-- @migration-task Error while migrating Svelte code: end is out of bounds -->
|
||||
<script context="module" lang="ts">
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import BeforeAfter from './BeforeAfter.svelte';
|
||||
// @ts-ignore raw
|
||||
import componentDocs from './stories/docs/component.md?raw';
|
||||
// @ts-ignore raw
|
||||
import withOverlaysDocs from './stories/docs/withOverlays.md?raw';
|
||||
// @ts-ignore raw
|
||||
import ariaDescriptionsDocs from './stories/docs/ariaDescriptions.md?raw';
|
||||
|
||||
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
|
||||
|
||||
export const meta = {
|
||||
title: 'Components/Graphics/BeforeAfter',
|
||||
const { Story } = defineMeta({
|
||||
title: 'Components/Multimedia/BeforeAfter',
|
||||
component: BeforeAfter,
|
||||
...withComponentDocs(componentDocs),
|
||||
argTypes: {
|
||||
handleColour: { control: 'color' },
|
||||
width: {
|
||||
|
|
@ -21,96 +12,54 @@
|
|||
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { Template, Story } from '@storybook/addon-svelte-csf';
|
||||
|
||||
// @ts-ignore raw
|
||||
import beforeImg from './stories/myrne-before.jpg';
|
||||
// @ts-ignore raw
|
||||
import afterImg from './stories/myrne-after.jpg';
|
||||
import beforeImg from './images/myrne-before.jpg';
|
||||
import afterImg from './images/myrne-after.jpg';
|
||||
</script>
|
||||
|
||||
<Template let:args>
|
||||
<BeforeAfter {...args} />
|
||||
</Template>
|
||||
|
||||
<Story
|
||||
name="Default"
|
||||
args="{{
|
||||
name="Demo"
|
||||
args={{
|
||||
beforeSrc: beforeImg,
|
||||
beforeAlt:
|
||||
'Satellite image of Russian base at Myrne taken on July 7, 2020.',
|
||||
afterSrc: afterImg,
|
||||
afterAlt:
|
||||
'Satellite image of Russian base at Myrne taken on Oct. 20, 2020.',
|
||||
}}"
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story name="With overlays" {...withStoryDocs(withOverlaysDocs)}>
|
||||
<Story name="With text" exportName="WithText">
|
||||
<BeforeAfter
|
||||
beforeSrc="{beforeImg}"
|
||||
beforeSrc={beforeImg}
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc="{afterImg}"
|
||||
afterSrc={afterImg}
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
>
|
||||
<div slot="beforeOverlay" class="overlay p-3 before">
|
||||
<p class="h4 font-bold">July 7, 2020</p>
|
||||
<p class="body-note">Initially, this site was far smaller.</p>
|
||||
</div>
|
||||
<div slot="afterOverlay" class="overlay p-3 after">
|
||||
<p class="h4 font-bold">Oct. 20, 2020</p>
|
||||
<p class="body-note">But then forces built up.</p>
|
||||
</div>
|
||||
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
|
||||
{#snippet beforeOverlay()}
|
||||
<div class="overlay p-3 before text-left">
|
||||
<p class="h4 font-bold">July 7, 2020</p>
|
||||
<p class="body-note">Initially, this site was far smaller.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet afterOverlay()}
|
||||
<div class="overlay p-3 after text-right">
|
||||
<p class="h4 font-bold">Oct. 20, 2020</p>
|
||||
<p class="body-note">But then forces built up.</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet caption()}
|
||||
<p class="body-note">Photos by MAXAR Technologies, 2021.</p>
|
||||
{/snippet}
|
||||
</BeforeAfter>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
max-width: 350px;
|
||||
&.after {
|
||||
text-align: right;
|
||||
}
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</Story>
|
||||
|
||||
<Story name="ARIA descriptions" {...withStoryDocs(ariaDescriptionsDocs)}>
|
||||
<BeforeAfter
|
||||
beforeSrc="{beforeImg}"
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc="{afterImg}"
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
>
|
||||
<div let:description="{id}" slot="beforeOverlay" class="overlay p-3">
|
||||
<p class="body-caption" {id}>
|
||||
On July 7, 2020, the base contained only a few transport vehicles.
|
||||
</p>
|
||||
</div>
|
||||
<div let:description="{id}" slot="afterOverlay" class="overlay p-3">
|
||||
<!-- 👇 id can also be used on an element containing multiple text elements -->
|
||||
<div {id}>
|
||||
<p class="body-caption">
|
||||
But by October, tanks and artillery could be seen.
|
||||
</p>
|
||||
<p class="body-caption">
|
||||
In total, over 50 pieces of heavy machinery and 200 personnel are now
|
||||
based here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
|
||||
</BeforeAfter>
|
||||
<style lang="scss">
|
||||
div.overlay {
|
||||
max-width: 250px;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,119 +1,149 @@
|
|||
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
|
||||
<!-- @component `BeforeAfter` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-beforeafter--docs) -->
|
||||
<script lang="ts">
|
||||
import { type Snippet } from 'svelte';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import Block from '../Block/Block.svelte';
|
||||
import type { ContainerWidth } from '../@types/global';
|
||||
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
|
||||
import type { ContainerWidth } from '../@types/global';
|
||||
import { random4 } from '../../utils/';
|
||||
|
||||
/** Width of the chart within the text well. */
|
||||
export let width: ContainerWidth = 'normal'; // options: wide, wider, widest, fluid
|
||||
/** Height of the component */
|
||||
export let height = 600;
|
||||
interface Props {
|
||||
/** Width of the chart within the text well. Options: wide, wider, widest, fluid */
|
||||
width?: ContainerWidth;
|
||||
/** Height of the component */
|
||||
height?: number;
|
||||
/**
|
||||
* If set, makes the height a ratio of the component's width.
|
||||
*/
|
||||
heightRatio?: number;
|
||||
/**
|
||||
* Before image source
|
||||
*/
|
||||
beforeSrc: string;
|
||||
/**
|
||||
* Before image altText
|
||||
*/
|
||||
beforeAlt: string;
|
||||
/**
|
||||
* After image source
|
||||
*/
|
||||
afterSrc: string;
|
||||
/**
|
||||
* After image altText
|
||||
*/
|
||||
afterAlt: string;
|
||||
/**
|
||||
* Class to target with SCSS.
|
||||
*/
|
||||
class?: string;
|
||||
/** Drag handle colour */
|
||||
handleColour?: string;
|
||||
/** Drag handle opacity */
|
||||
handleInactiveOpacity?: number;
|
||||
/** Margin at the edge of the image to stop dragging */
|
||||
handleMargin?: number;
|
||||
/** Percentage of the component width the handle will travel ona key press */
|
||||
keyPressStep?: number;
|
||||
/** Initial offset of the handle, between 0 and 1. */
|
||||
offset?: number;
|
||||
/** ID to target with SCSS. */
|
||||
id?: string;
|
||||
/**
|
||||
* Optional snippet for a custom overlay for the before image.
|
||||
*/
|
||||
beforeOverlay?: Snippet;
|
||||
/**
|
||||
* Optional snippet for a custom overlay for the after image.
|
||||
*/
|
||||
afterOverlay?: Snippet;
|
||||
/**
|
||||
* Optional snippet for a custom caption.
|
||||
*/
|
||||
caption?: Snippet;
|
||||
/** Custom ARIA label language to label the component. */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, makes the height a ratio of the component's width.
|
||||
* @type {number}
|
||||
let {
|
||||
width = 'normal',
|
||||
height = 600,
|
||||
heightRatio,
|
||||
beforeSrc,
|
||||
beforeAlt,
|
||||
afterSrc,
|
||||
afterAlt,
|
||||
class: cls = '',
|
||||
handleColour = 'white',
|
||||
handleInactiveOpacity = 0.9,
|
||||
handleMargin = 20,
|
||||
keyPressStep = 0.05,
|
||||
offset = 0.5,
|
||||
id = 'before-after-' + random4() + random4(),
|
||||
beforeOverlay,
|
||||
afterOverlay,
|
||||
caption,
|
||||
ariaLabel = 'Stacked before and after images with an adjustable slider',
|
||||
}: Props = $props();
|
||||
|
||||
/** DOM nodes are undefined until the component is mounted — in other words, you should read it inside an effect or an event handler, but not during component initialisation.
|
||||
*/
|
||||
export let heightRatio: number | null = null;
|
||||
let img: HTMLImageElement | undefined = $state(undefined);
|
||||
|
||||
/**
|
||||
* Before image src
|
||||
* @required
|
||||
*/
|
||||
export let beforeSrc: string | null = null;
|
||||
/**
|
||||
* Before image altText
|
||||
* @required
|
||||
*/
|
||||
export let beforeAlt: string | null = null;
|
||||
/**
|
||||
* After image src
|
||||
* @required
|
||||
*/
|
||||
export let afterSrc: string | null = null;
|
||||
/**
|
||||
* After image altText
|
||||
* @required
|
||||
*/
|
||||
export let afterAlt: string | null = null;
|
||||
|
||||
/**
|
||||
* Set a class to target with SCSS.
|
||||
* @type {string}
|
||||
*/
|
||||
let cls: string = '';
|
||||
export { cls as class };
|
||||
|
||||
/** Drag handle colour */
|
||||
export let handleColour = 'white';
|
||||
/** Drag handle opacity */
|
||||
export let handleInactiveOpacity = 0.9;
|
||||
/** Margin at the edge of the image to stop dragging */
|
||||
export let handleMargin = 20;
|
||||
/** Percentage of the component width the handle will travel ona key press */
|
||||
export let keyPressStep = 0.05;
|
||||
|
||||
/** Initial offset of the handle, between 0 and 1. */
|
||||
export let offset = 0.5;
|
||||
|
||||
const random4 = () =>
|
||||
Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
|
||||
/**
|
||||
* Add an ID to target with SCSS.
|
||||
* @type {string}
|
||||
*/
|
||||
export let id: string = 'before-after-' + random4() + random4();
|
||||
|
||||
let img: HTMLImageElement;
|
||||
let imgOffset: DOMRect | null = null;
|
||||
/** Defaults with an empty DOMRect with all values set to 0 */
|
||||
let imgOffset: DOMRect = $state(new DOMRect());
|
||||
let sliding = false;
|
||||
let figure: HTMLElement;
|
||||
let beforeOverlayWidth = 0;
|
||||
let figure: HTMLElement | undefined = $state(undefined);
|
||||
let beforeOverlayWidth = $state(0);
|
||||
let isFocused = false;
|
||||
let containerWidth: number;
|
||||
let containerWidth: number = $state(0); // Defaults to 0
|
||||
|
||||
$: containerHeight =
|
||||
containerWidth && heightRatio ? containerWidth * heightRatio : height;
|
||||
let containerHeight = $derived(
|
||||
containerWidth && heightRatio ? containerWidth * heightRatio : height
|
||||
);
|
||||
|
||||
$: w = (imgOffset && imgOffset.width) || 0;
|
||||
$: x = w * offset;
|
||||
$: figStyle = `width:100%;height:${containerHeight}px;`;
|
||||
$: imgStyle = 'width:100%;height:100%;';
|
||||
$: beforeOverlayClip =
|
||||
x < beforeOverlayWidth ? Math.abs(x - beforeOverlayWidth) : 0;
|
||||
let w = $derived(imgOffset.width);
|
||||
let x = $derived(w * offset);
|
||||
let figStyle = $derived(`width:100%;height:${containerHeight}px;`);
|
||||
const imgStyle = 'width:100%;height:100%;';
|
||||
let beforeOverlayClip = $derived(
|
||||
x < beforeOverlayWidth ? Math.abs(x - beforeOverlayWidth) : 0
|
||||
);
|
||||
|
||||
const onFocus = () => (isFocused = true);
|
||||
const onBlur = () => (isFocused = false);
|
||||
/** Toggle `isFocused` */
|
||||
const onfocus = () => (isFocused = true);
|
||||
const onblur = () => (isFocused = false);
|
||||
|
||||
/** Handle left or right arrows being pressed */
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isFocused) return;
|
||||
const { keyCode } = e;
|
||||
const { code, key } = e;
|
||||
const margin = handleMargin / w;
|
||||
if (keyCode === 39) {
|
||||
if (code === 'ArrowRight' || key === 'ArrowRight') {
|
||||
offset = Math.min(1 - margin, offset + keyPressStep);
|
||||
} else if (keyCode === 37) {
|
||||
} else if (code === 'ArrowLeft' || key === 'ArrowLeft') {
|
||||
offset = Math.max(0 + margin, offset - keyPressStep);
|
||||
}
|
||||
};
|
||||
|
||||
/** Measure image and set image offset */
|
||||
const measureImage = () => {
|
||||
if (img && img.complete) imgOffset = img.getBoundingClientRect();
|
||||
};
|
||||
|
||||
/** Reset image offset on resize */
|
||||
const resize = () => {
|
||||
measureImage();
|
||||
};
|
||||
|
||||
/** Measure image and set image offset on load */
|
||||
const measureLoadedImage = (e: Event) => {
|
||||
if (e.type === 'load') {
|
||||
imgOffset = (e.target as HTMLImageElement).getBoundingClientRect();
|
||||
}
|
||||
};
|
||||
|
||||
/** Move the slider */
|
||||
const move = (e: MouseEvent | TouchEvent) => {
|
||||
if (sliding && imgOffset) {
|
||||
const el =
|
||||
|
|
@ -130,115 +160,115 @@
|
|||
offset = x / w;
|
||||
}
|
||||
};
|
||||
|
||||
/** Starts the slider */
|
||||
const start = (e: MouseEvent | TouchEvent) => {
|
||||
sliding = true;
|
||||
move(e);
|
||||
};
|
||||
|
||||
/** Sets `sliding` to `false`*/
|
||||
const end = () => {
|
||||
sliding = false;
|
||||
};
|
||||
|
||||
/** Keep this warning since these values are often read from an ArchieML doc, which will not trigger typescript errors even if required values don't exist */
|
||||
if (!(beforeSrc && beforeAlt && afterSrc && afterAlt)) {
|
||||
console.warn('Missing required src or alt props for BeforeAfter component');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// This is necessary b/c on:load doesn't reliably fire on the image...
|
||||
const interval = setInterval(() => {
|
||||
if (imgOffset) clearInterval(interval);
|
||||
if (img && img.complete && !imgOffset) measureImage();
|
||||
}, 50);
|
||||
});
|
||||
/** @TODO - Double check if this onMount is still necessary */
|
||||
// onMount(() => {
|
||||
// // This is necessary b/c on:load doesn't reliably fire on the image...
|
||||
// const interval = setInterval(() => {
|
||||
// if (imgOffset) clearInterval(interval);
|
||||
// if (img && img.complete && !imgOffset) measureImage();
|
||||
// }, 50);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:touchmove="{move}"
|
||||
on:touchend="{end}"
|
||||
on:mousemove="{move}"
|
||||
on:mouseup="{end}"
|
||||
on:resize="{throttle(resize, 100)}"
|
||||
on:keydown="{handleKeyDown}"
|
||||
ontouchmove={move}
|
||||
ontouchend={end}
|
||||
onmousemove={move}
|
||||
onmouseup={end}
|
||||
onresize={throttle(resize, 100)}
|
||||
onkeydown={handleKeyDown}
|
||||
/>
|
||||
|
||||
<!-- Since we usually read these values from ArchieML, check that they exist -->
|
||||
{#if beforeSrc && beforeAlt && afterSrc && afterAlt}
|
||||
<Block {width} {id} class="photo before-after fmy-6 {cls}">
|
||||
<div
|
||||
style="height: {containerHeight}px;"
|
||||
bind:clientWidth="{containerWidth}"
|
||||
>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<figure
|
||||
style="{figStyle}"
|
||||
<div style="height: {containerHeight}px;" bind:clientWidth={containerWidth}>
|
||||
<button
|
||||
style={figStyle}
|
||||
class="before-after-container relative overflow-hidden my-0 mx-auto"
|
||||
on:touchstart="{start}"
|
||||
on:mousedown="{start}"
|
||||
bind:this="{figure}"
|
||||
aria-labelledby="{($$slots.caption && `${id}-caption`) || undefined}"
|
||||
ontouchstart={start}
|
||||
onmousedown={start}
|
||||
bind:this={figure}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<img
|
||||
bind:this="{img}"
|
||||
src="{afterSrc}"
|
||||
alt="{afterAlt}"
|
||||
on:load="{measureLoadedImage}"
|
||||
on:mousedown|preventDefault
|
||||
style="{imgStyle}"
|
||||
bind:this={img}
|
||||
src={afterSrc}
|
||||
alt={afterAlt}
|
||||
onload={measureLoadedImage}
|
||||
style={imgStyle}
|
||||
class="after absolute block m-0 max-w-full object-cover"
|
||||
aria-describedby="{($$slots.beforeOverlay && `${id}-before`) ||
|
||||
undefined}"
|
||||
aria-describedby={beforeOverlay ?
|
||||
`${id}-before-description`
|
||||
: undefined}
|
||||
/>
|
||||
|
||||
<img
|
||||
src="{beforeSrc}"
|
||||
alt="{beforeAlt}"
|
||||
on:mousedown|preventDefault
|
||||
src={beforeSrc}
|
||||
alt={beforeAlt}
|
||||
style="clip: rect(0 {x}px {containerHeight}px 0);{imgStyle}"
|
||||
class="before absolute block m-0 max-w-full object-cover"
|
||||
aria-describedby="{($$slots.afterOverlay && `${id}-after`) ||
|
||||
undefined}"
|
||||
aria-describedby={afterOverlay ?
|
||||
`${id}-after-description`
|
||||
: undefined}
|
||||
/>
|
||||
{#if $$slots.beforeOverlay}
|
||||
{#if beforeOverlay}
|
||||
<div
|
||||
id="image-before-label"
|
||||
id="{id}-before-description"
|
||||
class="overlay-container before absolute"
|
||||
bind:clientWidth="{beforeOverlayWidth}"
|
||||
bind:clientWidth={beforeOverlayWidth}
|
||||
style="clip-path: inset(0 {beforeOverlayClip}px 0 0);"
|
||||
>
|
||||
<!-- Overlay for before image -->
|
||||
<slot
|
||||
name="beforeOverlay"
|
||||
description="{`${id}-before-description`}"
|
||||
/>
|
||||
{@render beforeOverlay()}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $$slots.afterOverlay}
|
||||
<div id="image-after-label" class="overlay-container after absolute">
|
||||
{#if afterOverlay}
|
||||
<div
|
||||
id="{id}-after-description"
|
||||
class="overlay-container after absolute"
|
||||
>
|
||||
<!-- Overlay for after image -->
|
||||
<slot
|
||||
name="afterOverlay"
|
||||
description="{`${id}-after-description`}"
|
||||
/>
|
||||
{@render afterOverlay()}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
tabindex="0"
|
||||
role="slider"
|
||||
aria-valuenow="{Math.round(offset * 100)}"
|
||||
aria-valuenow={Math.round(offset * 100)}
|
||||
class="handle"
|
||||
style="left: calc({offset *
|
||||
100}% - 20px); --before-after-handle-colour: {handleColour}; --before-after-handle-inactive-opacity: {handleInactiveOpacity};"
|
||||
on:focus="{onFocus}"
|
||||
on:blur="{onBlur}"
|
||||
{onfocus}
|
||||
{onblur}
|
||||
>
|
||||
<div class="arrow-left"></div>
|
||||
<div class="arrow-right"></div>
|
||||
</div>
|
||||
</figure>
|
||||
</button>
|
||||
</div>
|
||||
{#if $$slots.caption}
|
||||
<PaddingReset containerIsFluid="{width === 'fluid'}">
|
||||
<aside class="before-after-caption mx-auto" id="{`${id}-caption`}">
|
||||
{#if caption}
|
||||
<PaddingReset containerIsFluid={width === 'fluid'}>
|
||||
<aside class="before-after-caption mx-auto" id={`${id}-caption`}>
|
||||
<!-- Caption for image credits -->
|
||||
<slot name="caption" />
|
||||
{@render caption()}
|
||||
</aside>
|
||||
</PaddingReset>
|
||||
{/if}
|
||||
|
|
@ -246,9 +276,9 @@
|
|||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../scss/mixins' as *;
|
||||
@use '../../scss/mixins' as mixins;
|
||||
|
||||
figure.before-after-container {
|
||||
button.before-after-container {
|
||||
box-sizing: content-box;
|
||||
|
||||
img {
|
||||
|
|
@ -264,7 +294,7 @@
|
|||
user-select: none;
|
||||
}
|
||||
.overlay-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
:global(:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
@ -337,9 +367,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
aside.before-after-caption {
|
||||
.before-after-caption {
|
||||
:global(p) {
|
||||
@include body-caption;
|
||||
@include mixins.body-caption;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 715 KiB After Width: | Height: | Size: 715 KiB |
|
Before Width: | Height: | Size: 472 KiB After Width: | Height: | Size: 472 KiB |
|
|
@ -1,35 +0,0 @@
|
|||
Use text elements in your overlays as [ARIA descriptions](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-describedby) for your images by setting an ID on text elements within each overlay with the `description` [slot prop](https://svelte.dev/tutorial/slot-props).
|
||||
|
||||
> **💡 TIP:** You must always use the `beforeAlt` / `afterAlt` props to label your image for visually impaired readers, but you can use these descriptions to provide more information or context that the reader might also need.
|
||||
|
||||
```svelte
|
||||
<BeforeAfter
|
||||
beforeSrc="{beforeImg}"
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc="{afterImg}"
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
>
|
||||
<div slot="beforeOverlay" class="overlay p-3 before">
|
||||
<p class="h4 font-bold">July 7, 2020</p>
|
||||
<p class="body-note">Initially, this site was far smaller.</p>
|
||||
</div>
|
||||
<div slot="afterOverlay" class="overlay p-3 after">
|
||||
<p class="h4 font-bold">Oct. 20, 2020</p>
|
||||
<p class="body-note">But then forces built up.</p>
|
||||
</div>
|
||||
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
|
||||
</BeforeAfter>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
max-width: 350px;
|
||||
&.after {
|
||||
text-align: right;
|
||||
}
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
A before and after image comparison component.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { BeforeAfter } from '@reuters-graphics/graphics-components';
|
||||
import { assets } from '$app/paths'; // If using in the Graphics Kit
|
||||
</script>
|
||||
|
||||
<BeforeAfter
|
||||
beforeSrc="{`${assets}/images/before-after/myrne-before.jpg`}"
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc="{`${assets}/images/before-after/myrne-after.jpg`}"
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
/>
|
||||
```
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
Add overlays with the `beforeOverlay` and `afterOverlay` slots and a caption to the `caption` slot, then style these elements to match your page design as below.
|
||||
|
||||
```svelte
|
||||
<BeforeAfter
|
||||
beforeSrc="{beforeImg}"
|
||||
beforeAlt="Satellite image of Russian base at Myrne taken on July 7, 2020."
|
||||
afterSrc="{afterImg}"
|
||||
afterAlt="Satellite image of Russian base at Myrne taken on Oct. 20, 2020."
|
||||
>
|
||||
<div slot="beforeOverlay" class="overlay p-3 before">
|
||||
<p class="h4 font-bold">July 7, 2020</p>
|
||||
<p class="body-note">Initially, this site was far smaller.</p>
|
||||
</div>
|
||||
<div slot="afterOverlay" class="overlay p-3 after">
|
||||
<p class="h4 font-bold">Oct. 20, 2020</p>
|
||||
<p class="body-note">But then forces built up.</p>
|
||||
</div>
|
||||
<p slot="caption">Photos by MAXAR Technologies, 2021.</p>
|
||||
</BeforeAfter>
|
||||
|
||||
<style lang="scss">
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
max-width: 350px;
|
||||
&.after {
|
||||
text-align: right;
|
||||
}
|
||||
p {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
|
@ -17,12 +17,10 @@
|
|||
publishTime: string;
|
||||
/**
|
||||
* Update time as a datetime string.
|
||||
* @type {string}
|
||||
*/
|
||||
updateTime: string;
|
||||
/**
|
||||
* Alignment of the byline.
|
||||
* @type {string}
|
||||
*/
|
||||
align?: 'left' | 'center';
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import slugify from 'slugify';
|
||||
|
||||
/** Helper function to generate a random 4-character string */
|
||||
export const random4 = () =>
|
||||
Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
|
||||
/**
|
||||
* Custom function that returns an author page URL.
|
||||
*/
|
||||
export const getAuthorPageUrl = (author: string): string => {
|
||||
const authorSlug = slugify(author.trim(), { lower: true });
|
||||
return `https://www.reuters.com/authors/${authorSlug}/`;
|
||||
};
|
||||
};
|
||||
Loading…
Reference in a new issue