mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(card): context wrapper added
This commit is contained in:
parent
e9bbf2e673
commit
f5b3c788e4
@ -1,6 +1,9 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {filterDOMProps} from "@react-aria/utils";
|
||||
|
||||
import {useCardContext} from "./card-context";
|
||||
|
||||
const CardBody = forwardRef<HTMLNextUIProps, "div">((props, ref) => {
|
||||
const {as, className, children, ...otherProps} = props;
|
||||
@ -8,14 +11,15 @@ const CardBody = forwardRef<HTMLNextUIProps, "div">((props, ref) => {
|
||||
const Component = as || "div";
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, styles} = useCardContext();
|
||||
|
||||
const bodyStyles = clsx(styles?.body, className);
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={domRef}
|
||||
{...otherProps}
|
||||
className={clsx(
|
||||
"relative flex flex-auto flex-col place-content-inherit align-items-inherit w-full h-auto py-5 px-3 text-left overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
className={slots.body?.({class: bodyStyles})}
|
||||
{...filterDOMProps(otherProps, {labelable: true})}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
|
||||
10
packages/components/card/src/card-context.tsx
Normal file
10
packages/components/card/src/card-context.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import {createContext} from "@nextui-org/shared-utils";
|
||||
|
||||
import {ContextType} from "./use-card";
|
||||
|
||||
export const [CardProvider, useCardContext] = createContext<ContextType>({
|
||||
name: "CardContext",
|
||||
strict: true,
|
||||
errorMessage:
|
||||
"useCardContext: `context` is undefined. Seems you forgot to wrap component within <Card />",
|
||||
});
|
||||
@ -1,24 +1,31 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {filterDOMProps} from "@react-aria/utils";
|
||||
|
||||
const CardFooter = forwardRef<HTMLNextUIProps & {isBlurred?: boolean}, "div">((props, ref) => {
|
||||
import {useCardContext} from "./card-context";
|
||||
|
||||
export interface CardFooterProps extends HTMLNextUIProps<"div"> {
|
||||
isBlurred?: boolean;
|
||||
}
|
||||
|
||||
const CardFooter = forwardRef<CardFooterProps, "div">((props, ref) => {
|
||||
const {as, className, children, isBlurred, ...otherProps} = props;
|
||||
|
||||
const Component = as || "div";
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, styles} = useCardContext();
|
||||
|
||||
const footerStyles = clsx(styles?.body, className, {
|
||||
"backdrop-blur-md backdrop-saturate-[1.8]": isBlurred,
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={domRef}
|
||||
{...otherProps}
|
||||
className={clsx(
|
||||
"w-full h-auto p-3 flex items-center overflow-hidden color-inherit rounded-b-xl",
|
||||
{
|
||||
"backdrop-blur-md backdrop-saturate-[1.8]": isBlurred,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
className={slots.footer?.({class: footerStyles})}
|
||||
{...filterDOMProps(otherProps, {labelable: true})}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {filterDOMProps} from "@react-aria/utils";
|
||||
|
||||
import {useCardContext} from "./card-context";
|
||||
|
||||
const CardHeader = forwardRef<HTMLNextUIProps, "div">((props, ref) => {
|
||||
const {as, className, children, ...otherProps} = props;
|
||||
@ -8,15 +11,16 @@ const CardHeader = forwardRef<HTMLNextUIProps, "div">((props, ref) => {
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, styles} = useCardContext();
|
||||
|
||||
const headerStyles = clsx(styles?.header, className);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component
|
||||
ref={domRef}
|
||||
{...otherProps}
|
||||
className={clsx(
|
||||
"flex justify-start items-center shrink-0 w-full overflow-inherit color-inherit p-3 z-10",
|
||||
className,
|
||||
)}
|
||||
className={slots.header?.({class: headerStyles})}
|
||||
{...filterDOMProps(otherProps, {labelable: true})}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
|
||||
@ -1,21 +1,30 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {Drip} from "@nextui-org/drip";
|
||||
|
||||
import {CardProvider} from "./card-context";
|
||||
import {useCard, UseCardProps} from "./use-card";
|
||||
|
||||
export interface CardProps extends Omit<UseCardProps, "ref"> {}
|
||||
|
||||
const Card = forwardRef<CardProps, "div">((props, ref) => {
|
||||
const {children, Component, drips, isPressable, disableAnimation, disableRipple, getCardProps} =
|
||||
useCard({
|
||||
ref,
|
||||
...props,
|
||||
});
|
||||
const {
|
||||
children,
|
||||
context,
|
||||
Component,
|
||||
drips,
|
||||
isPressable,
|
||||
disableAnimation,
|
||||
disableRipple,
|
||||
getCardProps,
|
||||
} = useCard({
|
||||
ref,
|
||||
...props,
|
||||
});
|
||||
|
||||
return (
|
||||
<Component {...getCardProps()}>
|
||||
<CardProvider value={context}>{children}</CardProvider>
|
||||
{isPressable && !disableAnimation && !disableRipple && <Drip drips={drips} />}
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
// export types
|
||||
export type {CardProps} from "./card";
|
||||
export type {CardFooterProps} from "./card-footer";
|
||||
|
||||
// export hooks
|
||||
export {useCard} from "./use-card";
|
||||
|
||||
// export components
|
||||
export {default as Card} from "./card";
|
||||
export {default as CardHeader} from "./card-header";
|
||||
export {default as CardBody} from "./card-body";
|
||||
export {default as CardFooter} from "./card-footer";
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import type {FocusableProps, PressEvents} from "@react-types/shared";
|
||||
import type {SlotsToClasses, CardSlots, CardVariantProps} from "@nextui-org/theme";
|
||||
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 {filterDOMProps, mergeProps} from "@react-aria/utils";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
import {useButton as useAriaButton} from "@react-aria/button";
|
||||
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
|
||||
import {callAllHandlers, clsx, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {callAllHandlers, clsx, dataAttr, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {useDrip} from "@nextui-org/drip";
|
||||
import {AriaButtonProps} from "@react-aria/button";
|
||||
@ -19,7 +19,7 @@ export interface Props extends HTMLNextUIProps<"div"> {
|
||||
*/
|
||||
ref: ReactRef<HTMLDivElement | null>;
|
||||
/**
|
||||
* Whether the card should show a ripple animation on press
|
||||
* 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;
|
||||
@ -48,6 +48,16 @@ export interface Props extends HTMLNextUIProps<"div"> {
|
||||
|
||||
export type UseCardProps = Props & PressEvents & FocusableProps & CardVariantProps;
|
||||
|
||||
export type ContextType = {
|
||||
slots: CardReturnType;
|
||||
styles?: SlotsToClasses<CardSlots>;
|
||||
variant?: CardVariantProps["variant"];
|
||||
isDisabled?: CardVariantProps["isDisabled"];
|
||||
isFooterBlurred?: CardVariantProps["isFooterBlurred"];
|
||||
disableAnimation?: CardVariantProps["disableAnimation"];
|
||||
fullWidth?: CardVariantProps["fullWidth"];
|
||||
};
|
||||
|
||||
export function useCard(originalProps: UseCardProps) {
|
||||
const [props, variantProps] = mapPropsVariants(originalProps, card.variantKeys);
|
||||
|
||||
@ -80,9 +90,9 @@ export function useCard(originalProps: UseCardProps) {
|
||||
|
||||
const {buttonProps, isPressed} = useAriaButton(
|
||||
{
|
||||
onPress,
|
||||
elementType: as,
|
||||
isDisabled: !originalProps.isPressable,
|
||||
onPress,
|
||||
onClick: callAllHandlers(onClick, handleDrip),
|
||||
allowTextSelectionOnPress,
|
||||
...otherProps,
|
||||
@ -108,6 +118,27 @@ export function useCard(originalProps: UseCardProps) {
|
||||
[...Object.values(variantProps), isFocusVisible],
|
||||
);
|
||||
|
||||
const context = useMemo<ContextType>(
|
||||
() => ({
|
||||
variant: originalProps.variant,
|
||||
isDisabled: originalProps.isDisabled,
|
||||
isFooterBlurred: originalProps.isFooterBlurred,
|
||||
disableAnimation: originalProps.disableAnimation,
|
||||
fullWidth: originalProps.fullWidth,
|
||||
slots,
|
||||
styles,
|
||||
}),
|
||||
[
|
||||
slots,
|
||||
styles,
|
||||
originalProps.variant,
|
||||
originalProps.isDisabled,
|
||||
originalProps.isFooterBlurred,
|
||||
originalProps.disableAnimation,
|
||||
originalProps.fullWidth,
|
||||
],
|
||||
);
|
||||
|
||||
const getCardProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
@ -115,11 +146,15 @@ export function useCard(originalProps: UseCardProps) {
|
||||
className: slots.base({class: baseStyles}),
|
||||
role: originalProps.isPressable ? "button" : "section",
|
||||
tabIndex: originalProps.isPressable ? 0 : -1,
|
||||
"data-hover": dataAttr(isHovered),
|
||||
"data-pressed": dataAttr(isPressed),
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-disabled": dataAttr(originalProps.isDisabled),
|
||||
...mergeProps(
|
||||
originalProps.isPressable ? {...buttonProps, ...focusProps} : {},
|
||||
originalProps.isHoverable ? hoverProps : {},
|
||||
otherProps,
|
||||
props,
|
||||
filterDOMProps(otherProps, {labelable: true}),
|
||||
filterDOMProps(props, {labelable: true}),
|
||||
),
|
||||
};
|
||||
},
|
||||
@ -129,6 +164,10 @@ export function useCard(originalProps: UseCardProps) {
|
||||
baseStyles,
|
||||
originalProps.isPressable,
|
||||
originalProps.isHoverable,
|
||||
originalProps.isDisabled,
|
||||
isHovered,
|
||||
isPressed,
|
||||
isFocusVisible,
|
||||
buttonProps,
|
||||
focusProps,
|
||||
hoverProps,
|
||||
@ -137,6 +176,7 @@ export function useCard(originalProps: UseCardProps) {
|
||||
);
|
||||
|
||||
return {
|
||||
context,
|
||||
domRef,
|
||||
Component,
|
||||
styles,
|
||||
|
||||
@ -34,37 +34,85 @@ const card = tv({
|
||||
"dark:bg-neutral-900",
|
||||
"dark:text-foreground-dark",
|
||||
],
|
||||
header: "",
|
||||
body: "",
|
||||
footer: "",
|
||||
header: [
|
||||
"flex",
|
||||
"justify-start",
|
||||
"items-center",
|
||||
"shrink-0",
|
||||
"w-full",
|
||||
"overflow-inherit",
|
||||
"color-inherit",
|
||||
"p-3",
|
||||
"z-10",
|
||||
],
|
||||
body: [
|
||||
"relative",
|
||||
"flex",
|
||||
"flex-auto",
|
||||
"flex-col",
|
||||
"place-content-inherit",
|
||||
"align-items-inherit",
|
||||
"w-full",
|
||||
"h-auto",
|
||||
"py-5",
|
||||
"px-3",
|
||||
"text-left",
|
||||
"overflow-y-auto",
|
||||
],
|
||||
footer: [
|
||||
"w-full",
|
||||
"h-auto",
|
||||
"p-3",
|
||||
"flex",
|
||||
"items-center",
|
||||
"overflow-hidden",
|
||||
"color-inherit",
|
||||
"rounded-b-xl",
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
shadow: "drop-shadow-lg",
|
||||
bordered: "border-border dark:border-border-dark",
|
||||
flat: "bg-neutral-100",
|
||||
shadow: {
|
||||
base: "drop-shadow-lg",
|
||||
},
|
||||
bordered: {
|
||||
base: "border-border dark:border-border-dark",
|
||||
},
|
||||
flat: {
|
||||
base: "bg-neutral-100",
|
||||
},
|
||||
},
|
||||
borderWeight: {
|
||||
light: "",
|
||||
normal: "",
|
||||
bold: "",
|
||||
extrabold: "",
|
||||
black: "",
|
||||
fullWidth: {
|
||||
true: {
|
||||
base: "w-full",
|
||||
},
|
||||
},
|
||||
isHoverable: {
|
||||
true: "hover:drop-shadow-lg",
|
||||
true: {
|
||||
base: "hover:drop-shadow-lg",
|
||||
},
|
||||
},
|
||||
isPressable: {
|
||||
true: "cursor-pointer",
|
||||
true: {base: "cursor-pointer"},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
base: [...ringClasses],
|
||||
},
|
||||
},
|
||||
isFooterBlurred: {
|
||||
true: {
|
||||
footer: "backdrop-blur-md backdrop-saturate-[1.8]",
|
||||
},
|
||||
},
|
||||
isDisabled: {
|
||||
true: {
|
||||
base: "opacity-50 cursor-not-allowed",
|
||||
},
|
||||
},
|
||||
disableAnimation: {
|
||||
true: "",
|
||||
false: "!transition motion-reduce:transition-none",
|
||||
false: {base: "!transition motion-reduce:transition-none"},
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
@ -113,5 +161,6 @@ const card = tv({
|
||||
|
||||
export type CardVariantProps = VariantProps<typeof card>;
|
||||
export type CardSlots = keyof ReturnType<typeof card>;
|
||||
export type CardReturnType = ReturnType<typeof card>;
|
||||
|
||||
export {card};
|
||||
|
||||
@ -27,7 +27,7 @@ const chip = tv({
|
||||
avatar: "flex-shrink-0",
|
||||
closeButton: [
|
||||
"z-10",
|
||||
"apparance-none",
|
||||
"appearance-none",
|
||||
"outline-none",
|
||||
"select-none",
|
||||
"transition-opacity",
|
||||
|
||||
@ -23,7 +23,7 @@ const snippet = tv({
|
||||
slots: {
|
||||
base: "inline-flex items-center justify-between space-x-3 rounded-md",
|
||||
pre: "bg-transparent text-inherit font-mono whitespace-pre-wrap",
|
||||
copy: "z-10 apparance-none outline-none",
|
||||
copy: "z-10 appearance-none outline-none",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user