From 0bc1fadc94ab7abfcaabce1339691f096664f9fc Mon Sep 17 00:00:00 2001 From: Pavel Klibani Date: Tue, 19 Nov 2024 08:25:30 +0100 Subject: [PATCH] Feat(web-react): Introduce Card component #1535 --- .../examples/CardComposition.stories.tsx | 315 ++++++++++++++++++ packages/web-react/scripts/entryPoints.js | 1 + .../web-react/src/components/Card/Card.tsx | 29 ++ .../src/components/Card/CardBody.tsx | 21 ++ .../src/components/Card/CardEyebrow.tsx | 21 ++ .../src/components/Card/CardFooter.tsx | 27 ++ .../src/components/Card/CardLink.tsx | 20 ++ .../src/components/Card/CardMedia.tsx | 28 ++ .../src/components/Card/CardTitle.tsx | 27 ++ .../web-react/src/components/Card/README.md | 147 ++++++++ .../components/Card/__tests__/Card.test.tsx | 51 +++ .../Card/__tests__/CardBody.test.tsx | 27 ++ .../Card/__tests__/CardEyebrows.test.tsx | 27 ++ .../Card/__tests__/CardFooter.test.tsx | 33 ++ .../Card/__tests__/CardLink.test.tsx | 27 ++ .../Card/__tests__/CardMedia.test.tsx | 47 +++ .../Card/__tests__/CardTitle.test.tsx | 39 +++ .../Card/__tests__/useCardStyleProps.test.ts | 39 +++ .../Card/demo/CardFooterAlignment.tsx | 74 ++++ .../Card/demo/CardGeneralOptions.tsx | 75 +++++ .../src/components/Card/demo/CardGrid.tsx | 79 +++++ .../Card/demo/CardHorizontalLayout.tsx | 57 ++++ .../components/Card/demo/CardMediaOptions.tsx | 139 ++++++++ .../components/Card/demo/CardMediaSizes.tsx | 162 +++++++++ .../demo/CardReversedHorizontalLayout.tsx | 57 ++++ .../components/Card/demo/CardTitleOptions.tsx | 48 +++ .../src/components/Card/demo/constants.ts | 2 + .../src/components/Card/demo/index.tsx | 40 +++ .../web-react/src/components/Card/index.html | 1 + .../web-react/src/components/Card/index.ts | 10 + .../components/Card/stories/Card.stories.tsx | 79 +++++ .../Card/stories/CardBody.stories.tsx | 64 ++++ .../Card/stories/CardEyebrow.stories.tsx | 69 ++++ .../Card/stories/CardFooter.stories.tsx | 81 +++++ .../Card/stories/CardLink.stories.tsx | 82 +++++ .../Card/stories/CardMedia.stories.tsx | 102 ++++++ .../Card/stories/CardTitle.stories.tsx | 85 +++++ .../src/components/Card/useCardStyleProps.ts | 63 ++++ packages/web-react/src/components/index.ts | 1 + packages/web-react/src/types/card.ts | 74 ++++ packages/web-react/src/types/index.ts | 1 + 41 files changed, 2371 insertions(+) create mode 100644 packages/web-react/docs/stories/examples/CardComposition.stories.tsx create mode 100644 packages/web-react/src/components/Card/Card.tsx create mode 100644 packages/web-react/src/components/Card/CardBody.tsx create mode 100644 packages/web-react/src/components/Card/CardEyebrow.tsx create mode 100644 packages/web-react/src/components/Card/CardFooter.tsx create mode 100644 packages/web-react/src/components/Card/CardLink.tsx create mode 100644 packages/web-react/src/components/Card/CardMedia.tsx create mode 100644 packages/web-react/src/components/Card/CardTitle.tsx create mode 100644 packages/web-react/src/components/Card/README.md create mode 100644 packages/web-react/src/components/Card/__tests__/Card.test.tsx create mode 100644 packages/web-react/src/components/Card/__tests__/CardBody.test.tsx create mode 100644 packages/web-react/src/components/Card/__tests__/CardEyebrows.test.tsx create mode 100644 packages/web-react/src/components/Card/__tests__/CardFooter.test.tsx create mode 100644 packages/web-react/src/components/Card/__tests__/CardLink.test.tsx create mode 100644 packages/web-react/src/components/Card/__tests__/CardMedia.test.tsx create mode 100644 packages/web-react/src/components/Card/__tests__/CardTitle.test.tsx create mode 100644 packages/web-react/src/components/Card/__tests__/useCardStyleProps.test.ts create mode 100644 packages/web-react/src/components/Card/demo/CardFooterAlignment.tsx create mode 100644 packages/web-react/src/components/Card/demo/CardGeneralOptions.tsx create mode 100644 packages/web-react/src/components/Card/demo/CardGrid.tsx create mode 100644 packages/web-react/src/components/Card/demo/CardHorizontalLayout.tsx create mode 100644 packages/web-react/src/components/Card/demo/CardMediaOptions.tsx create mode 100644 packages/web-react/src/components/Card/demo/CardMediaSizes.tsx create mode 100644 packages/web-react/src/components/Card/demo/CardReversedHorizontalLayout.tsx create mode 100644 packages/web-react/src/components/Card/demo/CardTitleOptions.tsx create mode 100644 packages/web-react/src/components/Card/demo/constants.ts create mode 100644 packages/web-react/src/components/Card/demo/index.tsx create mode 100644 packages/web-react/src/components/Card/index.html create mode 100644 packages/web-react/src/components/Card/index.ts create mode 100644 packages/web-react/src/components/Card/stories/Card.stories.tsx create mode 100644 packages/web-react/src/components/Card/stories/CardBody.stories.tsx create mode 100644 packages/web-react/src/components/Card/stories/CardEyebrow.stories.tsx create mode 100644 packages/web-react/src/components/Card/stories/CardFooter.stories.tsx create mode 100644 packages/web-react/src/components/Card/stories/CardLink.stories.tsx create mode 100644 packages/web-react/src/components/Card/stories/CardMedia.stories.tsx create mode 100644 packages/web-react/src/components/Card/stories/CardTitle.stories.tsx create mode 100644 packages/web-react/src/components/Card/useCardStyleProps.ts create mode 100644 packages/web-react/src/types/card.ts diff --git a/packages/web-react/docs/stories/examples/CardComposition.stories.tsx b/packages/web-react/docs/stories/examples/CardComposition.stories.tsx new file mode 100644 index 0000000000..11a72e7044 --- /dev/null +++ b/packages/web-react/docs/stories/examples/CardComposition.stories.tsx @@ -0,0 +1,315 @@ +import React, { ElementType } from 'react'; +import { + Button, + Card, + CardBody, + CardEyebrow, + CardFooter, + CardLink, + CardMedia, + CardTitle, + Container, + Grid, + UseCardStyleProps, +} from '../../../src/components'; +import { MEDIA_IMAGE } from '../../../src/components/Card/demo/constants'; +import { AlignmentX, Direction, Sizes } from '../../../src/constants'; +import { GridColumns } from '../../../src/types'; + +type CardCompositionType = { + cardElementType: ElementType; + contentText: string; + eyebrowText: string; + gridCols: GridColumns; + image: string; + numCards: number; + showContent: boolean; + showEyebrow: boolean; + showFooter: boolean; + showMedia: boolean; + showTitle: boolean; + titleElementType: ElementType; + titleText: string; + titleWithLink: boolean; + wrapInContainer: boolean; +} & UseCardStyleProps; + +export default { + title: 'Examples/Compositions', + argTypes: { + alignmentX: { + control: 'select', + description: 'Alignment inside CardFooter component.', + options: [...Object.values(AlignmentX)], + table: { + category: 'CardFooter', + defaultValue: { summary: AlignmentX.LEFT }, + }, + }, + cardElementType: { + control: 'text', + name: 'elementType', + description: 'Element type for the card.', + table: { + category: 'Card', + defaultValue: { summary: 'article' }, + }, + }, + contentText: { + control: 'text', + description: 'Text for the user content.', + name: 'children', + table: { + category: 'CardBody', + defaultValue: { + summary: '', + }, + }, + }, + direction: { + control: 'select', + description: 'Direction of the card.', + options: [...Object.values(Direction), 'horizontal-reversed'], + table: { + category: 'Card', + defaultValue: { summary: Direction.VERTICAL }, + }, + }, + eyebrowText: { + control: 'text', + description: 'Text for the CardEyebrow component.', + name: 'children', + table: { + category: 'CardEyebrow', + defaultValue: { summary: '' }, + }, + }, + gridCols: { + control: 'select', + name: 'grid columns', + description: 'Number of columns in the grid.', + options: [1, 2, 3, 4, 5, 6, 12], + }, + hasFilledHeight: { + control: 'boolean', + description: 'Fill the height of the media.', + table: { + category: 'CardMedia', + defaultValue: { summary: false }, + }, + }, + image: { + control: 'text', + description: 'Image source for the CardMedia image.', + name: 'image url', + table: { + category: 'CardMedia', + subcategory: 'Demo settings', + }, + }, + isBoxed: { + control: 'boolean', + description: 'Border around the card.', + table: { + category: 'Card', + defaultValue: { summary: false }, + }, + }, + isExpanded: { + control: 'boolean', + description: 'Expand the media to fill the card. Only works when isBoxed is true.', + table: { + category: 'CardMedia', + defaultValue: { summary: false }, + }, + }, + isHeading: { + control: 'boolean', + description: 'If true, the CardTitle will render as a heading.', + table: { + category: 'CardTitle', + defaultValue: { summary: true }, + }, + }, + numCards: { + control: 'select', + name: 'number of cards', + description: 'Number of cards to display.', + options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + }, + showFooter: { + control: 'boolean', + description: 'Show the CardFooter component.', + name: 'show footer', + table: { + category: 'CardFooter', + subcategory: 'Demo settings', + }, + }, + showContent: { + control: 'boolean', + description: 'Show the user content component.', + name: 'show card content', + table: { + category: 'CardBody', + subcategory: 'Demo settings', + }, + }, + showEyebrow: { + control: 'boolean', + description: 'Show the CardEyebrow component.', + name: 'show eyebrow', + table: { + category: 'CardEyebrow', + subcategory: 'Demo settings', + }, + }, + showMedia: { + control: 'boolean', + description: 'Show the CardMedia component.', + name: 'show media', + table: { + category: 'CardMedia', + subcategory: 'Demo settings', + }, + }, + showTitle: { + control: 'boolean', + description: 'Show the CardTitle component.', + name: 'show title', + table: { + category: 'CardTitle', + subcategory: 'Demo settings', + }, + }, + size: { + control: 'select', + description: 'Size of the media.', + options: [...Object.values(Sizes), 'auto'], + table: { + category: 'CardMedia', + defaultValue: { summary: Sizes.MEDIUM }, + }, + }, + titleElementType: { + control: 'text', + name: 'elementType', + description: 'Element type for the title.', + table: { + category: 'CardTitle', + defaultValue: { summary: 'h4' }, + }, + }, + titleText: { + control: 'text', + description: 'Text for the CardTitle component.', + name: 'children', + table: { + category: 'CardTitle', + defaultValue: { summary: '' }, + }, + }, + titleWithLink: { + control: 'boolean', + description: 'Add a link to the CardTitle component.', + name: 'title as link', + table: { + category: 'CardTitle', + subcategory: 'Demo settings', + }, + }, + wrapInContainer: { + control: 'boolean', + description: 'Wrap the card in a container.', + name: 'wrap cards in container', + }, + }, + args: { + alignmentX: AlignmentX.LEFT, + cardElementType: 'article', + contentText: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc imperdiet justo nec dolor.', + direction: Direction.VERTICAL, + eyebrowText: 'Eyebrow title', + gridCols: 3, + hasFilledHeight: false, + image: MEDIA_IMAGE, + isBoxed: false, + isExpanded: false, + isHeading: true, + numCards: 3, + showFooter: true, + showContent: true, + showEyebrow: true, + showMedia: true, + showTitle: true, + size: Sizes.MEDIUM, + titleElementType: 'h4', + titleText: 'Card Title', + titleWithLink: false, + wrapInContainer: true, + }, +}; + +export const CardComposition = (args: CardCompositionType) => { + const { + alignmentX, + cardElementType, + contentText, + direction, + eyebrowText, + gridCols, + hasFilledHeight, + image, + isBoxed, + isExpanded, + isHeading, + numCards, + showContent, + showEyebrow, + showFooter, + showMedia, + showTitle, + size, + titleElementType, + titleText, + titleWithLink, + wrapInContainer, + ...restProps + } = args; + + const renderTitle = () => ( + + {titleWithLink ? {titleText} : titleText} + + ); + + const renderCard = () => ( + + {Array.from({ length: numCards }, (_, index) => ( + + {showMedia && ( + + + + )} + {(showEyebrow || showTitle || showContent) && ( + + {showEyebrow && {eyebrowText}} + {showTitle && renderTitle()} + {showContent &&

