Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
55b333d
NXT-802 Move utils folder from storyblokSectionNav to root utils for …
shilpanice Dec 19, 2025
632bd24
NXT-802 Add preheading to the pageHeader
shilpanice Dec 19, 2025
e14be38
NXT-802 Add unit test cases to cover getPreheading functionality
shilpanice Dec 22, 2025
8f526b0
NXT-802 Add guidanceListPage.feature,indicatorListPage.feature,filte…
shilpanice Dec 23, 2025
02efc2e
NXT-802 Remove guidance and indicator feature files from exclusion li…
shilpanice Dec 24, 2025
04371ef
NXT-802 Remove hideSidenav condition when calling buildTree
shilpanice Jan 6, 2026
bb45bb5
NXT-802 Change SectionNavUtils name to ContentStructureUtils
shilpanice Jan 6, 2026
cb7811e
NXT-802 Refactor section navigation logic to use getSectionnavTitle f…
shilpanice Jan 6, 2026
9fbac1b
NXT-802 Fix lint issues
shilpanice Jan 6, 2026
5a38b9d
NXT-802 Update test to pass preheading when section nav is hidden
shilpanice Jan 6, 2026
506d129
NXT-802 Update "ContentStructureUtils" utils file name to follow came…
shilpanice Feb 19, 2026
f3b219c
NXT-802 Refactor import statement for contentStructureUtils to use al…
shilpanice Feb 19, 2026
7e674aa
NXT-802 Refactor import statements for contentStructureUtils to use c…
shilpanice Feb 19, 2026
fa87e93
Rename ContentStructureUtils to contentStructureUtils
shilpanice Feb 23, 2026
4c131b9
NXT-802 Move preheading logic in InfoPage to contentStructureUtils
shilpanice Feb 23, 2026
ae8a6c5
NXT-802 Fix function name casing for getSectionNavTitle in StoryblokS…
shilpanice Feb 23, 2026
eff2847
NXT-802 Rename getSectionNavTitle to getSectionTitle for consistency
shilpanice Mar 4, 2026
fb80b0c
NXT-802 Replace sectionNavIsPopulated with treeHasItems for consisten…
shilpanice Mar 4, 2026
28b327a
NXT-802 Modify the import of getPreheading from StoryblokPageHeader
shilpanice Mar 4, 2026
9953caf
Merge branch 'main' into NXT-802-apply-pre-heading-to-h-1-on-info-pages
shilpanice Mar 10, 2026
5c64df4
NXT-802 Add hidePreheader to hide preheader from infopages
shilpanice Mar 10, 2026
0c99fa9
NXT-802 Update storyblok types from Alpha
shilpanice Mar 10, 2026
ca8f025
NXT-802 Add correct types to hidePreHeader unit test cases
shilpanice Mar 10, 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { render, screen } from "@testing-library/react";
import { NextSeo } from "next-seo";
import React from "react";

import { type ExtendedSBLink } from "@/components/Storyblok/StoryblokSectionNav/utils/Utils";
import { logger } from "@/logger";
import { getAdditionalMetaTags } from "@/utils/storyblok";
import { type ExtendedSBLink } from "@/utils/storyblok/contentStructureUtils";

import { CorporateContentPage } from "./CorporateContentPage";

Expand Down
126 changes: 126 additions & 0 deletions web/src/components/Storyblok/InfoPage/InfoPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { StoryblokComponent } from "@storyblok/react";
import { screen } from "@testing-library/react";

import sampleDataHeroInPageNav from "@/mockData/storyblok/infoPageWithHeroAndInPageNav.json";
import sampleDataNoNav from "@/mockData/storyblok/infoPageWithNoNav.json";
import sampleDataPageHeaderSectionNav from "@/mockData/storyblok/infoPageWithPageHeaderAndSectionNav.json";
import { render } from "@/test-utils/rendering";
import { InfoPageStoryblok } from "@/types/storyblok";
import * as contentStructureUtils from "@/utils/storyblok/contentStructureUtils";

import { InfoPage, type InfoPageBlokProps } from "./InfoPage";

Expand Down Expand Up @@ -34,7 +36,16 @@ const mockPropsWithNoNav: InfoPageBlokProps = {
slug: sampleDataNoNav.slug,
};

jest.mock("@storyblok/react", () => ({
StoryblokComponent: jest.fn(() => null),
}));

