Update input components

This commit is contained in:
2026-02-16 23:36:32 -05:00
parent da0db483aa
commit a61e7ce19a
10 changed files with 171 additions and 105 deletions

View File

@@ -1,41 +1,47 @@
import { DangerButton } from "$/component/button";
import type { FileInputProps } from "$/types/InputTypes";
import { humanReadableBytes } from "$/util/FileUtil";
import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
import { useRef, useState } from "react";
import { MdClose } from "react-icons/md";
export default function DragAndDropFileInput({
id,
className,
name,
ariaLabel,
minSize,
maxSize,
showFileName,
showSize,
showFileName = true,
showSize = true,
onChange,
disabled,
children
}: FileInputProps){
}: Readonly<FileInputProps>){
const [ file, setFile ] = useState<File>();
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
onChange?.(file);
}, [ file, onChange ]);
return (
<label
className={clsx(
"flex flex-col items-center justify-center border-2 rounded-lg w-full h-full cursor-pointer",
"flex flex-col items-center justify-center border-2 rounded-lg cursor-pointer",
//TODO: Make hover classes
className
)}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
setFile(e.dataTransfer.files[0]);
const currentFile = e.dataTransfer.files[0];
setFile(currentFile);
if ((minSize && currentFile.size < minSize) || (maxSize && currentFile.size > maxSize)) return;
onChange?.(currentFile);
if(inputRef.current){ inputRef.current.files = e.dataTransfer.files; }
}}
aria-label={ariaLabel}
>
<input
ref={inputRef}
@@ -43,24 +49,40 @@ export default function DragAndDropFileInput({
id={id}
className="sr-only"
name={name}
onChange={(e) => setFile(e.target.files?.[0])}
onChange={(e) => {
const currentFile = e.target.files?.[0];
setFile(currentFile);
if ((minSize && currentFile && currentFile.size < minSize) || (maxSize && currentFile && currentFile.size > maxSize)) return;
onChange?.(currentFile);
}}
disabled={disabled}
/>
<div
className="flex flex-col items-center justify-between w-full h-full px-2"
className="flex flex-col items-center justify-between px-2"
>
{children}
<div className="flex flex-row items-center justify-center">
{children}
</div>
<div
className="flex flex-row items-center justify-between gap-x-8 w-full"
className="flex flex-row items-center justify-between gap-x-2 w-full"
>
{
showFileName &&
<div
className="text-center"
>
<div className="flex flex-row items-center justify-center gap-x-2">
{file?.name}
</div>
}
{
file &&
<DangerButton
className="mr-4"
shape="square"
variant="icon"
onClick={(e) => { e.preventDefault(); setFile(undefined); onChange?.(undefined); }}
>
<MdClose size={22} className="fill-danger"/>
</DangerButton>
}
{
showSize &&
<div

View File

@@ -1,22 +1,25 @@
import { SecondaryButton } from "$/component/button";
import { DangerButton, SecondaryButton } from "$/component/button";
import type { FileInputProps } from "$/types/InputTypes";
import { humanReadableBytes } from "$/util/FileUtil";
import clsx from "clsx";
import { useRef, useState } from "react";
import { FaRegFolderOpen } from "react-icons/fa6";
import { MdClose } from "react-icons/md";
export default function FileInput({
id,
className,
name,
ariaLabel,
minSize,
maxSize,
showFileName,
showSize,
showFileName = true,
showSize = true,
onChange,
disabled,
children
}: FileInputProps){
}: Readonly<FileInputProps>){
const inputRef = useRef<HTMLInputElement>(null);
const [ file, setFile ] = useState<File>();
@@ -24,7 +27,7 @@ export default function FileInput({
return (
<div
className={clsx(
"flex flex-row items-center justify-between w-full border-2 rounded-lg",
"flex flex-row items-center justify-between border-2 rounded-lg",
className
)}
>
@@ -34,7 +37,13 @@ export default function FileInput({
type="file"
className="sr-only"
name={name}
onChange={(e) => { setFile(e.target.files?.[0]); onChange?.(e.target.files?.[0]); }}
aria-label={ariaLabel}
onChange={(e) => {
const currentFile = e.target.files?.[0];
setFile(currentFile);
if ((minSize && currentFile && currentFile.size < minSize) || (maxSize && currentFile && currentFile.size > maxSize)) return;
onChange?.(currentFile);
}}
disabled={disabled}
/>
<div
@@ -48,6 +57,16 @@ export default function FileInput({
showFileName &&
<div>{file?.name}</div>
}
{
file &&
<DangerButton
shape="square"
variant="icon"
onClick={(e) => { e.preventDefault(); setFile(undefined); onChange?.(undefined); }}
>
<MdClose size={22} className="fill-danger"/>
</DangerButton>
}
{
!children && !showFileName &&
<>&nbsp;</>
@@ -56,12 +75,13 @@ export default function FileInput({
showSize &&
<div
className={clsx(
"ml-4",
{
"text-red-600": minSize && file?.size && file?.size < minSize,
"text-red-600 ": maxSize && file?.size && file?.size > maxSize,
"text-green-600": minSize && !maxSize && file?.size && file?.size > minSize,
"text-green-600 ": !minSize && maxSize && file?.size && file?.size < maxSize,
" text-green-600": minSize && maxSize && file?.size && file?.size > minSize && file?.size < maxSize
"text-danger": minSize && file?.size && file?.size < minSize,
"text-danger ": maxSize && file?.size && file?.size > maxSize,
"text-success": minSize && !maxSize && file?.size && file?.size > minSize,
"text-success ": !minSize && maxSize && file?.size && file?.size < maxSize,
" text-success": minSize && maxSize && file?.size && file?.size > minSize && file?.size < maxSize
}
)}
>
@@ -75,10 +95,13 @@ export default function FileInput({
<SecondaryButton
className="text-nowrap rounded-r-lg"
rounding="none"
shape="square"
size="lg"
onClick={() => { inputRef.current?.click(); }}
disabled={disabled}
aria-label="Select File"
>
Click Me
<FaRegFolderOpen />
</SecondaryButton>
</div>
</div>

View File

@@ -10,38 +10,42 @@ export default function NumberInput({
name,
min,
max,
defaultValue,
step,
prefix,
suffix,
value,
onChange,
disabled,
children
}: NumberInputProps){
}: Readonly<NumberInputProps>){
return (
<div
className={clsx(
"flex flex-row items-center justify-center rounded-lg border-2 w-full",
"flex flex-row items-center justify-center rounded-lg border-2",
className
)}
>
<div
className="relative flex flex-row items-center justify-center px-2 py-1 w-full"
>
<input
type="number"
id={id}
className={clsx(
"peer bg-transparent outline-none placeholder-transparent w-full",
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
inputClassName
)}
name={name}
min={min}
max={max}
defaultValue={defaultValue}
value={value}
onChange={(e) => onChange?.(e.target.valueAsNumber)}
disabled={disabled}
/>
<div className="relative flex flex-row items-center justify-center px-2 py-1 w-full">
<div className="flex flex-row items-center justify-start">
{ prefix && <span>{prefix}</span> }
<input
type="number"
id={id}
className={clsx(
"peer bg-transparent outline-none placeholder-transparent w-full",
"[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
inputClassName
)}
name={name}
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(e.target.valueAsNumber || 0)}
disabled={disabled}
/>
{ suffix && <span>{suffix}</span> }
</div>
<label
className={clsx(
"absolute ml-2 -top-3 left-0 text-sm rounded-md px-1 select-none cursor-default",

View File

@@ -9,28 +9,31 @@ export default function NumberSlider({
min,
max,
step,
defaultValue,
value,
onChange,
disabled
}: NumberSliderProps){
disabled,
ariaLabel
}: Readonly<NumberSliderProps>){
return (
<input
type="range"
id={id}
className={clsx(
"w-full appearance-none [-moz-range-thumb:background:#04AA6D]",
"h-6 bg-blue-300 accent-blue-600",
"appearance-none [-moz-range-thumb:background:#04AA6D]",
"h-5 px-0.5 rounded-full bg-primary",
className
)}
name={name}
min={min}
max={max}
step={step}
defaultValue={defaultValue}
value={value}
onChange={(e) => onChange?.(e.target.valueAsNumber)}
onChange={(e) => onChange(e.target.valueAsNumber || 0)}
disabled={disabled}
aria-label={ariaLabel}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
/>
);
}

View File

@@ -3,15 +3,13 @@ import { ListboxOption } from "@headlessui/react";
import clsx from "clsx";
export default function OptionInput(props: OptionInputProps){
const {
id,
className,
value,
children
} = props;
export default function OptionInput({
id,
className,
value,
disabled,
children
}: Readonly<OptionInputProps>){
return (
<ListboxOption
id={id}
@@ -20,6 +18,7 @@ export default function OptionInput(props: OptionInputProps){
className
)}
value={value}
disabled={disabled}
>
{children}
</ListboxOption>

View File

@@ -4,28 +4,28 @@ import clsx from "clsx";
import { BsChevronDown } from "react-icons/bs";
export default function SelectInput(props: SelectInputProps){
const {
label,
value,
onChange,
children
} = props;
export default function SelectInput({
placeholder,
value,
onChange,
disabled,
children
}: Readonly<SelectInputProps>){
return (
<Listbox
value={value}
onChange={onChange}
disabled={disabled}
>
<ListboxButton
className={clsx(
"group relative flex flex-row items-center justify-between w-full",
"border-2 px-2 py-1 rounded-lg"
"border-2 px-2 py-1 rounded-lg",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
//"not-data-open:rounded-lg data-open:rounded-t-lg"
)}
>
<span>{label}</span>
<span>{placeholder}</span>
<span className="block group-data-open:rotate-180 transition-transform duration-250"><BsChevronDown size={22}/></span>
</ListboxButton>
<ListboxOptions

View File

@@ -1,23 +1,27 @@
import type { TextAreaProps } from "$/types/InputTypes";
import clsx from "clsx";
import { useId } from "react";
export default function TextArea({
id = crypto.randomUUID().replaceAll("-", ""),
id,
className,
inputClassName,
labelClassName,
name,
maxLength,
rows,
rows = 3,
cols,
spellCheck,
placeholder,
defaultValue,
value,
onChange,
disabled
}: TextAreaProps){
}: Readonly<TextAreaProps>){
const componentId = useId();
const activeId = id ?? componentId;
return (
<div
className={clsx(
@@ -29,7 +33,7 @@ export default function TextArea({
className="relative flex flex-row items-center justify-center px-2 py-1 w-full"
>
<textarea
id={id}
id={activeId}
className={clsx(
"peer bg-transparent outline-none placeholder-transparent w-full",
inputClassName
@@ -39,7 +43,6 @@ export default function TextArea({
maxLength={maxLength}
rows={rows}
cols={cols}
defaultValue={defaultValue}
value={value}
onChange={onChange}
disabled={disabled}
@@ -54,7 +57,7 @@ export default function TextArea({
labelClassName
)}
style={{ transitionProperty: "top, left, font-size, line-height", transitionTimingFunction: "cubic-bezier(0.4 0, 0.2, 1)", transitionDuration: "250ms" }}
htmlFor={id}
htmlFor={activeId}
>
{placeholder}
</label>

View File

@@ -1,9 +1,10 @@
import type { TextInputProps } from "$/types/InputTypes";
import clsx from "clsx";
import { useId } from "react";
export default function TextInput({
id = crypto.randomUUID().replaceAll("-", ""),
id,
className,
inputClassName,
labelClassName,
@@ -11,11 +12,14 @@ export default function TextInput({
maxLength,
spellCheck,
placeholder,
defaultValue,
value,
onChange,
disabled
}: TextInputProps){
}: Readonly<TextInputProps>){
const componentId = useId();
const activeId = id ?? componentId;
return (
<div
className={clsx(
@@ -28,7 +32,7 @@ export default function TextInput({
>
<input
type="text"
id={id}
id={activeId}
className={clsx(
"peer bg-transparent outline-none placeholder-transparent w-full",
inputClassName
@@ -36,7 +40,6 @@ export default function TextInput({
name={name}
placeholder={placeholder}
maxLength={maxLength}
defaultValue={defaultValue}
value={value}
onChange={onChange}
disabled={disabled}
@@ -51,7 +54,7 @@ export default function TextInput({
labelClassName
)}
style={{ transitionProperty: "top, left, font-size, line-height", transitionTimingFunction: "cubic-bezier(0.4 0, 0.2, 1)", transitionDuration: "250ms" }}
htmlFor={id}
htmlFor={activeId}
>
{placeholder}
</label>

View File

@@ -11,7 +11,6 @@ export interface TextInputProps {
maxLength?: number;
spellCheck?: boolean;
placeholder?: string;
defaultValue?: string;
value?: string;
onChange?: ChangeEventHandler<HTMLInputElement>;
disabled?: boolean;
@@ -26,7 +25,6 @@ export interface TextAreaProps {
maxLength?: number;
spellCheck?: boolean;
placeholder?: string;
defaultValue?: string;
value?: string;
disabled?: boolean;
rows?: number;
@@ -35,9 +33,10 @@ export interface TextAreaProps {
}
export interface SelectInputProps {
label: React.ReactNode;
placeholder: React.ReactNode;
value?: string;
onChange?: (newValue: string) => void;
disabled?: boolean;
children: React.ReactNode;
}
@@ -45,6 +44,7 @@ export interface OptionInputProps {
id?: string;
className?: string;
value: string;
disabled?: boolean;
children: React.ReactNode;
}
@@ -88,9 +88,11 @@ export interface NumberInputProps {
name?: string;
min?: number;
max?: number;
defaultValue?: number;
value?: number;
onChange?: (newValue: number) => void;
step?: number;
prefix?: string;
suffix?: string;
value: number;
onChange: (newValue: number) => void;
disabled?: boolean;
children?: React.ReactNode;
}
@@ -102,20 +104,21 @@ export interface NumberSliderProps {
min?: number;
max?: number;
step?: number;
defaultValue?: number;
value?: number;
onChange?: (newValue: number) => void;
value: number;
onChange: (newValue: number) => void;
disabled?: boolean;
ariaLabel?: string;
}
export interface FileInputProps {
id?: string;
className?: string;
name?: string;
ariaLabel?: string;
minSize?: number;
maxSize?: number;
showFileName?: boolean;
showSize?: boolean;
showFileName: boolean;
showSize: boolean;
onChange?: (newFile: File | undefined) => void;
disabled?: boolean;
children?: React.ReactNode;

View File

@@ -14,6 +14,7 @@ import WarningCheckbox from "$/component/input/checkbox/WarningCheckbox";
import DateInput from "$/component/input/date/DateInput";
import DateTimeInput from "$/component/input/date/DateTimeInput";
import TimeInput from "$/component/input/date/TimeInput";
import NumberSlider from "$/component/input/number/NumberSlider";
import DangerRadioButton from "$/component/input/radio/DangerRadioButton";
import DarkRadioButton from "$/component/input/radio/DarkRadioButton";
import InfoRadioButton from "$/component/input/radio/InfoRadioButton";
@@ -572,6 +573,7 @@ export function TextContent(){
];
const [ selected, setSelected ] = useState(selectOptions[0]);
const [ numberValue, setNumberValue ] = useState(0);
return (
<div
@@ -584,7 +586,7 @@ export function TextContent(){
<TextArea placeholder="Textarea" className="resize" labelClassName="bg-(--bg-color) peer-focus:bg-(--bg-color)"/>
</TextDisplay>
<TextDisplay title="Select">
<SelectInput label={selected.label} onChange={(newValue) => setSelected(selectOptions.find((option) => option.value === newValue) || selectOptions[0])}>
<SelectInput placeholder={selected.label} onChange={(newValue) => setSelected(selectOptions.find((option) => option.value === newValue) || selectOptions[0])}>
{
selectOptions.map((option) => (
<OptionInput
@@ -603,20 +605,24 @@ export function TextContent(){
>
<NumberInput
labelClassName="bg-(--bg-color)"
value={numberValue}
onChange={setNumberValue}
>
Number Test
</NumberInput>
</TextDisplay>
{/*
{/* */
<TextDisplay
title="Number Slider"
>
<NumberSlider
min={0}
max={10}
value={numberValue}
onChange={setNumberValue}
/>
</TextDisplay>
*/}
/* */}
</div>
);
}