{contentText}

} +
+ )} + {showFooter && ( + + + + + )} +
+ ))} +
+ ); + + return wrapInContainer ? {renderCard()} : renderCard(); +}; diff --git a/packages/web-react/scripts/entryPoints.js b/packages/web-react/scripts/entryPoints.js index a66a19e95a..266716c8d5 100644 --- a/packages/web-react/scripts/entryPoints.js +++ b/packages/web-react/scripts/entryPoints.js @@ -14,6 +14,7 @@ const entryPoints = [ { dirs: ['components', 'Alert'] }, { dirs: ['components', 'Breadcrumbs'] }, { dirs: ['components', 'Button'] }, + { dirs: ['components', 'Card'] }, { dirs: ['components', 'Checkbox'] }, { dirs: ['components', 'Collapse'] }, { dirs: ['components', 'Container'] }, diff --git a/packages/web-react/src/components/Card/Card.tsx b/packages/web-react/src/components/Card/Card.tsx new file mode 100644 index 0000000000..58fa30c203 --- /dev/null +++ b/packages/web-react/src/components/Card/Card.tsx @@ -0,0 +1,29 @@ +'use client'; + +import classNames from 'classnames'; +import React, { ElementType } from 'react'; +import { Direction } from '../../constants'; +import { useStyleProps } from '../../hooks'; +import { SpiritCardProps } from '../../types'; +import { useCardStyleProps } from './useCardStyleProps'; + +const defaultProps: Partial = { + elementType: 'article', + direction: Direction.VERTICAL, + isBoxed: false, +}; + +const Card = (props: SpiritCardProps) => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { elementType: ElementTag = 'article', direction, isBoxed, children, ...restProps } = propsWithDefaults; + const { classProps } = useCardStyleProps({ direction, isBoxed }); + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( + + {children} + + ); +}; + +export default Card; diff --git a/packages/web-react/src/components/Card/CardBody.tsx b/packages/web-react/src/components/Card/CardBody.tsx new file mode 100644 index 0000000000..3a2a6421ca --- /dev/null +++ b/packages/web-react/src/components/Card/CardBody.tsx @@ -0,0 +1,21 @@ +'use client'; + +import classNames from 'classnames'; +import React from 'react'; +import { useStyleProps } from '../../hooks'; +import { SpiritCardBodyProps } from '../../types'; +import { useCardStyleProps } from './useCardStyleProps'; + +const CardBody = (props: SpiritCardBodyProps) => { + const { children, ...restProps } = props; + const { classProps } = useCardStyleProps(); + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( +
+ {children} +
+ ); +}; + +export default CardBody; diff --git a/packages/web-react/src/components/Card/CardEyebrow.tsx b/packages/web-react/src/components/Card/CardEyebrow.tsx new file mode 100644 index 0000000000..011ff04d08 --- /dev/null +++ b/packages/web-react/src/components/Card/CardEyebrow.tsx @@ -0,0 +1,21 @@ +'use client'; + +import classNames from 'classnames'; +import React from 'react'; +import { useStyleProps } from '../../hooks'; +import { SpiritCardEyebrowProps } from '../../types'; +import { useCardStyleProps } from './useCardStyleProps'; + +const CardEyebrow = (props: SpiritCardEyebrowProps) => { + const { children, ...restProps } = props; + const { classProps } = useCardStyleProps(); + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( +
+ {children} +
+ ); +}; + +export default CardEyebrow; diff --git a/packages/web-react/src/components/Card/CardFooter.tsx b/packages/web-react/src/components/Card/CardFooter.tsx new file mode 100644 index 0000000000..24447c3e30 --- /dev/null +++ b/packages/web-react/src/components/Card/CardFooter.tsx @@ -0,0 +1,27 @@ +'use client'; + +import classNames from 'classnames'; +import React from 'react'; +import { AlignmentX } from '../../constants'; +import { useStyleProps } from '../../hooks'; +import { SpiritCardFooterProps } from '../../types'; +import { useCardStyleProps } from './useCardStyleProps'; + +const defaultProps: Partial = { + alignmentX: AlignmentX.LEFT, +}; + +const CardFooter = (props: SpiritCardFooterProps) => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { children, alignmentX, ...restProps } = propsWithDefaults; + const { classProps } = useCardStyleProps({ alignmentX }); + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( +
+ {children} +
+ ); +}; + +export default CardFooter; diff --git a/packages/web-react/src/components/Card/CardLink.tsx b/packages/web-react/src/components/Card/CardLink.tsx new file mode 100644 index 0000000000..80c00be89f --- /dev/null +++ b/packages/web-react/src/components/Card/CardLink.tsx @@ -0,0 +1,20 @@ +'use client'; + +import classNames from 'classnames'; +import React from 'react'; +import { useClassNamePrefix } from '../../hooks'; +import { SpiritLinkProps } from '../../types'; +import { Link } from '../Link'; + +const CardLink = (props: SpiritLinkProps) => { + const { children, UNSAFE_className, ...restProps } = props; + const cardClass = useClassNamePrefix('CardLink'); + + return ( + + {children} + + ); +}; + +export default CardLink; diff --git a/packages/web-react/src/components/Card/CardMedia.tsx b/packages/web-react/src/components/Card/CardMedia.tsx new file mode 100644 index 0000000000..e7641838bb --- /dev/null +++ b/packages/web-react/src/components/Card/CardMedia.tsx @@ -0,0 +1,28 @@ +'use client'; + +import classNames from 'classnames'; +import React from 'react'; +import { useStyleProps } from '../../hooks'; +import { SpiritCardMediaProps } from '../../types'; +import { useCardStyleProps } from './useCardStyleProps'; + +const defaultProps: Partial = { + size: 'auto', + isExpanded: false, + hasFilledHeight: false, +}; + +const CardMedia = (props: SpiritCardMediaProps) => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { children, size, isExpanded, hasFilledHeight, ...restProps } = propsWithDefaults; + const { classProps } = useCardStyleProps({ size, isExpanded, hasFilledHeight }); + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( +
+
{children}
+
+ ); +}; + +export default CardMedia; diff --git a/packages/web-react/src/components/Card/CardTitle.tsx b/packages/web-react/src/components/Card/CardTitle.tsx new file mode 100644 index 0000000000..af5b05abb3 --- /dev/null +++ b/packages/web-react/src/components/Card/CardTitle.tsx @@ -0,0 +1,27 @@ +'use client'; + +import classNames from 'classnames'; +import React, { ElementType } from 'react'; +import { useStyleProps } from '../../hooks'; +import { SpiritCardTitleProps } from '../../types'; +import { useCardStyleProps } from './useCardStyleProps'; + +const defaultProps: Partial = { + elementType: 'h4', + isHeading: true, +}; + +const CardTitle = (props: SpiritCardTitleProps) => { + const propsWithDefaults = { ...defaultProps, ...props }; + const { elementType: ElementTag = 'h4', children, isHeading, ...restProps } = propsWithDefaults; + const { classProps } = useCardStyleProps({ isHeading }); + const { styleProps, props: otherProps } = useStyleProps(restProps); + + return ( + + {children} + + ); +}; + +export default CardTitle; diff --git a/packages/web-react/src/components/Card/README.md b/packages/web-react/src/components/Card/README.md new file mode 100644 index 0000000000..d78b30de93 --- /dev/null +++ b/packages/web-react/src/components/Card/README.md @@ -0,0 +1,147 @@ +# Card + +Basic usage: + +```jsx + +``` + +Advanced example: + +```jsx + + … + +``` + +Card composition example: + +```jsx + + {/* */} + + + + + + {/* User content */} + … + {/* End user content */} + + … + +``` + +### API + +| Name | Type | Default | Required | Description | +| ------------- | --------------------------------------------------------------------- | ---------- | -------- | ---------------------------------------------- | +| `direction` | [[Direction dictionary][dictionary-direction], `horizontal-reversed`] | `vertical` | ✕ | Direction of the content inside Card component | +| `elementType` | `ElementType` | `article` | ✕ | Type of element | +| `isBoxed` | `bool` | `false` | ✕ | Whether the Card have border | + +On top of the API options, the components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +## Card Media + +Basic usage: + +```jsx +{/* */} +``` + +Advanced example: + +```jsx + + {/* */} + +``` + +### API + +| Name | Type | Default | Required | Description | +| ---------------------- | -------------------------------------------- | ------- | -------- | ------------------------------------------- | +| `hasFilledHeightClass` | `bool` | `false` | ✕ | Whether the image fill the height of a Card | +| `isExpanded` | `bool` | `false` | ✕ | Whether the media have space around | +| `size` | [[Size dictionary][dictionary-size], `auto`] | `auto` | ✕ | Size of the image media | + +On top of the API options, the components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +## Card Eyebrow + +Basic usage: + +```jsx + +``` + +## Card Title & Card Link + +Basic usage: + +```jsx + +``` + +Advanced example: + +```jsx + + + +``` + +### API + +| Name | Type | Default | Required | Description | +| ------------- | ------------- | ------- | -------- | ------------------------------------------ | +| `elementType` | `ElementType` | `h4` | ✕ | Type of element | +| `isHeading` | `bool` | `true` | ✕ | Whether the title is rendered as a heading | + +On top of the API options, the components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +## Card Body + +Basic usage: + +```jsx + +``` + +## Card Actions + +Basic usage: + +```jsx + +``` + +Advanced example: + +```jsx + +``` + +### API + +| Name | Type | Default | Required | Description | +| ------------ | --------------------------------------------- | ------- | -------- | --------------------------- | +| `alignmentX` | [AlignmentX dictionary][dictionary-alignment] | `left` | ✕ | Alignment of Footer Actions | + +On top of the API options, the components accept [additional attributes][readme-additional-attributes]. +If you need more control over the styling of a component, you can use [style props][readme-style-props] +and [escape hatches][readme-escape-hatches]. + +[dictionary-alignment]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#alignment +[dictionary-direction]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#direction +[dictionary-size]: https://github.com/lmc-eu/spirit-design-system/tree/main/docs/DICTIONARIES.md#size +[readme-additional-attributes]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#additional-attributes +[readme-escape-hatches]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#escape-hatches +[readme-style-props]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/README.md#style-props diff --git a/packages/web-react/src/components/Card/__tests__/Card.test.tsx b/packages/web-react/src/components/Card/__tests__/Card.test.tsx new file mode 100644 index 0000000000..8443c2f0fc --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/Card.test.tsx @@ -0,0 +1,51 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import Card from '../Card'; + +describe('Card', () => { + classNamePrefixProviderTest(Card, 'Card'); + + stylePropsTest(Card); + + restPropsTest(Card, 'article'); + + it('should render card component', () => { + render(); + + expect(screen.getByRole('article')).toBeInTheDocument(); + }); + + it('should render custom element', () => { + render(); + + expect(screen.getByTestId('test')).toContainHTML('section'); + }); + + it('should render boxed card', () => { + render(); + + expect(screen.getByRole('article')).toHaveClass('Card--boxed'); + }); + + it('should render horizontal card', () => { + render(); + + expect(screen.getByRole('article')).toHaveClass('Card--horizontal'); + }); + + it('should render horizontal reversed card', () => { + render(); + + expect(screen.getByRole('article')).toHaveClass('Card--horizontalReversed'); + }); + + it('should render text children', () => { + render(Hello World); + + expect(screen.getByRole('article')).toHaveTextContent('Hello World'); + }); +}); diff --git a/packages/web-react/src/components/Card/__tests__/CardBody.test.tsx b/packages/web-react/src/components/Card/__tests__/CardBody.test.tsx new file mode 100644 index 0000000000..2f75d1abdf --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/CardBody.test.tsx @@ -0,0 +1,27 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import CardBody from '../CardBody'; + +describe('CardBody', () => { + classNamePrefixProviderTest(CardBody, 'CardBody'); + + stylePropsTest(CardBody); + + restPropsTest(CardBody, '.CardBody'); + + it('should render card component', () => { + render(); + + expect(screen.getByTestId('test')).toBeInTheDocument(); + }); + + it('should render text children', () => { + render(Hello World); + + expect(screen.getByTestId('test')).toHaveTextContent('Hello World'); + }); +}); diff --git a/packages/web-react/src/components/Card/__tests__/CardEyebrows.test.tsx b/packages/web-react/src/components/Card/__tests__/CardEyebrows.test.tsx new file mode 100644 index 0000000000..b828e78b36 --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/CardEyebrows.test.tsx @@ -0,0 +1,27 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import CardEyebrow from '../CardEyebrow'; + +describe('CardEyebrow', () => { + classNamePrefixProviderTest(CardEyebrow, 'CardEyebrow'); + + stylePropsTest(CardEyebrow); + + restPropsTest(CardEyebrow, '.CardEyebrow'); + + it('should render card component', () => { + render(); + + expect(screen.getByTestId('test')).toBeInTheDocument(); + }); + + it('should render text children', () => { + render(Hello World); + + expect(screen.getByTestId('test')).toHaveTextContent('Hello World'); + }); +}); diff --git a/packages/web-react/src/components/Card/__tests__/CardFooter.test.tsx b/packages/web-react/src/components/Card/__tests__/CardFooter.test.tsx new file mode 100644 index 0000000000..2e01d1ec0b --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/CardFooter.test.tsx @@ -0,0 +1,33 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import CardFooter from '../CardFooter'; + +describe('CardFooter', () => { + classNamePrefixProviderTest(CardFooter, 'CardFooter'); + + stylePropsTest(CardFooter); + + restPropsTest(CardFooter, '.CardFooter'); + + it('should render card component', () => { + render(); + + expect(screen.getByRole('contentinfo')).toBeInTheDocument(); + }); + + it('should have alignment class', () => { + render(); + + expect(screen.getByRole('contentinfo')).toHaveClass('CardFooter--alignmentXCenter'); + }); + + it('should render text children', () => { + render(Hello World); + + expect(screen.getByRole('contentinfo')).toHaveTextContent('Hello World'); + }); +}); diff --git a/packages/web-react/src/components/Card/__tests__/CardLink.test.tsx b/packages/web-react/src/components/Card/__tests__/CardLink.test.tsx new file mode 100644 index 0000000000..691b32766b --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/CardLink.test.tsx @@ -0,0 +1,27 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import CardLink from '../CardLink'; + +describe('CardLink', () => { + classNamePrefixProviderTest(CardLink, 'CardLink'); + + stylePropsTest(CardLink); + + restPropsTest(CardLink, '.CardLink'); + + it('should render card component', () => { + render(); + + expect(screen.getByRole('link')).toBeInTheDocument(); + }); + + it('should render text children', () => { + render(Hello World); + + expect(screen.getByRole('link')).toHaveTextContent('Hello World'); + }); +}); diff --git a/packages/web-react/src/components/Card/__tests__/CardMedia.test.tsx b/packages/web-react/src/components/Card/__tests__/CardMedia.test.tsx new file mode 100644 index 0000000000..3949ac973f --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/CardMedia.test.tsx @@ -0,0 +1,47 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import CardMedia from '../CardMedia'; + +describe('CardMedia', () => { + classNamePrefixProviderTest(CardMedia, 'CardMedia'); + + stylePropsTest(CardMedia); + + restPropsTest(CardMedia, '.CardMedia'); + + it('should render card media component', () => { + render(); + + expect(screen.getByTestId('test')).toBeInTheDocument(); + }); + + it('should render small size', () => { + render(); + + expect(screen.getByTestId('test')).toHaveClass('CardMedia--small'); + }); + + it('should fill the height', () => { + render(); + + expect(screen.getByTestId('test')).toHaveClass('CardMedia--filledHeight'); + }); + + it('should render image', () => { + render( + + description + , + ); + + const image = screen.getByRole('img'); + + expect(image).toHaveAttribute('src', 'image.png'); + expect(image).toHaveAttribute('alt', 'description'); + expect(image.parentElement).toHaveClass('CardMedia__canvas'); + }); +}); diff --git a/packages/web-react/src/components/Card/__tests__/CardTitle.test.tsx b/packages/web-react/src/components/Card/__tests__/CardTitle.test.tsx new file mode 100644 index 0000000000..196bb22254 --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/CardTitle.test.tsx @@ -0,0 +1,39 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest'; +import { restPropsTest } from '../../../../tests/providerTests/restPropsTest'; +import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest'; +import CardTitle from '../CardTitle'; + +describe('CardTitle', () => { + classNamePrefixProviderTest(CardTitle, 'CardTitle'); + + stylePropsTest(CardTitle); + + restPropsTest(CardTitle, '.CardTitle'); + + it('should render card component', () => { + render(); + + expect(screen.getByTestId('test')).toBeInTheDocument(); + }); + + it('should render custom element', () => { + render(); + + expect(screen.getByTestId('test')).toContainHTML('h1'); + }); + + it('should render as heading', () => { + render(); + + expect(screen.getByTestId('test')).toHaveClass('CardTitle--heading'); + }); + + it('should render text children', () => { + render(Hello World); + + expect(screen.getByTestId('test')).toHaveTextContent('Hello World'); + }); +}); diff --git a/packages/web-react/src/components/Card/__tests__/useCardStyleProps.test.ts b/packages/web-react/src/components/Card/__tests__/useCardStyleProps.test.ts new file mode 100644 index 0000000000..0668854c30 --- /dev/null +++ b/packages/web-react/src/components/Card/__tests__/useCardStyleProps.test.ts @@ -0,0 +1,39 @@ +import { renderHook } from '@testing-library/react'; +import { SpiritCardProps } from '../../../types'; +import { useCardStyleProps } from '../useCardStyleProps'; + +describe('useCardStyleProps', () => { + it('should return defaults', () => { + const props = {}; + const { result } = renderHook(() => useCardStyleProps(props)); + + expect(result.current.classProps).toEqual({ + root: 'Card', + media: 'CardMedia', + body: 'CardBody', + eyebrow: 'CardEyebrow', + title: 'CardTitle', + footer: 'CardFooter', + }); + }); + + it('should return with all classes', () => { + const props = { + alignmentX: 'right', + direction: 'horizontal', + isBoxed: true, + isExpanded: true, + size: 'small', + } as SpiritCardProps; + const { result } = renderHook(() => useCardStyleProps(props)); + + expect(result.current.classProps).toEqual({ + root: 'Card Card--horizontal Card--boxed', + media: 'CardMedia CardMedia--small CardMedia--expanded', + body: 'CardBody', + eyebrow: 'CardEyebrow', + title: 'CardTitle', + footer: 'CardFooter CardFooter--alignmentXRight', + }); + }); +}); diff --git a/packages/web-react/src/components/Card/demo/CardFooterAlignment.tsx b/packages/web-react/src/components/Card/demo/CardFooterAlignment.tsx new file mode 100644 index 0000000000..8b65bd645d --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardFooterAlignment.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Grid } from '../../Grid'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +const CardFooterAlignment = () => { + return ( + + + + + + + Footer alignment + + Left + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+ + + + + + Footer alignment + + Center + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+ + + + + + Footer alignment + + Right + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+
+ ); +}; + +export default CardFooterAlignment; diff --git a/packages/web-react/src/components/Card/demo/CardGeneralOptions.tsx b/packages/web-react/src/components/Card/demo/CardGeneralOptions.tsx new file mode 100644 index 0000000000..05c5588c0a --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardGeneralOptions.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Grid } from '../../Grid'; +import { Link } from '../../Link'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +const CardGeneralOptions = () => ( + + + + + + + Eyebrow title + + Basic card + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ + Read more + + {/* End user content */} +
+
+ + + + + + Eyebrow title + + Boxed card + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+ + + + + + Eyebrow title + + Boxed card, expanded media + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+
+); + +export default CardGeneralOptions; diff --git a/packages/web-react/src/components/Card/demo/CardGrid.tsx b/packages/web-react/src/components/Card/demo/CardGrid.tsx new file mode 100644 index 0000000000..adbb26f65b --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardGrid.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Grid } from '../../Grid'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +const CardGrid = () => ( + + + + + + + Eyebrow title + + Card title + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+ + + + + + Eyebrow title + + Card title + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+ + + + + + Eyebrow title + + Card title + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. + Integer lacinia. Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+
+); + +export default CardGrid; diff --git a/packages/web-react/src/components/Card/demo/CardHorizontalLayout.tsx b/packages/web-react/src/components/Card/demo/CardHorizontalLayout.tsx new file mode 100644 index 0000000000..e6ad3918e4 --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardHorizontalLayout.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Grid } from '../../Grid'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +const CardHorizontalLayout = () => ( + + + + + + + Eyebrow title + + Card title + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+ + + + + + Eyebrow title + + Card title + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+
+); + +export default CardHorizontalLayout; diff --git a/packages/web-react/src/components/Card/demo/CardMediaOptions.tsx b/packages/web-react/src/components/Card/demo/CardMediaOptions.tsx new file mode 100644 index 0000000000..895109c9db --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardMediaOptions.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Grid, GridItem } from '../../Grid'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +// https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors + +export const CardMediaOptions = () => ( + + + + + + + + Media options + + Auto size + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+
+ + + + + + + Media options + + Auto size, expanded + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+
+ + + + + + + Media options + + Medium size + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+
+ + + + + + + Media options + + Medium size, filled height + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+
+ + + + + + + Media options + + Medium size, expanded + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+
+
+); diff --git a/packages/web-react/src/components/Card/demo/CardMediaSizes.tsx b/packages/web-react/src/components/Card/demo/CardMediaSizes.tsx new file mode 100644 index 0000000000..6b36a4c90a --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardMediaSizes.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Grid } from '../../Grid'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +const CardMediaSizes = () => ( + <> + + + + + + + Media size + + Small + +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+
+ + + + +
+ + + + + + Media size + + Medium + +

Lorem ipsum dolor sit amet.

+
+ + + + +
+ + + + + + Media size + + Large + +

Lorem ipsum dolor sit amet.

+
+ + + + +
+ + + + + + Media size + + Auto + +

Lorem ipsum dolor sit amet.

+
+ + + + +
+
+ + + + + + + + Media size + + Small + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+ + + + + + Media size + + Medium + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+ + + + + + Media size + + Large + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+ + + + + + Media size + + Auto + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+
+ +); + +export default CardMediaSizes; diff --git a/packages/web-react/src/components/Card/demo/CardReversedHorizontalLayout.tsx b/packages/web-react/src/components/Card/demo/CardReversedHorizontalLayout.tsx new file mode 100644 index 0000000000..41dd71611b --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardReversedHorizontalLayout.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Button } from '../../Button'; +import { Grid } from '../../Grid'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +const CardReversedHorizontalLayout = () => ( + + + + + + + Eyebrow title + + Card title + + {/* User content */} +

+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia. + Sed vel lectus. +

+ {/* End user content */} +
+ + + + +
+ + + + + + Eyebrow title + + Card title + + {/* User content */} +

Lorem ipsum dolor sit amet.

+ {/* End user content */} +
+ + + + +
+
+); + +export default CardReversedHorizontalLayout; diff --git a/packages/web-react/src/components/Card/demo/CardTitleOptions.tsx b/packages/web-react/src/components/Card/demo/CardTitleOptions.tsx new file mode 100644 index 0000000000..52348ce470 --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardTitleOptions.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Grid } from '../../Grid'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from './constants'; + +const CardTitleOptions = () => ( + + + + + + + + + Body-style primary link + + + + + + + + + + + + Body-style secondary link + + + + + + + + + + + Heading-style primary link + + + + +); + +export default CardTitleOptions; diff --git a/packages/web-react/src/components/Card/demo/constants.ts b/packages/web-react/src/components/Card/demo/constants.ts new file mode 100644 index 0000000000..ed4c150f70 --- /dev/null +++ b/packages/web-react/src/components/Card/demo/constants.ts @@ -0,0 +1,2 @@ +export const MEDIA_IMAGE = + 'https://images.unsplash.com/photo-1506260408121-e353d10b87c7?q=80&w=2728&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'; diff --git a/packages/web-react/src/components/Card/demo/index.tsx b/packages/web-react/src/components/Card/demo/index.tsx new file mode 100644 index 0000000000..929f234034 --- /dev/null +++ b/packages/web-react/src/components/Card/demo/index.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import DocsSection from '../../../../docs/DocsSections'; +import CardFooterAlignment from './CardFooterAlignment'; +import CardGeneralOptions from './CardGeneralOptions'; +import CardGrid from './CardGrid'; +import CardHorizontalLayout from './CardHorizontalLayout'; +import { CardMediaOptions } from './CardMediaOptions'; +import CardMediaSizes from './CardMediaSizes'; +import CardReversedHorizontalLayout from './CardReversedHorizontalLayout'; +import CardTitleOptions from './CardTitleOptions'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + + + + + + + + + + + + + + + + + + + , +); diff --git a/packages/web-react/src/components/Card/index.html b/packages/web-react/src/components/Card/index.html new file mode 100644 index 0000000000..d5795e0a0a --- /dev/null +++ b/packages/web-react/src/components/Card/index.html @@ -0,0 +1 @@ +{{> web-react/demo title="Card" parentPageName="Components" }} diff --git a/packages/web-react/src/components/Card/index.ts b/packages/web-react/src/components/Card/index.ts new file mode 100644 index 0000000000..ad96dadb04 --- /dev/null +++ b/packages/web-react/src/components/Card/index.ts @@ -0,0 +1,10 @@ +'use client'; + +export { default as Card } from './Card'; +export { default as CardBody } from './CardBody'; +export { default as CardEyebrow } from './CardEyebrow'; +export { default as CardFooter } from './CardFooter'; +export { default as CardLink } from './CardLink'; +export { default as CardMedia } from './CardMedia'; +export { default as CardTitle } from './CardTitle'; +export * from './useCardStyleProps'; diff --git a/packages/web-react/src/components/Card/stories/Card.stories.tsx b/packages/web-react/src/components/Card/stories/Card.stories.tsx new file mode 100644 index 0000000000..aa73d44c8e --- /dev/null +++ b/packages/web-react/src/components/Card/stories/Card.stories.tsx @@ -0,0 +1,79 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Direction } from '../../../constants'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from '../demo/constants'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Card', + component: Card, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + direction: { + control: 'select', + description: 'Direction of the card.', + options: [...Object.values(Direction), 'horizontal-reversed'], + table: { + defaultValue: { summary: Direction.VERTICAL }, + }, + }, + elementType: { + control: 'text', + }, + isBoxed: { + control: 'boolean', + description: 'Border around the card.', + table: { + defaultValue: { summary: 'false' }, + }, + }, + }, + args: { + direction: 'vertical', + elementType: 'article', + isBoxed: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const CardComponent: Story = { + name: 'Card', + render: (args) => ( + + + + + + + Card Title + Card Title +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, + nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi + fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc + imperdiet justo nec dolor. +

