From ac63c17cc67d69a056cfe87486ed9cda56a53091 Mon Sep 17 00:00:00 2001 From: Peter Kulko Date: Wed, 4 Dec 2024 16:58:56 +0200 Subject: [PATCH 1/3] feat: replaced bootstrap BaseCard component --- src/Card/BaseCard.jsx | 81 +++++++++++++++++++++++++++++++++++++++++++ src/Card/index.jsx | 2 +- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/Card/BaseCard.jsx diff --git a/src/Card/BaseCard.jsx b/src/Card/BaseCard.jsx new file mode 100644 index 0000000000..e5d42e51b1 --- /dev/null +++ b/src/Card/BaseCard.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import CardBody from './CardBody'; + +const BASE_CARD_CLASSNAME = 'card'; + +const BaseCard = 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} + + ); + }, +); + +const colorVariants = [ + 'primary', + 'secondary', + 'success', + 'danger', + 'warning', + 'info', + 'dark', + 'light', +]; + +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, 'white', 'muted']), + /** 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, +}; + +BaseCard.defaultProps = { + prefix: undefined, + hasBody: false, + as: 'div', + borderColor: undefined, + className: undefined, + children: undefined, + bgColor: undefined, + textColor: undefined, +}; + +export default BaseCard; 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'; From f3beb20b2cde1750de1c15b15160f80b73df9013 Mon Sep 17 00:00:00 2001 From: Peter Kulko Date: Thu, 5 Dec 2024 10:28:27 +0200 Subject: [PATCH 2/3] refactor: added tests --- src/Card/BaseCard.jsx | 4 +- src/Card/README.md | 2 - src/Card/tests/BaseCard.test.jsx | 82 +++++++++++++++++++++++++++++++ www/src/components/PropsTable.tsx | 3 -- 4 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/Card/tests/BaseCard.test.jsx diff --git a/src/Card/BaseCard.jsx b/src/Card/BaseCard.jsx index e5d42e51b1..088cb5aae1 100644 --- a/src/Card/BaseCard.jsx +++ b/src/Card/BaseCard.jsx @@ -14,9 +14,9 @@ const BaseCard = React.forwardRef( bgColor, textColor, borderColor, - hasBody = false, + hasBody, children, - as: Component = 'div', + as: Component, ...props }, ref, 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/tests/BaseCard.test.jsx b/src/Card/tests/BaseCard.test.jsx new file mode 100644 index 0000000000..e555793191 --- /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('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`, From 9965409705e47cc1c57c825cb1520bbe43bc4a49 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 5 Dec 2024 12:56:45 -0500 Subject: [PATCH 3/3] chore: add typescript types for `BaseCard` --- src/Card/{BaseCard.jsx => BaseCard.tsx} | 63 +++++++++++++++---------- src/Card/tests/BaseCard.test.jsx | 2 +- 2 files changed, 38 insertions(+), 27 deletions(-) rename src/Card/{BaseCard.jsx => BaseCard.tsx} (66%) diff --git a/src/Card/BaseCard.jsx b/src/Card/BaseCard.tsx similarity index 66% rename from src/Card/BaseCard.jsx rename to src/Card/BaseCard.tsx index 088cb5aae1..d1c7eccebf 100644 --- a/src/Card/BaseCard.jsx +++ b/src/Card/BaseCard.tsx @@ -2,11 +2,43 @@ 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 BaseCard = React.forwardRef( +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, @@ -14,9 +46,9 @@ const BaseCard = React.forwardRef( bgColor, textColor, borderColor, - hasBody, + hasBody = false, children, - as: Component, + as: Component = 'div', ...props }, ref, @@ -37,24 +69,14 @@ const BaseCard = React.forwardRef( }, ); -const colorVariants = [ - 'primary', - 'secondary', - 'success', - 'danger', - 'warning', - 'info', - 'dark', - 'light', -]; - +/* 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, 'white', 'muted']), + 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. */ @@ -67,15 +89,4 @@ BaseCard.propTypes = { children: PropTypes.node, }; -BaseCard.defaultProps = { - prefix: undefined, - hasBody: false, - as: 'div', - borderColor: undefined, - className: undefined, - children: undefined, - bgColor: undefined, - textColor: undefined, -}; - export default BaseCard; diff --git a/src/Card/tests/BaseCard.test.jsx b/src/Card/tests/BaseCard.test.jsx index e555793191..2558a943af 100644 --- a/src/Card/tests/BaseCard.test.jsx +++ b/src/Card/tests/BaseCard.test.jsx @@ -48,7 +48,7 @@ describe('BaseCard Component', () => { ); const contentElement = screen.getByText('Direct Content'); expect(contentElement).toBeInTheDocument(); - expect(contentElement.closest('div')).not.toHaveClass('card-body'); + expect(contentElement.closest('div')).not.toHaveClass('pgn__card-body'); }); it('supports a custom tag with the `as` prop', () => {