From 7e19bb4ffbd6ed17e414fd1ccf7237c9a963e1b8 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Tue, 10 Dec 2024 13:10:52 -0500 Subject: [PATCH 1/5] feat: resetting styles for Card --- packages/odyssey-react-mui/src/Card.tsx | 536 +++++++++++++++--- .../odyssey-mui/Card/Card.stories.tsx | 357 +++++++----- 2 files changed, 671 insertions(+), 222 deletions(-) diff --git a/packages/odyssey-react-mui/src/Card.tsx b/packages/odyssey-react-mui/src/Card.tsx index e4dacf33e7..865c4cfa44 100644 --- a/packages/odyssey-react-mui/src/Card.tsx +++ b/packages/odyssey-react-mui/src/Card.tsx @@ -11,107 +11,202 @@ */ import { + memo, MouseEventHandler, ReactElement, - memo, + ReactNode, useMemo, - useEffect, } from "react"; import { + CardActionArea as MuiCardActionArea, Card as MuiCard, CardActions as MuiCardActions, - CardActionArea as MuiCardActionArea, } from "@mui/material"; -import styled from "@emotion/styled"; - -import { Button, ButtonContext, MenuButton, MenuButtonProps } from "./Buttons"; -import { MoreIcon } from "./icons.generated"; import { + Button, + ButtonContext, DesignTokens, + Heading5, + MenuButtonProps, + Paragraph, + Support, useOdysseyDesignTokens, -} from "./OdysseyDesignTokensContext"; -import { Heading5, Paragraph, Support } from "./Typography"; -import { Box } from "./Box"; +} from "."; +import styled from "@emotion/styled"; -export const CARD_IMAGE_HEIGHT = "64px"; +export const cardVariantValues = ["tile", "stack", "compact"] as const; export type CardProps = { + children?: ReactNode; description?: string; + detailPanel?: ReactNode; image?: ReactElement; + isLoading?: boolean; overline?: string; title?: string; + variant?: (typeof cardVariantValues)[number]; } & ( | { - onClick: MouseEventHandler; + accessory?: never; button?: never; menuButtonChildren?: never; + onClick: MouseEventHandler; } | { - onClick?: never; + accessory?: ReactNode; button?: ReactElement; menuButtonChildren?: MenuButtonProps["children"]; + onClick?: never; } ); -const ImageContainer = styled("div", { +const StyledCard = styled(MuiCard, { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isClickable", +})<{ + odysseyDesignTokens: DesignTokens; + isClickable: boolean; +}>(({ odysseyDesignTokens, isClickable }) => ({ + border: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderColorDisplay} ${odysseyDesignTokens.BorderStyleMain}`, + boxShadow: "none", + padding: 0, + + ["&:hover"]: isClickable + ? { + borderColor: odysseyDesignTokens.HueNeutralWhite, + boxShadow: odysseyDesignTokens.DepthMedium, + } + : {}, +})); + +const StyledCardActionArea = styled(MuiCardActionArea)(() => ({ + margin: 0, + padding: 0, +})); + +const AccessoriesContainer = styled("div", { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "variant", +})<{ + odysseyDesignTokens: DesignTokens; + variant: CardProps["variant"]; +}>(({ odysseyDesignTokens, variant }) => ({ + // display: "flex", + // TODO: remove the variant thing + backgroundColor: + variant === "tile" + ? odysseyDesignTokens.HueNeutralWhite + : odysseyDesignTokens.HueNeutralWhite, + display: "none", +})); + +const InnerContainer = styled("div", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "hasMenuButtonChildren", + prop !== "odysseyDesignTokens" && prop !== "variant", })<{ odysseyDesignTokens: DesignTokens; - hasMenuButtonChildren: boolean; -}>(({ odysseyDesignTokens, hasMenuButtonChildren }) => ({ + variant: CardProps["variant"]; +}>(({ odysseyDesignTokens, variant }) => ({ display: "flex", - alignItems: "flex-start", - maxHeight: CARD_IMAGE_HEIGHT, - height: CARD_IMAGE_HEIGHT, - marginBlockEnd: odysseyDesignTokens.Spacing5, - paddingRight: hasMenuButtonChildren ? odysseyDesignTokens.Spacing5 : 0, + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + padding: odysseyDesignTokens.Spacing5, + flexDirection: variant === "tile" ? "column" : "row", + gap: odysseyDesignTokens.Spacing4, })); -const MenuButtonContainer = styled("div", { +const ImageContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})<{ odysseyDesignTokens: DesignTokens }>(({ odysseyDesignTokens }) => ({ - position: "absolute", - right: odysseyDesignTokens.Spacing3, - top: odysseyDesignTokens.Spacing3, +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + height: "4.5714285714rem", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, // TODO: remove })); -const CardContentContainer = styled("div")(() => ({ +const ContentContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ display: "flex", + flexDirection: "column", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + gap: odysseyDesignTokens.Spacing4, + + "& > *": { + marginBlock: 0, + }, })); -const buttonProviderValue = { isFullWidth: true }; +const UpperContentContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + display: "flex", + flexDirection: "column", + gap: odysseyDesignTokens.Spacing1, + + "& > .MuiTypography-root": { + marginBlockEnd: 0, + }, +})); + +const Content = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + display: "flex", + flexDirection: "column", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, +})); const Card = ({ - button, + children, description, + detailPanel, image, - menuButtonChildren, - onClick, + isLoading, overline, title, + variant, + accessory, + button, + menuButtonChildren, + onClick, }: CardProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); - const cardContent = useMemo( - () => ( - - - {image && ( - - {image} - - )} + console.log(isLoading, variant, menuButtonChildren, onClick); - {overline && {overline}} - {title && {title}} + const cardContent = useMemo(() => { + const buttonProviderValue = { isFullWidth: true }; + return ( + + {image && ( + + {image} + + )} + + + {overline && {overline}} + {title && {title}} + {description && ( {description} )} + {children && ( + + {children} + + )} + {button && ( @@ -119,63 +214,322 @@ const Card = ({ )} - - - ), - [ - button, - description, - image, - menuButtonChildren, - overline, - title, - odysseyDesignTokens, - ], - ); + + {detailPanel && ( + + {detailPanel} + + )} + + + ); + }, [ + button, + children, + description, + detailPanel, + image, + odysseyDesignTokens, + overline, + title, + variant, + ]); return ( - + + + {accessory} + {onClick ? ( - {cardContent} + + {cardContent} + ) : ( cardContent )} - - {menuButtonChildren && ( - - } - ariaLabel="Card menu" - buttonVariant="floating" - menuAlignment="right" - size="small" - tooltipText="Actions" - > - {menuButtonChildren} - - - )} - + ); }; const MemoizedCard = memo(Card); MemoizedCard.displayName = "Card"; -/** - * @deprecated The 'Tile' component is now called 'Card'. Please update your references as 'Tile' will be deprecated soon. - */ -const Tile = (props: CardProps) => { - useEffect(() => { - console.warn( - "Warning: The 'Tile' component is now called 'Card'. Please update your references as 'Tile' will be deprecated soon.", - ); - }, []); +export { MemoizedCard as Card }; - return ; -}; +// import { +// MouseEventHandler, +// ReactElement, +// memo, +// useMemo, +// ReactNode, +// } from "react"; +// import { +// Card as MuiCard, +// CardActions as MuiCardActions, +// CardActionArea as MuiCardActionArea, +// } from "@mui/material"; +// import styled from "@emotion/styled"; +// import { useTranslation } from "react-i18next"; + +// import { Box } from "./Box"; +// import { +// Button, +// ButtonContext, +// MenuButton, +// MenuButtonProps, +// } from "./Buttons"; +// import { +// DesignTokens, +// useOdysseyDesignTokens, +// } from "./OdysseyDesignTokensContext"; +// import { Heading5, Paragraph, Support } from "./Typography"; +// import { +// MoreIcon, +// } from "./icons.generated"; + +// export const CARD_IMAGE_SIZE = "64px"; +// export const CARD_IMAGE_SIZE_COMPACT = "48px"; + +// export const cardVariantValues = ["tile", "stack", "compact"] as const; + +// export type CardProps = { +// children?: ReactNode; +// description?: string; +// image?: ReactElement; +// overline?: string; +// title?: string; +// variant?: (typeof cardVariantValues)[number]; +// } & ( +// | { +// Accessory?: never; +// button?: never; +// menuButtonChildren?: never; +// onClick: MouseEventHandler; +// } +// | { +// Accessory?: ReactNode; +// button?: ReactElement; +// menuButtonChildren?: MenuButtonProps["children"]; +// onClick?: never; +// } +// ); + +// const StyledMuiCard = styled(MuiCard, { +// shouldForwardProp: (prop) => +// prop !== "odysseyDesignTokens" && +// prop !== "hasAccessory" && +// prop !== "variant", +// })<{ +// odysseyDesignTokens: DesignTokens; +// hasAccessory?: boolean; +// variant: (typeof cardVariantValues)[number]; +// }>(({ odysseyDesignTokens, hasAccessory = false, variant = "tile" }) => ({ +// border: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.HueNeutral200} ${odysseyDesignTokens.BorderStyleMain}`, +// })); + +// const AccessoryContainer = styled("div", { +// shouldForwardProp: (prop) => +// prop !== "odysseyDesignTokens" && prop !== "variant", +// })<{ +// odysseyDesignTokens: DesignTokens; +// variant: (typeof cardVariantValues)[number]; +// }>(({ odysseyDesignTokens, variant }) => ({ +// display: "flex", +// flexDirection: variant === "compact" ? "row" : "column", +// alignItems: "center", +// gap: odysseyDesignTokens.Spacing2, +// height: variant === "compact" ? CARD_IMAGE_SIZE_COMPACT : "auto", +// })); + +// const ImageContainer = styled("div", { +// shouldForwardProp: (prop) => +// prop !== "odysseyDesignTokens" && +// prop !== "hasMenuButtonChildren" && +// prop !== "variant", +// })<{ +// odysseyDesignTokens: DesignTokens; +// hasMenuButtonChildren: boolean; +// variant: (typeof cardVariantValues)[number]; +// }>(({ odysseyDesignTokens, hasMenuButtonChildren, variant }) => ({ +// display: "flex", +// alignItems: "flex-start", +// height: variant === "compact" ? CARD_IMAGE_SIZE_COMPACT : CARD_IMAGE_SIZE, +// maxHeight: variant === "compact" ? CARD_IMAGE_SIZE_COMPACT : CARD_IMAGE_SIZE, +// marginBlockEnd: variant === "tile" ? odysseyDesignTokens.Spacing5 : 0, +// paddingRight: hasMenuButtonChildren ? odysseyDesignTokens.Spacing5 : 0, +// })); + +// const MenuButtonContainer = styled("div", { +// shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +// })<{ +// odysseyDesignTokens: DesignTokens; +// variant: (typeof cardVariantValues)[number]; +// }>(({ odysseyDesignTokens, variant }) => ({ +// position: "absolute", +// right: odysseyDesignTokens.Spacing3, +// top: +// variant === "compact" +// ? odysseyDesignTokens.Spacing4 +// : odysseyDesignTokens.Spacing3, +// height: variant === "compact" ? CARD_IMAGE_SIZE_COMPACT : "auto", +// display: "flex", +// alignItems: "center", +// })); + +// const CardInnerContainer = styled("div", { +// shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +// })<{ odysseyDesignTokens: DesignTokens }>(({ odysseyDesignTokens }) => ({ +// display: "flex", +// gap: odysseyDesignTokens.Spacing3, +// })); + +// const CardImageAndContentContainer = styled("div", { +// shouldForwardProp: (prop) => prop !== "variant", +// })<{ variant: (typeof cardVariantValues)[number]; }>( +// ({ variant }) => ({ +// display: "flex", +// flexDirection: variant === "tile" ? "column" : "row", +// alignItems: "flex-start", +// }), +// ); + +// const CardContent = styled("div", { +// shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +// })<{ +// odysseyDesignTokens: DesignTokens; +// variant: (typeof cardVariantValues)[number]; +// }>(({ odysseyDesignTokens, variant }) => ({ +// "& > .MuiTypography-h5:not(:last-child)": { +// marginBlockEnd: `${variant === "compact" ? odysseyDesignTokens.Spacing1 : odysseyDesignTokens.Spacing3} !important`, +// }, +// "& > *:last-child": { +// marginBlockEnd: 0, +// }, +// })); + +// const CardChildrenContainer = styled("div", { +// shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +// })<{ odysseyDesignTokens: DesignTokens }>(({ odysseyDesignTokens }) => ({ +// ["&:not(:first-child)"]: { +// marginBlockStart: odysseyDesignTokens.Spacing3, +// }, +// })); + +// const buttonProviderValue = { isFullWidth: true }; + +// const Card = ({ +// Accessory: AccessoryProp, +// button, +// children, +// description, +// image, +// menuButtonChildren, +// onClick, +// overline, +// title, +// variant = "tile", +// }: CardProps) => { +// const odysseyDesignTokens = useOdysseyDesignTokens(); +// const { t } = useTranslation(); + +// const Accessory = useMemo(() => { +// return ( +// +// {AccessoryProp} +// +// ); +// }, [AccessoryProp, odysseyDesignTokens, variant]); + +// const cardContent = useMemo(() => { +// return ( +// +// {AccessoryProp && {Accessory}} +// +// {image && ( +// +// {image} +// +// )} + +// +// {overline && {overline}} +// {title && {title}} +// {description && ( +// {description} +// )} + +// {button && ( +// +// +// {button} +// +// +// )} + +// {children && ( +// +// {children} +// +// )} + +// +// +// +// ); +// }, [odysseyDesignTokens, AccessoryProp, Accessory, variant, image, menuButtonChildren, overline, title, description, button, children]); + +// return ( +// +// {onClick ? ( +// {cardContent} +// ) : ( +// cardContent +// )} + +// {menuButtonChildren && ( +// +// } +// ariaLabel={t("table.moreactions.arialabel")} +// buttonVariant="floating" +// menuAlignment="right" +// size="small" +// tooltipText={t("table.actions")} +// > +// {menuButtonChildren} +// +// +// )} +// +// ); +// }; -const MemoizedTile = memo(Tile); -MemoizedTile.displayName = "Tile"; +// const MemoizedCard = memo(Card); +// MemoizedCard.displayName = "Card"; -export { MemoizedCard as Card, MemoizedTile as Tile }; +// export { MemoizedCard as Card }; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx index b09761c5eb..b8c34425d5 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx @@ -13,97 +13,90 @@ import { Meta, StoryObj } from "@storybook/react"; import { MuiThemeDecorator } from "../../../../.storybook/components"; import { - Box, Button, Card, MenuItem, CardProps, + cardVariantValues, + Checkbox, } from "@okta/odyssey-react-mui"; +import { CheckboxProps as MuiCheckboxProps } from "@okta/odyssey-react-mui"; +import { ChevronDownIcon, ChevronUpIcon } from "@okta/odyssey-react-mui/icons"; +import { jest } from "@storybook/jest"; +import { MouseEventHandler, useCallback, useState } from "react"; +type CardMetaProps = Omit< + CardProps, + | "children" + | "detailPanel" + | "image" + | "accessory" + | "button" + | "menuButtonChildren" + | "onClick" +> & { + children?: boolean; + detailPanel?: boolean; + image?: boolean; + accessory?: boolean; + button?: boolean; + menuButtonChildren?: boolean; + onClick?: boolean; +}; -const storybookMeta: Meta = { +const storybookMeta: Meta = { title: "MUI Components/Card", - component: Card, argTypes: { - title: { - control: "text", - description: "The heading of the card.", - table: { - type: { - summary: "string", - }, - defaultValue: "", - }, + children: { + control: "boolean", }, description: { control: "text", - description: - "The body text of the card. The consumer is responsible for truncating this string.", - table: { - type: { - summary: "string", - }, - defaultValue: "", - }, + }, + detailPanel: { + control: "boolean", + }, + image: { + control: "boolean", + }, + isLoading: { + control: "boolean", }, overline: { control: "text", - description: 'The "eyebrow" text above the card title.', - table: { - type: { - summary: "string", - }, - defaultValue: "", - }, }, - image: { - control: null, - description: - "An optional image or icon at the top of the card, preferably as an img or svg element.", - table: { - type: { - summary: "ReactElement", - }, - defaultValue: "", - }, + title: { + control: "text", }, - onClick: { - control: null, - description: "The event handler for when the user clicks the card.", - table: { - type: { - summary: "MouseEventHandler", - }, - defaultValue: "", - }, + variant: { + control: "select", + options: cardVariantValues, + }, + accessory: { + control: "boolean", }, button: { - control: null, - description: - "The main action button for the card. Not valid if the card itself is clickable.", - table: { - type: { - summary: "ReactElement", - }, - defaultValue: "", - }, + control: "boolean", }, menuButtonChildren: { - control: null, - description: - "Menu items to be rendered in the card's optional menu button. If this prop is undefined, the menu button will not be shown. Not valid if the card itself is clickable.", - table: { - type: { - summary: "[MenuItem | Divider | ListSubheader]", - }, - defaultValue: "", - }, + control: "boolean", + }, + onClick: { + control: "boolean", }, }, args: { - title: "Title", + children: false, description: - "Identity can create great user experiences, increase customer sign-ups, and...", - overline: "Overline", + "This is a description of the app. Descriptions should be concise, and truncation is handled by the consumer rather than the component, so if you write a novel, it will display a novel.", + detailPanel: false, + image: undefined, + isLoading: false, + overline: "Category", + title: "App title", + variant: "tile", + accessory: false, + button: undefined, + menuButtonChildren: false, onClick: undefined, }, decorators: [MuiThemeDecorator], @@ -111,82 +104,184 @@ const storybookMeta: Meta = { export default storybookMeta; -export const Default: StoryObj = { - render: ({ ...props }) => ( - - } - image={Example logo} - menuButtonChildren={ - <> - Menu option - Menu option - Menu option - - } +const image = Example logo; + +const children = <>Children.; +const detailPanel = <>Detail panel.; +const button =