+
+ + + + +
+
+ ), +}; diff --git a/packages/web-react/src/components/Card/stories/CardBody.stories.tsx b/packages/web-react/src/components/Card/stories/CardBody.stories.tsx new file mode 100644 index 0000000000..30194d1ca6 --- /dev/null +++ b/packages/web-react/src/components/Card/stories/CardBody.stories.tsx @@ -0,0 +1,64 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from '../demo/constants'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Card', + component: CardBody, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + children: { + control: 'text', + description: 'Text to display in the CardBody.', + table: { + defaultValue: { summary: 'CardBody Text' }, + }, + }, + }, + args: { + children: 'CardBody Text', + }, +}; + +export default meta; +type Story = StoryObj; + +export const CardBodyComponent: Story = { + name: 'CardBody', + render: (args) => { + const { children } = args; + + return ( + + + + + + + Card Title + Card Title + {children} + + + + + + + + ); + }, +}; diff --git a/packages/web-react/src/components/Card/stories/CardEyebrow.stories.tsx b/packages/web-react/src/components/Card/stories/CardEyebrow.stories.tsx new file mode 100644 index 0000000000..bf096a4974 --- /dev/null +++ b/packages/web-react/src/components/Card/stories/CardEyebrow.stories.tsx @@ -0,0 +1,69 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from '../demo/constants'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Card', + component: CardEyebrow, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + children: { + control: 'text', + description: 'Text to display in the CardEyebrow.', + table: { + defaultValue: { summary: 'Card Eyebrow text' }, + }, + }, + }, + args: { + children: 'Card Eyebrow', + }, +}; + +export default meta; +type Story = StoryObj; + +export const CardEyebrowComponent: Story = { + name: 'CardEyebrow', + render: (args) => { + const { children } = args; + + return ( + + + + + + + {children} + Card Title +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, + nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi + fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc + imperdiet justo nec dolor. +

+
+ + + + +
+
+ ); + }, +}; diff --git a/packages/web-react/src/components/Card/stories/CardFooter.stories.tsx b/packages/web-react/src/components/Card/stories/CardFooter.stories.tsx new file mode 100644 index 0000000000..43969c3121 --- /dev/null +++ b/packages/web-react/src/components/Card/stories/CardFooter.stories.tsx @@ -0,0 +1,81 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { AlignmentX } from '../../../constants'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from '../demo/constants'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Card', + component: CardFooter, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + alignmentX: { + control: 'select', + description: 'Alignment inside CardFooter component.', + options: [...Object.values(AlignmentX)], + table: { + defaultValue: { summary: AlignmentX.LEFT }, + }, + }, + children: { + control: 'object', + description: 'Content to display in the CardFooter.', + table: { + defaultValue: { summary: '' }, + }, + }, + }, + args: { + alignmentX: 'left', + children: ( + <> + + + + ), + }, +}; + +export default meta; +type Story = StoryObj; + +export const CardFooterComponent: Story = { + name: 'CardFooter', + render: (args) => { + const { children } = args; + + return ( + + + + + + + Card Title + Card Title +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, + nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi + fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc + imperdiet justo nec dolor. +

