Updates
This commit is contained in:
@@ -11,10 +11,14 @@ import Row from "../layout/Row";
|
||||
import { Info } from "lucide-react";
|
||||
import Span from "../layout/Span";
|
||||
|
||||
export type CheckboxProps = React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
export type CheckboxProps = Omit<
|
||||
React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>,
|
||||
"title"
|
||||
> & {
|
||||
title?: string | ReactNode;
|
||||
wrapperProps?: DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
@@ -29,6 +33,7 @@ export type CheckboxProps = React.DetailedHTMLProps<
|
||||
setChecked?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
checked?: boolean;
|
||||
readOnly?: boolean;
|
||||
noLabel?: boolean;
|
||||
size?: number;
|
||||
changeHandler?: (value: boolean) => void;
|
||||
info?: string | ReactNode;
|
||||
@@ -54,6 +59,8 @@ export default function Checkbox({
|
||||
changeHandler,
|
||||
info,
|
||||
wrapperWrapperProps,
|
||||
noLabel,
|
||||
title,
|
||||
...props
|
||||
}: CheckboxProps) {
|
||||
const finalSize = size || 20;
|
||||
@@ -62,8 +69,8 @@ export default function Checkbox({
|
||||
defaultChecked || externalChecked || false
|
||||
);
|
||||
|
||||
const finalTitle = props.title
|
||||
? props.title
|
||||
const finalTitle = title
|
||||
? title
|
||||
: `Checkbox-${Math.round(Math.random() * 100000)}`;
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -112,17 +119,19 @@ export default function Checkbox({
|
||||
>
|
||||
{checked && <CheckMarkSVG />}
|
||||
</div>
|
||||
<Stack className="gap-0.5">
|
||||
<div
|
||||
{...labelProps}
|
||||
className={twMerge(
|
||||
"select-none whitespace-normal md:whitespace-nowrap",
|
||||
labelProps?.className
|
||||
)}
|
||||
>
|
||||
{label || finalTitle}
|
||||
</div>
|
||||
</Stack>
|
||||
{!noLabel && (
|
||||
<Stack className="gap-0.5">
|
||||
<div
|
||||
{...labelProps}
|
||||
className={twMerge(
|
||||
"select-none whitespace-normal md:whitespace-nowrap",
|
||||
labelProps?.className
|
||||
)}
|
||||
>
|
||||
{label || finalTitle}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
{info && (
|
||||
<Row className="gap-1" title={info.toString()}>
|
||||
|
||||
@@ -12,12 +12,23 @@ import Row from "../layout/Row";
|
||||
import Input from "./Input";
|
||||
import Loading from "../elements/Loading";
|
||||
|
||||
type FileInputUtils = {
|
||||
clearFileInput?: () => void;
|
||||
};
|
||||
|
||||
type ImageUploadProps = DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
onChangeHandler?: (
|
||||
fileData: FileInputToBase64FunctionReturn | undefined
|
||||
fileData: FileInputToBase64FunctionReturn | undefined,
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>,
|
||||
utils?: FileInputUtils
|
||||
) => any;
|
||||
changeHandler?: (
|
||||
fileData: FileInputToBase64FunctionReturn | undefined,
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>,
|
||||
utils?: FileInputUtils
|
||||
) => any;
|
||||
onClear?: () => void;
|
||||
fileInputProps?: DetailedHTMLProps<
|
||||
@@ -74,6 +85,7 @@ export default function FileUpload({
|
||||
loading,
|
||||
multiple,
|
||||
onClear,
|
||||
changeHandler,
|
||||
...props
|
||||
}: ImageUploadProps) {
|
||||
const [file, setFile] = React.useState<
|
||||
@@ -98,6 +110,19 @@ export default function FileUpload({
|
||||
}
|
||||
}, [existingFile]);
|
||||
|
||||
function clearFileInput() {
|
||||
setFile(undefined);
|
||||
externalSetFile?.(undefined);
|
||||
onChangeHandler?.(undefined);
|
||||
changeHandler?.(undefined);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
onClear?.();
|
||||
}
|
||||
|
||||
const fileInputUtils: FileInputUtils = { clearFileInput };
|
||||
|
||||
return (
|
||||
<Stack
|
||||
{...props}
|
||||
@@ -136,7 +161,12 @@ export default function FileUpload({
|
||||
(res) => {
|
||||
setFile(res);
|
||||
externalSetFile?.(res);
|
||||
onChangeHandler?.(res);
|
||||
onChangeHandler?.(
|
||||
res,
|
||||
inputRef,
|
||||
fileInputUtils
|
||||
);
|
||||
changeHandler?.(res, inputRef, fileInputUtils);
|
||||
fileInputProps?.onChange?.(e);
|
||||
}
|
||||
);
|
||||
@@ -191,13 +221,7 @@ export default function FileUpload({
|
||||
"hover:bg-white dark:hover:bg-black"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
setFile(undefined);
|
||||
externalSetFile?.(undefined);
|
||||
onChangeHandler?.(undefined);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
onClear?.();
|
||||
clearFileInput();
|
||||
}}
|
||||
title="Cancel File Upload Button"
|
||||
>
|
||||
@@ -250,6 +274,7 @@ export default function FileUpload({
|
||||
setFile(undefined);
|
||||
externalSetFile?.(undefined);
|
||||
onChangeHandler?.(undefined);
|
||||
changeHandler?.(undefined);
|
||||
setFileUrl(undefined);
|
||||
}}
|
||||
title="Cancel File Button"
|
||||
@@ -300,6 +325,7 @@ export default function FileUpload({
|
||||
setFile(res);
|
||||
externalSetFile?.(res);
|
||||
onChangeHandler?.(res);
|
||||
changeHandler?.(res);
|
||||
}
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -75,6 +75,10 @@ export type InputProps<KeyType extends string> = DetailedHTMLProps<
|
||||
info?: string | ReactNode;
|
||||
ready?: boolean;
|
||||
validity?: TWUISelectValidityObject;
|
||||
clearInputProps?: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
};
|
||||
|
||||
let refreshes = 0;
|
||||
@@ -114,6 +118,7 @@ export default function Input<KeyType extends string>(
|
||||
info,
|
||||
changeHandler,
|
||||
validity: existingValidity,
|
||||
clearInputProps,
|
||||
...props
|
||||
} = inputProps;
|
||||
|
||||
@@ -256,7 +261,7 @@ export default function Input<KeyType extends string>(
|
||||
}
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"w-full outline-none bg-transparent",
|
||||
"w-full outline-none bg-transparent grow",
|
||||
"twui-textarea",
|
||||
props.className
|
||||
)}
|
||||
@@ -285,7 +290,7 @@ export default function Input<KeyType extends string>(
|
||||
"w-full outline-none bg-transparent border-none",
|
||||
"hover:border-none hover:outline-none focus:border-none focus:outline-none",
|
||||
"dark:bg-transparent dark:outline-none dark:border-none",
|
||||
"p-0",
|
||||
"p-0 grow",
|
||||
"twui-input",
|
||||
props.className
|
||||
)}
|
||||
@@ -301,6 +306,7 @@ export default function Input<KeyType extends string>(
|
||||
onChange={handleValueChange}
|
||||
type={inputType}
|
||||
defaultValue={defaultInitialValue}
|
||||
autoComplete={autoComplete}
|
||||
value={props.value ? getFinalValue(props.value) : undefined}
|
||||
/>
|
||||
);
|
||||
@@ -381,10 +387,12 @@ export default function Input<KeyType extends string>(
|
||||
{props.type == "search" || props.readOnly ? null : (
|
||||
<div
|
||||
title="Clear Input Field"
|
||||
{...clearInputProps}
|
||||
className={twMerge(
|
||||
"p-1 -my-2 -mx-1 opacity-0 cursor-pointer",
|
||||
"p-1 -my-2 -mx-1 opacity-0 cursor-pointer w-7 h-7",
|
||||
"bg-background-light dark:bg-background-dark",
|
||||
"twui-clear-input-field-button"
|
||||
"twui-clear-input-field-button",
|
||||
clearInputProps?.className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -397,9 +405,10 @@ export default function Input<KeyType extends string>(
|
||||
}
|
||||
|
||||
updateValue("");
|
||||
clearInputProps?.onClick?.(e);
|
||||
}}
|
||||
>
|
||||
<X size={15} />
|
||||
<X className="w-full h-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ComponentProps,
|
||||
DetailedHTMLProps,
|
||||
InputHTMLAttributes,
|
||||
LabelHTMLAttributes,
|
||||
} from "react";
|
||||
import Row from "../layout/Row";
|
||||
import twuiSlugify from "../utils/slugify";
|
||||
import twuiSlugToNormalText from "../utils/slug-to-normal-text";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Value = {
|
||||
value: string;
|
||||
title?: string;
|
||||
default?: boolean;
|
||||
};
|
||||
|
||||
export type TWUI_FORM_RADIO_PROPS = {
|
||||
values: Value[];
|
||||
name: string;
|
||||
inputProps?: DetailedHTMLProps<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
>;
|
||||
labelProps?: DetailedHTMLProps<
|
||||
LabelHTMLAttributes<HTMLLabelElement>,
|
||||
HTMLLabelElement
|
||||
>;
|
||||
wrapperProps?: ComponentProps<typeof Row>;
|
||||
changeHandler?: (value: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Form Radios Component
|
||||
* @className twui-textarea
|
||||
*/
|
||||
export default function Radios({
|
||||
values,
|
||||
name,
|
||||
inputProps,
|
||||
labelProps,
|
||||
wrapperProps,
|
||||
changeHandler,
|
||||
}: TWUI_FORM_RADIO_PROPS) {
|
||||
const finalName = twuiSlugify(name);
|
||||
const finalTitle = twuiSlugToNormalText(finalName);
|
||||
|
||||
return (
|
||||
<Row
|
||||
title={finalTitle}
|
||||
{...wrapperProps}
|
||||
className={twMerge("gap-4", wrapperProps?.className)}
|
||||
>
|
||||
{values.map((v, i) => {
|
||||
const valueName = twuiSlugify(`${finalName}-${v.value}`);
|
||||
const valueTitle = v.title || twuiSlugToNormalText(v.value);
|
||||
|
||||
return (
|
||||
<Row key={i} className="gap-1.5">
|
||||
<input
|
||||
id={valueName}
|
||||
type="radio"
|
||||
defaultChecked={v.default}
|
||||
name={finalName}
|
||||
onChange={(e) => {
|
||||
const targetValue = v.value;
|
||||
changeHandler?.(targetValue);
|
||||
}}
|
||||
{...inputProps}
|
||||
/>
|
||||
<label htmlFor={valueName} {...labelProps}>
|
||||
{valueTitle}
|
||||
</label>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -47,17 +47,23 @@ export default function SearchSelect<
|
||||
const [currentOptions, setCurrentOptions] =
|
||||
React.useState<TWUISelectOptionObject<KeyType, T>[]>(options);
|
||||
|
||||
const defaultOption = options.find((opt) => opt.default) || options[0];
|
||||
const defaultOption = (options.find((opt) => opt.default) || options[0]) as
|
||||
| TWUISelectOptionObject<KeyType, T>
|
||||
| undefined;
|
||||
|
||||
const [value, setValue] = React.useState<
|
||||
TWUISelectOptionObject<KeyType, T>
|
||||
>({
|
||||
value: defaultOption.value,
|
||||
data: defaultOption.data,
|
||||
});
|
||||
TWUISelectOptionObject<KeyType, T> | undefined
|
||||
>(
|
||||
defaultOption
|
||||
? {
|
||||
value: defaultOption?.value,
|
||||
data: defaultOption?.data,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
const [inputValue, setInputValue] = React.useState<string>(
|
||||
defaultOption.value
|
||||
defaultOption?.value || ""
|
||||
);
|
||||
const [selectIndex, setSelectIndex] = React.useState<number | undefined>();
|
||||
|
||||
@@ -91,11 +97,14 @@ export default function SearchSelect<
|
||||
}, [open]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatchState?.(value.data);
|
||||
setInputValue(value.value);
|
||||
if (value) {
|
||||
dispatchState?.(value.data);
|
||||
setInputValue(value.value);
|
||||
changeHandler?.(value.value);
|
||||
}
|
||||
|
||||
clearTimeout(focusTimeout);
|
||||
setOpen(false);
|
||||
changeHandler?.(value.value);
|
||||
setSelectIndex(undefined);
|
||||
}, [value]);
|
||||
|
||||
@@ -246,7 +255,7 @@ export default function SearchSelect<
|
||||
targetWrapperProps={{ className: "w-full" }}
|
||||
contentWrapperProps={{ className: "w-full" }}
|
||||
className="w-full"
|
||||
externalOpen={open}
|
||||
externalOpen={currentOptions?.[0] && open}
|
||||
>
|
||||
<Paper
|
||||
className={twMerge(
|
||||
@@ -255,37 +264,39 @@ export default function SearchSelect<
|
||||
componentRef={contentWrapperRef}
|
||||
>
|
||||
<Stack className="w-full items-start gap-0">
|
||||
{currentOptions.map((_o, index) => {
|
||||
const isTargetOption = index === selectIndex;
|
||||
const targetOptionClasses = twMerge(
|
||||
"bg-background-dark dark:bg-background-light text-foreground-dark dark:text-foreground-light",
|
||||
"twui-select-target-option"
|
||||
);
|
||||
{currentOptions?.[0]
|
||||
? currentOptions.map((_o, index) => {
|
||||
const isTargetOption = index === selectIndex;
|
||||
const targetOptionClasses = twMerge(
|
||||
"bg-background-dark dark:bg-background-light text-foreground-dark dark:text-foreground-light",
|
||||
"twui-select-target-option"
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Button
|
||||
title={_o.title || "Option"}
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setValue(_o);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={twMerge(
|
||||
"w-full text-foreground-light dark:text-foreground-dark",
|
||||
"hover:bg-gray/20 dark:hover:bg-gray-dark/20",
|
||||
isTargetOption
|
||||
? targetOptionClasses
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{_o.value}
|
||||
</Button>
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Button
|
||||
title={_o.title || "Option"}
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setValue(_o);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={twMerge(
|
||||
"w-full text-foreground-light dark:text-foreground-dark",
|
||||
"hover:bg-gray/20 dark:hover:bg-gray-dark/20",
|
||||
isTargetOption
|
||||
? targetOptionClasses
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{_o.value}
|
||||
</Button>
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Dropdown>
|
||||
|
||||
@@ -25,8 +25,8 @@ export type TWUISelectValidityObject = {
|
||||
};
|
||||
|
||||
export type TWUISelectOptionObject<
|
||||
KeyType extends string,
|
||||
T extends { [k: string]: any } = any
|
||||
KeyType extends string = string,
|
||||
T extends { [k: string]: any } = { [k: string]: any }
|
||||
> = {
|
||||
title?: string;
|
||||
value: KeyType;
|
||||
@@ -36,7 +36,7 @@ export type TWUISelectOptionObject<
|
||||
|
||||
export type TWUISelectProps<
|
||||
KeyType extends string,
|
||||
T extends { [k: string]: any } = any
|
||||
T extends { [k: string]: any } = { [k: string]: any }
|
||||
> = DetailedHTMLProps<
|
||||
SelectHTMLAttributes<HTMLSelectElement>,
|
||||
HTMLSelectElement
|
||||
@@ -168,6 +168,7 @@ export default function Select<
|
||||
|
||||
<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",
|
||||
|
||||
Reference in New Issue
Block a user