Skip to content

Commit

Permalink
feat: create card list item (#1082)
Browse files Browse the repository at this point in the history
* feat: create card list item component

* feat: create disabled variant

* feat: create small variantion

* test: generate card list unit tests

* docs: update css stories

* docs: update css stories

* style: lint sass file

* fix: adjust full width style

* fix: adjust backgrounds

* fix: remove icon background on small size

* fix: remove padding on small size

* ci: trigger codecov
  • Loading branch information
Luan Peil authored Sep 27, 2023
1 parent 4b0a6bf commit 21f665b
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/ocean-core/src/components/_all.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
@import 'drawer';
@import 'snackbar';
@import 'shortcut';
@import 'card-list-item';
118 changes: 118 additions & 0 deletions packages/ocean-core/src/components/_card-list-item.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
.ods-card-list-item {
align-items: center;
border: $border-width-hairline solid $color-interface-light-down;
border-radius: $border-radius-md;
box-sizing: border-box;
cursor: pointer;
display: flex;
flex-direction: row;
gap: $spacing-inline-xs;
padding: $spacing-stack-xs;
transition: 200ms;
width: 320px;

&__leading-icon {
background-color: $color-interface-light-up;
border-radius: 50%;
box-sizing: content-box;
color: $color-brand-primary-down;
height: $font-size-md;
padding: $spacing-stack-xxs;
transition: 200ms;
width: $font-size-md;

svg {
height: $font-size-md;
width: $font-size-md;
}
}

&__content {
display: flex;
flex-direction: column;
width: 100%;

&__title {
color: $color-interface-dark-pure;
font-family: $font-family-base;
font-size: $font-size-xs;
font-weight: $font-weight-regular;
line-height: $line-height-comfy;
}

&__description {
color: $color-interface-dark-down;
font-family: $font-family-base;
font-size: $font-size-xxs;
font-weight: $font-weight-regular;
line-height: $line-height-comfy;
}

&__caption {
color: $color-interface-dark-down;
font-family: $font-family-base;
font-size: $font-size-xxxs;
font-weight: $font-weight-regular;
line-height: $line-height-comfy;
margin-top: $spacing-inset-xxs;
}
}

&__action {
background-color: transparent;
color: $color-interface-dark-up;
height: $font-size-sm;
width: $font-size-sm;

&:hover {
background-color: transparent;
}

svg {
height: $font-size-sm;
width: $font-size-sm;
}
}

&:hover:not(.ods-card-list-item--disabled) {
background-color: $color-interface-light-up;

&:not(.ods-card-list-item--size-small) .ods-card-list-item__leading-icon {
background-color: rgba(184, 195, 255, 0.16);
}
}

&--disabled {
cursor: not-allowed;

.ods-card-list-item__content__title,
.ods-card-list-item__content__description,
.ods-card-list-item__content__caption {
color: $color-interface-dark-up;
}

.ods-card-list-item__leading-icon,
.ods-card-list-item__action {
color: $color-interface-light-deep;
}
}

&--size-small {
.ods-card-list-item__content__title {
font-size: $font-size-xxs;
}

.ods-card-list-item__content__description {
font-size: $font-size-xxxs;
}

.ods-card-list-item__leading-icon {
background-color: transparent;
padding: 0;
}
}

&--full-width {
width: 100%;
}
}
59 changes: 59 additions & 0 deletions packages/ocean-react/src/CardListItem/CardListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import classNames from 'classnames';

interface CardListItemProps {
title: string;
description?: string;
caption?: string;
actionIcon?: React.ReactNode;
leadingIcon?: React.ReactNode;
size?: 'small' | 'medium';
disabled?: boolean;
fullWidth?: boolean;
onClick?: () => void;
}

const CardListItem = ({
title,
description,
caption,
leadingIcon,
actionIcon,
size = 'medium',
disabled = false,
fullWidth = false,
onClick,
}: CardListItemProps): JSX.Element => (
<div
data-testid="card-list-item"
className={classNames(
'ods-card-list-item',
`ods-card-list-item--size-${size}`,
{ 'ods-card-list-item--disabled': disabled },
{ 'ods-card-list-item--full-width': fullWidth }
)}
onClick={() => {
if (!disabled && onClick) onClick();
}}
>
{leadingIcon && (
<div className="ods-card-list-item__leading-icon">{leadingIcon}</div>
)}
<div className="ods-card-list-item__content">
<div className="ods-card-list-item__content__title">{title}</div>
{description && (
<div className="ods-card-list-item__content__description">
{description}
</div>
)}
{caption && size === 'medium' && (
<div className="ods-card-list-item__content__caption">{caption}</div>
)}
</div>
{actionIcon && (
<div className="ods-card-list-item__action">{actionIcon}</div>
)}
</div>
);

export default CardListItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import CardListItem from '../CardListItem';

describe('CardListItem', () => {
test('renders the title', () => {
render(<CardListItem title="Test Title" />);
expect(screen.getByText('Test Title')).toBeInTheDocument();
});

test('renders the description', () => {
render(<CardListItem title="Test Title" description="Test Description" />);
expect(screen.getByText('Test Description')).toBeInTheDocument();
});

test('renders the caption when size is medium', () => {
render(<CardListItem title="Test Title" caption="Test Caption" />);
expect(screen.getByText('Test Caption')).toBeInTheDocument();
});

test('does not render the caption when size is small', () => {
render(
<CardListItem title="Test Title" caption="Test Caption" size="small" />
);
expect(screen.queryByText('Test Caption')).not.toBeInTheDocument();
});

test('calls onClick when clicked and not disabled', () => {
const onClick = jest.fn();
render(<CardListItem title="Test Title" onClick={onClick} />);
fireEvent.click(screen.getByTestId('card-list-item'));

expect(onClick).toHaveBeenCalled();
});

test('does not call onClick when clicked and disabled', () => {
const onClick = jest.fn();
render(<CardListItem title="Test Title" onClick={onClick} disabled />);
fireEvent.click(screen.getByTestId('card-list-item'));
expect(onClick).not.toHaveBeenCalled();
});

test('renders the leading icon', () => {
render(
<CardListItem title="Test Title" leadingIcon={<div>Leading Icon</div>} />
);
expect(screen.getByText('Leading Icon')).toBeInTheDocument();
});

test('renders the action icon', () => {
render(
<CardListItem title="Test Title" actionIcon={<div>Action Icon</div>} />
);
expect(screen.getByText('Action Icon')).toBeInTheDocument();
});

test('renders the small size', () => {
render(<CardListItem title="Test Title" size="small" />);
expect(screen.getByTestId('card-list-item')).toHaveClass(
'ods-card-list-item--size-small'
);
});

test('renders the disabled state', () => {
render(<CardListItem title="Test Title" disabled />);
expect(screen.getByTestId('card-list-item')).toHaveClass(
'ods-card-list-item--disabled'
);
});

test('renders the full width variation', () => {
render(<CardListItem title="Test Title" fullWidth />);
expect(screen.getByTestId('card-list-item')).toHaveClass(
'ods-card-list-item--full-width'
);
});
});
1 change: 1 addition & 0 deletions packages/ocean-react/src/CardListItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CardListItem';
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { PlaceholderOutline } from '@useblu/ocean-icons-react';
import CardListItem from '../CardListItem';

<Meta title="Components/CardListItem" component={CardListItem} />

# CrossSellCard APIs

The API documentation of the CardListItem React component. Learn more about the props and the CSS customization points.

## Import

```javascript
import { CardListItem } from '@useblu/ocean-react';
```

## Usage

<Canvas withSource="open" withToolbar>
<Story name="usage">
<CardListItem
title="Title"
description="Description"
caption="Caption"
leadingIcon={<PlaceholderOutline />}
actionIcon={<PlaceholderOutline />}
/>
</Story>
</Canvas>

## CSS

| Global class | Description |
| --------------------------------------------- | ------------------------------------------------------------------ |
| .ods-card-list-item | Styles applied to the root element. |
| .ods-card-list-item--size-small | Styles applied to the root element when size is set to small. |
| .ods-card-list-item--size-medium | Styles applied to the root element when size is set to medium. |
| .ods-card-list-item--disabled | Styles applied to the root element when the component is disabled. |
| .ods-card-list-item--full-width | Styles applied to the root element when is set to full width. |
| .ods-card-list-item\_\_leading-icon | Styles applied to the leading icon. |
| .ods-card-list-item\_\_content | Styles applied to the content element. |
| .ods-card-list-item\_\_content\_\_title | Styles applied to the content title. |
| .ods-card-list-item\_\_content\_\_description | Styles applied to the content description. |
| .ods-card-list-item\_\_content\_\_caption | Styles applied to the content caption. |
| .ods-card-list-item\_\_action-icon | Styles applied to the action icon. |

If that's not sufficient, you can check the [implementation of the component](https://github.com/ocean-ds/ocean-web/blob/master/packages/ocean-react/src/CardListItem/CardListItem.tsx) for more detail.

## Playground

<Canvas>
<Story
name="playground"
args={{
title: 'Title',
description: 'Description',
caption: 'Caption',
leadingIcon: <PlaceholderOutline />,
actionIcon: <PlaceholderOutline />,
disabled: false,
size: 'medium',
onClick: () => null,
fullWidth: false,
}}
>
{(props) => <CardListItem {...props} />}
</Story>
</Canvas>
3 changes: 3 additions & 0 deletions packages/ocean-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,6 @@ export * from './Snackbar';

export { default as Shortcut } from './Shortcut';
export * from './Shortcut';

export { default as CardListItem } from './CardListItem';
export * from './CardListItem';

0 comments on commit 21f665b

Please sign in to comment.