262 lines
6.9 KiB
TypeScript

import type {CheckboxVariantProps, CheckboxSlots, SlotsToClasses} from "@nextui-org/theme";
import type {AriaCheckboxProps} from "@react-types/checkbox";
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import {ReactNode, Ref, useCallback} from "react";
import {useMemo, useRef} from "react";
import {useToggleState} from "@react-stately/toggle";
import {checkbox} from "@nextui-org/theme";
import {useHover} from "@react-aria/interactions";
import {useFocusRing} from "@react-aria/focus";
import {mergeProps} from "@react-aria/utils";
import {useFocusableRef} from "@nextui-org/dom-utils";
import {__DEV__, warn, clsx, dataAttr} from "@nextui-org/shared-utils";
import {
useCheckbox as useReactAriaCheckbox,
useCheckboxGroupItem as useReactAriaCheckboxGroupItem,
} from "@react-aria/checkbox";
import {FocusableRef} from "@react-types/shared";
import {useCheckboxGroupContext} from "./checkbox-group-context";
export type CheckboxIconProps = {
"data-checked": string;
isSelected: boolean;
isIndeterminate: boolean;
disableAnimation: boolean;
className: string;
};
interface Props extends HTMLNextUIProps<"label"> {
/**
* Ref to the DOM node.
*/
ref?: Ref<HTMLElement>;
/**
* The label of the checkbox.
*/
children?: ReactNode;
/**
* Whether the checkbox is disabled.
* @default false
*/
isDisabled?: boolean;
/**
* The icon to be displayed when the checkbox is checked.
*/
icon?: ReactNode | ((props: CheckboxIconProps) => 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
* <Checkbox styles={{
* base:"base-classes",
* wrapper: "wrapper-classes",
* icon: "icon-classes",
* label: "label-classes",
* }} />
* ```
*/
styles?: SlotsToClasses<CheckboxSlots>;
}
export type UseCheckboxProps = Omit<Props, "defaultChecked" | "onChange"> &
Omit<AriaCheckboxProps, keyof CheckboxVariantProps> &
Omit<CheckboxVariantProps, "isFocusVisible">;
export function useCheckbox(props: UseCheckboxProps) {
const groupContext = useCheckboxGroupContext();
const isInGroup = !!groupContext;
const {
as,
ref,
value = "",
children,
icon,
name,
isRequired = false,
isReadOnly = false,
isSelected: isSelectedProp,
size = groupContext?.size ?? "md",
color = groupContext?.color ?? "primary",
radius = groupContext?.radius ?? "md",
lineThrough = groupContext?.lineThrough ?? false,
isDisabled = groupContext?.isDisabled ?? false,
disableAnimation = groupContext?.disableAnimation ?? false,
isIndeterminate = false,
validationState,
defaultSelected,
styles,
onChange,
className,
...otherProps
} = props;
if (groupContext && __DEV__) {
if (isSelectedProp) {
warn(
"The Checkbox.Group is being used, `isSelected` will be ignored. Use the `value` of the Checkbox.Group instead.",
"Checkbox",
);
}
if (defaultSelected) {
warn(
"The Checkbox.Group is being used, `defaultSelected` will be ignored. Use the `defaultValue` of the Checkbox.Group instead.",
"Checkbox",
);
}
}
const Component = as || "label";
const inputRef = useRef(null);
const domRef = useFocusableRef(ref as FocusableRef<HTMLLabelElement>, inputRef);
const ariaCheckboxProps = useMemo(() => {
return {
name,
value,
children,
defaultSelected,
isSelected: isSelectedProp,
isDisabled,
isIndeterminate,
isRequired,
isReadOnly,
validationState,
"aria-label": otherProps["aria-label"],
"aria-labelledby": otherProps["aria-labelledby"],
onChange,
};
}, [
value,
name,
children,
isIndeterminate,
isDisabled,
isSelectedProp,
defaultSelected,
validationState,
otherProps["aria-label"],
otherProps["aria-labelledby"],
onChange,
]);
const {inputProps} = isInGroup
? // eslint-disable-next-line
useReactAriaCheckboxGroupItem(
{
...ariaCheckboxProps,
validationState,
},
groupContext.groupState,
inputRef,
)
: useReactAriaCheckbox(ariaCheckboxProps, useToggleState(ariaCheckboxProps), inputRef); // eslint-disable-line
const isSelected = inputProps.checked;
const isInvalid = useMemo(() => validationState === "invalid", [validationState]);
if (isRequired) {
inputProps.required = true;
}
const {hoverProps, isHovered} = useHover({
isDisabled: inputProps.disabled,
});
const {focusProps, isFocused, isFocusVisible} = useFocusRing({
autoFocus: inputProps.autoFocus,
});
const slots = useMemo(
() =>
checkbox({
color,
size,
radius,
lineThrough,
isDisabled,
isFocusVisible,
disableAnimation,
}),
[color, size, radius, lineThrough, isDisabled, isFocusVisible, disableAnimation],
);
const baseStyles = clsx(styles?.base, className);
const getBaseProps: PropGetter = () => {
return {
ref: domRef,
className: slots.base({class: baseStyles}),
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(isSelected),
"data-invalid": dataAttr(isInvalid),
...mergeProps(hoverProps, otherProps),
};
};
const getWrapperProps: PropGetter = () => {
return {
"data-hover": dataAttr(isHovered),
"data-checked": dataAttr(isSelected),
"data-focus": dataAttr(isFocused),
"data-focus-visible": dataAttr(isFocused && isFocusVisible),
"data-indeterminate": dataAttr(isIndeterminate),
"data-disabled": dataAttr(isDisabled),
"data-invalid": dataAttr(isInvalid),
"data-readonly": dataAttr(inputProps.readOnly),
"aria-hidden": true,
className: clsx(slots.wrapper({class: styles?.wrapper})),
};
};
const getInputProps: PropGetter = () => {
return {
ref: inputRef,
...mergeProps(inputProps, focusProps),
};
};
const getLabelProps: PropGetter = useCallback(
() => ({
"data-disabled": dataAttr(isDisabled),
"data-checked": dataAttr(isSelected),
"data-invalid": dataAttr(isInvalid),
className: slots.label({class: styles?.label}),
}),
[slots, styles?.label, isDisabled, isSelected, isInvalid],
);
const getIconProps = useCallback(
() =>
({
"data-checked": dataAttr(isSelected),
isSelected: isSelected,
isIndeterminate: !!isIndeterminate,
disableAnimation: !!disableAnimation,
className: slots.icon({class: styles?.icon}),
} as CheckboxIconProps),
[slots, styles?.icon, isSelected, isIndeterminate, disableAnimation],
);
return {
Component,
icon,
children,
isSelected,
isDisabled,
isInvalid,
getBaseProps,
getWrapperProps,
getInputProps,
getLabelProps,
getIconProps,
};
}
export type UseCheckboxReturn = ReturnType<typeof useCheckbox>;