[T-4866: feat] Switch component (#92)

* ️ feat: create switch component

* 📝 docs: add switch to the example page

* 🔧 chore: install `@radix-ui/react-switch`

* 🎨 style: add inset shadow

* 🎨 style: addjust input outline when error and focus
This commit is contained in:
Wahyu Kurniawan 2024-02-22 17:42:13 +07:00 committed by GitHub
parent d2ca4df35a
commit 7d1810ebd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 211 additions and 0 deletions

View File

@ -7,6 +7,7 @@
"@material-tailwind/react": "^2.1.7", "@material-tailwind/react": "^2.1.7",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^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-radio-group": "^1.1.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",

View File

@ -50,6 +50,7 @@ export const inputTheme = tv(
'outline-offset-0', 'outline-offset-0',
'outline-border-danger', 'outline-border-danger',
'shadow-none', 'shadow-none',
'focus:outline-border-danger',
], ],
helperText: 'text-elements-danger', helperText: 'text-elements-danger',
}, },

View File

@ -0,0 +1,84 @@
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>;

View File

@ -0,0 +1,85 @@
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;
};

View File

@ -0,0 +1 @@
export * from './Switch';

View File

@ -18,6 +18,7 @@ import {
renderTabs, renderTabs,
renderVerticalTabs, renderVerticalTabs,
} from './renders/tabs'; } from './renders/tabs';
import { Switch } from 'components/shared/Switch';
import { RADIO_OPTIONS } from './renders/radio'; import { RADIO_OPTIONS } from './renders/radio';
import { Radio } from 'components/shared/Radio'; import { Radio } from 'components/shared/Radio';
import { import {
@ -30,6 +31,7 @@ import { renderTooltips } from './renders/tooltip';
const Page = () => { const Page = () => {
const [singleDate, setSingleDate] = useState<Value>(); const [singleDate, setSingleDate] = useState<Value>();
const [dateRange, setDateRange] = useState<Value>(); const [dateRange, setDateRange] = useState<Value>();
const [switchValue, setSwitchValue] = useState(false);
const [selectedRadio, setSelectedRadio] = useState<string>(''); const [selectedRadio, setSelectedRadio] = useState<string>('');
return ( return (
@ -163,6 +165,28 @@ const Page = () => {
<div className="w-full h border border-gray-200 px-20 my-10" /> <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 */} {/* Radio */}
<div className="flex flex-col gap-10 items-center justify-between"> <div className="flex flex-col gap-10 items-center justify-between">
<h1 className="text-2xl font-bold">Radio</h1> <h1 className="text-2xl font-bold">Radio</h1>

View File

@ -154,6 +154,7 @@ export default withMT({
calendar: calendar:
'0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)', '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)', field: '0px 1px 2px rgba(0, 0, 0, 0.04)',
inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)',
}, },
spacing: { spacing: {
2.5: '0.625rem', 2.5: '0.625rem',

View File

@ -3480,6 +3480,20 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.1" "@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": "@radix-ui/react-tabs@^1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"