Regions" }}
+ />,
+ );
+ expect(
+ screen.getByRole("heading", { name: "Regions" }),
+ ).toBeInTheDocument();
+ expect(screen.queryByText("Regions")).not.toBeInTheDocument();
+ });
});
diff --git a/src/components/SearchAndFilter/FilterPanelSection/FilterPanelSection.tsx b/src/components/SearchAndFilter/FilterPanelSection/FilterPanelSection.tsx
index 662a07370..c71999f82 100644
--- a/src/components/SearchAndFilter/FilterPanelSection/FilterPanelSection.tsx
+++ b/src/components/SearchAndFilter/FilterPanelSection/FilterPanelSection.tsx
@@ -113,10 +113,9 @@ const FilterPanelSection = ({
+ >
+ {highlightSubString(heading, searchTerm).text}
+
)}
{
- it("returns a substring match", () => {
- const string = "react";
- const subString = "act";
- const testString = highlightSubString(string, subString);
- expect(testString).toEqual({ match: true, text: "react" });
- });
-
- it("gracefully fails when invalid string is provided", () => {
- expect(highlightSubString(undefined, "somesub")).toEqual({
- text: "",
- match: false,
- });
- });
-
- it("gracefully fails when invalid substring is provided", () => {
- expect(highlightSubString("somestring", undefined)).toEqual({
- text: "somestring",
- match: false,
- });
- });
-
- it("handles special substrings for regex matching", () => {
- expect(highlightSubString("somestring\\", "\\")).toEqual({
- text: "somestring\\",
- match: true,
- });
- });
-});
diff --git a/src/utils.test.tsx b/src/utils.test.tsx
new file mode 100644
index 000000000..f307e5594
--- /dev/null
+++ b/src/utils.test.tsx
@@ -0,0 +1,66 @@
+import { render } from "@testing-library/react";
+import React from "react";
+
+import { highlightSubString } from "./utils";
+
+describe("highlightSubString function ", () => {
+ it("returns a substring match", () => {
+ const { text, match } = highlightSubString("react", "act");
+ expect(match).toBe(true);
+ expect(render(<>{text}>).container.innerHTML).toBe(
+ "react",
+ );
+ });
+
+ it("matches case insensitively and emphasises every occurrence", () => {
+ const { text, match } = highlightSubString("Banana", "AN");
+ expect(match).toBe(true);
+ expect(render(<>{text}>).container.innerHTML).toBe(
+ "Banana",
+ );
+ });
+
+ it("does not parse the string as HTML", () => {
+ const { text, match } = highlightSubString("bold", "bold");
+ expect(match).toBe(true);
+ expect(render(<>{text}>).container.innerHTML).toBe(
+ "<b>bold</b>",
+ );
+ });
+
+ it("returns the string unchanged when there is no match", () => {
+ expect(highlightSubString("react", "xyz")).toEqual({
+ text: "react",
+ match: false,
+ });
+ });
+
+ it("gracefully fails when invalid string is provided", () => {
+ expect(highlightSubString(undefined, "somesub")).toEqual({
+ text: "",
+ match: false,
+ });
+ });
+
+ it("gracefully fails when invalid substring is provided", () => {
+ expect(highlightSubString("somestring", undefined)).toEqual({
+ text: "somestring",
+ match: false,
+ });
+ });
+
+ it("does not match an empty substring", () => {
+ expect(highlightSubString("somestring", "")).toEqual({
+ text: "somestring",
+ match: false,
+ });
+ });
+
+ it("handles special substrings for regex matching", () => {
+ const { text, match } = highlightSubString("somestring\\", "\\");
+ expect(match).toBe(true);
+ expect(render(<>{text}>).container.innerHTML).toBe(
+ "somestring\\",
+ );
+ });
+});
diff --git a/src/utils.ts b/src/utils.ts
index 1c5f8962e..e695253ec 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,19 +1,24 @@
import { NavLink, NavLinkAnchor, NavLinkButton } from "components/Navigation";
-import { isValidElement, ReactNode } from "react";
+import { createElement, isValidElement, ReactNode } from "react";
export const IS_DEV = process.env.NODE_ENV === "development";
/**
- * Find substring and wrap in tag
+ * Find substring and emphasise every match by wrapping it in a element.
+ * The result is a React node, so the input is never parsed as HTML.
* @param {string} str - The string to search
* @param {string} subString - The substring to find
- * @return {Obj} newStr - Object with text and match bool
+ * @return {Obj} Object with the text (as React nodes) and match bool
*/
export const highlightSubString = (
str?: string,
subString?: string,
-): { text: string; match: boolean } => {
- if (typeof str !== "string" || typeof subString !== "string") {
+): { text: ReactNode; match: boolean } => {
+ if (
+ typeof str !== "string" ||
+ typeof subString !== "string" ||
+ subString === ""
+ ) {
return {
text: str || "",
match: false,
@@ -21,15 +26,22 @@ export const highlightSubString = (
}
const escapedSubstring = subString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- const caseInsensitiveRegex = new RegExp(escapedSubstring, "gi");
- const newStr = str.replace(
- caseInsensitiveRegex,
- (match) => `${match}`,
- );
+ // The capture group makes split() keep the matched text, at every odd index.
+ const caseInsensitiveRegex = new RegExp(`(${escapedSubstring})`, "gi");
+ const parts = str.split(caseInsensitiveRegex);
+
+ if (parts.length === 1) {
+ return {
+ text: str,
+ match: false,
+ };
+ }
return {
- text: subString === "" ? str : newStr,
- match: newStr !== str,
+ text: parts.map((part, index) =>
+ index % 2 === 1 ? createElement("strong", { key: index }, part) : part,
+ ),
+ match: true,
};
};