Skip to content

Commit 052302b

Browse files
committed
Refactor loop video logic out of presentation
1 parent e74da9e commit 052302b

File tree

3 files changed

+178
-92
lines changed

3 files changed

+178
-92
lines changed

dotcom-rendering/src/components/LoopVideo.importable.tsx

Lines changed: 20 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,18 @@
11
import { css } from '@emotion/react';
22
import { log } from '@guardian/libs';
3-
import { space } from '@guardian/source/foundations';
43
import { SvgAudio, SvgAudioMute } from '@guardian/source/react-components';
54
import { useEffect, useRef, useState } from 'react';
65
import { getZIndex } from '../lib/getZIndex';
76
import { useIsInView } from '../lib/useIsInView';
87
import { useShouldAdapt } from '../lib/useShouldAdapt';
9-
import { palette } from '../palette';
10-
import { narrowPlayIconWidth, PlayIcon } from './Card/components/PlayIcon';
118
import { useConfig } from './ConfigContext';
12-
import { LoopVideoProgressBar } from './LoopVideoProgressBar';
9+
import { LoopVideoPlayer } from './LoopVideoPlayer';
1310

1411
const videoContainerStyles = css`
1512
z-index: ${getZIndex('loop-video-container')};
16-
cursor: pointer;
1713
position: relative;
1814
`;
1915

20-
const videoStyles = css`
21-
position: relative;
22-
width: 100%;
23-
height: auto;
24-
/* Find out why this is needed to align the video with its container. */
25-
margin-bottom: -3px;
26-
`;
27-
28-
const playIconStyles = css`
29-
position: absolute;
30-
top: calc(50% - ${narrowPlayIconWidth / 2}px);
31-
left: calc(50% - ${narrowPlayIconWidth / 2}px);
32-
cursor: pointer;
33-
border: none;
34-
background: none;
35-
padding: 0;
36-
`;
37-
38-
const audioButtonStyles = css`
39-
border: none;
40-
background: none;
41-
padding: 0;
42-
position: absolute;
43-
bottom: ${space[8]}px;
44-
right: ${space[8]}px;
45-
cursor: pointer;
46-
`;
47-
4816
type Props = {
4917
src: string;
5018
videoId: string;
@@ -78,8 +46,6 @@ export const LoopVideo = ({
7846
const [isInView, setNode] = useIsInView({
7947
repeat: true,
8048
threshold: 0.5,
81-
node: vidRef.current ?? undefined,
82-
debounce: true,
8349
});
8450

8551
/**
@@ -167,7 +133,7 @@ export const LoopVideo = ({
167133
};
168134

169135
const handleKeyDown = (
170-
event: React.KeyboardEvent<HTMLDivElement>,
136+
event: React.KeyboardEvent<HTMLVideoElement>,
171137
): void => {
172138
switch (event.key) {
173139
case 'Enter':
@@ -189,66 +155,28 @@ export const LoopVideo = ({
189155
<div
190156
className="loop-video-container"
191157
ref={setNode}
192-
onClick={handleClick}
193-
onKeyDown={handleKeyDown}
194-
role="button"
195-
tabIndex={0}
196158
css={videoContainerStyles}
197159
>
198-
{/* eslint-disable-next-line jsx-a11y/media-has-caption -- Captions will be considered later. */}
199-
<video
200-
id={`loop-video-${videoId}`}
201-
ref={vidRef}
202-
preload="none"
203-
loop={true}
204-
muted={isMuted}
205-
playsInline={true}
206-
height={height}
160+
<LoopVideoPlayer
161+
src={src}
162+
videoId={videoId}
207163
width={width}
208-
onPlaying={() => {
209-
setIsPlaying(true);
210-
}}
211-
onCanPlay={() => {
212-
setIsPlayable(true);
213-
}}
164+
height={height}
165+
hasAudio={hasAudio}
166+
fallbackImage={fallbackImage}
167+
ref={vidRef}
168+
isPlayable={isPlayable}
169+
setIsPlayable={setIsPlayable}
170+
isPlaying={isPlaying}
171+
setIsPlaying={setIsPlaying}
172+
isMuted={isMuted}
173+
setIsMuted={setIsMuted}
174+
handleClick={handleClick}
175+
handleKeyDown={handleKeyDown}
214176
onError={onError}
215-
css={videoStyles}
216-
>
217-
{/* Ensure webm source is provided */}
218-
{/* <source src={webmSrc} type="video/webm"> */}
219-
<source src={src} type="video/mp4" />
220-
{fallbackImage}
221-
</video>
222-
{vidRef.current && (
223-
<>
224-
{isPlayable && !isPlaying && (
225-
<div css={playIconStyles}>
226-
<PlayIcon iconWidth="narrow" />
227-
</div>
228-
)}
229-
<LoopVideoProgressBar
230-
currentTime={elapsedTime}
231-
duration={vidRef.current.duration}
232-
/>
233-
{hasAudio && (
234-
<button
235-
type="button"
236-
onClick={(event) => {
237-
event.stopPropagation(); // Don't pause the video
238-
setIsMuted(!isMuted);
239-
}}
240-
css={audioButtonStyles}
241-
>
242-
<AudioIcon
243-
size="small"
244-
theme={{
245-
fill: palette('--loop-video-audio-icon'),
246-
}}
247-
/>
248-
</button>
249-
)}
250-
</>
251-
)}
177+
elapsedTime={elapsedTime}
178+
AudioIcon={AudioIcon}
179+
/>
252180
</div>
253181
);
254182
};

