⚡️ 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