mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
264 lines
7.1 KiB
TypeScript
264 lines
7.1 KiB
TypeScript
import type {InputVariantProps, SlotsToClasses, InputSlots} from "@nextui-org/theme";
|
|
|
|
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
|
import {AriaTextFieldProps} from "@react-types/textfield";
|
|
import {useFocusRing} from "@react-aria/focus";
|
|
import {input} from "@nextui-org/theme";
|
|
import {useDOMRef} from "@nextui-org/dom-utils";
|
|
import {usePress} from "@react-aria/interactions";
|
|
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
|
import {useControlledState} from "@react-stately/utils";
|
|
import {useMemo, Ref, RefObject} from "react";
|
|
import {chain, filterDOMProps, mergeProps} from "@react-aria/utils";
|
|
|
|
import {useAriaTextField} from "./use-aria-textfield";
|
|
|
|
export interface Props extends HTMLNextUIProps<"input"> {
|
|
/**
|
|
* Ref to the DOM node.
|
|
*/
|
|
ref?: Ref<HTMLInputElement>;
|
|
/**
|
|
* Element to be rendered in the left side of the input.
|
|
*/
|
|
startContent?: React.ReactNode;
|
|
/**
|
|
* Element to be rendered in the right side of the input.
|
|
* if you pass this prop and the `onClear` prop, the passed element
|
|
* will have the clear button props and it will be rendered instead of the
|
|
* default clear button.
|
|
*/
|
|
endContent?: React.ReactNode;
|
|
/**
|
|
* Classname or List of classes to change the styles of the element.
|
|
* if `className` is passed, it will be added to the base slot.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* <Input styles={{
|
|
* base:"base-classes",
|
|
* label: "label-classes",
|
|
* inputWrapper: "input-wrapper-classes",
|
|
* input: "input-classes",
|
|
* clearButton: "clear-button-classes",
|
|
* description: "description-classes",
|
|
* errorMessage: "error-message-classes",
|
|
* }} />
|
|
* ```
|
|
*/
|
|
styles?: SlotsToClasses<InputSlots>;
|
|
/**
|
|
* Callback fired when the value is cleared.
|
|
* if you pass this prop, the clear button will be shown.
|
|
*/
|
|
onClear?: () => void;
|
|
/**
|
|
* React aria onChange event.
|
|
*/
|
|
onValueChange?: AriaTextFieldProps["onChange"];
|
|
}
|
|
|
|
export type UseInputProps = Props & Omit<AriaTextFieldProps, "onChange"> & InputVariantProps;
|
|
|
|
export function useInput(originalProps: UseInputProps) {
|
|
const [props, variantProps] = mapPropsVariants(originalProps, input.variantKeys);
|
|
|
|
const {
|
|
ref,
|
|
as,
|
|
label,
|
|
description,
|
|
errorMessage,
|
|
className,
|
|
styles,
|
|
autoFocus,
|
|
startContent,
|
|
endContent,
|
|
onClear,
|
|
onChange,
|
|
onValueChange,
|
|
...otherProps
|
|
} = props;
|
|
|
|
const [inputValue, setInputValue] = useControlledState(props.value, props.defaultValue, () => {});
|
|
|
|
const Component = as || "div";
|
|
const baseStyles = clsx(styles?.base, className, !!inputValue ? "is-filled" : "");
|
|
const isMultiline = originalProps.isMultiline;
|
|
|
|
const domRef = useDOMRef(ref) as typeof isMultiline extends "true"
|
|
? RefObject<HTMLTextAreaElement>
|
|
: RefObject<HTMLInputElement>;
|
|
|
|
const handleClear = () => {
|
|
setInputValue(undefined);
|
|
|
|
if (domRef.current) {
|
|
domRef.current.value = "";
|
|
domRef.current.focus();
|
|
}
|
|
onClear?.();
|
|
};
|
|
|
|
const {labelProps, inputProps, descriptionProps, errorMessageProps} = useAriaTextField(
|
|
{
|
|
...originalProps,
|
|
inputElementType: isMultiline ? "textarea" : "input",
|
|
value: inputValue,
|
|
onChange: chain(onValueChange, setInputValue),
|
|
},
|
|
domRef,
|
|
);
|
|
|
|
const {isFocusVisible, isFocused, focusProps} = useFocusRing({
|
|
autoFocus,
|
|
isTextInput: true,
|
|
});
|
|
|
|
const {focusProps: clearFocusProps, isFocusVisible: isClearButtonFocusVisible} = useFocusRing();
|
|
|
|
const {pressProps: clearPressProps} = usePress({
|
|
isDisabled: !!originalProps?.isDisabled,
|
|
onPress: handleClear,
|
|
});
|
|
|
|
const isInvalid = props.validationState === "invalid";
|
|
const labelPosition = originalProps.labelPosition || "inside";
|
|
const isLabelPlaceholder = !props.placeholder && labelPosition !== "outside-left" && !isMultiline;
|
|
const isClearable = !!onClear || originalProps.isClearable;
|
|
|
|
const shouldLabelBeOutside = labelPosition === "outside" || labelPosition === "outside-left";
|
|
const shouldLabelBeInside = labelPosition === "inside";
|
|
|
|
const hasStartContent = !!startContent;
|
|
|
|
const slots = useMemo(
|
|
() =>
|
|
input({
|
|
...variantProps,
|
|
isInvalid,
|
|
isClearable,
|
|
isFocusVisible,
|
|
isLabelPlaceholder: isLabelPlaceholder && !hasStartContent,
|
|
isClearButtonFocusVisible,
|
|
}),
|
|
[
|
|
...Object.values(variantProps),
|
|
isInvalid,
|
|
isClearable,
|
|
isClearButtonFocusVisible,
|
|
isLabelPlaceholder,
|
|
hasStartContent,
|
|
isFocusVisible,
|
|
],
|
|
);
|
|
|
|
const getBaseProps: PropGetter = (props = {}) => {
|
|
return {
|
|
className: slots.base({class: baseStyles}),
|
|
"data-focus-visible": dataAttr(isFocusVisible),
|
|
"data-focused": dataAttr(isFocused),
|
|
"data-invalid": dataAttr(isInvalid),
|
|
...props,
|
|
};
|
|
};
|
|
|
|
const getLabelProps: PropGetter = (props = {}) => {
|
|
return {
|
|
className: slots.label({class: styles?.label}),
|
|
...labelProps,
|
|
...props,
|
|
};
|
|
};
|
|
|
|
const getInputProps: PropGetter = (props = {}) => {
|
|
return {
|
|
ref: domRef,
|
|
className: slots.input({class: clsx(styles?.input, !!inputValue ? "is-filled" : "")}),
|
|
"data-focus-visible": dataAttr(isFocusVisible),
|
|
"data-focused": dataAttr(isFocused),
|
|
"data-invalid": dataAttr(isInvalid),
|
|
...mergeProps(focusProps, inputProps, filterDOMProps(otherProps, {labelable: true}), props),
|
|
onChange: chain(inputProps.onChange, onChange),
|
|
};
|
|
};
|
|
|
|
const getInputWrapperProps: PropGetter = (props = {}) => {
|
|
return {
|
|
className: slots.inputWrapper({
|
|
class: clsx(styles?.inputWrapper, !!inputValue ? "is-filled" : ""),
|
|
}),
|
|
onClick: (e: React.MouseEvent) => {
|
|
if (e.target === e.currentTarget) {
|
|
domRef.current?.focus();
|
|
}
|
|
},
|
|
...props,
|
|
style: {
|
|
cursor: "text",
|
|
...props.style,
|
|
},
|
|
};
|
|
};
|
|
|
|
const getInnerWrapperProps: PropGetter = (props = {}) => {
|
|
return {
|
|
className: slots.innerWrapper({
|
|
class: styles?.innerWrapper,
|
|
}),
|
|
...props,
|
|
};
|
|
};
|
|
|
|
const getDescriptionProps: PropGetter = (props = {}) => {
|
|
return {
|
|
className: slots.description({class: styles?.description}),
|
|
...descriptionProps,
|
|
...props,
|
|
};
|
|
};
|
|
|
|
const getErrorMessageProps: PropGetter = (props = {}) => {
|
|
return {
|
|
className: slots.errorMessage({class: styles?.errorMessage}),
|
|
...errorMessageProps,
|
|
...props,
|
|
};
|
|
};
|
|
|
|
const getClearButtonProps: PropGetter = () => {
|
|
return {
|
|
role: "button",
|
|
tabIndex: 0,
|
|
className: slots.clearButton({class: styles?.clearButton}),
|
|
...mergeProps(clearPressProps, clearFocusProps),
|
|
};
|
|
};
|
|
|
|
return {
|
|
Component,
|
|
styles,
|
|
domRef,
|
|
label,
|
|
description,
|
|
startContent,
|
|
endContent,
|
|
labelPosition,
|
|
isClearable,
|
|
isInvalid,
|
|
shouldLabelBeOutside,
|
|
shouldLabelBeInside,
|
|
errorMessage,
|
|
getBaseProps,
|
|
getLabelProps,
|
|
getInputProps,
|
|
getInputWrapperProps,
|
|
getInnerWrapperProps,
|
|
getDescriptionProps,
|
|
getErrorMessageProps,
|
|
getClearButtonProps,
|
|
};
|
|
}
|
|
|
|
export type UseInputReturn = ReturnType<typeof useInput>;
|