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
22 changes: 11 additions & 11 deletions src/app/components/Curation/HighImpactPromo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ Typically used when editors set a curation item's prominence to "Maximum" in Tip
- Renders promotional content with enhanced visual styling.
- Integrates with the existing Standard Grid flow.
- Provides clear visual distinction from standard curation items.
- Attribution is automatically derived from the service context (e.g., brand name and service URL) but can be overridden with the `attribution` prop.
- Subject link is derived from `relatedTopic` when present, otherwise it falls back to service context (brand name and service URL).

---

## Props

| Prop | Type | Required | Description |
| ------------------- | ------- | -------- | --------------------------------------------------------------------- |
| `title` | string | Yes | The promotional headline. |
| `link` | string | Yes | URL destination for the promo. |
| `imageUrl` | string | Yes | Image URL for the promotional content. |
| `imageAlt` | string | Yes | Alt text for the promotional image. |
| `lazy` | boolean | No | Enables lazy loading for the image. |
| `headingLevel` | number | No | The heading level for the title (defaults to 3). |
| `eventTrackingData` | object | No | Tracking metadata for analytics. |
| `attribution` | object | No | An object with `text` and `link` to override the default attribution. |
| Prop | Type | Required | Description |
| ------------------- | ------- | -------- | ---------------------------------------------------------------- |
| `title` | string | Yes | The promotional headline. |
| `link` | string | Yes | URL destination for the promo. |
| `imageUrl` | string | Yes | Image URL for the promotional content. |
| `imageAlt` | string | Yes | Alt text for the promotional image. |
| `lazy` | boolean | No | Enables lazy loading for the image. |
| `headingLevel` | number | No | The heading level for the title (defaults to 3). |
| `eventTrackingData` | object | No | Tracking metadata for analytics. |
| `relatedTopic` | object | No | An object with `title` and `link.url` used for the subject link. |

---

Expand Down
75 changes: 33 additions & 42 deletions src/app/components/Curation/HighImpactPromo/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,47 @@ import metadata from './metadata.json';
import readme from './README.md';

const highImpactFixtureCuration = fixture.data.curations[0] as BaseCuration;
const baseProps = highImpactFixtureCuration.summaries?.[0] as Summary;

const Component = () => {
return (
<div
style={{
display: 'flex',
gap: '2rem',
flexDirection: 'column',
maxWidth: '480px',
}}
>
<HighImpactPromo
{...(highImpactFixtureCuration.summaries?.[0] as Summary)}
attributions={[
{
title: 'BBC News Pidgin',
link: { url: '/pidgin' },
},
]}
/>
<HighImpactPromo
{...(highImpactFixtureCuration.summaries?.[1] as Summary)}
attributions={[
{
title: 'BBC News Mundo',
link: { url: '/mundo' },
},
]}
/>
<HighImpactPromo
{...(highImpactFixtureCuration.summaries?.[2] as Summary)}
attributions={[
{
title: 'BBC',
link: { url: '/' },
},
]}
/>
</div>
);
};
interface ExternalProps {
relatedTopic?: { title: string; link: { url: string } } | null;
}

const Component = ({ relatedTopic }: ExternalProps) => (
<HighImpactPromo {...baseProps} relatedTopic={relatedTopic} />
);

export default {
title: 'Components/Curation/High Impact Promo',
component: Component,
Component,
decorators: [
Story => (
<div
style={{
display: 'flex',
gap: '2rem',
flexDirection: 'column',
maxWidth: '480px',
}}
>
<Story />
</div>
),
],
parameters: {
metadata,
docs: { readme },
chromatic: { disable: true },
},
args: {
relatedTopic: {
title: 'Related Topic Example',
link: { url: '/topic/example' },
},
},
argTypes: {
relatedTopic: { control: 'object' },
},
};

export const Example = {};
export const Example = Component;
66 changes: 25 additions & 41 deletions src/app/components/Curation/HighImpactPromo/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ const promoFixtureData = summaries?.[0] as HighImpactPromoProps;
interface FixtureProps {
promoData?: HighImpactPromoProps;
headingLevel?: number;
attributions?: { title: string; link: { url: string } }[] | null;
relatedTopic?: { title: string; link: { url: string } } | null | undefined;
}

const Fixture = ({
promoData = promoFixtureData,
headingLevel,
attributions,
relatedTopic,
}: FixtureProps) => (
<HighImpactPromo
{...promoData}
headingLevel={headingLevel}
attributions={attributions}
{...(relatedTopic !== undefined && { relatedTopic })}
/>
);

Expand Down Expand Up @@ -64,16 +64,16 @@ describe('High Impact Promo', () => {
);
});

