Skip to content

Commit 98989b6

Browse files
committed
Allow cinemagraph option for looping video
1 parent 8dbbd49 commit 98989b6

File tree

9 files changed

+149
-72
lines changed

9 files changed

+149
-72
lines changed

dotcom-rendering/src/components/Card/Card.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,10 @@ export const Card = ({
896896
<MediaWrapper
897897
mediaSize={mediaSize}
898898
mediaType={media.type}
899+
isCinemagraph={
900+
media.mainMedia?.type === 'SelfHostedVideo' &&
901+
media.mainMedia.videoStyle === 'Cinemagraph'
902+
}
899903
mediaPositionOnDesktop={mediaPositionOnDesktop}
900904
mediaPositionOnMobile={mediaPositionOnMobile}
901905
hideMediaOverlay={media.type === 'slideshow'}
@@ -952,6 +956,12 @@ export const Card = ({
952956
uniqueId={uniqueId}
953957
height={media.mainMedia.height}
954958
width={media.mainMedia.width}
959+
isCinemagraph={
960+
media.mainMedia?.type ===
961+
'SelfHostedVideo' &&
962+
media.mainMedia.videoStyle ===
963+
'Cinemagraph'
964+
}
955965
posterImage={media.mainMedia.image ?? ''}
956966
fallbackImage={media.mainMedia.image ?? ''}
957967
fallbackImageSize={mediaSize}

dotcom-rendering/src/components/Card/components/MediaWrapper.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SerializedStyles } from '@emotion/react';
22
import { css } from '@emotion/react';
33
import { between, from, space, until } from '@guardian/source/foundations';
4+
import { getZIndex } from '../../../lib/getZIndex';
45
import type { CardMediaType } from '../../../types/layout';
56

67
const mediaFixedSize = {
@@ -29,6 +30,7 @@ type Props = {
2930
children: React.ReactNode;
3031
mediaSize: MediaSizeType;
3132
mediaType?: CardMediaType;
33+
isCinemagraph?: boolean;
3234
mediaPositionOnDesktop: MediaPositionType;
3335
mediaPositionOnMobile: MediaPositionType;
3436
/**
@@ -48,6 +50,9 @@ const mediaOverlayContainerStyles = css`
4850
left: 0;
4951
width: 100%;
5052
height: 100%;
53+
z-index: ${getZIndex('mediaOverlay')};
54+
cursor: pointer;
55+
pointer-events: none;
5156
`;
5257

5358
/**
@@ -201,6 +206,7 @@ export const MediaWrapper = ({
201206
children,
202207
mediaSize,
203208
mediaType,
209+
isCinemagraph,
204210
mediaPositionOnDesktop,
205211
mediaPositionOnMobile,
206212
hideMediaOverlay,
@@ -267,8 +273,10 @@ export const MediaWrapper = ({
267273
>
268274
<>
269275
{children}
270-
{/* This image overlay is styled when the CardLink is hovered */}
271-
{(mediaType === 'picture' || mediaType === 'slideshow') &&
276+
{/* This overlay is styled when the CardLink is hovered */}
277+
{(mediaType === 'picture' ||
278+
mediaType === 'slideshow' ||
279+
(mediaType === 'loop-video' && isCinemagraph)) &&
272280
!hideMediaOverlay && (
273281
<div
274282
css={[

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

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const videoContainerStyles = css`
3333
position: relative;
3434
`;
3535

36+
const cinemagraphContainerStyles = css`
37+
pointer-events: none;
38+
`;
39+
3640
/**
3741
* Dispatches a custom play audio event so that other videos listening
3842
* for this event will be muted.
@@ -116,6 +120,11 @@ type Props = {
116120
uniqueId: string;
117121
height: number;
118122
width: number;
123+
/**
124+
* All controls on the video are hidden: the video looks like a GIF.
125+
* This includes but may not be limited to: audio icon, play/pause icon, subtitles, progress bar.
126+
*/
127+
isCinemagraph: boolean;
119128
posterImage: string;
120129
fallbackImage: CardPictureProps['mainImage'];
121130
fallbackImageSize: CardPictureProps['imageSize'];
@@ -133,6 +142,7 @@ export const LoopVideo = ({
133142
uniqueId,
134143
height,
135144
width,
145+
isCinemagraph,
136146
posterImage,
137147
fallbackImage,
138148
fallbackImageSize,
@@ -642,8 +652,13 @@ export const LoopVideo = ({
642652
return (
643653
<figure
644654
ref={setNode}
645-
css={videoContainerStyles}
646-
className="loop-video-container"
655+
css={[
656+
videoContainerStyles,
657+
isCinemagraph && cinemagraphContainerStyles,
658+
]}
659+
className={`loop-video-container ${
660+
isCinemagraph ? 'cinemagraph' : ''
661+
}`}
647662
data-component="gu-video-loop"
648663
>
649664
<LoopVideoPlayer
@@ -663,17 +678,20 @@ export const LoopVideo = ({
663678
handleLoadedMetadata={handleLoadedMetadata}
664679
handleLoadedData={handleLoadedData}
665680
handleCanPlay={handleCanPlay}
666-
handlePlayPauseClick={handlePlayPauseClick}
667-
handleAudioClick={handleAudioClick}
668-
handleKeyDown={handleKeyDown}
669-
handlePause={handlePause}
681+
handlePlayPauseClick={
682+
!isCinemagraph ? handlePlayPauseClick : undefined
683+
}
684+
handleAudioClick={!isCinemagraph ? handleAudioClick : undefined}
685+
handleKeyDown={!isCinemagraph ? handleKeyDown : undefined}
686+
handlePause={!isCinemagraph ? handlePause : undefined}
670687
onError={onError}
671-
AudioIcon={hasAudio ? AudioIcon : null}
688+
AudioIcon={hasAudio && !isCinemagraph ? AudioIcon : null}
672689
preloadPartialData={preloadPartialData}
673-
showPlayIcon={showPlayIcon}
674-
subtitleSource={subtitleSource}
675-
subtitleSize={subtitleSize}
690+
showPlayIcon={!isCinemagraph ? showPlayIcon : false}
691+
subtitleSize={!isCinemagraph ? subtitleSize : undefined}
692+
subtitleSource={!isCinemagraph ? subtitleSource : undefined}
676693
activeCue={activeCue}
694+
isCinemagraph={isCinemagraph}
677695
/>
678696
</figure>
679697
);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ export const Without5to4Ratio: Story = {
7171
},
7272
};
7373

74+
export const WithCinemagraph: Story = {
75+
name: 'With cinemagraph',
76+
args: {
77+
...Default.args,
78+
isCinemagraph: true,
79+
},
80+
};
81+
7482
export const PausePlay: Story = {
7583
...Default,
7684
name: 'Pause and play interaction',

dotcom-rendering/src/components/LoopVideoPlayer.tsx

Lines changed: 73 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ import { SubtitleOverlay } from './SubtitleOverlay';
2222

2323
export type SubtitleSize = 'small' | 'medium' | 'large';
2424

25-
const videoStyles = (
26-
width: number,
27-
height: number,
28-
subtitleSize: SubtitleSize,
29-
) => css`
25+
const videoStyles = (width: number, height: number) => css`
3026
position: relative;
3127
display: block;
3228
height: auto;
@@ -35,7 +31,9 @@ const videoStyles = (
3531
/* Prevents CLS by letting the browser know the space the video will take up. */
3632
aspect-ratio: ${width} / ${height};
3733
object-fit: cover;
34+
`;
3835

36+
const subtitleStyles = (subtitleSize: SubtitleSize | undefined) => css`
3937
::cue {
4038
/* Hide the cue by default as we prefer custom overlay */
4139
visibility: hidden;
@@ -112,19 +110,21 @@ type Props = {
112110
handleLoadedMetadata: (event: SyntheticEvent) => void;
113111
handleLoadedData: (event: SyntheticEvent) => void;
114112
handleCanPlay: (event: SyntheticEvent) => void;
115-
handlePlayPauseClick: (event: SyntheticEvent) => void;
116-
handleAudioClick: (event: SyntheticEvent) => void;
117-
handleKeyDown: (event: React.KeyboardEvent<HTMLVideoElement>) => void;
118-
handlePause: (event: SyntheticEvent) => void;
113+
handlePlayPauseClick?: (event: SyntheticEvent) => void;
114+
handleAudioClick?: (event: SyntheticEvent) => void;
115+
handleKeyDown?: (event: React.KeyboardEvent<HTMLVideoElement>) => void;
116+
handlePause?: (event: SyntheticEvent) => void;
119117
onError: (event: SyntheticEvent<HTMLVideoElement>) => void;
120118
AudioIcon: ((iconProps: IconProps) => JSX.Element) | null;
121119
posterImage?: string;
122120
preloadPartialData: boolean;
123121
showPlayIcon: boolean;
124122
subtitleSource?: string;
125-
subtitleSize: SubtitleSize;
123+
subtitleSize?: SubtitleSize;
126124
/* used in custom subtitle overlays */
127125
activeCue?: ActiveCue | null;
126+
/* Cinemagraphs do not display subtitles or video controls */
127+
isCinemagraph: boolean;
128128
};
129129

130130
/**
@@ -165,16 +165,26 @@ export const LoopVideoPlayer = forwardRef(
165165
subtitleSource,
166166
subtitleSize,
167167
activeCue,
168+
isCinemagraph,
168169
}: Props,
169170
ref: React.ForwardedRef<HTMLVideoElement>,
170171
) => {
171172
const loopVideoId = `loop-video-${uniqueId}`;
173+
const showSubtitles =
174+
!!subtitleSource &&
175+
!!subtitleSize &&
176+
!!activeCue?.text &&
177+
!isCinemagraph;
178+
172179
return (
173180
<>
174181
{/* eslint-disable-next-line jsx-a11y/media-has-caption -- Captions will be considered later. */}
175182
<video
176183
id={loopVideoId}
177-
css={videoStyles(width, height, subtitleSize)}
184+
css={[
185+
videoStyles(width, height),
186+
showSubtitles && subtitleStyles(subtitleSize),
187+
]}
178188
crossOrigin="anonymous"
179189
ref={ref}
180190
tabIndex={0}
@@ -228,61 +238,65 @@ export const LoopVideoPlayer = forwardRef(
228238
)}
229239
{FallbackImageComponent}
230240
</video>
231-
{!!activeCue?.text && (
241+
{showSubtitles && (
232242
<SubtitleOverlay
233243
text={activeCue.text}
234244
subtitleSize={subtitleSize}
235245
/>
236246
)}
237-
{ref && 'current' in ref && ref.current && isPlayable && (
238-
<>
239-
{/* Play icon */}
240-
{showPlayIcon && (
241-
<button
242-
type="button"
243-
onClick={handlePlayPauseClick}
244-
css={playIconStyles}
245-
data-link-name={`gu-video-loop-play-${atomId}`}
246-
data-testid="play-icon"
247-
>
248-
<PlayIcon iconWidth="narrow" />
249-
</button>
250-
)}
251-
{/* Progress bar */}
252-
<LoopVideoProgressBar
253-
videoId={loopVideoId}
254-
currentTime={currentTime}
255-
duration={ref.current.duration}
256-
/>
257-
{/* Audio icon */}
258-
{AudioIcon && (
259-
<button
260-
type="button"
261-
onClick={handleAudioClick}
262-
css={audioButtonStyles}
263-
data-link-name={`gu-video-loop-${
264-
isMuted ? 'unmute' : 'mute'
265-
}-${atomId}`}
266-
>
267-
<div
268-
css={audioIconContainerStyles}
269-
data-testid={`${
247+
{ref &&
248+
'current' in ref &&
249+
ref.current &&
250+
isPlayable &&
251+
!isCinemagraph && (
252+
<>
253+
{/* Play icon */}
254+
{showPlayIcon && (
255+
<button
256+
type="button"
257+
onClick={handlePlayPauseClick}
258+
css={playIconStyles}
259+
data-link-name={`gu-video-loop-play-${atomId}`}
260+
data-testid="play-icon"
261+
>
262+
<PlayIcon iconWidth="narrow" />
263+
</button>
264+
)}
265+
{/* Progress bar */}
266+
<LoopVideoProgressBar
267+
videoId={loopVideoId}
268+
currentTime={currentTime}
269+
duration={ref.current.duration}
270+
/>
271+
{/* Audio icon */}
272+
{AudioIcon && (
273+
<button
274+
type="button"
275+
onClick={handleAudioClick}
276+
css={audioButtonStyles}
277+
data-link-name={`gu-video-loop-${
270278
isMuted ? 'unmute' : 'mute'
271-
}-icon`}
279+
}-${atomId}`}
272280
>
273-
<AudioIcon
274-
size="xsmall"
275-
theme={{
276-
fill: palette(
277-
'--loop-video-audio-icon',
278-
),
279-
}}
280-
/>
281-
</div>
282-
</button>
283-
)}
284-
</>
285-
)}
281+
<div
282+
css={audioIconContainerStyles}
283+
data-testid={`${
284+
isMuted ? 'unmute' : 'mute'
285+
}-icon`}
286+
>
287+
<AudioIcon
288+
size="xsmall"
289+
theme={{
290+
fill: palette(
291+
'--loop-video-audio-icon',
292+
),
293+
}}
294+
/>
295+
</div>
296+
</button>
297+
)}
298+
</>
299+
)}
286300
</>
287301
);
288302
},

dotcom-rendering/src/frontend/schemas/feFront.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3239,6 +3239,14 @@
32393239
},
32403240
"activeVersion": {
32413241
"type": "number"
3242+
},
3243+
"videoPlayerFormat": {
3244+
"enum": [
3245+
"Cinemagraph",
3246+
"Default",
3247+
"Loop"
3248+
],
3249+
"type": "string"
32423250
}
32433251
},
32443252
"required": [

dotcom-rendering/src/frontend/schemas/feTagPage.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,14 @@
14121412
},
14131413
"activeVersion": {
14141414
"type": "number"
1415+
},
1416+
"videoPlayerFormat": {
1417+
"enum": [
1418+
"Cinemagraph",
1419+
"Default",
1420+
"Loop"
1421+
],
1422+
"type": "string"
14151423
}
14161424
},
14171425
"required": [

dotcom-rendering/src/lib/getZIndex.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ const indices = [
8686
'bodyArea',
8787
'rightColumnArea',
8888

89+
// Media overlay
90+
'mediaOverlay',
91+
8992
// Loop video container
9093
'loop-video-progress-bar-foreground',
9194
'loop-video-progress-bar-background',

0 commit comments

Comments
 (0)