Skip to content

Commit

Permalink
Merge pull request #12123 from bbc/WSTEAMA-1421-create-jumpto-skeleto…
Browse files Browse the repository at this point in the history
…n-component

Add JumpTo Component (Experiment - Hindi Service)
  • Loading branch information
pvaliani authored Nov 7, 2024
2 parents 74e7db4 + 79f51a8 commit 2de931d
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 8 deletions.
3 changes: 2 additions & 1 deletion src/app/components/InlineLink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ const InlineLink: FC<Props> = ({
}: Props) => {
const { externalLinkText } = useContext(ServiceContext);
const { hostname } = new Url(to);
const isExternalLink = !bbcDomains.some(bbcDomain => hostname === bbcDomain);
const isExternalLink =
hostname && !bbcDomains.some(bbcDomain => hostname === bbcDomain);
const linkProps = {
...(isExternalLink &&
typeof text === 'string' && {
Expand Down
12 changes: 12 additions & 0 deletions src/app/components/JumpTo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Description

The JumpTo component serves as an in-page navigation menu, similar to a table of contents, allowing users to quickly jump to specific headings within an article. This component renders text as a `strong` element, a list of `anchor` links, and groups them within a `navigation` landmark. When a link is actioned, the page scrolls down to the relevant heading, identified by matching anchor `ids`.

This component is typically used in articles with multiple headings, enhancing content findability by providing quick navigation options.

## Props

| Name | type | Description |
| ----------------- | --------------------- | ----------------------------------------------- |
| jumpToData | object | Contains article headings with titles and IDs |
| eventTrackingData | eventTrackingMetadata | Contains click and view tracking data for Piano |
84 changes: 84 additions & 0 deletions src/app/components/JumpTo/fixtureData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const pidginArticleFixtureWithJumpToBlock = {
data: {
article: {
metadata: {
atiAnalytics: {
categoryName: 'US+election+2024',
contentId: 'urn:bbc:optimo:asset:cn03je8kwpko',
contentType: 'article',
language: 'pcm',
pageTitle: 'Kamala Harris Fox News interview in four short points',
timePublished: '2024-10-17T09:27:26.770Z',
timeUpdated: '2024-10-17T09:41:25.801Z',
},
id: 'urn:bbc:ares::article:cn03je8kwpko',
type: 'article',
language: 'pcm',
},
content: {
model: {
blocks: [
{
id: 'headline',
type: 'headline',
model: {
text: 'Kamala Harris Fox interview in four short points',
},
},
{
id: 'timestamp',
type: 'timestamp',
model: {
firstPublished: 1729157246770,
lastPublished: 1729158085801,
},
},
{
id: 'text-intro',
type: 'text',
model: {
blocks: [
{
type: 'paragraph',
model: {
text: 'Kamala Harris discusses her views in a Fox interview.',
},
},
],
},
},
{
id: 'jumpTo',
type: 'jumpTo',
model: {
jumpToHeadings: [
{ heading: 'Harris separates from Biden' },
{ heading: 'Prison gender surgery debate' },
{ heading: 'Apology challenge' },
{ heading: "Biden's mental state questioned" },
],
},
},
{
id: 'text-main',
type: 'text',
model: {
blocks: [
{
type: 'paragraph',
model: {
text: "Harris touched on several critical issues in the interview, including Biden's policy and her stance on prison reforms.",
},
},
],
},
},
],
},
},
},
},
contentType: 'application/json; charset=utf-8',
};

export default pidginArticleFixtureWithJumpToBlock;
27 changes: 27 additions & 0 deletions src/app/components/JumpTo/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import pidginArticleFixtureWithJumpToBlock from './fixtureData';
import JumpTo, { JumpToProps } from '.';
import metadata from './metadata.json';
import readme from './README.md';

const Component = ({ jumpToHeadings = [] }: JumpToProps) => {
return <JumpTo jumpToHeadings={jumpToHeadings} />;
};

export default {
title: 'Components/JumpTo',
Component,
parameters: {
docs: { readme },
metadata,
},
};

export const Example = () => {
const jumpToHeadings =
pidginArticleFixtureWithJumpToBlock.data.article.content.model.blocks.find(
block => block.type === 'jumpTo',
)?.model.jumpToHeadings;

return <Component jumpToHeadings={jumpToHeadings} />;
};
58 changes: 58 additions & 0 deletions src/app/components/JumpTo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @jsx jsx */
import { useContext } from 'react';
import { jsx } from '@emotion/react';
import { ServiceContext } from '#contexts/ServiceContext';
import useViewTracker from '#app/hooks/useViewTracker';
import useClickTrackerHandler from '#app/hooks/useClickTrackerHandler';
import { EventTrackingMetadata } from '#app/models/types/eventTracking';
import Text from '#app/components/Text';
import InlineLink from '#app/components/InlineLink';
import idSanitiser from '../../lib/utilities/idSanitiser';

export interface JumpToProps {
jumpToHeadings?: Array<{ heading: string }>;
eventTrackingData?: EventTrackingMetadata;
}

const JumpTo = ({ jumpToHeadings, eventTrackingData }: JumpToProps) => {
const { translations } = useContext(ServiceContext);
const { jumpTo = 'Jump to' } = translations?.articlePage || {};

const viewRef = useViewTracker(eventTrackingData);
const clickTrackerHandler = useClickTrackerHandler({
...eventTrackingData,
componentName: 'jumpto',
});

const titleId = 'jump-to-heading';

return (
<nav
ref={viewRef}
role="navigation"
aria-labelledby={titleId}
data-testid="jump-to"
>
<Text as="strong" id={titleId}>
{jumpTo}
</Text>
<ol role="list">
{jumpToHeadings?.map(({ heading }) => {
const sanitisedId = idSanitiser(heading);
return (
<li key={sanitisedId}>
<InlineLink
to={`#${sanitisedId}`}
onClick={clickTrackerHandler}
data-testid={`jump-to-link-${sanitisedId}`}
text={heading}
/>
</li>
);
})}
</ol>
</nav>
);
};

export default JumpTo;
29 changes: 29 additions & 0 deletions src/app/components/JumpTo/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"alpha": false,
"lastUpdated": {
"day": 1,
"month": "November",
"year": 2024
},
"uxAccessibilityDoc": {
"done": true,
"reference": {
"url": "https://www.figma.com/design/axYpw2KkNQAMjzdOYa677D/WS-OJ-experiment-handover?node-id=3506-3763&node-type=frame&t=djsGA5Y4x4ey3ar4-0",
"label": "Screen Reader UX"
}
},
"acceptanceCriteria": {
"done": true,
"reference": {
"url": "https://paper.dropbox.com/doc/Jump-to-menu-Accessibility-acceptance-criteria--CZuZGgB0eyva~6M50q_UCCQUAg-b5wF4xr9YMgITE0Ui9sWc",
"label": "Accessibility Acceptance Criteria"
}
},
"swarm": {
"done": false,
"reference": {
"url": "",
"label": "Accessibility Swarm Notes"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const useHashChangeHandler = hash => {
const getHashProp = path(['location', 'hash']);

const withHashChangeHandler = Component => props => {
const hash = getHashProp(props);
const hash = decodeURIComponent(getHashProp(props));

useHashChangeHandler(hash);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,74 @@ import withHashChangeHandler from '.';
const Fixture = withHashChangeHandler(() => (
<>
<a href="#section-1">Go to section 1</a>
<section id="section-1">Section 1</section>
<h2 id="section-1">Section 1</h2>
</>
));

const CyrillicFixture = withHashChangeHandler(() => (
<>
<a href="#Мирнија-сам-неко-брине-о-њој-док-је-у-школи">Go to section 1</a>
<h2 id="Мирнија-сам-неко-брине-о-њој-док-је-у-школи">Section 1</h2>
</>
));

const HindiFixture = withHashChangeHandler(() => (
<>
<a href="#आंखों-के-सामने-छा-गया-था-अंधेरा">Go to section 1</a>
<h2 id="आंखों-के-सामने-छा-गया-था-अंधेरा">Section 1</h2>
</>
));

window.HTMLElement.prototype.scrollIntoView = jest.fn();
window.HTMLElement.prototype.focus = jest.fn();

it('should scroll into view and focus on element when hash location changes', async () => {
const { rerender } = render(<Fixture location={{ hash: '' }} />);
describe('withHashChangeHandler', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should scroll into view and focus on element when hash location changes', async () => {
const { rerender } = render(<Fixture location={{ hash: '' }} />);

rerender(<Fixture location={{ hash: '#section-1' }} />);

expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalledTimes(
1,
);
expect(window.HTMLElement.prototype.focus).toHaveBeenCalledTimes(1);
});

it('should scroll into view and focus on element when hash location contains Cyrillic characters', async () => {
render(
<CyrillicFixture
location={{
hash:
// This is the encoded version of Мирнија-сам-неко-брине-о-њој-док-је-у-школи
'#%D0%9C%D0%B8%D1%80%D0%BD%D0%B8%D1%98%D0%B0-%D1%81%D0%B0%D0%BC-%D0%BD%D0%B5%D0%BA%D0%BE-%D0%B1%D1%80%D0%B8%D0%BD%D0%B5-%D0%BE-%D1%9A%D0%BE%D1%98-%D0%B4%D0%BE%D0%BA-%D1%98%D0%B5-%D1%83-%D1%88%D0%BA%D0%BE%D0%BB%D0%B8',
}}
/>,
);

expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalledTimes(
1,
);
expect(window.HTMLElement.prototype.focus).toHaveBeenCalledTimes(1);
});

rerender(<Fixture location={{ hash: '#section-1' }} />);
it('should scroll into view and focus on element when hash location contains Hindi characters', async () => {
render(
<HindiFixture
location={{
hash:
// This is the encoded version of आंखों-के-सामने-छा-गया-था-अंधेरा
'#%E0%A4%86%E0%A4%82%E0%A4%96%E0%A5%8B%E0%A4%82-%E0%A4%95%E0%A5%87-%E0%A4%B8%E0%A4%BE%E0%A4%AE%E0%A4%A8%E0%A5%87-%E0%A4%9B%E0%A4%BE-%E0%A4%97%E0%A4%AF%E0%A4%BE-%E0%A4%A5%E0%A4%BE-%E0%A4%85%E0%A4%82%E0%A4%A7%E0%A5%87%E0%A4%B0%E0%A4%BE',
}}
/>,
);

expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalledTimes(1);
expect(window.HTMLElement.prototype.focus).toHaveBeenCalledTimes(1);
expect(window.HTMLElement.prototype.scrollIntoView).toHaveBeenCalledTimes(
1,
);
expect(window.HTMLElement.prototype.focus).toHaveBeenCalledTimes(1);
});
});
3 changes: 3 additions & 0 deletions src/app/lib/config/services/hindi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export const service: DefaultServiceConfig = {
audioPlayer: 'ऑडिया प्लेयर',
videoPlayer: 'वीडियो प्लेयर',
},
articlePage: {
jumpTo: 'Jump to', // replace with Hindi translation later
},
liveExperiencePage: {
liveLabel: 'लाइव',
liveCoverage: 'लाइव कवरेज',
Expand Down
3 changes: 3 additions & 0 deletions src/app/models/types/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export interface Translations {
audioPlayer: string;
videoPlayer: string;
};
articlePage?: {
jumpTo: string;
};
liveExperiencePage: {
liveLabel: string;
liveCoverage: string;
Expand Down

0 comments on commit 2de931d

Please sign in to comment.