dotcom-rendering/src/components/LoopVideo.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { breakpoints } from '@guardian/source/foundations';
12
import type { Meta, StoryObj } from '@storybook/react';
23
import { centreColumnDecorator } from '../../.storybook/decorators/gridDecorators';
34
import { CardPicture } from './CardPicture';
@@ -8,6 +9,11 @@ export default {
89
title: 'Components/LoopVideo',
910
decorators: [centreColumnDecorator],
1011
render: (args) => <LoopVideo {...args} />,
12+
parameters: {
13+
chromatic: {
14+
viewports: [breakpoints.mobile, breakpoints.wide],
15+
},
16+
},
1117
} satisfies Meta<typeof LoopVideo>;
1218

1319
export const Default = {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { css } from '@emotion/react';
2+
import { space } from '@guardian/source/foundations';
3+
import type { IconProps } from '@guardian/source/react-components';
4+
import type { Dispatch, SetStateAction, SyntheticEvent } from 'react';
5+
import { forwardRef } from 'react';
6+
import { palette } from '../palette';
7+
import { narrowPlayIconWidth, PlayIcon } from './Card/components/PlayIcon';
8+
import { LoopVideoProgressBar } from './LoopVideoProgressBar';
9+
10+
const videoStyles = css`
11+
position: relative;
12+
width: 100%;
13+
height: auto;
14+
/* Find out why this is needed to align the video with its container. */
15+
margin-bottom: -3px;
16+
cursor: pointer;
17+
`;
18+
19+
const playIconStyles = css`
20+
position: absolute;
21+
top: calc(50% - ${narrowPlayIconWidth / 2}px);
22+
left: calc(50% - ${narrowPlayIconWidth / 2}px);
23+
cursor: pointer;
24+
border: none;
25+
background: none;
26+
padding: 0;
27+
`;
28+
29+
const audioButtonStyles = css`
30+
border: none;
31+
background: none;
32+
padding: 0;
33+
position: absolute;
34+
bottom: ${space[8]}px;
35+
right: ${space[8]}px;
36+
cursor: pointer;
37+
`;
38+
39+
type Props = {
40+
src: string;
41+
videoId: string;
42+
width: number;
43+
height: number;
44+
hasAudio: boolean;
45+
fallbackImage: JSX.Element;
46+
isPlayable: boolean;
47+
setIsPlayable: Dispatch<SetStateAction<boolean>>;
48+
isPlaying: boolean;
49+
setIsPlaying: Dispatch<SetStateAction<boolean>>;
50+
isMuted: boolean;
51+
setIsMuted: Dispatch<SetStateAction<boolean>>;
52+
handleClick: (event: SyntheticEvent) => void;
53+
handleKeyDown: (event: React.KeyboardEvent<HTMLVideoElement>) => void;
54+
onError: (event: SyntheticEvent<HTMLVideoElement>) => void;
55+
elapsedTime: number;
56+
AudioIcon: (iconProps: IconProps) => JSX.Element;
57+
};
58+
59+
/**
60+
* Note that in React 19, forwardRef is no longer necessary:
61+
* https://react.dev/reference/react/forwardRef
62+
*/
63+
export const LoopVideoPlayer = forwardRef(
64+
(
65+
{
66+
src,
67+
videoId,
68+
width,
69+
height,
70+
hasAudio,
71+
fallbackImage,
72+
isPlayable,
73+
setIsPlayable,
74+
isPlaying,
75+
setIsPlaying,
76+
isMuted,
77+
setIsMuted,
78+
handleClick,
79+
handleKeyDown,
80+
onError,
81+
elapsedTime,
82+
AudioIcon,
83+
}: Props,
84+
ref: React.ForwardedRef<HTMLVideoElement>,
85+
) => {
86+
return (
87+
<>
88+
{/* eslint-disable-next-line jsx-a11y/media-has-caption -- Captions will be considered later. */}
89+
<video
90+
id={`loop-video-${videoId}`}
91+
ref={ref}
92+
preload="none"
93+
loop={true}
94+
muted={isMuted}
95+
playsInline={true}
96+
height={height}
97+
width={width}
98+
onPlaying={() => {
99+
setIsPlaying(true);
100+
}}
101+
onCanPlay={() => {
102+
setIsPlayable(true);
103+
}}
104+
onClick={handleClick}
105+
onKeyDown={handleKeyDown}
106+
role="button"
107+
tabIndex={0}
108+
onError={onError}
109+
css={videoStyles}
110+
>
111+
{/* Ensure webm source is provided. Encoding the video to a webm file will improve
112+
performance on supported browsers. https://web.dev/articles/video-and-source-tags */}
113+
{/* <source src={webmSrc} type="video/webm"> */}
114+
<source src={src} type="video/mp4" />
115+
{fallbackImage}
116+
</video>
117+
{ref && 'current' in ref && ref.current && (
118+
<>
119+
{isPlayable && !isPlaying && (
120+
<div css={playIconStyles}>
121+
<PlayIcon iconWidth="narrow" />
122+
</div>
123+
)}
124+
<LoopVideoProgressBar
125+
currentTime={elapsedTime}
126+
duration={ref.current.duration}
127+
/>
128+
{hasAudio && (
129+
<button
130+
type="button"
131+
onClick={(event) => {
132+
event.stopPropagation(); // Don't pause the video
133+
setIsMuted(!isMuted);
134+
}}
135+
css={audioButtonStyles}
136+
>
137+
<AudioIcon
138+
size="small"
139+
theme={{
140+
fill: palette(
141+
'--loop-video-audio-icon',
142+
),
143+
}}
144+
/>
145+
</button>
146+
)}
147+
</>
148+
)}
149+
</>
150+
);
151+
},
152+
);

0 commit comments

Comments
 (0)