Skip to content

Commit 3013b01

Browse files
Support strike through in product buttons (#16705)
* feat: support strike through in product buttons * fix accessible product label double spacing
1 parent 9680460 commit 3013b01

10 files changed

Lines changed: 143 additions & 19 deletions

dotcom-rendering/src/components/Button/ProductLinkButton.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ export const WithLongLabel = {
3434
label: '£10.99 for a 5 x 5 x 50cm sheet at Amazon',
3535
},
3636
} satisfies Story;
37+
38+
export const WithStruckThroughLabel = {
39+
args: {
40+
label: '~£10~ £5 at Amazon',
41+
},
42+
} satisfies Story;

dotcom-rendering/src/components/Button/ProductLinkButton.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import type {
77
import { LinkButton } from '@guardian/source/react-components';
88
import { SKIMLINK_REL } from '../../lib/affiliateLinksUtils';
99
import { palette } from '../../palette';
10+
import {
11+
createAccessibleProductLabel,
12+
createStrikeThroughProductLabel,
13+
} from './productUtils';
1014
import { heightAutoStyle, wrapButtonTextStyle } from './styles';
1115
import { getPropsForLinkUrl } from './utils';
1216

@@ -37,6 +41,12 @@ const minimisePaddingStyle = css`
3741
}
3842
`;
3943

44+
const strikeThroughStyle = css`
45+
s {
46+
font-weight: normal;
47+
}
48+
`;
49+
4050
export const theme: Partial<ThemeButton> = {
4151
backgroundPrimary: palette('--product-button-primary-background'),
4252
backgroundPrimaryHover: palette(
@@ -67,7 +77,7 @@ export const ProductLinkButton = ({
6777

6878
return (
6979
<LinkButton
70-
{...getPropsForLinkUrl(label)}
80+
{...getPropsForLinkUrl(createAccessibleProductLabel(label))}
7181
href={url}
7282
rel={SKIMLINK_REL}
7383
priority={priority}
@@ -82,9 +92,9 @@ export const ProductLinkButton = ({
8292
>
8393
<span
8494
style={fullWidthText ? { width: '100%' } : {}}
85-
css={wrapButtonTextStyle}
95+
css={[wrapButtonTextStyle, strikeThroughStyle]}
8696
>
87-
{label}
97+
{createStrikeThroughProductLabel(label)}
8898
</span>
8999
</LinkButton>
90100
);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { render } from '@testing-library/react';
2+
import {
3+
createAccessibleProductLabel,
4+
createStrikeThroughProductLabel,
5+
} from './productUtils';
6+
7+
describe('createStrikeThroughProductLabel', () => {
8+
it('returns the label unchanged when there is no struck-through text', () => {
9+
const { container } = render(
10+
<>{createStrikeThroughProductLabel('£5 at Shop')}</>,
11+
);
12+
13+
expect(container.textContent).toBe('£5 at Shop');
14+
expect(container.querySelector('s')).toBeNull();
15+
});
16+
17+
it('renders the old price in an s element and preserves the rest', () => {
18+
const { container } = render(
19+
<>{createStrikeThroughProductLabel('~£10~ £5 at Shop')}</>,
20+
);
21+
22+
expect(container.querySelector('s')).toHaveTextContent('£10');
23+
expect(container.textContent).toBe('£10 £5 at Shop');
24+
});
25+
});
26+
27+
describe('createAccessibleProductLabel', () => {
28+
it('returns the label unchanged when there is no struck-through text', () => {
29+
expect(createAccessibleProductLabel('£5 at Shop')).toBe('£5 at Shop');
30+
});
31+
32+
it('describes the old and new prices accessibly', () => {
33+
expect(createAccessibleProductLabel('~£10~ £5 at Shop')).toBe(
34+
'Was £10, now £5 at Shop',
35+
);
36+
});
37+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { isUndefined } from '@guardian/libs';
2+
import type { ProductCta } from '../../types/content';
3+
4+
const strikeThroughRegex = /~([^~]+)~(.*)/;
5+
6+
type ParsedProductLabel = {
7+
struckThrough: string;
8+
restOfLabel: string;
9+
};
10+
11+
const parseProductLabel = (label: string): ParsedProductLabel | undefined => {
12+
const match = label.match(strikeThroughRegex);
13+
const struckThrough = match?.[1];
14+
const restOfLabel = match?.[2];
15+
16+
if (isUndefined(struckThrough) || isUndefined(restOfLabel)) {
17+
return undefined;
18+
}
19+
20+
return { struckThrough, restOfLabel };
21+
};
22+
23+
/**
24+
* Create a struck through React component from a label
25+
* for a product link button or product CTA
26+
* that may contain a strikethroughed price
27+
* @param label the label text that may contain strikethrough eg '~£10~ £5 at Shop'
28+
*/
29+
export const createStrikeThroughProductLabel = (label: string) => {
30+
const parsedLabel = parseProductLabel(label);
31+
32+
if (isUndefined(parsedLabel)) {
33+
return label;
34+
} else {
35+
return (
36+
<>
37+
<s>{parsedLabel.struckThrough}</s>
38+
{parsedLabel.restOfLabel}
39+
</>
40+
);
41+
}
42+
};
43+
44+
/**
45+
* Create accessible label text
46+
* for a product link button or product CTA
47+
* that may contain a struck through price
48+
* @param label the label text that may contain strikethrough eg '~£10~ £5 at Shop'
49+
*/
50+
export const createAccessibleProductLabel = (label: string): string => {
51+
const parsedLabel = parseProductLabel(label);
52+
if (isUndefined(parsedLabel)) {
53+
return label;
54+
} else {
55+
return `Was ${parsedLabel.struckThrough}, now ${parsedLabel.restOfLabel.trimStart()}`;
56+
}
57+
};
58+
59+
export const getProductLinkLabelWithoutPrice = (
60+
cardCta: ProductCta,
61+
): string => {
62+
return cardCta.text !== '' ? cardCta.text : `Buy at ${cardCta.retailer}`;
63+
};
64+
65+
export const getProductLinkLabelWithPrice = (cta: ProductCta): string => {
66+
const overrideLabel = cta.text.trim().length > 0;
67+
return overrideLabel ? cta.text : `${cta.price} at ${cta.retailer}`;
68+
};

dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88
textSansBold17,
99
} from '@guardian/source/foundations';
1010
import { Link } from '@guardian/source/react-components';
11-
import { getProductLinkLabelWithoutPrice } from '../lib/affiliateLinksUtils';
1211
import type { ArticleFormat } from '../lib/articleFormat';
1312
import { palette } from '../palette';
1413
import type { SummaryProduct } from '../types/content';
1514
import { ProductLinkButton } from './Button/ProductLinkButton';
15+
import { getProductLinkLabelWithoutPrice } from './Button/productUtils';
1616
import { ProductCardImage } from './ProductCardImage';
1717

1818
const horizontalCard = css`

dotcom-rendering/src/components/ProductCardButtons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ThemeButton } from '@guardian/source/react-components';
2-
import { getProductLinkLabelWithPrice } from '../lib/affiliateLinksUtils';
32
import type { ProductCta } from '../types/content';
43
import { ProductLinkButton } from './Button/ProductLinkButton';
4+
import { getProductLinkLabelWithPrice } from './Button/productUtils';
55

66
export const ProductCardButtons = ({
77
productCtas,

dotcom-rendering/src/components/ProductCardInline.stories.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,18 @@ export const ProductCardOnlyDisplayCredit = meta.story({
7878
image: { ...productImage, displayCredit: true },
7979
},
8080
});
81+
82+
export const WithStrikeThroughPrice = meta.story({
83+
args: {
84+
...meta.input.args,
85+
productCtas: [
86+
{
87+
url: 'https://www.theguardian.com',
88+
retailer: 'Amazon',
89+
text: '',
90+
price: '~£95.99~ £89.99',
91+
},
92+
...meta.input.args.productCtas.slice(1),
93+
],
94+
},
95+
});

dotcom-rendering/src/components/ProductCarouselCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import {
99
textSansBold17,
1010
} from '@guardian/source/foundations';
1111
import { Link } from '@guardian/source/react-components';
12-
import { getProductLinkLabelWithoutPrice } from '../lib/affiliateLinksUtils';
1312
import type { ArticleFormat } from '../lib/articleFormat';
1413
import { palette } from '../palette';
1514
import type { SummaryProduct } from '../types/content';
1615
import { ProductLinkButton } from './Button/ProductLinkButton';
16+
import { getProductLinkLabelWithoutPrice } from './Button/productUtils';
1717
import { ProductCardImage } from './ProductCardImage';
1818

1919
export type ProductCarouselCardProps = {

dotcom-rendering/src/components/ProductCtaList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { css } from '@emotion/react';
22
import { article17, palette, remSpace } from '@guardian/source/foundations';
3-
import { getProductLinkLabelWithPrice } from '../lib/affiliateLinksUtils';
43
import type { ArticleFormat } from '../lib/articleFormat';
54
import type { SummaryProduct } from '../types/content';
65
import { ProductLinkButton } from './Button/ProductLinkButton';
6+
import { getProductLinkLabelWithPrice } from './Button/productUtils';
77
import { Subheading } from './Subheading';
88

99
const listStyles = css`

dotcom-rendering/src/lib/affiliateLinksUtils.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { ABParticipations } from '../experiments/lib/ab-tests';
2-
import type { ProductCta } from '../types/content';
32

43
export const SKIMLINK_REL = 'sponsored noreferrer noopener';
54

@@ -138,14 +137,3 @@ export const buildXcustParamForAffiliateLink = ({
138137
xcustComponentId,
139138
});
140139
};
141-
142-
export const getProductLinkLabelWithoutPrice = (
143-
cardCta: ProductCta,
144-
): string => {
145-
return cardCta.text !== '' ? cardCta.text : `Buy at ${cardCta.retailer}`;
146-
};
147-
148-
export const getProductLinkLabelWithPrice = (cta: ProductCta): string => {
149-
const overrideLabel = cta.text.trim().length > 0;
150-
return overrideLabel ? cta.text : `${cta.price} at ${cta.retailer}`;
151-
};

0 commit comments

Comments
 (0)