From 3dd8108c5cb81263a7f99e15b700fc93dc23f3d1 Mon Sep 17 00:00:00 2001 From: Ghazwan Date: Thu, 16 Apr 2026 16:10:13 +0700 Subject: [PATCH 1/2] [BOOKINGSG-9158][GZ] introduce a thin component layer for dropdown wrapper --- src/input-group/input-group-list-addon.tsx | 35 +++-------- src/input-multi-select/input-multi-select.tsx | 35 +++++------ .../input-nested-multi-select.tsx | 32 ++++------ .../input-nested-select.tsx | 32 ++++------ src/input-range-select/input-range-select.tsx | 23 +++---- src/input-select/input-select.tsx | 42 +++++-------- .../dropdown-wrapper/dropdown-wrapper.tsx | 63 +++++++++++++++++++ src/shared/dropdown-wrapper/types.ts | 7 +-- 8 files changed, 132 insertions(+), 137 deletions(-) create mode 100644 src/shared/dropdown-wrapper/dropdown-wrapper.tsx diff --git a/src/input-group/input-group-list-addon.tsx b/src/input-group/input-group-list-addon.tsx index f4514cc212..888374ed59 100644 --- a/src/input-group/input-group-list-addon.tsx +++ b/src/input-group/input-group-list-addon.tsx @@ -1,11 +1,14 @@ import type { OpenChangeReason } from "@floating-ui/react"; -import clsx from "clsx"; import React, { useEffect, useRef, useState } from "react"; import { concatIds, VisuallyHidden } from "../shared/accessibility"; import { DropdownList, DropdownListState } from "../shared/dropdown-list"; import { ElementWithDropdown } from "../shared/dropdown-wrapper"; -import * as dropdownWrapperStyles from "../shared/dropdown-wrapper/dropdown-wrapper.styles"; +import { + LabelContainer, + PlaceholderLabel, + ValueLabel, +} from "../shared/dropdown-wrapper/dropdown-wrapper"; import { SimpleIdGenerator } from "../util"; import { Divider, @@ -182,32 +185,14 @@ export const Component = ( // ============================================================================= const renderLabel = () => { if (selected) { - return ( -
- {getDisplayValue()} -
- ); + return {getDisplayValue()}; } - return ( -
- {placeholder} -
- ); + return {placeholder}; }; const renderSelectorContent = () => ( -
- {renderLabel()} -
+ {renderLabel()} ); const renderElement = () => { @@ -273,9 +258,7 @@ export const Component = ( aria-describedby={ariaDescribedBy} aria-invalid={ariaInvalid} > -
- {getDisplayValue()} -
+ {getDisplayValue()} ) : null; } else { diff --git a/src/input-multi-select/input-multi-select.tsx b/src/input-multi-select/input-multi-select.tsx index 4ee4b3fc97..6a5618752f 100644 --- a/src/input-multi-select/input-multi-select.tsx +++ b/src/input-multi-select/input-multi-select.tsx @@ -1,5 +1,4 @@ import type { OpenChangeReason } from "@floating-ui/react"; -import clsx from "clsx"; import findIndex from "lodash/findIndex"; import type React from "react"; import { useEffect, useRef, useState } from "react"; @@ -10,7 +9,11 @@ import { ExpandableElement, } from "../shared/dropdown-list"; import { ElementWithDropdown } from "../shared/dropdown-wrapper"; -import * as dropdownWrapperStyles from "../shared/dropdown-wrapper/dropdown-wrapper.styles"; +import { + LabelContainer, + PlaceholderLabel, + ValueLabel, +} from "../shared/dropdown-wrapper/dropdown-wrapper"; import { InputBox } from "../shared/input-wrapper"; import { SimpleIdGenerator } from "../util"; import type { InputMultiSelectProps } from "./types"; @@ -186,37 +189,27 @@ export const InputMultiSelect = ({ const renderLabel = () => { if (!selected || selected.length === 0) { return ( -
{placeholder} -
+ ); } else { return ( -
{getDisplayValue()} -
+ ); } }; const renderSelectorContent = () => ( -
- {renderLabel()} -
+ {renderLabel()} ); const renderElement = () => { diff --git a/src/input-nested-multi-select/input-nested-multi-select.tsx b/src/input-nested-multi-select/input-nested-multi-select.tsx index fd712b592f..7ad3a8ffca 100644 --- a/src/input-nested-multi-select/input-nested-multi-select.tsx +++ b/src/input-nested-multi-select/input-nested-multi-select.tsx @@ -1,5 +1,4 @@ import type { OpenChangeReason } from "@floating-ui/react"; -import clsx from "clsx"; import isEmpty from "lodash/isEmpty"; import isEqual from "lodash/isEqual"; import type React from "react"; @@ -15,7 +14,11 @@ import { NestedDropdownList, } from "../shared/dropdown-list"; import { ElementWithDropdown } from "../shared/dropdown-wrapper"; -import * as dropdownWrapperStyles from "../shared/dropdown-wrapper/dropdown-wrapper.styles"; +import { + LabelContainer, + PlaceholderLabel, + ValueLabel, +} from "../shared/dropdown-wrapper/dropdown-wrapper"; import { InputBox } from "../shared/input-wrapper"; import { SimpleIdGenerator, StringHelper } from "../util"; import type { SelectedItem } from "./helpers"; @@ -250,36 +253,23 @@ export const InputNestedMultiSelect = ({ const renderLabel = () => { if (isEmpty(selectedItems)) { return ( -
+ {placeholder} -
+ ); } else { return ( -
+ {truncateValue(getDisplayValue())} -
+ ); } }; const renderSelectorContent = () => ( -
+ {renderLabel()} -
+ ); const renderElement = () => { diff --git a/src/input-nested-select/input-nested-select.tsx b/src/input-nested-select/input-nested-select.tsx index 04b6e9071c..c34705580d 100644 --- a/src/input-nested-select/input-nested-select.tsx +++ b/src/input-nested-select/input-nested-select.tsx @@ -1,5 +1,4 @@ import type { OpenChangeReason } from "@floating-ui/react"; -import clsx from "clsx"; import isEmpty from "lodash/isEmpty"; import type React from "react"; import { useEffect, useRef, useState } from "react"; @@ -15,7 +14,11 @@ import { NestedDropdownList, } from "../shared/dropdown-list"; import { ElementWithDropdown } from "../shared/dropdown-wrapper"; -import * as dropdownWrapperStyles from "../shared/dropdown-wrapper/dropdown-wrapper.styles"; +import { + LabelContainer, + PlaceholderLabel, + ValueLabel, +} from "../shared/dropdown-wrapper/dropdown-wrapper"; import { InputBox } from "../shared/input-wrapper"; import { SimpleIdGenerator, StringHelper } from "../util"; import type { @@ -206,36 +209,23 @@ export const InputNestedSelect = ({ const renderLabel = () => { if (isEmpty(selectedItem)) { return ( -
+ {placeholder} -
+ ); } else { return ( -
+ {truncateValue(getDisplayValue())} -
+ ); } }; const renderSelectorContent = () => ( -
+ {renderLabel()} -
+ ); const renderElement = () => { diff --git a/src/input-range-select/input-range-select.tsx b/src/input-range-select/input-range-select.tsx index 5e77f872c1..c34e64a976 100644 --- a/src/input-range-select/input-range-select.tsx +++ b/src/input-range-select/input-range-select.tsx @@ -1,4 +1,3 @@ -import clsx from "clsx"; import type React from "react"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -7,7 +6,10 @@ import { concatIds, VisuallyHidden } from "../shared/accessibility"; import type { DropdownListApi } from "../shared/dropdown-list"; import { DropdownList, DropdownListState } from "../shared/dropdown-list"; import { ElementWithDropdown } from "../shared/dropdown-wrapper"; -import * as dropdownWrapperStyles from "../shared/dropdown-wrapper/dropdown-wrapper.styles"; +import { + PlaceholderLabel, + ValueLabel, +} from "../shared/dropdown-wrapper/dropdown-wrapper"; import { RangeInputInnerContainer } from "../shared/range-input-inner-container"; import { SimpleIdGenerator } from "../util"; import { StringHelper } from "../util/string-helper"; @@ -284,15 +286,9 @@ export const InputRangeSelect = ({ if (!selected) { return ( -
+ {truncateValue(rangeType, placeholders?.[rangeType] || "")} -
+ ); } @@ -301,12 +297,9 @@ export const InputRangeSelect = ({ } return ( -
+ {truncateValue(rangeType, getDisplayValue(rangeType))} -
+ ); }; diff --git a/src/input-select/input-select.tsx b/src/input-select/input-select.tsx index 01fdb29a88..94539d653a 100644 --- a/src/input-select/input-select.tsx +++ b/src/input-select/input-select.tsx @@ -1,5 +1,4 @@ import type { OpenChangeReason } from "@floating-ui/react"; -import clsx from "clsx"; import type React from "react"; import { useEffect, useRef, useState } from "react"; @@ -8,11 +7,12 @@ import { DropdownListState, ExpandableElement, } from "../shared/dropdown-list"; +import { ElementWithDropdown } from "../shared/dropdown-wrapper"; import { - type DropdownWrapperValueLabelDataAttrs, - ElementWithDropdown, -} from "../shared/dropdown-wrapper"; -import * as dropdownWrapperStyles from "../shared/dropdown-wrapper/dropdown-wrapper.styles"; + LabelContainer, + PlaceholderLabel, + ValueLabel, +} from "../shared/dropdown-wrapper/dropdown-wrapper"; import { InputBox } from "../shared/input-wrapper"; import { SimpleIdGenerator } from "../util"; import { StringHelper } from "../util/string-helper"; @@ -181,45 +181,33 @@ export const InputSelect = ({ // RENDER FUNCTIONS // ============================================================================= const renderLabel = () => { - const valueLabelProps: DropdownWrapperValueLabelDataAttrs = { - "data-variant": variant, - "data-truncate": optionTruncationType, - }; - if (!selected) { return ( -
{placeholder} -
+ ); } else if (renderCustomSelectedOption) { return renderCustomSelectedOption(selected); } else { return ( -
{truncateValue(getDisplayValue())} -
+ ); } }; const renderSelectorContent = () => ( -
+ {renderLabel()} -
+ ); const renderElement = () => { diff --git a/src/shared/dropdown-wrapper/dropdown-wrapper.tsx b/src/shared/dropdown-wrapper/dropdown-wrapper.tsx new file mode 100644 index 0000000000..aaed550e74 --- /dev/null +++ b/src/shared/dropdown-wrapper/dropdown-wrapper.tsx @@ -0,0 +1,63 @@ +import clsx from "clsx"; +import type React from "react"; + +import type { DropdownVariantType, TruncateType } from "../dropdown-list/types"; +import * as styles from "./dropdown-wrapper.styles"; + +// ============================================================================= +// SHARED PROPS +// ============================================================================= + +interface ValueLabelProps { + variant?: DropdownVariantType; + truncateType?: TruncateType; + children?: React.ReactNode; +} + +interface LabelContainerProps { + disabled?: boolean; + children?: React.ReactNode; + ref?: React.Ref; +} + +// ============================================================================= +// COMPONENTS +// ============================================================================= + +export const ValueLabel = ({ + variant, + truncateType, + children, +}: ValueLabelProps) => ( +
+ {children} +
+); + +export const PlaceholderLabel = ({ + variant, + truncateType, + children, +}: ValueLabelProps) => ( +
+ {children} +
+); + +export const LabelContainer = ({ + disabled, + children, + ref, +}: LabelContainerProps) => ( +
+ {children} +
+); diff --git a/src/shared/dropdown-wrapper/types.ts b/src/shared/dropdown-wrapper/types.ts index 823b9f268a..b5a376a126 100644 --- a/src/shared/dropdown-wrapper/types.ts +++ b/src/shared/dropdown-wrapper/types.ts @@ -1,4 +1,4 @@ -import type { DropdownVariantType, TruncateType } from "../dropdown-list/types"; +import type { DropdownVariantType } from "../dropdown-list/types"; export interface DropdownSelectorProps { children: React.ReactNode; @@ -13,8 +13,3 @@ export interface DropdownSelectorProps { } export type DropdownAlignmentType = "left" | "right"; - -export interface DropdownWrapperValueLabelDataAttrs { - "data-variant"?: DropdownVariantType; - "data-truncate"?: TruncateType; -} From 959f7f7d440c1dc451996fecd607c3ccef0f9370 Mon Sep 17 00:00:00 2001 From: Ghazwan Date: Thu, 16 Apr 2026 16:33:24 +0700 Subject: [PATCH 2/2] [BOOKINGSG-9158][GZ] move types & add forwardRef --- .../dropdown-wrapper/dropdown-wrapper.tsx | 44 ++++++------------- src/shared/dropdown-wrapper/types.ts | 13 +++++- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/shared/dropdown-wrapper/dropdown-wrapper.tsx b/src/shared/dropdown-wrapper/dropdown-wrapper.tsx index aaed550e74..989075b5a2 100644 --- a/src/shared/dropdown-wrapper/dropdown-wrapper.tsx +++ b/src/shared/dropdown-wrapper/dropdown-wrapper.tsx @@ -1,28 +1,8 @@ import clsx from "clsx"; -import type React from "react"; +import { forwardRef } from "react"; -import type { DropdownVariantType, TruncateType } from "../dropdown-list/types"; import * as styles from "./dropdown-wrapper.styles"; - -// ============================================================================= -// SHARED PROPS -// ============================================================================= - -interface ValueLabelProps { - variant?: DropdownVariantType; - truncateType?: TruncateType; - children?: React.ReactNode; -} - -interface LabelContainerProps { - disabled?: boolean; - children?: React.ReactNode; - ref?: React.Ref; -} - -// ============================================================================= -// COMPONENTS -// ============================================================================= +import type { LabelContainerProps, ValueLabelProps } from "./types"; export const ValueLabel = ({ variant, @@ -52,12 +32,16 @@ export const PlaceholderLabel = ({ ); -export const LabelContainer = ({ - disabled, - children, - ref, -}: LabelContainerProps) => ( -
- {children} -
+export const LabelContainer = forwardRef( + function LabelContainer({ disabled, children }, ref) { + return ( +
+ {children} +
+ ); + } ); diff --git a/src/shared/dropdown-wrapper/types.ts b/src/shared/dropdown-wrapper/types.ts index b5a376a126..74d58b8b6b 100644 --- a/src/shared/dropdown-wrapper/types.ts +++ b/src/shared/dropdown-wrapper/types.ts @@ -1,4 +1,4 @@ -import type { DropdownVariantType } from "../dropdown-list/types"; +import type { DropdownVariantType, TruncateType } from "../dropdown-list/types"; export interface DropdownSelectorProps { children: React.ReactNode; @@ -12,4 +12,15 @@ export interface DropdownSelectorProps { variant?: DropdownVariantType | undefined; } +export interface ValueLabelProps { + variant?: DropdownVariantType | undefined; + truncateType?: TruncateType | undefined; + children: React.ReactNode; +} + +export interface LabelContainerProps { + disabled?: boolean | undefined; + children: React.ReactNode; +} + export type DropdownAlignmentType = "left" | "right";