Updates
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import { ChevronDown, Info, LucideProps } from "lucide-react";
|
||||
import React, {
|
||||
ComponentProps,
|
||||
DetailedHTMLProps,
|
||||
Dispatch,
|
||||
InputHTMLAttributes,
|
||||
LabelHTMLAttributes,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
SelectHTMLAttributes,
|
||||
SetStateAction,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Row from "../layout/Row";
|
||||
import Dropdown from "../elements/Dropdown";
|
||||
import Card from "../elements/Card";
|
||||
import Span from "../layout/Span";
|
||||
import Stack from "../layout/Stack";
|
||||
import twuiSlugify from "../utils/slugify";
|
||||
import twuiSlugToNormalText from "../utils/slug-to-normal-text";
|
||||
|
||||
export type TWUISelectValidityObject = {
|
||||
isValid?: boolean;
|
||||
msg?: string;
|
||||
};
|
||||
|
||||
export type TWUISelectOptionObject<
|
||||
KeyType extends string = string,
|
||||
T extends { [k: string]: any } = { [k: string]: any }
|
||||
> = {
|
||||
title?: string;
|
||||
value: KeyType;
|
||||
default?: boolean;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
export type TWUISelectProps<
|
||||
KeyType extends string,
|
||||
T extends { [k: string]: any } = { [k: string]: any }
|
||||
> = DetailedHTMLProps<
|
||||
SelectHTMLAttributes<HTMLSelectElement>,
|
||||
HTMLSelectElement
|
||||
> & {
|
||||
options: TWUISelectOptionObject<KeyType, T>[];
|
||||
label?: string;
|
||||
showLabel?: boolean;
|
||||
wrapperProps?: DetailedHTMLProps<
|
||||
InputHTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
wrapperWrapperProps?: ComponentProps<typeof Stack>;
|
||||
labelProps?: DetailedHTMLProps<
|
||||
LabelHTMLAttributes<HTMLLabelElement>,
|
||||
HTMLLabelElement
|
||||
>;
|
||||
componentRef?: RefObject<HTMLSelectElement>;
|
||||
iconProps?: LucideProps;
|
||||
changeHandler?: (value: KeyType, data?: T) => void;
|
||||
info?: string | ReactNode;
|
||||
validateValueFn?: (value: string) => Promise<TWUISelectValidityObject>;
|
||||
dispatchState?: Dispatch<SetStateAction<T | undefined>>;
|
||||
name?: KeyType;
|
||||
};
|
||||
|
||||
export type TWUISelectValueObject<
|
||||
KeyType extends string,
|
||||
T extends { [k: string]: any } = { [k: string]: any }
|
||||
> = {
|
||||
value: KeyType;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Select Element
|
||||
* @className twui-select-wrapper
|
||||
* @className twui-select
|
||||
* @className twui-select-dropdown-icon
|
||||
*/
|
||||
export default function Select<
|
||||
KeyType extends string,
|
||||
T extends { [k: string]: any } = { [k: string]: any }
|
||||
>({
|
||||
label,
|
||||
options,
|
||||
componentRef,
|
||||
labelProps,
|
||||
wrapperProps,
|
||||
showLabel,
|
||||
iconProps,
|
||||
changeHandler,
|
||||
info,
|
||||
validateValueFn,
|
||||
wrapperWrapperProps,
|
||||
dispatchState,
|
||||
...props
|
||||
}: TWUISelectProps<KeyType, T>) {
|
||||
const [validity, setValidity] = React.useState<TWUISelectValidityObject>({
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
const selectRef = componentRef || React.useRef<HTMLSelectElement>(null);
|
||||
|
||||
const [value, setValue] = React.useState<TWUISelectValueObject<KeyType, T>>(
|
||||
{
|
||||
value: options[0]?.value,
|
||||
data: options[0]?.data,
|
||||
}
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const currentSelectValue = selectRef.current?.value;
|
||||
|
||||
if (currentSelectValue && validateValueFn) {
|
||||
validateValueFn(currentSelectValue).then((res) => {
|
||||
setValidity(res);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatchState?.(value.data);
|
||||
}, [value]);
|
||||
|
||||
const selectID = label
|
||||
? twuiSlugify(label)
|
||||
: props.name
|
||||
? twuiSlugify(props.name)
|
||||
: props.title
|
||||
? twuiSlugify(props.title)
|
||||
: `select-${Math.round(Math.random() * 1000000)}`;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
{...wrapperWrapperProps}
|
||||
className={twMerge("gap-1", wrapperWrapperProps?.className)}
|
||||
>
|
||||
<div
|
||||
{...wrapperProps}
|
||||
className={twMerge(
|
||||
"relative w-full flex items-center border rounded-default",
|
||||
"border-slate-300 dark:border-white/20 pr-2",
|
||||
"focus:border-slate-700 dark:focus:border-white/50",
|
||||
"outline-slate-300 dark:outline-white/20",
|
||||
"focus:outline-slate-700 dark:focus:outline-white/50",
|
||||
"bg-white dark:bg-background-dark",
|
||||
validity.isValid ? "" : "outline-warning border-warning",
|
||||
wrapperProps?.className
|
||||
)}
|
||||
>
|
||||
{showLabel && (
|
||||
<label
|
||||
htmlFor={selectID}
|
||||
{...labelProps}
|
||||
className={twMerge(
|
||||
"text-xs absolute -top-2.5 left-2 text-foreground-light/80 bg-background-light",
|
||||
"dark:text-foreground-dark/70 dark:bg-background-dark px-1.5 rounded-t",
|
||||
"twui-input-label",
|
||||
labelProps?.className
|
||||
)}
|
||||
>
|
||||
{label || props.title || props.name}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<select
|
||||
id={selectID}
|
||||
aria-label={props["aria-label"] || props.title}
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"w-full pl-3 py-2 rounded-default appearance-none pr-8",
|
||||
"grow !border-none !outline-none",
|
||||
"twui-select",
|
||||
props.className
|
||||
)}
|
||||
ref={selectRef}
|
||||
value={
|
||||
options.flat().find((opt) => opt.default)?.value ||
|
||||
undefined
|
||||
}
|
||||
onChange={(e) => {
|
||||
const targetValue = options.find(
|
||||
(opt) => opt.value == e.target.value
|
||||
);
|
||||
|
||||
if (targetValue) {
|
||||
setValue(targetValue);
|
||||
}
|
||||
|
||||
changeHandler?.(
|
||||
e.target.value as (typeof options)[number]["value"],
|
||||
targetValue?.data
|
||||
);
|
||||
|
||||
props.onChange?.(e);
|
||||
|
||||
validateValueFn?.(e.target.value).then((res) => {
|
||||
setValidity(res);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{options.flat().map((option, index) => {
|
||||
const optionTitle =
|
||||
option.title || twuiSlugToNormalText(option.value);
|
||||
|
||||
return (
|
||||
<option key={index} value={option.value}>
|
||||
{optionTitle}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
|
||||
<ChevronDown
|
||||
size={20}
|
||||
{...iconProps}
|
||||
className={twMerge(
|
||||
"pointer-events-none -ml-6",
|
||||
iconProps?.className
|
||||
)}
|
||||
/>
|
||||
|
||||
{info && (
|
||||
<Dropdown
|
||||
target={
|
||||
<div title="Select Info Button">
|
||||
<Info size={20} />
|
||||
</div>
|
||||
}
|
||||
hoverOpen
|
||||
>
|
||||
<Card className="min-w-[250px] p-6">
|
||||
{typeof info == "string" ? (
|
||||
<Span>{info}</Span>
|
||||
) : (
|
||||
info
|
||||
)}
|
||||
</Card>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
{!validity.isValid && validity.msg ? (
|
||||
<Span size="smaller" className="text-warning">
|
||||
{validity.msg}
|
||||
</Span>
|
||||
) : undefined}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user