Skip to content
200 changes: 200 additions & 0 deletions dotcom-rendering/src/components/DirectoryPageNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ import {
headlineMedium15Object,
headlineMedium17Object,
palette,
space,
textSans14Object,
textSansBold14Object,
} from '@guardian/source/foundations';
import { grid } from '../grid';
import { generateImageURL } from '../lib/image';
import { palette as themePalette } from '../palette';
import type { TagType } from '../types/tag';

type Props = {
Expand All @@ -23,6 +27,8 @@ type Props = {
interface DirectoryPageNavConfig {
pageIds: string[];
tagIds: string[];
variant?: 'subnav';
subLinkBadge?: string;
textColor: string;
backgroundColor: string;
title: { label: string; id: string };
Expand Down Expand Up @@ -120,8 +126,76 @@ const configs = [
'https://uploads.guim.co.uk/2026/03/03/winter-paralympics-980px.jpg',
},
},
// World Cup 2026
{
pageIds: [] as string[],
tagIds: ['football/world-cup-2026'],
variant: 'subnav',
textColor: palette.neutral[7],
backgroundColor: palette.brand[400],
title: {
label: 'World Cup 2026',
id: 'football/world-cup-2026',
},
subLinkBadge:
'data:image/svg+xml,%3Csvg%20width%3D%2216%22%20height%3D%2217%22%20viewBox%3D%220%200%2016%2017%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%3Crect%20width%3D%224.39184%22%20height%3D%2211.5286%22%20fill%3D%22%2390DCFF%22/%3E%3Crect%20x%3D%225.80347%22%20y%3D%225%22%20width%3D%224.39184%22%20height%3D%2211.5286%22%20fill%3D%22%2390DCFF%22/%3E%3Crect%20x%3D%2211.6084%22%20width%3D%224.39184%22%20height%3D%2211.5286%22%20fill%3D%22%2390DCFF%22/%3E%3Ccircle%20cx%3D%227.99939%22%20cy%3D%222.19592%22%20r%3D%222.19592%22%20fill%3D%22%2390DCFF%22/%3E%3C/svg%3E',
links: [
{
label: 'World Cup',
id: 'football/world-cup-2026',
},
{
label: 'Match centre',
id: 'football/world-cup-2026/overview',
},
{
label: 'Player guide',
id: '',
},
{
label: 'Bracketology',
id: '',
},
{
label: 'Golden boot',
id: '',
},
{
label: 'More football',
id: 'football',
},
],
},
] satisfies DirectoryPageNavConfig[];

/**
* Mirrors the centering of the Masthead Titlepiece's content area at each
* breakpoint (matching Section/ElementContainer's margin-auto + max-width
* pattern), with side padding matching ElementContainer's sidePadding.
*/
const subnavInnerStyles = css({
position: 'relative',
margin: 'auto',
paddingLeft: 10,
paddingRight: 10,
[from.mobileLandscape]: {
paddingLeft: 20,
paddingRight: 20,
},
[from.tablet]: {
maxWidth: 740,
},
[from.desktop]: {
maxWidth: 980,
},
[from.leftCol]: {
maxWidth: 1140,
},
[from.wide]: {
maxWidth: 1300,
},
});

export const DirectoryPageNav = ({ pageId, pageTags }: Props) => {
const config = configs.find(
(cfg) =>
Expand All @@ -135,6 +209,132 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => {
return null;
}

if (config.variant === 'subnav') {
const subnavWrapperStyles = css({
backgroundColor: config.backgroundColor,
// paddingBottom: space[1],
});

const subnavListStyles = css({
...textSans14Object,
display: 'flex',
alignItems: 'center',
columnGap: space[2],
minHeight: 28,
width: '100%',
[from.tablet]: {
minHeight: 30,
},
overflowX: 'scroll',
scrollbarWidth: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
listStyle: 'none',
padding: 0,
});

const subnavListItemStyles = css({
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
'&:first-of-type': {
borderRight: `1px solid ${themePalette(
'--masthead-nav-lines',
)}`,
paddingRight: space[1],
'a:not(:hover)': {
color: palette.sport[600],
},
},
});

const subnavLinkStyles = css({
color: themePalette('--masthead-nav-link-text'),
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
columnGap: space[1],
paddingRight: space[1],
'&:hover': {
textDecoration: 'underline',
color: themePalette('--masthead-nav-link-text-hover'),
},
});

const subLinkBadgeStyles = css({
width: 16,
height: 17,
flexShrink: 0,
});

const subnavSelectedLinkStyles = css({
...textSansBold14Object,
});

const subnavInnerWithBorderStyles = css(subnavInnerStyles, {
paddingTop: space[2],
paddingBottom: space[2],
[from.tablet]: {
'&::before': {
content: '""',
borderLeft: `1px solid ${themePalette(
'--masthead-nav-lines',
)}`,
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
},
'&::after': {
content: '""',
borderRight: `1px solid ${themePalette(
'--masthead-nav-lines',
)}`,
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
},
},
});

return (
<nav css={subnavWrapperStyles}>
<div css={subnavInnerWithBorderStyles}>
{/* eslint-disable jsx-a11y/no-redundant-roles -- A11y fix for Safari */}
<ul css={subnavListStyles} role="list">
{/* eslint-enable jsx-a11y/no-redundant-roles */}
{config.links.map((link) => (
<li key={link.label} css={subnavListItemStyles}>
<a
href={`/${link.id}`}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if – as a small convenience – we could hide the link if it doesn't have an id yet (i.e. because the underlying article hasn't been published and the URL doesn't exist)

css={[
subnavLinkStyles,
pageId === link.id
? subnavSelectedLinkStyles
: undefined,
]}
>
{config.subLinkBadge &&
link.label === 'World Cup' ? (
<img
src={config.subLinkBadge}
alt=""
aria-hidden="true"
css={subLinkBadgeStyles}
/>
) : null}
{link.label}
</a>
</li>
))}
</ul>
</div>
</nav>
);
}

const { textColor, backgroundColor } = config;

const nav = css({
Expand Down
6 changes: 5 additions & 1 deletion dotcom-rendering/src/layouts/AudioLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ export const AudioLayout = (props: WebProps | AppProps) => {

const isLabs = format.theme === ArticleSpecial.Labs;

const isWorldCup2026 = article.tags.some(
(tag) => tag.id === 'football/world-cup-2026',
);

const renderAds = canRenderAds(article);

return (
Expand Down Expand Up @@ -191,7 +195,7 @@ export const AudioLayout = (props: WebProps | AppProps) => {
discussionApiUrl={article.config.discussionApiUrl}
idApiUrl={article.config.idApiUrl}
contributionsServiceUrl={contributionsServiceUrl}
showSubNav={!isLabs}
showSubNav={!isLabs && !isWorldCup2026}
showSlimNav={false}
hasPageSkinContentSelfConstrain={true}
pageId={article.pageId}
Expand Down
6 changes: 5 additions & 1 deletion dotcom-rendering/src/layouts/CommentLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ export const CommentLayout = (props: WebProps | AppsProps) => {

const renderAds = canRenderAds(article);

const isWorldCup2026 = article.tags.some(
(tag) => tag.id === 'football/world-cup-2026',
);

return (
<>
{isWeb && (
Expand All @@ -325,7 +329,7 @@ export const CommentLayout = (props: WebProps | AppsProps) => {
discussionApiUrl={article.config.discussionApiUrl}
idApiUrl={article.config.idApiUrl}
contributionsServiceUrl={contributionsServiceUrl}
showSubNav={true}
showSubNav={!isWorldCup2026}
showSlimNav={false}
hasPageSkin={false}
hasPageSkinContentSelfConstrain={false}
Expand Down
6 changes: 5 additions & 1 deletion dotcom-rendering/src/layouts/LiveLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ export const LiveLayout = (props: WebProps | AppsProps) => {

const showComments = article.isCommentable && !isPaidContent;

const isWorldCup2026 = article.tags.some(
(tag) => tag.id === 'football/world-cup-2026',
);

return (
<>
{isWeb && (
Expand Down Expand Up @@ -337,7 +341,7 @@ export const LiveLayout = (props: WebProps | AppsProps) => {
discussionApiUrl={article.config.discussionApiUrl}
idApiUrl={article.config.idApiUrl}
contributionsServiceUrl={contributionsServiceUrl}
showSubNav={true}
showSubNav={!isWorldCup2026}
showSlimNav={false}
hasPageSkin={false}
hasPageSkinContentSelfConstrain={false}
Expand Down
6 changes: 5 additions & 1 deletion dotcom-rendering/src/layouts/PictureLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ export const PictureLayout = (props: WebProps | AppsProps) => {

const renderAds = canRenderAds(article);

const isWorldCup2026 = article.tags.some(
(tag) => tag.id === 'football/world-cup-2026',
);

const avatarUrl = getSoleContributor(article.tags, article.byline)
?.bylineLargeImageUrl;

Expand Down Expand Up @@ -303,7 +307,7 @@ export const PictureLayout = (props: WebProps | AppsProps) => {
discussionApiUrl={article.config.discussionApiUrl}
idApiUrl={article.config.idApiUrl}
contributionsServiceUrl={contributionsServiceUrl}
showSubNav={true}
showSubNav={!isWorldCup2026}
showSlimNav={false}
hasPageSkin={false}
hasPageSkinContentSelfConstrain={false}
Expand Down
8 changes: 6 additions & 2 deletions dotcom-rendering/src/layouts/ShowcaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => {

const isLabs = format.theme === ArticleSpecial.Labs;

const isWorldCup2026 = article.tags.some(
(tag) => tag.id === 'football/world-cup-2026',
);

return (
<>
{isWeb && (
Expand Down Expand Up @@ -282,7 +286,7 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => {
contributionsServiceUrl={
contributionsServiceUrl
}
showSubNav={true}
showSubNav={!isWorldCup2026}
showSlimNav={false}
hasPageSkin={false}
hasPageSkinContentSelfConstrain={false}
Expand Down Expand Up @@ -321,7 +325,7 @@ export const ShowcaseLayout = (props: WebProps | AppsProps) => {
contributionsServiceUrl={
contributionsServiceUrl
}
showSubNav={true}
showSubNav={!isWorldCup2026}
showSlimNav={true}
hasPageSkin={false}
hasPageSkinContentSelfConstrain={false}
Expand Down
6 changes: 5 additions & 1 deletion dotcom-rendering/src/layouts/StandardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ export const StandardLayout = (props: WebProps | AppProps) => {

const isLabs = format.theme === ArticleSpecial.Labs;

const isWorldCup2026 = article.tags.some(
(tag) => tag.id === 'football/world-cup-2026',
);

const renderAds = canRenderAds(article);

return (
Expand Down Expand Up @@ -410,7 +414,7 @@ export const StandardLayout = (props: WebProps | AppProps) => {
discussionApiUrl={article.config.discussionApiUrl}
idApiUrl={article.config.idApiUrl}
contributionsServiceUrl={contributionsServiceUrl}
showSubNav={!isLabs}
showSubNav={!isLabs && !isWorldCup2026}
showSlimNav={false}
hasPageSkinContentSelfConstrain={true}
pageId={article.pageId}
Expand Down
Loading