+
+ {children} +
+
+ ); + }, +}; diff --git a/packages/web-react/src/components/Card/stories/CardLink.stories.tsx b/packages/web-react/src/components/Card/stories/CardLink.stories.tsx new file mode 100644 index 0000000000..5892891532 --- /dev/null +++ b/packages/web-react/src/components/Card/stories/CardLink.stories.tsx @@ -0,0 +1,82 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from '../demo/constants'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Card', + component: CardLink, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + children: { + control: 'text', + description: 'Text to display in the CardLink.', + table: { + defaultValue: { summary: 'Card Eyebrow text' }, + }, + }, + elementType: { + control: 'text', + description: 'Element type for the CardLink component.', + table: { + defaultValue: { summary: 'a' }, + }, + }, + }, + args: { + children: 'Card Link Title', + elementType: 'a', + }, +}; + +export default meta; +type Story = StoryObj; + +export const CardLinkComponent: Story = { + name: 'CardLink', + render: (args) => { + const { children } = args; + + return ( + + + + + + + Card Title + + + {children} + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, + nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi + fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc + imperdiet justo nec dolor. +

+
+ + + + +
+
+ ); + }, +}; diff --git a/packages/web-react/src/components/Card/stories/CardMedia.stories.tsx b/packages/web-react/src/components/Card/stories/CardMedia.stories.tsx new file mode 100644 index 0000000000..be9ed1dfdf --- /dev/null +++ b/packages/web-react/src/components/Card/stories/CardMedia.stories.tsx @@ -0,0 +1,102 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Sizes } from '../../../constants'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from '../demo/constants'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Card', + component: CardMedia, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + hasFilledHeight: { + control: 'boolean', + description: 'Fill the height of the media.', + table: { + defaultValue: { summary: 'false' }, + }, + }, + isExpanded: { + control: 'boolean', + description: 'Expand the media to fill the card. Only works when isBoxed is true.', + table: { + defaultValue: { summary: 'false' }, + }, + }, + size: { + control: 'select', + description: 'Size of the media.', + options: [...Object.values(Sizes), 'auto'], + table: { + defaultValue: { summary: 'auto' }, + }, + }, + }, + args: { + hasFilledHeight: false, + isExpanded: false, + size: 'auto', + }, +}; + +export default meta; +type Story = StoryObj; + +export const CardMediaComponent: Story = { + name: 'CardMedia', + render: (args) => ( + + + + + + + Card Title + Card Title +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, + nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi + fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc + imperdiet justo nec dolor. +

