Merge pull request #260 from reuters-graphics/mf-video

Updates Video
This commit is contained in:
Jon McClure 2025-04-16 12:07:07 +01:00 committed by GitHub
commit 9d17cb3181
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 479 additions and 911 deletions

View file

@ -1,44 +0,0 @@
<script module lang="ts">
import TestForSvelte5 from './TestForSvelte5.svelte';
// Don't lose the "?raw" in markdown imports!
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
import { withComponentDocs } from '$docs/utils/withParams.js';
export const meta = {
title: 'Components/Graphics/TestForSvelte5',
component: TestForSvelte5,
...withComponentDocs(componentDocs),
// https://storybook.js.org/docs/svelte/essentials/controls
argTypes: {
width: {
control: 'select',
options: ['normal', 'wide', 'wider', 'widest', 'fluid'],
},
},
};
</script>
<script lang="ts">
import { Template, Story } from '@storybook/addon-svelte-csf';
// 🖼️ You can import images you need in stories directly in code!
// @ts-ignore img
import SharkImg from './stories/shark.jpg';
</script>
<Template>
{#snippet children({ ...args })}
<TestForSvelte5 {...args} />
{/snippet}
</Template>
<Story
name="Default"
args={{
width: 'normal',
src: SharkImg,
altText: "Duh dum! It's a shark!!",
}}
/>

View file

@ -1,60 +0,0 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `TestForSvelte5` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-graphics-testforsvelte5--docs) -->
<script lang="ts">
/** ✏️ DOCUMENT your chart's props using TypeScript and JSDoc comments like below! */
/**
* A source for the image.
* @required
*/
export let src: string;
/**
* AltText for the image.
* @required
*/
export let altText: string;
/** Height of the image. */
export let height: number = 500;
// You can declare custom types to help users implement your component.
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
/** Width of the component within the text well. */
export let width: 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 };
import Block from '../Block/Block.svelte';
</script>
<Block {width} {id} class="photo {cls}">
<div
style:background-image={`url(${src})`}
style:height={`${height}px`}
></div>
<p class="visually-hidden">{altText}</p>
</Block>
<style lang="scss">
div {
width: 100%;
background-repeat: no-repeat;
background-size: cover;
}
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>

View file

