From 9b6c777f5f2abe67e1bc58b99423f0cd73b9f349 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Fri, 23 Feb 2024 10:20:08 +0700 Subject: [PATCH] [T-4865: feat] Segmented controls component (#91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⚡️ feat: create segmented controls component * 📝 docs: add segmented controls component to example page * ♻️ refactor: put the icon size to icon theme * 🐛 fix: remove `value` from `useCallback` dependency --- .../SegmentedControlItem.tsx | 77 +++++++++++++++ .../SegmentedControls.theme.ts | 76 +++++++++++++++ .../SegmentedControls/SegmentedControls.tsx | 93 +++++++++++++++++++ .../shared/SegmentedControls/index.ts | 2 + .../frontend/src/pages/components/index.tsx | 24 +++++ .../components/renders/segmentedControls.tsx | 44 +++++++++ 6 files changed, 316 insertions(+) create mode 100644 packages/frontend/src/components/shared/SegmentedControls/SegmentedControlItem.tsx create mode 100644 packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.theme.ts create mode 100644 packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.tsx create mode 100644 packages/frontend/src/components/shared/SegmentedControls/index.ts create mode 100644 packages/frontend/src/pages/components/renders/segmentedControls.tsx diff --git a/packages/frontend/src/components/shared/SegmentedControls/SegmentedControlItem.tsx b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControlItem.tsx new file mode 100644 index 00000000..3fe5bda6 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControlItem.tsx @@ -0,0 +1,77 @@ +import React, { + forwardRef, + type ComponentPropsWithoutRef, + type ReactNode, +} from 'react'; + +import { + segmentedControlsTheme, + type SegmentedControlsVariants, +} from './SegmentedControls.theme'; +import { cloneIcon } from 'utils/cloneIcon'; + +/** + * Interface for the props of a segmented control item component. + */ +export interface SegmentedControlItemProps + extends Omit, 'type' | 'children'>, + SegmentedControlsVariants { + /** + * The optional left icon element for a component. + */ + leftIcon?: ReactNode; + /** + * The optional right icon element to display. + */ + rightIcon?: ReactNode; + /** + * Indicates whether the item is active or not. + */ + active?: boolean; + /** + * Optional prop that represents the children of a React component. + */ + children?: ReactNode; +} + +/** + * A functional component that represents an item in a segmented control. + * @returns The rendered segmented control item. + */ +const SegmentedControlItem = forwardRef< + HTMLButtonElement, + SegmentedControlItemProps +>( + ( + { + className, + children, + size, + type, + leftIcon, + rightIcon, + active = false, + ...props + }, + ref, + ) => { + const { item, icon } = segmentedControlsTheme({ size, type }); + + return ( + + ); + }, +); + +SegmentedControlItem.displayName = 'SegmentedControlItem'; + +export { SegmentedControlItem }; diff --git a/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.theme.ts b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.theme.ts new file mode 100644 index 00000000..ed54a072 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.theme.ts @@ -0,0 +1,76 @@ +import { tv, type VariantProps } from 'tailwind-variants'; + +/** + * Defines the theme for a segmented controls. + */ +export const segmentedControlsTheme = tv({ + slots: { + parent: [ + 'flex', + 'items-center', + 'bg-base-bg-emphasized', + 'gap-0.5', + 'rounded-lg', + ], + item: [ + 'flex', + 'items-center', + 'justify-center', + 'gap-2', + 'text-elements-mid-em', + 'bg-transparent', + 'border', + 'border-transparent', + 'cursor-default', + 'whitespace-nowrap', + 'rounded-lg', + 'focus-ring', + 'hover:bg-controls-tertiary-hovered', + 'focus-visible:z-20', + 'focus-visible:bg-controls-tertiary-hovered', + 'disabled:text-controls-disabled', + 'disabled:bg-transparent', + 'disabled:cursor-not-allowed', + 'disabled:border-transparent', + 'data-[active=true]:bg-controls-tertiary', + 'data-[active=true]:text-elements-high-em', + 'data-[active=true]:border-border-interactive/10', + 'data-[active=true]:shadow-field', + 'data-[active=true]:hover:bg-controls-tertiary-hovered', + ], + icon: [], + }, + variants: { + size: { + sm: { + item: ['px-3', 'py-2', 'text-xs'], + icon: ['h-4', 'w-4'], + }, + md: { + item: ['px-4', 'py-3', 'text-sm', 'tracking-[-0.006em]'], + icon: ['h-5', 'w-5'], + }, + }, + type: { + 'fixed-width': { + parent: ['w-fit'], + item: ['w-fit'], + }, + 'full-width': { + parent: ['w-full'], + item: ['w-full'], + }, + }, + }, + defaultVariants: { + size: 'md', + type: 'fixed-width', + }, +}); + +/** + * Defines the type for the variants of a segmented controls. + */ +export type SegmentedControlsVariants = VariantProps< + typeof segmentedControlsTheme +>; diff --git a/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.tsx b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.tsx new file mode 100644 index 00000000..f96fe8c2 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.tsx @@ -0,0 +1,93 @@ +import React, { + useCallback, + type ComponentPropsWithoutRef, + type ReactNode, +} from 'react'; + +import { + SegmentedControlItem, + type SegmentedControlItemProps, +} from './SegmentedControlItem'; +import { + segmentedControlsTheme, + type SegmentedControlsVariants, +} from './SegmentedControls.theme'; + +/** + * Represents an option for a segmented control. + */ +export interface SegmentedControlsOption + extends Omit { + /** + * The label of the item. + */ + label: ReactNode; + /** + * The value of the item. + * + */ + value: string; +} + +/** + * Represents the props for the SegmentedControls component. + */ +export interface SegmentedControlsProps + extends Omit, 'onChange'>, + SegmentedControlsVariants { + /** + * An array of options for a segmented control component. + */ + options: SegmentedControlsOption[]; + /** + * An optional string value. + */ + value?: T; + /** + * Optional callback function to handle changes in state. + */ + onChange?: (v: T) => void; +} + +/** + * A component that renders segmented controls with customizable options. + */ +export function SegmentedControls({ + className, + options, + value, + type, + size, + onChange, + ...props +}: SegmentedControlsProps) { + const { parent } = segmentedControlsTheme({ size, type }); + + /** + * Handles the change event for a given option. + */ + const handleChange = useCallback( + (option: T) => { + if (!option) return; + onChange?.(option); + }, + [onChange], + ); + + return ( +
+ {options.map((option, index) => ( + handleChange(option.value as T)} + {...option} + > + {option.label} + + ))} +
+ ); +} diff --git a/packages/frontend/src/components/shared/SegmentedControls/index.ts b/packages/frontend/src/components/shared/SegmentedControls/index.ts new file mode 100644 index 00000000..5d569509 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/index.ts @@ -0,0 +1,2 @@ +export * from './SegmentedControlItem'; +export * from './SegmentedControls'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index fc6acbec..e6dd56bc 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -18,6 +18,8 @@ import { renderTabs, renderVerticalTabs, } from './renders/tabs'; +import { SegmentedControls } from 'components/shared/SegmentedControls'; +import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls'; import { Switch } from 'components/shared/Switch'; import { RADIO_OPTIONS } from './renders/radio'; import { Radio } from 'components/shared/Radio'; @@ -32,6 +34,8 @@ import { renderTooltips } from './renders/tooltip'; const Page = () => { const [singleDate, setSingleDate] = useState(); const [dateRange, setDateRange] = useState(); + const [selectedSegmentedControl, setSelectedSegmentedControl] = + useState('Test 1'); const [switchValue, setSwitchValue] = useState(false); const [selectedRadio, setSelectedRadio] = useState(''); @@ -176,6 +180,26 @@ const Page = () => {
+ {/* Segmented Controls */} +
+

Segmented Controls

+
+ + +
+
+ +
+ {/* Switch */}

Switch

diff --git a/packages/frontend/src/pages/components/renders/segmentedControls.tsx b/packages/frontend/src/pages/components/renders/segmentedControls.tsx new file mode 100644 index 00000000..8f4a019f --- /dev/null +++ b/packages/frontend/src/pages/components/renders/segmentedControls.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Badge } from 'components/shared/Badge'; +import { SegmentedControlsOption } from 'components/shared/SegmentedControls'; + +export const SEGMENTED_CONTROLS_OPTIONS: SegmentedControlsOption[] = [ + { label: 'Test 1', value: 'Test 1' }, + { + label: 'Test 2', + value: 'Test 2', + leftIcon: ( + + 1 + + ), + }, + { + label: 'Test 3', + value: 'Test 3', + rightIcon: ( + + 1 + + ), + }, + { + label: 'Test 4', + value: 'Test 4', + leftIcon: ( + + 1 + + ), + rightIcon: ( + + 1 + + ), + }, + { + label: 'Test 5', + value: 'Test 5', + disabled: true, + }, +];