Skip to content
Open
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
2 changes: 1 addition & 1 deletion dotcom-rendering/src/components/Distribution.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css } from '@emotion/react';
import { headlineBold34, text } from '@guardian/source/foundations';
import { isLight } from '../lib/isLight';
import { isLight } from '../lib/colour';

type Props = {
left: BarType;
Expand Down
2 changes: 1 addition & 1 deletion dotcom-rendering/src/components/Doughnut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
text,
textSans15,
} from '@guardian/source/foundations';
import { isLight } from '../lib/isLight';
import { isLight } from '../lib/colour';

type Props = {
sections: SectionType[];
Expand Down
132 changes: 132 additions & 0 deletions dotcom-rendering/src/components/FootballMatchStat.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { css } from '@emotion/react';
import { space } from '@guardian/source/foundations';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator';
import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat';
import { palette } from '../palette';
import { FootballMatchStat } from './FootballMatchStat';

Expand All @@ -20,6 +22,13 @@ const meta = {
<Story />
</div>
),
splitTheme([
{
design: ArticleDesign.Standard,
display: ArticleDisplay.Standard,
theme: Pillar.News,
},
]),
],
parameters: {
viewport: {
Expand All @@ -31,6 +40,97 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;

const teams = [
{
home: { name: 'Wolves', colour: '#faa01b' },
away: { name: 'Man Utd', colour: '#b00101' },
},
{
home: { name: 'Fulham', colour: '#ffffff' },
away: { name: 'C Palace', colour: '#af1f17' },
},
{
home: { name: 'Brighton', colour: '#2a449a' },
away: { name: 'West Ham', colour: '#7c1e42' },
},
{
home: { name: 'Leeds', colour: '#f5f5f5' },
away: { name: 'Liverpool', colour: '#ce070c' },
},
{
home: { name: 'AFC Bournemouth', colour: '#c80000' },
away: { name: 'Chelsea', colour: '#005ca4' },
},
{
home: { name: 'Everton', colour: '#00349a' },
away: { name: 'Nottm Forest', colour: '#db1812' },
},
{
home: { name: 'Man City', colour: '#5cbfeb' },
away: { name: 'Sunderland', colour: '#d51022' },
},
{
home: { name: 'Newcastle', colour: '#383838' },
away: { name: 'Burnley', colour: '#570e30' },
},
{
home: { name: 'Spurs', colour: '#ffffff' },
away: { name: 'Brentford', colour: '#c4040f' },
},
{
home: { name: 'Aston Villa', colour: '#720e44' },
away: { name: 'Arsenal', colour: '#c40007' },
},
{
home: { name: 'Birmingham', colour: '#01009a' },
away: { name: 'Norwich', colour: '#ffe400' },
},
{
home: { name: 'Derby', colour: '#ffffff' },
away: { name: 'Watford', colour: '#fef502' },
},
{
home: { name: 'Leicester', colour: '#4b2cd3' },
away: { name: 'Stoke', colour: '#cc0617' },
},
{
home: { name: 'Oxford Utd', colour: '#fec726' },
away: { name: 'Middlesbrough', colour: '#e70101' },
},
{
home: { name: 'Portsmouth', colour: '#0077ac' },
away: { name: 'Millwall', colour: '#1a2791' },
},
{
home: { name: 'QPR', colour: '#1f539f' },
away: { name: 'Hull', colour: '#f2b100' },
},
{
home: { name: 'Bristol City', colour: '#c70c23' },
away: { name: 'Swansea', colour: '#ffffff' },
},
{
home: { name: 'Charlton', colour: '#d4222b' },
away: { name: 'Southampton', colour: '#d71921' },
},
{
home: { name: 'Coventry', colour: '#b1d0ff' },
away: { name: 'West Brom', colour: '#00246a' },
},
{
home: { name: 'Celtic', colour: '#559861' },
away: { name: 'Dundee', colour: '#000033' },
},
{
home: { name: 'Falkirk', colour: '#002341' },
away: { name: 'Motherwell', colour: '#f0c650' },
},
{
home: { name: 'Dundee Utd', colour: '#ff6c00' },
away: { name: 'Rangers', colour: '#195091' },
},
];

export const Default = {
args: {
label: 'Goal Attempts',
Expand Down Expand Up @@ -77,3 +177,35 @@ export const LargeNumbersOnDesktop = {
largeNumbersOnDesktop: true,
},
} satisfies Story;

export const TeamColours = {
render: (args) => (
<div
css={css`
display: flex;
flex-direction: column;
gap: ${space[2]}px;
`}
>
{teams.map((match, index) => (
<FootballMatchStat
{...args}
home={{
teamName: match.home.name,
teamColour: match.home.colour,
value: 50,
}}
away={{
teamName: match.away.name,
teamColour: match.away.colour,
value: 50,
}}
key={index}
/>
))}
</div>
),
args: {
...ShownAsPercentage.args,
},
} satisfies Story;
83 changes: 80 additions & 3 deletions dotcom-rendering/src/components/FootballMatchStat.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { css } from '@emotion/react';
import {
from,
palette as sourcePalette,
space,
textSansBold14,
textSansBold15,
textSansBold20,
textSansBold28,
visuallyHidden,
} from '@guardian/source/foundations';
import { getContrast } from '../lib/colour';
import { palette } from '../palette';

const containerCss = css`
Expand Down Expand Up @@ -62,6 +64,24 @@ const largeNumberCss = css`
}
`;

const numberLightContrastCss = css`
@media (prefers-color-scheme: light) {
color: ${palette('--football-match-stat-text')};
}
[data-color-scheme='light'] & {
color: ${palette('--football-match-stat-text')};
}
`;

const numberDarkContrastCss = css`
@media (prefers-color-scheme: dark) {
color: ${palette('--football-match-stat-text')};
}
[data-color-scheme='dark'] & {
color: ${palette('--football-match-stat-text')};
}
`;

const awayStatCss = css`
grid-area: away-stat;
justify-self: end;
Expand All @@ -80,6 +100,24 @@ const barCss = css`
border-radius: 8px;
`;

const barLightContrastCss = css`
@media (prefers-color-scheme: light) {
border: 1px solid ${palette('--football-match-stat-border')};
}
[data-color-scheme='light'] & {
border: 1px solid ${palette('--football-match-stat-border')};
}
`;

const barDarkContrastCss = css`
@media (prefers-color-scheme: dark) {
border: 1px solid ${palette('--football-match-stat-border')};
}
[data-color-scheme='dark'] & {
border: 1px solid ${palette('--football-match-stat-border')};
}
`;

type MatchStatistic = {
teamName: string;
teamColour: string;
Expand Down Expand Up @@ -109,12 +147,41 @@ export const FootballMatchStat = ({
const homePercentage = (home.value / (home.value + away.value)) * 100;
const awayPercentage = (away.value / (home.value + away.value)) * 100;

const minimumContrast = 3.1; // https://www.w3.org/TR/WCAG21/#contrast-minimum

const backgroundLight = sourcePalette.neutral[97];
const backgroundDark = sourcePalette.neutral[10];
Comment on lines +152 to +153
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would get these from the palette declarations, but they're only available as custom properties so we would have to fetch the values on the client, preventing us from doing the contrast calculations during the server side render.


const homeNeedsContrastWhenLight =
getContrast(home.teamColour, backgroundLight) < minimumContrast;
const awayNeedsContrastWhenLight =
getContrast(away.teamColour, backgroundLight) < minimumContrast;
const homeNeedsContrastWhenDark =
getContrast(home.teamColour, backgroundDark) < minimumContrast;
const awayNeedsContrastWhenDark =
getContrast(away.teamColour, backgroundDark) < minimumContrast;

/**
* If either team colour lacks sufficient contrast we adjust both numbers
* so we don't appear to be favouring one team over the other. For the chart
* we keep the team colour and apply a contrasting border colour.
*/
const numbersNeedContrastWhenLight =
homeNeedsContrastWhenLight || awayNeedsContrastWhenLight;
const numbersNeedContrastWhenDark =
homeNeedsContrastWhenDark || awayNeedsContrastWhenDark;

return (
<div css={containerCss}>
<div css={[headerCss, raiseLabelOnDesktop && raiseLabelCss]}>
<span css={labelCss}>{label}</span>
<span
css={[numberCss, largeNumbersOnDesktop && largeNumberCss]}
css={[
numberCss,
largeNumbersOnDesktop && largeNumberCss,
numbersNeedContrastWhenLight && numberLightContrastCss,
numbersNeedContrastWhenDark && numberDarkContrastCss,
]}
style={{ '--match-stat-team-colour': home.teamColour }}
>
<span
Expand All @@ -131,6 +198,8 @@ export const FootballMatchStat = ({
numberCss,
awayStatCss,
largeNumbersOnDesktop && largeNumberCss,
numbersNeedContrastWhenLight && numberLightContrastCss,
numbersNeedContrastWhenDark && numberDarkContrastCss,
]}
style={{ '--match-stat-team-colour': away.teamColour }}
>
Expand All @@ -146,14 +215,22 @@ export const FootballMatchStat = ({
</div>
<div aria-hidden="true" css={chartCss}>
<div
css={barCss}
css={[
barCss,
homeNeedsContrastWhenLight && barLightContrastCss,
homeNeedsContrastWhenDark && barDarkContrastCss,
]}
style={{
'--match-stat-percentage': `${homePercentage}%`,
'--match-stat-team-colour': home.teamColour,
}}
></div>
<div
css={barCss}
css={[
barCss,
awayNeedsContrastWhenLight && barLightContrastCss,
awayNeedsContrastWhenDark && barDarkContrastCss,
]}
style={{
'--match-stat-percentage': `${awayPercentage}%`,
'--match-stat-team-colour': away.teamColour,
Expand Down
2 changes: 1 addition & 1 deletion dotcom-rendering/src/components/GoalAttempts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
palette as sourcePalette,
textSans15,
} from '@guardian/source/foundations';
import { isLight } from '../lib/isLight';
import { isLight } from '../lib/colour';
import { transparentColour } from '../lib/transparentColour';
import { palette as themePalette } from '../palette';
import type { ColourName } from '../paletteDeclarations';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { isLight } from './isLight';
import { getContrast, isLight } from './colour';

const round = (value: number): number => Math.round(value * 100) / 100;

describe('isLight', () => {
it('should return the correct response for dark hex colours', () => {
Expand Down Expand Up @@ -38,3 +40,18 @@ describe('isLight', () => {
expect(isLight('wyx')).toBe(false);
});
});

/**
* Contrast ratio calculated by `getContrast` should match that calculated
* by the WebAIM contrast checker: https://webaim.org/resources/contrastchecker
*/
describe('getContrast', () => {
it('should return the correct contrast ratio for two colours', () => {
expect(round(getContrast('#000', '#fff'))).toEqual(21);
expect(round(getContrast('#f00', '#fff'))).toEqual(4);
expect(round(getContrast('#faa01b', '#f6f6f6'))).toEqual(1.92);
expect(round(getContrast('#2a449a', '#f6f6f6'))).toEqual(8.12);
expect(round(getContrast('#f0c650', '#1a1a1a'))).toEqual(10.69);
expect(round(getContrast('#559861', '#1a1a1a'))).toEqual(5.01);
});
});
Loading
Loading