@ -1,23 +0,0 @@
> **Welcome to your new component!** Use this template to build your component and customise its storybook.
- Build your component in `TestForSvelte5/TestForSvelte5.svelte`.
- Write your component's storybook in `TestForSvelte5/TestForSvelte5.stories.svelte`.
- Don't forget to add your component to `src/index.js`:
```javascript
// ...
export { default as TestForSvelte5 } from './components/TestForSvelte5/TestForSvelte5.svelte';
```
- Commit your component to a new branch and push it to GitHub! 🏁
---
```html
<script>
import { TestForSvelte5 } from '@reuters-graphics/graphics-components';
</script>
<TestForSvelte5 />
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

View file

@ -1,79 +0,0 @@
<script lang="ts">
import Fa from 'svelte-fa';
import { faReply, faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let {
paused = $bindable(),
clickedOnPauseBtn = $bindable(),
controlsOpacity,
controlsPosition,
widthVideoContainer,
heightVideoContainer,
controlsBorderOffset,
resetCondition,
separateReplayIcon,
controlsColour,
} = $props();
function forwardBtnClick() {
paused = !paused;
clickedOnPauseBtn = paused === true; // so video doesn't autoplay when coming into view again if paused previously
dispatch('pausePlayEvent', {
paused,
clickedOnPauseBtn,
});
}
</script>
<button
onclick={forwardBtnClick}
style="
opacity: {controlsOpacity};
top: {controlsPosition === 'top left' || controlsPosition === 'top right' ?
`${10}px`
: controlsPosition === 'center' ?
`${(heightVideoContainer - controlsBorderOffset) / 2}px`
: `${heightVideoContainer - controlsBorderOffset}px`};
left: {(
controlsPosition === 'top left' || controlsPosition === 'bottom left'
) ?
`${10}px`
: controlsPosition === 'center' ?
`${(widthVideoContainer - controlsBorderOffset) / 2}px`
: `${widthVideoContainer - controlsBorderOffset}px`};
"
>
{#if resetCondition}
<i class="play-pause-icon replay">
{#if separateReplayIcon}
<Fa icon={faReply} size="2x" color={controlsColour} />
{:else}
<Fa icon={faPlay} size="2x" color={controlsColour} />
{/if}
</i>
{:else if paused === false}
<i class="play-pause-icon pause">
<Fa icon={faPause} size="2x" color={controlsColour} />
</i>
{:else if paused === true}
<i class="play-pause-icon play">
<Fa icon={faPlay} size="2x" color={controlsColour} />
</i>
{:else}
error
{/if}
</button>
<style lang="scss">
button {
z-index: 2;
position: absolute;
cursor: pointer;
background-color: transparent;
border: none;
}
</style>

View file

@ -0,0 +1,155 @@
import { Meta, Canvas } from '@storybook/blocks';
import * as VideoStories from './Video.stories.svelte';
<Meta of={VideoStories} />
# Video
The `Video` component adds a video with various controls to your page such as:
- Play/pause button
- Autoplay controls, i.e. whether the video plays when it comes into view or on page load
- Looping
- Audio controls
- Text elements such as notes and titles
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
</script>
<Video
ariaDescription="Required description of your video for screen readers."
src="my-video.mp4"
width="wide"
notes="Optional caption for your video."
/>
```
<Canvas of={VideoStories.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: video
src: videos/my-video.mp4
width: wide
ariaDescription: Required description of your video for screen readers.
notes: Optional caption for your video.
loopVideo: true
[]
```
... which you'll parse out of a ArchieML block object before passing to the `Video` component.
```svelte
<!-- App.svelte -->
<script>
import { Video } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // 👈 If using in the Graphics Kit...
import { truthy } from '$utils/propValidators'; // 👈 If using in the Graphics Kit...
</script>
{#each content.blocks as block}
{#if block.type === 'video'}
<Video
ariaDescription={block.ariaDescription}
src={`${assets}/${block.src}`}
width={block.width}
loopVideo={truthy(block.loopVideo)}
notes={block.notes}
/>
{/if}
{/each}
```
> **Note:** Some props, like `loopVideo`, expect boolean values. If you're using the Graphics Kit, you can use the `truthy()` util function to convert a string value to a boolean.
## Autoplay controls
By default, the video starts playing when 50% (0.5) of the video element's height comes into view. Adjust this with `playVideoThreshold`, which is a value between 0 and 1, where 0 means the video will start playing as soon as its top enters the viewport, and 1 means it will start when the whole video is in the viewport.
To make the video play on page load regardless of whether it is in view, set the prop `playVideoWhenInView` to `false`.
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
</script>
<Video
ariaDescription="Required description of your video for screen readers."
src="https://..."
loopVideo={true}
playVideoThreshold={0.9}
notes="World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters"
/>
```
<Canvas of={VideoStories.Autoplay} />
## Audio controls
On most browsers, [autoplaying videos with sound](https://developer.chrome.com/blog/autoplay#:~:text=Muted%20autoplay%20is%20always%20allowed,to%20allow%20autoplay%20with%20sound.) is allowed only if the user has interacted with the page. (Autoplay is allowed with muted videos.)
By default, this component will not autoplay videos with sound. To change this, set `soundAutoplay` to `true`. This will allow the video to autoplay with sound when it comes into view, but only if the user has already interacted with the page by clicking or tapping on it.
Test this with the example below: the video will autoplay when it comes into view _only if_ you have clicked or tapped on the page before scrolling down to it.
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
</script>
<Video
ariaDescription="Required description of your video for screen readers."
src="https://..."
controlsColour="#152a1c"
controlsOpacityMax={1}
controlsOpacityMin={0.5}
muteVideo={false}
soundAutoplay={true}
/>
```
<Canvas of={VideoStories.Audio} />
## Adding text
The `Video` component allows you to add a title, description and notes to your video, which are rendered by the `GraphicBlock` component.
Customise the `notes` section by passing a [snippet](https://svelte.dev/docs/svelte/snippet) instead of a string.
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
</script>
<Video
src="https://..."
ariaDescription="Required description of your video for screen readers."
title="Title for your video"
description="Description for your video"
>
<!-- Custom notes snippet -->
{#snippet notes()}
<aside>
<p class="notes">Custom-styled notes for the video.</p>
</aside>
{/snippet}
</Video>
<style lang="scss">
@use '@reuters-graphics/graphics-components/dist/scss/mixins' as mixins;
p.notes {
@include mixins.body-note;
}
</style>
```
<Canvas of={VideoStories.Text} />

View file

@ -1,46 +1,22 @@
<script module lang="ts">
// @ts-ignore raw
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore raw
import playAndLoopDocs from './stories/docs/playAndLoop.md?raw';
// @ts-ignore raw
import controlsDocs from './stories/docs/controls.md?raw';
// @ts-ignore raw
// import withSoundDocs from './stories/docs/withSound.md?raw';
import { defineMeta } from '@storybook/addon-svelte-csf';
import Video from './Video.svelte';
import {
withComponentDocs,
withStoryDocs,
} from '$lib/docs/utils/withParams.js';
export const meta = {
const { Story } = defineMeta({
title: 'Components/Multimedia/Video',
component: Video,
...withComponentDocs(componentDocs),
};
});
</script>
<script>
import { Template, Story } from '@storybook/addon-svelte-csf';
// @ts-ignore raw
import SilentVideo from './stories/videos/silent-video.mp4';
// @ts-ignore raw
import SoundVideo from './stories/videos/sound-video.mp4';
import SilentVideo from './demo/silent-video.mp4';
import SoundVideo from './demo/sound-video.mp4';
</script>
<Template>
{#snippet children({ args })}
<Video {...args} />
{/snippet}
</Template>
<Story
name="Default"
name="Demo"
args={{
ariaDescription: 'Compulsory description of your video for screen readers.',
ariaDescription: 'Required description of your video for screen readers.',
src: SilentVideo,
width: 'wide',
notes: 'Optional caption for your video.',
@ -48,52 +24,73 @@
/>
<Story
name="Playing and looping"
name="Autoplay controls"
exportName="Autoplay"
args={{
ariaDescription: 'Compulsory description of your video for screen readers.',
ariaDescription: 'Required description of your video for screen readers.',
src: SilentVideo,
width: 'normal',
loopVideo: true,
notes:
"World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters",
playVideoThreshold: 0.9,
}}
{...withStoryDocs(playAndLoopDocs)}
/>
<Story
name="Audio controls"
exportName="Audio"
args={{
ariaDescription: 'Required description of your video for screen readers.',
src: SoundVideo,
notes:
"World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters",
controlsColour: '#152a1c',
controlsOpacityMax: 1,
controlsOpacityMin: 0.5,
playVideoThreshold: 0.9,
muteVideo: false,
soundAutoplay: true,
}}
/>
<Story
name="Controls"
args={{
ariaDescription: 'Compulsory description of your video for screen readers.',
ariaDescription: 'Required description of your video for screen readers.',
src: SilentVideo,
width: 'normal',
notes:
"World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters",
playVideoThreshold: 0.9,
controlsColour: 'white',
controlsOpacity: 1,
controlsOpacityMax: 1,
controlsOpacityMin: 0.5,
controlsPosition: 'bottom right',
separateReplayIcon: true,
loopVideo: false,
hoverToSeeControls: true,
soundAutoplay: true,
}}
{...withStoryDocs(controlsDocs)}
/>
<Story
name="Videos with sound"
args={{
ariaDescription: 'Compulsory description of your video for screen readers.',
src: SoundVideo,
width: 'normal',
notes:
"World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters",
playVideoThreshold: 0.9,
showControls: true,
loopVideo: false,
muteVideo: false,
playVideoWhenInView: true,
allowSoundToAutoplay: true,
}}
{...withStoryDocs(controlsDocs)}
/>
<Story name="Text elements" exportName="Text">
<Video
src={SilentVideo}
ariaDescription="Required description of your video for screen readers."
title="Title for your video"
description="Description for your video"
>
{#snippet notes()}
<aside>
<p class="notes">Custom-styled notes for the video.</p>
</aside>
{/snippet}
</Video>
</Story>
<style lang="scss">
@use '../../scss/mixins' as mixins;
p.notes {
@include mixins.body-note;
}
</style>

View file

@ -1,169 +1,200 @@
<!-- @migration-task Error while migrating Svelte code: Cannot set properties of undefined (setting 'next') -->
<!-- @component `Video` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-multimedia-video--docs) -->
<script lang="ts">
import IntersectionObserver from 'svelte-intersection-observer';
import Controls from './Controls.svelte';
import GraphicBlock from '../GraphicBlock/GraphicBlock.svelte';
// Fa icons
import Fa from 'svelte-fa';
import { faReply, faPlay, faPause } from '@fortawesome/free-solid-svg-icons';
// types
import type { ContainerWidth } from '../@types/global';
import type { Snippet } from 'svelte';
import type { ControlsPosition } from './types';
import { getButtonPosition } from './utils';
/// //////////////////////////////////
/// /////////// Props ////////////////
/// //////////////////////////////////
interface Props {
/** Video source */
src: string;
/** Image to be shown while the video is downloading */
poster?: string;
/** ARIA description, passed in as a markdown string. */
ariaDescription: string;
/** Add extra classes to the block tag to target it with custom CSS. */
class?: string;
/** Title of the graphic */
title?: string;
/** Notes to the graphic, passed in as a markdown string OR a custom snippet. */
notes?: string | Snippet;
/** Description of the graphic, passed in as a markdown string. */
description?: string;
/** Width of the block within the article well. */
width?: ContainerWidth;
/** Set a different width for the text within the text well, for example, "normal" to keep the title, description and notes inline with the rest of the text well. Can't ever be wider than `width`. */
textWidth?: ContainerWidth;
/** Preload options. `auto` is ignored if `autoplay` is true. Can also be `none` or `metadata`. */
preloadVideo?: 'auto' | 'none' | 'metadata';
/** Whether the video should loop. */
loopVideo?: boolean;
/** Whether video should have sound or not. */
muteVideo?: boolean;
/** If `true`, this allow videos with sound to autoplay if the user has previously interacted with DOM */
soundAutoplay?: boolean;
/** Whether the video should play when it comes into view or just on page load */
playVideoWhenInView?: boolean;
/** Controls how much of the video should be visible when it starts playing. This is a number between 0 and 1, where 0 means the video will start playing as soon as its top enters the viewport, and 1 means it will start when the whole video is in the viewport. */
playVideoThreshold?: number;
/** Whether to have the option to pause and play video */
possibleToPlayPause?: boolean;
/** Whether to show the play / pause buttons */
showControls?: boolean;
/** Whether to use a separate replay icon or use the play icon for replay as well */
separateReplayIcon?: boolean;
/** Change the colour of the play/pause button */
controlsColour?: string;
/** Change the minimum opacity of the play/pause button, which you see on mouseover. Must be between 0 and 1. */
controlsOpacityMin?: number;
/** Change the maximum opacity of the play/pause button, which you see on mouseout. Must be between 0 and 1. */
controlsOpacityMax?: number;
/** Have four options for controls position - top right, top left, bottom right, bottom left */
controlsPosition?: ControlsPosition;
/** Offset for the controls from the border */
controlsBorderOffset?: number;
}
/**
* Video src
* @type {string}
* @required
*/
export let src: string;
/**
* Image to be shown while the video is downloading
*/
export let poster: string = '';
/**
* Whether to wrap the graphic with an aria hidden tag.
*/
export let hidden: boolean = true;
/**
* ARIA description, passed in as a markdown string.
* @type {string}
*/
export let ariaDescription: string | null = null;
/** Add extra classes to the block tag to target it with custom CSS. */
let cls: string = '';
export { cls as class };
/**
* Title of the graphic
* @type {string}
*/
export let title: string | null = null;
/**
* Notes to the graphic, passed in as a markdown string.
* @type {string}
*/
export let notes: string | null = null;
/**
* Description of the graphic, passed in as a markdown string.
* @type {string}
*/
export let description: string | null = null;
/**
* Width of the block within the article well.
* @type {string}
*/
export let width: ContainerWidth = 'normal';
type PreloadOptions = 'auto' | 'none' | 'metadata';
/**
* Set a different width for the text within the text well, for example,
* "normal" to keep the title, description and notes inline with the rest
* of the text well. Can't ever be wider than `width`.
* @type {string}
*/
export let textWidth: ContainerWidth | null = 'normal';
/**
* Preload options. `auto` is ignored if `autoplay` is true. Can also be `none` or `metadata`.
* @type {string}
*/
export let preloadVideo: PreloadOptions = 'auto';
/**
* Whether the video should loop.
*/
export let loopVideo: boolean = true;
/**
* Whether video should have sound or not.
*/
export let muteVideo: boolean = true;
export let allowSoundToAutoplay = false; // for video with sound, whether video should be allowed to autoplay if the user has previously interacted with DOM
export let playVideoWhenInView = true; // whether the video should play when it comes into view or just on page load
export let playVideoThreshold = 0.5; // if video plays with intersection observer, how much of it should be into view to start playing
export let possibleToPlayPause = true; // whether to have the option to pause and play video
export let showControls = true; // whetner to show the play / pause buttons
export let hoverToSeeControls = false; // whether you need to hover over the video to see the controls
export let separateReplayIcon = false; // whether to use a separate replay icon or use the play icon for replay as well
export let controlsColour = '#333'; // change the colour of the play/pause button
export let controlsOpacity = 0.5; // change the opacity of the play/pause button
$: interactiveControlsOpacity = 0;
export let controlsPosition = 'top left'; // have four options for controls position - top right, top left, bottom right, bottom left
let {
src,
poster = '',
ariaDescription,
class: cls = '',
title,
notes,
description,
width = 'normal',
textWidth = 'normal',
preloadVideo = 'auto',
loopVideo = false,
muteVideo = true,
soundAutoplay = false,
playVideoWhenInView = true,
playVideoThreshold = 0.5,
possibleToPlayPause = true,
showControls = true,
separateReplayIcon = false,
controlsColour = '#333',
controlsOpacityMin = 0,
controlsOpacityMax = 0.7,
controlsPosition = 'top left',
controlsBorderOffset = 10,
}: Props = $props();
/// //////////////////////////////////
/// /////// Internal Logic ///////////
/// //////////////////////////////////
// If it's not possible to play/pause, then hide the controls
if (!possibleToPlayPause) showControls = false;
// Internal props
let time = 0;
let duration;
let paused = true;
let clickedOnPauseBtn = false; // special variable to track if user clicked on 'pause' btn to help with audio logic
$: resetCondition = time >= duration; // - 0.1;
let paused = $state(false);
let time = $state(0);
let duration = $state(0);
// let paused = $state(true);
let clickedOnPauseBtn = $state(false); // special variable to track if user clicked on 'pause' btn to help with audio logic
let resetCondition = $derived(time >= duration); // - 0.1;
// Dimensions etc other useful things
let heightVideo;
let widthVideo;
let heightVideoContainer;
let widthVideoContainer;
const controlsBorderOffset = 50;
let videoHeight = $state(0);
let videoWidth = $state(0);
let videoHeightContainer = $state(0);
let videoWidthContainer = $state(0);
// For intersection observer
let intersecting;
let element;
let videoElement;
let intersecting = $state(false);
let element: HTMLElement | undefined = $state(undefined); // ; | null
let videoElement: HTMLVideoElement | undefined = $state(undefined);
// For video with sound, check if there has been an interaction with the DOM
let interactedWithDom = false;
const setInteractedWithDom = () => {
interactedWithDom = true;
};
const setInteractedWithDom = () => (interactedWithDom = true);
// Play the video (with no sound) if it's intersecting; pause when it's no longer intersecting
$: if (playVideoWhenInView && intersecting && muteVideo) paused = false;
$: if (playVideoWhenInView && !intersecting) paused = true;
// Special case for video with sound
// Only ff you've clicked on play button or interacted with DOM in any way previously, video with audio will play
$: if (
allowSoundToAutoplay &&
playVideoWhenInView &&
intersecting &&
!muteVideo &&
interactedWithDom &&
!clickedOnPauseBtn // so video doesn't autoplay when coming into view again if paused previously
) {
paused = false;
}
let interactiveControlsOpacity = $state(controlsOpacityMax);
$: if (allowSoundToAutoplay && !muteVideo && !interactedWithDom) {
paused = true;
}
// Get control button positioning
let controlButtonPosition = $derived(
getButtonPosition(controlsPosition, controlsBorderOffset)
);
$: if (!possibleToPlayPause) showControls = true;
/** Control play/pause */
$effect(() => {
// Play the video (with no sound) if it's intersecting; pause when it's no longer intersecting
if (playVideoWhenInView && intersecting && muteVideo) paused = false;
// To get the pause state passed up from the Controls
const pausePlayEvent = (e) => {
const fwdPaused = e.detail.paused;
const fwdClickedOnPauseBtn = e.detail.clickedOnPauseBtn;
paused = fwdPaused;
clickedOnPauseBtn = fwdClickedOnPauseBtn;
};
// Pause the video if it's no longer intersecting
if (playVideoWhenInView && !intersecting) paused = true;
// Warning to missing aria attributes
if (hidden && !ariaDescription) {
console.warn(
'Must provide aria description for video components if hidden is true.'
);
}
// Special case for video with sound
// Only ff you've clicked on play button or interacted with DOM in any way previously, video with audio will play
if (
soundAutoplay &&
playVideoWhenInView &&
intersecting &&
!muteVideo &&
interactedWithDom &&
!clickedOnPauseBtn // so video doesn't autoplay when coming into view again if paused previously
)
paused = false;
if (soundAutoplay && !muteVideo && !interactedWithDom) paused = true;
});
</script>
<!-- Controls button snippet -->
{#snippet controls()}
<button
class="controls"
onclick={() => {
paused = !paused;
clickedOnPauseBtn = paused === true; // so video doesn't autoplay when coming into view again if paused previously
}}
style="
opacity: {interactiveControlsOpacity};
{controlButtonPosition}
"
>
{#if resetCondition}
<i class="play-pause-icon replay">
{#if separateReplayIcon}
<Fa icon={faReply} size="2x" color={controlsColour} />
{:else}
<Fa icon={faPlay} size="2x" color={controlsColour} />
{/if}
</i>
{:else if paused === false}
<i class="play-pause-icon pause">
<Fa icon={faPause} size="2x" color={controlsColour} />
</i>
{:else if paused === true}
<i class="play-pause-icon play">
<Fa icon={faPlay} size="2x" color={controlsColour} />
</i>
{:else}
error
{/if}
</button>
{/snippet}
<!-- Transparent, invisible button that will pause/play when the video is clicked -->
{#snippet transparentButton()}
<button
class="border-0 m-0 p-0 bg-transparent absolute"
aria-label="Play or pause video"
onclick={() => {
paused = !paused;
}}
style="top: 0; left: 0; width: {videoWidthContainer}px; height: {videoHeightContainer}px;"
></button>
{/snippet}
<svelte:window
on:click={setInteractedWithDom}
on:touchstart={setInteractedWithDom}
@ -173,131 +204,44 @@
{textWidth}
{title}
{description}
{notes}
notes={typeof notes === 'string' ? notes : undefined}
{width}
class="video {cls}"
>
<div
role="figure"
on:mouseover={() => {
interactiveControlsOpacity = controlsOpacity;
onmouseover={() => {
interactiveControlsOpacity = controlsOpacityMax;
}}
on:focus={() => {
interactiveControlsOpacity = controlsOpacity;
onfocus={() => {
interactiveControlsOpacity = controlsOpacityMax;
}}
on:mouseout={() => {
interactiveControlsOpacity = 0;
onmouseout={() => {
interactiveControlsOpacity = controlsOpacityMin;
}}
on:blur={() => {
interactiveControlsOpacity = 0;
onblur={() => {
interactiveControlsOpacity = controlsOpacityMin;
}}
>
{#if (hidden && ariaDescription) || !hidden}
{#if ariaDescription}
<p class="visually-hidden">{ariaDescription}</p>
{/if}
{#if playVideoWhenInView}
<!-- Video element with Intersection Observer -->
<IntersectionObserver
{element}
bind:intersecting
threshold={playVideoThreshold}
once={false}
>
<div
bind:this={element}
class="video-wrapper relative block"
aria-hidden={hidden}
bind:clientWidth={widthVideoContainer}
bind:clientHeight={heightVideoContainer}
>
{#if possibleToPlayPause}
{#if showControls}
<Controls
on:pausePlayEvent={pausePlayEvent}
{paused}
{clickedOnPauseBtn}
controlsOpacity={hoverToSeeControls ?
interactiveControlsOpacity
: controlsOpacity}
{controlsPosition}
{widthVideoContainer}
{heightVideoContainer}
{controlsBorderOffset}
{resetCondition}
{separateReplayIcon}
{controlsColour}
/>
{:else}
<button
class="border-0 m-0 p-0 bg-transparent absolute"
on:click={() => {
if (paused === true) {
paused = false;
} else {
paused = true;
}
}}
style="top: 0; left: 0; width: {widthVideoContainer}px; height: {heightVideoContainer}px;"
></button>
{/if}
{/if}
<video
bind:this={videoElement}
{src}
{poster}
class="pointer-events-none relative"
width="100%"
muted={muteVideo}
playsinline
preload={preloadVideo}
loop={loopVideo}
bind:currentTime={time}
bind:duration
bind:paused
bind:clientWidth={widthVideo}
bind:clientHeight={heightVideo}
>
<track kind="captions" />
</video>
</div>
</IntersectionObserver>
{:else}
<!-- Video element without Intersection observer -->
{#if playVideoWhenInView}
<!-- Video element with Intersection Observer -->
<IntersectionObserver
{element}
bind:intersecting
threshold={playVideoThreshold}
once={false}
>
<div
class="video-wrapper relative"
aria-hidden={hidden}
bind:clientWidth={widthVideoContainer}
bind:clientHeight={heightVideoContainer}
bind:this={element}
class="video-wrapper relative block"
bind:clientWidth={videoWidthContainer}
bind:clientHeight={videoHeightContainer}
>
{#if possibleToPlayPause}
{#if showControls}
<Controls
on:pausePlayEvent={pausePlayEvent}
{paused}
{clickedOnPauseBtn}
{controlsOpacity}
{controlsPosition}
{widthVideoContainer}
{heightVideoContainer}
{controlsBorderOffset}
{resetCondition}
{separateReplayIcon}
{controlsColour}
/>
{@render controls()}
{:else}
<button
class="border-0 m-0 p-0 bg-transparent absolute"
on:click={() => {
if (paused === true) {
paused = false;
} else {
paused = true;
}
}}
style="top: 0; left: 0; width: {widthVideoContainer}px; height: {heightVideoContainer}px;"
></button>
{@render transparentButton()}
{/if}
{/if}
<video
@ -310,21 +254,66 @@
playsinline
preload={preloadVideo}
loop={loopVideo}
aria-label={ariaDescription}
bind:currentTime={time}
bind:duration
bind:paused
autoplay
bind:clientWidth={widthVideo}
bind:clientHeight={heightVideo}
bind:clientWidth={videoWidth}
bind:clientHeight={videoHeight}
>
<track kind="captions" />
</video>
</div>
{/if}
</IntersectionObserver>
{:else}
<!-- Video element without Intersection observer -->
<div
class="video-wrapper relative"
bind:clientWidth={videoWidthContainer}
bind:clientHeight={videoHeightContainer}
>
{#if possibleToPlayPause}
{#if showControls}
{@render controls()}
{:else}
{@render transparentButton()}
{/if}
{/if}
<video
bind:this={videoElement}
{src}
{poster}
class="pointer-events-none relative"
width="100%"
muted={muteVideo}
playsinline
preload={preloadVideo}
loop={loopVideo}
bind:currentTime={time}
bind:duration
bind:paused
autoplay
bind:clientWidth={videoWidth}
bind:clientHeight={videoHeight}
>
<track kind="captions" />
</video>
</div>
{/if}
</div>
{#if $$slots.notes}
<!-- Custom notes and source slot -->
<slot name="notes" />
<!-- Custom notes snippet -->
{#if notes && typeof notes !== 'string'}
{@render notes()}
{/if}
</GraphicBlock>
<style lang="scss">
button.controls {
z-index: 2;
position: absolute;
cursor: pointer;
background-color: transparent;
border: none;
transition: opacity 0.2s;
}
</style>

View file

@ -1,275 +0,0 @@
---
title: Video
description: General-purpose video component. Can play on load or when the video comes into view and has play/pause controls. Supports videos with or without audio.
slug: video
---
<script>
import {assets} from '$app/paths'; import Video from './index.svelte'; import
DemoContainer from '../_docs/DemoContainer/index.svelte';
</script>
<section>
## {title}
{description}
</section>
```svelte
<script>
import { assets } from '$app/paths'; // helper if importing video from 'statics'
import { Video } from '@reuters-graphics/graphics-svelte-components';
</script>
<Video ariaDescription={"Compulsory description of your video for screen
readers."} src={`${assets}/videos/myVideo.mp4`} // or a URL to an external video
width={'wide'} // normal, wide, wider, widest or fluid caption={'Optional
caption for your video.'} />
```
<DemoContainer>
<Video
ariaDescription="Description of your video for screen readers."
src={`${assets}/videos/Sequence silent video_4.mp4`}
width="wide"
caption="World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters"
/>
</DemoContainer>
<section>
#### When to play and whether to loop
`playVideoWhenInView`, `playVideoThreshold`
- By default, the video will **start playing when 50% of the video element's height is visible on the page**.
To control the threshold of visibility at which the video starts playing, add the prop `playVideoThreshold` and set it to a value between 0 and 1,
where 0 means that the video will start playing as soon as its top enters the viewport, while 1 means it will start when the whole video is in the viewport.
- If you don't want the video to play when you scroll to it, but **on page load**, add the prop `playVideoWhenInView={false}`. The default of the prop is `true`,
which corresponds to the behaviour described above.
`loopVideo`
- By default, the video will **loop**. If you don't want that, add the prop `loopVideo={false}`.
Here is an example of what the same video would look like with a visibility threshold of 0.9 and not looping. Scroll down slowly to observe the behaviour.
</section>
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-svelte-components';
</script>
<video
ariaDescription="{'Compulsory description of your video for screen readers.'}"
src="{'path-to-video-or-external-url'}"
width="{'normal'}"
loopVideo="{false}"
playVideoThreshold="{0.9}"></video>
```
<DemoContainer>
<Video
ariaDescription="Description of your video for screen readers."
src={`${assets}/videos/Sequence silent video_4.mp4`}
width="normal"
caption="World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters"
loopVideo={false}
playVideoThreshold={0.9}
/>
</DemoContainer>
<section>
#### Controls (play / pause)
`showControls`
- By default, the video has a play/pause button, which corresponds to
`showControls={true}`. If you don't want these, just set `showControls={false}`.
When you do that, the icons themselves will disappear but the functionality of playing and pausing remains and can be done by clicking on the video itself.
If you don't want to enable any play/pause functionality, add `possibleToPlayPause={false}`.
- If you want to show the controls only when the video is hovered, set `hoverToSeeControls={true}`.
`controlsColour`, `controlsOpacity`, `controlsPosition`, `separateReplayIcon`
If you do want to leave the controls, you have a couple of options to style them:
- Set `controlsColour` to a colour of your choosing.
- Set `controlsOpacity` to a value between `0` and `1` to control the opacity. The default is `0.5`.
- Change the placement of the controls to one of: `top right`, `top left`, `bottom right`, `bottom left` by setting `controlsPosition`.
- Change the play button to a replay button at the end of the video with the option `separateReplayIcon={true}`.
Here is an example with bottom right corner white opaque controls, with a replay button, where you have to hover on the video to see the controls.
</section>
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-svelte-components';
</script>
<Video ariaDescription={"Compulsory description of your video for screen
readers."} src={'path-to-video-or-external-url'} size={'normal'}
caption={'Optional caption for your video.'} playVideoThreshold={0.1}
controlsColour={'white'} controlsOpacity={1} controlsPosition={'bottom right'}
separateReplayIcon={true} loopVideo={false} // If you don't set loopVideo to
false, you won't see the loop button hoverToSeeControls={true} />
```
<DemoContainer>
<Video
ariaDescription="Description of your video for screen readers."
src={`${assets}/videos/Sequence silent video_4.mp4`}
size="normal"
caption="World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters"
playVideoThreshold={0.1}
controlsColour={'white'}
controlsOpacity={1}
controlsPosition={'bottom right'}
separateReplayIcon={true}
loopVideo={false}
hoverToSeeControls={true}
/>
</DemoContainer>
<section>
#### Videos with sound
If you've ever had to put sound on a page in recent years,
you'll know that auto-playing sound is not allowed by browsers. The user will need to interact with the page first, and it will depend on your
particular use case how and when you'd like this to happen. This component provides two options to deal with this.
If you have a video with sound, make sure to add the prop `muteVideo={false}`.
Then you can either:
- `allowSoundToAutoplay={false}` (default) : Don't allow the video to autoplay under any circumstances other than when the user clicks the 'play' on the video. Note that this
works whether or not you have the controls visible, i.e. with `showControls` being `true` or `false`, as long as you allow
play/pause behaviour with `possibleToPlayPause={true}` (default).
- `allowSoundToAutoplay={true}` : Allow the video to autoplay when it comes into view as long as the user has interacted with the page preivously, i.e. they have clicked/tapped
anywhere on the page.
You should keep `playVideoWhenInView={true}` (default). There is no option to autoplay video with sound when the user clicks on the page
elsewhere if the video is not in view. In other words, you can't start playing sound for a video which is not in view with this component.
This is probably not a behaviour you'd want anyway.
The example below allows for autoplay if the user has interacted with the page before the video comes into view. To see this, reload the page
and go to the top. Click anywhere on the page before scrolling down to the video and you should see it autoplay when it comes into view.
</section>
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-svelte-components';
</script>
<video
ariaDescription="{'Compulsory description of your video for screen readers.'}"
src="{'path-to-video-or-external-url'}"
width="{'normal'}"
controlsOpacity="{1}"
loopVideo="{false}"
muteVideo="{false}"
allowSoundToAutoplay="{true}"></video>
```
<DemoContainer>
<Video
ariaDescription="Description of your video for screen readers."
src={`${assets}/videos/Sequence sound video.mp4`}
width="normal"
caption="World's longest glass bridge opens to public in Vietnam. (c) 2022 Thomson Reuters"
controlsOpacity={1}
loopVideo={false}
muteVideo={false}
playVideoWhenInView={true}
showControls={true}
allowSoundToAutoplay={true}
/>
</DemoContainer>
<section>
### All the props
Here is a list of all the props that you can pass to the component for reference. Most of them are discussed above.
##### Required
- **src** (string) - Path to the video relative to the `statics` folder.
- **ariaDescription** (string) and **ariaHidden** (bool) - Either write a description for screen readers in ariaDescription or set ariaHidden to false.
##### Optional
- **caption** (string) - default <span class='default'>**" " (none)**</span> <span class='separator'></span> options: Write a caption to go with the video.
- **size** (string) - default <span class='default'>**'normal'**</span> <span class='separator'></span> options: _'normal'_, _'wide'_, _'wider'_, _'widest'_, _'fluid'_
- **preloadVideo** (string) - default <span class='default'>**'auto'**</span> <span class='separator'></span>
options: _'none'_ (Don't preload the video on page load),
_'auto'_ (Preload the video),
_'metadata'_ (Only preload the metadata)
- **loopVideo** (bool) - default <span class='default'>**true**</span> <span class='separator'></span> options: _true_, _false_
- **muteVideo** (bool) - default <span class='default'>**true**</span> <span class='separator'></span> options: _true_, _false_
- **allowSoundToAutoplay** (bool) - default <span class='default'>**false**</span><span class='separator'></span> options: _true_, _false_.
For video with sound, whether video should be allowed to autoplay if the user has previously interacted with DOM. You need to set `muteVideo` to `true`
for this to work.
- **playVideoWhenInView** (bool) - default <span class='default'>**true**</span> <span class='separator'></span>
options: _true_ (Only start playing the video when it comes into view), _false_ (Start playing as soon as the page and video load)
- **playVideoThreshold** (float) - default <span class='default'>**0.5**</span> <span class='separator'></span> options: _float between 0 and 1_.
How much of the video should be into view to start playing when playVideoWhenInView is true.
- **possibleToPlayPause** (bool) - default <span class='default'>**true**</span> <span class='separator'></span> options: _true_, _false_
- **showControls** (bool) - default <span class='default'>**true**</span> <span class='separator'></span> options: _true_, _false_.
Whether to show the play/pause controls or not.
- **hoverToSeeControls** (bool) - default <span class='default'>**false**</span><span class='separator'></span> options: _true_, _false_.
Whether you need to hover over the video to see the controls.
- **separateReplayIcon** (bool) - default <span class='default'>**false**</span> <span class='separator'></span> options: _true_, _false_.
Whether to use a separate replay icon or use the play icon for replay as well.
- **controlsColour** (colour string) - default <span class='default'>**'#333'**</span> <span class='separator'></span> options: _any valid colour string_.
Controls the colour of the play/pause buttons.
- **controlsOpacity** (float) - default <span class='default'>**0.5**</span> <span class='separator'></span> options: _float between 0 and 1_.
Controls the opacity of the play/pause buttons.
- **controlsPosition** (string) - default <span class='default'>**'top left'**</span> <span class='separator'></span>
options: _'top right'_, _'top left'_, _'bottom right'_, _'bottom left'_.
Controls the position of the play/pause buttons.
</section>
<style>
ul {
list-style-type: none;
}
li {
font-size: 1.2rem;
}
.default {
color: #006d77;
}
.separator {
display: inline-block;
width: 5px;
height: 5px;
background-color: #666;
border-radius: 50%;
margin: 0 5px 0 5px;
transform: translateY(-2.5px);
}
</style>

View file

@ -1,15 +0,0 @@
General-purpose video component. Can play on load or when the video comes into view and has play/pause controls. Supports videos with or without audio.
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
import { assets } from '$app/paths'; // If using local video in the Graphics Kit
</script>
<Video
ariaDescription="{'Compulsory description of your video for screen readers.'}"
src="{`${assets}/videos/myVideo.mp4`}"
width="{'wide'}"
caption="{'Optional caption for your video.'}"
/>
```

View file

@ -1,38 +0,0 @@
`showControls`
- By default, the video has a play/pause button, which corresponds to
`showControls={true}`. If you don't want these, just set `showControls={false}`.
When you do that, the icons themselves will disappear but the functionality of playing and pausing remains and can be done by clicking on the video itself.
If you don't want to enable any play/pause functionality, add `possibleToPlayPause={false}`.
- If you want to show the controls only when the video is hovered, set `hoverToSeeControls={true}`.
`controlsColour`, `controlsOpacity`, `controlsPosition`, `separateReplayIcon`
If you do want to leave the controls, you have a couple of options to style them:
- Set `controlsColour` to a colour of your choosing.
- Set `controlsOpacity` to a value between `0` and `1` to control the opacity. The default is `0.5`.
- Change the placement of the controls to one of: `top right`, `top left`, `bottom right`, `bottom left`, `center` by setting `controlsPosition`.
- Change the play button to a replay button at the end of the video with the option `separateReplayIcon={true}`.
Here is an example with bottom right corner white opaque controls, with a replay button, where you have to hover on the video to see the controls.
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
</script>
<Video
ariaDescription="{'Compulsory description of your video for screen readers.'}"
src="{'path-to-video-or-external-url'}"
width="{'normal'}"
caption="{'Optional caption for your video.'}"
playVideoThreshold="{0.1}"
controlsColour="{'white'}"
controlsOpacity="{1}"
controlsPosition="{'bottom right'}"
separateReplayIcon="{true}"
loopVideo="{false}"
hoverToSeeControls="{true}"
/>
```

View file

@ -1,28 +0,0 @@
`playVideoWhenInView`, `playVideoThreshold`
- By default, the video will **start playing when 50% of the video element's height is visible on the page**.
To control the threshold of visibility at which the video starts playing, add the prop `playVideoThreshold` and set it to a value between 0 and 1,
where 0 means that the video will start playing as soon as its top enters the viewport, while 1 means it will start when the whole video is in the viewport.
- If you don't want the video to play when you scroll to it, but **on page load**, add the prop `playVideoWhenInView={false}`. The default of the prop is `true`,
which corresponds to the behaviour described above.
`loopVideo`
- By default, the video will **loop**. If you don't want that, add the prop `loopVideo={false}`.
Here is an example of what the same video would look like with a visibility threshold of 0.9 and not looping. Scroll down slowly to observe the behaviour.
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
</script>
<Video
ariaDescription="{'Compulsory description of your video for screen readers.'}"
src="{'path-to-video-or-external-url'}"
width="{'normal'}"
loopVideo="{false}"
playVideoThreshold="{0.9}"
/>
```

View file

@ -1,36 +0,0 @@
If you've ever had to put sound on a page in recent years,
you'll know that auto-playing sound is not allowed by browsers. The user will need to interact with the page first, and it will depend on your
particular use case how and when you'd like this to happen. This component provides two options to deal with this.
If you have a video with sound, make sure to add the prop `muteVideo={false}`.
Then you can either:
- `allowSoundToAutoplay={false}` (default) : Don't allow the video to autoplay under any circumstances other than when the user clicks the 'play' on the video. Note that this
works whether or not you have the controls visible, i.e. with `showControls` being `true` or `false`, as long as you allow
play/pause behaviour with `possibleToPlayPause={true}` (default).
- `allowSoundToAutoplay={true}` : Allow the video to autoplay when it comes into view as long as the user has interacted with the page preivously, i.e. they have clicked/tapped
anywhere on the page.
You should keep `playVideoWhenInView={true}` (default). There is no option to autoplay video with sound when the user clicks on the page
elsewhere if the video is not in view. In other words, you can't start playing sound for a video which is not in view with this component.
This is probably not a behaviour you'd want anyway.
The example below allows for autoplay if the user has interacted with the page before the video comes into view. To see this, reload the page
and go to the top. Click anywhere on the page before scrolling down to the video and you should see it autoplay when it comes into view.
```svelte
<script>
import { Video } from '@reuters-graphics/graphics-components';
</script>
<Video
ariaDescription="{'Compulsory description of your video for screen readers.'}"
src="{'path-to-video-or-external-url'}"
width="{'normal'}"
controlsOpacity="{1}"
loopVideo="{false}"
muteVideo="{false}"
allowSoundToAutoplay="{true}"
/>
```

View file

@ -0,0 +1,6 @@
export type ControlsPosition =
| 'middle'
| 'top right'
| 'top left'
| 'bottom right'
| 'bottom left';

View file

@ -0,0 +1,19 @@
import type { ControlsPosition } from './types';
/** Returns the CSS positions for the controls button */
export const getButtonPosition = (
controlsPosition: ControlsPosition,
borderOffset: number
) => {
if (controlsPosition === 'top left')
return `top: ${borderOffset}px; left: ${borderOffset}px;`;
if (controlsPosition === 'top right')
return `top: ${borderOffset}px; right: ${borderOffset}px;`;
if (controlsPosition === 'bottom left')
return `bottom: ${borderOffset}px; left: ${borderOffset}px;`;
if (controlsPosition === 'bottom right')
return `bottom: ${borderOffset}px; right: ${borderOffset}px;`;
// Otherwise, centre it
return `top: 50%; left: 50%; transform: translate(-50%, -50%);`;
};