basic carousel
This commit is contained in:
parent
82dbf976c3
commit
774b25da72
8 changed files with 501 additions and 1 deletions
|
|
@ -78,6 +78,7 @@
|
|||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.3",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@splidejs/svelte-splide": "^0.2.9",
|
||||
"@sveltejs/svelte-scroller": "^2.0.7",
|
||||
"bootstrap": "^5.2.0",
|
||||
"classnames": "^2.3.1",
|
||||
|
|
@ -222,4 +223,4 @@
|
|||
".": "./dist/index.js"
|
||||
},
|
||||
"svelte": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
src/components/PhotoCarousel/PhotoCarousel.stories.svelte
Normal file
82
src/components/PhotoCarousel/PhotoCarousel.stories.svelte
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import { Meta, Template, Story } from '@storybook/addon-svelte-csf';
|
||||
|
||||
// Don't lose the "?raw" in markdown imports!
|
||||
// @ts-ignore
|
||||
import componentDocs from './stories/docs/component.md?raw';
|
||||
// @ts-ignore
|
||||
import customDocs from './stories/docs/withCustom.md?raw';
|
||||
|
||||
import PhotoCarousel from './PhotoCarousel.svelte';
|
||||
|
||||
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
|
||||
|
||||
import photosJson from './stories/photos.json';
|
||||
|
||||
const photos = photosJson.map((p) => ({ ...p, altText: p.caption }));
|
||||
|
||||
const meta = {
|
||||
title: 'Components/PhotoCarousel',
|
||||
component: PhotoCarousel,
|
||||
...withComponentDocs(componentDocs),
|
||||
// https://storybook.js.org/docs/svelte/essentials/controls
|
||||
argTypes: {
|
||||
width: {
|
||||
control: 'select',
|
||||
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<Meta {...meta} />
|
||||
|
||||
<Template let:args>
|
||||
<PhotoCarousel {...args} />
|
||||
</Template>
|
||||
|
||||
<Story
|
||||
name="Default"
|
||||
args="{{
|
||||
width: 'wider',
|
||||
photos,
|
||||
}}"
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="Custom credits and captions"
|
||||
args="{{
|
||||
width: 'wider',
|
||||
photos,
|
||||
}}"
|
||||
{...withStoryDocs(customDocs)}
|
||||
>
|
||||
<PhotoCarousel
|
||||
{...{
|
||||
width: 'wider',
|
||||
photos,
|
||||
}}
|
||||
>
|
||||
<p class="custom-credit" slot="credit" let:credit>{credit}</p>
|
||||
<p class="custom-caption" slot="caption" let:caption>{caption}</p>
|
||||
</PhotoCarousel>
|
||||
</Story>
|
||||
|
||||
<style lang="scss">
|
||||
p {
|
||||
position: absolute;
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
font-family: sans-serif;
|
||||
font-size: 0.8rem;
|
||||
padding: 0 5px;
|
||||
&.custom-credit {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
&.custom-caption {
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
284
src/components/PhotoCarousel/PhotoCarousel.svelte
Normal file
284
src/components/PhotoCarousel/PhotoCarousel.svelte
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
<!-- @component `PhotoCarousel` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-PhotoCarousel--default) -->
|
||||
<script lang="ts">
|
||||
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
|
||||
|
||||
/** Width of the component within the text well. */
|
||||
export let width: ContainerWidth = 'wider';
|
||||
|
||||
/** Add an ID to target with SCSS. */
|
||||
export let id: string = '';
|
||||
|
||||
/** Add a class to target with SCSS. */
|
||||
export let cls: string = '';
|
||||
|
||||
interface Image {
|
||||
/**
|
||||
* Image src
|
||||
* @required
|
||||
*/
|
||||
src: string;
|
||||
/**
|
||||
* Image alt-text
|
||||
* @required
|
||||
*/
|
||||
altText: string;
|
||||
/** Optional caption */
|
||||
caption?: string;
|
||||
/** Optional credit */
|
||||
credit?: string;
|
||||
/** Optional object-fit rule */
|
||||
objectFit?: string;
|
||||
/** Optional object-position rule */
|
||||
objectPosition?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of photos.
|
||||
* @required
|
||||
*/
|
||||
export let photos: Image[] = [];
|
||||
|
||||
/**
|
||||
* Max height of the carousel
|
||||
*/
|
||||
export let maxHeight: number = 660;
|
||||
|
||||
type ObjectFit = 'cover' | 'contain';
|
||||
|
||||
/**
|
||||
* Default Image object-fit style, either `cover` or `contain`.
|
||||
*/
|
||||
export let defaultImageObjectFit: ObjectFit = 'cover';
|
||||
|
||||
/**
|
||||
* Default image object-position style, e.g., `center center` or `50% 50%`.
|
||||
*/
|
||||
export let defaultImageObjectPosition: string = 'center center';
|
||||
|
||||
/**
|
||||
* ARIA label for the carousel.
|
||||
* @required
|
||||
*/
|
||||
export let carouselAriaLabel: string = 'Photo gallery';
|
||||
|
||||
/**
|
||||
* Set height of the carousel as a ratio of its width
|
||||
* as long as its below whatever you set in `maxHeight`.
|
||||
*/
|
||||
export let heightRatio: number = 0.68;
|
||||
|
||||
/**
|
||||
* Number of images to preload ahead of the active image.
|
||||
*/
|
||||
export let preloadImages: number = 1;
|
||||
|
||||
import Block from '../Block/Block.svelte';
|
||||
import { Splide, SplideSlide, SplideTrack } from '@splidejs/svelte-splide';
|
||||
import '@splidejs/svelte-splide/css/core';
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import {
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { fly } from 'svelte/transition';
|
||||
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
|
||||
|
||||
let containerWidth;
|
||||
|
||||
let activeImageIndex = 0;
|
||||
|
||||
$: carouselHeight = containerWidth
|
||||
? Math.min(containerWidth * heightRatio, maxHeight)
|
||||
: maxHeight;
|
||||
|
||||
const handleActiveChange = (e) => {
|
||||
activeImageIndex = e.detail.dest;
|
||||
};
|
||||
</script>
|
||||
|
||||
<Block width="{width}" id="{id}" cls="photo-carousel {cls}">
|
||||
<div class="carousel-container" bind:clientWidth="{containerWidth}">
|
||||
<Splide
|
||||
hasTrack="{false}"
|
||||
options="{{
|
||||
height: carouselHeight,
|
||||
fixedHeight: carouselHeight,
|
||||
lazyLoad: 'nearby',
|
||||
preloadPages: preloadImages,
|
||||
}}"
|
||||
aria-label="{carouselAriaLabel}"
|
||||
on:move="{handleActiveChange}"
|
||||
>
|
||||
<div class="image-container">
|
||||
<SplideTrack>
|
||||
{#each photos as photo, i}
|
||||
<SplideSlide>
|
||||
<div class="photo-slide">
|
||||
<figure style="height: {carouselHeight}px;">
|
||||
<img
|
||||
data-splide-lazy="{photo.src}"
|
||||
alt="{photo.altText}"
|
||||
style:object-fit="{photo.objectFit ||
|
||||
defaultImageObjectFit}"
|
||||
style:object-position="{photo.objectPosition ||
|
||||
defaultImageObjectPosition}"
|
||||
/>
|
||||
{#if $$slots.credit}
|
||||
<slot name="credit" credit="{photo.credit}" />
|
||||
{:else}
|
||||
<span
|
||||
class="credit"
|
||||
class:contain-fit="{photo.objectFit === 'contain' ||
|
||||
defaultImageObjectFit === 'contain'}"
|
||||
>{photo.credit}</span
|
||||
>
|
||||
{/if}
|
||||
</figure>
|
||||
</div>
|
||||
</SplideSlide>
|
||||
{/each}
|
||||
</SplideTrack>
|
||||
|
||||
{#if photos[activeImageIndex].caption}
|
||||
<PaddingReset containerIsFluid="{width === 'fluid'}">
|
||||
{#if $$slots.caption}
|
||||
<slot
|
||||
name="caption"
|
||||
caption="{photos[activeImageIndex].caption}"
|
||||
/>
|
||||
{:else}
|
||||
{#key activeImageIndex}
|
||||
<p class="caption" in:fly|local="{{ x: 20, duration: 175 }}">
|
||||
{photos[activeImageIndex].caption}
|
||||
</p>
|
||||
{/key}
|
||||
{/if}
|
||||
</PaddingReset>
|
||||
{/if}
|
||||
|
||||
<div class="splide__progress">
|
||||
<div class="splide__progress__bar"></div>
|
||||
</div>
|
||||
|
||||
<div class="splide__arrows">
|
||||
<button class="splide__arrow splide__arrow--prev">
|
||||
<Fa icon="{faChevronLeft}" fw />
|
||||
</button>
|
||||
<button class="splide__arrow splide__arrow--next">
|
||||
<Fa icon="{faChevronRight}" fw />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Splide>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../scss/fonts/variables';
|
||||
@import '../../scss/colours/thematic/tr';
|
||||
|
||||
.carousel-container {
|
||||
margin-bottom: 10px;
|
||||
.photo-slide {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
figure {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
span.credit {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 10px;
|
||||
margin: 0;
|
||||
font-size: 0.7rem;
|
||||
color: white;
|
||||
letter-spacing: 0.75px;
|
||||
text-shadow: -1px -1px 0 #333, 1px -1px 0 #333, -1px 1px 0 #333,
|
||||
1px 1px 0 #333;
|
||||
font-family: var(--theme-font-family-note);
|
||||
&.contain-fit {
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.splide {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.splide__arrows {
|
||||
width: 275px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0px;
|
||||
padding-top: 4px;
|
||||
button {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
color: var(--theme-colour-text-secondary);
|
||||
opacity: 0.4;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
border-color: var(--theme-colour-text-secondary);
|
||||
color: var(--theme-colour-text-secondary);
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
&:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ul.splide__pagination {
|
||||
width: 200px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
margin: -26px auto 0;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
li {
|
||||
flex-grow: 1;
|
||||
button {
|
||||
width: 100%;
|
||||
height: 7px;
|
||||
border-radius: 0;
|
||||
margin: 0 0px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--theme-colour-background);
|
||||
background: var(--theme-colour-text-secondary);
|
||||
opacity: 0.4;
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p.caption {
|
||||
margin: 5px 0 0;
|
||||
font-family: var(--theme-font-family-note, $font-family-display);
|
||||
color: var(--theme-colour-text-secondary, $tr-medium-grey);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.1rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
src/components/PhotoCarousel/stories/docs/component.md
Normal file
21
src/components/PhotoCarousel/stories/docs/component.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
A simple, accessible photo carousel with lazy-loading and mobile swipe built in.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { PhotoCarousel } from '@reuters-graphics/graphics-components';
|
||||
|
||||
const photos = [
|
||||
{
|
||||
src: 'https://.../myImage.jpg',
|
||||
altText: 'A picture of...',
|
||||
caption: 'My caption...', // Optional
|
||||
credit: 'REUTERS/Jane Doe', // Optional
|
||||
objectFit: 'contain', // Optional
|
||||
objectPosition: '50% 50%', // Optional
|
||||
},
|
||||
// ...
|
||||
];
|
||||
</script>
|
||||
|
||||
<PhotoCarousel photos="{photos}" />
|
||||
```
|
||||
27
src/components/PhotoCarousel/stories/docs/withCustom.md
Normal file
27
src/components/PhotoCarousel/stories/docs/withCustom.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
Use named slots to style your own custom credits and/or captions.
|
||||
|
||||
```svelte
|
||||
<PhotoCarousel photos="{photos}">
|
||||
<p slot="credit" class="custom-credit" let:credit>{credit}</p>
|
||||
<p slot="caption" class="custom-caption" let:caption>{caption}</p>
|
||||
</PhotoCarousel>
|
||||
|
||||
<style lang="scss">
|
||||
p {
|
||||
position: absolute;
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
font-family: sans-serif;
|
||||
font-size: 0.8rem;
|
||||
padding: 0 5px;
|
||||
&.custom-credit {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
&.custom-caption {
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
72
src/components/PhotoCarousel/stories/photos.json
Normal file
72
src/components/PhotoCarousel/stories/photos.json
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
[
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T194630Z_544493697_UP1E.jpeg",
|
||||
"caption": "Spain's Sergio Busquets and Aymeric Laporte react before a Germany goal is disallowed following a VAR review.",
|
||||
"credit": "REUTERS/Molly Darlington"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T194619Z_2007900040_UP1.jpeg",
|
||||
"caption": "Spain's Sergio Busquets fouls Germany's Jamal Musiala before being shown yellow card.",
|
||||
"credit": "REUTERS/Kai Pfaffenbach"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T194619Z_635809122_UP1E.jpeg",
|
||||
"caption": "Spain's Sergio Busquets is shown a yellow card by referee Danny Desmond Makkelie.",
|
||||
"credit": "REUTERS/Albert Gea"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T191015Z_1293757566_UP1.jpeg",
|
||||
"caption": "Spain's Sergio Busquets in action with Germany's Thomas Muller.",
|
||||
"credit": "REUTERS/John Sibley"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T181411Z_1_MTZXEIBR0QNN.jpeg",
|
||||
"caption": "Spain fans inside the stadium before the match.",
|
||||
"credit": "REUTERS/Albert Gea"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T194827Z_345059331_UP1E.jpeg",
|
||||
"caption": "Spain's Gavi.",
|
||||
"credit": "REUTERS/Fabrizio Bensch"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T175149Z_1_MTZXEIBR0PMD.jpeg",
|
||||
"caption": "",
|
||||
"credit": "REUTERS/John Sibley"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T203232Z_890709671_UP1E.jpeg",
|
||||
"caption": "Spain's Alvaro Morata scores their first goal.",
|
||||
"credit": "REUTERS/Kai Pfaffenbach"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T203612Z_1399473226_UP1.jpeg",
|
||||
"caption": "Spain's Alvaro Morata celebrates scoring their first goal.",
|
||||
"credit": "REUTERS/Molly Darlington"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T204305Z_1795686896_UP1.jpeg",
|
||||
"caption": "Germany's Niclas Fullkrug scores their first goal.",
|
||||
"credit": "REUTERS/Molly Darlington"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T204528Z_151067034_UP1E.jpeg",
|
||||
"caption": "Germany's Niclas Fullkrug celebrates scoring their first goal.",
|
||||
"credit": "REUTERS/Molly Darlington"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T205041Z_2076149593_UP1.jpeg",
|
||||
"caption": "Spain coach Luis Enrique.",
|
||||
"credit": "REUTERS/John Sibley"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T205604Z_1468073277_UP1.jpeg",
|
||||
"caption": "Germany's Manuel Neuer applauds fans after the match.",
|
||||
"credit": "REUTERS/Kai Pfaffenbach"
|
||||
},
|
||||
{
|
||||
"src": "https://graphics.thomsonreuters.com/cdn/django-tools/media/graphics-gallery/galleries/world-cup-2022/spain-germany-11-27/2022-11-27T205854Z_408619749_UP1E.jpeg",
|
||||
"caption": "Spain players applaud fans after the match.",
|
||||
"credit": "REUTERS/Albert Gea"
|
||||
}
|
||||
]
|
||||
|
|
@ -12,6 +12,7 @@ export { default as Headline } from './components/Headline/Headline.svelte';
|
|||
export { default as Hero } from './components/Hero/Hero.svelte';
|
||||
export { default as NoteText } from './components/NoteText/NoteText.svelte';
|
||||
export { default as PaddingReset } from './components/PaddingReset/PaddingReset.svelte';
|
||||
export { default as PhotoCarousel } from './components/PhotoCarousel/PhotoCarousel.svelte';
|
||||
export { default as PhotoPack } from './components/PhotoPack/PhotoPack.svelte';
|
||||
export { default as PymChild } from './components/PymChild/PymChild.svelte';
|
||||
export { pymChildStore } from './components/PymChild/stores.js';
|
||||
|
|
|
|||
12
yarn.lock
12
yarn.lock
|
|
@ -1526,6 +1526,18 @@
|
|||
estree-walker "^2.0.1"
|
||||
picomatch "^2.2.2"
|
||||
|
||||
"@splidejs/splide@^4.1.3":
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@splidejs/splide/-/splide-4.1.4.tgz#02d029360569e7d75d28357a9727fc48322015a3"
|
||||
integrity sha512-5I30evTJcAJQXt6vJ26g2xEkG+l1nXcpEw4xpKh0/FWQ8ozmAeTbtniVtVmz2sH1Es3vgfC4SS8B2X4o5JMptA==
|
||||
|
||||
"@splidejs/svelte-splide@^0.2.9":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@splidejs/svelte-splide/-/svelte-splide-0.2.9.tgz#35659e305da61c32e3d65878c37c50e02d9a1cba"
|
||||
integrity sha512-04ekJnDIJKEAhklKQMhkg4Yx0Ihtkk18eA9JeHPON0lDTngQxlOYdEYTJWH2UON45VxmVqoLHv04I++JphO36w==
|
||||
dependencies:
|
||||
"@splidejs/splide" "^4.1.3"
|
||||
|
||||
"@storybook/addon-actions@6.5.9":
|
||||
version "6.5.9"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.5.9.tgz#d50d65631403e1a5b680961429d9c0d7bd383e68"
|
||||
|
|
|
|||
Loading…
Reference in a new issue