⚡️ feat: create segmented controls component
This commit is contained in:
parent
ea44efa0f2
commit
aabda9b486
@ -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 };
|
@ -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
|
||||
>;
|
@ -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>
|
||||
);
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './SegmentedControlItem';
|
||||
export * from './SegmentedControls';
|
Loading…
Reference in New Issue
Block a user