mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
195 lines
5.2 KiB
TypeScript
195 lines
5.2 KiB
TypeScript
import type {FocusableProps, PressEvents} from "@react-types/shared";
|
|
import type {SlotsToClasses, CardSlots, CardReturnType, CardVariantProps} from "@nextui-org/theme";
|
|
|
|
import {card} from "@nextui-org/theme";
|
|
import {MouseEvent, useCallback, useMemo} from "react";
|
|
import {mergeProps} from "@react-aria/utils";
|
|
import {useFocusRing} from "@react-aria/focus";
|
|
import {useHover} from "@react-aria/interactions";
|
|
import {useAriaButton} from "@nextui-org/use-aria-button";
|
|
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
|
import {callAllHandlers, clsx, dataAttr} from "@nextui-org/shared-utils";
|
|
import {ReactRef} from "@nextui-org/react-utils";
|
|
import {useDOMRef} from "@nextui-org/react-utils";
|
|
import {useDrip} from "@nextui-org/drip";
|
|
import {AriaButtonProps} from "@react-aria/button";
|
|
|
|
export interface Props extends HTMLNextUIProps<"div"> {
|
|
/**
|
|
* Ref to the DOM node.
|
|
*/
|
|
ref: ReactRef<HTMLDivElement | null>;
|
|
/**
|
|
* Whether the card should show a ripple animation on press, this prop is ignored if `disableAnimation` is true or `isPressable` is false.
|
|
* @default false
|
|
*/
|
|
disableRipple?: boolean;
|
|
|
|
/**
|
|
* Whether the card should allow text selection on press. (only for pressable cards)
|
|
* @default true
|
|
*/
|
|
allowTextSelectionOnPress?: boolean;
|
|
/**
|
|
* Classname or List of classes to change the classNames of the element.
|
|
* if `className` is passed, it will be added to the base slot.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* <Card classNames={{
|
|
* base:"base-classes",
|
|
* header: "dot-classes",
|
|
* body: "content-classes",
|
|
* footer: "avatar-classes",
|
|
* }} />
|
|
* ```
|
|
*/
|
|
classNames?: SlotsToClasses<CardSlots>;
|
|
}
|
|
|
|
export type UseCardProps = Props & PressEvents & FocusableProps & CardVariantProps;
|
|
|
|
export type ContextType = {
|
|
slots: CardReturnType;
|
|
classNames?: SlotsToClasses<CardSlots>;
|
|
isDisabled?: CardVariantProps["isDisabled"];
|
|
isFooterBlurred?: CardVariantProps["isFooterBlurred"];
|
|
disableAnimation?: CardVariantProps["disableAnimation"];
|
|
fullWidth?: CardVariantProps["fullWidth"];
|
|
};
|
|
|
|
export function useCard(originalProps: UseCardProps) {
|
|
const [props, variantProps] = mapPropsVariants(originalProps, card.variantKeys);
|
|
|
|
const {
|
|
ref,
|
|
as,
|
|
children,
|
|
disableRipple = false,
|
|
onClick,
|
|
onPress,
|
|
autoFocus,
|
|
className,
|
|
classNames,
|
|
allowTextSelectionOnPress = true,
|
|
...otherProps
|
|
} = props;
|
|
|
|
const domRef = useDOMRef<HTMLDivElement>(ref);
|
|
const Component = as || (originalProps.isPressable ? "button" : "div");
|
|
|
|
const baseStyles = clsx(className, classNames?.base);
|
|
|
|
const {onClick: onDripClickHandler, drips} = useDrip();
|
|
|
|
const handleDrip = (e: MouseEvent<HTMLDivElement>) => {
|
|
if (!originalProps.disableAnimation && !disableRipple && domRef.current) {
|
|
onDripClickHandler(e);
|
|
}
|
|
};
|
|
|
|
const {buttonProps, isPressed} = useAriaButton(
|
|
{
|
|
onPress,
|
|
elementType: as,
|
|
isDisabled: !originalProps.isPressable,
|
|
onClick: callAllHandlers(onClick, handleDrip),
|
|
allowTextSelectionOnPress,
|
|
...otherProps,
|
|
} as AriaButtonProps,
|
|
domRef,
|
|
);
|
|
|
|
const {hoverProps, isHovered} = useHover({
|
|
isDisabled: !originalProps.isHoverable,
|
|
...otherProps,
|
|
});
|
|
|
|
const {isFocusVisible, isFocused, focusProps} = useFocusRing({
|
|
autoFocus,
|
|
});
|
|
|
|
const slots = useMemo(
|
|
() =>
|
|
card({
|
|
...variantProps,
|
|
}),
|
|
[...Object.values(variantProps)],
|
|
);
|
|
|
|
const context = useMemo<ContextType>(
|
|
() => ({
|
|
isDisabled: originalProps.isDisabled,
|
|
isFooterBlurred: originalProps.isFooterBlurred,
|
|
disableAnimation: originalProps.disableAnimation,
|
|
fullWidth: originalProps.fullWidth,
|
|
slots,
|
|
classNames,
|
|
}),
|
|
[
|
|
slots,
|
|
classNames,
|
|
originalProps.isDisabled,
|
|
originalProps.isFooterBlurred,
|
|
originalProps.disableAnimation,
|
|
originalProps.fullWidth,
|
|
],
|
|
);
|
|
|
|
const getCardProps = useCallback<PropGetter>(
|
|
(props = {}) => {
|
|
return {
|
|
ref: domRef,
|
|
className: slots.base({class: baseStyles}),
|
|
tabIndex: originalProps.isPressable ? 0 : -1,
|
|
"data-hover": dataAttr(isHovered),
|
|
"data-pressed": dataAttr(isPressed),
|
|
"data-focus": dataAttr(isFocused),
|
|
"data-focus-visible": dataAttr(isFocusVisible),
|
|
"data-disabled": dataAttr(originalProps.isDisabled),
|
|
...mergeProps(
|
|
originalProps.isPressable ? {...buttonProps, ...focusProps, role: "button"} : {},
|
|
originalProps.isHoverable ? hoverProps : {},
|
|
otherProps,
|
|
props,
|
|
),
|
|
};
|
|
},
|
|
[
|
|
domRef,
|
|
slots,
|
|
baseStyles,
|
|
originalProps.isPressable,
|
|
originalProps.isHoverable,
|
|
originalProps.isDisabled,
|
|
isHovered,
|
|
isPressed,
|
|
isFocusVisible,
|
|
buttonProps,
|
|
focusProps,
|
|
hoverProps,
|
|
otherProps,
|
|
],
|
|
);
|
|
|
|
return {
|
|
context,
|
|
domRef,
|
|
Component,
|
|
classNames,
|
|
children,
|
|
drips,
|
|
isHovered,
|
|
isPressed,
|
|
isPressable: originalProps.isPressable,
|
|
isHoverable: originalProps.isHoverable,
|
|
disableAnimation: originalProps.disableAnimation,
|
|
disableRipple,
|
|
onDripClickHandler,
|
|
isFocusVisible,
|
|
getCardProps,
|
|
};
|
|
}
|
|
|
|
export type UseCardReturn = ReturnType<typeof useCard>;
|