From 2ffdfdd44f21d336d61542ad58c7f210d3b8cc74 Mon Sep 17 00:00:00 2001 From: Ian Tjahjono Date: Tue, 26 May 2026 16:51:03 +0800 Subject: [PATCH 01/12] [BOOKINGSG-9340][IT] Rename style file extensions --- ...tification-banner.styles.tsx => notification-banner.styles.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/notification-banner/{notification-banner.styles.tsx => notification-banner.styles.ts} (100%) diff --git a/src/notification-banner/notification-banner.styles.tsx b/src/notification-banner/notification-banner.styles.ts similarity index 100% rename from src/notification-banner/notification-banner.styles.tsx rename to src/notification-banner/notification-banner.styles.ts From 977a03ca03e0705a889fd1156036dec9d700fe0b Mon Sep 17 00:00:00 2001 From: Ian Tjahjono Date: Tue, 26 May 2026 16:53:52 +0800 Subject: [PATCH 02/12] [BOOKINGSG-9340][IT] Migrate v3 design tokens to v4 tokens --- .../notification-banner.styles.ts | 52 ++++++++----------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/src/notification-banner/notification-banner.styles.ts b/src/notification-banner/notification-banner.styles.ts index 6133335c25..1b957ed15f 100644 --- a/src/notification-banner/notification-banner.styles.ts +++ b/src/notification-banner/notification-banner.styles.ts @@ -2,14 +2,8 @@ import styled, { css } from "styled-components"; import { Layout } from "../layout"; import { ClickableIcon } from "../shared/clickable-icon"; +import { Colour, Font, Motion, Radius, Spacing } from "../theme"; 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 $ @@ -28,19 +22,19 @@ interface ContentStyleProps { // STYLING // ============================================================================= const commonLinkStyle = css` - color: ${V3_Colour["hyperlink-inverse"]}; + color: ${Colour["hyperlink-inverse"]}; svg { - color: ${V3_Colour["icon-primary-inverse"]}; + color: ${Colour["icon-primary-inverse"]}; } &:hover, &:active, &:visited, &:focus { - color: ${V3_Colour["hyperlink-inverse"]}; + color: ${Colour["hyperlink-inverse"]}; svg { - color: ${V3_Colour["icon-primary-inverse"]}; + color: ${Colour["icon-primary-inverse"]}; } } `; @@ -50,9 +44,9 @@ export const Wrapper = styled.div` 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"]}; + transition: all ${Motion["duration-800"]} ${Motion["ease-default"]}; + background: ${Colour["bg-inverse-subtle"]}; + border-radius: ${Radius["none"]}; z-index: 25; cursor: ${(props) => (props.$clickable ? "pointer" : "default")}; `; @@ -62,7 +56,7 @@ export const Container = styled(Layout.Content)``; export const ContentContainer = styled.div` flex: 1; align-items: flex-start; - padding: ${V3_Spacing["spacing-24"]} 0; + padding: ${Spacing["spacing-24"]} 0; `; export const Content = styled.div` @@ -70,20 +64,20 @@ export const Content = styled.div` flex: 1; align-items: flex-start; - ${V3_Font["body-baseline-regular"]} - color: ${V3_Colour["text-inverse"]}; + ${Font["body-baseline-regular"]} + color: ${Colour["text-inverse"]}; p { display: inline-block; } strong { - ${V3_Font["body-baseline-semibold"]} - color: ${V3_Colour["text-inverse"]}; + ${Font["body-baseline-semibold"]} + color: ${Colour["text-inverse"]}; } a { - ${V3_Font["body-baseline-regular"]} + ${Font["body-baseline-regular"]} ${commonLinkStyle} } `; @@ -119,27 +113,27 @@ export const ContentLink = styled(Typography.LinkBL)` `; export const StyledIconButton = styled(ClickableIcon)` - margin-right: -${V3_Spacing["spacing-24"]}; - padding-left: ${V3_Spacing["spacing-16"]}; + margin-right: -${Spacing["spacing-24"]}; + padding-left: ${Spacing["spacing-16"]}; height: max-content; svg { height: 1.5rem; width: 1.5rem; - color: ${V3_Colour["icon-inverse"]}; + color: ${Colour["icon-inverse"]}; } `; export const ActionButton = styled.button` display: flex; align-items: center; - gap: ${V3_Spacing["spacing-4"]}; + gap: ${Spacing["spacing-4"]}; align-self: flex-start; - margin-top: ${V3_Spacing["spacing-8"]}; + margin-top: ${Spacing["spacing-8"]}; border: none; background: transparent; - color: ${V3_Colour["hyperlink-inverse"]}; - ${V3_Font["body-md-semibold"]}; + color: ${Colour["hyperlink-inverse"]}; + ${Font["body-md-semibold"]}; cursor: pointer; `; @@ -157,12 +151,12 @@ export const AccessibleBannerButton = styled.button` export const IconContainer = styled.div` height: 1.5rem; width: 1.5rem; - margin: ${V3_Spacing["spacing-24"]} ${V3_Spacing["spacing-24"]} 0 0; + margin: ${Spacing["spacing-24"]} ${Spacing["spacing-24"]} 0 0; flex-shrink: 0; svg { height: 100%; width: 100%; - color: ${V3_Colour["hyperlink-inverse"]}; + color: ${Colour["hyperlink-inverse"]}; } `; From ac5cc33ceaea4198f77d14e180900b63ce5a6c48 Mon Sep 17 00:00:00 2001 From: Ian Tjahjono Date: Tue, 26 May 2026 17:05:39 +0800 Subject: [PATCH 03/12] [BOOKINGSG-9340][IT] Convert styled interpolations to class names --- .../notification-banner.styles.ts | 62 +++++++++---------- .../notification-banner.tsx | 30 ++++++--- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/notification-banner/notification-banner.styles.ts b/src/notification-banner/notification-banner.styles.ts index 1b957ed15f..9dc20b565b 100644 --- a/src/notification-banner/notification-banner.styles.ts +++ b/src/notification-banner/notification-banner.styles.ts @@ -5,21 +5,13 @@ import { ClickableIcon } from "../shared/clickable-icon"; import { Colour, Font, Motion, Radius, Spacing } from "../theme"; import { Typography } from "../typography"; -// ============================================================================= -// 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; -} +export const tokens = { + contentText: { + maxCollapsedHeight: + "--fds-internal-notificationBanner-contentText-maxCollapsedHeight", + }, +}; -// ============================================================================= -// STYLING // ============================================================================= const commonLinkStyle = css` color: ${Colour["hyperlink-inverse"]}; @@ -39,8 +31,8 @@ const commonLinkStyle = css` } `; -export const Wrapper = styled.div` - position: ${(props) => (props.$sticky ? "sticky" : "relative")}; +export const Wrapper = styled.div` + position: relative; left: 0; top: 0; width: 100%; @@ -48,7 +40,15 @@ export const Wrapper = styled.div` background: ${Colour["bg-inverse-subtle"]}; border-radius: ${Radius["none"]}; z-index: 25; - cursor: ${(props) => (props.$clickable ? "pointer" : "default")}; + cursor: default; + + &.wrapperSticky { + position: sticky; + } + + &.wrapperClickable { + cursor: pointer; + } `; export const Container = styled(Layout.Content)``; @@ -59,7 +59,7 @@ export const ContentContainer = styled.div` padding: ${Spacing["spacing-24"]} 0; `; -export const Content = styled.div` +export const Content = styled.div` display: flex; flex: 1; align-items: flex-start; @@ -88,22 +88,22 @@ export const ContentWrapper = styled.div` flex: 1; `; -export const ContentText = styled.div<{ $maxCollapsedHeight?: number }>` +export const ContentText = styled.div` + ${tokens.contentText.maxCollapsedHeight}: initial; 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}; - `; - } - }} + + &.contentTextCollapsed { + 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 = styled(Typography.LinkBL)` diff --git a/src/notification-banner/notification-banner.tsx b/src/notification-banner/notification-banner.tsx index a596722db8..b96205ee28 100644 --- a/src/notification-banner/notification-banner.tsx +++ b/src/notification-banner/notification-banner.tsx @@ -1,8 +1,10 @@ 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 { formatUnitValue, useApplyStyle } from "../theme"; import { AccessibleBannerButton, ActionButton, @@ -14,6 +16,7 @@ import { ContentWrapper, IconContainer, StyledIconButton, + tokens, Wrapper, } from "./notification-banner.styles"; import type { @@ -33,6 +36,7 @@ export const NBComponent = ({ onClick, actionButton, icon, + className, ...otherProps }: NotificationBannerWithForwardedRefProps) => { // ============================================================================= @@ -42,6 +46,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, { + [tokens.contentText.maxCollapsedHeight]: isCollapsed + ? formatUnitValue(maxCollapsedHeight, "px") + : undefined, + }); // ============================================================================= // EFFECTS @@ -111,11 +125,8 @@ export const NBComponent = ({ maxCollapsedHeight - ? maxCollapsedHeight - : undefined - } + ref={contentTextRef} + className={clsx(isCollapsed && "contentTextCollapsed")} >
{children}
@@ -131,8 +142,11 @@ export const NBComponent = ({ return ( Date: Tue, 26 May 2026 17:25:58 +0800 Subject: [PATCH 04/12] [BOOKINGSG-9340][IT] Convert styled components to linaria --- .../notification-banner.styles.ts | 97 +++++++++---------- .../notification-banner.tsx | 90 ++++++++++------- 2 files changed, 100 insertions(+), 87 deletions(-) diff --git a/src/notification-banner/notification-banner.styles.ts b/src/notification-banner/notification-banner.styles.ts index 9dc20b565b..83cbea7021 100644 --- a/src/notification-banner/notification-banner.styles.ts +++ b/src/notification-banner/notification-banner.styles.ts @@ -1,9 +1,6 @@ -import styled, { css } from "styled-components"; +import { css } from "@linaria/core"; -import { Layout } from "../layout"; -import { ClickableIcon } from "../shared/clickable-icon"; import { Colour, Font, Motion, Radius, Spacing } from "../theme"; -import { Typography } from "../typography"; export const tokens = { contentText: { @@ -13,25 +10,9 @@ export const tokens = { }; // ============================================================================= -const commonLinkStyle = css` - color: ${Colour["hyperlink-inverse"]}; - - svg { - color: ${Colour["icon-primary-inverse"]}; - } - - &:hover, - &:active, - &:visited, - &:focus { - color: ${Colour["hyperlink-inverse"]}; - svg { - color: ${Colour["icon-primary-inverse"]}; - } - } -`; - -export const Wrapper = styled.div` +// STYLING +// ============================================================================= +export const wrapper = css` position: relative; left: 0; top: 0; @@ -41,25 +22,41 @@ export const Wrapper = styled.div` border-radius: ${Radius["none"]}; z-index: 25; cursor: default; +`; - &.wrapperSticky { - position: sticky; - } - - &.wrapperClickable { - cursor: pointer; - } +export const wrapperSticky = css` + position: sticky; `; -export const Container = styled(Layout.Content)``; +export const wrapperClickable = css` + cursor: pointer; +`; -export const ContentContainer = styled.div` +export const contentContainer = css` flex: 1; align-items: flex-start; padding: ${Spacing["spacing-24"]} 0; `; -export const Content = styled.div` +const commonLinkStyle = css` + color: ${Colour["hyperlink-inverse"]}; + + svg { + color: ${Colour["icon-primary-inverse"]}; + } + + &:hover, + &:active, + &:visited, + &:focus { + color: ${Colour["hyperlink-inverse"]}; + svg { + color: ${Colour["icon-primary-inverse"]}; + } + } +`; + +export const content = css` display: flex; flex: 1; align-items: flex-start; @@ -82,40 +79,36 @@ export const Content = styled.div` } `; -export const ContentWrapper = styled.div` +export const contentWrapper = css` display: flex; flex-direction: column; flex: 1; `; -export const ContentText = styled.div` +export const contentText = css` ${tokens.contentText.maxCollapsedHeight}: initial; flex: 1; word-wrap: break-word; overflow-wrap: break-word; +`; - &.contentTextCollapsed { - 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 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 = styled(Typography.LinkBL)` +export const contentLink = css` position: relative; - ${commonLinkStyle} `; -export const StyledIconButton = styled(ClickableIcon)` - margin-right: -${Spacing["spacing-24"]}; +export const styledIconButton = css` + margin-right: calc(${Spacing["spacing-24"]} * -1); padding-left: ${Spacing["spacing-16"]}; height: max-content; + svg { height: 1.5rem; width: 1.5rem; @@ -123,7 +116,7 @@ export const StyledIconButton = styled(ClickableIcon)` } `; -export const ActionButton = styled.button` +export const actionButton = css` display: flex; align-items: center; gap: ${Spacing["spacing-4"]}; @@ -138,7 +131,7 @@ export const ActionButton = styled.button` cursor: pointer; `; -export const AccessibleBannerButton = styled.button` +export const accessibleBannerButton = css` clip: rect(0 0 0 0); clip-path: inset(50%); height: 1px; @@ -148,7 +141,7 @@ export const AccessibleBannerButton = styled.button` width: 1px; `; -export const IconContainer = styled.div` +export const iconContainer = css` height: 1.5rem; width: 1.5rem; margin: ${Spacing["spacing-24"]} ${Spacing["spacing-24"]} 0 0; diff --git a/src/notification-banner/notification-banner.tsx b/src/notification-banner/notification-banner.tsx index b96205ee28..110465461a 100644 --- a/src/notification-banner/notification-banner.tsx +++ b/src/notification-banner/notification-banner.tsx @@ -4,21 +4,11 @@ import type { NamedExoticComponent } from "react"; import React, { useEffect, useRef, useState } from "react"; import { useResizeDetector } from "react-resize-detector"; +import { Layout } from "../layout"; +import { ClickableIcon } from "../shared/clickable-icon"; import { formatUnitValue, useApplyStyle } from "../theme"; -import { - AccessibleBannerButton, - ActionButton, - Container, - Content, - ContentContainer, - ContentLink as NBLink, - ContentText, - ContentWrapper, - IconContainer, - StyledIconButton, - tokens, - Wrapper, -} from "./notification-banner.styles"; +import { Typography } from "../typography"; +import * as styles from "./notification-banner.styles"; import type { NotificationBannerProps, NotificationBannerWithForwardedRefProps, @@ -52,7 +42,7 @@ export const NBComponent = ({ maxCollapsedHeight && contentHeight > maxCollapsedHeight; useApplyStyle(contentTextRef, { - [tokens.contentText.maxCollapsedHeight]: isCollapsed + [styles.tokens.contentText.maxCollapsedHeight]: isCollapsed ? formatUnitValue(maxCollapsedHeight, "px") : undefined, }); @@ -91,7 +81,8 @@ export const NBComponent = ({ if (!isVisible) return null; const renderDismissButton = () => ( - - + ); const renderActionButton = () => { if (!actionButton) return null; return ( - {actionButton.children} - + ); }; const renderContent = () => ( - - - +
+
{children}
- +
{renderActionButton()} - - +
+ ); const renderAccessibleBannerButton = () => ( - +