Files
helix-engage/src/components/base/toggle/toggle.tsx

139 lines
4.6 KiB
TypeScript

import type { ReactNode } from "react";
import type { SwitchProps as AriaSwitchProps } from "react-aria-components";
import { Switch as AriaSwitch } from "react-aria-components";
import { cx } from "@/utils/cx";
interface ToggleBaseProps {
size?: "sm" | "md";
slim?: boolean;
className?: string;
isHovered?: boolean;
isFocusVisible?: boolean;
isSelected?: boolean;
isDisabled?: boolean;
}
export const ToggleBase = ({ className, isHovered, isDisabled, isFocusVisible, isSelected, slim, size = "sm" }: ToggleBaseProps) => {
const styles = {
default: {
sm: {
root: "h-5 w-9 p-0.5",
switch: cx("size-4", isSelected && "translate-x-4"),
},
md: {
root: "h-6 w-11 p-0.5",
switch: cx("size-5", isSelected && "translate-x-5"),
},
},
slim: {
sm: {
root: "h-4 w-8",
switch: cx("size-4", isSelected && "translate-x-4"),
},
md: {
root: "h-5 w-10",
switch: cx("size-5", isSelected && "translate-x-5"),
},
},
};
const classes = slim ? styles.slim[size] : styles.default[size];
return (
<div
className={cx(
"cursor-pointer rounded-full bg-tertiary outline-focus-ring transition duration-150 ease-linear",
isSelected && "bg-brand-solid",
isSelected && isHovered && "bg-brand-solid_hover",
isDisabled && "cursor-not-allowed bg-disabled",
isFocusVisible && "outline-2 outline-offset-2",
slim && "ring-1 ring-secondary ring-inset",
slim && isSelected && "ring-transparent",
classes.root,
className,
)}
>
<div
style={{
transition: "transform 0.15s ease-in-out, translate 0.15s ease-in-out, border-color 0.1s linear, background-color 0.1s linear",
}}
className={cx(
"rounded-full bg-fg-white shadow-sm",
isDisabled && "bg-toggle-button-fg_disabled",
slim && "shadow-xs",
slim && "border border-toggle-border",
slim && isSelected && "border-toggle-slim-border_pressed",
slim && isSelected && isHovered && "border-toggle-slim-border_pressed-hover",
classes.switch,
)}
/>
</div>
);
};
interface ToggleProps extends AriaSwitchProps {
size?: "sm" | "md";
label?: string;
hint?: ReactNode;
slim?: boolean;
}
export const Toggle = ({ label, hint, className, size = "sm", slim, ...ariaSwitchProps }: ToggleProps) => {
const sizes = {
sm: {
root: "gap-2",
textWrapper: "",
label: "text-sm font-medium",
hint: "text-sm",
},
md: {
root: "gap-3",
textWrapper: "gap-0.5",
label: "text-md font-medium",
hint: "text-md",
},
};
return (
<AriaSwitch
{...ariaSwitchProps}
className={(renderProps) =>
cx(
"flex w-max items-start",
renderProps.isDisabled && "cursor-not-allowed",
sizes[size].root,
typeof className === "function" ? className(renderProps) : className,
)
}
>
{({ isSelected, isDisabled, isFocusVisible, isHovered }) => (
<>
<ToggleBase
slim={slim}
size={size}
isHovered={isHovered}
isDisabled={isDisabled}
isFocusVisible={isFocusVisible}
isSelected={isSelected}
className={slim ? "mt-0.5" : ""}
/>
{(label || hint) && (
<div className={cx("flex flex-col", sizes[size].textWrapper)}>
{label && <p className={cx("text-secondary select-none", sizes[size].label)}>{label}</p>}
{hint && (
<span className={cx("text-tertiary", sizes[size].hint)} onClick={(event) => event.stopPropagation()}>
{hint}
</span>
)}
</div>
)}
</>
)}
</AriaSwitch>
);
};