️ feat: create segmented controls component

This commit is contained in:
Wahyu Kurniawan 2024-02-22 02:18:47 +07:00
parent ea44efa0f2
commit aabda9b486
No known key found for this signature in database
GPG Key ID: 040A1549143A8E33
4 changed files with 247 additions and 0 deletions

View File

@ -0,0 +1,79 @@
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<ComponentPropsWithoutRef<'button'>, '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 iconSize = size === 'sm' ? 16 : 20;
const { item } = segmentedControlsTheme({ size, type });
return (
<button
{...props}
ref={ref}
className={item({ className })}
data-active={active}
>
{leftIcon && cloneIcon(leftIcon, { width: iconSize, height: iconSize })}
{children}
{rightIcon &&
cloneIcon(rightIcon, { width: iconSize, height: iconSize })}
</button>
);
},
);
SegmentedControlItem.displayName = 'SegmentedControlItem';
export { SegmentedControlItem };

View File

@ -0,0 +1,73 @@
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',
],
},
variants: {
size: {
sm: {
item: ['px-3', 'py-2', 'text-xs'],
},
md: {
item: ['px-4', 'py-3', 'text-sm', 'tracking-[-0.006em]'],
},
},
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
>;

View File

@ -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<SegmentedControlItemProps, 'children'> {
/**
* The label of the item.
*/
label: ReactNode;
/**
* The value of the item.
*
*/
value: string;
}
/**
* Represents the props for the SegmentedControls component.
*/
export interface SegmentedControlsProps<T extends string = string>
extends Omit<ComponentPropsWithoutRef<'div'>, '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<T extends string = string>({
className,
options,
value,
type,
size,
onChange,
...props
}: SegmentedControlsProps<T>) {
const { parent } = segmentedControlsTheme({ size, type });
/**
* Handles the change event for a given option.
*/
const handleChange = useCallback(
(option: T) => {
if (!option) return;
onChange?.(option);
},
[value, onChange],
);
return (
<div {...props} className={parent({ className })}>
{options.map((option, index) => (
<SegmentedControlItem
key={index}
active={value === option.value}
size={size}
type={type}
onClick={() => handleChange(option.value as T)}
{...option}
>
{option.label}
</SegmentedControlItem>
))}
</div>
);
}

View File

@ -0,0 +1,2 @@
export * from './SegmentedControlItem';
export * from './SegmentedControls';