UNPKG

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
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;