Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3493543
Render mini nav data
OlwenPearce Oct 23, 2026
fc381ec
Reduce top bar padding
OlwenPearce Oct 28, 2026
f17e526
Scroll with title
OlwenPearce Oct 28, 2026
b754a47
Add case to show image not rendered
OlwenPearce Oct 28, 2026
b97830d
Only display mini nav when flex gen boost and above
OlwenPearce Oct 28, 2026
fe81fe1
Remove not rendered storybook
OlwenPearce Oct 28, 2026
26db67b
Extract test fixture
OlwenPearce Oct 28, 2026
b4dac1a
Reduce re-ordering
OlwenPearce Oct 29, 2026
5ede409
Post rebase fix
OlwenPearce Oct 29, 2026
8989900
Adjust to use cards kicker colour
OlwenPearce Oct 29, 2026
5be29bf
Use standard new card in stories
OlwenPearce Oct 29, 2026
9352018
Fix underlines / hovering
OlwenPearce Oct 29, 2026
ecbe166
Make card size larger
OlwenPearce Oct 29, 2026
ac23a89
Merge branch 'main' into show-mini-nav
OlwenPearce Nov 2, 2026
a6f187b
Use the same padding without a dividing line as with
OlwenPearce Nov 3, 2026
53aa841
Pad ever so slightly
OlwenPearce Nov 3, 2026
6875ab9
Try to make things cleaner / more readable
OlwenPearce Nov 3, 2026
1161860
Merge branch 'main' into show-mini-nav
OlwenPearce Nov 6, 2026
5eaf655
Post rebase issues
OlwenPearce Nov 6, 2026
c050538
Fix padding reversion
OlwenPearce Nov 6, 2026
f2b0a18
Slight padding
OlwenPearce Nov 6, 2026
a31ced5
Merge branch 'main' into show-mini-nav
OlwenPearce Nov 6, 2026
2dc143f
Merge branch 'show-mini-nav' of github.com:guardian/dotcom-rendering …
OlwenPearce Nov 6, 2026
f024f86
Generate new schemas
OlwenPearce Nov 6, 2026
1bd5664
Rebase fix: make pageId optional
OlwenPearce Nov 6, 2026
71179f8
Rebase fix: remove unused var
OlwenPearce Nov 6, 2026
6ec5fd5
Rebase fix: remove unused var
OlwenPearce Nov 6, 2026
a411dc2
Separate out mini nav somewhat
OlwenPearce Nov 9, 2026
7c48c30
Create separate mini nav component
OlwenPearce Nov 9, 2026
16d56ad
Remove fallback that got dropped
OlwenPearce Nov 9, 2026
7e5fd84
Revert slight pad
OlwenPearce Nov 9, 2026
42be00f
Post rebase fix
OlwenPearce Nov 9, 2026
deac4bc
Make padding split clearer (I think)
OlwenPearce Nov 9, 2026
25bc226
Fix padding on no header case
OlwenPearce Nov 9, 2026
0296e73
Tweaks
OlwenPearce Nov 9, 2026
8893e9f
Tweaks
OlwenPearce Nov 9, 2026
6f4b03a
Clearer split of styles
OlwenPearce Nov 10, 2026
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
26 changes: 26 additions & 0 deletions dotcom-rendering/fixtures/manual/customSubnav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { CustomSubnav } from '../../src/types/customSubnav';

export const customSubnav: CustomSubnav = {
id: 'us-politics',
header: {
headerText: 'US politics',
copy: '',
dotcomPath: '/us-news/us-politics',
showHeaderText: true,
},
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: [],
};
4 changes: 4 additions & 0 deletions dotcom-rendering/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { TrailText, type TrailTextSize } from './components/TrailText';
export type Position = 'inner' | 'outer' | 'none';

