From 112b1d4b38b37d6be230050dd6e4008843b02353 Mon Sep 17 00:00:00 2001 From: Nghi To Date: Mon, 13 Apr 2026 13:58:45 +0700 Subject: [PATCH 01/16] [BOOKINGSG-9121][Nghi] rename style files --- .../toggle-icon/{toggle-icon.styles.tsx => toggle-icon.styles.ts} | 0 src/toggle/{toggle.styles.tsx => toggle.styles.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/shared/toggle-icon/{toggle-icon.styles.tsx => toggle-icon.styles.ts} (100%) rename src/toggle/{toggle.styles.tsx => toggle.styles.ts} (100%) diff --git a/src/shared/toggle-icon/toggle-icon.styles.tsx b/src/shared/toggle-icon/toggle-icon.styles.ts similarity index 100% rename from src/shared/toggle-icon/toggle-icon.styles.tsx rename to src/shared/toggle-icon/toggle-icon.styles.ts diff --git a/src/toggle/toggle.styles.tsx b/src/toggle/toggle.styles.ts similarity index 100% rename from src/toggle/toggle.styles.tsx rename to src/toggle/toggle.styles.ts From a53c721c25f2323c062c22645e3f0e49b98faae8 Mon Sep 17 00:00:00 2001 From: Nghi To Date: Mon, 13 Apr 2026 14:43:32 +0700 Subject: [PATCH 02/16] [BOOKINGSG-9121][Nghi] migrate tokens --- src/shared/toggle-icon/toggle-icon.styles.ts | 10 +- src/toggle/toggle.styles.ts | 112 +++++++++---------- 2 files changed, 59 insertions(+), 63 deletions(-) diff --git a/src/shared/toggle-icon/toggle-icon.styles.ts b/src/shared/toggle-icon/toggle-icon.styles.ts index 6951ea73e8..6f4d44749a 100644 --- a/src/shared/toggle-icon/toggle-icon.styles.ts +++ b/src/shared/toggle-icon/toggle-icon.styles.ts @@ -1,6 +1,6 @@ import styled, { css } from "styled-components"; -import { V3_Colour } from "../../v3_theme"; +import { Colour } from "../../theme"; interface StyleProps { $active?: boolean; @@ -25,23 +25,23 @@ export const Wrapper = styled.div` if (props.$disabled) { if (props.$active) { return css` - color: ${V3_Colour["icon-selected-disabled"]}; + color: ${Colour["icon-selected-disabled"]}; `; } else { return css` - color: ${V3_Colour["icon-disabled-subtle"]}; + color: ${Colour["icon-disabled-subtle"]}; `; } } if (props.$active) { return css` - color: ${V3_Colour["icon-selected"]}; + color: ${Colour["icon-selected"]}; `; } return css` - color: ${V3_Colour["icon-subtle"]}; + color: ${Colour["icon-subtle"]}; `; }}; `; diff --git a/src/toggle/toggle.styles.ts b/src/toggle/toggle.styles.ts index 337eea2a1e..c7ce2c0726 100644 --- a/src/toggle/toggle.styles.ts +++ b/src/toggle/toggle.styles.ts @@ -3,8 +3,8 @@ import styled, { css } from "styled-components"; import { Alert } from "../alert"; import { ToggleIcon } from "../shared/toggle-icon/toggle-icon"; import { TextList } from "../text-list"; +import { Colour, Font, MediaQuery, Radius } from "../theme"; import { Typography } from "../typography"; -import { V3_Colour, V3_Font, V3_MediaQuery, V3_Radius } from "../v3_theme"; import type { ToggleStyleType } from "./types"; // ============================================================================= @@ -47,16 +47,16 @@ export const Container = styled.div` position: relative; display: inline-flex; min-width: 10.375rem; - border-radius: ${V3_Radius["sm"]}; + border-radius: ${Radius["sm"]}; border-width: 1px; border-style: solid; overflow: hidden; flex-direction: column; height: fit-content; - background: ${V3_Colour.bg}; + background: ${Colour.bg}; &:has(input:focus-visible) { - outline: 2px solid ${V3_Colour["focus-ring"]}; + outline: 2px solid ${Colour["focus-ring"]}; outline-offset: 0; } @@ -84,15 +84,15 @@ export const Container = styled.div` if (props.$error) { if (props.$disabled) { return css` - border-color: ${V3_Colour["border-error"]}; + border-color: ${Colour["border-error"]}; `; } else { return css` - border-color: ${V3_Colour["border-error"]}; + border-color: ${Colour["border-error"]}; &:has(${HeaderContainer}:hover) { @media (pointer: fine) { - background: ${V3_Colour["bg-hover-subtle"]}; + background: ${Colour["bg-hover-subtle"]}; } } `; @@ -103,7 +103,7 @@ export const Container = styled.div` if (props.$selected) { return css` border: none; - background: ${V3_Colour["bg-selected-disabled"]}; + background: ${Colour["bg-selected-disabled"]}; `; } else { return css` @@ -115,18 +115,18 @@ export const Container = styled.div` if (props.$selected) { return css` border: none; - background: ${V3_Colour["bg-selected"]}; + background: ${Colour["bg-selected"]}; &:has(${HeaderContainer}:hover) { @media (pointer: fine) { - background: ${V3_Colour["bg-selected-hover"]}; + background: ${Colour["bg-selected-hover"]}; & ${TextContainer} { - color: ${V3_Colour["text-selected-hover"]}; + color: ${Colour["text-selected-hover"]}; } & ${StyledToggleIcon} { - color: ${V3_Colour["icon-selected-hover"]}; + color: ${Colour["icon-selected-hover"]}; } } } @@ -138,7 +138,7 @@ export const Container = styled.div` &:has(${HeaderContainer}:hover) { @media (pointer: fine) { - background: ${V3_Colour["bg-hover-subtle"]}; + background: ${Colour["bg-hover-subtle"]}; } } `; @@ -148,15 +148,15 @@ export const Container = styled.div` if (props.$error) { if (props.$disabled) { return css` - border-color: ${V3_Colour["border-error"]}; + border-color: ${Colour["border-error"]}; `; } else { return css` - border-color: ${V3_Colour["border-error"]}; + border-color: ${Colour["border-error"]}; &:has(${HeaderContainer}:hover) { @media (pointer: fine) { - background: ${V3_Colour["bg-hover-subtle"]}; + background: ${Colour["bg-hover-subtle"]}; } } `; @@ -166,34 +166,32 @@ export const Container = styled.div` if (props.$disabled) { if (props.$selected) { return css` - border-color: ${V3_Colour[ - "border-selected-disabled" - ]}; - background: ${V3_Colour["bg-selected-disabled"]}; + border-color: ${Colour["border-selected-disabled"]}; + background: ${Colour["bg-selected-disabled"]}; `; } else { return css` - border-color: ${V3_Colour["border-disabled"]}; - background: ${V3_Colour["bg-disabled"]}; + border-color: ${Colour["border-disabled"]}; + background: ${Colour["bg-disabled"]}; `; } } if (props.$selected) { return css` - border-color: ${V3_Colour["border-selected"]}; - background: ${V3_Colour["bg-selected"]}; + border-color: ${Colour["border-selected"]}; + background: ${Colour["bg-selected"]}; &:has(${HeaderContainer}:hover) { @media (pointer: fine) { - background: ${V3_Colour["bg-selected-hover"]}; + background: ${Colour["bg-selected-hover"]}; & ${TextContainer} { - color: ${V3_Colour["text-selected-hover"]}; + color: ${Colour["text-selected-hover"]}; } & ${StyledToggleIcon} { - color: ${V3_Colour["icon-selected-hover"]}; + color: ${Colour["icon-selected-hover"]}; } } } @@ -201,11 +199,11 @@ export const Container = styled.div` } return css` - border-color: ${V3_Colour.border}; + border-color: ${Colour.border}; &:has(${HeaderContainer}:hover) { @media (pointer: fine) { - background: ${V3_Colour["bg-hover-subtle"]}; + background: ${Colour["bg-hover-subtle"]}; } } `; @@ -243,23 +241,23 @@ export const TextContainer = styled.div` if (props.$disabled) { if (props.$selected) { return css` - color: ${V3_Colour["text-selected-disabled"]}; + color: ${Colour["text-selected-disabled"]}; `; } else { return css` - color: ${V3_Colour["text-disabled"]}; + color: ${Colour["text-disabled"]}; `; } } if (props.$selected) { return css` - color: ${V3_Colour["text-selected"]}; + color: ${Colour["text-selected"]}; `; } return css` - color: ${V3_Colour.text}; + color: ${Colour.text}; `; }} `; @@ -267,24 +265,24 @@ export const TextContainer = styled.div` export const Label = styled.label` ${(props) => props.$selected - ? V3_Font["body-baseline-semibold"] - : V3_Font["body-baseline-regular"]}; + ? Font["body-baseline-semibold"] + : Font["body-baseline-regular"]}; overflow: hidden; display: -webkit-box; text-overflow: ellipsis; -webkit-box-orient: vertical; overflow-wrap: break-word; -webkit-line-clamp: ${(props) => props.$maxLines?.desktop ?? "none"}; - ${V3_MediaQuery.MaxWidth.lg} { + ${MediaQuery.MaxWidth.lg} { -webkit-line-clamp: ${(props) => props.$maxLines?.tablet ?? "none"}; } - ${V3_MediaQuery.MaxWidth.sm} { + ${MediaQuery.MaxWidth.sm} { -webkit-line-clamp: ${(props) => props.$maxLines?.mobile ?? "none"}; } `; export const SubLabel = styled.div` - ${V3_Font["body-md-regular"]} + ${Font["body-md-regular"]} margin-top: 0.5rem; z-index: 1; // forces sublabel to render above the input @@ -292,7 +290,7 @@ export const SubLabel = styled.div` strong, b { - ${V3_Font["body-md-semibold"]} + ${Font["body-md-semibold"]} } `; @@ -313,9 +311,9 @@ export const IndicatorLabelContainer = styled.div` color: ${(props) => - props.$disabled ? V3_Colour["text-disabled"] : V3_Colour["text-error"]}; + props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; white-space: nowrap; - ${V3_Font["body-md-semibold"]} + ${Font["body-md-semibold"]} height: fit-content; padding: 0.6875rem 1rem 0.6875rem 0.5rem; border: none; @@ -326,15 +324,13 @@ export const RemoveButton = styled.button` export const ExpandButton = styled.button` color: ${(props) => - props.disabled - ? V3_Colour["text-disabled"] - : V3_Colour["text-primary"]}; - ${V3_Font["body-baseline-semibold"]} + props.disabled ? Colour["text-disabled"] : Colour["text-primary"]}; + ${Font["body-baseline-semibold"]} display: flex; align-items: center; justify-content: flex-end; border: none; - background-color: ${V3_Colour.bg}; + background-color: ${Colour.bg}; cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; padding: 0 1rem 0.6875rem 1rem; padding-top: ${(props) => @@ -351,9 +347,9 @@ export const ExpandButton = styled.button` export const ErrorContainer = styled.div` width: 100%; color: ${(props) => - props.$disabled ? V3_Colour["text-disabled"] : V3_Colour["text-error"]}; + props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; border: none; - background: ${V3_Colour.bg}; + background: ${Colour.bg}; cursor: ${(props) => (props.$disabled ? "not-allowed" : "pointer")}; padding: 0.6875rem 1rem 0.5rem 1rem; `; @@ -367,20 +363,20 @@ export const Children = styled.div` padding: 0 1rem; padding-top: 0.6875rem; padding-bottom: ${(props) => (props.$isFinalItem ? "0.6875rem" : "0.5rem")}; - background-color: ${V3_Colour.bg}; + background-color: ${Colour.bg}; ${(props) => { if (props.$disabled) { return css` - color: ${V3_Colour["text-disabled"]}; + color: ${Colour["text-disabled"]}; `; } else if (props.$selected) { return css` - color: ${V3_Colour["text-selected"]}; + color: ${Colour["text-selected"]}; `; } else { return css` - color: ${V3_Colour.text}; + color: ${Colour.text}; `; } }} @@ -388,12 +384,12 @@ export const Children = styled.div` export const ErrorText = styled(Typography.BodyMD)` color: ${(props) => - props.$disabled ? V3_Colour["text-disabled"] : V3_Colour["text-error"]}; + props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; `; export const ErrorList = styled(TextList.Ul)` color: ${(props) => - props.$disabled ? V3_Colour["text-disabled"] : V3_Colour["text-error"]}; + props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; `; export const StyledToggleIcon = styled(ToggleIcon)` @@ -401,22 +397,22 @@ export const StyledToggleIcon = styled(ToggleIcon)` if (props.$disabled) { if (props.$selected) { return css` - color: ${V3_Colour["icon-selected-disabled"]}; + color: ${Colour["icon-selected-disabled"]}; `; } else { return css` - color: ${V3_Colour["icon-disabled-subtle"]}; + color: ${Colour["icon-disabled-subtle"]}; `; } } if (props.$selected) { return css` - color: ${V3_Colour["icon-selected"]}; + color: ${Colour["icon-selected"]}; `; } return css` - color: ${V3_Colour["icon-subtle"]}; + color: ${Colour["icon-subtle"]}; `; }}; `; From 8023c9d04d92bf525b7f9c1b145c776c8e39766c Mon Sep 17 00:00:00 2001 From: Nghi To Date: Tue, 14 Apr 2026 16:22:38 +0700 Subject: [PATCH 03/16] [BOOKINGSG-9121][Nghi] convert transient props to class based for toggle-icon --- src/shared/toggle-icon/toggle-icon.styles.ts | 38 ++++++++------------ src/shared/toggle-icon/toggle-icon.tsx | 12 +++++-- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/shared/toggle-icon/toggle-icon.styles.ts b/src/shared/toggle-icon/toggle-icon.styles.ts index 6f4d44749a..b5a1c463b3 100644 --- a/src/shared/toggle-icon/toggle-icon.styles.ts +++ b/src/shared/toggle-icon/toggle-icon.styles.ts @@ -21,27 +21,19 @@ export const Wrapper = styled.div` width: 100%; } - ${(props) => { - if (props.$disabled) { - if (props.$active) { - return css` - color: ${Colour["icon-selected-disabled"]}; - `; - } else { - return css` - color: ${Colour["icon-disabled-subtle"]}; - `; - } - } - - if (props.$active) { - return css` - color: ${Colour["icon-selected"]}; - `; - } - - return css` - color: ${Colour["icon-subtle"]}; - `; - }}; + &.wrapperActive { + color: ${Colour["icon-selected"]}; + } + + &.wrapperActiveDisabled { + color: ${Colour["icon-selected-disabled"]}; + } + + &.wrapperDisabled { + color: ${Colour["icon-disabled-subtle"]}; + } +`; + +export const basicWrapperColor = css` + color: ${Colour["icon-subtle"]}; `; diff --git a/src/shared/toggle-icon/toggle-icon.tsx b/src/shared/toggle-icon/toggle-icon.tsx index c4ce741a71..96f72a386a 100644 --- a/src/shared/toggle-icon/toggle-icon.tsx +++ b/src/shared/toggle-icon/toggle-icon.tsx @@ -4,8 +4,9 @@ import { CrossIcon } from "@lifesg/react-icons/cross"; import { SquareIcon } from "@lifesg/react-icons/square"; import { SquareTickFillIcon } from "@lifesg/react-icons/square-tick-fill"; import { TickIcon } from "@lifesg/react-icons/tick"; +import clsx from "clsx"; -import { Wrapper } from "./toggle-icon.styles"; +import { basicWrapperColor, Wrapper } from "./toggle-icon.styles"; export type ToggleIconType = "checkbox" | "radio" | "tick" | "cross"; @@ -44,8 +45,13 @@ export const ToggleIcon = ({ return ( From 54cf0347c16ecb6d4c8d6e8dd7ce184692703a31 Mon Sep 17 00:00:00 2001 From: Nghi To Date: Tue, 14 Apr 2026 16:54:51 +0700 Subject: [PATCH 04/16] [BOOKINGSG-9121][Nghi] remove style component props for toggle-icon --- src/shared/toggle-icon/toggle-icon.styles.ts | 7 +------ src/shared/toggle-icon/toggle-icon.tsx | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/shared/toggle-icon/toggle-icon.styles.ts b/src/shared/toggle-icon/toggle-icon.styles.ts index b5a1c463b3..920c85a43c 100644 --- a/src/shared/toggle-icon/toggle-icon.styles.ts +++ b/src/shared/toggle-icon/toggle-icon.styles.ts @@ -2,15 +2,10 @@ import styled, { css } from "styled-components"; import { Colour } from "../../theme"; -interface StyleProps { - $active?: boolean; - $disabled?: boolean; -} - // ============================================================================= // STYLING // ============================================================================= -export const Wrapper = styled.div` +export const Wrapper = styled.div` height: 1.625rem; width: 1.625rem; margin-right: 0.5rem; diff --git a/src/shared/toggle-icon/toggle-icon.tsx b/src/shared/toggle-icon/toggle-icon.tsx index 96f72a386a..277efd4d2a 100644 --- a/src/shared/toggle-icon/toggle-icon.tsx +++ b/src/shared/toggle-icon/toggle-icon.tsx @@ -52,7 +52,6 @@ export const ToggleIcon = ({ basicWrapperColor, className )} - $disabled={disabled} aria-hidden > {component} From 4e708544e01bd5eea235b79ce317e63235a9fa2a Mon Sep 17 00:00:00 2001 From: Nghi To Date: Wed, 15 Apr 2026 14:14:43 +0700 Subject: [PATCH 05/16] [BOOKINGSG-9121][Nghi] convert transient props to class based for toggle --- src/toggle/toggle.styles.ts | 494 +++++++++++++++++------------------- src/toggle/toggle.tsx | 221 ++++++++++------ 2 files changed, 378 insertions(+), 337 deletions(-) diff --git a/src/toggle/toggle.styles.ts b/src/toggle/toggle.styles.ts index c7ce2c0726..6ed89cc23d 100644 --- a/src/toggle/toggle.styles.ts +++ b/src/toggle/toggle.styles.ts @@ -1,11 +1,10 @@ -import styled, { css } from "styled-components"; +import styled from "styled-components"; import { Alert } from "../alert"; import { ToggleIcon } from "../shared/toggle-icon/toggle-icon"; import { TextList } from "../text-list"; import { Colour, Font, MediaQuery, Radius } from "../theme"; import { Typography } from "../typography"; -import type { ToggleStyleType } from "./types"; // ============================================================================= // STYLE INTERFACES, transient props are denoted with $ @@ -17,33 +16,72 @@ interface StyleProps { $indicator?: boolean; } -interface ContainerStyleProps extends StyleProps { - $styleType?: ToggleStyleType; - $error?: boolean; - $useContentWidth?: boolean; -} - -interface IndicatorLabelContainerStyleProps { - $addPadding?: boolean; -} - interface LabelStyleProps { $maxLines?: { desktop?: number; mobile?: number; tablet?: number }; } -interface ExpandButtonStyleProps extends StyleProps { - $paddingTopRequired?: boolean; -} - -interface ChildrenStyleProps extends StyleProps { - $isFinalItem?: boolean; -} +export const tokens = { + label: { + desktopLineClamp: "--fds-internal-toggle-label-desktopLineClamp", + tabletLineClamp: "--fds-internal-toggle-label-tabletLineClamp", + mobileLineClamp: "--fds-internal-toggle-label-mobileLineClamp", + }, +} as const; // ============================================================================= // STYLING // ============================================================================= -export const Container = styled.div` +export const HeaderContainer = styled.div` + display: flex; + align-items: flex-start; + justify-content: space-between; +`; + +export const TextContainer = styled.div` + display: flex; + flex-direction: column; + overflow-wrap: anywhere; + width: 100%; + overflow: hidden; + + // apply header container text color + &.toggleTextContainerDisabledSelected { + color: ${Colour["text-selected-disabled"]}; + } + + &.toggleTextContainerDisabled { + color: ${Colour["text-disabled"]}; + } + + &.toggleTextContainerSelected { + color: ${Colour["text-selected"]}; + } + + &.toggleTextContainerDefault { + color: ${Colour.text}; + } +`; + +export const StyledToggleIcon = styled(ToggleIcon)` + &.toggleStyledToggleIconDisabledSelected { + color: ${Colour["icon-selected-disabled"]}; + } + + &.toggleStyledToggleIconDisabled { + color: ${Colour["icon-disabled-subtle"]}; + } + + &.toggleStyledToggleIconSelected { + color: ${Colour["icon-selected"]}; + } + + &.toggleStyledToggleIconDefault { + color: ${Colour["icon-subtle"]}; + } +`; + +export const Container = styled.div` position: relative; display: inline-flex; min-width: 10.375rem; @@ -60,166 +98,137 @@ export const Container = styled.div` outline-offset: 0; } - ${(props) => { - if (!props.$indicator) { - return css` - justify-content: center; - `; - } - }} + &.noIndicator { + justify-content: center; + } // Container min width to fit content - ${(props) => { - if (props.$useContentWidth) { - return css` - min-width: unset; - `; + &.useContentWidth { + min-width: unset; + } + + &.toggleContainerNoBorderErrorDisabled { + border-color: ${Colour["border-error"]}; + } + + &.toggleContainerNoBorderError { + border-color: ${Colour["border-error"]}; + + &:has(${HeaderContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; + } } - }} - - // apply container border and header background color - ${(props) => { - switch (props.$styleType) { - case "no-border": { - if (props.$error) { - if (props.$disabled) { - return css` - border-color: ${Colour["border-error"]}; - `; - } else { - return css` - border-color: ${Colour["border-error"]}; - - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-hover-subtle"]}; - } - } - `; - } - } + } - if (props.$disabled) { - if (props.$selected) { - return css` - border: none; - background: ${Colour["bg-selected-disabled"]}; - `; - } else { - return css` - border: none; - `; - } + &.toggleContainerNoBorderDisabledSelected { + border: none; + background: ${Colour["bg-selected-disabled"]}; + } + + &.toggleContainerNoBorderDisabled { + border: none; + } + + &.toggleContainerNoBorderSelected { + border: none; + background: ${Colour["bg-selected"]}; + + &:has(${HeaderContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-selected-hover"]}; + + & ${TextContainer} { + color: ${Colour["text-selected-hover"]}; } - if (props.$selected) { - return css` - border: none; - background: ${Colour["bg-selected"]}; - - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-selected-hover"]}; - - & ${TextContainer} { - color: ${Colour["text-selected-hover"]}; - } - - & ${StyledToggleIcon} { - color: ${Colour["icon-selected-hover"]}; - } - } - } - `; + & ${StyledToggleIcon} { + color: ${Colour["icon-selected-hover"]}; } + } + } + } - return css` - border: none; + &.toggleContainerNoBorderDefault { + border: none; - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-hover-subtle"]}; - } - } - `; + &:has(${HeaderContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; } + } + } - default: { - if (props.$error) { - if (props.$disabled) { - return css` - border-color: ${Colour["border-error"]}; - `; - } else { - return css` - border-color: ${Colour["border-error"]}; - - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-hover-subtle"]}; - } - } - `; - } - } + &.toggleContainerDefaultErrorDisabled { + border-color: ${Colour["border-error"]}; + } + + &.toggleContainerDefaultError { + border-color: ${Colour["border-error"]}; - if (props.$disabled) { - if (props.$selected) { - return css` - border-color: ${Colour["border-selected-disabled"]}; - background: ${Colour["bg-selected-disabled"]}; - `; - } else { - return css` - border-color: ${Colour["border-disabled"]}; - background: ${Colour["bg-disabled"]}; - `; - } + &:has(${HeaderContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; + } + } + } + + &.toggleContainerDefaultDisabledSelected { + border-color: ${Colour["border-selected-disabled"]}; + background: ${Colour["bg-selected-disabled"]}; + } + + &.toggleContainerDefaultDisabled { + border-color: ${Colour["border-disabled"]}; + background: ${Colour["bg-disabled"]}; + } + + &.toggleContainerDefaultSelected { + border-color: ${Colour["border-selected"]}; + background: ${Colour["bg-selected"]}; + + &:has(${HeaderContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-selected-hover"]}; + + & ${TextContainer} { + color: ${Colour["text-selected-hover"]}; } - if (props.$selected) { - return css` - border-color: ${Colour["border-selected"]}; - background: ${Colour["bg-selected"]}; - - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-selected-hover"]}; - - & ${TextContainer} { - color: ${Colour["text-selected-hover"]}; - } - - & ${StyledToggleIcon} { - color: ${Colour["icon-selected-hover"]}; - } - } - } - `; + & ${StyledToggleIcon} { + color: ${Colour["icon-selected-hover"]}; } + } + } + } - return css` - border-color: ${Colour.border}; + &.toggleContainerDefault { + border-color: ${Colour.border}; - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-hover-subtle"]}; - } - } - `; + &:has(${HeaderContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; } } - }} + } `; export const Input = styled.input` position: absolute; height: 100%; width: 100%; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + cursor: pointer; top: 0; left: 0; + &.toggleInputDisabled { + cursor: not-allowed; + } + + &.toggleInputEnabled { + cursor: pointer; + } + /* Hide appearance but keep it focusable using keyboard interactions */ appearance: none; background: transparent; @@ -229,55 +238,31 @@ export const InputContainer = styled.div` display: flex; `; -export const TextContainer = styled.div` - display: flex; - flex-direction: column; - overflow-wrap: anywhere; - width: 100%; - overflow: hidden; - - // apply header container text color - ${(props) => { - if (props.$disabled) { - if (props.$selected) { - return css` - color: ${Colour["text-selected-disabled"]}; - `; - } else { - return css` - color: ${Colour["text-disabled"]}; - `; - } - } - - if (props.$selected) { - return css` - color: ${Colour["text-selected"]}; - `; - } +export const Label = styled.label` + &.toggleLabelSelected { + ${Font["body-baseline-semibold"]} + } - return css` - color: ${Colour.text}; - `; - }} -`; + &.toggleLabelDefault { + ${Font["body-baseline-regular"]} + } -export const Label = styled.label` - ${(props) => - props.$selected - ? Font["body-baseline-semibold"] - : Font["body-baseline-regular"]}; overflow: hidden; display: -webkit-box; text-overflow: ellipsis; -webkit-box-orient: vertical; overflow-wrap: break-word; - -webkit-line-clamp: ${(props) => props.$maxLines?.desktop ?? "none"}; + + ${tokens.label.desktopLineClamp}: initial; + ${tokens.label.tabletLineClamp}: initial; + ${tokens.label.mobileLineClamp}: initial; + + -webkit-line-clamp: var(${tokens.label.desktopLineClamp}, none); ${MediaQuery.MaxWidth.lg} { - -webkit-line-clamp: ${(props) => props.$maxLines?.tablet ?? "none"}; + -webkit-line-clamp: var(${tokens.label.tabletLineClamp}, none); } ${MediaQuery.MaxWidth.sm} { - -webkit-line-clamp: ${(props) => props.$maxLines?.mobile ?? "none"}; + -webkit-line-clamp: var(${tokens.label.mobileLineClamp}, none); } `; @@ -294,49 +279,56 @@ export const SubLabel = styled.div` } `; -export const HeaderContainer = styled.div` - display: flex; - align-items: flex-start; - justify-content: space-between; -`; - -export const IndicatorLabelContainer = styled.div` +export const IndicatorLabelContainer = styled.div` display: flex; height: 100%; width: 100%; position: relative; - padding: ${(props) => - props.$addPadding ? "0.6875rem 0rem 0.6875rem 1rem" : "0.6875rem 1rem"}; + padding: 0.6875rem 1rem; + + &.indicatorLabelContainerAddPadding { + padding: 0.6875rem 0rem 0.6875rem 1rem; + } `; -export const RemoveButton = styled.button` - color: ${(props) => - props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; +export const RemoveButton = styled.button` + color: ${Colour["text-error"]}; white-space: nowrap; ${Font["body-md-semibold"]} height: fit-content; padding: 0.6875rem 1rem 0.6875rem 0.5rem; border: none; background: none; + cursor: pointer; - cursor: ${(props) => (props.$disabled ? "not-allowed" : "pointer")}; + &.removeButtonDisabled { + color: ${Colour["text-disabled"]}; + cursor: not-allowed; + } `; -export const ExpandButton = styled.button` - color: ${(props) => - props.disabled ? Colour["text-disabled"] : Colour["text-primary"]}; +export const ExpandButton = styled.button` + color: ${Colour["text-primary"]}; ${Font["body-baseline-semibold"]} display: flex; align-items: center; justify-content: flex-end; border: none; background-color: ${Colour.bg}; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + cursor: pointer; padding: 0 1rem 0.6875rem 1rem; - padding-top: ${(props) => - props.$paddingTopRequired ? "0.6875rem" : "0rem"}; + padding-top: 0rem; width: 100%; + &.expandButtonDisabled { + color: ${Colour["text-disabled"]}; + cursor: not-allowed; + } + + &.expandButtonPaddingTopRequired { + padding-top: 0.6875rem; + } + svg { width: 1em; height: 1em; @@ -344,14 +336,18 @@ export const ExpandButton = styled.button` } `; -export const ErrorContainer = styled.div` +export const ErrorContainer = styled.div` width: 100%; - color: ${(props) => - props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; + color: ${Colour["text-error"]}; border: none; background: ${Colour.bg}; - cursor: ${(props) => (props.$disabled ? "not-allowed" : "pointer")}; + cursor: pointer; padding: 0.6875rem 1rem 0.5rem 1rem; + + &.errorContainerDisabled { + color: ${Colour["text-disabled"]}; + cursor: not-allowed; + } `; export const AlertContainer = styled(Alert)` @@ -359,60 +355,38 @@ export const AlertContainer = styled(Alert)` user-select: none; `; -export const Children = styled.div` +export const Children = styled.div` padding: 0 1rem; padding-top: 0.6875rem; - padding-bottom: ${(props) => (props.$isFinalItem ? "0.6875rem" : "0.5rem")}; + padding-bottom: 0.5rem; background-color: ${Colour.bg}; + color: ${Colour.text}; - ${(props) => { - if (props.$disabled) { - return css` - color: ${Colour["text-disabled"]}; - `; - } else if (props.$selected) { - return css` - color: ${Colour["text-selected"]}; - `; - } else { - return css` - color: ${Colour.text}; - `; - } - }} -`; + &.childrenIsFinalItem { + padding-bottom: 0.6875rem; + } + + &.childrenDisabled { + color: ${Colour["text-disabled"]}; + } -export const ErrorText = styled(Typography.BodyMD)` - color: ${(props) => - props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; + &.childrenSelected { + color: ${Colour["text-selected"]}; + } `; -export const ErrorList = styled(TextList.Ul)` - color: ${(props) => - props.$disabled ? Colour["text-disabled"] : Colour["text-error"]}; +export const ErrorText = styled(Typography.BodyMD)` + color: ${Colour["text-error"]}; + + &.errorTextDisabled { + color: ${Colour["text-disabled"]}; + } `; -export const StyledToggleIcon = styled(ToggleIcon)` - ${(props) => { - if (props.$disabled) { - if (props.$selected) { - return css` - color: ${Colour["icon-selected-disabled"]}; - `; - } else { - return css` - color: ${Colour["icon-disabled-subtle"]}; - `; - } - } +export const ErrorList = styled(TextList.Ul)` + color: ${Colour["text-error"]}; - if (props.$selected) { - return css` - color: ${Colour["icon-selected"]}; - `; - } - return css` - color: ${Colour["icon-subtle"]}; - `; - }}; + &.errorListDisabled { + color: ${Colour["text-disabled"]}; + } `; diff --git a/src/toggle/toggle.tsx b/src/toggle/toggle.tsx index 45fe2debe2..e1dba7223b 100644 --- a/src/toggle/toggle.tsx +++ b/src/toggle/toggle.tsx @@ -1,28 +1,12 @@ import { ChevronDownIcon } from "@lifesg/react-icons/chevron-down"; import { ChevronUpIcon } from "@lifesg/react-icons/chevron-up"; +import clsx from "clsx"; import { useEffect, useMemo, useRef, useState } from "react"; import { Markup } from "../markup"; import type { ToggleIconType } from "../shared/toggle-icon/toggle-icon"; import { SimpleIdGenerator } from "../util"; -import { - AlertContainer, - Children, - Container, - ErrorContainer, - ErrorList, - ErrorText, - ExpandButton, - HeaderContainer, - IndicatorLabelContainer, - Input, - InputContainer, - Label, - RemoveButton, - StyledToggleIcon, - SubLabel, - TextContainer, -} from "./toggle.styles"; +import * as styles from "./toggle.styles"; import type { ToggleProps } from "./types"; export const Toggle = ({ @@ -132,6 +116,74 @@ export const Toggle = ({ e.stopPropagation(); }; + const containerStateClass = (() => { + if (styleType === "no-border") { + if (error) { + return disabled + ? "toggleContainerNoBorderErrorDisabled" + : "toggleContainerNoBorderError"; + } + + if (disabled) { + return selected + ? "toggleContainerNoBorderDisabledSelected" + : "toggleContainerNoBorderDisabled"; + } + + if (selected) { + return "toggleContainerNoBorderSelected"; + } + + return "toggleContainerNoBorderDefault"; + } + + if (error) { + return disabled + ? "toggleContainerDefaultErrorDisabled" + : "toggleContainerDefaultError"; + } + + if (disabled) { + return selected + ? "toggleContainerDefaultDisabledSelected" + : "toggleContainerDefaultDisabled"; + } + + if (selected) { + return "toggleContainerDefaultSelected"; + } + + return "toggleContainerDefault"; + })(); + + const textContainerStateClass = (() => { + if (disabled) { + return selected + ? "toggleTextContainerDisabledSelected" + : "toggleTextContainerDisabled"; + } + + if (selected) { + return "toggleTextContainerSelected"; + } + + return "toggleTextContainerDefault"; + })(); + + const styledToggleIconStateClass = (() => { + if (disabled) { + return selected + ? "toggleStyledToggleIconDisabledSelected" + : "toggleStyledToggleIconDisabled"; + } + + if (selected) { + return "toggleStyledToggleIconSelected"; + } + + return "toggleStyledToggleIconDefault"; + })(); + // ============================================================================= // RENDER FUNCTIONS // ============================================================================= @@ -152,12 +204,11 @@ export const Toggle = ({ } return ( - ); }; @@ -176,28 +227,32 @@ export const Toggle = ({ // Hide sublabel from screen readers as the main input already has aria-describedby that points to the sublabel return ( - {component} - + ); }; const renderCompositeChildren = () => { return ( (!collapsible || expanded) && ( - {compositeSectionChildren} - + ) ); }; @@ -206,8 +261,12 @@ export const Toggle = ({ const collapsedWithoutErrors = !expanded && !hasCompositeSectionError; return ( collapsible && ( - )} - + ) ); }; @@ -233,25 +292,27 @@ export const Toggle = ({ .join(" ") || undefined; return ( - - - + + - {indicator && renderIndicator()} - - + {subLabel && renderSubLabel()} - - - - + + + {removable && ( - Remove - + )} - + ); }; const renderErrorList = (errors: string[]) => { + const errorStateClass = disabled ? "errorTextDisabled" : ""; + const errorListStateClass = disabled ? "errorListDisabled" : ""; return ( <> - + Error - - + + {errors?.map((item, index) => { return (
  • - {item} - +
  • ); })} -
    + ); }; @@ -324,21 +392,21 @@ export const Toggle = ({ collapsible && !expanded && hasCompositeSectionError && ( - - {Array.isArray(errors) ? renderErrorList(errors) : errors} - - + + ) ); }; @@ -356,19 +424,18 @@ export const Toggle = ({ }; return ( - {renderToggleWithRemoveButton()} {renderCompositeSection()} - + ); }; From cf785c23a838ac99a32df623829cc88fa0d5ad92 Mon Sep 17 00:00:00 2001 From: Nghi To Date: Wed, 15 Apr 2026 14:49:16 +0700 Subject: [PATCH 06/16] [BOOKINGSG-9121][Nghi] convert styled components to linaria css for toggle-icon --- src/shared/toggle-icon/toggle-icon.styles.ts | 25 +++++++++----------- src/shared/toggle-icon/toggle-icon.tsx | 14 +++++------ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/shared/toggle-icon/toggle-icon.styles.ts b/src/shared/toggle-icon/toggle-icon.styles.ts index 920c85a43c..be1530b450 100644 --- a/src/shared/toggle-icon/toggle-icon.styles.ts +++ b/src/shared/toggle-icon/toggle-icon.styles.ts @@ -1,34 +1,31 @@ -import styled, { css } from "styled-components"; +import { css } from "@linaria/core"; import { Colour } from "../../theme"; // ============================================================================= // STYLING // ============================================================================= -export const Wrapper = styled.div` +export const WrapperBase = css` height: 1.625rem; width: 1.625rem; margin-right: 0.5rem; flex-shrink: 0; + color: ${Colour["icon-subtle"]}; svg { height: 100%; width: 100%; } +`; - &.wrapperActive { - color: ${Colour["icon-selected"]}; - } - - &.wrapperActiveDisabled { - color: ${Colour["icon-selected-disabled"]}; - } +export const WrapperDisabled = css` + color: ${Colour["icon-disabled-subtle"]}; +`; - &.wrapperDisabled { - color: ${Colour["icon-disabled-subtle"]}; - } +export const WrapperActive = css` + color: ${Colour["icon-selected"]}; `; -export const basicWrapperColor = css` - color: ${Colour["icon-subtle"]}; +export const WrapperActiveDisabled = css` + color: ${Colour["icon-selected-disabled"]}; `; diff --git a/src/shared/toggle-icon/toggle-icon.tsx b/src/shared/toggle-icon/toggle-icon.tsx index 277efd4d2a..9b49ac67ff 100644 --- a/src/shared/toggle-icon/toggle-icon.tsx +++ b/src/shared/toggle-icon/toggle-icon.tsx @@ -6,7 +6,7 @@ import { SquareTickFillIcon } from "@lifesg/react-icons/square-tick-fill"; import { TickIcon } from "@lifesg/react-icons/tick"; import clsx from "clsx"; -import { basicWrapperColor, Wrapper } from "./toggle-icon.styles"; +import * as styles from "./toggle-icon.styles"; export type ToggleIconType = "checkbox" | "radio" | "tick" | "cross"; @@ -44,17 +44,17 @@ export const ToggleIcon = ({ } return ( - {component} - + ); }; From 51a1486daf3509c3676a3efa12b584f6f3366b95 Mon Sep 17 00:00:00 2001 From: Nghi To Date: Thu, 16 Apr 2026 15:05:02 +0700 Subject: [PATCH 07/16] [BOOKINGSG-9121][Nghi] convert styled components to linaria css for toggle --- src/toggle/toggle.styles.ts | 349 ++++++++++++++++++------------------ src/toggle/toggle.tsx | 202 +++++++++++++-------- 2 files changed, 299 insertions(+), 252 deletions(-) diff --git a/src/toggle/toggle.styles.ts b/src/toggle/toggle.styles.ts index 6ed89cc23d..0f437e6a4f 100644 --- a/src/toggle/toggle.styles.ts +++ b/src/toggle/toggle.styles.ts @@ -1,25 +1,12 @@ -import styled from "styled-components"; +import { css } from "@linaria/core"; -import { Alert } from "../alert"; -import { ToggleIcon } from "../shared/toggle-icon/toggle-icon"; -import { TextList } from "../text-list"; +import { WrapperBase } from "../shared/toggle-icon/toggle-icon.styles"; import { Colour, Font, MediaQuery, Radius } from "../theme"; -import { Typography } from "../typography"; // ============================================================================= // STYLE INTERFACES, transient props are denoted with $ // See more https://styled-components.com/docs/api#transient-props // ============================================================================= -interface StyleProps { - $selected?: boolean; - $disabled?: boolean; - $indicator?: boolean; -} - -interface LabelStyleProps { - $maxLines?: { desktop?: number; mobile?: number; tablet?: number }; -} - export const tokens = { label: { desktopLineClamp: "--fds-internal-toggle-label-desktopLineClamp", @@ -32,56 +19,53 @@ export const tokens = { // STYLING // ============================================================================= -export const HeaderContainer = styled.div` +export const headerContainer = css` display: flex; align-items: flex-start; justify-content: space-between; `; -export const TextContainer = styled.div` +export const textContainer = css` display: flex; flex-direction: column; overflow-wrap: anywhere; width: 100%; overflow: hidden; +`; - // apply header container text color - &.toggleTextContainerDisabledSelected { - color: ${Colour["text-selected-disabled"]}; - } +export const toggleTextContainerDefault = css` + color: ${Colour.text}; +`; - &.toggleTextContainerDisabled { - color: ${Colour["text-disabled"]}; - } +export const toggleTextContainerSelected = css` + color: ${Colour["text-selected"]}; +`; - &.toggleTextContainerSelected { - color: ${Colour["text-selected"]}; - } +export const toggleTextContainerDisabled = css` + color: ${Colour["text-disabled"]}; +`; - &.toggleTextContainerDefault { - color: ${Colour.text}; - } +export const toggleTextContainerDisabledSelected = css` + color: ${Colour["text-selected-disabled"]}; `; -export const StyledToggleIcon = styled(ToggleIcon)` - &.toggleStyledToggleIconDisabledSelected { - color: ${Colour["icon-selected-disabled"]}; - } +export const toggleStyledToggleIconDefault = css` + color: ${Colour["icon-subtle"]}; +`; - &.toggleStyledToggleIconDisabled { - color: ${Colour["icon-disabled-subtle"]}; - } +export const toggleStyledToggleIconSelected = css` + color: ${Colour["icon-selected"]}; +`; - &.toggleStyledToggleIconSelected { - color: ${Colour["icon-selected"]}; - } +export const toggleStyledToggleIconDisabled = css` + color: ${Colour["icon-disabled-subtle"]}; +`; - &.toggleStyledToggleIconDefault { - color: ${Colour["icon-subtle"]}; - } +export const toggleStyledToggleIconDisabledSelected = css` + color: ${Colour["icon-selected-disabled"]}; `; -export const Container = styled.div` +export const container = css` position: relative; display: inline-flex; min-width: 10.375rem; @@ -98,122 +82,132 @@ export const Container = styled.div` outline-offset: 0; } - &.noIndicator { - justify-content: center; - } + &.toggleContainerDefault { + border-color: ${Colour.border}; - // Container min width to fit content - &.useContentWidth { - min-width: unset; + &:has(.${headerContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; + } + } } +`; - &.toggleContainerNoBorderErrorDisabled { - border-color: ${Colour["border-error"]}; - } +export const noIndicatorContainer = css` + justify-content: center; +`; + +export const useContentWidthContainer = css` + min-width: unset; +`; - &.toggleContainerNoBorderError { +export const toggleContainerNoBorderErrorDisabled = css` + border-color: ${Colour["border-error"]}; +`; + +export const toggleContainerNoBorderError = css` border-color: ${Colour["border-error"]}; - &:has(${HeaderContainer}:hover) { + &:has(.${headerContainer}:hover) { @media (pointer: fine) { background: ${Colour["bg-hover-subtle"]}; } } } +`; - &.toggleContainerNoBorderDisabledSelected { - border: none; - background: ${Colour["bg-selected-disabled"]}; - } +export const toggleContainerNoBorderDisabledSelected = css` + border: none; + background: ${Colour["bg-selected-disabled"]}; +`; - &.toggleContainerNoBorderDisabled { - border: none; - } +export const toggleContainerNoBorderDisabled = css` + border: none; +`; - &.toggleContainerNoBorderSelected { - border: none; - background: ${Colour["bg-selected"]}; +export const toggleContainerNoBorderSelected = css` + border: none; + background: ${Colour["bg-selected"]}; - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-selected-hover"]}; + &:has(.${headerContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-selected-hover"]}; - & ${TextContainer} { - color: ${Colour["text-selected-hover"]}; - } + & .${textContainer} { + color: ${Colour["text-selected-hover"]}; + } - & ${StyledToggleIcon} { - color: ${Colour["icon-selected-hover"]}; - } + & .${WrapperBase} { + color: ${Colour["icon-selected-hover"]}; } } } +`; - &.toggleContainerNoBorderDefault { - border: none; +export const toggleContainerNoBorderDefault = css` + border: none; - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-hover-subtle"]}; - } + &:has(.${headerContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; } } +`; - &.toggleContainerDefaultErrorDisabled { - border-color: ${Colour["border-error"]}; - } +export const toggleContainerDefaultErrorDisabled = css` + border-color: ${Colour["border-error"]}; +`; - &.toggleContainerDefaultError { - border-color: ${Colour["border-error"]}; +export const toggleContainerDefaultError = css` + border-color: ${Colour["border-error"]}; - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-hover-subtle"]}; - } + &:has(.${headerContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; } } +`; - &.toggleContainerDefaultDisabledSelected { - border-color: ${Colour["border-selected-disabled"]}; - background: ${Colour["bg-selected-disabled"]}; - } +export const toggleContainerDefaultDisabledSelected = css` + border-color: ${Colour["border-selected-disabled"]}; + background: ${Colour["bg-selected-disabled"]}; +`; - &.toggleContainerDefaultDisabled { - border-color: ${Colour["border-disabled"]}; - background: ${Colour["bg-disabled"]}; - } +export const toggleContainerDefaultDisabled = css` + border-color: ${Colour["border-disabled"]}; + background: ${Colour["bg-disabled"]}; +`; - &.toggleContainerDefaultSelected { - border-color: ${Colour["border-selected"]}; - background: ${Colour["bg-selected"]}; +export const toggleContainerDefaultSelected = css` + border-color: ${Colour["border-selected"]}; + background: ${Colour["bg-selected"]}; - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-selected-hover"]}; + &:has(.${headerContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-selected-hover"]}; - & ${TextContainer} { - color: ${Colour["text-selected-hover"]}; - } + & .${textContainer} { + color: ${Colour["text-selected-hover"]}; + } - & ${StyledToggleIcon} { - color: ${Colour["icon-selected-hover"]}; - } + & .${WrapperBase} { + color: ${Colour["icon-selected-hover"]}; } } } +`; - &.toggleContainerDefault { - border-color: ${Colour.border}; +export const toggleContainerDefault = css` + border-color: ${Colour.border}; - &:has(${HeaderContainer}:hover) { - @media (pointer: fine) { - background: ${Colour["bg-hover-subtle"]}; - } + &:has(.${headerContainer}:hover) { + @media (pointer: fine) { + background: ${Colour["bg-hover-subtle"]}; } } `; -export const Input = styled.input` +export const input = css` position: absolute; height: 100%; width: 100%; @@ -221,32 +215,25 @@ export const Input = styled.input` top: 0; left: 0; - &.toggleInputDisabled { - cursor: not-allowed; - } - - &.toggleInputEnabled { - cursor: pointer; - } - /* Hide appearance but keep it focusable using keyboard interactions */ appearance: none; background: transparent; border: none; `; -export const InputContainer = styled.div` - display: flex; + +export const toggleInputDisabled = css` + cursor: not-allowed; `; -export const Label = styled.label` - &.toggleLabelSelected { - ${Font["body-baseline-semibold"]} - } +export const toggleInputEnabled = css` + cursor: pointer; +`; - &.toggleLabelDefault { - ${Font["body-baseline-regular"]} - } +export const inputContainer = css` + display: flex; +`; +export const label = css` overflow: hidden; display: -webkit-box; text-overflow: ellipsis; @@ -266,7 +253,15 @@ export const Label = styled.label` } `; -export const SubLabel = styled.div` +export const toggleLabelDefault = css` + ${Font["body-baseline-regular"]} +`; + +export const toggleLabelSelected = css` + ${Font["body-baseline-semibold"]} +`; + +export const subLabel = css` ${Font["body-md-regular"]} margin-top: 0.5rem; @@ -279,19 +274,19 @@ export const SubLabel = styled.div` } `; -export const IndicatorLabelContainer = styled.div` +export const indicatorLabelContainer = css` display: flex; height: 100%; width: 100%; position: relative; padding: 0.6875rem 1rem; +`; - &.indicatorLabelContainerAddPadding { - padding: 0.6875rem 0rem 0.6875rem 1rem; - } +export const indicatorLabelContainerAddPadding = css` + padding: 0.6875rem 0rem 0.6875rem 1rem; `; -export const RemoveButton = styled.button` +export const removeButton = css` color: ${Colour["text-error"]}; white-space: nowrap; ${Font["body-md-semibold"]} @@ -300,14 +295,14 @@ export const RemoveButton = styled.button` border: none; background: none; cursor: pointer; +`; - &.removeButtonDisabled { - color: ${Colour["text-disabled"]}; - cursor: not-allowed; - } +export const removeButtonDisabled = css` + color: ${Colour["text-disabled"]}; + cursor: not-allowed; `; -export const ExpandButton = styled.button` +export const expandButton = css` color: ${Colour["text-primary"]}; ${Font["body-baseline-semibold"]} display: flex; @@ -320,15 +315,6 @@ export const ExpandButton = styled.button` padding-top: 0rem; width: 100%; - &.expandButtonDisabled { - color: ${Colour["text-disabled"]}; - cursor: not-allowed; - } - - &.expandButtonPaddingTopRequired { - padding-top: 0.6875rem; - } - svg { width: 1em; height: 1em; @@ -336,57 +322,66 @@ export const ExpandButton = styled.button` } `; -export const ErrorContainer = styled.div` +export const expandButtonDisabled = css` + color: ${Colour["text-disabled"]}; + cursor: not-allowed; +`; + +export const expandButtonPaddingTopRequired = css` + padding-top: 0.6875rem; +`; + +export const errorContainer = css` width: 100%; color: ${Colour["text-error"]}; border: none; background: ${Colour.bg}; cursor: pointer; padding: 0.6875rem 1rem 0.5rem 1rem; +`; - &.errorContainerDisabled { - color: ${Colour["text-disabled"]}; - cursor: not-allowed; - } +export const errorContainerDisabled = css` + color: ${Colour["text-disabled"]}; + cursor: not-allowed; `; -export const AlertContainer = styled(Alert)` +export const alertContainer = css` width: 100%; user-select: none; `; -export const Children = styled.div` +export const children = css` padding: 0 1rem; padding-top: 0.6875rem; padding-bottom: 0.5rem; background-color: ${Colour.bg}; color: ${Colour.text}; +`; - &.childrenIsFinalItem { - padding-bottom: 0.6875rem; - } +export const childrenIsFinalItem = css` + padding-bottom: 0.6875rem; +`; - &.childrenDisabled { - color: ${Colour["text-disabled"]}; - } +export const childrenSelected = css` + color: ${Colour["text-selected"]}; +`; - &.childrenSelected { - color: ${Colour["text-selected"]}; - } +export const childrenDisabled = css` + color: ${Colour["text-disabled"]}; `; -export const ErrorText = styled(Typography.BodyMD)` +export const errorText = css` color: ${Colour["text-error"]}; +`; - &.errorTextDisabled { - color: ${Colour["text-disabled"]}; - } +export const errorTextDisabled = css` + color: ${Colour["text-disabled"]}; `; -export const ErrorList = styled(TextList.Ul)` +export const errorList = css` color: ${Colour["text-error"]}; +`; - &.errorListDisabled { - color: ${Colour["text-disabled"]}; - } +export const errorListDisabled = css` + color: ${Colour["text-disabled"]}; `; diff --git a/src/toggle/toggle.tsx b/src/toggle/toggle.tsx index e1dba7223b..b5ba9aaee3 100644 --- a/src/toggle/toggle.tsx +++ b/src/toggle/toggle.tsx @@ -3,8 +3,15 @@ import { ChevronUpIcon } from "@lifesg/react-icons/chevron-up"; import clsx from "clsx"; import { useEffect, useMemo, useRef, useState } from "react"; +import { Alert } from "../alert"; import { Markup } from "../markup"; -import type { ToggleIconType } from "../shared/toggle-icon/toggle-icon"; +import { + ToggleIcon, + type ToggleIconType, +} from "../shared/toggle-icon/toggle-icon"; +import { UnorderedList } from "../text-list/unordered-list"; +import { useApplyStyle } from "../theme"; +import { BodyMD } from "../typography/typography"; import { SimpleIdGenerator } from "../util"; import * as styles from "./toggle.styles"; import type { ToggleProps } from "./types"; @@ -51,6 +58,22 @@ export const Toggle = ({ const generatedId = id ? `${id}` : `tg-${uniqueId}`; const inputRef = useRef(null); + const labelRef = useRef(null); + + useApplyStyle(labelRef, { + [styles.tokens.label.desktopLineClamp]: + childrenMaxLines?.desktop == null + ? null + : `${childrenMaxLines.desktop}`, + [styles.tokens.label.tabletLineClamp]: + childrenMaxLines?.tablet == null + ? null + : `${childrenMaxLines.tablet}`, + [styles.tokens.label.mobileLineClamp]: + childrenMaxLines?.mobile == null + ? null + : `${childrenMaxLines.mobile}`, + }); // ============================================================================= // EFFECTS @@ -120,68 +143,68 @@ export const Toggle = ({ if (styleType === "no-border") { if (error) { return disabled - ? "toggleContainerNoBorderErrorDisabled" - : "toggleContainerNoBorderError"; + ? styles.toggleContainerNoBorderErrorDisabled + : styles.toggleContainerNoBorderError; } if (disabled) { return selected - ? "toggleContainerNoBorderDisabledSelected" - : "toggleContainerNoBorderDisabled"; + ? styles.toggleContainerNoBorderDisabledSelected + : styles.toggleContainerNoBorderDisabled; } if (selected) { - return "toggleContainerNoBorderSelected"; + return styles.toggleContainerNoBorderSelected; } - return "toggleContainerNoBorderDefault"; + return styles.toggleContainerNoBorderDefault; } if (error) { return disabled - ? "toggleContainerDefaultErrorDisabled" - : "toggleContainerDefaultError"; + ? styles.toggleContainerDefaultErrorDisabled + : styles.toggleContainerDefaultError; } if (disabled) { return selected - ? "toggleContainerDefaultDisabledSelected" - : "toggleContainerDefaultDisabled"; + ? styles.toggleContainerDefaultDisabledSelected + : styles.toggleContainerDefaultDisabled; } if (selected) { - return "toggleContainerDefaultSelected"; + return styles.toggleContainerDefaultSelected; } - return "toggleContainerDefault"; + return styles.toggleContainerDefault; })(); const textContainerStateClass = (() => { if (disabled) { return selected - ? "toggleTextContainerDisabledSelected" - : "toggleTextContainerDisabled"; + ? styles.toggleTextContainerDisabledSelected + : styles.toggleTextContainerDisabled; } if (selected) { - return "toggleTextContainerSelected"; + return styles.toggleTextContainerSelected; } - return "toggleTextContainerDefault"; + return styles.toggleTextContainerDefault; })(); const styledToggleIconStateClass = (() => { if (disabled) { return selected - ? "toggleStyledToggleIconDisabledSelected" - : "toggleStyledToggleIconDisabled"; + ? styles.toggleStyledToggleIconDisabledSelected + : styles.toggleStyledToggleIconDisabled; } if (selected) { - return "toggleStyledToggleIconSelected"; + return styles.toggleStyledToggleIconSelected; } - return "toggleStyledToggleIconDefault"; + return styles.toggleStyledToggleIconDefault; })(); // ============================================================================= @@ -204,11 +227,11 @@ export const Toggle = ({ } return ( - ); }; @@ -227,32 +250,34 @@ export const Toggle = ({ // Hide sublabel from screen readers as the main input already has aria-describedby that points to the sublabel return ( - {component} - + ); }; const renderCompositeChildren = () => { return ( (!collapsible || expanded) && ( - {compositeSectionChildren} - + ) ); }; @@ -261,11 +286,12 @@ export const Toggle = ({ const collapsedWithoutErrors = !expanded && !hasCompositeSectionError; return ( collapsible && ( - )} - + ) ); }; @@ -292,26 +318,32 @@ export const Toggle = ({ .join(" ") || undefined; return ( - - +
    - - {indicator && renderIndicator()} - - {children} - + {subLabel && renderSubLabel()} - - - +
    + + {removable && ( - Remove - + )} -
    + ); }; const renderErrorList = (errors: string[]) => { - const errorStateClass = disabled ? "errorTextDisabled" : ""; - const errorListStateClass = disabled ? "errorListDisabled" : ""; return ( <> - Error - - + + {errors?.map((item, index) => { return (
  • - {item} - +
  • ); })} -
    + ); }; @@ -392,21 +440,24 @@ export const Toggle = ({ collapsible && !expanded && hasCompositeSectionError && ( - - {Array.isArray(errors) ? renderErrorList(errors) : errors} - - + + ) ); }; @@ -424,11 +475,12 @@ export const Toggle = ({ }; return ( - {renderToggleWithRemoveButton()} {renderCompositeSection()} - + ); }; From e625a78ae47137e776eb5eb4a5a0738d5638128a Mon Sep 17 00:00:00 2001 From: Nghi To Date: Fri, 17 Apr 2026 11:15:52 +0700 Subject: [PATCH 08/16] [BOOKINGSG-9121][Nghi] clean up code and modify for more readable code --- .../toggle-icon.styles.ts | 10 +-- .../toggle-icon => toggle}/toggle-icon.tsx | 22 ++++- src/toggle/toggle.styles.ts | 58 ++----------- src/toggle/toggle.tsx | 83 ++++++++----------- 4 files changed, 63 insertions(+), 110 deletions(-) rename src/{shared/toggle-icon => toggle}/toggle-icon.styles.ts (75%) rename src/{shared/toggle-icon => toggle}/toggle-icon.tsx (78%) diff --git a/src/shared/toggle-icon/toggle-icon.styles.ts b/src/toggle/toggle-icon.styles.ts similarity index 75% rename from src/shared/toggle-icon/toggle-icon.styles.ts rename to src/toggle/toggle-icon.styles.ts index be1530b450..84e29f180a 100644 --- a/src/shared/toggle-icon/toggle-icon.styles.ts +++ b/src/toggle/toggle-icon.styles.ts @@ -1,11 +1,11 @@ import { css } from "@linaria/core"; -import { Colour } from "../../theme"; +import { Colour } from "../theme"; // ============================================================================= // STYLING // ============================================================================= -export const WrapperBase = css` +export const wrapperBase = css` height: 1.625rem; width: 1.625rem; margin-right: 0.5rem; @@ -18,14 +18,14 @@ export const WrapperBase = css` } `; -export const WrapperDisabled = css` +export const wrapperDisabled = css` color: ${Colour["icon-disabled-subtle"]}; `; -export const WrapperActive = css` +export const wrapperActive = css` color: ${Colour["icon-selected"]}; `; -export const WrapperActiveDisabled = css` +export const wrapperActiveDisabled = css` color: ${Colour["icon-selected-disabled"]}; `; diff --git a/src/shared/toggle-icon/toggle-icon.tsx b/src/toggle/toggle-icon.tsx similarity index 78% rename from src/shared/toggle-icon/toggle-icon.tsx rename to src/toggle/toggle-icon.tsx index 9b49ac67ff..54723acd12 100644 --- a/src/shared/toggle-icon/toggle-icon.tsx +++ b/src/toggle/toggle-icon.tsx @@ -25,6 +25,22 @@ export const ToggleIcon = ({ }: ToggleIconProps) => { let component: JSX.Element | null; + const getWrapperStateClass = (() => { + if (active && disabled) { + return styles.wrapperActiveDisabled; + } + + if (disabled) { + return styles.wrapperDisabled; + } + + if (active) { + return styles.wrapperActive; + } + + return undefined; + })(); + switch (type) { case "checkbox": component = active ? : ; @@ -46,10 +62,8 @@ export const ToggleIcon = ({ return (
    { + // ============================================================================= + // HELPER FUNCTIONS + // ============================================================================= + + const getContainerStateClass = (() => { if (styleType === "no-border") { if (error) { return disabled @@ -186,7 +188,7 @@ export const Toggle = ({ return styles.toggleContainerDefault; })(); - const textContainerStateClass = (() => { + const getTextContainerStateClass = (() => { if (disabled) { return selected ? styles.toggleTextContainerDisabledSelected @@ -196,22 +198,6 @@ export const Toggle = ({ if (selected) { return styles.toggleTextContainerSelected; } - - return styles.toggleTextContainerDefault; - })(); - - const styledToggleIconStateClass = (() => { - if (disabled) { - return selected - ? styles.toggleStyledToggleIconDisabledSelected - : styles.toggleStyledToggleIconDisabled; - } - - if (selected) { - return styles.toggleStyledToggleIconSelected; - } - - return styles.toggleStyledToggleIconDefault; })(); // ============================================================================= @@ -238,7 +224,6 @@ export const Toggle = ({ type={toggleIconType} active={selected} disabled={disabled} - className={styledToggleIconStateClass} /> ); }; @@ -333,8 +318,8 @@ export const Toggle = ({ >