Skip to content

Commit

Permalink
fixup! Feat(web-react): Introduce Card component #1535
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelklibani committed Nov 21, 2024
1 parent df3c3fb commit ed16f18
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 56 deletions.
2 changes: 1 addition & 1 deletion packages/web-react/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { SpiritCardProps } from '../../types';
import { useCardStyleProps } from './useCardStyleProps';

const defaultProps: Partial<SpiritCardProps> = {
elementType: 'article',
direction: Direction.VERTICAL,
elementType: 'article',
isBoxed: false,
};

Expand Down
25 changes: 14 additions & 11 deletions packages/web-react/src/components/Card/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@ Card composition example:
<CardTitle>
<CardLink href="#"></CardLink>
</CardTitle>
{/* User content */}
{/* End user content */}
{/* User content */}…{/* End user content */}
</CardBody>
<CardActions></CardAction>
<CardFooter></CardFooter>
</Card>
```

Expand Down Expand Up @@ -96,7 +94,7 @@ Advanced example:
</CardTitle>
```

### API
### API - Card Title

| Name | Type | Default | Required | Description |
| ------------- | ------------- | ------- | -------- | ------------------------------------------ |
Expand All @@ -107,6 +105,10 @@ On top of the API options, the components accept [additional attributes][readme-
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].

### API - Card Link

`CardLink` utilizes the `Link` component internally and shares its API. For more details, please refer to the [Link component documentation][link-readme].

## Card Body

