Skip to content

Commit f1c2b54

Browse files
authored
Football mini match stats component (#14947)
* Basic layout and styling for match stat * Add divider to chart * Move hardcoded values to props * Calculate bar width * Include team name for accessibility * Add FootballMiniMatchStats components * Update match stat stories * Simplified live grid layout for stories * Rename stories * Support layout variations for use in different contexts * Hide chart from assistive technology * Add football stat colours to palette * Update dark mode palette based on latest designs * Add button hover colours to palette * Remove console log * Use spacing from Source where possible * Incorrect team name being used for home stat
1 parent 642828e commit f1c2b54

File tree

5 files changed

+430
-0
lines changed

5 files changed

+430
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { css } from '@emotion/react';
2+
import { space } from '@guardian/source/foundations';
3+
import type { Meta, StoryObj } from '@storybook/react-webpack5';
4+
import { palette } from '../palette';
5+
import { FootballMatchStat } from './FootballMatchStat';
6+
7+
const meta = {
8+
title: 'Components/Football Match Stat',
9+
component: FootballMatchStat,
10+
decorators: [
11+
(Story) => (
12+
<div
13+
css={css`
14+
padding: ${space[4]}px;
15+
background-color: ${palette(
16+
'--football-live-blog-background',
17+
)};
18+
`}
19+
>
20+
<Story />
21+
</div>
22+
),
23+
],
24+
parameters: {
25+
viewport: {
26+
defaultViewport: 'mobileMedium',
27+
},
28+
},
29+
} satisfies Meta<typeof FootballMatchStat>;
30+
31+
export default meta;
32+
type Story = StoryObj<typeof meta>;
33+
34+
export const Default = {
35+
args: {
36+
label: 'Goal Attempts',
37+
home: {
38+
teamName: 'Manchester United',
39+
teamColour: '#da020e',
40+
value: 7,
41+
},
42+
away: {
43+
teamName: 'Arsenal',
44+
teamColour: '#023474',
45+
value: 4,
46+
},
47+
},
48+
} satisfies Story;
49+
50+
export const ShownAsPercentage = {
51+
args: {
52+
label: 'Possession',
53+
home: {
54+
teamName: 'West Ham',
55+
teamColour: '#722642',
56+
value: 39,
57+
},
58+
away: {
59+
teamName: 'Newcastle',
60+
teamColour: '#383838',
61+
value: 61,
62+
},
63+
showPercentage: true,
64+
},
65+
} satisfies Story;
66+
67+
export const RaisedLabelOnDesktop = {
68+
args: {
69+
...Default.args,
70+
raiseLabelOnDesktop: true,
71+
},
72+
} satisfies Story;
73+
74+
export const LargeNumbersOnDesktop = {
75+
args: {
76+
...Default.args,
77+
largeNumbersOnDesktop: true,
78+
},
79+
} satisfies Story;
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { css } from '@emotion/react';
2+
import {
3+
from,
4+
space,
5+
textSansBold14,
6+
textSansBold15,
7+
textSansBold20,
8+
textSansBold28,
9+
visuallyHidden,
10+
} from '@guardian/source/foundations';
11+
import { palette } from '../palette';
12+
13+
const containerCss = css`
14+
position: relative;
15+
padding: 5px 10px 10px;
16+
border: 1px solid ${palette('--football-match-stat-border')};
17+
border-radius: 6px;
18+
&::before {
19+
position: absolute;
20+
content: '';
21+
left: 50%;
22+
bottom: 0;
23+
width: 1px;
24+
height: ${space[6]}px;
25+
background-color: ${palette('--football-match-stat-border')};
26+
}
27+
`;
28+
29+
const headerCss = css`
30+
display: grid;
31+
grid-template-columns: auto 1fr auto;
32+
grid-template-areas: 'home-stat label away-stat';
33+
`;
34+
35+
const raiseLabelCss = css`
36+
${from.desktop} {
37+
grid-template-areas:
38+
'label label label'
39+
'home-stat . away-stat';
40+
}
41+
`;
42+
43+
const labelCss = css`
44+
${textSansBold14};
45+
grid-area: label;
46+
justify-self: center;
47+
color: ${palette('--football-match-stat-name')};
48+
${from.desktop} {
49+
${textSansBold15};
50+
}
51+
`;
52+
53+
const numberCss = css`
54+
${textSansBold20};
55+
grid-area: home-stat;
56+
color: var(--match-stat-team-colour);
57+
`;
58+
59+
const largeNumberCss = css`
60+
${from.desktop} {
61+
${textSansBold28}
62+
}
63+
`;
64+
65+
const awayStatCss = css`
66+
grid-area: away-stat;
67+
justify-self: end;
68+
`;
69+
70+
const chartCss = css`
71+
position: relative;
72+
display: flex;
73+
gap: 10px;
74+
`;
75+
76+
const barCss = css`
77+
height: ${space[2]}px;
78+
width: var(--match-stat-percentage);
79+
background-color: var(--match-stat-team-colour);
80+
border-radius: 8px;
81+
`;
82+
83+
type MatchStatistic = {
84+
teamName: string;
85+
teamColour: string;
86+
value: number;
87+
};
88+
89+
type Props = {
90+
label: string;
91+
home: MatchStatistic;
92+
away: MatchStatistic;
93+
showPercentage?: boolean;
94+
raiseLabelOnDesktop?: boolean;
95+
largeNumbersOnDesktop?: boolean;
96+
};
97+
98+
const formatValue = (value: number, showPercentage: boolean) =>
99+
`${value}${showPercentage ? '%' : ''}`;
100+
101+
export const FootballMatchStat = ({
102+
label,
103+
home,
104+
away,
105+
showPercentage = false,
106+
raiseLabelOnDesktop = false,
107+
largeNumbersOnDesktop = false,
108+
}: Props) => {
109+
const homePercentage = (home.value / (home.value + away.value)) * 100;
110+
const awayPercentage = (away.value / (home.value + away.value)) * 100;
111+
112+
return (
113+
<div css={containerCss}>
114+
<div css={[headerCss, raiseLabelOnDesktop && raiseLabelCss]}>
115+
<span css={labelCss}>{label}</span>
116+
<span
117+
css={[numberCss, largeNumbersOnDesktop && largeNumberCss]}
118+
style={{ '--match-stat-team-colour': home.teamColour }}
119+
>
120+
<span
121+
css={css`
122+
${visuallyHidden}
123+
`}
124+
>
125+
{home.teamName}
126+
</span>
127+
{formatValue(home.value, showPercentage)}
128+
</span>
129+
<span
130+
css={[
131+
numberCss,
132+
awayStatCss,
133+
largeNumbersOnDesktop && largeNumberCss,
134+
]}
135+
style={{ '--match-stat-team-colour': away.teamColour }}
136+
>
137+
<span
138+
css={css`
139+
${visuallyHidden}
140+
`}
141+
>
142+
{away.teamName}
143+
</span>
144+
{formatValue(away.value, showPercentage)}
145+
</span>
146+
</div>
147+
<div aria-hidden="true" css={chartCss}>
148+
<div
149+
css={barCss}
150+
style={{
151+
'--match-stat-percentage': `${homePercentage}%`,
152+
'--match-stat-team-colour': home.teamColour,
153+
}}
154+
></div>
155+
<div
156+
css={barCss}
157+
style={{
158+
'--match-stat-percentage': `${awayPercentage}%`,
159+
'--match-stat-team-colour': away.teamColour,
160+
}}
161+
></div>
162+
</div>
163+
</div>
164+
);
165+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { css } from '@emotion/react';
2+
import { breakpoints, from } from '@guardian/source/foundations';
3+
import type { Meta, StoryObj } from '@storybook/react-webpack5';
4+
import { palette } from '../palette';
5+
import { FootballMiniMatchStats as FootballMiniMatchStatsComponent } from './FootballMiniMatchStats';
6+
7+
const gridCss = css`
8+
background-color: ${palette('--football-live-blog-background')};
9+
/**
10+
* Extremely simplified live blog grid layout as we're only interested in
11+
* the 240px wide left column added at the desktop breakpoint.
12+
* dotcom-rendering/src/layouts/LiveLayout.tsx
13+
*/
14+
${from.desktop} {
15+
display: grid;
16+
grid-column-gap: 20px;
17+
grid-template-columns: 240px 1fr;
18+
}
19+
`;
20+
21+
const containerCss = css`
22+
padding: 10px;
23+
${from.desktop} {
24+
padding-left: 20px;
25+
padding-right: 0;
26+
}
27+
`;
28+
29+
const meta = {
30+
title: 'Components/Football Mini Match Stats',
31+
component: FootballMiniMatchStatsComponent,
32+
decorators: [
33+
(Story) => (
34+
<div css={gridCss}>
35+
<div css={containerCss}>
36+
<Story />
37+
</div>
38+
</div>
39+
),
40+
],
41+
parameters: {
42+
chromatic: {
43+
viewports: [
44+
breakpoints.mobileMedium,
45+
breakpoints.tablet,
46+
breakpoints.wide,
47+
],
48+
},
49+
},
50+
} satisfies Meta<typeof FootballMiniMatchStatsComponent>;
51+
52+
export default meta;
53+
type Story = StoryObj<typeof meta>;
54+
55+
export const FootballMiniMatchStats = {
56+
args: {
57+
homeTeam: {
58+
name: 'Manchester United',
59+
colour: '#da020e',
60+
},
61+
awayTeam: {
62+
name: 'Arsenal',
63+
colour: '#023474',
64+
},
65+
stats: [
66+
{
67+
label: 'Possession',
68+
homeValue: 39,
69+
awayValue: 61,
70+
showPercentage: true,
71+
},
72+
{ label: 'Goal Attempts', homeValue: 7, awayValue: 4 },
73+
],
74+
},
75+
} satisfies Story;

0 commit comments

Comments
 (0)