feat(card): context wrapper added

This commit is contained in:
Junior Garcia 2023-03-25 14:39:15 -03:00
parent e9bbf2e673
commit f5b3c788e4
10 changed files with 176 additions and 49 deletions

View File

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

View 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 />",
});

View File

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

View File

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

View File

@ -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>
);
});

View File

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

View File

@ -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,

View File

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

View File

@ -27,7 +27,7 @@ const chip = tv({
avatar: "flex-shrink-0",
closeButton: [
"z-10",
"apparance-none",
"appearance-none",
"outline-none",
"select-none",
"transition-opacity",

View File

@ -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: {