@@ -25,7 +25,8 @@ import type {
2525 DCRFrontImage ,
2626 DCRSupportingContent ,
2727} from '../types/front' ;
28- import type { MainMedia , YoutubeVideo } from '../types/mainMedia' ;
28+ import type { CardMediaType } from '../types/layout' ;
29+ import type { MainMedia } from '../types/mainMedia' ;
2930import { BrandingLabel } from './BrandingLabel' ;
3031import { CardFooter } from './Card/components/CardFooter' ;
3132import { CardLink } from './Card/components/CardLink' ;
@@ -41,6 +42,7 @@ import { FeatureCardCommentCount } from './FeatureCardCommentCount';
4142import { FormatBoundary } from './FormatBoundary' ;
4243import { Island } from './Island' ;
4344import { Pill } from './Pill' ;
45+ import { SelfHostedVideo } from './SelfHostedVideo.importable' ;
4446import { StarRating } from './StarRating/StarRating' ;
4547import { StarRatingDeprecated } from './StarRating/StarRatingDeprecated' ;
4648import { SupportingContent } from './SupportingContent' ;
@@ -49,17 +51,6 @@ import { YoutubeBlockComponent } from './YoutubeBlockComponent.importable';
4951
5052export type Position = 'inner' | 'outer' | 'none' ;
5153
52- type Media =
53- | {
54- type : 'picture' ;
55- imageUrl : string ;
56- imageAltText ?: string ;
57- }
58- | {
59- type : 'youtube-video' ;
60- mainMedia : YoutubeVideo ;
61- } ;
62-
6354const baseCardStyles = css `
6455 display : flex;
6556 flex-direction : column;
@@ -138,7 +129,7 @@ const immersiveOverlayContainerStyles = css`
138129 * 48px is to ensure the gradient does not render the content inaccessible.
139130 */
140131 width : 268px ;
141- z-index : 1 ;
132+ z-index : ${ getZIndex ( 'feature-card-overlay' ) } ;
142133 }
143134` ;
144135
@@ -165,11 +156,14 @@ const overlayMaskGradientStyles = (angle: string) => css`
165156 );
166157` ;
167158const overlayStyles = css `
159+ position : relative;
168160 display : flex;
169161 flex-direction : column;
170162 text-align : start;
171163 gap : ${ space [ 1 ] } px;
172164 padding : 64px ${ space [ 2 ] } px ${ space [ 2 ] } px;
165+ /* Needs to be above self-hosted video */
166+ z-index : ${ getZIndex ( 'feature-card-overlay' ) } ;
173167 backdrop-filter : blur (12px ) brightness (0.5 );
174168 @supports not (backdrop-filter : blur (12px )) {
175169 background-color : ${ transparentColour ( sourcePalette . neutral [ 10 ] , 0.7 ) } ;
@@ -264,7 +258,26 @@ const getMedia = ({
264258 imageAltText ?: string ;
265259 mainMedia ?: MainMedia ;
266260 showVideo ?: boolean ;
267- } ) : Media | undefined => {
261+ } ) => {
262+ if ( mainMedia ?. type === 'SelfHostedVideo' && showVideo ) {
263+ let type : CardMediaType ;
264+ switch ( mainMedia . videoStyle ) {
265+ case 'Loop' :
266+ type = 'loop-video' ;
267+ break ;
268+ case 'Cinemagraph' :
269+ type = 'cinemagraph' ;
270+ break ;
271+ default :
272+ type = 'default-video' ;
273+ }
274+
275+ return {
276+ type,
277+ mainMedia,
278+ } as const ;
279+ }
280+
268281 if ( mainMedia ?. type === 'YoutubeVideo' && showVideo ) {
269282 return {
270283 type : 'youtube-video' ,
@@ -328,8 +341,11 @@ export type Props = {
328341 * Note YouTube recommends a minimum width of 480px @see https://developers.google.com/youtube/terms/required-minimum-functionality#embedded-youtube-player-size
329342 * At 300px or below, the player will begin to lose functionality e.g. volume controls being omitted.
330343 * Youtube requires a minimum width 200px.
344+ * Similarly for self-hosted videos, we shouldn't display videos in too small a container.
345+ * For example, subtitles will not be legible in too small a player.
331346 */
332347 canPlayInline ?: boolean ;
348+ showVideo ?: boolean ;
333349 kickerText ?: string ;
334350 showPulsingDot ?: boolean ;
335351 starRating ?: Rating ;
@@ -354,13 +370,13 @@ export type Props = {
354370 * The highlights container above the header is 0, the first container below the header is 1, etc.
355371 */
356372 collectionId : number ;
373+ uniqueId : string ;
357374 isNewsletter ?: boolean ;
358375 /**
359376 * An immersive feature card variant. It dictates that the card has a full width background image on
360377 * all breakpoints. It also dictates the the card change aspect ratio to 5:3 on desktop and 4:5 on mobile.
361378 */
362379 isImmersive ?: boolean ;
363- showVideo ?: boolean ;
364380 isStorylines ?: boolean ;
365381 isInStarRatingVariant ?: boolean ;
366382 starRatingSize : RatingSizeType ;
@@ -381,6 +397,7 @@ export const FeatureCard = ({
381397 showClock,
382398 mainMedia,
383399 canPlayInline = false ,
400+ showVideo = false ,
384401 kickerText,
385402 showPulsingDot,
386403 dataLinkName,
@@ -396,9 +413,9 @@ export const FeatureCard = ({
396413 starRating,
397414 showQuotes,
398415 collectionId,
416+ uniqueId,
399417 isNewsletter = false ,
400418 isImmersive = false ,
401- showVideo = false ,
402419 isStorylines = false ,
403420 isInStarRatingVariant,
404421 starRatingSize,
@@ -407,6 +424,10 @@ export const FeatureCard = ({
407424
408425 const isVideoArticle = format . design === ArticleDesign . Video ;
409426
427+ /**
428+ * Determine which type of media to use for the card.
429+ * For example, a video might be available, but if we don't want to show it, use an image instead.
430+ */
410431 const media = getMedia ( {
411432 imageUrl : image ?. src ,
412433 imageAltText : image ?. altText ,
@@ -419,6 +440,11 @@ export const FeatureCard = ({
419440
420441 const showCommentCount = discussionId !== undefined ;
421442
443+ const isSelfHostedVideo =
444+ media ?. type === 'loop-video' ||
445+ media ?. type === 'default-video' ||
446+ media ?. type === 'cinemagraph' ;
447+
422448 const labsDataAttributes = branding
423449 ? getOphanComponents ( {
424450 branding,
@@ -507,7 +533,47 @@ export const FeatureCard = ({
507533 ) } ;
508534 ` }
509535 >
510- { /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- A PR to add self-hosted video is upcoming where this check will be needed. */ }
536+ { isSelfHostedVideo && (
537+ < Island
538+ priority = "critical"
539+ defer = { { until : 'visible' } }
540+ >
541+ < SelfHostedVideo
542+ sources = { media . mainMedia . sources }
543+ atomId = { media . mainMedia . atomId }
544+ uniqueId = { uniqueId }
545+ height = { media . mainMedia . height }
546+ width = { media . mainMedia . width }
547+ containerAspectRatio = {
548+ isImmersive ? 5 / 3 : 4 / 5
549+ }
550+ // Only cinemagraphs are currently supported in feature cards
551+ videoStyle = "Cinemagraph"
552+ posterImage = {
553+ media . mainMedia . image ?? ''
554+ }
555+ fallbackImage = {
556+ media . mainMedia . image ?? ''
557+ }
558+ fallbackImageSize = { imageSize }
559+ fallbackImageLoading = { imageLoading }
560+ fallbackImageAlt = {
561+ media . imageAltText
562+ }
563+ fallbackImageAspectRatio = {
564+ isImmersive ? '5:3' : '4:5'
565+ }
566+ linkTo = { linkTo }
567+ subtitleSource = {
568+ media . mainMedia . subtitleSource
569+ }
570+ subtitleSize = "large"
571+ enableHls = { false }
572+ isFeatureCard = { true }
573+ />
574+ </ Island >
575+ ) }
576+
511577 { media . type === 'picture' && (
512578 < >
513579 < CardPicture
@@ -555,6 +621,11 @@ export const FeatureCard = ({
555621 overlayContainerStyles ,
556622 isImmersive &&
557623 immersiveOverlayContainerStyles ,
624+ // The whole card is clickable on cinemagraphs
625+ media . type === 'cinemagraph' &&
626+ css `
627+ pointer-events : none;
628+ ` ,
558629 ] }
559630 >
560631 { mainMedia ?. type === 'Audio' &&
0 commit comments