Skip to content

Commit f970caf

Browse files
authored
Football match header tabs (#14997)
The tabs component of the new football match header design. Note that this does not yet include "live" styles.
1 parent b69e6cf commit f970caf

File tree

3 files changed

+214
-0
lines changed

3 files changed

+214
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { Meta, StoryObj } from '@storybook/react-webpack5';
2+
import { gridContainerDecorator } from '../../../.storybook/decorators/gridDecorators';
3+
import { palette } from '../../palette';
4+
import { Tabs } from './Tabs';
5+
6+
const meta = {
7+
component: Tabs,
8+
} satisfies Meta<typeof Tabs>;
9+
10+
export default meta;
11+
12+
type Story = StoryObj<typeof meta>;
13+
14+
export const MatchInfoWhenFixture = {
15+
args: {
16+
selected: 'info',
17+
},
18+
parameters: {
19+
colourSchemeBackground: {
20+
light: palette('--football-match-header-fixture-result-background'),
21+
dark: palette('--football-match-header-fixture-result-background'),
22+
},
23+
},
24+
decorators: [gridContainerDecorator],
25+
} satisfies Story;
26+
27+
export const LiveWhenLive = {
28+
...MatchInfoWhenFixture,
29+
args: {
30+
selected: 'live',
31+
infoURL: new URL(
32+
'https://www.theguardian.com/football/match/2025/nov/26/arsenal-v-bayernmunich',
33+
),
34+
},
35+
} satisfies Story;
36+
37+
export const ReportWhenResult = {
38+
...MatchInfoWhenFixture,
39+
args: {
40+
selected: 'report',
41+
liveURL: new URL(
42+
'https://www.theguardian.com/football/live/2025/nov/26/arsenal-v-bayern-munich-champions-league-live',
43+
),
44+
infoURL: LiveWhenLive.args.infoURL,
45+
},
46+
} satisfies Story;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { css } from '@emotion/react';
2+
import {
3+
from,
4+
headlineBold15Object,
5+
headlineBold17Object,
6+
space,
7+
} from '@guardian/source/foundations';
8+
import type { ReactNode } from 'react';
9+
import { grid } from '../../grid';
10+
import { palette } from '../../palette';
11+
12+
type Props =
13+
| {
14+
selected: 'info';
15+
reportURL?: URL;
16+
liveURL?: URL;
17+
}
18+
| {
19+
selected: 'live';
20+
reportURL?: URL;
21+
infoURL: URL;
22+
}
23+
| {
24+
selected: 'report';
25+
liveURL?: URL;
26+
infoURL: URL;
27+
};
28+
29+
export const Tabs = (props: Props) => (
30+
<nav css={[grid.column.centre]}>
31+
<ul
32+
css={{
33+
...headlineBold15Object,
34+
paddingTop: space[2],
35+
display: 'flex',
36+
width: '100%',
37+
borderBottomWidth: 1,
38+
borderStyle: 'solid',
39+
borderColor: palette(
40+
'--football-match-header-fixture-result-border',
41+
),
42+
[from.leftCol]: headlineBold17Object,
43+
}}
44+
>
45+
<MatchReport {...props} />
46+
<LiveFeed {...props} />
47+
<MatchInfo {...props} />
48+
</ul>
49+
</nav>
50+
);
51+
52+
const MatchReport = (props: Props) => {
53+
if (props.selected === 'report') {
54+
return <Tab>Match report</Tab>;
55+
}
56+
57+
if (props.reportURL !== undefined) {
58+
return <Tab href={props.reportURL}>Match report</Tab>;
59+
}
60+
61+
return null;
62+
};
63+
64+
const LiveFeed = (props: Props) => {
65+
if (props.selected === 'live') {
66+
return <Tab>Live feed</Tab>;
67+
}
68+
69+
if (props.liveURL !== undefined) {
70+
return <Tab href={props.liveURL}>Live feed</Tab>;
71+
}
72+
73+
return null;
74+
};
75+
76+
const MatchInfo = (props: Props) => {
77+
if (props.selected === 'info') {
78+
return <Tab>Match info</Tab>;
79+
}
80+
81+
return <Tab href={props.infoURL}>Match info</Tab>;
82+
};
83+
84+
const Tab = (props: { children: ReactNode; href?: URL }) => (
85+
<li
86+
css={{
87+
// Ensures that if there are only two tabs they take up exactly 50%
88+
flex: '1 1 50%',
89+
borderLeftStyle: 'solid',
90+
borderLeftColor: palette(
91+
'--football-match-header-fixture-result-border',
92+
),
93+
'&:not(:first-of-type)': {
94+
paddingLeft: space[2],
95+
borderLeftWidth: 1,
96+
},
97+
[from.leftCol]: {
98+
paddingLeft: space[2],
99+
borderLeftWidth: 1,
100+
flex: '0 0 auto',
101+
},
102+
}}
103+
>
104+
<TabText href={props.href}>{props.children}</TabText>
105+
</li>
106+
);
107+
108+
const TabText = (props: { children: ReactNode; href?: URL }) => {
109+
if (props.href !== undefined) {
110+
return (
111+
<a href={props.href.toString()} css={tabTextStyles}>
112+
{props.children}
113+
</a>
114+
);
115+
}
116+
117+
return (
118+
<span
119+
css={tabTextStyles}
120+
style={{
121+
borderBottomColor: palette(
122+
'--football-match-header-fixture-result-selected',
123+
),
124+
}}
125+
>
126+
{props.children}
127+
</span>
128+
);
129+
};
130+
131+
const tabTextStyles = css({
132+
display: 'block',
133+
paddingBottom: space[2],
134+
borderBottomWidth: space[1],
135+
borderBottomStyle: 'solid',
136+
borderBottomColor: 'transparent',
137+
color: palette('--football-match-header-fixture-result-primary-text'),
138+
textDecoration: 'none',
139+
'&:hover': {
140+
borderBottomColor: palette(
141+
'--football-match-header-fixture-result-selected',
142+
),
143+
},
144+
[from.leftCol]: {
145+
paddingRight: space[6],
146+
paddingBottom: space[3],
147+
},
148+
});

dotcom-rendering/src/paletteDeclarations.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7092,6 +7092,26 @@ const paletteColours = {
70927092
light: () => sourcePalette.neutral[97],
70937093
dark: () => sourcePalette.neutral[10],
70947094
},
7095+
'--football-match-header-fixture-result-background': {
7096+
light: () => sourcePalette.sport[300],
7097+
dark: () => sourcePalette.sport[300],
7098+
},
7099+
'--football-match-header-fixture-result-border': {
7100+
light: () => `${sourcePalette.neutral[100]}33`,
7101+
dark: () => `${sourcePalette.neutral[100]}33`,
7102+
},
7103+
'--football-match-header-fixture-result-primary-text': {
7104+
light: () => sourcePalette.neutral[100],
7105+
dark: () => sourcePalette.neutral[100],
7106+
},
7107+
'--football-match-header-fixture-result-secondary-text': {
7108+
light: () => sourcePalette.sport[700],
7109+
dark: () => sourcePalette.sport[700],
7110+
},
7111+
'--football-match-header-fixture-result-selected': {
7112+
light: () => sourcePalette.sport[600],
7113+
dark: () => sourcePalette.sport[600],
7114+
},
70957115
'--football-match-hover': {
70967116
light: () => sourcePalette.neutral[93],
70977117
dark: () => sourcePalette.neutral[38],

0 commit comments

Comments
 (0)