diff --git a/dotcom-rendering/src/components/Button/ProductLinkButton.stories.tsx b/dotcom-rendering/src/components/Button/ProductLinkButton.stories.tsx index 8c8e7a9d1ec..b3e77745ac4 100644 --- a/dotcom-rendering/src/components/Button/ProductLinkButton.stories.tsx +++ b/dotcom-rendering/src/components/Button/ProductLinkButton.stories.tsx @@ -34,3 +34,9 @@ export const WithLongLabel = { label: '£10.99 for a 5 x 5 x 50cm sheet at Amazon', }, } satisfies Story; + +export const WithStruckThroughLabel = { + args: { + label: '~£10~ £5 at Amazon', + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/Button/ProductLinkButton.tsx b/dotcom-rendering/src/components/Button/ProductLinkButton.tsx index d157fcd1d20..5e1ede3659c 100644 --- a/dotcom-rendering/src/components/Button/ProductLinkButton.tsx +++ b/dotcom-rendering/src/components/Button/ProductLinkButton.tsx @@ -7,6 +7,10 @@ import type { import { LinkButton } from '@guardian/source/react-components'; import { SKIMLINK_REL } from '../../lib/affiliateLinksUtils'; import { palette } from '../../palette'; +import { + createAccessibleProductLabel, + createStrikeThroughProductLabel, +} from './productUtils'; import { heightAutoStyle, wrapButtonTextStyle } from './styles'; import { getPropsForLinkUrl } from './utils'; @@ -37,6 +41,12 @@ const minimisePaddingStyle = css` } `; +const strikeThroughStyle = css` + s { + font-weight: normal; + } +`; + export const theme: Partial = { backgroundPrimary: palette('--product-button-primary-background'), backgroundPrimaryHover: palette( @@ -67,7 +77,7 @@ export const ProductLinkButton = ({ return ( - {label} + {createStrikeThroughProductLabel(label)} ); diff --git a/dotcom-rendering/src/components/Button/productUtils.test.tsx b/dotcom-rendering/src/components/Button/productUtils.test.tsx new file mode 100644 index 00000000000..cf2efa1531c --- /dev/null +++ b/dotcom-rendering/src/components/Button/productUtils.test.tsx @@ -0,0 +1,37 @@ +import { render } from '@testing-library/react'; +import { + createAccessibleProductLabel, + createStrikeThroughProductLabel, +} from './productUtils'; + +describe('createStrikeThroughProductLabel', () => { + it('returns the label unchanged when there is no struck-through text', () => { + const { container } = render( + <>{createStrikeThroughProductLabel('£5 at Shop')}, + ); + + expect(container.textContent).toBe('£5 at Shop'); + expect(container.querySelector('s')).toBeNull(); + }); + + it('renders the old price in an s element and preserves the rest', () => { + const { container } = render( + <>{createStrikeThroughProductLabel('~£10~ £5 at Shop')}, + ); + + expect(container.querySelector('s')).toHaveTextContent('£10'); + expect(container.textContent).toBe('£10 £5 at Shop'); + }); +}); + +describe('createAccessibleProductLabel', () => { + it('returns the label unchanged when there is no struck-through text', () => { + expect(createAccessibleProductLabel('£5 at Shop')).toBe('£5 at Shop'); + }); + + it('describes the old and new prices accessibly', () => { + expect(createAccessibleProductLabel('~£10~ £5 at Shop')).toBe( + 'Was £10, now £5 at Shop', + ); + }); +}); diff --git a/dotcom-rendering/src/components/Button/productUtils.tsx b/dotcom-rendering/src/components/Button/productUtils.tsx new file mode 100644 index 00000000000..90d885e0bfd --- /dev/null +++ b/dotcom-rendering/src/components/Button/productUtils.tsx @@ -0,0 +1,68 @@ +import { isUndefined } from '@guardian/libs'; +import type { ProductCta } from '../../types/content'; + +const strikeThroughRegex = /~([^~]+)~(.*)/; + +type ParsedProductLabel = { + struckThrough: string; + restOfLabel: string; +}; + +const parseProductLabel = (label: string): ParsedProductLabel | undefined => { + const match = label.match(strikeThroughRegex); + const struckThrough = match?.[1]; + const restOfLabel = match?.[2]; + + if (isUndefined(struckThrough) || isUndefined(restOfLabel)) { + return undefined; + } + + return { struckThrough, restOfLabel }; +}; + +/** + * Create a struck through React component from a label + * for a product link button or product CTA + * that may contain a strikethroughed price + * @param label the label text that may contain strikethrough eg '~£10~ £5 at Shop' + */ +export const createStrikeThroughProductLabel = (label: string) => { + const parsedLabel = parseProductLabel(label); + + if (isUndefined(parsedLabel)) { + return label; + } else { + return ( + <> + {parsedLabel.struckThrough} + {parsedLabel.restOfLabel} + + ); + } +}; + +/** + * Create accessible label text + * for a product link button or product CTA + * that may contain a struck through price + * @param label the label text that may contain strikethrough eg '~£10~ £5 at Shop' + */ +export const createAccessibleProductLabel = (label: string): string => { + const parsedLabel = parseProductLabel(label); + if (isUndefined(parsedLabel)) { + return label; + } else { + return `Was ${parsedLabel.struckThrough}, now ${parsedLabel.restOfLabel.trimStart()}`; + } +}; + +export const getProductLinkLabelWithoutPrice = ( + cardCta: ProductCta, +): string => { + return cardCta.text !== '' ? cardCta.text : `Buy at ${cardCta.retailer}`; +}; + +export const getProductLinkLabelWithPrice = (cta: ProductCta): string => { + const overrideLabel = cta.text.trim().length > 0; + return overrideLabel ? cta.text : `${cta.price} at ${cta.retailer}`; +}; diff --git a/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx b/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx index d7ccadee965..645d7dc7f7c 100644 --- a/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx +++ b/dotcom-rendering/src/components/HorizontalSummaryProductCard.tsx @@ -8,11 +8,11 @@ import { textSansBold17, } from '@guardian/source/foundations'; import { Link } from '@guardian/source/react-components'; -import { getProductLinkLabelWithoutPrice } from '../lib/affiliateLinksUtils'; import type { ArticleFormat } from '../lib/articleFormat'; import { palette } from '../palette'; import type { SummaryProduct } from '../types/content'; import { ProductLinkButton } from './Button/ProductLinkButton'; +import { getProductLinkLabelWithoutPrice } from './Button/productUtils'; import { ProductCardImage } from './ProductCardImage'; const horizontalCard = css` diff --git a/dotcom-rendering/src/components/ProductCardButtons.tsx b/dotcom-rendering/src/components/ProductCardButtons.tsx index 94ab20681f8..d3100c31166 100644 --- a/dotcom-rendering/src/components/ProductCardButtons.tsx +++ b/dotcom-rendering/src/components/ProductCardButtons.tsx @@ -1,7 +1,7 @@ import type { ThemeButton } from '@guardian/source/react-components'; -import { getProductLinkLabelWithPrice } from '../lib/affiliateLinksUtils'; import type { ProductCta } from '../types/content'; import { ProductLinkButton } from './Button/ProductLinkButton'; +import { getProductLinkLabelWithPrice } from './Button/productUtils'; export const ProductCardButtons = ({ productCtas, diff --git a/dotcom-rendering/src/components/ProductCardInline.stories.tsx b/dotcom-rendering/src/components/ProductCardInline.stories.tsx index 4428c214c0f..ceb6bfaf6ae 100644 --- a/dotcom-rendering/src/components/ProductCardInline.stories.tsx +++ b/dotcom-rendering/src/components/ProductCardInline.stories.tsx @@ -78,3 +78,18 @@ export const ProductCardOnlyDisplayCredit = meta.story({ image: { ...productImage, displayCredit: true }, }, }); + +export const WithStrikeThroughPrice = meta.story({ + args: { + ...meta.input.args, + productCtas: [ + { + url: 'https://www.theguardian.com', + retailer: 'Amazon', + text: '', + price: '~£95.99~ £89.99', + }, + ...meta.input.args.productCtas.slice(1), + ], + }, +}); diff --git a/dotcom-rendering/src/components/ProductCarouselCard.tsx b/dotcom-rendering/src/components/ProductCarouselCard.tsx index e9d7ffa684a..651c9c24188 100644 --- a/dotcom-rendering/src/components/ProductCarouselCard.tsx +++ b/dotcom-rendering/src/components/ProductCarouselCard.tsx @@ -9,11 +9,11 @@ import { textSansBold17, } from '@guardian/source/foundations'; import { Link } from '@guardian/source/react-components'; -import { getProductLinkLabelWithoutPrice } from '../lib/affiliateLinksUtils'; import type { ArticleFormat } from '../lib/articleFormat'; import { palette } from '../palette'; import type { SummaryProduct } from '../types/content'; import { ProductLinkButton } from './Button/ProductLinkButton'; +import { getProductLinkLabelWithoutPrice } from './Button/productUtils'; import { ProductCardImage } from './ProductCardImage'; export type ProductCarouselCardProps = { diff --git a/dotcom-rendering/src/components/ProductCtaList.tsx b/dotcom-rendering/src/components/ProductCtaList.tsx index 6131102e96a..7887a198d76 100644 --- a/dotcom-rendering/src/components/ProductCtaList.tsx +++ b/dotcom-rendering/src/components/ProductCtaList.tsx @@ -1,9 +1,9 @@ import { css } from '@emotion/react'; import { article17, palette, remSpace } from '@guardian/source/foundations'; -import { getProductLinkLabelWithPrice } from '../lib/affiliateLinksUtils'; import type { ArticleFormat } from '../lib/articleFormat'; import type { SummaryProduct } from '../types/content'; import { ProductLinkButton } from './Button/ProductLinkButton'; +import { getProductLinkLabelWithPrice } from './Button/productUtils'; import { Subheading } from './Subheading'; const listStyles = css` diff --git a/dotcom-rendering/src/lib/affiliateLinksUtils.ts b/dotcom-rendering/src/lib/affiliateLinksUtils.ts index 5ed99907b5a..bbe31e11f46 100644 --- a/dotcom-rendering/src/lib/affiliateLinksUtils.ts +++ b/dotcom-rendering/src/lib/affiliateLinksUtils.ts @@ -1,5 +1,4 @@ import type { ABParticipations } from '../experiments/lib/ab-tests'; -import type { ProductCta } from '../types/content'; export const SKIMLINK_REL = 'sponsored noreferrer noopener'; @@ -138,14 +137,3 @@ export const buildXcustParamForAffiliateLink = ({ xcustComponentId, }); }; - -export const getProductLinkLabelWithoutPrice = ( - cardCta: ProductCta, -): string => { - return cardCta.text !== '' ? cardCta.text : `Buy at ${cardCta.retailer}`; -}; - -export const getProductLinkLabelWithPrice = (cta: ProductCta): string => { - const overrideLabel = cta.text.trim().length > 0; - return overrideLabel ? cta.text : `${cta.price} at ${cta.retailer}`; -};