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
4 changes: 2 additions & 2 deletions dotcom-rendering/src/components/Masthead/Masthead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactElement } from 'react';
import type { EditionId } from '../../lib/edition';
import type { NavType } from '../../model/extract-nav';
import { palette as themePalette } from '../../palette';
import type { CustomSubnav } from '../../types/customSubnav';
import type { CustomSubnavForPage } from '../../types/customSubnav';
import { Island } from '../Island';
import { Section } from '../Section';
import { Titlepiece } from '../Titlepiece.island';
Expand All @@ -18,7 +18,7 @@ type Props = {
discussionApiUrl: string;
idApiUrl: string;
showSubNav?: boolean;
customSubnav?: CustomSubnav;
customSubnav?: CustomSubnavForPage;
/** The slim nav is a slimmed down version of the Titlepiece usually used for Immersive articles */
showSlimNav?: boolean;
hasPageSkin?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { css } from '@emotion/react';
import { palette as sourcePalette, space } from '@guardian/source/foundations';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import {
ArticleDesign,
ArticleDisplay,
Pillar,
} from '../../../lib/articleFormat';
import type { CustomSubnav } from '../../../types/customSubnav';
import { CustomSubNav } from './CustomSubNav';

const customSubNav: CustomSubnav = {
id: 'us-politics',
header: {
headerText: 'US politics',
copy: '',
dotcomPath: '/us-news/us-politics',
},
format: 'large',
links: [
{
linkText: 'Trump administration',
dotcomPath: '/us-news/trump-administration',
},
{ linkText: 'US Congress', dotcomPath: '/us-news/us-congress' },
{ linkText: 'US supreme court', dotcomPath: '/law/us-supreme-court' },
{
linkText: 'US elections 2024',
dotcomPath: '/us-news/us-elections-2024',
},
{ linkText: 'US immigration', dotcomPath: '/us-news/usimmigration' },
],
pages: [],
};

const meta = {
component: CustomSubNav,
title: 'Components/Masthead/Titlepiece/CustomSubNav',
decorators: [
(Story) => (
<div
css={css`
background-color: ${sourcePalette.brand[400]};
padding: ${space[3]}px;
`}
>
<Story />
</div>
),
],
render: (args) => <CustomSubNav {...args} />,
args: {
customSubNav,
currentNavLink: 'US Congress',
hasPageSkin: false,
},
} satisfies Meta<typeof CustomSubNav>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Front = {
args: { renderingPage: 'front' },
} satisfies Story;

export const Article = {
args: { renderingPage: 'article' },
parameters: {
formats: [
{
design: ArticleDesign.Standard,
display: ArticleDisplay.Standard,
theme: Pillar.News,
},
],
},
} satisfies Story;
235 changes: 206 additions & 29 deletions dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
*/
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 } from '../../../types/customSubnav';
import type { CustomSubnav, RenderingPage } from '../../../types/customSubnav';

type Props = {
customSubNav: CustomSubnav;
currentNavLink: string;
renderingPage: RenderingPage;
hasPageSkin?: boolean;
};

Expand All @@ -43,6 +46,132 @@ const subNavStylesFromLeftCol = css`
}
`;

/** On articles the header and links sit on a single aligned row. */
const articleContainerStyles = css`
display: flex;
align-items: stretch;
min-height: 36px;

${from.mobileMedium} {
min-height: 40px;
}
${from.tablet} {
min-height: 42px;
}
${from.leftCol} {
min-height: 44px;
}
`;

/**
* On articles the links row sits inline next to the header. The stacked top margins are
* removed so the header divider spans the full component height set by the container.
*/
const articleSubNavStyles = css`
margin-top: 0;
align-items: center;

${from.mobileMedium} {
margin-top: 0;
}
${from.leftCol} {
margin-top: 0;
}
`;

const articleHeaderStyles = css`
${textSansBold14}
color: ${themePalette('--custom-subnav-header-text')};
display: flex;
align-items: center;
white-space: nowrap;
padding-right: ${space[2]}px;
margin-right: ${space[2]}px;
border-right: 1px solid ${themePalette('--masthead-nav-lines')};
`;

/** Served from `src/static`; temporary test assets until real images arrive in the payload. */
const headerImageBasePath = '/static/frontend/customsubnav';

/**
* 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: auto;
`;

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;
}
`;

/** Temporary breakpoint-specific header images; swap for payload images later. */
const HeaderImage = () => (
<picture>
<source
media={`(min-width: ${breakpoints.wide}px)`}
srcSet={`${headerImageBasePath}/wc-wide.png`}
/>
<source
media={`(min-width: ${breakpoints.desktop}px)`}
srcSet={`${headerImageBasePath}/wc-desktop.png`}
/>
<source
media={`(min-width: ${breakpoints.tablet}px)`}
srcSet={`${headerImageBasePath}/wc-tablet.png`}
/>
<img
src={`${headerImageBasePath}/wc-mobile.png`}
alt=""
css={headerImageStyles}
/>
</picture>
);

/** Sets horizontal scrolling behaviour and removes the scrollbar */
const scrollableSubNavStyles = css`
overflow-x: scroll;
Expand Down Expand Up @@ -75,56 +204,104 @@ const selectedLink = css`
`;

/**
* Renders a custom subnav (header + links, images and further data to follow) targeted at a front.
* Renders a custom subnav (header + links, images and further data to follow) assigned to fronts or articles.
*/
export const CustomSubNav = ({
customSubNav,
currentNavLink,
renderingPage,
hasPageSkin,
}: Props) => {
const isArticle = renderingPage === 'article';
const hasHeaderImage = !isArticle && (customSubNav.images?.length ?? 0) > 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 />
<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}`}
data-component-id={customSubNav.id}
css={[
headlineBold28,
css`
margin-top: ${space[2]}px;
`,
]}
data-rendering-page={renderingPage}
css={
isArticle
? articleContainerStyles
: [
headlineBold28,
css`
margin-top: ${space[2]}px;
color: ${themePalette(
'--masthead-nav-link-text',
)};
`,
]
}
>
{customSubNav.header.headerText}
{isArticle ? (
<span css={articleHeaderStyles}>
{customSubNav.header.headerText}
</span>
) : (
customSubNav.header.headerText
)}
<ul
css={[
subNavStyles,
!hasPageSkin && subNavStylesFromLeftCol,
scrollableSubNavStyles,
isArticle && articleSubNavStyles,
]}
role="list"
style={{
'--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
Loading
Loading