Skip to content

Commit

Permalink
feat: Replaced bootstrap BaseCard component (#3318)
Browse files Browse the repository at this point in the history
* feat: replaced bootstrap BaseCard component

* refactor: added tests

* chore: add typescript types for `BaseCard`

---------

Co-authored-by: Brian Smith <[email protected]>
  • Loading branch information
PKulkoRaccoonGang and brian-smith-tcril authored Dec 11, 2024
1 parent e96baef commit 33f862b
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 6 deletions.
92 changes: 92 additions & 0 deletions src/Card/BaseCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import type { ComponentWithAsProp, BsPropsWithAs } from '../utils/types/bootstrap';

// @ts-ignore
import CardBody from './CardBody';

const BASE_CARD_CLASSNAME = 'card';

const colorVariants = [
'primary',
'secondary',
'success',
'danger',
'warning',
'info',
'dark',
'light',
] as const;

const textVariants = [
'white',
'muted',
] as const;

type ColorVariant = typeof colorVariants[number];
type TextVariant = typeof textVariants[number];
interface Props extends BsPropsWithAs {
prefix?: string;
bgColor?: ColorVariant;
textColor?: ColorVariant | TextVariant;
borderColor?: ColorVariant;
hasBody?: boolean;
className?: string;
children: React.ReactNode;
}
type BaseCardType = ComponentWithAsProp<'div', Props>;

const BaseCard : BaseCardType = React.forwardRef<HTMLDivElement, Props>(
(
{
prefix,
className,
bgColor,
textColor,
borderColor,
hasBody = false,
children,
as: Component = 'div',
...props
},
ref,
) => {
const classes = classNames(
className,
prefix ? `${prefix}-${BASE_CARD_CLASSNAME}` : BASE_CARD_CLASSNAME,
bgColor && `bg-${bgColor}`,
textColor && `text-${textColor}`,
borderColor && `border-${borderColor}`,
);

return (
<Component ref={ref} {...props} className={classes}>
{hasBody ? <CardBody>{children}</CardBody> : children}
</Component>
);
},
);

/* eslint-disable react/require-default-props */
BaseCard.propTypes = {
/** Prefix for component CSS classes. */
prefix: PropTypes.string,
/** Background color of the card. */
bgColor: PropTypes.oneOf(colorVariants),
/** Text color of the card. */
textColor: PropTypes.oneOf([...colorVariants, ...textVariants]),
/** Border color of the card. */
borderColor: PropTypes.oneOf(colorVariants),
/** Determines whether the card should render its children inside a `CardBody` wrapper. */
hasBody: PropTypes.bool,
/** Set a custom element for this component. */
as: PropTypes.elementType,
/** Additional CSS class names to apply to the card element. */
className: PropTypes.string,
/** The content to render inside the card. */
children: PropTypes.node,
};

export default BaseCard;
2 changes: 0 additions & 2 deletions src/Card/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ notes: |

`Card` supports `vertical` and `horizontal` orientation which is controlled by `CardContext`, see examples below.

This component uses a `Card` from react-bootstrap as a base component and extends it with additional subcomponents. <br/> <a href="https://react-bootstrap-v4.netlify.app/components/cards/" target="_blank" rel="noopener noreferrer">See React-Bootstrap for additional documentation.</a>

## Basic Usage

```jsx live
Expand Down
2 changes: 1 addition & 1 deletion src/Card/index.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import BaseCard from 'react-bootstrap/Card';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import BaseCard from './BaseCard';
import CardContext, { CardContextProvider } from './CardContext';
import CardHeader from './CardHeader';
import CardDivider from './CardDivider';
Expand Down
82 changes: 82 additions & 0 deletions src/Card/tests/BaseCard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import BaseCard from '../BaseCard';

describe('BaseCard Component', () => {
it('renders a default card', () => {
render(<BaseCard>Default Card Content</BaseCard>);
const cardElement = screen.getByText('Default Card Content');
expect(cardElement).toBeInTheDocument();
expect(cardElement).toHaveClass('card');
});

it('applies the correct background color', () => {
render(<BaseCard bgColor="primary">Card with Background</BaseCard>);
const cardElement = screen.getByText('Card with Background');
expect(cardElement).toHaveClass('bg-primary');
});

it('applies the correct text color', () => {
render(<BaseCard textColor="muted">Card with Text Color</BaseCard>);
const cardElement = screen.getByText('Card with Text Color');
expect(cardElement).toHaveClass('text-muted');
});

it('applies the correct border color', () => {
render(<BaseCard borderColor="danger">Card with Border Color</BaseCard>);
const cardElement = screen.getByText('Card with Border Color');
expect(cardElement).toHaveClass('border-danger');
});

it('renders children inside CardBody when hasBody is true', () => {
render(
<BaseCard hasBody>
<span>Content in CardBody</span>
</BaseCard>,
);
const cardBodyElement = screen.getByText('Content in CardBody');
expect(cardBodyElement).toBeInTheDocument();
expect(cardBodyElement.closest('div')).toHaveClass('pgn__card-body');
});

it('renders children directly when hasBody is false', () => {
render(
<BaseCard>
<span>Direct Content</span>
</BaseCard>,
);
const contentElement = screen.getByText('Direct Content');
expect(contentElement).toBeInTheDocument();
expect(contentElement.closest('div')).not.toHaveClass('pgn__card-body');
});

it('supports a custom tag with the `as` prop', () => {
render(
<BaseCard as="section">
<span>Custom Tag</span>
</BaseCard>,
);
const sectionElement = screen.getByText('Custom Tag').closest('section');
expect(sectionElement).toBeInTheDocument();
expect(sectionElement).toHaveClass('card');
});

it('applies additional class names', () => {
render(<BaseCard className="custom-class">Custom Class</BaseCard>);
const cardElement = screen.getByText('Custom Class');
expect(cardElement).toHaveClass('custom-class');
});

it('uses prefix correctly', () => {
render(<BaseCard prefix="test-prefix">Prefixed Card</BaseCard>);
const cardElement = screen.getByText('Prefixed Card');
expect(cardElement).toHaveClass('test-prefix-card');
});

it('renders without children', () => {
render(<BaseCard />);
const cardElement = document.querySelector('.card');
expect(cardElement).toBeInTheDocument();
});
});
3 changes: 0 additions & 3 deletions www/src/components/PropsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ const BOOTSTRAP_BASE_URL = 'https://react-bootstrap-v4.netlify.app/components';

const bootstrapLinks = {
Button: `${BOOTSTRAP_BASE_URL}/buttons/#button-props`,
Card: `${BOOTSTRAP_BASE_URL}/cards/#card-props`,
CardBody: `${BOOTSTRAP_BASE_URL}/cards/#card-body-props`,
CardDeck: `${BOOTSTRAP_BASE_URL}/cards/#card-deck-props`,
Dropdown: `${BOOTSTRAP_BASE_URL}/dropdowns/#dropdown-props`,
DropdownToggle: `${BOOTSTRAP_BASE_URL}/dropdowns/#dropdown-toggle-props`,
DropdownItem: `${BOOTSTRAP_BASE_URL}/dropdowns/#dropdown-item-props`,
Expand Down

0 comments on commit 33f862b

Please sign in to comment.