set up photo carousel

This commit is contained in:
MinamiFunakoshiTR 2025-03-20 14:49:53 -07:00
parent 4a674123df
commit 45eb8178e8
Failed to extract signature
7 changed files with 195 additions and 210 deletions

View file

@ -48,3 +48,22 @@ export interface ScrollerStep {
*/
foregroundProps?: object;
}
export interface PhotoCarouselImage {
/**
* Image src
*/
src: string;
/**
* Image alt text
*/
altText: string;
/** Optional caption as a string */
caption?: string;
/** Optional credit as string */
credit?: string;
/** Optional object-fit rule */
objectFit?: string;
/** Optional object-position rule */
objectPosition?: string;
}

View file

@ -0,0 +1,63 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as PhotoCarouselStories from './PhotoCarousel.stories.svelte';
<Meta of={PhotoCarouselStories} />
# PhotoCarousel
The `PhotoCarousel` component creates 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} />
```
<Canvas of={PhotoCarouselStories.Demo} />
## Custom text
Use named slots to style your own custom credits and/or captions.
```svelte
<PhotoCarousel {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>
```
<Canvas of={PhotoCarouselStories.CustomText} />

View file

@ -1,68 +1,41 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore raw
import customDocs from './stories/docs/withCustom.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import PhotoCarousel from './PhotoCarousel.svelte';
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Multimedia/PhotoCarousel',
component: PhotoCarousel,
...withComponentDocs(componentDocs),
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
import photosJson from './stories/photos.json';
import photosJson from './demo/photos.json';
// import { PhotoCarouselImage } from '../@types/global';
const photos = photosJson.map((p) => ({ ...p, altText: p.caption }));
</script>
<Template >
{#snippet children({ args })}
<PhotoCarousel {...args} />
{/snippet}
</Template>
<Story
name="Default"
args="{{
width: 'wider',
name="Demo"
args={{
photos,
}}"
}}
/>
<Story
name="Custom credits and captions"
args="{{
width: 'wider',
photos,
}}"
{...withStoryDocs(customDocs)}
>
<PhotoCarousel
{...{
width: 'wider',
photos,
}}
>
{#snippet credit({ credit })}
<p class="custom-credit" >{credit}</p>
{/snippet}
{#snippet caption({ caption })}
<p class="custom-caption" >{caption}</p>
{/snippet}
<Story name="Custom text" exportName="CustomText">
<PhotoCarousel {photos}>
{#snippet credit(photo)}
<p class="custom-credit">{photo.credit}</p>
{/snippet}
{#snippet caption(photo)}
<p class="custom-caption">{photo.caption}</p>
{/snippet}
</PhotoCarousel>
</Story>

View file

@ -1,108 +1,86 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `PhotoCarousel` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-multimedia-photocarousel--docs) -->
<script lang="ts">
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
/** Width of the component within the text well. */
export let width: ContainerWidth = 'wider';
/**
* Set a different width for captions within the text well, for example,
* "normal" to keep captions inline with the rest of the text well.
* Can't ever be wider than `width`.
* @type {string}
*/
export let textWidth: ContainerWidth = 'normal';
/** Add an ID to target with SCSS. */
export let id: string = '';
/** Add a class to target with SCSS. */
let cls: string = '';
export { cls as class };
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';
// Utils
import '@splidejs/svelte-splide/css/core';
// @ts-ignore no types
import { fly } from 'svelte/transition';
import { Splide, SplideSlide, SplideTrack } from '@splidejs/svelte-splide';
// Icons
import Fa from 'svelte-fa/src/fa.svelte';
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons';
import { fly } from 'svelte/transition';
// Components
import Block from '../Block/Block.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
// Types
import type { MoveEventDetail } from '@splidejs/svelte-splide/types';
import type { Snippet } from 'svelte';
import type { PhotoCarouselImage } from '../@types/global';
let containerWidth: number;
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
let activeImageIndex = 0;
type ObjectFit = 'cover' | 'contain';
$: carouselHeight =
interface Props {
/** Array of photos. */
photos: PhotoCarouselImage[];
/** Width of the component within the text well: normal, wide, wider, widest, fluid */
width?: ContainerWidth;
/**
* Set a different width for captions within the text well, e.g. "normal," to keep captions inline with the rest of the text well. Can't ever be wider than `width`.
*/
textWidth?: ContainerWidth;
/** Add an ID to target with SCSS. */
id?: string;
/** Add a class to target with SCSS. */
cls?: string;
/** Max height of the carousel */
maxHeight?: number;
/** Default Image object-fit style: cover, contain */
defaultImageObjectFit?: ObjectFit;
/** Default image object-position style, e.g., `center center` or `50% 50%`. */
defaultImageObjectPosition?: string;
/** ARIA label for the carousel. */
carouselAriaLabel?: string;
/** Set height of the carousel as a ratio of its width as long as its below whatever you set in `maxHeight`. */
heightRatio?: number;
/** Number of images to preload ahead of the active image. */
preloadImages?: number;
/** Optional custom credit format as a snippet, which takes the argument `photo`. */
credit?: Snippet<[PhotoCarouselImage]>;
/** Optional custom caption format as a snippet, which takes the argument `photo`. */
caption?: Snippet<[PhotoCarouselImage]>;
}
let {
width = 'wider',
textWidth = 'normal',
id = '',
cls = '',
photos,
maxHeight = 660,
defaultImageObjectFit = 'cover',
defaultImageObjectPosition = 'center center',
carouselAriaLabel = 'Photo gallery',
heightRatio = 0.68,
preloadImages = 1,
credit,
caption,
}: Props = $props();
let containerWidth: number | undefined = $state(undefined);
let activeImageIndex = $state(0);
// Derive carousel height based on container width
let carouselHeight = $derived(
containerWidth ?
Math.min(containerWidth * heightRatio, maxHeight)
: maxHeight;
: maxHeight
);
const handleActiveChange = (e?: CustomEvent<MoveEventDetail>) => {
if (!e) return;
@ -111,17 +89,17 @@
</script>
<Block {width} {id} class="photo-carousel fmy-6 {cls}">
<div class="carousel-container" bind:clientWidth="{containerWidth}">
<div class="carousel-container" bind:clientWidth={containerWidth}>
<Splide
hasTrack="{false}"
options="{{
hasTrack={false}
options={{
height: carouselHeight,
fixedHeight: carouselHeight,
lazyLoad: 'nearby',
preloadPages: preloadImages,
}}"
aria-label="{carouselAriaLabel}"
on:move="{handleActiveChange}"
}}
aria-label={carouselAriaLabel}
on:move={handleActiveChange}
>
<div class="image-container">
<SplideTrack>
@ -134,20 +112,20 @@
>
<img
class="w-full h-full fmy-0"
data-splide-lazy="{photo.src}"
alt="{photo.altText}"
style:object-fit="{photo.objectFit ||
defaultImageObjectFit}"
style:object-position="{photo.objectPosition ||
defaultImageObjectPosition}"
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}
{#if credit}
<!-- Render custom credit snippet -->
{@render credit(photo)}
{:else if photo.credit}
<span
class="credit absolute fmb-1 fml-1 leading-tighter font-note text-xxs"
class:contain-fit="{photo.objectFit === 'contain' ||
defaultImageObjectFit === 'contain'}"
class:contain-fit={photo.objectFit === 'contain' ||
defaultImageObjectFit === 'contain'}
>{photo.credit}</span
>
{/if}
@ -158,20 +136,20 @@
</SplideTrack>
{#if photos[activeImageIndex].caption}
<PaddingReset containerIsFluid="{width === 'fluid'}">
<Block width="{textWidth}">
{#if $$slots.caption}
<slot
name="caption"
caption="{photos[activeImageIndex].caption}"
/>
{:else}
{@const activePhoto = photos[activeImageIndex]}
<PaddingReset containerIsFluid={width === 'fluid'}>
<Block width={textWidth}>
{#if caption && activePhoto.caption}
{#key activeImageIndex}
{@render caption(activePhoto)}
{/key}
{:else if typeof activePhoto.caption}
{#key activeImageIndex}
<p
class="caption body-caption text-center"
in:fly|local="{{ x: 20, duration: 175 }}"
in:fly|local={{ x: 20, duration: 175 }}
>
{photos[activeImageIndex].caption}
{activePhoto.caption}
</p>
{/key}
{/if}
@ -185,10 +163,10 @@
<div class="splide__arrows fp-1">
<button class="splide__arrow splide__arrow--prev">
<Fa icon="{faChevronLeft}" fw />
<Fa icon={faChevronLeft} fw />
</button>
<button class="splide__arrow splide__arrow--next">
<Fa icon="{faChevronRight}" fw />
<Fa icon={faChevronRight} fw />
</button>
</div>
</div>

View file

@ -1,21 +0,0 @@
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}" />
```

View file

@ -1,27 +0,0 @@
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>
```