Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions dotcom-rendering/fixtures/manual/trails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,11 +648,11 @@ export const newsletterTrails: [DCRFrontCard, DCRFrontCard] = [
},
];

export const selfHostedVideo54Card = {
export const selfHostedLoopVideo54Card = {
...defaultCardProps,
dataLinkName: 'news | group-0 | card-@2',
url: '/uk-news/2025/jan/22/prince-harry-says-sun-publisher-made-historic-admission-as-he-settles-case',
headline: 'Self-hosted 5:4 video card',
headline: 'Self-hosted 5:4 loop video card',
trailText: 'Trail text for looping video card',
mainMedia: {
type: 'SelfHostedVideo',
Expand All @@ -676,11 +676,11 @@ export const selfHostedVideo54Card = {
showVideo: true,
} satisfies DCRFrontCard;

export const selfHostedVideo45Card = {
...selfHostedVideo54Card,
headline: 'Self-hosted 4:5 video card',
export const selfHostedLoopVideo45Card = {
...selfHostedLoopVideo54Card,
headline: 'Self-hosted 4:5 loop video card',
mainMedia: {
...selfHostedVideo54Card.mainMedia,
...selfHostedLoopVideo54Card.mainMedia,
sources: [
{
mimeType: 'video/mp4',
Expand All @@ -692,11 +692,11 @@ export const selfHostedVideo45Card = {
},
} satisfies DCRFrontCard;

export const selfHostedVideo53Card = {
...selfHostedVideo54Card,
headline: 'Self-hosted 5:3 video card',
export const selfHostedLoopVideo53Card = {
...selfHostedLoopVideo54Card,
headline: 'Self-hosted 5:3 loop video card',
mainMedia: {
...selfHostedVideo54Card.mainMedia,
...selfHostedLoopVideo54Card.mainMedia,
sources: [
{
mimeType: 'video/mp4',
Expand All @@ -708,11 +708,11 @@ export const selfHostedVideo53Card = {
},
} satisfies DCRFrontCard;

export const selfHostedVideo916Card = {
...selfHostedVideo54Card,
headline: 'Self-hosted 9:16 video card',
export const selfHostedLoopVideo916Card = {
...selfHostedLoopVideo54Card,
headline: 'Self-hosted 9:16 loop video card',
mainMedia: {
...selfHostedVideo54Card.mainMedia,
...selfHostedLoopVideo54Card.mainMedia,
sources: [
{
mimeType: 'video/mp4',
Expand All @@ -724,11 +724,11 @@ export const selfHostedVideo916Card = {
},
} satisfies DCRFrontCard;

export const selfHostedVideo169Card = {
...selfHostedVideo54Card,
headline: 'Self-hosted 16:9 video card',
export const selfHostedLoopVideo169Card = {
...selfHostedLoopVideo54Card,
headline: 'Self-hosted 16:9 loop video card',
mainMedia: {
...selfHostedVideo54Card.mainMedia,
...selfHostedLoopVideo54Card.mainMedia,
sources: [
{
mimeType: 'video/mp4',
Expand Down
30 changes: 27 additions & 3 deletions dotcom-rendering/src/components/FeatureCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,8 @@ export const WithSelfHostedLoopVideo = {
atomId: 'atom-id-123',
sources: [
{
src: 'https://uploads.guim.co.uk/2025/11/27/4_5_Test--1d34df3e-8c92-4090-8bb6-d79fc7fb9467-1.0.mp4',
mimeType: 'video/mp4',
src: 'https://uploads.guim.co.uk/2026/01/09/Front_loop__Iran_TiF_Latest--64220ebf-d63d-48dd-9317-16b3b150a4ac-1.1.m3u8',
mimeType: 'application/vnd.apple.mpegurl',
},
],
height: 720,
Expand All @@ -437,8 +437,19 @@ export const WithSelfHostedCinemagraphVideo = {
} satisfies Story;

/**
* Loops look like cinemagraphs, as only cinemagraphs are currently supported in Feature Cards.
* Default video will loop until non-looping video is supported.
*/
export const WithSelfHostedDefaultVideo = {
args: {
...WithSelfHostedLoopVideo.args,
showVideo: true,
mainMedia: {
...WithSelfHostedLoopVideo.args.mainMedia,
videoStyle: 'Default',
},
},
} satisfies Story;

export const WithSelfHostedImmersiveLoopVideo = {
args: {
...WithSelfHostedLoopVideo.args,
Expand Down Expand Up @@ -466,3 +477,16 @@ export const WithSelfHostedImmersiveCinemagraphVideo = {
},
},
} satisfies Story;

/**
* Default video will loop until non-looping video is supported.
*/
export const WithSelfHostedImmersiveDefaultVideo = {
args: {
...WithSelfHostedImmersiveLoopVideo.args,
mainMedia: {
...WithSelfHostedImmersiveLoopVideo.args.mainMedia,
videoStyle: 'Default',
},
},
} satisfies Story;
91 changes: 60 additions & 31 deletions dotcom-rendering/src/components/FeatureCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ const baseCardStyles = css`
text-decoration: none;
`;

const underlineOnHoverStyles = css`
/* Only underline the headline element we want to target (not kickers/sublink headlines) */
:hover .card-headline .show-underline {
text-decoration: underline;
}
`;

const hoverStyles = css`
:hover .media-overlay {
position: absolute;
Expand All @@ -86,14 +93,9 @@ const hoverStyles = css`
background-color: ${palette('--card-background-hover')};
}

/* Only underline the headline element we want to target (not kickers/sublink headlines) */
:hover .card-headline .show-underline {
text-decoration: underline;
}
`;
${underlineOnHoverStyles}

/** When we hover on sublinks, we want to prevent the general hover styles applying */
const sublinkHoverStyles = css`
/** When we hover on sublinks, we want to prevent the general hover styles applying */
:has(ul.sublinks:hover, .branding-logo:hover) {
.card-headline .show-underline {
text-decoration: none;
Expand All @@ -118,6 +120,7 @@ const overlayContainerStyles = css`
left: 0;
width: 100%;
cursor: pointer;
z-index: ${getZIndex('mediaOverlay')};
`;

const immersiveOverlayContainerStyles = css`
Expand All @@ -130,15 +133,11 @@ const immersiveOverlayContainerStyles = css`
* 48px is to ensure the gradient does not render the content inaccessible.
*/
width: 268px;
z-index: 1;
z-index: ${getZIndex('mediaOverlay')};
}
`;

const cinemagraphOverlayStyles = css`
/* Needs to be above the video player */
z-index: ${getZIndex('mediaOverlay')};

/* The whole card is clickable on cinemagraphs */
const noPointerEvents = css`
pointer-events: none;
`;

Expand Down Expand Up @@ -178,8 +177,11 @@ const overlayStyles = css`
}
${overlayMaskGradientStyles('180deg')};

/* Ensure the waveform is behind the other elements, e.g. headline, pill */
> :not(.waveform) {
/*
* Ensure the waveform is behind the other elements, e.g. headline, pill.
* Links define their own z-index.
*/
> :not(.waveform):not(a) {
z-index: 1;
}
`;
Expand All @@ -198,7 +200,7 @@ const immersiveOverlayStyles = css`

const podcastImageContainerStyles = css`
position: relative;
/* Needs to display above of the image mask overlay */
/* Needs to display above the image mask overlay */
z-index: ${getZIndex('card-podcast-image')};
`;

Expand Down Expand Up @@ -443,15 +445,22 @@ export const FeatureCard = ({
showVideo: showVideo && canPlayInline,
});

if (!media) return null;

const showCardAge =
webPublicationDate !== undefined && showClock !== undefined;

const showCommentCount = discussionId !== undefined;

const isYoutubeVideo = media.type === 'youtube-video';

const isSelfHostedVideo =
media?.type === 'loop-video' ||
media?.type === 'default-video' ||
media?.type === 'cinemagraph';
media.type === 'loop-video' ||
media.type === 'default-video' ||
media.type === 'cinemagraph';

const isSelfHostedVideoWithControls =
media.type === 'loop-video' || media.type === 'default-video';

const labsDataAttributes = branding
? getOphanComponents({
Expand All @@ -464,13 +473,16 @@ export const FeatureCard = ({

const aspectRatioNumber = isImmersive ? 5 / 3 : 4 / 5;

if (!media) return null;

return (
<FormatBoundary format={format}>
<ContainerOverrides containerPalette={containerPalette}>
<div css={[baseCardStyles, hoverStyles, sublinkHoverStyles]}>
{media.type !== 'youtube-video' && (
<div
css={[
baseCardStyles,
!isSelfHostedVideoWithControls && hoverStyles,
]}
>
{!isYoutubeVideo && !isSelfHostedVideoWithControls && (
<CardLink
linkTo={linkTo}
headlineText={headlineText}
Expand All @@ -479,7 +491,7 @@ export const FeatureCard = ({
/>
)}
<div css={contentStyles}>
{media.type === 'youtube-video' && (
{isYoutubeVideo && (
<div
data-chromatic="ignore"
data-component="youtube-atom"
Expand Down Expand Up @@ -534,7 +546,7 @@ export const FeatureCard = ({
</Island>
</div>
)}
{media.type !== 'youtube-video' && (
{!isYoutubeVideo && (
<div
css={css`
position: relative;
Expand All @@ -554,8 +566,9 @@ export const FeatureCard = ({
uniqueId={uniqueId}
height={media.mainMedia.height}
width={media.mainMedia.width}
// Only cinemagraphs are currently supported in feature cards
videoStyle="Cinemagraph"
videoStyle={
media.mainMedia.videoStyle
}
posterImage={
media.mainMedia.image ?? ''
}
Expand All @@ -571,10 +584,12 @@ export const FeatureCard = ({
aspectRatio
}
linkTo={linkTo}
showProgressBar={false}
subtitleSource={
media.mainMedia.subtitleSource
}
subtitleSize="large"
subtitleSize="small"
controlsPosition="top"
minAspectRatio={aspectRatioNumber}
maxAspectRatio={aspectRatioNumber}
/>
Expand Down Expand Up @@ -621,15 +636,17 @@ export const FeatureCard = ({
)}

{/* This overlay is styled when the CardLink is hovered */}
<div className="media-overlay" />

{!isSelfHostedVideoWithControls && (
<div className="media-overlay" />
)}
<div
css={[
overlayContainerStyles,
isImmersive &&
immersiveOverlayContainerStyles,
/* The whole card is clickable on cinemagraphs */
media.type === 'cinemagraph' &&
cinemagraphOverlayStyles,
noPointerEvents,
]}
>
{mainMedia?.type === 'Audio' &&
Expand All @@ -656,8 +673,20 @@ export const FeatureCard = ({
overlayStyles,
isImmersive &&
immersiveOverlayStyles,
isSelfHostedVideoWithControls &&
underlineOnHoverStyles,
]}
>
{/** Only the overlay is a link for self-hosted videos with controls. */}
{isSelfHostedVideoWithControls && (
<CardLink
linkTo={linkTo}
headlineText={headlineText}
dataLinkName={dataLinkName}
isExternalLink={isExternalLink}
/>
)}

{isImmersive &&
mainMedia?.type === 'Audio' &&
!!mainMedia.podcastImage?.src && (
Expand Down
22 changes: 11 additions & 11 deletions dotcom-rendering/src/components/FlexibleGeneral.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import {
galleryTrails,
getSublinks,
opinionTrails,
selfHostedVideo169Card,
selfHostedVideo45Card,
selfHostedVideo53Card,
selfHostedVideo54Card,
selfHostedVideo916Card,
selfHostedLoopVideo169Card,
selfHostedLoopVideo45Card,
selfHostedLoopVideo53Card,
selfHostedLoopVideo54Card,
selfHostedLoopVideo916Card,
slideshowCard,
trails,
youtubeVideoTrails,
Expand Down Expand Up @@ -592,11 +592,11 @@ export const SelfHostedVideoCardsInSplashSlots: Story = {
] as BoostLevel[];

const videos = [
selfHostedVideo54Card,
selfHostedVideo45Card,
selfHostedVideo53Card,
selfHostedVideo916Card,
selfHostedVideo169Card,
selfHostedLoopVideo54Card,
selfHostedLoopVideo45Card,
selfHostedLoopVideo53Card,
selfHostedLoopVideo916Card,
selfHostedLoopVideo169Card,
];

return (
Expand All @@ -623,7 +623,7 @@ export const SelfHostedVideoInStandardSlot: Story = {
'This slot is too small for video. Show image instead',
groupedTrails: {
...emptyGroupedTrails,
standard: [selfHostedVideo54Card], // Self-hosted video is disabled at standard card size
standard: [selfHostedLoopVideo54Card], // Self-hosted video is disabled at standard card size
},
},
};
Expand Down
Loading
Loading