diff --git a/components/base/input/input.tsx b/components/base/input/input.tsx index d31cf19..0d5602d 100644 --- a/components/base/input/input.tsx +++ b/components/base/input/input.tsx @@ -1,7 +1,7 @@ "use client"; import { type ComponentType, type HTMLAttributes, type ReactNode, type Ref, createContext, useContext, useState } from "react"; -import { Eye, EyeOff, HelpCircle, InfoCircle } from "@untitledui/icons"; +import { Eye, EyeOff, HelpCircle, InfoCircle, XClose } from "@untitledui/icons"; import type { InputProps as AriaInputProps, TextFieldProps as AriaTextFieldProps } from "react-aria-components"; import { Button as AriaButton, Group as AriaGroup, Input as AriaInput, TextField as AriaTextField } from "react-aria-components"; import { HintText } from "@/components/base/input/hint-text"; @@ -39,6 +39,10 @@ export interface InputBaseProps extends Omit { groupRef?: Ref; /** Icon component to display on the left side of the input. */ icon?: ComponentType>; + /** Whether to show a clear (X) button when the input has a value. */ + isClearable?: boolean; + /** Called when the clear button is pressed. */ + onClear?: () => void; } export const InputBase = ({ @@ -56,14 +60,18 @@ export const InputBase = ({ tooltipClassName, inputClassName, iconClassName, + isClearable, + onClear, type = "text", ...inputProps }: InputBaseProps) => { const [isPasswordVisible, setIsPasswordVisible] = useState(false); // Check if the input has a leading icon or tooltip - const hasTrailingIcon = tooltip || isInvalid; + const hasTrailingIcon = tooltip || isInvalid || isClearable; const hasLeadingIcon = Icon; + // Clear button can coexist with tooltip/invalid; shift the secondary icon left when so. + const hasStackedTrailing = isClearable && (tooltip || isInvalid); // If the input is inside a `TextFieldContext`, use its context to simplify applying styles const context = useContext(TextFieldContext); @@ -72,21 +80,39 @@ export const InputBase = ({ const sizes = sortCx({ sm: { - root: cx("px-3 py-2 text-sm", hasLeadingIcon && "pl-9", hasTrailingIcon && "pr-9"), + root: cx( + "px-3 py-2 text-sm", + hasLeadingIcon && "pl-9", + hasTrailingIcon && "pr-9", + hasStackedTrailing && "pr-15 placeholder-shown:pr-9", + ), iconLeading: "left-3 size-4 stroke-[2.25px]", iconTrailing: "right-3", + iconTrailingSecondary: hasStackedTrailing ? "right-9 peer-placeholder-shown:right-3" : "right-3", shortcut: "pr-1.5", }, md: { - root: cx("px-3 py-2 text-md", hasLeadingIcon && "pl-10", hasTrailingIcon && "pr-9"), + root: cx( + "px-3 py-2 text-md", + hasLeadingIcon && "pl-10", + hasTrailingIcon && "pr-9", + hasStackedTrailing && "pr-15 placeholder-shown:pr-9", + ), iconLeading: "left-3 size-5", iconTrailing: "right-3", + iconTrailingSecondary: hasStackedTrailing ? "right-9 peer-placeholder-shown:right-3" : "right-3", shortcut: "pr-2", }, lg: { - root: cx("px-3.5 py-2.5 text-md", hasLeadingIcon && "pl-10.5", hasTrailingIcon && "pr-9.5"), + root: cx( + "px-3.5 py-2.5 text-md", + hasLeadingIcon && "pl-10.5", + hasTrailingIcon && "pr-9.5", + hasStackedTrailing && "pr-15.5 placeholder-shown:pr-9.5", + ), iconLeading: "left-3.5 size-5", iconTrailing: "right-3.5", + iconTrailingSecondary: hasStackedTrailing ? "right-9.5 peer-placeholder-shown:right-3.5" : "right-3.5", shortcut: "pr-2.5", }, }); @@ -131,20 +157,34 @@ export const InputBase = ({ type={type === "password" && isPasswordVisible ? "text" : type} placeholder={placeholder} className={cx( - "m-0 w-full bg-transparent text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary disabled:cursor-not-allowed", + "peer m-0 w-full bg-transparent text-primary ring-0 outline-hidden placeholder:text-placeholder autofill:rounded-lg autofill:text-primary disabled:cursor-not-allowed", sizes[inputSize].root, context?.inputClassName, inputClassName, )} /> + {/* Clear button */} + {isClearable && ( + + + + )} + {/* Tooltip and help icon */} {tooltip && type !== "password" && (