Skip to content

Commit 8594a1f

Browse files
VIA-608 Render care card headings at level specified in Content-API
Fixes issue on the Rotavirus page where the care card was rendering without a heading. In the content-api response this care card heading is at level h4 because it sits beneath another subheading, which we have not seen on other pages up until this point. Updates the content styling service to preserve the heading level and use it when rendering the react component. Heading level is optional to preserve compatibility with other non-content api usages of the care card (EliD, rsv-pregnancy, etc); if heading level is not specified the react component library will render at the default level as it did before (h2 at time of writing). Note that incorrect heading levels would not have been obviously visible in the UI as a single css style was applied which sets the text-size. However, a screenreader would have noticed that the h2/h3/h4 elements were appearing in the wrong structure.
1 parent 4dbfbfa commit 8594a1f

File tree

7 files changed

+101
-17
lines changed

7 files changed

+101
-17
lines changed

src/app/_components/nhs-frontend/NonUrgentCareCard.test.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,25 @@ describe("NonUrgentCareCard", () => {
1313
it("should render heading", () => {
1414
render(<NonUrgentCareCard heading={"Contact GP:"} content={<p>this is their contact</p>} />);
1515

16-
const heading: HTMLElement = screen.getByRole("heading", { name: "Non-urgent advice: Contact GP:", level: 2 });
16+
const defaultLevel2Heading: HTMLElement = screen.getByRole("heading", {
17+
name: "Non-urgent advice: Contact GP:",
18+
level: 2,
19+
});
1720

18-
expect(heading).toBeVisible();
21+
expect(defaultLevel2Heading).toBeVisible();
22+
});
23+
24+
it("should render heading at specified level", () => {
25+
render(
26+
<NonUrgentCareCard heading={"Contact GP lvl 4:"} headingLevel={"h4"} content={<p>this is their contact</p>} />,
27+
);
28+
29+
const level4Heading: HTMLElement = screen.getByRole("heading", {
30+
name: "Non-urgent advice: Contact GP lvl 4:",
31+
level: 4,
32+
});
33+
34+
expect(level4Heading).toBeVisible();
1935
});
2036

2137
it("should render content inside of care card", () => {

src/app/_components/nhs-frontend/NonUrgentCareCard.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { HeadingLevel } from "@src/services/content-api/types";
34
import { Heading } from "@src/services/eligibility-api/types";
45
import { Card } from "nhsuk-react-components";
56
import { JSX } from "react";
@@ -8,13 +9,18 @@ import styles from "./styles.module.css";
89

910
interface NonUrgentCareCardProps {
1011
heading: Heading | string | JSX.Element;
12+
headingLevel?: HeadingLevel;
1113
content: JSX.Element;
1214
}
1315

14-
const NonUrgentCareCard = ({ heading, content }: NonUrgentCareCardProps) => {
16+
const NonUrgentCareCard = ({ heading, headingLevel, content }: NonUrgentCareCardProps) => {
1517
return (
1618
<Card cardType="non-urgent" data-testid="non-urgent-care-card">
17-
<Card.Heading>{heading}</Card.Heading>
19+
{headingLevel ? (
20+
<Card.Heading headingLevel={headingLevel}>{heading}</Card.Heading>
21+
) : (
22+
<Card.Heading>{heading}</Card.Heading>
23+
)}
1824
<Card.Content className={styles.careCardZeroMarginBottom}>{content}</Card.Content>
1925
</Card>
2026
);

src/app/_components/nhs-frontend/UrgentCareCard.test.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,28 @@ describe("UrgentCareCard", () => {
1313
it("should render heading", () => {
1414
render(<UrgentCareCard heading={"Contact GP:"} content={<p>this is their contact</p>} />);
1515

16+
const defaultLevel2Heading: HTMLElement = screen.getByRole("heading", {
17+
name: "Urgent advice: Contact GP:",
18+
level: 2,
19+
});
20+
21+
expect(defaultLevel2Heading).toBeVisible();
22+
});
23+
24+
it("should render heading at specified heading level", () => {
25+
render(<UrgentCareCard heading={"Contact GP lvl 3:"} headingLevel={"h3"} content={<p>this is their contact</p>} />);
26+
27+
const level3Heading: HTMLElement = screen.getByRole("heading", {
28+
name: "Urgent advice: Contact GP lvl 3:",
29+
level: 3,
30+
});
31+
32+
expect(level3Heading).toBeVisible();
33+
});
34+
35+
it("should render heading at level 2 by default if no heading level provided", () => {
36+
render(<UrgentCareCard heading={"Contact GP:"} content={<p>this is their contact</p>} />);
37+
1638
const heading: HTMLElement = screen.getByRole("heading", { name: "Urgent advice: Contact GP:", level: 2 });
1739

1840
expect(heading).toBeVisible();

src/app/_components/nhs-frontend/UrgentCareCard.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { HeadingLevel } from "@src/services/content-api/types";
34
import { Heading } from "@src/services/eligibility-api/types";
45
import { Card } from "nhsuk-react-components";
56
import { JSX } from "react";
@@ -8,13 +9,18 @@ import styles from "./styles.module.css";
89

910
interface NonUrgentCareCardProps {
1011
heading: Heading | string | JSX.Element;
12+
headingLevel?: HeadingLevel;
1113
content: JSX.Element;
1214
}
1315

14-
const UrgentCareCard = ({ heading, content }: NonUrgentCareCardProps) => {
16+
const UrgentCareCard = ({ heading, headingLevel, content }: NonUrgentCareCardProps) => {
1517
return (
1618
<Card cardType="urgent" data-testid="urgent-care-card">
17-
<Card.Heading>{heading}</Card.Heading>
19+
{headingLevel ? (
20+
<Card.Heading headingLevel={headingLevel}>{heading}</Card.Heading>
21+
) : (
22+
<Card.Heading>{heading}</Card.Heading>
23+
)}
1824
<Card.Content className={styles.careCardZeroMarginBottom}>{content}</Card.Content>
1925
</Card>
2026
);

src/services/content-api/parsers/content-styling-service.test.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
VaccinePageSection,
1414
VaccinePageSubsection,
1515
} from "@src/services/content-api/types";
16-
import { render, screen } from "@testing-library/react";
16+
import { render, screen, within } from "@testing-library/react";
1717
import { JSX, isValidElement } from "react";
1818

1919
const mockNBSBookingActionHTML = "NBS Booking Link Test";
@@ -135,11 +135,16 @@ describe("ContentStylingService", () => {
135135
render(styledSubsection);
136136

137137
const text: HTMLElement = screen.getByText("This is a styled paragraph non-urgent subsection");
138-
const heading: HTMLElement = screen.getByText("Heading for Non Urgent Component");
138+
const cardHeading: HTMLElement = screen.getByRole("heading", {
139+
level: 3,
140+
name: "Non-urgent advice: Heading for Non Urgent Component",
141+
});
139142
const nonUrgent: HTMLElement = screen.getByText("Non-urgent advice:");
143+
const visibleHeading: HTMLElement = within(cardHeading).getByText("Heading for Non Urgent Component");
140144

141145
expect(text).toBeInTheDocument();
142-
expect(heading).toBeInTheDocument();
146+
expect(cardHeading).toBeInTheDocument();
147+
expect(visibleHeading).toBeInTheDocument();
143148
expect(nonUrgent).toBeInTheDocument();
144149
});
145150

@@ -148,11 +153,16 @@ describe("ContentStylingService", () => {
148153
render(styledSubsection);
149154

150155
const text: HTMLElement = screen.getByText("This is a styled paragraph urgent subsection");
151-
const heading: HTMLElement = screen.getByText("Heading for Urgent Component");
156+
const cardHeading: HTMLElement = screen.getByRole("heading", {
157+
level: 3,
158+
name: "Urgent advice: Heading for Urgent Component",
159+
});
160+
const visibleHeading: HTMLElement = within(cardHeading).getByText("Heading for Urgent Component");
152161
const nonUrgent: HTMLElement = screen.getByText("Urgent advice:");
153162

154163
expect(text).toBeInTheDocument();
155-
expect(heading).toBeInTheDocument();
164+
expect(cardHeading).toBeInTheDocument();
165+
expect(visibleHeading).toBeInTheDocument();
156166
expect(nonUrgent).toBeInTheDocument();
157167
});
158168

@@ -341,6 +351,23 @@ describe("ContentStylingService", () => {
341351
expect(headingAndContent.content).toEqual("<p>you have not been contacted</p>");
342352
});
343353

354+
it("should extract heading level", async () => {
355+
const headingAndContentFromLevel4: HeadingWithContent = extractHeadingAndContent(
356+
"<h4>Heading level 4</h4><p>you have not been contacted</p>",
357+
);
358+
359+
expect(headingAndContentFromLevel4.headingLevel).toEqual("h4");
360+
});
361+
362+
it("should extract first heading at any level from simple non-urgent html string", async () => {
363+
const heading: HeadingWithContent = extractHeadingAndContent(
364+
"<h3>Heading level 3</h3><p>you have not been contacted</p><h4>heading level 4</h4>",
365+
);
366+
367+
expect(heading.heading).toEqual("Heading level 3");
368+
expect(heading.headingLevel).toEqual("h3");
369+
});
370+
344371
it("should return empty heading and content from empty string", async () => {
345372
const headingAndContent: HeadingWithContent = extractHeadingAndContent("");
346373

src/services/content-api/parsers/content-styling-service.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import UrgentCareCard from "@src/app/_components/nhs-frontend/UrgentCareCard";
33
import { VaccineType } from "@src/models/vaccine";
44
import { styleHowToGetSectionForRsv } from "@src/services/content-api/parsers/custom/rsv";
55
import { styleHowToGetSectionForRsvPregnancy } from "@src/services/content-api/parsers/custom/rsv-pregnancy";
6-
import type {
6+
import {
7+
HeadingLevel,
78
HeadingWithContent,
89
Overview,
910
StyledPageSection,
@@ -62,20 +63,22 @@ const styleSubsection = (subsection: VaccinePageSubsection, id: number, isLastSu
6263
if (subsection.name === Subsections.INFORMATION) {
6364
return <InsetText key={id}>{_getDivWithSanitisedHtml(text)}</InsetText>;
6465
} else if (subsection.name === Subsections.NON_URGENT) {
65-
const { heading, content } = extractHeadingAndContent(subsection.text);
66+
const { heading, headingLevel, content } = extractHeadingAndContent(subsection.text);
6667
return (
6768
<NonUrgentCareCard
6869
key={id}
6970
heading={_getDivWithSanitisedHtml(heading)}
71+
headingLevel={headingLevel}
7072
content={_getDivWithSanitisedHtml(content)}
7173
/>
7274
);
7375
} else if (subsection.name === Subsections.URGENT) {
74-
const { heading, content } = extractHeadingAndContent(subsection.text);
76+
const { heading, headingLevel, content } = extractHeadingAndContent(subsection.text);
7577
return (
7678
<UrgentCareCard
7779
key={id}
7880
heading={_getDivWithSanitisedHtml(heading)}
81+
headingLevel={headingLevel}
7982
content={_getDivWithSanitisedHtml(content)}
8083
/>
8184
);
@@ -117,14 +120,15 @@ const styleSection = (section: VaccinePageSection): StyledPageSection => {
117120
};
118121

119122
const extractHeadingAndContent = (text: string): HeadingWithContent => {
120-
const pattern: RegExp = /^<h3>(.*?)<\/h3>/;
123+
const pattern: RegExp = /^<(h\d)>(.*?)<\/h\d>/;
121124
const match: RegExpMatchArray | null = pattern.exec(text);
122125

123126
if (match) {
124-
const firstOccurrence: string = match[1];
127+
const firstCaptureGroup: HeadingLevel = match[1] as HeadingLevel;
128+
const secondCaptureGroup: string = match[2];
125129
const remainingText: string = text.replace(pattern, "").trim();
126130

127-
return { heading: firstOccurrence, content: remainingText };
131+
return { heading: secondCaptureGroup, headingLevel: firstCaptureGroup, content: remainingText };
128132
} else {
129133
return {
130134
heading: "",

src/services/content-api/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,11 @@ export type StyledPageSection = {
117117
component: JSX.Element;
118118
};
119119

120+
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | undefined;
121+
120122
export type HeadingWithContent = {
121123
heading: string;
124+
headingLevel?: HeadingLevel;
122125
content: string;
123126
};
124127

0 commit comments

Comments
 (0)