diff --git a/docs/DICTIONARIES.md b/docs/DICTIONARIES.md index 83820fdc2e..9f1c60f22a 100644 --- a/docs/DICTIONARIES.md +++ b/docs/DICTIONARIES.md @@ -35,6 +35,13 @@ This project uses `dictionaries` to unify props between different components. | Emotion Color | `success`, `informative`, `warning`, `danger` | EmotionColor | | Text Color | `primary`, `secondary`, `primary-inverted`, `secondary-inverted` | TextColor | +### Direction + +| Dictionary | Values | Code name | +| ------------- | ------------------------ | ------------- | +| Direction | `horizontal`, `vertical` | Direction | +| DirectionAxis | `x`, `y` | DirectionAxis | + ### Emphasis | Dictionary | Values | Code name | diff --git a/packages/web-react/src/components/Card/README.md b/packages/web-react/src/components/Card/README.md index fbff0fac4a..dc1b1cb4a2 100644 --- a/packages/web-react/src/components/Card/README.md +++ b/packages/web-react/src/components/Card/README.md @@ -69,6 +69,16 @@ Card can be displayed in a vertical, horizontal, or reversed horizontal layout. πŸ‘‰ Keep in mind that, no matter the layout, the Card subcomponents must be arranged in the order [specified above](#card-1). +### Responsive Card Layout + +Pass an object to props to set different values for different breakpoints. The values will +be applied from mobile to desktop and if not set for a breakpoint, the value from the +previous breakpoint will be used. + +```jsx +{/* … */} +``` + ### Boxed Cards Card can be displayed with a border and a box shadow on hover. @@ -79,11 +89,11 @@ Card can be displayed with a border and a box shadow on hover. ### API -| Name | Type | Default | Required | Description | -| ------------- | --------------------------------------------------------------------- | ---------- | -------- | ---------------------------------------------- | -| `direction` | [[Direction dictionary][dictionary-direction], `horizontal-reversed`] | `vertical` | βœ• | Direction of the content inside Card component | -| `elementType` | `ElementType` | `article` | βœ• | Type of element | -| `isBoxed` | `bool` | `false` | βœ• | Whether the Card have border | +| Name | Type | Default | Required | Description | +| ------------- | --------------------------------------------------------------------------------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `direction` | [[Direction dictionary][dictionary-direction], `horizontal-reversed` \| `object`] | `vertical` | βœ• | Direction of the content inside Card component, use object to set responsive values, e.g. `{ mobile: 'horizontal', tablet: 'vertical', desktop: 'horizontal-reversed' }` | +| `elementType` | `ElementType` | `article` | βœ• | Type of element | +| `isBoxed` | `bool` | `false` | βœ• | Whether the Card have border | 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] @@ -451,6 +461,7 @@ When you put it all together: ℹ️ A big shout-out to [OndΕ™ej Pohl][ondrej-pohl] for sharing many of these best practices! [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/blob/main/docs/DICTIONARIES.md#size [grid]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web-react/src/components/Grid/README.md [heydon-pickering-card]: https://inclusive-components.design/cards/ diff --git a/packages/web-react/src/components/Card/demo/CardResponsiveCard.tsx b/packages/web-react/src/components/Card/demo/CardResponsiveCard.tsx new file mode 100644 index 0000000000..3251347b07 --- /dev/null +++ b/packages/web-react/src/components/Card/demo/CardResponsiveCard.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { ButtonLink } from '../../Button'; +import { Grid } from '../../Grid'; +import { PartnerLogo } from '../../PartnerLogo'; +import Card from '../Card'; +import CardBody from '../CardBody'; +import CardEyebrow from '../CardEyebrow'; +import CardFooter from '../CardFooter'; +import CardLink from '../CardLink'; +import CardLogo from '../CardLogo'; +import CardMedia from '../CardMedia'; +import CardTitle from '../CardTitle'; +import { LOGO, MEDIA_IMAGE } from './constants'; + +const CardResponsiveCard = () => { + return ( + + + {MEDIA_IMAGE} + + + {LOGO} + + + + Responsive card layout + + Vertical β†’ horizontal β†’ reversed horizontal + + {/* User content */} +

Lorem ipsum dolor sit amet, consectetuer adipiscing elit.