const mockedStoryblokComponent = StoryblokComponent as unknown as jest.Mock;

describe("InfoPage", () => {
beforeAll(() => {
jest.spyOn(contentStructureUtils, "treeHasItems");
});
afterEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -114,4 +125,119 @@ describe("InfoPage", () => {
screen.queryByTestId("info-page-nav-wrapper")
).not.toBeInTheDocument();
});
it("passes preheading when section nav is populated and title differs", () => {
(contentStructureUtils.treeHasItems as jest.Mock).mockReturnValue(true);

render(<InfoPage {...mockPropsWithPageHeaderAndSectionNav} />);

expect(mockedStoryblokComponent).toHaveBeenCalledWith(
expect.objectContaining({
preheading: "Info Page with Hero and In Page Nav",
}),
{}
);
});
it("passes preheading when section nav is hidden", () => {
(contentStructureUtils.treeHasItems as jest.Mock).mockReturnValue(true);
const mockPropsWithSectionNav = {
...mockPropsWithPageHeaderAndSectionNav,
hideSectionNav: "true",
};
render(<InfoPage {...mockPropsWithSectionNav} />);
expect(mockedStoryblokComponent).toHaveBeenCalledWith(
expect.objectContaining({
preheading: "Info Page with Hero and In Page Nav",
}),
{}
);
});
it("passes empty preheading when section name matches header title", () => {
(contentStructureUtils.treeHasItems as jest.Mock).mockReturnValue(true);

const mockPropsWithSameTitleandPreheading = {
...mockPropsWithPageHeaderAndSectionNav,
tree: [
{
id: 681214537,
uuid: "e5cfb560-56ac-45a6-9dec-b421837e7692",
slug: "unit-test-data/info-page-hero-in-page-nav",
path: null,
parent_id: 659520607,
name: "Info Page with Page Header and Section Nav",
is_folder: false,
published: true,
is_startpage: false,
position: -30,
real_path: "/unit-test-data/info-page-hero-in-page-nav",
childLinks: [],
},
],
blok: {
...mockPropsWithPageHeaderAndSectionNav.blok,
header: [
{
...mockPropsWithPageHeaderAndSectionNav.blok.header[0],
title: "Info Page with Page Header and Section Nav",
},
],
},
};

render(<InfoPage {...mockPropsWithSameTitleandPreheading} />);

expect(mockedStoryblokComponent).toHaveBeenCalledWith(
expect.objectContaining({
preheading: "",
}),
{}
);
});
it("passes empty preheading when pageHeader is missing", () => {
(contentStructureUtils.treeHasItems as jest.Mock).mockReturnValue(true);
render(<InfoPage {...mockPropsWithHeroAndInPageNav} />);

expect(mockedStoryblokComponent).toHaveBeenCalledWith(
expect.objectContaining({
preheading: "",
}),
{}
);
});
it("returns undefined preheading when hidePreHeader is 'true'", () => {
const mockProps = {
...mockPropsWithPageHeaderAndSectionNav,
blok: {
...mockPropsWithPageHeaderAndSectionNav.blok,
hidePreHeader: "true" as "true" | "false",
},
};

render(<InfoPage {...mockProps} />);

expect(mockedStoryblokComponent).toHaveBeenCalledWith(
expect.objectContaining({
preheading: undefined,
}),
{}
);
});

it("calls getPreheading when hidePreHeader is not 'true'", () => {
const mockProps = {
...mockPropsWithPageHeaderAndSectionNav,
blok: {
...mockPropsWithPageHeaderAndSectionNav.blok,
hidePreHeader: "false" as "true" | "false",
},
};

render(<InfoPage {...mockProps} />);

expect(mockedStoryblokComponent).toHaveBeenCalledWith(
expect.objectContaining({
preheading: expect.any(String),
}),
{}
);
});
});
16 changes: 10 additions & 6 deletions web/src/components/Storyblok/InfoPage/InfoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { StoryblokComponent } from "@storyblok/react";
import { Grid, GridItem } from "@nice-digital/nds-grid";
import { InPageNav } from "@nice-digital/nds-in-page-nav";