it('should render default values if attribution prop is not provided', () => {
it('should render default subject values when relatedTopic prop is not provided', () => {
render(<Fixture />, { service: 'mundo' });

const attributionLink = screen.getByRole('link', {
const subjectLink = screen.getByRole('link', {
name: 'BBC News Mundo',
});
expect(attributionLink).toBeInTheDocument();
expect(attributionLink).toHaveAttribute('href', '/mundo');
expect(subjectLink).toBeInTheDocument();
expect(subjectLink).toHaveAttribute('href', '/mundo');

const divider = attributionLink.previousElementSibling;
const divider = subjectLink.previousElementSibling;
expect(divider).toBeInTheDocument();
expect(divider).toHaveStyle({
'background-color': '#EB0000',
Expand All @@ -82,39 +82,6 @@ describe('High Impact Promo', () => {
});
});

it('should render correct attribution when an attributions prop is provided', () => {
const customAttributions = [
{
title: 'Pidgin Related Topic',
link: { url: '/pidgin/topics/234567' },
},
];
render(<Fixture attributions={customAttributions} />);

const attributionLink = screen.getByRole('link', {
name: 'Pidgin Related Topic',
});
expect(attributionLink).toBeInTheDocument();
expect(attributionLink).toHaveAttribute('href', '/pidgin/topics/234567');
});
it('should render default attribution when attributions prop is null', () => {
render(<Fixture attributions={null} />, { service: 'mundo' });
const attributionLink = screen.getByRole('link', {
name: 'BBC News Mundo',
});
expect(attributionLink).toBeInTheDocument();
expect(attributionLink).toHaveAttribute('href', '/mundo');
});

it('should render default attribution when attributions prop is an empty array', () => {
render(<Fixture attributions={[]} />, { service: 'mundo' });
const attributionLink = screen.getByRole('link', {
name: 'BBC News Mundo',
});
expect(attributionLink).toBeInTheDocument();
expect(attributionLink).toHaveAttribute('href', '/mundo');
});

it.each<[Services, string]>([
['mundo', 'ltr'],
['arabic', 'rtl'],
Expand All @@ -123,4 +90,21 @@ describe('High Impact Promo', () => {
const promo = screen.getByTestId('high-impact-promo');
expect(promo).toHaveAttribute('dir', dir);
});

it('should render relatedTopic when provided', () => {
const relatedTopic = {
title: 'Россия',
link: { url: 'https://www.bbc.com/russian/topics/cw6eyw7m0m1t' },
};
render(<Fixture relatedTopic={relatedTopic} />);

const relatedTopicLink = screen.getByRole('link', {
name: 'Россия',
});
expect(relatedTopicLink).toBeInTheDocument();
expect(relatedTopicLink).toHaveAttribute(
'href',
'https://www.bbc.com/russian/topics/cw6eyw7m0m1t',
);
});
});
27 changes: 13 additions & 14 deletions src/app/components/Curation/HighImpactPromo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ import { ServiceContext } from '#app/contexts/ServiceContext';
import { getBrandPath } from '#app/legacy/containers/Brand';
import styles from './index.styles';

type AttributionLink = {
type RelatedTopicLink = {
url: string;
scheme?: string;
host?: string;
path?: string;
};

type Attribution = {
type RelatedTopic = {
link: RelatedTopicLink;
title: string;
link: AttributionLink;
};
export interface HighImpactPromoProps extends Summary {
attributions?: Attribution[] | null;
relatedTopic?: RelatedTopic | null;
}

const HighImpactPromo = ({
Expand All @@ -30,16 +30,15 @@ const HighImpactPromo = ({
link,
headingLevel = 3,
eventTrackingData,
attributions,
relatedTopic,
}: HighImpactPromoProps) => {
const { isAmp } = use(RequestContext);
const { dir, service, brandName } = use(ServiceContext) || {};

const [firstAttribution] = attributions || [];
const attributionLink =
firstAttribution?.link?.url || (service ? getBrandPath(service) : null);
const attributionText = firstAttribution?.title || brandName;
const hasAttribution = Boolean(attributionLink && attributionText);
const subjectLink =
relatedTopic?.link?.url || (service ? getBrandPath(service) : undefined);
const subjectText = relatedTopic?.title || brandName;
const hasSubject = Boolean(subjectLink && subjectText);

const clickTrackerHandler = useClickTrackerHandler(eventTrackingData);

Expand All @@ -65,14 +64,14 @@ const HighImpactPromo = ({
{title}
</Promo.A>
</Promo.Heading>
{hasAttribution && <div css={styles.divider} />}
{hasAttribution && attributionLink && (
{hasSubject && <div css={styles.divider} />}
{hasSubject && (
<Promo.A
href={attributionLink}
href={subjectLink}
css={styles.subject}
{...clickTrackerHandler}
>
{attributionText}
{subjectText}
</Promo.A>
)}
</div>
Expand Down
Loading