diff --git a/src/Card/BaseCard.tsx b/src/Card/BaseCard.tsx new file mode 100644 index 0000000000..d1c7eccebf --- /dev/null +++ b/src/Card/BaseCard.tsx @@ -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( + ( + { + 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 ( + + {hasBody ? {children} : children} + + ); + }, +); + +/* 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; diff --git a/src/Card/README.md b/src/Card/README.md index 742cc3efea..a7ba05894d 100644 --- a/src/Card/README.md +++ b/src/Card/README.md @@ -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.
See React-Bootstrap for additional documentation. - ## Basic Usage ```jsx live diff --git a/src/Card/index.jsx b/src/Card/index.jsx index 8dd3bc0f5c..07dc4cf06a 100644 --- a/src/Card/index.jsx +++ b/src/Card/index.jsx @@ -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'; diff --git a/src/Card/tests/BaseCard.test.jsx b/src/Card/tests/BaseCard.test.jsx new file mode 100644 index 0000000000..2558a943af --- /dev/null +++ b/src/Card/tests/BaseCard.test.jsx @@ -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(Default Card Content); + const cardElement = screen.getByText('Default Card Content'); + expect(cardElement).toBeInTheDocument(); + expect(cardElement).toHaveClass('card'); + }); + + it('applies the correct background color', () => { + render(Card with Background); + const cardElement = screen.getByText('Card with Background'); + expect(cardElement).toHaveClass('bg-primary'); + }); + + it('applies the correct text color', () => { + render(Card with Text Color); + const cardElement = screen.getByText('Card with Text Color'); + expect(cardElement).toHaveClass('text-muted'); + }); + + it('applies the correct border color', () => { + render(Card with Border Color); + const cardElement = screen.getByText('Card with Border Color'); + expect(cardElement).toHaveClass('border-danger'); + }); + + it('renders children inside CardBody when hasBody is true', () => { + render( + + Content in CardBody + , + ); + 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( + + Direct Content + , + ); + 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( + + Custom Tag + , + ); + const sectionElement = screen.getByText('Custom Tag').closest('section'); + expect(sectionElement).toBeInTheDocument(); + expect(sectionElement).toHaveClass('card'); + }); + + it('applies additional class names', () => { + render(Custom Class); + const cardElement = screen.getByText('Custom Class'); + expect(cardElement).toHaveClass('custom-class'); + }); + + it('uses prefix correctly', () => { + render(Prefixed Card); + const cardElement = screen.getByText('Prefixed Card'); + expect(cardElement).toHaveClass('test-prefix-card'); + }); + + it('renders without children', () => { + render(); + const cardElement = document.querySelector('.card'); + expect(cardElement).toBeInTheDocument(); + }); +}); diff --git a/www/src/components/PropsTable.tsx b/www/src/components/PropsTable.tsx index eff2e99199..bc9a87d640 100644 --- a/www/src/components/PropsTable.tsx +++ b/www/src/components/PropsTable.tsx @@ -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`,