Compare commits

..
Author SHA1 Message Date
Zachery Ng 54540cf8e9 feat: tooltip component 2024-02-22 17:28:38 +08:00
Zachery Ng 2715d0216c fix: button forwardRef 2024-02-22 17:28:18 +08:00
19 changed files with 0 additions and 780 deletions
-2
View File
@@ -7,8 +7,6 @@
"@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@testing-library/jest-dom": "^5.17.0",
@@ -50,7 +50,6 @@ export const inputTheme = tv(
'outline-offset-0',
'outline-border-danger',
'shadow-none',
'focus:outline-border-danger',
],
helperText: 'text-elements-danger',
},
@@ -1,54 +0,0 @@
import { VariantProps, tv } from 'tailwind-variants';
export const radioTheme = tv({
slots: {
root: ['flex', 'gap-3', 'flex-wrap'],
wrapper: ['flex', 'items-center', 'gap-2', 'group'],
label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'],
radio: [
'w-5',
'h-5',
'rounded-full',
'border',
'group',
'border-border-interactive/10',
'shadow-button',
'group-hover:border-border-interactive/[0.14]',
'focus-ring',
// Checked
'data-[state=checked]:bg-controls-primary',
'data-[state=checked]:group-hover:bg-controls-primary-hovered',
],
indicator: [
'flex',
'items-center',
'justify-center',
'relative',
'w-full',
'h-full',
'after:content-[""]',
'after:block',
'after:w-2.5',
'after:h-2.5',
'after:rounded-full',
'after:bg-transparent',
'after:group-hover:bg-controls-disabled',
'after:group-focus-visible:bg-controls-disabled',
// Checked
'after:data-[state=checked]:bg-elements-on-primary',
'after:data-[state=checked]:group-hover:bg-elements-on-primary',
'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary',
],
},
variants: {
orientation: {
vertical: { root: ['flex-col'] },
horizontal: { root: ['flex-row'] },
},
},
defaultVariants: {
orientation: 'vertical',
},
});
export type RadioTheme = VariantProps<typeof radioTheme>;
@@ -1,63 +0,0 @@
import React from 'react';
import {
Root as RadixRoot,
RadioGroupProps,
} from '@radix-ui/react-radio-group';
import { RadioTheme, radioTheme } from './Radio.theme';
import { RadioItem, RadioItemProps } from './RadioItem';
export interface RadioOption extends RadioItemProps {
/**
* The label of the radio option.
*/
label: string;
/**
* The value of the radio option.
*/
value: string;
}
export interface RadioProps extends RadioGroupProps, RadioTheme {
/**
* The options of the radio.
* @default []
* @example
* ```tsx
* const options = [
* {
* label: 'Label 1',
* value: '1',
* },
* {
* label: 'Label 2',
* value: '2',
* },
* {
* label: 'Label 3',
* value: '3',
* },
* ];
* ```
*/
options: RadioOption[];
}
/**
* The Radio component is used to select one option from a list of options.
*/
export const Radio = ({
className,
options,
orientation,
...props
}: RadioProps) => {
const { root } = radioTheme({ orientation });
return (
<RadixRoot {...props} className={root({ className })}>
{options.map((option) => (
<RadioItem key={option.value} {...option} />
))}
</RadixRoot>
);
};
@@ -1,74 +0,0 @@
import React, { ComponentPropsWithoutRef } from 'react';
import {
Item as RadixRadio,
Indicator as RadixIndicator,
RadioGroupItemProps,
RadioGroupIndicatorProps,
} from '@radix-ui/react-radio-group';
import { radioTheme } from './Radio.theme';
export interface RadioItemProps extends RadioGroupItemProps {
/**
* The wrapper props of the radio item.
* You can use this prop to customize the wrapper props.
*/
wrapperProps?: ComponentPropsWithoutRef<'div'>;
/**
* The label props of the radio item.
* You can use this prop to customize the label props.
*/
labelProps?: ComponentPropsWithoutRef<'label'>;
/**
* The indicator props of the radio item.
* You can use this prop to customize the indicator props.
*/
indicatorProps?: RadioGroupIndicatorProps;
/**
* The id of the radio item.
*/
id?: string;
/**
* The label of the radio item.
*/
label?: string;
}
/**
* The RadioItem component is used to render a radio item.
*/
export const RadioItem = ({
className,
wrapperProps,
labelProps,
indicatorProps,
label,
id,
...props
}: RadioItemProps) => {
const { wrapper, label: labelClass, radio, indicator } = radioTheme();
// Generate a unique id for the radio item from the label if the id is not provided
const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-');
const componentId = id ?? kebabCaseLabel;
return (
<div className={wrapper({ className: wrapperProps?.className })}>
<RadixRadio {...props} className={radio({ className })} id={componentId}>
<RadixIndicator
forceMount
{...indicatorProps}
className={indicator({ className: indicatorProps?.className })}
/>
</RadixRadio>
{label && (
<label
{...labelProps}
className={labelClass({ className: labelProps?.className })}
htmlFor={componentId}
>
{label}
</label>
)}
</div>
);
};
@@ -1,2 +0,0 @@
export * from './Radio';
export * from './RadioItem';
@@ -1,77 +0,0 @@
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 { item, icon } = segmentedControlsTheme({ size, type });
return (
<button
{...props}
ref={ref}
className={item({ className })}
data-active={active}
>
{leftIcon && cloneIcon(leftIcon, { className: icon({ size }) })}
{children}
{rightIcon && cloneIcon(rightIcon, { className: icon({ size }) })}
</button>
);
},
);
SegmentedControlItem.displayName = 'SegmentedControlItem';
export { SegmentedControlItem };
@@ -1,76 +0,0 @@
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
>;
@@ -1,93 +0,0 @@
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);
},
[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>
);
}
@@ -1,2 +0,0 @@
export * from './SegmentedControlItem';
export * from './SegmentedControls';
@@ -1,84 +0,0 @@
import { tv, type VariantProps } from 'tailwind-variants';
export const switchTheme = tv({
slots: {
wrapper: ['flex', 'items-start', 'gap-4', 'w-[375px]'],
switch: [
'h-6',
'w-12',
'rounded-full',
'transition-all',
'duration-500',
'relative',
'cursor-default',
'shadow-inset',
'focus-ring',
'outline-none',
],
thumb: [
'block',
'h-4',
'w-4',
'translate-x-1',
'transition-transform',
'duration-100',
'will-change-transform',
'rounded-full',
'shadow-button',
'data-[state=checked]:translate-x-7',
'bg-controls-elevated',
],
label: [
'flex',
'flex-1',
'flex-col',
'px-1',
'gap-1',
'text-sm',
'text-elements-high-em',
'tracking-[-0.006em]',
],
description: ['text-xs', 'text-elements-low-em'],
},
variants: {
checked: {
true: {
switch: [
'bg-controls-primary',
'hover:bg-controls-primary-hovered',
'focus-visible:bg-controls-primary-hovered',
],
},
false: {
switch: [
'bg-controls-inset',
'hover:bg-controls-inset-hovered',
'focus-visible:bg-controls-inset-hovered',
],
},
},
disabled: {
true: {
switch: ['bg-controls-disabled', 'cursor-not-allowed'],
thumb: ['bg-elements-on-disabled'],
},
},
fullWidth: {
true: {
wrapper: ['w-full', 'justify-between'],
},
},
},
compoundVariants: [
{
checked: true,
disabled: true,
class: {
switch: ['bg-controls-disabled-active'],
thumb: ['bg-snowball-900'],
},
},
],
});
export type SwitchVariants = VariantProps<typeof switchTheme>;
@@ -1,85 +0,0 @@
import React, { type ComponentPropsWithoutRef } from 'react';
import { type SwitchProps as SwitchRadixProps } from '@radix-ui/react-switch';
import * as SwitchRadix from '@radix-ui/react-switch';
import { switchTheme, type SwitchVariants } from './Switch.theme';
interface SwitchProps
extends Omit<SwitchRadixProps, 'checked'>,
SwitchVariants {
/**
* The label of the switch.
*/
label?: string;
/**
* The description of the switch.
*/
description?: string;
/**
* Custom wrapper props for the switch.
*/
wrapperProps?: ComponentPropsWithoutRef<'div'>;
/**
* Function that is called when the checked state of the switch changes.
* @param checked The new checked state of the switch.
*/
onCheckedChange?(checked: boolean): void;
}
/**
* A switch is a component used for toggling between two states.
*/
export const Switch = ({
className,
checked,
label,
description,
disabled,
name,
wrapperProps,
fullWidth,
...props
}: SwitchProps) => {
const {
wrapper,
switch: switchClass,
thumb,
label: labelClass,
description: descriptionClass,
} = switchTheme({
checked,
disabled,
fullWidth,
});
const switchComponent = (
<SwitchRadix.Root
{...props}
checked={checked}
disabled={disabled}
className={switchClass({ className })}
>
<SwitchRadix.Thumb className={thumb()} />
</SwitchRadix.Root>
);
// If a label is provided, wrap the switch in a label element.
if (label) {
return (
<div
{...wrapperProps}
className={wrapper({ className: wrapperProps?.className })}
>
<label className={labelClass()} htmlFor={name}>
{label}
{description && (
<span className={descriptionClass()}>{description}</span>
)}
</label>
{switchComponent}
</div>
);
}
return switchComponent;
};
@@ -1 +0,0 @@
export * from './Switch';
@@ -9,8 +9,6 @@ export const tabsTheme = tv({
// Horizontal default
'px-1',
'pb-5',
'cursor-default',
'select-none',
'text-elements-low-em',
'border-b-2',
'border-transparent',
@@ -18,11 +18,6 @@ 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';
import {
renderInlineNotificationWithDescriptions,
renderInlineNotifications,
@@ -33,10 +28,6 @@ import { renderTooltips } from './renders/tooltip';
const Page = () => {
const [singleDate, setSingleDate] = useState<Value>();
const [dateRange, setDateRange] = useState<Value>();
const [selectedSegmentedControl, setSelectedSegmentedControl] =
useState<string>('Test 1');
const [switchValue, setSwitchValue] = useState(false);
const [selectedRadio, setSelectedRadio] = useState<string>('');
return (
<div className="relative h-full min-h-full">
@@ -169,69 +160,6 @@ const Page = () => {
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Segmented Controls */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Segmented Controls</h1>
<div className="flex flex-col gap-10">
<SegmentedControls
options={SEGMENTED_CONTROLS_OPTIONS}
value={selectedSegmentedControl}
onChange={setSelectedSegmentedControl}
/>
<SegmentedControls
size="sm"
options={SEGMENTED_CONTROLS_OPTIONS}
value={selectedSegmentedControl}
onChange={setSelectedSegmentedControl}
/>
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Switch */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Switch</h1>
<div className="flex flex-col gap-10 items-center justify-center">
<Switch
label="Label"
checked={switchValue}
onCheckedChange={setSwitchValue}
/>
<Switch
label="Label"
description="Additional information or context"
checked={switchValue}
onCheckedChange={setSwitchValue}
/>
<Switch disabled label="Disabled unchecked" />
<Switch disabled checked label="Disabled checked" />
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Radio */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Radio</h1>
<div className="flex gap-20 items-start">
<Radio
options={RADIO_OPTIONS}
orientation="vertical"
value={selectedRadio}
onValueChange={setSelectedRadio}
/>
<Radio
options={RADIO_OPTIONS}
orientation="horizontal"
value={selectedRadio}
onValueChange={setSelectedRadio}
/>
</div>
</div>
<div className="w-full h border border-gray-200 px-20 my-10" />
{/* Inline notification */}
<div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Inline Notification</h1>
@@ -1,16 +0,0 @@
import { RadioOption } from 'components/shared/Radio';
export const RADIO_OPTIONS: RadioOption[] = [
{
label: 'Label 1',
value: '1',
},
{
label: 'Label 2',
value: '2',
},
{
label: 'Label 3',
value: '3',
},
];
@@ -1,44 +0,0 @@
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: (
<Badge size="xs" variant="tertiary">
1
</Badge>
),
},
{
label: 'Test 3',
value: 'Test 3',
rightIcon: (
<Badge size="xs" variant="tertiary">
1
</Badge>
),
},
{
label: 'Test 4',
value: 'Test 4',
leftIcon: (
<Badge size="xs" variant="tertiary">
1
</Badge>
),
rightIcon: (
<Badge size="xs" variant="tertiary">
1
</Badge>
),
},
{
label: 'Test 5',
value: 'Test 5',
disabled: true,
},
];
-1
View File
@@ -154,7 +154,6 @@ export default withMT({
calendar:
'0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)',
field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)',
},
spacing: {
2.5: '0.625rem',
-31
View File
@@ -3439,23 +3439,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-radio-group@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3"
integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-direction" "1.0.1"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-roving-focus" "1.0.4"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/react-roving-focus@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
@@ -3480,20 +3463,6 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-switch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e"
integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1"
"@radix-ui/react-use-previous" "1.0.1"
"@radix-ui/react-use-size" "1.0.1"
"@radix-ui/react-tabs@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"