diff --git a/e2e/nextjs-app/src/app/components/notification-banner/all-variants.e2e.tsx b/e2e/nextjs-app/src/app/components/notification-banner/all-variants.e2e.tsx
new file mode 100644
index 0000000000..ac92831c3c
--- /dev/null
+++ b/e2e/nextjs-app/src/app/components/notification-banner/all-variants.e2e.tsx
@@ -0,0 +1,27 @@
+"use client";
+import { PlaceholderIcon } from "@lifesg/react-icons/placeholder";
+import { NotificationBanner } from "@lifesg/react-design-system/notification-banner";
+
+export default function Story() {
+ return (
+
+
+ This is a default notification banner
+
+
+ }
+ >
+ This is a notification banner with an icon
+
+
+
+ This is a non-dismissible notification banner
+
+
+ );
+}
diff --git a/e2e/nextjs-app/src/app/components/notification-banner/custom-content.e2e.tsx b/e2e/nextjs-app/src/app/components/notification-banner/custom-content.e2e.tsx
new file mode 100644
index 0000000000..2f465fb0c7
--- /dev/null
+++ b/e2e/nextjs-app/src/app/components/notification-banner/custom-content.e2e.tsx
@@ -0,0 +1,20 @@
+"use client";
+import { GearFillIcon } from "@lifesg/react-icons/gear-fill";
+import { NotificationBanner } from "@lifesg/react-design-system/notification-banner";
+import styles from "./notification-banner.module.css";
+
+export default function Story() {
+ return (
+
+
+
+
+ Custom content
+
+
+
+ );
+}
diff --git a/e2e/nextjs-app/src/app/components/notification-banner/long-content.e2e.tsx b/e2e/nextjs-app/src/app/components/notification-banner/long-content.e2e.tsx
new file mode 100644
index 0000000000..5b164854ea
--- /dev/null
+++ b/e2e/nextjs-app/src/app/components/notification-banner/long-content.e2e.tsx
@@ -0,0 +1,43 @@
+"use client";
+import { NotificationBanner } from "@lifesg/react-design-system/notification-banner";
+import { ArrowRightIcon } from "@lifesg/react-icons/arrow-right";
+
+export default function Story() {
+ return (
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
+ in reprehenderit in voluptate velit esse cillum dolore eu fugiat
+ nulla pariatur. Excepteur sint occaecat cupidatat non proident,
+ sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+
+ View more
+
+ >
+ ),
+ }}
+ >
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
+ eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris
+ nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
+ in reprehenderit in voluptate velit esse cillum dolore eu fugiat
+ nulla pariatur. Excepteur sint occaecat cupidatat non proident,
+ sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+ );
+}
diff --git a/e2e/nextjs-app/src/app/components/notification-banner/non-sticky.e2e.tsx b/e2e/nextjs-app/src/app/components/notification-banner/non-sticky.e2e.tsx
new file mode 100644
index 0000000000..b007cbc558
--- /dev/null
+++ b/e2e/nextjs-app/src/app/components/notification-banner/non-sticky.e2e.tsx
@@ -0,0 +1,25 @@
+"use client";
+import { NotificationBanner } from "@lifesg/react-design-system/notification-banner";
+import styles from "./notification-banner.module.css";
+
+export default function Story() {
+ return (
+
+
+ This is a non-sticky notification banner.
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
+ do eiusmod tempor incididunt ut labore et dolore magna
+ aliqua. Ut enim ad minim veniam, quis nostrud exercitation
+ ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit
+ esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia
+ deserunt mollit anim id est laborum.
+
+
+
+ );
+}
diff --git a/e2e/nextjs-app/src/app/components/notification-banner/notification-banner.module.css b/e2e/nextjs-app/src/app/components/notification-banner/notification-banner.module.css
new file mode 100644
index 0000000000..61ac268d37
--- /dev/null
+++ b/e2e/nextjs-app/src/app/components/notification-banner/notification-banner.module.css
@@ -0,0 +1,15 @@
+.custom {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ width: 100%;
+ padding: 1rem;
+ color: black;
+ background: yellow;
+}
+
+.sticky {
+ height: 200vh;
+ background-color: teal;
+ border: solid 10px crimson;
+}
diff --git a/e2e/nextjs-app/src/app/components/notification-banner/sticky.e2e.tsx b/e2e/nextjs-app/src/app/components/notification-banner/sticky.e2e.tsx
new file mode 100644
index 0000000000..01cd0ad090
--- /dev/null
+++ b/e2e/nextjs-app/src/app/components/notification-banner/sticky.e2e.tsx
@@ -0,0 +1,25 @@
+"use client";
+import { NotificationBanner } from "@lifesg/react-design-system/notification-banner";
+import styles from "./notification-banner.module.css";
+
+export default function Story() {
+ return (
+
+
+ This is a sticky notification banner.
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
+ do eiusmod tempor incididunt ut labore et dolore magna
+ aliqua. Ut enim ad minim veniam, quis nostrud exercitation
+ ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Duis aute irure dolor in reprehenderit in voluptate velit
+ esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+ occaecat cupidatat non proident, sunt in culpa qui officia
+ deserunt mollit anim id est laborum.
+
+
+
+ );
+}
diff --git a/e2e/nextjs-app/src/app/components/notification-banner/text-styling.e2e.tsx b/e2e/nextjs-app/src/app/components/notification-banner/text-styling.e2e.tsx
new file mode 100644
index 0000000000..a2c6dc4364
--- /dev/null
+++ b/e2e/nextjs-app/src/app/components/notification-banner/text-styling.e2e.tsx
@@ -0,0 +1,19 @@
+"use client";
+import { NotificationBanner } from "@lifesg/react-design-system/notification-banner";
+
+export default function Story() {
+ return (
+
+
+ This banner has bold text , italic text
+ , anchor tag for normal{" "}
+ link , and Link component
+ for{" "}
+
+ external link
+
+ .
+
+
+ );
+}
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-All-variants--mount.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-All-variants--mount.png
new file mode 100644
index 0000000000..50a2cd76f9
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-All-variants--mount.png differ
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-All-variants-dark-mode---mount.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-All-variants-dark-mode---mount.png
new file mode 100644
index 0000000000..0986b17d43
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-All-variants-dark-mode---mount.png differ
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Custom-content--mount.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Custom-content--mount.png
new file mode 100644
index 0000000000..a7639adc84
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Custom-content--mount.png differ
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Long-content--mount.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Long-content--mount.png
new file mode 100644
index 0000000000..8ca43de4d1
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Long-content--mount.png differ
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Mobile--mount.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Mobile--mount.png
new file mode 100644
index 0000000000..89343ff223
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Mobile--mount.png differ
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Sticky-banner-behaviour--after-scroll.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Sticky-banner-behaviour--after-scroll.png
new file mode 100644
index 0000000000..7bb71f41ca
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Sticky-banner-behaviour--after-scroll.png differ
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Sticky-banner-behaviour--mount.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Sticky-banner-behaviour--mount.png
new file mode 100644
index 0000000000..64368e3133
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Sticky-banner-behaviour--mount.png differ
diff --git a/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Text-styling--mount.png b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Text-styling--mount.png
new file mode 100644
index 0000000000..2ff04eff40
Binary files /dev/null and b/e2e/tests/components/notification-banner/__screenshots__/chromium/NotificationBanner-Text-styling--mount.png differ
diff --git a/e2e/tests/components/notification-banner/notification-banner.e2e.spec.ts b/e2e/tests/components/notification-banner/notification-banner.e2e.spec.ts
new file mode 100644
index 0000000000..d38a0d3b34
--- /dev/null
+++ b/e2e/tests/components/notification-banner/notification-banner.e2e.spec.ts
@@ -0,0 +1,136 @@
+import { test as base, expect, Locator, Page } from "@playwright/test";
+import { AbstractStoryPage, compareScreenshot } from "../../utils";
+
+class StoryPage extends AbstractStoryPage {
+ protected readonly component = "notification-banner";
+
+ public readonly locators: {
+ bannerNonSticky: Locator;
+ bannerSticky: Locator;
+ };
+
+ constructor(page: Page) {
+ super(page);
+
+ this.locators = {
+ bannerNonSticky: page.getByTestId("banner-non-sticky"),
+ bannerSticky: page.getByTestId("banner-sticky"),
+ };
+ }
+}
+
+const test = base.extend<{ story: StoryPage }>({
+ story: async ({ page }, use) => {
+ const story = new StoryPage(page);
+ await use(story);
+ },
+});
+
+test.describe("NotificationBanner", () => {
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("all-variants");
+ });
+
+ test("All variants", async ({ story }) => {
+ await compareScreenshot(story, "mount");
+ });
+ });
+
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("all-variants", { mode: "dark" });
+ });
+
+ test("All variants (dark mode)", async ({ story }) => {
+ await compareScreenshot(story, "mount");
+ });
+ });
+
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("all-variants", { size: "mobile" });
+ });
+
+ test("Mobile", async ({ story }) => {
+ await compareScreenshot(story, "mount");
+ });
+ });
+
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("long-content");
+ });
+
+ test("Long content", async ({ story }) => {
+ await compareScreenshot(story, "mount");
+ });
+ });
+
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("custom-content");
+ });
+
+ test("Custom content", async ({ story }) => {
+ await compareScreenshot(story, "mount");
+ });
+ });
+
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("text-styling");
+ });
+
+ test("Text styling", async ({ story }) => {
+ await compareScreenshot(story, "mount");
+ });
+ });
+
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("non-sticky");
+ });
+
+ test("Non-sticky banner scrolls out of view", async ({ story }) => {
+ await test.step("Verify non-sticky banner is visible initially", async () => {
+ await expect(story.locators.bannerNonSticky).toBeInViewport();
+ });
+
+ await test.step("Scroll down the page", async () => {
+ await story.page.mouse.wheel(0, 100);
+ await story.page.waitForTimeout(300);
+ });
+
+ await test.step("Verify non-sticky banner scrolled out of view", async () => {
+ await expect(
+ story.locators.bannerNonSticky
+ ).not.toBeInViewport();
+ });
+ });
+ });
+
+ test.describe(() => {
+ test.beforeEach(async ({ story }) => {
+ await story.init("sticky");
+ });
+
+ test("Sticky banner behaviour", async ({ story }) => {
+ await test.step("Verify sticky banner is visible initially", async () => {
+ await expect(story.locators.bannerSticky).toBeInViewport();
+ await compareScreenshot(story, "mount", { fullscreen: true });
+ });
+
+ await test.step("Scroll down the page", async () => {
+ await story.scrollToEnd({ scrollTarget: story.layout });
+ });
+
+ await test.step("Verify sticky banner remains in view", async () => {
+ await expect(story.locators.bannerSticky).toBeInViewport();
+ await compareScreenshot(story, "after-scroll", {
+ fullscreen: true,
+ });
+ });
+ });
+ });
+});
diff --git a/src/notification-banner/notification-banner.styles.ts b/src/notification-banner/notification-banner.styles.ts
new file mode 100644
index 0000000000..6cd31f330a
--- /dev/null
+++ b/src/notification-banner/notification-banner.styles.ts
@@ -0,0 +1,145 @@
+import { css } from "@linaria/core";
+
+import { Colour, Font, Motion, Radius, Spacing } from "../theme";
+
+export const tokens = {
+ contentText: {
+ maxCollapsedHeight:
+ "--fds-internal-notificationBanner-contentText-maxCollapsedHeight",
+ },
+};
+
+const linkStyle = `
+ color: ${Colour["hyperlink-inverse"]};
+
+ svg {
+ color: ${Colour["icon-primary-inverse"]};
+ }
+
+ &:hover,
+ &:active,
+ &:visited,
+ &:focus {
+ color: ${Colour["hyperlink-inverse"]};
+ svg {
+ color: ${Colour["icon-primary-inverse"]};
+ }
+ }
+`;
+
+// =============================================================================
+// STYLING
+// =============================================================================
+export const wrapper = css`
+ position: relative;
+ left: 0;
+ top: 0;
+ width: 100%;
+ transition: all ${Motion["duration-800"]} ${Motion["ease-default"]};
+ background: ${Colour["bg-inverse-subtle"]};
+ border-radius: ${Radius["none"]};
+ z-index: 25;
+ cursor: default;
+`;
+
+export const wrapperSticky = css`
+ position: sticky;
+`;
+
+export const wrapperClickable = css`
+ cursor: pointer;
+`;
+
+export const contentContainer = css`
+ flex: 1;
+ align-items: flex-start;
+ padding: ${Spacing["spacing-24"]} 0;
+`;
+
+export const content = css`
+ display: flex;
+ flex: 1;
+ align-items: flex-start;
+
+ ${Font["body-baseline-regular"]}
+ color: ${Colour["text-inverse"]};
+
+ p {
+ display: inline-block;
+ }
+
+ strong {
+ ${Font["body-baseline-semibold"]}
+ color: ${Colour["text-inverse"]};
+ }
+
+ a {
+ ${Font["body-baseline-regular"]}
+ ${linkStyle}
+ }
+`;
+
+export const contentWrapper = css`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+`;
+
+export const contentText = css`
+ ${tokens.contentText.maxCollapsedHeight}: initial;
+ flex: 1;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+`;
+
+export const contentTextCollapsed = css`
+ max-height: var(${tokens.contentText.maxCollapsedHeight});
+ overflow: hidden;
+ -webkit-mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
+ mask-image: linear-gradient(to bottom, black 50%, transparent 100%);
+`;
+
+export const contentLink = css`
+ position: relative;
+ ${linkStyle}
+`;
+
+export const dismissButton = css`
+ margin-right: calc(${Spacing["spacing-24"]} * -1);
+ padding-left: ${Spacing["spacing-16"]};
+ height: max-content;
+
+ svg {
+ height: 1.5rem;
+ width: 1.5rem;
+ color: ${Colour["icon-inverse"]};
+ }
+`;
+
+export const actionButton = css`
+ display: flex;
+ align-items: center;
+ gap: ${Spacing["spacing-4"]};
+ align-self: flex-start;
+ margin-top: ${Spacing["spacing-8"]};
+
+ border: none;
+ background: transparent;
+ color: ${Colour["hyperlink-inverse"]};
+ ${Font["body-md-semibold"]};
+
+ cursor: pointer;
+`;
+
+export const iconContainer = css`
+ height: 1.5rem;
+ width: 1.5rem;
+ margin: ${Spacing["spacing-24"]} ${Spacing["spacing-24"]} 0 0;
+ flex-shrink: 0;
+
+ svg {
+ height: 100%;
+ width: 100%;
+ color: ${Colour["hyperlink-inverse"]};
+ }
+`;
diff --git a/src/notification-banner/notification-banner.styles.tsx b/src/notification-banner/notification-banner.styles.tsx
deleted file mode 100644
index 6133335c25..0000000000
--- a/src/notification-banner/notification-banner.styles.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import styled, { css } from "styled-components";
-
-import { Layout } from "../layout";
-import { ClickableIcon } from "../shared/clickable-icon";
-import { Typography } from "../typography";
-import {
- V3_Colour,
- V3_Font,
- V3_Motion,
- V3_Radius,
- V3_Spacing,
-} from "../v3_theme";
-
-// =============================================================================
-// STYLE INTERFACES, transient props are denoted with $
-// See more https://styled-components.com/docs/api#transient-props
-// =============================================================================
-interface WrapperStyleProps {
- $sticky: boolean;
- $clickable: boolean;
-}
-
-interface ContentStyleProps {
- $maxCollapsedHeight?: number;
-}
-
-// =============================================================================
-// STYLING
-// =============================================================================
-const commonLinkStyle = css`
- color: ${V3_Colour["hyperlink-inverse"]};
-
- svg {
- color: ${V3_Colour["icon-primary-inverse"]};
- }
-
- &:hover,
- &:active,
- &:visited,
- &:focus {
- color: ${V3_Colour["hyperlink-inverse"]};
- svg {
- color: ${V3_Colour["icon-primary-inverse"]};
- }
- }
-`;
-
-export const Wrapper = styled.div`
- position: ${(props) => (props.$sticky ? "sticky" : "relative")};
- left: 0;
- top: 0;
- width: 100%;
- transition: all ${V3_Motion["duration-800"]} ${V3_Motion["ease-default"]};
- background: ${V3_Colour["bg-inverse-subtle"]};
- border-radius: ${V3_Radius["none"]};
- z-index: 25;
- cursor: ${(props) => (props.$clickable ? "pointer" : "default")};
-`;
-
-export const Container = styled(Layout.Content)``;
-
-export const ContentContainer = styled.div`
- flex: 1;
- align-items: flex-start;
- padding: ${V3_Spacing["spacing-24"]} 0;
-`;
-
-export const Content = styled.div`
- display: flex;
- flex: 1;
- align-items: flex-start;
-
- ${V3_Font["body-baseline-regular"]}
- color: ${V3_Colour["text-inverse"]};
-
- p {
- display: inline-block;
- }
-
- strong {
- ${V3_Font["body-baseline-semibold"]}
- color: ${V3_Colour["text-inverse"]};
- }
-
- a {
- ${V3_Font["body-baseline-regular"]}
- ${commonLinkStyle}
- }
-`;
-
-export const ContentWrapper = styled.div`
- display: flex;
- flex-direction: column;
- flex: 1;
-`;
-
-export const ContentText = styled.div<{ $maxCollapsedHeight?: number }>`
- flex: 1;
- word-wrap: break-word;
- overflow-wrap: break-word;
- ${(props) => {
- const gradient =
- "linear-gradient(to bottom, black 50%, transparent 100%)";
- if (props.$maxCollapsedHeight) {
- return css`
- max-height: ${props.$maxCollapsedHeight}px;
- overflow: hidden;
- -webkit-mask-image: ${gradient};
- mask-image: ${gradient};
- `;
- }
- }}
-`;
-
-export const ContentLink = styled(Typography.LinkBL)`
- position: relative;
-
- ${commonLinkStyle}
-`;
-
-export const StyledIconButton = styled(ClickableIcon)`
- margin-right: -${V3_Spacing["spacing-24"]};
- padding-left: ${V3_Spacing["spacing-16"]};
- height: max-content;
- svg {
- height: 1.5rem;
- width: 1.5rem;
- color: ${V3_Colour["icon-inverse"]};
- }
-`;
-
-export const ActionButton = styled.button`
- display: flex;
- align-items: center;
- gap: ${V3_Spacing["spacing-4"]};
- align-self: flex-start;
- margin-top: ${V3_Spacing["spacing-8"]};
-
- border: none;
- background: transparent;
- color: ${V3_Colour["hyperlink-inverse"]};
- ${V3_Font["body-md-semibold"]};
-
- cursor: pointer;
-`;
-
-export const AccessibleBannerButton = styled.button`
- clip: rect(0 0 0 0);
- clip-path: inset(50%);
- height: 1px;
- overflow: hidden;
- position: absolute;
- white-space: nowrap;
- width: 1px;
-`;
-
-export const IconContainer = styled.div`
- height: 1.5rem;
- width: 1.5rem;
- margin: ${V3_Spacing["spacing-24"]} ${V3_Spacing["spacing-24"]} 0 0;
- flex-shrink: 0;
-
- svg {
- height: 100%;
- width: 100%;
- color: ${V3_Colour["hyperlink-inverse"]};
- }
-`;
diff --git a/src/notification-banner/notification-banner.tsx b/src/notification-banner/notification-banner.tsx
index a596722db8..831fa18ae7 100644
--- a/src/notification-banner/notification-banner.tsx
+++ b/src/notification-banner/notification-banner.tsx
@@ -1,21 +1,15 @@
import { CrossIcon } from "@lifesg/react-icons";
+import clsx from "clsx";
import type { NamedExoticComponent } from "react";
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import { useResizeDetector } from "react-resize-detector";
-import {
- AccessibleBannerButton,
- ActionButton,
- Container,
- Content,
- ContentContainer,
- ContentLink as NBLink,
- ContentText,
- ContentWrapper,
- IconContainer,
- StyledIconButton,
- Wrapper,
-} from "./notification-banner.styles";
+import { Layout } from "../layout";
+import { VisuallyHidden } from "../shared/accessibility";
+import { ClickableIcon } from "../shared/clickable-icon";
+import { formatUnitValue, useApplyStyle } from "../theme";
+import { Typography } from "../typography";
+import * as styles from "./notification-banner.styles";
import type {
NotificationBannerProps,
NotificationBannerWithForwardedRefProps,
@@ -33,6 +27,7 @@ export const NBComponent = ({
onClick,
actionButton,
icon,
+ className,
...otherProps
}: NotificationBannerWithForwardedRefProps) => {
// =============================================================================
@@ -42,6 +37,16 @@ export const NBComponent = ({
const [isVisible, setVisible] = useState(visible);
const { height: contentHeight = 0, ref: contentRef } = useResizeDetector();
+ const contentTextRef = useRef(null);
+
+ const isCollapsed =
+ maxCollapsedHeight && contentHeight > maxCollapsedHeight;
+
+ useApplyStyle(contentTextRef, {
+ [styles.tokens.contentText.maxCollapsedHeight]: isCollapsed
+ ? formatUnitValue(maxCollapsedHeight, "px")
+ : undefined,
+ });
// =============================================================================
// EFFECTS
@@ -77,7 +82,8 @@ export const NBComponent = ({
if (!isVisible) return null;
const renderDismissButton = () => (
-
-
+
);
const renderActionButton = () => {
if (!actionButton) return null;
return (
-
{actionButton.children}
-
+
);
};
const renderContent = () => (
-
-
- maxCollapsedHeight
- ? maxCollapsedHeight
- : undefined
- }
+
+
+
{renderActionButton()}
-
-
+
+
);
const renderAccessibleBannerButton = () => (
-
+
+
+
);
return (
-
-
- {icon && {icon} }
- {renderContent()}
+
+ {icon && (
+
+ {icon}
+
+ )}
+ {renderContent()}
{dismissible && renderDismissButton()}
-
+
{onClick && renderAccessibleBannerButton()}
-
+
);
};
@@ -157,6 +177,20 @@ const NBWithRef = (
return ;
};
+const NBLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps
+>(function NotificationBannerLink(props, ref) {
+ return (
+
+ );
+});
+(NBLink as NamedExoticComponent).displayName = "NotificationBanner.Link";
+
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
diff --git a/tests/notification-banner/notification-banner.spec.tsx b/tests/notification-banner/notification-banner.spec.tsx
index 9925fdd41d..6b833cf7f7 100644
--- a/tests/notification-banner/notification-banner.spec.tsx
+++ b/tests/notification-banner/notification-banner.spec.tsx
@@ -1,4 +1,4 @@
-import { render, screen } from "@testing-library/react";
+import { fireEvent, render, screen } from "@testing-library/react";
import {
NotificationBanner,
withNotificationBanner,
@@ -46,6 +46,44 @@ describe("NotificationBanner", () => {
).not.toBeInTheDocument();
});
+ it("should call onDismiss when dismiss button is clicked", () => {
+ const mockOnDismiss = jest.fn();
+
+ render(
+
+ {DEFAULT_TEXT}
+
+ );
+
+ const dismissButton = screen.getByTestId(
+ "notification-banner-dismiss-button"
+ );
+ fireEvent.click(dismissButton);
+
+ expect(mockOnDismiss).toHaveBeenCalledTimes(1);
+ });
+
+ it("should call onClick when banner is clicked", () => {
+ const mockOnClick = jest.fn();
+
+ render(
+
+ {DEFAULT_TEXT}
+
+ );
+
+ const banner = screen.getByTestId("notification-banner");
+ fireEvent.click(banner);
+
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
it("should sanitise the content", () => {
const HOCElement = withNotificationBanner([
{