export type Props = {
headerContent?: React.ReactNode;
linkTo: string;
format: ArticleFormat;
/** The format of the article holding the card */
Expand Down Expand Up @@ -353,6 +354,7 @@ const decideSublinkPosition = (
};

export const Card = ({
headerContent,
linkTo,
format,
contextFormat,
Expand Down Expand Up @@ -818,6 +820,7 @@ export const Card = ({

return (
<CardWrapper
topBarPadding={headerContent ? space[1] : undefined}
format={format}
showTopBarDesktop={showTopBarDesktop}
showTopBarMobile={showTopBarMobile}
Expand All @@ -828,6 +831,7 @@ export const Card = ({
: undefined
}
>
{headerContent}
<CardLink
linkTo={linkTo}
headlineText={headlineText}
Expand Down
33 changes: 22 additions & 11 deletions dotcom-rendering/src/components/Card/components/CardWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type Props = {
showTopBarMobile: boolean;
containerPalette?: DCRContainerPalette;
topBarColour?: string;
topBarPadding?: number;
isLoopAndInLoopClickTestVariant?: boolean;
};

const baseCardStyles = css`
Expand Down Expand Up @@ -41,7 +43,7 @@ const baseCardStyles = css`
text-decoration: none;
`;

const hoverStyles = css`
const hoverStyles = (isLoopAndInLoopClickTestVariant: boolean) => css`
:hover .media-overlay {
width: 100%;
height: 100%;
Expand All @@ -59,7 +61,12 @@ const hoverStyles = css`
*/
:has(
ul.sublinks:hover,
.video-container:not(.cinemagraph):hover,
.mini-nav:hover,
.video-container:not(
${isLoopAndInLoopClickTestVariant
? `.cinemagraph, .loop`
: `.cinemagraph`}
):hover,
Comment thread
OlwenPearce marked this conversation as resolved.
Resolved
.slideshow-carousel-footer:hover,
.branding-logo:hover
) {
Expand All @@ -72,23 +79,23 @@ const hoverStyles = css`
}
`;

const topBarStyles = (colour: string) => css`
const topBarStyles = (colour: string, padding: number) => css`
::before {
border-top: 1px solid ${colour};
content: '';
width: 100%;
padding-bottom: ${space[2]}px;
padding-bottom: ${padding}px;
background-color: unset;
}
`;
const mobileTopBarStyles = (colour: string) => css`
const mobileTopBarStyles = (colour: string, padding: number) => css`
${until.tablet} {
${topBarStyles(colour)}
${topBarStyles(colour, padding)}
}
`;
const desktopTopBarStyles = (colour: string) => css`
const desktopTopBarStyles = (colour: string, padding: number) => css`
${from.tablet} {
${topBarStyles(colour)}
${topBarStyles(colour, padding)}
}
`;

Expand All @@ -99,16 +106,20 @@ export const CardWrapper = ({
showTopBarMobile,
containerPalette,
topBarColour = palette('--card-border-top'),
topBarPadding = space[2],
isLoopAndInLoopClickTestVariant,
}: Props) => {
return (
<FormatBoundary format={format}>
<ContainerOverrides containerPalette={containerPalette}>
<div
css={[
baseCardStyles,
hoverStyles,
showTopBarDesktop && desktopTopBarStyles(topBarColour),
showTopBarMobile && mobileTopBarStyles(topBarColour),
hoverStyles(isLoopAndInLoopClickTestVariant === true),
showTopBarDesktop &&
desktopTopBarStyles(topBarColour, topBarPadding),
showTopBarMobile &&
mobileTopBarStyles(topBarColour, topBarPadding),
]}
>
{children}
Expand Down
35 changes: 35 additions & 0 deletions dotcom-rendering/src/components/CardMiniNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { css } from '@emotion/react';
import { getZIndex } from '../lib/getZIndex';
import { palette } from '../palette';
import type { DCRFrontCard } from '../types/front';
import { CustomSubNav } from './Masthead/Titlepiece/CustomSubNav';

type Props = {
miniNavData: DCRFrontCard['miniNavData'];
};

export const CardMiniNav = ({ miniNavData }: Props) => {
if (!miniNavData) {
return null;
}

return (
<div
className="mini-nav"
css={css`
position: relative;
z-index: ${getZIndex('card-nested-link')};
min-width: 0;
--custom-subnav-header-text: ${palette(
miniNavData.header.dotcomPath
? '--card-kicker-text'
: '--card-headline',
)};
--masthead-nav-link-text: ${palette('--card-headline')};
--masthead-nav-link-text-hover: ${palette('--card-headline')};
`}
>
<CustomSubNav customSubNav={miniNavData} renderingPage="card" />
</div>
);
};
201 changes: 201 additions & 0 deletions dotcom-rendering/src/components/FrontCard.miniNav.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { render, screen } from '@testing-library/react';
import { customSubnav } from '../../fixtures/manual/customSubnav';
import { trails } from '../../fixtures/manual/trails';
import type { CustomSubnav } from '../types/customSubnav';
import type { DCRContainerType, DCRFrontCard } from '../types/front';
import { ConfigProvider } from './ConfigContext';
import { FrontCard } from './FrontCard';

const miniNavData = {
...customSubnav,
id: 'football-mini-nav',
header: {
...customSubnav.header,
headerText: 'Football',
dotcomPath: '/football',
copy: 'Football navigation',
},
format: 'compact',
links: [{ linkText: 'More Football', dotcomPath: '/football' }],
} satisfies CustomSubnav;

const defaultTrail = {
...trails[0],
boostLevel: 'boost',
showMiniNav: true,
miniNavData,
} satisfies DCRFrontCard;

const renderCard = ({
trail = defaultTrail,
...props
}: {
trail?: DCRFrontCard;
containerType?: DCRContainerType;
} = {}) =>
render(
<ConfigProvider
value={{
renderingTarget: 'Web',
darkModeAvailable: false,
assetOrigin: '/',
editionId: 'UK',
}}
>
<FrontCard
trail={trail}
containerType="flexible/general"
imageLoading="eager"
serverTime={1793031866891}
{...props}
/>
</ConfigProvider>,
);

const expectMiniNavAbsent = () => {
expect(screen.queryByText('Football')).not.toBeInTheDocument();
expect(
screen.queryByRole('link', { name: 'More Football' }),
).not.toBeInTheDocument();
expect(screen.getByText(defaultTrail.headline)).toBeInTheDocument();
};

describe('FrontCard mini navigation', () => {
it.each(['boost', 'megaboost', 'gigaboost'] as const)(
'renders mini navigation for a %s card',
(boostLevel) => {
renderCard({ trail: { ...defaultTrail, boostLevel } });

expect(
screen.getByRole('link', { name: 'More Football' }),
).toHaveAttribute('href', '/football');
expect(screen.getByText(defaultTrail.headline)).toBeInTheDocument();
},
);

it.each([undefined, 'default'] as const)(
'does not render mini navigation with boost level %s',
(boostLevel) => {
renderCard({ trail: { ...defaultTrail, boostLevel } });

expectMiniNavAbsent();
},
);

it.each([undefined, 'static/medium/4', 'flexible/special'] as const)(
'does not render mini navigation in container %s',
(containerType) => {
renderCard({ containerType });

expectMiniNavAbsent();
},
);

it('does not render mini navigation when disabled', () => {
renderCard({ trail: { ...defaultTrail, showMiniNav: false } });

expectMiniNavAbsent();
});

it('does not render mini navigation when configuration is missing', () => {
renderCard({ trail: { ...defaultTrail, miniNavData: undefined } });

expectMiniNavAbsent();
});

it('renders a header link without an underline when header text is enabled', () => {
renderCard();

const header = screen.getByRole('link', { name: 'Football' });
expect(header).toHaveAttribute('href', '/football');
expect(header).toHaveStyle('text-decoration: none');
});

it('renders header text without a link when the header has no path', () => {
renderCard({
trail: {
...defaultTrail,
miniNavData: {
...miniNavData,
header: { ...miniNavData.header, dotcomPath: undefined },
},
},
});

expect(screen.getByText('Football')).toBeInTheDocument();
expect(screen.getByText('Football').closest('a')).toBeNull();
expect(screen.getByText('Football').closest('.mini-nav')).toHaveStyle({
'--custom-subnav-header-text': 'var(--card-headline)',
});
expect(
screen.getByRole('link', { name: 'More Football' }),
).toHaveAttribute('href', '/football');
});

it('hides the header without hiding navigation links when header text is disabled', () => {
renderCard({
trail: {
...defaultTrail,
miniNavData: {
...miniNavData,
header: { ...miniNavData.header, showHeaderText: false },
},
},
});

expect(screen.queryByText('Football')).not.toBeInTheDocument();
expect(
screen.getByRole('link', { name: 'More Football' }),
).toHaveAttribute('href', '/football');
expect(screen.getByText(defaultTrail.headline)).toBeInTheDocument();
});

it('places mini navigation before the card content in a full-width column', () => {
renderCard();

const miniNav = screen
.getByRole('link', { name: 'More Football' })
.closest('[data-component-id]');
const navWrapper = miniNav?.parentElement;
const cardWrapper = navWrapper?.parentElement;

expect(cardWrapper).toHaveStyle({
display: 'flex',
flexDirection: 'column',
width: '100%',
});
expect(cardWrapper?.firstElementChild).toBe(navWrapper);
expect(cardWrapper).toContainElement(
screen.getByText(defaultTrail.headline),
);
});

it('uses card styling rather than article subnav styling', () => {
renderCard();

const miniNav = screen
.getByRole('link', { name: 'More Football' })
.closest('[data-component-id]');
const list = screen.getByRole('list');

expect(miniNav).toHaveAttribute('data-rendering-page', 'card');
// Article subnavs have a divider (with an 8px gap) and fixed row heights; card mini-navs must not.
// (jsdom drops the `border-left` shorthand as it contains a CSS var, so assert on the divider's padding.)
expect(list).not.toHaveStyle({ paddingLeft: '8px' });
expect(miniNav).not.toHaveStyle({ minHeight: '36px' });
});

it('uses the card kicker colour for the header and headline colour for links', () => {
renderCard();

const miniNav = screen
.getByRole('link', { name: 'More Football' })
.closest('[data-component-id]');

expect(miniNav?.parentElement).toHaveStyle({
'--custom-subnav-header-text': 'var(--card-kicker-text)',
'--masthead-nav-link-text': 'var(--card-headline)',
'--masthead-nav-link-text-hover': 'var(--card-headline)',
});
});
});
Loading
Loading