diff --git a/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.stories.tsx b/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.stories.tsx index 8e8800f4f12..79b42a6052b 100644 --- a/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.stories.tsx +++ b/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.stories.tsx @@ -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', @@ -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: { diff --git a/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.tsx b/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.tsx index fd6f63a1b27..681ef8acf4e 100644 --- a/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.tsx +++ b/dotcom-rendering/src/components/Masthead/Titlepiece/CustomSubNav.tsx @@ -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; @@ -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 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 fallback; the rest become s. */ + const fallback = sorted.at(-1); + if (!fallback) { + return null; + } + return ( + + {sorted.slice(0, -1).map((image) => ( + + ))} + + + ); +}; + /** Sets horizontal scrolling behaviour and removes the scrollbar */ const scrollableSubNavStyles = css` overflow-x: scroll; @@ -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 }) => ( +
  • + + {linkText === currentNavLink ? ( + {linkText} + ) : ( + linkText + )} + +
  • + )); + + if (hasHeaderImage) { + return ( +
    +
    + + + {customSubNav.header.headerText} + +
    + +
    + ); + } + return (
    - {customSubNav.links.map(({ linkText, dotcomPath }) => ( -
  • - - {linkText === currentNavLink ? ( - {linkText} - ) : ( - linkText - )} - -
  • - ))} + {linkItems}
    ); diff --git a/dotcom-rendering/src/components/Titlepiece.island.tsx b/dotcom-rendering/src/components/Titlepiece.island.tsx index 3a49e2971ce..cba079f21f3 100644 --- a/dotcom-rendering/src/components/Titlepiece.island.tsx +++ b/dotcom-rendering/src/components/Titlepiece.island.tsx @@ -2,6 +2,7 @@ import { css, Global } from '@emotion/react'; import { from, headlineBold14, + palette as sourcePalette, space, until, visuallyHidden, @@ -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, @@ -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 (