Skip to content
Merged
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
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { gridContainerDecorator } from '../../../.storybook/decorators/gridDecorators';
import { palette } from '../../palette';
import { Tabs } from './Tabs';

const meta = {
component: Tabs,
} satisfies Meta<typeof Tabs>;

export default meta;

type Story = StoryObj<typeof meta>;

export const MatchInfoWhenFixture = {
args: {
selected: 'info',
},
parameters: {
colourSchemeBackground: {
light: palette('--football-match-header-fixture-result-background'),
dark: palette('--football-match-header-fixture-result-background'),
},
},
decorators: [gridContainerDecorator],
} satisfies Story;

export const LiveWhenLive = {
...MatchInfoWhenFixture,
args: {
selected: 'live',
infoURL: new URL(
'https://www.theguardian.com/football/match/2025/nov/26/arsenal-v-bayernmunich',
),
},
} satisfies Story;

export const ReportWhenResult = {
...MatchInfoWhenFixture,
args: {
selected: 'report',
liveURL: new URL(
'https://www.theguardian.com/football/live/2025/nov/26/arsenal-v-bayern-munich-champions-league-live',
),
infoURL: LiveWhenLive.args.infoURL,
},
} satisfies Story;
148 changes: 148 additions & 0 deletions dotcom-rendering/src/components/FootballMatchHeader/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { css } from '@emotion/react';
import {
from,
headlineBold15Object,
headlineBold17Object,
space,
} from '@guardian/source/foundations';
import type { ReactNode } from 'react';
import { grid } from '../../grid';
import { palette } from '../../palette';

type Props =
| {
selected: 'info';
reportURL?: URL;
liveURL?: URL;
}
| {
selected: 'live';
reportURL?: URL;
infoURL: URL;
}
| {
selected: 'report';
liveURL?: URL;
infoURL: URL;
};

export const Tabs = (props: Props) => (
<nav css={[grid.column.centre]}>
<ul
css={{
...headlineBold15Object,
paddingTop: space[2],
display: 'flex',
width: '100%',
borderBottomWidth: 1,
borderStyle: 'solid',
borderColor: palette(
'--football-match-header-fixture-result-border',
),
[from.leftCol]: headlineBold17Object,
}}
>
<MatchReport {...props} />
<LiveFeed {...props} />
<MatchInfo {...props} />
</ul>
</nav>
);

const MatchReport = (props: Props) => {
if (props.selected === 'report') {
return <Tab>Match report</Tab>;
}

if (props.reportURL !== undefined) {
return <Tab href={props.reportURL}>Match report</Tab>;
}

return null;
};

const LiveFeed = (props: Props) => {
if (props.selected === 'live') {
return <Tab>Live feed</Tab>;
}

if (props.liveURL !== undefined) {
return <Tab href={props.liveURL}>Live feed</Tab>;
}

return null;
};

const MatchInfo = (props: Props) => {
if (props.selected === 'info') {
return <Tab>Match info</Tab>;
}

return <Tab href={props.infoURL}>Match info</Tab>;
};

const Tab = (props: { children: ReactNode; href?: URL }) => (
<li
css={{
// Ensures that if there are only two tabs they take up exactly 50%
flex: '1 1 50%',
borderLeftStyle: 'solid',
borderLeftColor: palette(
'--football-match-header-fixture-result-border',
),
'&:not(:first-of-type)': {
paddingLeft: space[2],
borderLeftWidth: 1,
},
[from.leftCol]: {
paddingLeft: space[2],
borderLeftWidth: 1,
flex: '0 0 auto',
},
}}
>
<TabText href={props.href}>{props.children}</TabText>
</li>
);

const TabText = (props: { children: ReactNode; href?: URL }) => {
if (props.href !== undefined) {
return (
<a href={props.href.toString()} css={tabTextStyles}>
{props.children}
</a>
);
}

return (
<span
css={tabTextStyles}
style={{
borderBottomColor: palette(
'--football-match-header-fixture-result-selected',
),
}}
>
{props.children}
</span>
);
};

const tabTextStyles = css({
display: 'block',
paddingBottom: space[2],
borderBottomWidth: space[1],
borderBottomStyle: 'solid',
borderBottomColor: 'transparent',
color: palette('--football-match-header-fixture-result-primary-text'),
textDecoration: 'none',
'&:hover': {
borderBottomColor: palette(
'--football-match-header-fixture-result-selected',
),
},
[from.leftCol]: {
paddingRight: space[6],
paddingBottom: space[3],
},
});
20 changes: 20 additions & 0 deletions dotcom-rendering/src/paletteDeclarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7047,6 +7047,26 @@ const paletteColours = {
light: () => sourcePalette.neutral[97],
dark: () => sourcePalette.neutral[10],
},
'--football-match-header-fixture-result-background': {
light: () => sourcePalette.sport[300],
dark: () => sourcePalette.sport[300],
},
'--football-match-header-fixture-result-border': {
light: () => `${sourcePalette.neutral[100]}33`,
dark: () => `${sourcePalette.neutral[100]}33`,
},
'--football-match-header-fixture-result-primary-text': {
light: () => sourcePalette.neutral[100],
dark: () => sourcePalette.neutral[100],
},
'--football-match-header-fixture-result-secondary-text': {
light: () => sourcePalette.sport[700],
dark: () => sourcePalette.sport[700],
},
'--football-match-header-fixture-result-selected': {
light: () => sourcePalette.sport[600],
dark: () => sourcePalette.sport[600],
},
'--football-match-hover': {
light: () => sourcePalette.neutral[93],
dark: () => sourcePalette.neutral[38],
Expand Down
Loading