Skip to content
Merged
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
42 changes: 42 additions & 0 deletions dotcom-rendering/src/components/Button/LinkElementButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ProductLinkElementButton } from './ProductLinkElementButton';
import { StandardLinkElementButton } from './StandardLinkElementButton';

type LinkElementButtonProps = {
label: string;
url: string;
linkType: 'StandardButton' | 'ProductButton';
priority?: 'Primary' | 'Tertiary';
};

const LinkTypePriorityToButtonPriority = {
Primary: 'primary',
Tertiary: 'tertiary',
} as const;

export const LinkElementButton = ({
label,
url,
priority = 'Primary',
linkType,
}: LinkElementButtonProps) => {
switch (linkType) {
case 'StandardButton': {
return (
<StandardLinkElementButton
label={label}
url={url}
priority={LinkTypePriorityToButtonPriority[priority]}
/>
);
}
case 'ProductButton': {
return (
<ProductLinkElementButton
label={label}
url={url}
priority={LinkTypePriorityToButtonPriority[priority]}
/>
);
}
}
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { breakpoints } from '@guardian/source/foundations';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import { ProductLinkButton } from './ProductLinkButton';
import { ProductLinkElementButton } from './ProductLinkElementButton';

const meta = {
component: ProductLinkButton,
component: ProductLinkElementButton,
title: 'Components/ProductLinkButton',
parameters: {
layout: 'padded',
Expand All @@ -19,8 +19,9 @@ const meta = {
args: {
label: '£6.50 for 350g at Ollie’s Kimchi',
url: 'https://ollieskimchi.co.uk/shop/ols/products/ollies-kimchi',
priority: 'primary',
},
} satisfies Meta<typeof ProductLinkButton>;
} satisfies Meta<typeof ProductLinkElementButton>;

export default meta;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import type {
ButtonPriority,
ThemeButton,
} from '@guardian/source/react-components';
import {
LinkButton,
SvgArrowRightStraight,
} from '@guardian/source/react-components';
import { palette } from '../palette';
import { LinkButton } from '@guardian/source/react-components';
import { palette } from '../../palette';
import { getPropsForLinkUrl } from './utils';

type ProductLinkButtonProps = {
label: string;
Expand All @@ -17,7 +15,6 @@ type ProductLinkButtonProps = {
fullwidth?: boolean;
fullWidthText?: boolean;
priority?: ButtonPriority;
dataComponent?: string;
minimisePadding?: boolean;
};

Expand Down Expand Up @@ -49,15 +46,14 @@ export const theme: Partial<ThemeButton> = {
borderTertiary: palette('--product-button-primary-background'),
};

export const ProductLinkButton = ({
export const ProductLinkElementButton = ({
label,
url,
size = 'default',
fullwidth = false,
minimisePadding = false,
fullWidthText = false,
priority = 'primary',
dataComponent,
}: ProductLinkButtonProps) => {
const cssOverrides: SerializedStyles[] = [
heightAutoStyle,
Expand All @@ -67,16 +63,13 @@ export const ProductLinkButton = ({

return (
<LinkButton
{...getPropsForLinkUrl(label)}
href={url}
rel="sponsored noreferrer noopener"
target="_blank"
iconSide="right"
priority={priority}
aria-label={`Open ${label} in a new tab`}
icon={<SvgArrowRightStraight />}
theme={theme}
data-ignore="global-link-styling"
data-component={dataComponent}
data-link-name={`product link button ${priority}`}
data-spacefinder-role="inline"
size={size}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Meta, StoryFn, StoryObj } from '@storybook/react-webpack5';
import { lightDecorator } from '../../../.storybook/decorators/themeDecorator';
import {
ArticleDesign,
ArticleDisplay,
ArticleSpecial,
Pillar,
} from '../../lib/articleFormat';
import type { StandardLinkElementButtonProps } from './StandardLinkElementButton';
import { StandardLinkElementButton } from './StandardLinkElementButton';

const meta: Meta<typeof StandardLinkElementButton> = {
title: 'Components/StandardLinkElementButton',
component: StandardLinkElementButton,
} satisfies Meta<typeof StandardLinkElementButton>;

export default meta;

type Story = StoryObj<typeof meta>;

const pillars = [
Pillar.News,
Pillar.Sport,
Pillar.Culture,
Pillar.Lifestyle,
Pillar.Opinion,
ArticleSpecial.SpecialReport,
ArticleSpecial.Labs,
];

const allThemeStandardVariations = pillars.map((theme) => ({
design: ArticleDesign.Standard,
display: ArticleDisplay.Standard,
theme,
}));

const urls = ['https://www.theguardian.com/uk', 'https://www.bbc.co.uk'];

const Template: StoryFn<typeof StandardLinkElementButton> = (
args: Omit<StandardLinkElementButtonProps, 'url' | 'priority'>,
) => {
return (
<>
{urls.map((url) => (
<StandardLinkElementButton
key={url}
{...args}
label="Click me"
url={url}
/>
))}
</>
);
};

export const WhenPrimary = {
args: {
priority: 'primary',
},
render: Template,
decorators: [lightDecorator(allThemeStandardVariations)],
} satisfies Story;
// *****************************************************************************

export const WhenSecondary = {
args: {
priority: 'secondary',
},
render: Template,
decorators: [lightDecorator(allThemeStandardVariations)],
} satisfies Story;
// *****************************************************************************

export const WhenTertiary = {
args: {
priority: 'tertiary',
},
render: Template,
decorators: [lightDecorator(allThemeStandardVariations)],
} satisfies Story;
// *****************************************************************************

export const WhenSubdued = {
args: {
priority: 'subdued',
},
render: Template,
decorators: [lightDecorator(allThemeStandardVariations)],
} satisfies Story;
// *****************************************************************************
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ButtonPriority } from '@guardian/source/react-components';
import { EditorialLinkButton } from './EditorialLinkButton';
import { getPropsForLinkUrl, isExternalLink } from './utils';

export type StandardLinkElementButtonProps = {
label: string;
url: string;
priority?: ButtonPriority;
};

export const StandardLinkElementButton = ({
label,
url,
priority,
}: StandardLinkElementButtonProps) => {
const propsForLinkUrl = isExternalLink(url)
? getPropsForLinkUrl(label)
: {};

return (
<EditorialLinkButton
iconSide="right"
Copy link
Contributor

@SiAdcock SiAdcock Jan 21, 2026

Choose a reason for hiding this comment

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

Maybe iconSide="right" can go in getPropsForLinkUrl()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in #15186.

priority={priority}
data-link-name={`standard link button ${priority}`}
data-spacefinder-role="inline"
data-ignore="global-link-styling"
{...propsForLinkUrl}
>
{label}
</EditorialLinkButton>
);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import type { SerializedStyles } from '@emotion/react';
import { css } from '@emotion/react';
import { neutral } from '@guardian/source/foundations';
import type { ButtonPriority } from '@guardian/source/react-components';
import { palette } from '../../palette';

const WHITE = neutral[100];

export const decideBackground = (
priority: ButtonPriority,
): SerializedStyles => {
Expand Down Expand Up @@ -36,7 +33,7 @@ export const decideBorder = (priority: ButtonPriority): SerializedStyles => {
case 'secondary':
case 'tertiary':
return css`
border: 1px solid currentColor;
border: 1px solid ${palette('--editorial-button-background')};
`;
case 'subdued':
return css`
Expand All @@ -50,12 +47,12 @@ export const decideFont = (priority: ButtonPriority): SerializedStyles => {
case 'primary':
case 'secondary':
return css`
color: ${WHITE};
color: ${palette('--editorial-button-text')};
`;
case 'subdued':
case 'tertiary':
return css`
color: ${palette('--editorial-button-text')};
color: ${palette('--editorial-button-background')};
`;
}
};
19 changes: 19 additions & 0 deletions dotcom-rendering/src/components/Button/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SvgArrowRightStraight } from '@guardian/source/react-components';

const platformHostnames = [
// CODE
'https://code.dev-theguardian.com/',
'https://m.code.dev-theguardian.com/',
// PROD
'www.theguardian.com',
];

export const isExternalLink = (url: string) =>
platformHostnames.includes(new URL(url).hostname);

export const getPropsForLinkUrl = (label: string) => ({
rel: 'noreferrer noopener',
target: '_blank',
'aria-label': `${label} (opens in a new tab)`,
icon: <SvgArrowRightStraight />,
Copy link
Contributor

Choose a reason for hiding this comment

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

While a design question, I'm not sure if the right arrow gives enough affordance that a link is going to open in a new window. The usual icon for this behaviour is a box with a diagonal arrow protruding from it (<SvgExternal> in the Source component library). Perhaps we could raise this with the Design System Team

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will have implications for the product button, too (storybook) — happy to raise, let's chat out of band about the best forum 👍

});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
import type { ArticleFormat } from '../lib/articleFormat';
import { palette } from '../palette';
import type { ProductBlockElement } from '../types/content';
import { ProductLinkElementButton } from './Button/ProductLinkElementButton';
import { ProductCardImage } from './ProductCardImage';
import { ProductLinkButton } from './ProductLinkButton';

const horizontalCard = css`
position: relative;
Expand Down Expand Up @@ -107,7 +107,7 @@ export const HorizontalSummaryProductCard = ({
<div css={price}>{cardCta.price}</div>
</div>
<div css={buttonContainer}>
<ProductLinkButton
<ProductLinkElementButton
size="small"
fullwidth={true}
minimisePadding={true}
Expand Down
Empty file.
4 changes: 2 additions & 2 deletions dotcom-rendering/src/components/ProductCardButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ProductCta } from '../types/content';
import { ProductLinkButton } from './ProductLinkButton';
import { ProductLinkElementButton } from './Button/ProductLinkElementButton';

const getLabel = (cta: ProductCta): string => {
const overrideLabel = cta.text.trim().length > 0;
Expand All @@ -15,7 +15,7 @@ export const ProductCardButtons = ({
{productCtas.map((productCta, index) => {
const label = getLabel(productCta);
return (
<ProductLinkButton
<ProductLinkElementButton
key={label}
label={label}
url={productCta.url}
Expand Down
5 changes: 2 additions & 3 deletions dotcom-rendering/src/components/ProductCarouselCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { Link } from '@guardian/source/react-components';
import type { ArticleFormat } from '../lib/articleFormat';
import { palette } from '../palette';
import type { ProductBlockElement } from '../types/content';
import { ProductLinkElementButton } from './Button/ProductLinkElementButton';
import { ProductCardImage } from './ProductCardImage';
import { ProductLinkButton } from './ProductLinkButton';

export type ProductCarouselCardProps = {
product: ProductBlockElement;
Expand Down Expand Up @@ -122,12 +122,11 @@ export const ProductCarouselCard = ({
<div css={productNameFont}>{product.productName}</div>
</div>
)}

{firstCta && (
<>
<div css={priceStyle}>{firstCta.price}</div>
<div css={buttonWrapper}>
<ProductLinkButton
<ProductLinkElementButton
label={`Buy at ${firstCta.retailer}`}
url={firstCta.url}
fullwidth={true}
Expand Down
2 changes: 2 additions & 0 deletions dotcom-rendering/src/components/ProductElement.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,14 @@ const product = {
url: 'https://www.johnlewis.com/bosch-twk7203gb-sky-variable-temperature-kettle-1-7l-black/p3228625',
label: '£79.99 at John Lewis',
linkType: 'ProductButton',
priority: 'Primary',
},
{
_type: 'model.dotcomrendering.pageElements.LinkBlockElement',
url: 'https://www.amazon.co.uk/Bosch-TWK7203GB-Sky-Variable-Temperature/dp/B07Z8VQ2V6',
label: '£79.99 at Amazon',
linkType: 'ProductButton',
priority: 'Primary',
},
{
_type: 'model.dotcomrendering.pageElements.TextBlockElement',
Expand Down
Empty file.
2 changes: 1 addition & 1 deletion dotcom-rendering/src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css } from '@emotion/react';
import { space } from '@guardian/source/foundations';
import { Hide } from '@guardian/source/react-components';
import { EditorialButton } from './EditorialButton/EditorialButton';
import { EditorialButton } from './Button/EditorialButton';

export type ToastProps = {
count: number;
Expand Down
Loading
Loading