200 lines
6.7 KiB
TypeScript
200 lines
6.7 KiB
TypeScript
import * as React from "react";
|
|
import { forwardRef, useEffect, useState } from "react";
|
|
import { cn } from "~/utils/cn";
|
|
import { Badge } from "./Badge";
|
|
import { Paragraph } from "./Paragraph";
|
|
|
|
const variants = {
|
|
"simple/small": {
|
|
button: "w-fit pr-4",
|
|
label: "text-sm text-text-bright mt-0.5 select-none",
|
|
description: "text-text-dimmed",
|
|
inputPosition: "mt-1",
|
|
isChecked: "",
|
|
isDisabled: "opacity-70",
|
|
},
|
|
simple: {
|
|
button: "w-fit pr-4",
|
|
label: "text-text-bright select-none",
|
|
description: "text-text-dimmed",
|
|
inputPosition: "mt-1",
|
|
isChecked: "",
|
|
isDisabled: "opacity-70",
|
|
},
|
|
"button/small": {
|
|
button:
|
|
"flex items-center w-fit h-8 pl-2 pr-3 rounded border border-border-bright hover:bg-background-dimmed hover:border-border-brightest transition",
|
|
label: "text-sm text-text-bright select-none",
|
|
description: "text-text-dimmed",
|
|
inputPosition: "mt-0",
|
|
isChecked: "bg-background-dimmed border-grid-dimmed hover:bg-background-dimmed!",
|
|
isDisabled: "opacity-70 hover:bg-transparent",
|
|
},
|
|
button: {
|
|
button:
|
|
"w-fit py-2 pl-3 pr-4 rounded border border-border-bright hover:bg-background-dimmed hover:border-border-brightest transition",
|
|
label: "text-text-bright select-none",
|
|
description: "text-text-dimmed",
|
|
inputPosition: "mt-1",
|
|
isChecked: "bg-background-dimmed border-grid-dimmed hover:bg-background-dimmed!",
|
|
isDisabled: "opacity-70 hover:bg-transparent",
|
|
},
|
|
description: {
|
|
button: "w-full py-2 pl-3 pr-4 checked:hover:bg-background-dimmed transition",
|
|
label: "text-text-bright font-semibold",
|
|
description: "text-text-dimmed",
|
|
inputPosition: "mt-1",
|
|
isChecked: "bg-background-dimmed",
|
|
isDisabled: "opacity-70",
|
|
},
|
|
};
|
|
|
|
export type CheckboxProps = Omit<
|
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
"checked" | "onChange"
|
|
> & {
|
|
id?: string;
|
|
name?: string;
|
|
value?: string;
|
|
variant?: keyof typeof variants;
|
|
label: React.ReactNode;
|
|
description?: string;
|
|
badges?: string[];
|
|
className?: string;
|
|
labelClassName?: string;
|
|
onChange?: (isChecked: boolean) => void;
|
|
};
|
|
|
|
export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
|
(
|
|
{
|
|
id,
|
|
name,
|
|
value,
|
|
variant = "simple",
|
|
type,
|
|
label,
|
|
description,
|
|
defaultChecked,
|
|
badges,
|
|
disabled,
|
|
className,
|
|
labelClassName: externalLabelClassName,
|
|
...props
|
|
},
|
|
ref
|
|
) => {
|
|
const [isChecked, setIsChecked] = useState<boolean>(defaultChecked ?? false);
|
|
const [isDisabled, setIsDisabled] = useState<boolean>(disabled ?? false);
|
|
|
|
const buttonClassName = variants[variant].button;
|
|
const labelClassName = variants[variant].label;
|
|
const descriptionClassName = variants[variant].description;
|
|
const isCheckedClassName = variants[variant].isChecked;
|
|
const isDisabledClassName = variants[variant].isDisabled;
|
|
const inputPositionClasses = variants[variant].inputPosition;
|
|
|
|
useEffect(() => {
|
|
setIsDisabled(disabled ?? false);
|
|
}, [disabled]);
|
|
|
|
useEffect(() => {
|
|
if (props.onChange) {
|
|
props.onChange(isChecked);
|
|
}
|
|
}, [isChecked]);
|
|
|
|
useEffect(() => {
|
|
setIsChecked(defaultChecked ?? false);
|
|
}, [defaultChecked]);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"group flex items-start gap-x-2 transition ",
|
|
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
|
|
buttonClassName,
|
|
isChecked && isCheckedClassName,
|
|
(isDisabled || props.readOnly) && isDisabledClassName,
|
|
className
|
|
)}
|
|
onClick={(e) => {
|
|
//returning false is not setting the state to false, it stops the event from bubbling up
|
|
if (isDisabled || props.readOnly === true) return false;
|
|
setIsChecked((c) => !c);
|
|
}}
|
|
>
|
|
<input
|
|
{...props}
|
|
name={name}
|
|
type="checkbox"
|
|
value={value}
|
|
checked={isChecked}
|
|
onChange={(e) => {
|
|
//returning false is not setting the state to false, it stops the event from bubbling up
|
|
if (isDisabled || props.readOnly === true) return false;
|
|
setIsChecked(!isChecked);
|
|
}}
|
|
disabled={isDisabled}
|
|
className={cn(
|
|
inputPositionClasses,
|
|
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
|
|
"read-only:border-border-bright disabled:border-border-bright rounded-sm border border-border-bright bg-transparent transition checked:bg-indigo-500! read-only:bg-background-raised! group-hover:bg-background-deep checked:group-hover:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-hidden focus-visible:ring-indigo-500 disabled:bg-background-raised!"
|
|
)}
|
|
id={id}
|
|
ref={ref}
|
|
/>
|
|
<div>
|
|
<div className="flex items-center gap-x-2">
|
|
<label
|
|
htmlFor={id}
|
|
className={cn(
|
|
props.readOnly || disabled ? "cursor-default" : "cursor-pointer",
|
|
labelClassName,
|
|
externalLabelClassName
|
|
)}
|
|
onClick={(e) => e.preventDefault()}
|
|
>
|
|
{label}
|
|
</label>
|
|
{badges && (
|
|
<span className="-mr-2 flex gap-x-1.5">
|
|
{badges.map((badge) => (
|
|
<Badge key={badge}>{badge}</Badge>
|
|
))}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{variant === "description" && (
|
|
<Paragraph variant="small" className={cn("mt-0.5", descriptionClassName)}>
|
|
{description}
|
|
</Paragraph>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
type SimpleCheckboxProps = Omit<React.ComponentProps<"input">, "type">;
|
|
|
|
export const Checkbox = forwardRef<HTMLInputElement, SimpleCheckboxProps>(
|
|
({ className, ...props }: SimpleCheckboxProps, ref) => {
|
|
return (
|
|
<input
|
|
type="checkbox"
|
|
className={cn(
|
|
props.disabled
|
|
? "cursor-not-allowed"
|
|
: props.readOnly
|
|
? "cursor-default"
|
|
: "cursor-pointer",
|
|
"read-only:border-border-bright disabled:border-border-bright disabled:opacity-50 rounded-sm border border-border-bright bg-transparent transition checked:bg-indigo-500! read-only:bg-background-raised! group-hover:bg-background-deep checked:group-hover:bg-indigo-500 group-focus:ring-1 focus:ring-indigo-500 focus:ring-offset-0 focus:ring-offset-transparent focus-visible:outline-hidden focus-visible:ring-indigo-500 disabled:bg-background-raised!"
|
|
)}
|
|
{...props}
|
|
ref={ref}
|
|
/>
|
|
);
|
|
}
|
|
);
|