import { getPreheading } from "@/components/Storyblok/StoryblokPageHeader/StoryblokPageHeader";
import { StoryblokRichText } from "@/components/Storyblok/StoryblokRichText/StoryblokRichText";
import { StoryblokSectionNav } from "@/components/Storyblok/StoryblokSectionNav/StoryblokSectionNav";
import {
type ExtendedSBLink,
sectionNavIsPopulated,
} from "@/components/Storyblok/StoryblokSectionNav/utils/Utils";
import { type Breadcrumb } from "@/types/Breadcrumb";
import { type InfoPageStoryblok } from "@/types/storyblok";
import {
type ExtendedSBLink,
treeHasItems,
} from "@/utils/storyblok/contentStructureUtils";

import styles from "./InfoPage.module.scss";

Expand All @@ -29,6 +30,8 @@ export const InfoPage = ({
slug,
pageType,
}: InfoPageBlokProps): React.ReactElement => {
const preheading =
blok.hidePreHeader === "true" ? undefined : getPreheading(tree, blok);
return (
<div className={styles.infoPage}>
{blok.metadata &&
Expand All @@ -42,19 +45,20 @@ export const InfoPage = ({
blok={nestedBlok}
key={nestedBlok._uid}
breadcrumbs={breadcrumbs}
preheading={preheading}
/>
);
})}

<Grid
gutter="loose"
className={
!sectionNavIsPopulated(tree) || blok.hideSectionNav === "true"
!treeHasItems(tree) || blok.hideSectionNav === "true"
? styles["infoPage--reverse-order"]
: undefined
}
>
{((blok.hideSectionNav !== "true" && sectionNavIsPopulated(tree)) ||
{((blok.hideSectionNav !== "true" && treeHasItems(tree)) ||
blok.hideInPageNav !== "true") && (
<GridItem
cols={12}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,42 @@ import { PageHeader } from "@nice-digital/nds-page-header";
import { type Breadcrumb as TypeBreadcrumb } from "@/types/Breadcrumb";
import {
ButtonLinkStoryblok,
InfoPageStoryblok,
type PageHeaderStoryblok,
} from "@/types/storyblok";
import {
ExtendedSBLink,
getSectionTitle,
treeHasItems,
} from "@/utils/storyblok/contentStructureUtils";

import { StoryblokButtonLink } from "../StoryblokButtonLink/StoryblokButtonLink";

interface PageHeaderBlokProps {
blok: PageHeaderStoryblok;
breadcrumbs?: TypeBreadcrumb[];
preheading: string;
}

export const getPreheading = (
tree: ExtendedSBLink[],
blok: InfoPageStoryblok
): string => {
if (!treeHasItems(tree)) return "";

const headerTitle =
blok.header?.[0]?.component === "pageHeader" ? blok.header?.[0]?.title : "";
const sectionTitle = getSectionTitle(tree)?.name;

if (!headerTitle || !sectionTitle) return "";

return sectionTitle !== headerTitle ? sectionTitle : "";
};

export const StoryblokPageHeader = ({
blok,
breadcrumbs,
preheading,
}: PageHeaderBlokProps): React.ReactElement => {
const BreadcrumbComponent = breadcrumbs?.length ? (
<Breadcrumbs>
Expand All @@ -42,6 +65,7 @@ export const StoryblokPageHeader = ({
return (
<PageHeader
heading={title}
preheading={preheading}
lead={summary || undefined}
breadcrumbs={BreadcrumbComponent}
description={description}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import React from "react";

import { StackedNav, StackedNavLink } from "@nice-digital/nds-stacked-nav";

import { sectionNavIsPopulated } from "./utils/Utils";
import { type ExtendedSBLink } from "./utils/Utils";
import {
type ExtendedSBLink,
getSectionTitle,
treeHasItems,
} from "@/utils/storyblok/contentStructureUtils";

type StoryblokSectionNavProps = {
tree: ExtendedSBLink[];
Expand All @@ -14,18 +17,14 @@ export const StoryblokSectionNav = ({
tree,
slug,
}: StoryblokSectionNavProps): JSX.Element => {
const sectionNavLabel = sectionNavIsPopulated(tree) ? tree[0] : null;
const sectionNavTreeWithoutLabel = sectionNavIsPopulated(tree)
? tree.slice(1)
: [];
const sectionTitle = getSectionTitle(tree);
const sectionNavTreeWithoutLabel = treeHasItems(tree) ? tree.slice(1) : [];
return (
<>
{sectionNavIsPopulated(tree) && (
{treeHasItems(tree) && (
<StackedNav
label={sectionNavLabel?.name}
aria-label={`Section navigation: ${
sectionNavLabel?.name ?? "Section"
}`}
label={sectionTitle?.name}
aria-label={`Section navigation: ${sectionTitle?.name ?? "Section"}`}
data-testid="section-nav"
>
{sectionNavTreeWithoutLabel?.map((parent) => (
Expand Down
2 changes: 1 addition & 1 deletion web/src/types/SBCorporateContent.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExtendedSBLink } from "@/components/Storyblok/StoryblokSectionNav/utils/Utils";
import { ExtendedSBLink } from "@/utils/storyblok/contentStructureUtils";

export type SlugCatchAllSuccessProps = {
story: ISbStoryData<InfoPageStoryblok | CategoryNavigationStoryblok>;
Expand Down
1 change: 1 addition & 0 deletions web/src/types/storyblok.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ export interface InfogramEmbedStoryblok {
export interface InfoPageStoryblok {
header: (PageHeaderStoryblok | HeroStoryblok)[];
metadata?: MetadataStoryblok[];
hidePreHeader?: "true" | "false";
hideSectionNav?: "true" | "false";
hideInPageNav?: "true" | "false";
content: RichtextStoryblok;
Expand Down
6 changes: 3 additions & 3 deletions web/src/utils/getCorporateContentGssp.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as SectionNavUtils from "@/components/Storyblok/StoryblokSectionNav/utils/Utils";
import { logger } from "@/logger";
import {
getCorporateContentGssp,
Expand All @@ -11,13 +10,14 @@ import {
getStoryVersionFromQuery,
GENERIC_ERROR_MESSAGE,
} from "@/utils/storyblok";
import * as contentStructureUtils from "@/utils/storyblok/contentStructureUtils";

import type { GetServerSidePropsContext } from "next";

jest.mock("@/utils/storyblok");
jest.mock("@/logger");
//TODO do we need a better mock for the tree?
jest.mock("@/components/Storyblok/StoryblokSectionNav/utils/Utils", () => ({
jest.mock("@/utils/storyblok/contentStructureUtils", () => ({
buildTree: jest.fn(),
}));

Expand All @@ -27,7 +27,7 @@ const mockGetSlugFromParams = getSlugFromParams as jest.Mock;
const mockGetStoryVersionFromQuery = getStoryVersionFromQuery as jest.Mock;
const mockLoggerError = logger.error as jest.Mock;

const mockBuildTree = SectionNavUtils.buildTree as jest.Mock;
const mockBuildTree = contentStructureUtils.buildTree as jest.Mock;

describe("getCorporateContentGssp", () => {
const templateId = "test-template";
Expand Down
14 changes: 5 additions & 9 deletions web/src/utils/getCorporateContentGssp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import {
buildTree,
type ExtendedSBLink,
} from "@/components/Storyblok/StoryblokSectionNav/utils/Utils";
import { logger } from "@/logger";
import {
InfoPageStoryblok,
Expand All @@ -14,6 +10,10 @@ import {
getSlugFromParams,
getStoryVersionFromQuery,
} from "@/utils/storyblok";
import {
buildTree,
type ExtendedSBLink,
} from "@/utils/storyblok/contentStructureUtils";

import type { GetServerSidePropsContext, GetServerSideProps } from "next";

Expand Down Expand Up @@ -78,11 +78,7 @@ export const getCorporateContentGssp = <
const component = storyResult.story?.content?.component;
let tree: ExtendedSBLink[] = [];

if (
component === "infoPage" &&
storyResult.story?.content.hideSectionNav !== "true" &&
parentID !== null
) {
if (component === "infoPage" && parentID !== null) {
Comment thread
beccadawson marked this conversation as resolved.
tree = await buildTree(parentID, slug, isRootPage);
// TODO: move out of catchall page; would need API route as GSSP is not allowed in components whilst using pages router
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import mockData from "@/mockData/storyblok/sectionNavData.json";

import { sectionNavIsPopulated } from "./Utils";
import { treeHasItems } from "./contentStructureUtils";

describe("sectionNavIsNotEmpty", () => {
it("returns true for a non-empty array", () => {
const tree = mockData.tree;
expect(sectionNavIsPopulated(tree)).toBe(true);
expect(treeHasItems(tree)).toBe(true);
});

it("returns false for an empty array", () => {
expect(sectionNavIsPopulated([])).toBe(false);
expect(treeHasItems([])).toBe(false);
});
});
Loading