diff --git a/src/components/Card/Card-v2.module.css b/src/components/Card/Card-v2.module.css new file mode 100755 index 000000000..4e69aff22 --- /dev/null +++ b/src/components/Card/Card-v2.module.css @@ -0,0 +1,136 @@ +/*------------------------------------*\ +    # CARD +\*------------------------------------*/ + +/** + * A card is a block that typically contains a title, image, text, and/or calls to action. + * The `:where` pseudo class function allows easy overriding via className. + */ +:where(.card) { + position: relative; + display: flex; + flex-direction: column; + + padding: 1.5rem; + height: 100%; + border: var(--eds-theme-border-width) solid; + + + &.card--container-style-low { + border-color: var(--eds-theme-color-border-utility-default-low-emphasis); + } + + &.card--container-style-medium { + border-color: var(--eds-theme-color-border-utility-default-medium-emphasis) + } + + &.card--container-style-high { + border-color: var(--eds-theme-color-border-utility-default-high-emphasis); + } + + &.card--container-style-none { + border-color: transparent; + } + + &.card--background-default { + background-color: var(--eds-theme-color-background-utility-container); + + &:hover { + background-color: var(--eds-theme-color-background-utility-container-hover); + } + + &:active { + background-color: var(--eds-theme-color-background-utility-container-active); + } + } + + &.card--background-call-out { + background-color: var(--eds-theme-color-background-utility-information-low-emphasis); + + &:hover { + background-color: var(--eds-theme-color-background-utility-information-low-emphasis-hover); + } + + &:active { + background-color: var(--eds-theme-color-background-utility-information-low-emphasis-active); + } + } + + &.card--background-call-out.card--container-style-medium { + border-color: var(--eds-theme-color-border-utility-informational); + } + + &:focus-visible { + outline-offset: 1px; + outline: 0.125rem solid var(--eds-theme-color-border-utility-focus); + } + + /** + * Error States TODO-AH verify with design error states from interactive state table + */ + &.card--container-style-low.card--background-call-out { + border-color: fuchsia; + border-style: dashed; + } + +} + +.card__header { + display: flex; + + &.header--size-sm { + gap: 0.5rem + } + + &.header--size-md { + gap: 1rem; + } +} + +.card__top-stripe { + position: absolute; + top: 0; + left: 0; + + width: 100%; +} + +/* TODO-AH: revisit handling the brand colors for top stripe */ +:where(.card__top-stripe) { + background-color: var(--eds-theme-color-background-utility-default-high-emphasis); +} + +.top-stripe--medium { + height: 0.5rem; +} + +.top-stripe--high { + height: 1rem; +} + +.header__icon, +.header__action { + flex-grow: 0; +} + +.header__text { + flex-grow: 2; +} + +.card__body { + flex-grow: 1; + + color: var(--eds-theme-color-text-utility-default-primary); +} + +.header__eyebrow { + color: var(--eds-theme-color-text-utility-default-secondary); +} + +.header__title { + color: var(--eds-theme-color-text-utility-default-primary); +} + +.header__sub-title { + color: var(--eds-theme-color-text-utility-default-secondary); +} \ No newline at end of file diff --git a/src/components/Card/Card-v2.stories.tsx b/src/components/Card/Card-v2.stories.tsx new file mode 100644 index 000000000..a3a114cdc --- /dev/null +++ b/src/components/Card/Card-v2.stories.tsx @@ -0,0 +1,330 @@ +import type { StoryObj, Meta } from '@storybook/react'; +// import { userEvent, within } from '@storybook/testing-library'; +import React from 'react'; + +import { Card } from './Card-v2'; + +import { ButtonV2 as Button } from '../Button'; +import { ButtonGroupV2 as ButtonGroup } from '../ButtonGroup'; +import Icon from '../Icon'; +import { MenuV2 as Menu } from '../Menu'; +import { Text } from '../Text/Text'; + +export default { + title: 'Components/V2/Card', + component: Card, + parameters: { + badges: ['intro-1.0', 'current-2.0'], + }, + decorators: [(Story) =>
{Story()}
], + args: { + children: ( + <> + +
Card Header
+
+ +
Card Body
+
+ +
Card Footer
+
+ + ), + }, + argTypes: { + children: { + control: { + type: null, + }, + }, + }, +} as Meta; + +type Args = React.ComponentProps; + +/** + * Cards come with structural containers for semantic grouping. + */ +export const Default: StoryObj = {}; + +export const WithFullHeader: StoryObj = { + args: { + children: ( + <> + + +
Card Body
+
+ +
Card Footer
+
+ + ), + }, +}; + +export const WithFullHeaderAndIcon: StoryObj = { + args: { + children: ( + <> + + +
Card Body
+
+ +
Card Footer
+
+ + ), + }, +}; + +function CardMenu() { + return ( + + + + + + + Headless UI Docs + + + MDN: Menu + + console.log('Item clicked')}> + Trigger Action + + + Not Possible (disabled) + + + + ); +} + +export const WithSmallFullHeaderAndIcon: StoryObj = { + args: { + children: ( + <> + } + eyebrow="Recommended for you" + icon="account-circle" + size="sm" + subTitle="Get to know your colleagues" + title="Question of the day" + /> + +
Card Body
+
+ +
Card Footer
+
+ + ), + }, +}; + +export const WithHorizontalPrimaryButton: StoryObj = { + args: { + children: ( + <> + } + eyebrow="Recommended for you" + size="md" + subTitle="Get to know your colleagues" + title="Question of the day" + /> + +
Card Body
+
+ + + {/* This has to be manually tested since Tooltip tests are flaky in Chromatic */} + + + + + ), + }, +}; + +export const TopStripe: StoryObj = { + args: { + topStripe: 'medium', + children: ( + <> + } + eyebrow="Recommended for you" + size="md" + subTitle="Get to know your colleagues" + title="Question of the day" + /> + +
Card Body
+
+ + + {/* This has to be manually tested since Tooltip tests are flaky in Chromatic */} + + + + + ), + }, +}; + +export const CustomTopStripe: StoryObj = { + args: { + topStripe: 'high', + topStripeClassName: 'bg-utility-favorable-highEmphasis-hover', + children: ( + <> + } + eyebrow="Recommended for you" + size="md" + subTitle="Get to know your colleagues" + title="Question of the day" + /> + +
Card Body
+
+ + + {/* This has to be manually tested since Tooltip tests are flaky in Chromatic */} + + + + + ), + }, +}; + +export const BackgroundCallout: StoryObj = { + args: { + background: 'call-out', + containerStyle: 'medium', + topStripeClassName: 'bg-utility-favorable-highEmphasis-hover', + children: ( + <> + } + eyebrow="Recommended for you" + size="md" + subTitle="Get to know your colleagues" + title="Question of the day" + /> + +
Card Body
+
+ + + {/* This has to be manually tested since Tooltip tests are flaky in Chromatic */} + + + + + ), + }, +}; + +export const Focusable: StoryObj = { + args: { + tabIndex: 0, + topStripeClassName: 'bg-utility-favorable-highEmphasis-hover', + children: ( + <> + } + eyebrow="Recommended for you" + size="md" + subTitle="Get to know your colleagues" + title="Question of the day" + /> + +
Card Body
+
+ + + {/* This has to be manually tested since Tooltip tests are flaky in Chromatic */} + + + + + ), + }, +}; + +/** + * Implementation Example: Cancelling a card membership + */ +export const CancelMembership: StoryObj = { + parameters: { + badges: ['intro-1.0', 'current-2.0', 'implementationExample'], + }, + args: { + children: ( + <> + + + + Lorem ipsum dolor sit amet consectetur. Id pretium consequat + consequat aliquam arcu + + + + + {/* This has to be manually tested since Tooltip tests are flaky in Chromatic */} + + + + + ), + }, +}; diff --git a/src/components/Card/Card-v2.test.ts b/src/components/Card/Card-v2.test.ts new file mode 100644 index 000000000..5ff7a1619 --- /dev/null +++ b/src/components/Card/Card-v2.test.ts @@ -0,0 +1,8 @@ +import { generateSnapshots } from '@chanzuckerberg/story-utils'; +import type { StoryFile } from '@storybook/testing-react'; + +import * as stories from './Card-v2.stories'; + +describe(' (v2)', () => { + generateSnapshots(stories as StoryFile); +}); diff --git a/src/components/Card/Card-v2.tsx b/src/components/Card/Card-v2.tsx new file mode 100644 index 000000000..d49c7b67f --- /dev/null +++ b/src/components/Card/Card-v2.tsx @@ -0,0 +1,257 @@ +import clsx from 'clsx'; +import type { HTMLAttributes, ReactNode } from 'react'; +import React from 'react'; + +import type { Size } from '../../util/variant-types'; +import type { IconName } from '../Icon'; + +import Icon from '../Icon'; +import Text from '../Text'; + +import styles from './Card-v2.module.css'; + +/** + * TODO-AH: + * - handling of clickable cards (how/when possible) + * - handling of cards vs container/sections (discussion) + * - handling/disambiguation of containerStyle, background color that aren't allowed + */ + +export interface CardProps extends HTMLAttributes { + // Component API + /** + * Child node(s) that can be nested inside component + */ + children?: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; + // Design API + /** + * + * **Default is `"default"`**. + */ + background: 'default' | 'call-out'; + /** + * The bounding box and other container emphasis details + * + * **Default is `"low"`**. + */ + containerStyle: 'none' | 'low' | 'medium' | 'high'; + /** + * Decorative top bar used to cause a highlight on a given card. Use + * utility classes to adjust background color. + * + * **Default is `"none"`**. + */ + topStripe: 'none' | 'medium' | 'high'; + /** + * Class to adjust top stripe color. + */ + topStripeClassName: string; +} + +export interface CardSubComponentProps { + // Component API + /** + * Child node(s) that can be nested inside component + */ + children: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; +} + +export interface CardHeaderProps { + // Component API + /** + * Child node(s) that can be nested inside component. Used in place of any of the above named slots. + */ + children?: ReactNode; + /** + * CSS class names that can be appended to the component. + */ + className?: string; + // Design API + /** + * Component slot to add in an action-focused area to a card. Typically a button with hidden options. + */ + action?: ReactNode; + /** + * Text above the main title of the card, to add category text/information. + */ + eyebrow?: string; + /** + * Card slot for an icon sitting in front of the card header text + */ + icon?: IconName; + /** + * Overall size treatment of the Card header + */ + size?: Extract; + /** + * Text below the main title of the card, to add supplementary information about the title + */ + subTitle?: string; + /** + * Main title of the card + */ + title?: string; +} + +/** + * `import {Card} from "@chanzuckerberg/eds";` + * + * Card component is the outer wrapper for the block that typically contains a title, image, + * text, and/or calls to action. + */ +export const Card = ({ + background = 'default', + className, + children, + containerStyle = 'low', + topStripe = 'none', + topStripeClassName, + ...other +}: CardProps) => { + const componentClassName = clsx( + styles['card'], + styles[`card--container-style-${containerStyle}`], + styles[`card--background-${background}`], + className, + ); + return ( +
+ {children} + {topStripe && ( +
+ )} +
+ ); +}; + +/** + * Body of the Card component. + */ +const CardBody = ({ children, className, ...other }: CardSubComponentProps) => { + const componentClassName = clsx(styles['card__body'], className); + return ( +
+ {children} +
+ ); +}; + +/** + * Footer of the Card component. + */ +const CardFooter = ({ + children, + className, + ...other +}: CardSubComponentProps) => { + const componentClassName = clsx(styles['card__footer'], className); + return ( +
+ {children} +
+ ); +}; + +/** + * Header of the Card component. + */ +const CardHeader = ({ + action, + children, + className, + eyebrow, + icon, + size = 'md', + subTitle, + title, + ...other +}: CardHeaderProps) => { + const componentClassName = clsx( + styles['card__header'], + size && styles[`header--size-${size}`], + className, + ); + const headerEyebrowClassName = clsx( + styles['header__eyebrow'], + size && styles[`header--size-${size}`], + ); + const headerTitleClassName = clsx( + styles['header__title'], + size && styles[`header--size-${size}`], + ); + const headerSubTitleClassName = clsx( + styles['header__sub-title'], + size && styles[`header--size-${size}`], + ); + + return children ? ( +
+ {children} +
+ ) : ( +
+ {icon && ( +
+ +
+ )} +
+ {eyebrow && ( + + {eyebrow} + + )} + {title && ( + + {title} + + )} + {subTitle && ( + + {subTitle} + + )} +
+ {action &&
{action}
} +
+ ); +}; + +Card.displayName = 'Card'; +CardBody.displayName = 'Card.Body'; +CardFooter.displayName = 'Card.Footer'; +CardHeader.displayName = 'Card.Header'; + +Card.Body = CardBody; +Card.Footer = CardFooter; +Card.Header = CardHeader; diff --git a/src/components/Card/__snapshots__/Card-v2.test.ts.snap b/src/components/Card/__snapshots__/Card-v2.test.ts.snap new file mode 100644 index 000000000..abe444225 --- /dev/null +++ b/src/components/Card/__snapshots__/Card-v2.test.ts.snap @@ -0,0 +1,833 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` (v2) BackgroundCallout story renders snapshot 1`] = ` +
+
+
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+ +
+
+
+
+ Card Body +
+
+
+
+ +
+
+
+
+
+`; + +exports[` (v2) CancelMembership story renders snapshot 1`] = ` +
+
+
+
+
+ Cancel membership? +
+
+ We're sad to see you go +
+
+
+
+

+ Lorem ipsum dolor sit amet consectetur. Id pretium consequat consequat aliquam arcu +

+
+
+
+ +
+
+
+
+
+`; + +exports[` (v2) CustomTopStripe story renders snapshot 1`] = ` +
+
+
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+ +
+
+
+
+ Card Body +
+
+
+
+ +
+
+
+
+
+`; + +exports[` (v2) Default story renders snapshot 1`] = ` +
+
+
+
+ Card Header +
+
+
+
+ Card Body +
+
+
+
+ Card Footer +
+
+
+
+
+`; + +exports[` (v2) Focusable story renders snapshot 1`] = ` +
+
+
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+ +
+
+
+
+ Card Body +
+
+
+
+ +
+
+
+
+
+`; + +exports[` (v2) TopStripe story renders snapshot 1`] = ` +
+
+
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+ +
+
+
+
+ Card Body +
+
+
+
+ +
+
+
+
+
+`; + +exports[` (v2) WithFullHeader story renders snapshot 1`] = ` +
+
+
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+
+
+ Card Body +
+
+
+
+ Card Footer +
+
+
+
+
+`; + +exports[` (v2) WithFullHeaderAndIcon story renders snapshot 1`] = ` +
+
+
+
+ +
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+
+
+ Card Body +
+
+
+
+ Card Footer +
+
+
+
+
+`; + +exports[` (v2) WithHorizontalPrimaryButton story renders snapshot 1`] = ` +
+
+
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+ +
+
+
+
+ Card Body +
+
+
+
+ +
+
+
+
+
+`; + +exports[` (v2) WithSmallFullHeaderAndIcon story renders snapshot 1`] = ` +
+
+
+
+ +
+
+
+ Recommended for you +
+
+ Question of the day +
+
+ Get to know your colleagues +
+
+
+ +
+
+
+
+ Card Body +
+
+
+
+ Card Footer +
+
+
+
+
+`;