Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
b0d2b41
[copilot] Created basic TopicDiscovery component with scrollable tabs…
Nabeel1276 Apr 17, 2026
d7dc56d
Added disabled attributes for buttonn and focus styling
Nabeel1276 Apr 17, 2026
9bdf7ea
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 Apr 17, 2026
038e00c
Updated ScrollableTabs tab padding and colours
Nabeel1276 Apr 17, 2026
a61d00a
Added hover styling
Nabeel1276 Apr 20, 2026
26e64ed
[copilot] Added gradient fade for left and right buttons
Nabeel1276 Apr 20, 2026
f054e9a
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 Apr 20, 2026
a0d2f4a
Wired component to article page
Nabeel1276 Apr 20, 2026
2d67986
Wire TopicDiscovery component into ArticlePage with fixture data [cop…
Nabeel1276 Apr 20, 2026
ac0bfb6
Added view and click event tracking data [copilot]
Nabeel1276 Apr 20, 2026
e1b1a65
Removed ResizeObserver and replaced with eventlisteners
Nabeel1276 Apr 21, 2026
d0f3f31
[copilot] Added unit tests for topicDiscovery component
Nabeel1276 Apr 21, 2026
8a9d605
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 Apr 21, 2026
f113ba2
Added unit tests for scrollableTabs [copilot]
Nabeel1276 Apr 22, 2026
fee5063
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 Apr 22, 2026
ab81a57
Made responsive to allow 2 promos before overflowing onto next line […
Nabeel1276 Apr 22, 2026
e7b34f3
Removed topic tags component as replaced by topic discovery
Nabeel1276 Apr 22, 2026
c3bdeae
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 Apr 23, 2026
de52237
Added condition for topicTags to render test
Nabeel1276 Apr 23, 2026
d0623e0
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 Apr 23, 2026
aa4a4af
Added aria-labelledby in an attempt to fix cypress test
Nabeel1276 Apr 23, 2026
2f9ca03
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 Apr 24, 2026
c383979
Added documentation in readme
Nabeel1276 Apr 24, 2026
e8aeb96
Tidy up topicDiscovery and relatedTopics display logic
amoore108 Apr 27, 2026
2685537
Update ArticlePage.tsx
amoore108 Apr 27, 2026
b89e805
Prevent RelatedTopics showing if `topicDiscovery` data is found
amoore108 Apr 27, 2026
5cf98a3
Use `pixelToRem` util for pixel sizes
amoore108 Apr 27, 2026
be42f6b
Use `palette` colour
amoore108 Apr 27, 2026
233fef3
Fix border bottom on hover
amoore108 Apr 27, 2026
eb66fc8
Remove fixture from Article gSSP
amoore108 Apr 27, 2026
f70e3be
Add Storybook story for a11y testing
amoore108 Apr 27, 2026
364e96e
Use `palette` for gradient colouring
amoore108 Apr 27, 2026
737aa63
Mobile spacing fixes
amoore108 Apr 27, 2026
37c3c3c
Remove unneeded mapping
amoore108 Apr 27, 2026
8d94629
Show/hide arrows based on container width
amoore108 Apr 27, 2026
72b7d2c
Add `More from...` link
amoore108 Apr 27, 2026
b258381
Update tab font weight and colour
amoore108 Apr 27, 2026
2546115
Use `::after` for setting active/hover underline
amoore108 Apr 27, 2026
38ad740
Tab index and focus fixes
amoore108 Apr 27, 2026
c5c9f82
Update index.styles.ts
amoore108 Apr 27, 2026
59902a5
Remove `tabIndex` test
amoore108 Apr 27, 2026
e179633
Re-add `role=tablist`
amoore108 Apr 27, 2026
2bf6d1d
Remove keyboard nav tests
amoore108 Apr 27, 2026
52e4c43
Add arrow button click to scroll test
amoore108 Apr 27, 2026
72025cc
Improve sizing of media icon
amoore108 Apr 27, 2026
80e2236
Implement copilot suggestion for `aria-controls`
amoore108 Apr 27, 2026
44e303c
Move click tracker handler into ScrollableTabs component
amoore108 May 1, 2026
101cfda
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
amoore108 May 1, 2026
d7adc3e
update topictags integration test and snapshots
LilyL0u May 1, 2026
d2b3793
update unit test now that click tracker is inside scrollabel tabs com…
LilyL0u May 1, 2026
544ed41
update click tracker unit test to expect an id as part of the test id…
LilyL0u May 1, 2026
700f779
isLive check so doesnt go live. plus tests for that
LilyL0u May 1, 2026
c6d7aa0
Update fixtures with new data format
amoore108 May 1, 2026
1324f93
Use `topics` to construct tabs and use fixture as temp data
amoore108 May 1, 2026
b66e5b8
Update types
amoore108 May 1, 2026
d369955
Fix style below related content
amoore108 May 1, 2026
8b16375
Only pass `topics` to TopicDiscovery component
amoore108 May 1, 2026
5f2b45c
Add temp Storybook override to hide related content for story
amoore108 May 1, 2026
b280357
Invert logic to force showing Topic Discovery component
amoore108 May 1, 2026
503ef4b
Update index.test.tsx
amoore108 May 1, 2026
bb9b360
Update tests
amoore108 May 1, 2026
91deeee
Remove `topicDiscovery` from gSSP
amoore108 May 1, 2026
820d501
Update Article test
amoore108 May 1, 2026
de36172
Check for `topics` in ArticlePage before rendering component
amoore108 May 1, 2026
2b678c5
Enable translations for storybook story
amoore108 May 1, 2026
8f5d187
Fix gradient not appearing on left
amoore108 May 1, 2026
e3d9bd1
Update isolated Storybook bg colour
amoore108 May 1, 2026
86009af
Fix topic tag checks
amoore108 May 1, 2026
853929c
Increase gradient width
amoore108 May 1, 2026
e5ca6a8
Reduce size of fixture data
amoore108 May 1, 2026
8ab1faf
translations for chosen services
LilyL0u May 1, 2026
2d4c94d
update new translation in unit test
LilyL0u May 1, 2026
155dcab
Add fake loading and skeleton
amoore108 May 1, 2026
1c781cf
Update index.tsx
amoore108 May 1, 2026
e8dc816
Add 'More from' skeleton
amoore108 May 1, 2026
e2ae9c7
tests fore more from
LilyL0u May 1, 2026
cbe7fdd
Merge branch 'WS-2397-front-end-build-for-new-topic-discovery-compone…
LilyL0u May 1, 2026
e3626a2
test must now wait for component to load
LilyL0u May 1, 2026
248a77b
Remove `topicDiscovery` from `article` type
amoore108 May 4, 2026
04f8f72
RTL fix for 'moreFromLink' skeleton
amoore108 May 5, 2026
edef6b9
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 May 5, 2026
77e1379
different way with {topic}
LilyL0u May 5, 2026
9aa1ea2
Merge branch 'WS-2397-front-end-build-for-new-topic-discovery-compone…
Nabeel1276 May 5, 2026
f0cb6e3
Add basic caching mechanism to not re-fetch promos
amoore108 May 5, 2026
bcdf678
Move Topic Discovery as first OJ
amoore108 May 5, 2026
be821bb
Add test for caching behaviour
amoore108 May 5, 2026
9642ea1
Merge branch 'WS-2397-front-end-build-for-new-topic-discovery-compone…
LilyL0u May 5, 2026
70bb0ad
merge with latest canges added unit tests
LilyL0u May 5, 2026
4b3cd42
quotes in test name for clarity
LilyL0u May 5, 2026
637af47
removed comments
LilyL0u May 5, 2026
f6a38d5
destructure translations first
LilyL0u May 5, 2026
83f96b0
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
amoore108 May 6, 2026
39e4488
Only show gradient when scrolled
amoore108 May 6, 2026
d1a5b57
Removed handleKeyDown function because we are wanting tabbing not arr…
Nabeel1276 May 5, 2026
b7e2c8c
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 May 6, 2026
6ac8e4c
Merge branch 'WS-2397-front-end-build-for-new-topic-discovery-compone…
LilyL0u May 6, 2026
75b4dce
Merge pull request #13967 from bbc/ws-2559-support-translations-for-t…
LilyL0u May 6, 2026
119e67e
Add 'more from' link click tracking
amoore108 May 6, 2026
36f1014
Hide component when JS is disabled
amoore108 May 7, 2026
96f93eb
Hide Topic Discovery under 'continue reading'
amoore108 May 7, 2026
85748af
Update ArticlePage.tsx
amoore108 May 7, 2026
e5e0c84
Use `ResizeObserver` instead of `'resize'` event to fix arrow visibil…
amoore108 May 7, 2026
402091f
Fix contrast
amoore108 May 7, 2026
fc542d2
Mock `ResizeObserver` in Jest
amoore108 May 7, 2026
7329a2a
Only fetch when component is about to come into view
amoore108 May 7, 2026
61c5c62
Merge branch 'latest' into WS-2397-front-end-build-for-new-topic-disc…
Nabeel1276 May 7, 2026
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
4,181 changes: 4,181 additions & 0 deletions data/portuguese/articles/cgmpgpllnp7o.json

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions src/app/components/TopicDiscovery/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# TopicDiscovery

A tabbed content discovery component for article pages that surfaces closely related content based on topic-tag recommendations returned by the BFF (currently using fixture).
This component is intended to help readers continue exploring relevant content without manually searching, while enabling product teams to validate whether topic-based recommendations improve engagement and session depth.

## Overview

`TopicDiscovery` renders a topic-based recommendation module using BFF-provided data. Each topic is displayed as a tab, and selecting a tab reveals up to 4 associated content promos.

The component is:

- **Frontend-only**
- **Canonical-only** (not supported on AMP or lite)
- Designed for **experiment-based rollout**
- Instrumented for **analytics tracking**
- Built with **responsive** and **keyboard-accessible** behaviour in mind

## Features

- Renders only when valid topic discovery data is available
- Displays recommendations grouped into tabs by topic
- Supports:
- `article`
- `video`
- `audio`
- Shows the latest 4 items per topic (as provided by the BFF)
- Includes horizontally scrollable tabs with arrow controls
- Supports keyboard navigation across tabs
- Tracks:
- component impressions
- promo clicks
- tab interactions (via click handler where required)
- Reuses existing `CurationGrid` promo rendering patterns

## Usage

Minimal topicDiscovery shape:

```json
{
"topics": [
{
"topicId": "env",
"topicName": "Environment",
"items": [
{
"id": "item-1",
"title": "Climate action update",
"link": "/news/articles/item-1",
"imageUrl": "https://ichef.test.bbci.co.uk/images/ic/{width}xn/p01wjx8g.jpg",
"imageAlt": "Wind turbines",
"type": "article",
"description": "Short summary",
"firstPublished": 1600000000000
}
]
}
]
}
```

Example usage:

```tsx
import TopicDiscovery from '#app/components/TopicDiscovery';

<TopicDiscovery
topicDiscovery={topicDiscoveryData}
headingText="Explore related topics"
/>;
```
153 changes: 153 additions & 0 deletions src/app/components/TopicDiscovery/ScrollableTabs/index.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { css, Theme } from '@emotion/react';
import pixelsToRem from '#app/utilities/pixelsToRem';

const styles = {
wrapper: ({ palette, spacings }: Theme) =>
css({
display: 'flex',
alignItems: 'center',
gap: `${spacings.HALF}rem`,
borderBottom: `${pixelsToRem(1)}rem solid ${palette.GREY_5}`,
}),

tabList: () =>
css({
display: 'flex',
overflowX: 'auto',
scrollBehavior: 'auto',
scrollbarWidth: 'none',
msOverflowStyle: 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}),

tab: ({ palette, spacings, fontSizes, fontVariants, mq }: Theme) =>
css({
...fontVariants.sansBold,
...fontSizes.pica,
position: 'relative',
whiteSpace: 'nowrap',
background: 'none',
border: 'none',
padding: `${pixelsToRem(12)}rem ${spacings.FULL}rem`,
cursor: 'pointer',
color: palette.GREY_10,

[mq.FORCED_COLOURS]: {
forcedColorAdjust: 'none',
color: 'ButtonText',
fill: 'ButtonText',
},

'&:hover': {
'&::after': {
content: '""',
position: 'absolute',
left: 0,
bottom: 0,
width: '100%',
height: `${spacings.HALF}rem`,
background: palette.POSTBOX,
zIndex: 2,
},
},
'[type=button]&:focus-visible': {
outlineOffset: `${pixelsToRem(-3)}rem`,
boxShadow: 'none',
},
}),

tabActive: ({ spacings, palette }: Theme) =>
css({
'&::after': {
content: '""',
position: 'absolute',
left: 0,
bottom: 0,
width: '100%',
height: `${spacings.HALF}rem`,
background: palette.POSTBOX,
zIndex: 1,
},
}),

scrollButton: ({ palette, spacings }: Theme) =>
css({
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: `${pixelsToRem(44)}rem`,
height: `${pixelsToRem(44)}rem`,
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
color: palette.GREY_10,
'& svg': {
width: `${spacings.DOUBLE}rem`,
height: `${spacings.DOUBLE}rem`,
fill: 'currentcolor',
},
'&:disabled': {
cursor: 'default',
color: `${palette.GREY_5}`,
},
'[type=button]&:focus-visible': {
outlineOffset: `${pixelsToRem(-3)}rem`,
boxShadow: 'none',
},
}),

scrollButtonWrapper: () =>
css({
position: 'relative',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
}),

scrollButtonWrapperHidden: () =>
css({
display: 'none',
}),

scrollButtonFadeStart: ({ palette, spacings }: Theme) =>
css({
position: 'absolute',
top: 0,
height: '100%',
width: `${spacings.TRIPLE}rem`,
pointerEvents: 'none',
zIndex: 1,
"[dir='ltr'] &": {
right: `-${spacings.TRIPLE}rem`,
background: `linear-gradient(to right, ${palette.GREY_2}, ${palette.GREY_2}00)`,
},
"[dir='rtl'] &": {
left: `-${spacings.TRIPLE}rem`,
background: `linear-gradient(to right, ${palette.GREY_2}00, ${palette.GREY_2})`,
},
}),

scrollButtonFadeEnd: ({ palette, spacings }: Theme) =>
css({
position: 'absolute',
top: 0,
height: '100%',
width: `${spacings.TRIPLE}rem`,
pointerEvents: 'none',
zIndex: 1,
"[dir='ltr'] &": {
left: `-${spacings.TRIPLE}rem`,
background: `linear-gradient(to right, ${palette.GREY_2}00, ${palette.GREY_2})`,
},
"[dir='rtl'] &": {
right: `-${spacings.TRIPLE}rem`,
background: `linear-gradient(to right, ${palette.GREY_2}, ${palette.GREY_2}00)`,
},
}),
};

export default styles;
123 changes: 123 additions & 0 deletions src/app/components/TopicDiscovery/ScrollableTabs/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {
render,
screen,
fireEvent,
} from '#app/components/react-testing-library-with-providers';
import * as clickTrackerHook from '#app/hooks/useClickTrackerHandler';
import ScrollableTabs from '.';

const mockTabs = [
{ id: 'tab-1', label: 'Comportamento' },
{ id: 'tab-2', label: 'Mídia social' },
{ id: 'tab-3', label: 'Psicologia' },
];

describe('ScrollableTabs', () => {
it('should render all tabs', () => {
render(
<ScrollableTabs
tabs={mockTabs}
activeTabId="tab-1"
onTabChange={jest.fn()}
labelledBy="heading-id"
/>,
);

expect(
screen.getByRole('tab', { name: 'Comportamento' }),
).toBeInTheDocument();
expect(
screen.getByRole('tab', { name: 'Mídia social' }),
).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Psicologia' })).toBeInTheDocument();
});

it('should mark the active tab with aria-selected true', () => {
render(
<ScrollableTabs
tabs={mockTabs}
activeTabId="tab-2"
onTabChange={jest.fn()}
labelledBy="heading-id"
/>,
);

expect(screen.getByRole('tab', { name: 'Mídia social' })).toHaveAttribute(
'aria-selected',
'true',
);
expect(screen.getByRole('tab', { name: 'Comportamento' })).toHaveAttribute(
'aria-selected',
'false',
);
expect(screen.getByRole('tab', { name: 'Psicologia' })).toHaveAttribute(
'aria-selected',
'false',
);
});

it('should call onTabChange when a tab is clicked', () => {
const onTabChange = jest.fn();

render(
<ScrollableTabs
tabs={mockTabs}
activeTabId="tab-1"
onTabChange={onTabChange}
labelledBy="heading-id"
/>,
);

fireEvent.click(screen.getByRole('tab', { name: 'Mídia social' }));
expect(onTabChange).toHaveBeenCalledWith('tab-2');
});

it('should call clickTrackerHandler onClick when a tab is clicked', () => {
const mockClickHandler = jest.fn();
jest
.spyOn(clickTrackerHook, 'default')
.mockReturnValue({ onClick: mockClickHandler });

render(
<ScrollableTabs
tabs={mockTabs}
activeTabId="tab-1"
onTabChange={jest.fn()}
labelledBy="heading-id"
/>,
);

fireEvent.click(screen.getByRole('tab', { name: 'Mídia social' }));
expect(mockClickHandler).toHaveBeenCalledTimes(1);
});

it('should render a tablist with the correct aria-labelledby', () => {
render(
<ScrollableTabs
tabs={mockTabs}
activeTabId="tab-1"
onTabChange={jest.fn()}
labelledBy="my-heading"
/>,
);

expect(screen.getByRole('tablist')).toHaveAttribute(
'aria-labelledby',
'my-heading',
);
});

it('should render scroll buttons', () => {
render(
<ScrollableTabs
tabs={mockTabs}
activeTabId="tab-1"
onTabChange={jest.fn()}
labelledBy="heading-id"
/>,
);

expect(screen.getByTestId('scroll-start')).toBeInTheDocument();
expect(screen.getByTestId('scroll-end')).toBeInTheDocument();
});
});
Loading
Loading