Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ const customSubNav: CustomSubnav = {
pages: [],
};

/** Distinct images per breakpoint so it's obvious which one is being served. */
const customSubNavWithImages: CustomSubnav = {
...customSubNav,
images: [
{
breakpoint: 'mobile',
platforms: ['web'],
imageSrc:
'https://media.guim.co.uk/6537e163c9164d25ec6102641f6a04fa5ba76560/0_210_5472_3283/master/5472.jpg?width=740&height=140&quality=85&fit=crop&s=none',
},
{
breakpoint: 'tablet',
platforms: ['web'],
imageSrc:
'https://media.guim.co.uk/56b42eef576bc04c820da710459acd91082bb37b/0_0_6720_4480/6720.jpg?width=980&height=140&quality=85&fit=crop&s=none',
},
{
breakpoint: 'desktop',
platforms: ['web'],
imageSrc:
'https://media.guim.co.uk/c981848745e482e03e23b2ec9402e1f5c5bee6a6/102_73_3282_1848/2000.jpg?width=1300&height=140&quality=85&fit=crop&s=none',
},
],
};

const meta = {
component: CustomSubNav,
title: 'Components/Masthead/Titlepiece/CustomSubNav',
Expand Down Expand Up @@ -64,6 +89,14 @@ export const Front = {
args: { renderingPage: 'front' },
} satisfies Story;

/** On fronts, a web image is shown per breakpoint; resize the viewport to switch between mobile/tablet/desktop. */
export const FrontWithImage = {
args: {
renderingPage: 'front',
customSubNav: customSubNavWithImages,
},
} satisfies Story;

export const Article = {
args: { renderingPage: 'article' },
parameters: {
Expand Down
165 changes: 144 additions & 21 deletions dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@
*/
import { css } from '@emotion/react';
import {
breakpoints,
from,
headlineBold28,
space,
textSans14,
textSansBold14,
} from '@guardian/source/foundations';
import { grid } from '../../../grid';
import { nestedOphanComponents } from '../../../lib/ophan-helpers';
import { palette as themePalette } from '../../../palette';
import type { CustomSubnav, RenderingPage } from '../../../types/customSubnav';
import type {
CustomSubnav,
CustomSubnavImage,
RenderingPage,
} from '../../../types/customSubnav';

type Props = {
customSubNav: CustomSubnav;
Expand Down Expand Up @@ -88,6 +94,88 @@ const articleHeaderStyles = css`
border-right: 1px solid ${themePalette('--masthead-nav-lines')};
`;

/**
* On fronts, when an image is present the subnav mirrors DirectoryPageNav: a
* fixed-width, centred grid (blue) with the image spanning the full width on the
* top row and the links beneath it. The surrounding row is white (set by the
* Titlepiece wrapper), so only this padded container shows blue.
*/
const imageNavStyles = css`
${grid.paddedContainer}
position: relative;
background-color: ${themePalette('--masthead-nav-background')};
`;

const imageWrapperStyles = css`
${grid.column.all}
grid-row: 1;
position: relative;
display: block;
border-bottom: 1px solid ${themePalette('--masthead-nav-lines')};
`;

const headerImageStyles = css`
display: block;
width: 100%;
height: 140px;
object-fit: cover;
`;

const imageHeaderTextStyles = css`
${headlineBold28}
position: absolute;
left: ${space[3]}px;
bottom: ${space[1]}px;
color: ${themePalette('--masthead-nav-link-text')};

${from.mobileLandscape} {
left: ${space[5]}px;
}
`;

const imageListStyles = css`
${grid.column.all}
grid-row: 2;
${textSans14}
display: flex;
align-items: center;
column-gap: ${space[2]}px;
min-height: 28px;
padding: 0 ${space[3]}px;

${from.mobileLandscape} {
padding: 0 ${space[5]}px;
}
${from.tablet} {
min-height: 30px;
}
`;

/** Largest breakpoints first so the <source> media queries cascade correctly. */
const byBreakpointWidthDesc = (a: CustomSubnavImage, b: CustomSubnavImage) =>
breakpoints[b.breakpoint] - breakpoints[a.breakpoint];

const HeaderImage = ({ images }: { images: CustomSubnavImage[] }) => {
const sorted = [...images].sort(byBreakpointWidthDesc);
/** Smallest breakpoint is the <img> fallback; the rest become <source>s. */
const fallback = sorted.at(-1);
if (!fallback) {
return null;
}
return (
<picture>
{sorted.slice(0, -1).map((image) => (
<source
key={image.breakpoint}
media={`(min-width: ${breakpoints[image.breakpoint]}px)`}
srcSet={image.imageSrc}
/>
))}
<img src={fallback.imageSrc} alt="" css={headerImageStyles} />
</picture>
);
};

/** Sets horizontal scrolling behaviour and removes the scrollbar */
const scrollableSubNavStyles = css`
overflow-x: scroll;
Expand Down Expand Up @@ -136,6 +224,60 @@ export const CustomSubNav = ({
hasPageSkin,
}: Props) => {
const isArticle = renderingPage === 'article';
/** DCR receives images for all platforms; only web images are rendered here. */
const webImages = (customSubNav.images ?? []).filter((image) =>
image.platforms.includes('web'),
);
const hasHeaderImage = !isArticle && webImages.length > 0;

const linkItems = customSubNav.links.map(({ linkText, dotcomPath }) => (
<li key={dotcomPath} css={subnavListItemStyles}>
<a
css={subnavLinkStyles}
data-src-focus-disabled={true}
href={dotcomPath}
data-link-name={nestedOphanComponents(
'header',
'custom subnav',
linkText,
)}
>
{linkText === currentNavLink ? (
<span css={selectedLink}>{linkText}</span>
) : (
linkText
)}
</a>
</li>
));

if (hasHeaderImage) {
return (
<div
data-component={`custom-subnav-${customSubNav.header.headerText}`}
data-component-id={customSubNav.id}
data-rendering-page={renderingPage}
css={imageNavStyles}
>
<div css={imageWrapperStyles}>
<HeaderImage images={webImages} />
<span css={imageHeaderTextStyles}>
{customSubNav.header.headerText}
</span>
</div>
<ul
css={[imageListStyles, scrollableSubNavStyles]}
role="list"
style={{
'--sub-nav-link': themePalette('--sub-nav-link-header'),
}}
>
{linkItems}
</ul>
</div>
);
}

return (
<div
data-component={`custom-subnav-${customSubNav.header.headerText}`}
Expand All @@ -162,26 +304,7 @@ export const CustomSubNav = ({
'--sub-nav-link': themePalette('--sub-nav-link-header'),
}}
>
{customSubNav.links.map(({ linkText, dotcomPath }) => (
<li key={dotcomPath} css={subnavListItemStyles}>
<a
css={subnavLinkStyles}
data-src-focus-disabled={true}
href={dotcomPath}
data-link-name={nestedOphanComponents(
'header',
'custom subnav',
linkText,
)}
>
{linkText === currentNavLink ? (
<span css={selectedLink}>{linkText}</span>
) : (
linkText
)}
</a>
</li>
))}
{linkItems}
</ul>
</div>
);
Expand Down
22 changes: 21 additions & 1 deletion dotcom-rendering/src/components/Titlepiece.island.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { css, Global } from '@emotion/react';
import {
from,
headlineBold14,
palette as sourcePalette,
space,
until,
visuallyHidden,
Expand Down Expand Up @@ -316,6 +317,14 @@ const fadeStyles = css`
${themePalette('--masthead-nav-background')} 100%
);
`;

/** For the image custom subnav the row is white and full-bleed; CustomSubNav
* centres its own blue padded container on top (mirrors DirectoryPageNav). */
const customSubnavImageWrapper = css`
grid-column: viewport-start / viewport-end;
grid-row: 3;
background-color: ${sourcePalette.neutral[100]};
`;
export const Titlepiece = ({
nav,
editionId,
Expand All @@ -327,6 +336,13 @@ export const Titlepiece = ({
}: Props) => {
const { showBanner } = useEditionSwitcherBanner(pageId, editionId);

const hasCustomSubnavImage =
customSubnav?.renderingPage === 'front' &&
(customSubnav.data.images?.some((image) =>
image.platforms.includes('web'),
) ??
false);

return (
<Grid
type="nav"
Expand Down Expand Up @@ -590,7 +606,11 @@ export const Titlepiece = ({

{customSubnav && (
<div
css={subNavWrapper}
css={
hasCustomSubnavImage
? customSubnavImageWrapper
: subNavWrapper
}
data-print-layout="hide"
data-testid="sub-nav"
data-component="sub-nav"
Expand Down
37 changes: 30 additions & 7 deletions dotcom-rendering/src/frontend/schemas/feArticle.json
Original file line number Diff line number Diff line change
Expand Up @@ -6669,19 +6669,42 @@
"type": "string"
},
"breakpoint": {
"enum": [
"mobile",
"tablet",
"web"
],
"type": "string"
"$ref": "#/definitions/CustomSubnavImageBreakpoint"
},
"platforms": {
"type": "array",
"items": {
"$ref": "#/definitions/CustomSubnavImagePlatform"
}
}
},
"required": [
"breakpoint",
"imageSrc"
"imageSrc",
"platforms"
]
},
"CustomSubnavImageBreakpoint": {
"enum": [
"desktop",
"leftCol",
"mobile",
"mobileLandscape",
"mobileMedium",
"phablet",
"tablet",
"wide"
],
"type": "string"
},
"CustomSubnavImagePlatform": {
"enum": [
"android",
"ios",
"web"
],
"type": "string"
},
"ReaderRevenuePositions": {
"type": "object",
"properties": {
Expand Down
37 changes: 30 additions & 7 deletions dotcom-rendering/src/frontend/schemas/feFootballMatchInfoPage.json
Original file line number Diff line number Diff line change
Expand Up @@ -478,19 +478,42 @@
"type": "string"
},
"breakpoint": {
"enum": [
"mobile",
"tablet",
"web"
],
"type": "string"
"$ref": "#/definitions/CustomSubnavImageBreakpoint"
},
"platforms": {
"type": "array",
"items": {
"$ref": "#/definitions/CustomSubnavImagePlatform"
}
}
},
"required": [
"breakpoint",
"imageSrc"
"imageSrc",
"platforms"
]
},
"CustomSubnavImageBreakpoint": {
"enum": [
"desktop",
"leftCol",
"mobile",
"mobileLandscape",
"mobileMedium",
"phablet",
"tablet",
"wide"
],
"type": "string"
},
"CustomSubnavImagePlatform": {
"enum": [
"android",
"ios",
"web"
],
"type": "string"
},
"ReaderRevenuePositions": {
"type": "object",
"properties": {
Expand Down
Loading
Loading