` parent container when disabled', () => {
+ render(
+
+ Content
+
+ );
+
+ const card = screen.getByTestId('hefe');
+ expect(card.tagName).toBe('DIV');
+});
+
+it('should call `onClick` when "Enter" key is pressed', () => {
+ const handleClick = jest.fn();
+ render(
Accessible button);
+
+ const card = screen.getByText('Accessible button');
+ fireEvent.keyDown(card, { key: 'Enter', code: 'Enter' });
+
+ expect(handleClick).toHaveBeenCalledTimes(1);
+});
diff --git a/packages/components/card/src/card.story.js b/packages/components/card/src/card.story.js
index 271435b0c5..ebb57d0180 100644
--- a/packages/components/card/src/card.story.js
+++ b/packages/components/card/src/card.story.js
@@ -1,41 +1,60 @@
/* eslint-disable react/prop-types */
import { storiesOf } from '@storybook/react';
-import { withKnobs, text } from '@storybook/addon-knobs/react';
+import { withKnobs, text, boolean } from '@storybook/addon-knobs/react';
import Spacings from '@commercetools-uikit/spacings';
import Text from '@commercetools-uikit/text';
import Section from '../../../../docs/.storybook/decorators/section';
import Readme from '../README.md';
import Card from './card';
+import { BrowserRouter as Router } from 'react-router-dom';
const themes = ['light', 'dark'];
const insetScaleValues = ['none', 's', 'm', 'l', 'xl'];
const content =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Arcu dictum varius duis at consectetur lorem donec.';
-const CardStoryTemplate = (props) => (
-
- {themes.map((theme) => (
-
-
- theme: {theme}
-
-
- {insetScaleValues.map((scale) => (
-
-
- insetScale: {scale}
-
-
- {text('content', content)}
-
-
- ))}
-
-
- ))}
-
-);
+const CardStoryTemplate = (props) => {
+ const onClickEnabled = boolean('onClick enabled', false);
+ const isDisabled = boolean('isDisabled', false);
+ const to = text('to', '');
+
+ const handleClick = onClickEnabled ? () => alert('Card clicked') : undefined;
+
+ return (
+
+
+ {themes.map((theme) => (
+
+
+ theme: {theme}
+
+
+ {insetScaleValues.map((scale) => (
+
+
+ insetScale: {scale}
+
+
+ {text('content', content)}
+
+
+ ))}
+
+
+ ))}
+
+
+ );
+};
storiesOf('Components|Cards', module)
.addDecorator(withKnobs)
diff --git a/packages/components/card/src/card.tsx b/packages/components/card/src/card.tsx
index cc7234917a..3b0188bc1f 100644
--- a/packages/components/card/src/card.tsx
+++ b/packages/components/card/src/card.tsx
@@ -1,8 +1,10 @@
-import { ReactNode } from 'react';
+import { KeyboardEvent, ReactNode } from 'react';
import { css } from '@emotion/react';
import { designTokens } from '@commercetools-uikit/design-system';
-import { filterDataAttributes } from '@commercetools-uikit/utils';
+import { filterDataAttributes, warning } from '@commercetools-uikit/utils';
import Inset from '@commercetools-uikit/spacings-inset';
+import { Link } from 'react-router-dom';
+import type { LocationDescriptor } from 'history';
export type TCardProps = {
/**
@@ -25,12 +27,34 @@ export type TCardProps = {
*/
className?: string;
children?: ReactNode;
+ /**
+ * The callback function to be executed when the Card component is clicked. Prefer this for managing side effects rather than navigation.
+ */
+ onClick?: () => void;
+ /**
+ * The URL that the Card should point to. If provided, the Card will be rendered as an anchor element.
+ */
+ to?: string | LocationDescriptor;
+ /**
+ * A flag to indicate if the Card points to an external source.
+ */
+ isExternalLink?: boolean;
+ /**
+ * Indicates that a clickable Card should not allow clicks. This allows consumers to temporarily disable a clickable Card.
+ */
+ isDisabled?: boolean;
};
-const Card = (props: TCardProps) => (
-
{
+ const isClickable = Boolean(!props.isDisabled && (props.onClick || props.to));
+ // Only disable styling if the card is not clickable
+ const shouldBeDisabled = props.isDisabled && (props.onClick || props.to);
+
+ const commonProps = {
+ ...filterDataAttributes(props),
+ onClick: isClickable ? props.onClick : undefined,
+ 'aria-disabled': props.isDisabled ? true : undefined,
+ css: css`
box-sizing: border-box;
width: 100%;
font-size: 1rem;
@@ -44,23 +68,83 @@ const Card = (props: TCardProps) => (
background: ${props.theme === 'dark'
? designTokens.colorNeutral95
: designTokens.colorSurface};
- `}
- // Allow to override the styles by passing a `className` prop.
- // Custom styles can also be passed using the `css` prop from emotion.
- // https://emotion.sh/docs/css-prop#style-precedence
- className={props.className}
- >
- {props.insetScale === 'none' ? (
- // Use a `
` to ensure that there is always a wrapper container.
- // This is mostly useful in case custom styles are targeting this element.
+ cursor: ${shouldBeDisabled
+ ? 'not-allowed'
+ : isClickable
+ ? 'pointer'
+ : 'default'};
+ :hover {
+ background: ${props.theme === 'dark'
+ ? isClickable
+ ? designTokens.colorNeutral90
+ : undefined
+ : isClickable
+ ? designTokens.colorNeutral98
+ : undefined};
+ }
+ // Disables link text styling
+ color: inherit;
+ // Changes the opacity of the content, not the card itself
+ & > div {
+ opacity: ${shouldBeDisabled ? 0.5 : 1};
+ }
+ `,
+ className: props.className,
+ };
+
+ const content =
+ props.insetScale === 'none' ? (
{props.children}
) : (
{props.children}
- )}
-
-);
+ );
+
+ if (isClickable) {
+ if (props.to) {
+ if (props.isExternalLink) {
+ warning(
+ typeof props.to === 'string',
+ 'ui-kit/Card: "to" property must be a string when "isExternal" value is true'
+ );
+
+ return (
+
+ {content}
+
+ );
+ } else {
+ return (
+
+ {content}
+
+ );
+ }
+ }
+ }
+
+ return (
+
) => {
+ if (isClickable && props.onClick && event.key === 'Enter') {
+ props.onClick();
+ }
+ }}
+ >
+ {content}
+
+ );
+};
const defaultProps: Pick
= {
type: 'raised',
diff --git a/packages/components/card/src/card.visualroute.jsx b/packages/components/card/src/card.visualroute.jsx
index 6a37be219f..2bd08a697c 100644
--- a/packages/components/card/src/card.visualroute.jsx
+++ b/packages/components/card/src/card.visualroute.jsx
@@ -100,6 +100,39 @@ export const component = () => (
+
+
+ {text}
+
+
+
+
+ {text}
+
+
+
+
+ {text}
+
+
+
+
+ {text}
+
+
+