diff --git a/components/base/input/input.tsx b/components/base/input/input.tsx index d31cf19..6ef0bf5 100644 --- a/components/base/input/input.tsx +++ b/components/base/input/input.tsx @@ -1,7 +1,8 @@ "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 { useControlledState } from "@react-stately/utils"; 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 +40,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 +61,23 @@ 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; + // The clear button is only shown when the input has a value. Derive this from the + // value so visibility is robust even when no `placeholder` is set. + const hasValue = inputProps.value != null && inputProps.value !== ""; + const showClearButton = isClearable && hasValue; + + // Check if the input has a leading icon or trailing icon/tooltip + const hasTrailingIcon = tooltip || isInvalid || showClearButton; const hasLeadingIcon = Icon; + // Clear button can coexist with tooltip/invalid; shift the secondary icon left when so. + const hasStackedTrailing = showClearButton && (tooltip || isInvalid); // If the input is inside a `TextFieldContext`, use its context to simplify applying styles const context = useContext(TextFieldContext); @@ -72,21 +86,24 @@ 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"), iconLeading: "left-3 size-4 stroke-[2.25px]", iconTrailing: "right-3", + iconTrailingSecondary: hasStackedTrailing ? "right-9" : "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"), iconLeading: "left-3 size-5", iconTrailing: "right-3", + iconTrailingSecondary: hasStackedTrailing ? "right-9" : "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"), iconLeading: "left-3.5 size-5", iconTrailing: "right-3.5", + iconTrailingSecondary: hasStackedTrailing ? "right-9.5" : "right-3.5", shortcut: "pr-2.5", }, }); @@ -138,13 +155,27 @@ export const InputBase = ({ )} /> + {/* Clear button */} + {showClearButton && ( + + + + )} + {/* Tooltip and help icon */} {tooltip && type !== "password" && (