+ {/* End user content */} +
+ + + Primary + + + Secondary + + +
+ + + {MEDIA_IMAGE} + + + + {LOGO} + + + + Responsive card layout + + Vertical β†’ horizontal β†’ reversed horizontal, expanded media + + {/* User content */} +

Lorem ipsum dolor sit amet, consectetuer adipiscing elit.

+ {/* End user content */} +
+ + + Primary + + + Secondary + + +
+
+ ); +}; + +export default CardResponsiveCard; diff --git a/packages/web-react/src/components/Card/demo/index.tsx b/packages/web-react/src/components/Card/demo/index.tsx index d6bfb2867a..f7587d7c9a 100644 --- a/packages/web-react/src/components/Card/demo/index.tsx +++ b/packages/web-react/src/components/Card/demo/index.tsx @@ -16,6 +16,7 @@ import CardHorizontalLayout from './CardHorizontalLayout'; import CardLogoDemo from './CardLogo'; import { CardMediaOptions } from './CardMediaOptions'; import CardMediaSizes from './CardMediaSizes'; +import CardResponsiveCard from './CardResponsiveCard'; import CardReversedHorizontalLayout from './CardReversedHorizontalLayout'; import CardText from './CardText'; import CardTitleOptions from './CardTitleOptions'; @@ -38,6 +39,9 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + diff --git a/packages/web-react/src/components/Card/useCardStyleProps.ts b/packages/web-react/src/components/Card/useCardStyleProps.ts index 953e9a1f1d..1c2923ff2d 100644 --- a/packages/web-react/src/components/Card/useCardStyleProps.ts +++ b/packages/web-react/src/components/Card/useCardStyleProps.ts @@ -1,11 +1,11 @@ import classNames from 'classnames'; import { useAlignmentClass, useClassNamePrefix } from '../../hooks'; -import { CardAlignmentXType, CardDirectionDictionaryType, CardSizesDictionaryType } from '../../types'; -import { kebabCaseToCamelCase } from '../../utils'; +import { CardAlignmentXType, CardDirectionType, CardSizesDictionaryType } from '../../types'; +import { generateStylePropsClassNames, stringOrObjectKebabCaseToCamelCase } from '../../utils'; export interface UseCardStyleProps { artworkAlignmentX?: CardAlignmentXType; - direction?: CardDirectionDictionaryType; + direction?: CardDirectionType; footerAlignmentX?: CardAlignmentXType; hasFilledHeight?: boolean; isBoxed?: boolean; @@ -54,7 +54,9 @@ export function useCardStyleProps(props?: UseCardStyleProps): UseCardStylePropsR const titleClass = `${cardClass}Title`; const bodyIsSelectableClass = `${bodyClass}--selectable`; - const directionClass = direction ? `${cardClass}--${kebabCaseToCamelCase(direction)}` : ''; + + const directionClass = generateStylePropsClassNames(cardClass, stringOrObjectKebabCaseToCamelCase(direction!)); + const isBoxedClass = `${cardClass}--boxed`; const mediaCanvasClass = `${mediaClass}__canvas`; const mediaHasFilledHeightClass = `${mediaClass}--filledHeight`; @@ -75,7 +77,8 @@ export function useCardStyleProps(props?: UseCardStyleProps): UseCardStylePropsR [mediaIsExpandedClass]: isExpanded, [mediaHasFilledHeightClass]: hasFilledHeight, }); - const rootClasses = classNames(cardClass, directionClass, { + const rootClasses = classNames(cardClass, { + [directionClass]: direction, [isBoxedClass]: isBoxed, }); const titleClasses = classNames(titleClass, { diff --git a/packages/web-react/src/types/card.ts b/packages/web-react/src/types/card.ts index 75aa5fed0b..5c200bef04 100644 --- a/packages/web-react/src/types/card.ts +++ b/packages/web-react/src/types/card.ts @@ -40,9 +40,12 @@ export interface CardElementTypeProps { // Card types // Extend direction props to include horizontal-reversed export type HorizontalReversedType = 'horizontal-reversed'; +export type CardDirectionType = + | NonNullable + | { [key: string]: NonNullable }; export interface CardProps extends CardElementTypeProps { - direction?: CardDirectionDictionaryType; + direction?: CardDirectionType; isBoxed?: boolean; } diff --git a/packages/web-react/src/utils/__tests__/string.test.ts b/packages/web-react/src/utils/__tests__/string.test.ts index a63af20cd5..438dc5a76a 100644 --- a/packages/web-react/src/utils/__tests__/string.test.ts +++ b/packages/web-react/src/utils/__tests__/string.test.ts @@ -1,4 +1,4 @@ -import { kebabCaseToCamelCase } from '../string'; +import { kebabCaseToCamelCase, kebabCaseToCamelCaseValues, stringOrObjectKebabCaseToCamelCase } from '../string'; describe('string', () => { describe('#kebabCaseToCamelCase', () => { @@ -11,7 +11,41 @@ describe('string', () => { ['kebab-case-test', 'kebabCaseTest'], ])('should convert kebab-case string "%s" to camelCase string "%s"', (input, expected) => { const result = kebabCaseToCamelCase(input); - expect(result).toBe(expected); + expect(result).toEqual(expected); + }); + }); + + describe('#kebabCaseToCamelCaseValues', () => { + it.each([ + [{ test: 'foo-bar' }, { test: 'fooBar' }], + [{ test: 'test-case' }, { test: 'testCase' }], + [{ test: 'some-words-here' }, { test: 'someWordsHere' }], + [{ test: 'single' }, { test: 'single' }], + [{ test: '' }, { test: '' }], + [{ test: 'kebab-case-test' }, { test: 'kebabCaseTest' }], + ])('should convert kebab-case object "%s" to camelCase object "%s"', (input, expected) => { + const result = kebabCaseToCamelCaseValues(input); + expect(result).toEqual(expected); + }); + }); + + describe('#stringOrObjectKebabCaseToCamelCase', () => { + it.each([ + ['foo-bar', 'fooBar'], + ['test-case', 'testCase'], + ['some-words-here', 'someWordsHere'], + ['single', 'single'], + ['', ''], + ['kebab-case-test', 'kebabCaseTest'], + [{ test: 'foo-bar' }, { test: 'fooBar' }], + [{ test: 'test-case' }, { test: 'testCase' }], + [{ test: 'some-words-here' }, { test: 'someWordsHere' }], + [{ test: 'single' }, { test: 'single' }], + [{ test: '' }, { test: '' }], + [{ test: 'kebab-case-test' }, { test: 'kebabCaseTest' }], + ])('should convert kebab-case object "%s" to camelCase object "%s"', (input, expected) => { + const result = stringOrObjectKebabCaseToCamelCase(input); + expect(result).toEqual(expected); }); }); }); diff --git a/packages/web-react/src/utils/string.ts b/packages/web-react/src/utils/string.ts index 2ff6c6f02e..69e61586fa 100644 --- a/packages/web-react/src/utils/string.ts +++ b/packages/web-react/src/utils/string.ts @@ -1,6 +1,45 @@ /** - * Converts a kebab-case string to camelCase + * Converts a kebab-case string to camelCase. * - * @param str + * @param {string} str - The kebab-case string to be converted. + * @returns {string} The camelCase version of the input string. */ export const kebabCaseToCamelCase = (str: string): string => str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); + +/** + * Converts an object with kebab-case string values to camelCase. + * + * @param {Record} input - The input to be converted. + * @returns {Record} The converted input. + */ +export const kebabCaseToCamelCaseValues = (input: Record): Record => { + if (typeof input === 'object' && input !== null) { + const result: Record = {}; + for (const [key, value] of Object.entries(input)) { + result[key] = typeof value === 'string' ? kebabCaseToCamelCase(value) : value; + } + + return result; + } + + return input; +}; + +/** + * Converts a kebab-case string or an object with kebab-case values to camelCase. + * + * @param {string | Record} input - The input to be converted. + * @returns {string | Record} The converted input. + */ +export const stringOrObjectKebabCaseToCamelCase = ( + input: string | Record, +): string | Record => { + if (typeof input === 'string') { + return kebabCaseToCamelCase(input); + } + if (typeof input === 'object' && input !== null) { + return kebabCaseToCamelCaseValues(input); + } + + return input; +};