Basic usage:
Expand All @@ -115,25 +117,25 @@ Basic usage:
<CardBody></CardBody>
```

## Card Actions
## Card Footer

Basic usage:

```jsx
<CardActions></CardActions>
<CardFooter></CardFooter>
```

Advanced example:

```jsx
<CardActions alignmentX="right"></CardActions>
<CardFooter alignmentX="right"></CardFooter>
```

### API

| Name | Type | Default | Required | Description |
| ------------ | --------------------------------------------- | ------- | -------- | --------------------------- |
| `alignmentX` | [AlignmentX dictionary][dictionary-alignment] | `left` || Alignment of Footer Actions |
| Name | Type | Default | Required | Description |
| ------------ | --------------------------------------------- | ------- | -------- | ------------------------- |
| `alignmentX` | [AlignmentX dictionary][dictionary-alignment] | `left` || Alignment of Footer Items |

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]
Expand All @@ -142,6 +144,7 @@ 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
[link-readme]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Link/README.md#api
[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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest';
import { alignmentXPropsTest } from '../../../../tests/providerTests/dictionaryPropsTest';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';
import CardFooter from '../CardFooter';
Expand All @@ -13,18 +14,14 @@ describe('CardFooter', () => {

restPropsTest(CardFooter, '.CardFooter');

alignmentXPropsTest(CardFooter, 'CardFooter');

it('should render card component', () => {
render(<CardFooter />);

expect(screen.getByRole('contentinfo')).toBeInTheDocument();
});

it('should have alignment class', () => {
render(<CardFooter alignmentX="center" />);

expect(screen.getByRole('contentinfo')).toHaveClass('CardFooter--alignmentXCenter');
});

it('should render text children', () => {
render(<CardFooter>Hello World</CardFooter>);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { classNamePrefixProviderTest } from '../../../../tests/providerTests/classNamePrefixProviderTest';
import { sizePropsTest } from '../../../../tests/providerTests/dictionaryPropsTest';
import { restPropsTest } from '../../../../tests/providerTests/restPropsTest';
import { stylePropsTest } from '../../../../tests/providerTests/stylePropsTest';
import CardMedia from '../CardMedia';
Expand All @@ -13,16 +14,18 @@ describe('CardMedia', () => {

restPropsTest(CardMedia, '.CardMedia');

sizePropsTest(CardMedia);

it('should render card media component', () => {
render(<CardMedia data-testId="test" />);

expect(screen.getByTestId('test')).toBeInTheDocument();
});

it('should render small size', () => {
render(<CardMedia size="small" data-testId="test" />);
it('should render auto size', () => {
render(<CardMedia size="auto" data-testId="test" />);

expect(screen.getByTestId('test')).toHaveClass('CardMedia--small');
expect(screen.getByTestId('test')).toHaveClass('CardMedia--auto');
});

it('should fill the height', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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';
Expand All @@ -27,9 +26,7 @@ const CardGeneralOptions = () => (
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean fermentum risus id tortor. Integer lacinia.
Sed vel lectus.
</p>
<Link href="#" elementType="div" underlined="always">
Read more
</Link>
<div className="link-primary link-underlined">Read more</div>
{/* End user content */}
</CardBody>
</Card>
Expand Down
16 changes: 5 additions & 11 deletions packages/web-react/src/components/Card/demo/CardMediaOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { MEDIA_IMAGE } from './constants';

export const CardMediaOptions = () => (
<Grid cols={{ mobile: 1, tablet: 2, desktop: 4 }} UNSAFE_style={{ gridAutoFlow: 'dense' }}>
<GridItem UNSAFE_className="d-grid" UNSAFE_style={{ ['--grid-item-row-end-desktop' as string]: 'span 2' }}>
<GridItem UNSAFE_className="d-grid" rowEnd={{ desktop: 2 }}>
<Card isBoxed>
<CardMedia>
<img src={MEDIA_IMAGE} alt="" />
Expand All @@ -37,7 +37,7 @@ export const CardMediaOptions = () => (
</CardFooter>
</Card>
</GridItem>
<GridItem UNSAFE_className="d-grid" UNSAFE_style={{ ['--grid-item-row-end-desktop' as string]: 'span 2' }}>
<GridItem UNSAFE_className="d-grid" rowEnd={{ desktop: 2 }}>
<Card isBoxed>
<CardMedia isExpanded>
<img src={MEDIA_IMAGE} alt="" />
Expand All @@ -60,7 +60,7 @@ export const CardMediaOptions = () => (
</CardFooter>
</Card>
</GridItem>
<GridItem UNSAFE_className="d-grid" UNSAFE_style={{ ['--grid-item-column-end-tablet' as string]: 'span 2' }}>
<GridItem UNSAFE_className="d-grid" columnEnd={{ tablet: 2 }}>
<Card direction="horizontal" isBoxed>
<CardMedia size="medium">
<img src={MEDIA_IMAGE} alt="" />
Expand All @@ -83,7 +83,7 @@ export const CardMediaOptions = () => (
</CardFooter>
</Card>
</GridItem>
<GridItem UNSAFE_className="d-grid" UNSAFE_style={{ ['--grid-item-column-end-tablet' as string]: 'span 2' }}>
<GridItem UNSAFE_className="d-grid" columnEnd={{ tablet: 2 }}>
<Card direction="horizontal" isBoxed>
<CardMedia size="medium" hasFilledHeight>
<img src={MEDIA_IMAGE} alt="" />
Expand All @@ -106,13 +106,7 @@ export const CardMediaOptions = () => (
</CardFooter>
</Card>
</GridItem>
<GridItem
UNSAFE_className="d-grid"
UNSAFE_style={{
['--grid-item-column-end-tablet' as string]: 'span 2',
['--grid-item-column-end-desktop' as string]: 'span 4',
}}
>
<GridItem UNSAFE_className="d-grid" columnEnd={{ tablet: 2, desktop: 4 }}>
<Card direction="horizontal" isBoxed>
<CardMedia size="medium" isExpanded>
<img src={MEDIA_IMAGE} alt="" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const meta: Meta<typeof CardLink> = {
control: 'text',
description: 'Text to display in the CardLink.',
table: {
defaultValue: { summary: 'Card Eyebrow text' },
defaultValue: { summary: 'Card CardLink text' },
},
},
elementType: {
Expand All @@ -36,10 +36,18 @@ const meta: Meta<typeof CardLink> = {
defaultValue: { summary: 'a' },
},
},
href: {
control: 'text',
description: 'URL to link to.',
table: {
defaultValue: { summary: '' },
},
},
},
args: {
children: 'Card Link Title',
elementType: 'a',
href: '#',
},
};

Expand All @@ -60,9 +68,7 @@ export const CardLinkComponent: Story = {
<CardBody>
<CardEyebrow>Card Title</CardEyebrow>
<CardTitle>
<CardLink href="#" {...args}>
{children}
</CardLink>
<CardLink {...args}>{children}</CardLink>
</CardTitle>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const meta: Meta<typeof CardMedia> = {
argTypes: {
hasFilledHeight: {
control: 'boolean',
description: 'Fill the height of the media.',
description: 'Fill the height of the media. Only works when the card direction is not vertical.',
table: {
defaultValue: { summary: 'false' },
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const meta: Meta<typeof CardTitle> = {
control: 'text',
description: 'Text to display in the CardTitle.',
table: {
defaultValue: { summary: 'Card Eyebrow text' },
defaultValue: { summary: 'Card CardTitle text' },
},
},
elementType: {
Expand Down Expand Up @@ -65,7 +65,7 @@ export const CardTitleComponent: Story = {
<img src={MEDIA_IMAGE} alt="" />
</CardMedia>
<CardBody>
<CardEyebrow>Card Title</CardEyebrow>
<CardEyebrow>Card Eyebrow</CardEyebrow>
<CardTitle {...args}>{children}</CardTitle>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida,
Expand Down
29 changes: 16 additions & 13 deletions packages/web-react/src/components/Card/useCardStyleProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,29 @@ export interface UseCardStylePropsReturn {
export function useCardStyleProps(props?: UseCardStyleProps): UseCardStylePropsReturn {
const { direction, isBoxed, isExpanded, alignmentX, size, isHeading, hasFilledHeight } = props || {};
const cardClass = useClassNamePrefix('Card');
const isBoxedClass = `${cardClass}--boxed`;
const bodyClass = `${cardClass}Body`;
const directionClass = direction ? `${cardClass}--${kebabCaseToCamelCase(direction)}` : '';
const mediaSizeClass = size ? `${cardClass}Media--${size}` : '';
const mediaIsExpandedClass = `${cardClass}Media--expanded`;
const mediaHasFilledHeightClass = `${cardClass}Media--filledHeight`;
const eyebrowClass = `${cardClass}Eyebrow`;
const footerClass = `${cardClass}Footer`;
const isBoxedClass = `${cardClass}--boxed`;
const mediaClass = `${cardClass}Media`;
const mediaHasFilledHeightClass = `${mediaClass}--filledHeight`;
const mediaIsExpandedClass = `${mediaClass}--expanded`;
const mediaSizeClass = size ? `${mediaClass}--${size}` : '';
const titleClass = `${cardClass}Title`;

const rootClasses = classNames(cardClass, directionClass, {
[isBoxedClass]: isBoxed,
const footerClasses = classNames(footerClass, {
[useAlignmentClass(footerClass, alignmentX!, 'alignmentX')]: alignmentX,
});
const mediaClasses = classNames(`${cardClass}Media`, mediaSizeClass, {
const mediaClasses = classNames(mediaClass, mediaSizeClass, {
[mediaIsExpandedClass]: isExpanded,
[mediaHasFilledHeightClass]: hasFilledHeight,
});
const bodyClass = `${cardClass}Body`;
const eyebrowClass = `${cardClass}Eyebrow`;
const titleClasses = classNames(`${cardClass}Title`, {
[`${cardClass}Title--heading`]: isHeading,
const rootClasses = classNames(cardClass, directionClass, {
[isBoxedClass]: isBoxed,
});
const footerClasses = classNames(`${cardClass}Footer`, {
[useAlignmentClass(`${cardClass}Footer`, alignmentX!, 'alignmentX')]: alignmentX,
const titleClasses = classNames(titleClass, {
[`${titleClass}--heading`]: isHeading,
});

return {
Expand Down
54 changes: 54 additions & 0 deletions packages/web-react/tests/providerTests/dictionaryPropsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import {
TextColors,
ValidationStatesDictionaryType,
ValidationStates,
AlignmentX,
AlignmentXExtended,
AlignmentXDictionaryType,
AlignmentXExtendedDictionaryType,
AlignmentYDictionaryType,
AlignmentYExtendedDictionaryType,
} from '../../src';
import getElement from '../testUtils/getElement';

Expand Down Expand Up @@ -115,3 +121,51 @@ export const validationStatePropsTest = (Component: ComponentType<any>, prefix:
});
});
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const alignmentXPropsTest = (Component: ComponentType<any>, prefix?: string, testId?: string) => {
it.each([Object.values(AlignmentX)])('should render alignmentX %s', async (alignment) => {
const dom = render(<Component alignmentX={alignment as AlignmentXDictionaryType<string>} />);

await waitFor(() => {
const element = getElement(dom, testId);
expect(element).toHaveClass(`${prefix}--alignmentX${alignment.charAt(0).toUpperCase() + alignment.slice(1)}`);
});
});
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const alignmentXExtendedPropsTest = (Component: ComponentType<any>, prefix?: string, testId?: string) => {
it.each([Object.values(AlignmentXExtended)])('should render extended alignmentX %s', async (alignment) => {
const dom = render(<Component alignmentX={alignment as AlignmentXExtendedDictionaryType<string>} />);

await waitFor(() => {
const element = getElement(dom, testId);
expect(element).toHaveClass(`${prefix}--alignmentX${alignment.charAt(0).toUpperCase() + alignment.slice(1)}`);
});
});
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const alignmentYPropsTest = (Component: ComponentType<any>, prefix?: string, testId?: string) => {
it.each([Object.values(AlignmentX)])('should render alignmentY %s', async (alignment) => {
const dom = render(<Component alignmentY={alignment as AlignmentYDictionaryType<string>} />);

await waitFor(() => {
const element = getElement(dom, testId);
expect(element).toHaveClass(`${prefix}--alignmentY${alignment.charAt(0).toUpperCase() + alignment.slice(1)}`);
});
});
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const alignmentYExtendedPropsTest = (Component: ComponentType<any>, prefix?: string, testId?: string) => {
it.each([Object.values(AlignmentXExtended)])('should render extended alignmentY %s', async (alignment) => {
const dom = render(<Component alignmentY={alignment as AlignmentYExtendedDictionaryType<string>} />);

await waitFor(() => {
const element = getElement(dom, testId);
expect(element).toHaveClass(`${prefix}--alignmentY${alignment.charAt(0).toUpperCase() + alignment.slice(1)}`);
});
});
};

0 comments on commit ed16f18

Please sign in to comment.