2023-04-05 19:37:33 -03:00

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>;