+
+ + + + +
+ + + + + + Card Title + Card Title +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, + nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi + fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc + imperdiet justo nec dolor. +

+
+ + + + +
+
+ ), +}; diff --git a/packages/web-react/src/components/Card/stories/CardTitle.stories.tsx b/packages/web-react/src/components/Card/stories/CardTitle.stories.tsx new file mode 100644 index 0000000000..6efb530527 --- /dev/null +++ b/packages/web-react/src/components/Card/stories/CardTitle.stories.tsx @@ -0,0 +1,85 @@ +import { Markdown } from '@storybook/blocks'; +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { Button } from '../../Button'; +import { Container } from '../../Container'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { MEDIA_IMAGE } from '../demo/constants'; +import ReadMe from '../README.md'; + +const meta: Meta = { + title: 'Components/Card', + component: CardTitle, + parameters: { + docs: { + page: () => {ReadMe}, + }, + }, + argTypes: { + children: { + control: 'text', + description: 'Text to display in the CardTitle.', + table: { + defaultValue: { summary: 'Card Eyebrow text' }, + }, + }, + elementType: { + control: 'text', + description: 'Element type for the CardTitle component.', + table: { + defaultValue: { summary: 'h4' }, + }, + }, + isHeading: { + control: 'boolean', + description: 'If true, the CardTitle will render as a heading.', + table: { + defaultValue: { summary: 'true' }, + }, + }, + }, + args: { + children: 'Card Title', + elementType: 'h4', + isHeading: true, + }, +}; + +export default meta; +type Story = StoryObj; + +export const CardTitleComponent: Story = { + name: 'CardTitle', + render: (args) => { + const { children } = args; + + return ( + + + + + + + Card Title + {children} +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, + nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat. Morbi + fringilla convallis sapien. Sed ac felis. Aliquam erat volutpat. Aliquam euismod. Aenean vel lectus. Nunc + imperdiet justo nec dolor. +

+
+ + + + +
+
+ ); + }, +}; diff --git a/packages/web-react/src/components/Card/useCardStyleProps.ts b/packages/web-react/src/components/Card/useCardStyleProps.ts new file mode 100644 index 0000000000..0ff0fcb5be --- /dev/null +++ b/packages/web-react/src/components/Card/useCardStyleProps.ts @@ -0,0 +1,63 @@ +import classNames from 'classnames'; +import { useAlignmentClass, useClassNamePrefix } from '../../hooks'; +import { CardAlignmentXType, DirectionDictionaryType, HorizontalReversedType, SizesDictionaryType } from '../../types'; +import { kebabCaseToCamelCase } from '../../utils'; + +export interface UseCardStyleProps { + alignmentX?: CardAlignmentXType; + direction?: DirectionDictionaryType | HorizontalReversedType; + hasFilledHeight?: boolean; + isBoxed?: boolean; + isExpanded?: boolean; + isHeading?: boolean; + size?: SizesDictionaryType | 'auto'; +} + +export interface UseCardStylePropsReturn { + /** className props */ + classProps: { + body: string; + eyebrow: string; + footer: string; + media: string; + root: string; + title: string; + }; +} + +export function useCardStyleProps(props?: UseCardStyleProps): UseCardStylePropsReturn { + const { direction, isBoxed, isExpanded, alignmentX, size, isHeading, hasFilledHeight } = props || {}; + const cardClass = useClassNamePrefix('Card'); + const isBoxedClass = `${cardClass}--boxed`; + const directionClass = direction ? `${cardClass}--${kebabCaseToCamelCase(direction)}` : ''; + const mediaSizeClass = size ? `${cardClass}Media--${size}` : ''; + const mediaIsExpandedClass = `${cardClass}Media--expanded`; + const mediaHasFilledHeightClass = `${cardClass}Media--filledHeight`; + + const rootClasses = classNames(cardClass, directionClass, { + [isBoxedClass]: isBoxed, + }); + const mediaClasses = classNames(`${cardClass}Media`, mediaSizeClass, { + [mediaIsExpandedClass]: isExpanded, + [mediaHasFilledHeightClass]: hasFilledHeight, + }); + const bodyClass = `${cardClass}Body`; + const eyebrowClass = `${cardClass}Eyebrow`; + const titleClasses = classNames(`${cardClass}Title`, { + [`${cardClass}Title--heading`]: isHeading, + }); + const footerClasses = classNames(`${cardClass}Footer`, { + [useAlignmentClass(`${cardClass}Footer`, alignmentX!, 'alignmentX')]: alignmentX, + }); + + return { + classProps: { + body: bodyClass, + eyebrow: eyebrowClass, + footer: footerClasses, + media: mediaClasses, + root: rootClasses, + title: titleClasses, + }, + }; +} diff --git a/packages/web-react/src/components/index.ts b/packages/web-react/src/components/index.ts index 407dd57526..21485c5915 100644 --- a/packages/web-react/src/components/index.ts +++ b/packages/web-react/src/components/index.ts @@ -4,6 +4,7 @@ export * from './Accordion'; export * from './Alert'; export * from './Breadcrumbs'; export * from './Button'; +export * from './Card'; export * from './Checkbox'; export * from './Collapse'; export * from './Container'; diff --git a/packages/web-react/src/types/card.ts b/packages/web-react/src/types/card.ts new file mode 100644 index 0000000000..348085fbe0 --- /dev/null +++ b/packages/web-react/src/types/card.ts @@ -0,0 +1,74 @@ +import { ElementType, JSXElementConstructor } from 'react'; +import { + AlignmentXDictionaryType, + ChildrenProps, + DirectionDictionaryType, + SizesDictionaryType, + StyleProps, + TransferProps, +} from './shared'; + +export interface CardElementTypeProps { + /** + * The HTML element or React element used to render the Card, e.g. 'div'. + * + * @default 'article' + */ + elementType?: T | JSXElementConstructor; +} + +// Card types +// Extend direction props to include horizontal-reversed +export type HorizontalReversedType = 'horizontal-reversed'; + +export interface CardProps extends CardElementTypeProps { + direction?: DirectionDictionaryType | HorizontalReversedType; + isBoxed?: boolean; +} + +export interface SpiritCardProps + extends CardProps, + ChildrenProps, + StyleProps, + TransferProps {} + +// CardMedia types +// Extend size props to include auto +export type CardAutoSizeType = 'auto'; + +export interface CardMediaProps { + isExpanded?: boolean; + size?: SizesDictionaryType | CardAutoSizeType; + hasFilledHeight?: boolean; +} + +export interface SpiritCardMediaProps extends CardMediaProps, ChildrenProps, StyleProps, TransferProps {} + +// CardBody types +export interface SpiritCardBodyProps extends ChildrenProps, StyleProps, TransferProps {} + +// CardEyebrow types +export interface SpiritCardEyebrowProps extends ChildrenProps, StyleProps, TransferProps {} + +// CardTitle types +export interface CardHeadingProps { + isHeading?: boolean; +} + +export interface SpiritCardTitleProps + extends CardHeadingProps, + CardElementTypeProps, + ChildrenProps, + StyleProps, + TransferProps {} + +// CardFooter types +export type CardAlignmentXType = + | NonNullable + | { [key: string]: NonNullable }; + +export interface CardFooterProps { + alignmentX?: CardAlignmentXType; +} + +export interface SpiritCardFooterProps extends CardFooterProps, ChildrenProps, StyleProps, TransferProps {} diff --git a/packages/web-react/src/types/index.ts b/packages/web-react/src/types/index.ts index 55c79d6dc9..4d981c9cbb 100644 --- a/packages/web-react/src/types/index.ts +++ b/packages/web-react/src/types/index.ts @@ -3,6 +3,7 @@ export * from './alert'; export * from './avatar'; export * from './breadcrumbs'; export * from './button'; +export * from './card'; export * from './checkbox'; export * from './collapse'; export * from './divider';