react-inclusive-card
Version:
A simple, accessible, and flexible card component for React, inspired by the excellent book ["Inclusive Components"](https://inclusive-components.design/cards) by Heydon Pickering.
125 lines (124 loc) • 5.67 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import "./InclusiveCard.css";
import * as React from "react";
/**
* The Card component is a flexible and accessible card layout component
*
* It consists of an image and content area.
* you can specify the element type for the card using the `as` prop.
*/
const Card = ({ as, className = "", imageClassName = "", contentClassName = "", children, imageProps, ...rest }) => {
const Component = as ?? "article";
return (_jsxs(Component, { className: `inclusive-card ${className}`, ...rest, children: [_jsx("div", { className: `inclusive-card__content ${contentClassName}`, children: children }), _jsxs("div", { className: `inclusive-card__image ${imageClassName}`, children: [imageProps.customImageComponent, !imageProps.customImageComponent && (_jsx("img", { src: imageProps.src, alt: imageProps.alt }))] })] }));
};
/**
* @see {@link Card.Title} for more details
*/
const CardTitle = ({ as, href, children, className = "", customLinkComponent, "aria-describedby": ariaDescribedBy, linkProps = {}, ...rest }) => {
const Component = as ?? "h2";
const LinkComponent = customLinkComponent ? customLinkComponent : "a";
const hrefProp = href ? { href } : {};
return (_jsx(Component, { className: `inclusive-card__title ${className}`, ...rest, children: _jsxs(LinkComponent, { "aria-describedby": ariaDescribedBy, ...hrefProp, ...linkProps, children: [children, _jsx("span", { className: "inclusive-card__link-area" })] }) }));
};
/**
* @see {@link Card.Controls} for more details
*/
const CardControls = ({ as = "div", children, className = "", asChild = false, ...rest }) => {
if (asChild) {
try {
// Only works with a single React element child
const childrenCount = React.Children.count(children);
if (childrenCount !== 1) {
throw new Error("CardControls with asChild must have a single valid React element child.");
}
const child = React.Children.only(children);
if (!React.isValidElement(child)) {
throw new Error("CardControls with asChild have invalid child element.");
}
const childClassName = typeof child.props === "object" && child.props?.className
? child.props.className
: "";
// Clone the child and add the className
return React.cloneElement(child, {
className: ["inclusive-card__controls", childClassName, className]
.filter(Boolean)
.join(" "),
});
}
catch (error) {
console.error("Error in CardControls with asChild prop:", error instanceof Error ? error.message : error);
return null;
}
}
const Component = as;
return (_jsx(Component, { className: `inclusive-card__controls ${className}`, ...rest, children: children }));
};
/**
* @see {@link Card.CallToAction} for more details
*/
const CardCallToAction = ({ children, className = "", ...rest }) => {
return (_jsx("span", { className: `inclusive-card__cta ${className}`, "aria-hidden": "true", ...rest, children: children }));
};
// * EXPORTS -------------------------------------------------
export { Card };
/**
* The `<Card.Title>` component is used to render the title of the card,
* it contains a link and it makes the whole card clickable.
*
* You can pass a custom link component using the `customLinkComponent` prop,
* or just pass a `href` prop to use the default `<a>` element.
*/
Card.Title = CardTitle;
/**
* Wrap your interactive elements (like buttons and links) with `<Card.Controls>`
* to raise them above the card clickable area.
*
* It renders a `<div>` by default,
* - you can either change it to any other element using the `as` prop
* - or you can use the `asChild` prop to render the child element instead of the `<div>`
*
* ! Only one child element is allowed for the `<Card.Controls>` component when `asChild` is set to `true`
*/
Card.Controls = CardControls;
/**
* Wrap your "call to action" element with `<Card.CallToAction>` to hide it from screen readers.
* With clickable card, the call-to-action element should be rather decorative, providing additional context to the card link.
*
* The Card.CallToAction component gets focus styles when the `<Card.Title>` link is focused.
* You can customize a `box-shadow` & `text-decoration` using CSS custom properties:
* - `--inclusive-card-cta-focus-decoration`
* - `--inclusive-card-cta-focus-shadow`
*
* Or you can override default styles in your CSS file using selector:
* ```css
* .inclusive-card__title:focus-within ~ .inclusive-card__cta {
* Your custom styles here
* }
* ```
*
* You can add an `aria-describedby` attribute to the Card.Title to attach a call-to-action text to the card link.
*
* @example with default `<Card.Title>` link:
* ```jsx
* <Card as="article">
* <Card.Title href="/some-link" aria-describedby="desc-example-title">Example Title</Card.Title>
* <Card.CallToAction id="desc-example-title">Click here to learn more</Card.CallToAction>
* </Card>
* ```
*
* @example with custom `<Card.Title>` link:
* ```jsx
* const MyCustomLink = ({ children, ...props }) => (
* <Link aria-describedby="desc-example-title" {...props}>
* {children}
* </Link>
* );
*
* <Card as="article">
* <Card.Title customLinkComponent={MyCustomLink}>
* Example Title
* </Card.Title>
* <Card.CallToAction id="desc-example-title">Click here to learn more</Card.CallToAction>
* </Card>
*/
Card.CallToAction = CardCallToAction;