import { Check } from '@transferwise/icons'; import { clsx } from 'clsx'; import React, { forwardRef, FunctionComponent, useEffect, useId, useState } from 'react'; import Body from '../body'; import { Typography } from '../common'; import BaseCard, { type BaseCardProps } from '../common/baseCard'; import Display from '../display'; import Image from '../image/Image'; import Title from '../title'; import { usePromoCardContext } from './PromoCardContext'; import PromoCardIndicator, { type PromoCardIndicatorProps } from './PromoCardIndicator'; export type ReferenceType = React.Ref | React.Ref; export type RelatedTypes = | '' | 'alternate' | 'author' | 'bookmark' | 'external' | 'help' | 'license' | 'next' | 'nofollow' | 'noreferrer' | 'noopener' | 'prev' | 'search' | 'tag'; export interface PromoCardCommonProps { /** Optional prop to specify classNames onto the PromoCard */ className?: string; /** Optional prop to specify the ID of the PromoCard */ id?: string; /** Required prop to specify the descriptive text of the PromoCard */ description: string; /** * Optional prop to specify the heading level of the PromoCard * * @default 'h3' */ headingLevel?: 'h3' | 'h4' | 'h5' | 'h6'; /** Optional prop to specify text for the indicator label of the PromoCard */ indicatorLabel?: string; /** Optional prop to specify the icon for the indicator icon of the PromoCard */ indicatorIcon?: PromoCardIndicatorProps['icon']; /** Optional prop to specify an image alt text */ imageAlt?: string; /** Optional prop to specify an image class */ imageClass?: string; /** Optional prop to specify an image source url */ imageSource?: string; /** Specify whether the PromoCard is disabled, or not */ isDisabled?: boolean; /** Specify an onClick event handler */ onClick?: () => void; /** Specify an onKeyDown event handler */ onKeyDown?: (event: React.KeyboardEvent) => void; /** Optional prop to specify the ID used for testing */ testId?: string; /** Required prop to specify the title text of the PromoCard */ title: string; /** Set to false to use body font style for the title */ useDisplayFont?: boolean; ref?: ReferenceType; } export interface PromoCardLinkProps extends PromoCardCommonProps, Omit { /** * Optional prop to prompts a user to save the linked URL instead of * navigating to it */ download?: string; /** Optionally specify an href for your PromoCard to contain an element */ href?: string; /** Optionally specify the language of the linked URL */ hrefLang?: string; /** Optional property that can be pass a ref for the anchor. */ anchorRef?: React.Ref; /** * Optional prop to specify the ID of the anchor element which can be useful when using a ref. */ anchorId?: string; /** * Relationship between the PromoCard href URL and the current page. See * [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). */ rel?: RelatedTypes; /** Optional prop to to display where the linked URL will show */ target?: React.HTMLAttributeAnchorTarget; /** Only applies to role="radio" or "checkbox" */ defaultChecked?: never; isChecked?: never; tabIndex?: never; type?: never; ref?: ReferenceType; value?: never; } export interface PromoCardCheckedProps extends PromoCardCommonProps, Omit { /** Specify the initial checked attribute of the PromoCard */ defaultChecked?: boolean; /** Specify whether the PromoCard is checked, or not */ isChecked?: boolean; /** Optional prop to specify the tabIndex of the PromoCard */ tabIndex?: number; /** Optional property to provide component Ref */ ref?: ReferenceType; /** Optional prop to specify the input type of the PromoCard */ type?: 'checkbox' | 'radio'; /** Specify the value attribute of the PromoCard if Checkbox or Radio */ value?: string; /** Only applies to s */ download?: never; href?: never; anchorRef?: never; anchorId?: never; hrefLang?: never; rel?: never; target?: never; } export type PromoCardProps = PromoCardLinkProps | PromoCardCheckedProps; export type PolymorphicPromoCard = ( props: PromoCardLinkProps | PromoCardCheckedProps, ) => React.JSX.Element; /** * PromoCard component. * * PromoCard is a marketing style component that is used to group marketing * product related information (such as choosing Card types). It can be used to * display information in a structured way, and can be customized with various * props to suit different use cases. * * @component * @param {string} className - Additional class name for the PromoCard. * @param {string} description - Description text for the PromoCard. * @param {boolean} defaultChecked - Initial checked state for checkboxes and radios. * @param {string} download - Download file name for links. * @param {string} href - URL for links. * @param {string} hrefLang - Language code for linked URL. * @param {string} id - ID of the PromoCard. * @param {string} imageAlt - Alt text for the image. * @param {string} imageSource - Source URL of the image. * @param {string} indicatorLabel - Label for the indicator icon. * @param {boolean} isChecked - Checked state for checkboxes and radios. * @param {boolean} isDisabled - Whether the PromoCard is disabled. * @param {Function} onClick - Click event handler for the PromoCard. * @param {string} rel - Relationship between the URL and the current page. * @param {number} tabIndex - Tab index for keyboard navigation. * @param {string} target - Target window for links. * @param {string} testId - ID used for testing. * @param {string} title - Title text of the PromoCard. * @param {('checkbox'|'radio')} type - Type of the PromoCard (checkbox, radio). * @param {string} value - Value for checkboxes and radios. * @returns {React.JSX.Element} The rendered PromoCard component. * @example * */ const PromoCard: FunctionComponent = forwardRef( ( { className, description, defaultChecked, download, href, hrefLang, id, headingLevel = 'h3', imageAlt, imageClass, imageSource, indicatorLabel, indicatorIcon, isChecked, isDisabled, onClick, onKeyDown, rel, tabIndex, target, testId, title, type, value, isSmall, useDisplayFont = true, anchorRef, anchorId, ...props }, ref: ReferenceType, ) => { // Set the `checked` state to the value of `defaultChecked` if it is truthy, // or the value of `isChecked` if it is truthy, or `false` if neither // is truthy. const { state, onChange, isDisabled: contextIsDisabled } = usePromoCardContext(); const [checked, setChecked] = useState( type === 'checkbox' ? (defaultChecked ?? isChecked ?? false) : false, ); const handleClick = () => { if (type === 'radio') { onChange(value || ''); // Update the context state for radio } else if (type === 'checkbox') { setChecked(!checked); // Update local state for checkbox } }; const fallbackId = useId(); const componentId = id || fallbackId; // Set the icon to `'arrow'` if `href` is truthy and `type` is falsy, or // `'download'` if `download` is truthy. If neither condition is true, set // `icon` to `undefined`. // Create a function to get icon type const getIconType = () => { if (indicatorIcon) { return indicatorIcon; } if (download) { return 'download'; } if (href && !type) { return 'arrow'; } return undefined; }; const CardTitle = useDisplayFont ? Display : Title; // Define all class names string based on the values of the `href`, `type`, // `checked`, and `className` props. const commonClasses = clsx( { 'np-Card--promoCard': true, 'np-Card--checked': !href && type, 'np-Card--link': href && !type, 'is-checked': type === 'radio' ? value === state : type === 'checkbox' ? checked : undefined, }, className, ); // Object with common props that will be passed to the `Card` components const commonProps = { className: commonClasses, id: componentId, isDisabled: isDisabled || contextIsDisabled, onClick, onKeyDown, ref, 'data-testid': testId, isSmall, }; // Object with Anchor props that will be passed to the `a` element. These // won't be refurned if set to `isDisabled` const anchorProps = href && !isDisabled ? { download, href: href || undefined, hrefLang, rel, target, ref: anchorRef, id: anchorId, } : {}; // Object of all Checked props that will be passed to the root `Card` component const checkedProps = (type === 'checkbox' || type === 'radio') && !href ? { ...commonProps, 'aria-checked': type === 'radio' ? value === state : type === 'checkbox' ? checked : undefined, 'aria-describedby': `${componentId}-title`, 'aria-disabled': isDisabled, 'data-value': value ?? undefined, role: type === 'checkbox' || type === 'radio' ? type : undefined, onClick: handleClick, onKeyDown: (event: React.KeyboardEvent) => { if (event.key === 'Enter' || event.key === ' ') { handleClick(); } }, ref, tabIndex: 0, } : {}; const getTitle = () => { const titleContent = href && !type ? ( {title} ) : ( title ); const titleProps = { id: `${componentId}-title`, as: headingLevel, className: 'np-Card-title', }; return useDisplayFont ? ( {titleContent} ) : ( {titleContent} ); }; useEffect(() => { setChecked(defaultChecked ?? isChecked ?? false); }, [defaultChecked, isChecked]); return ( {(value === state || checked) && ( )} {getTitle()} {description} {imageSource && (
{imageAlt
)}
); }, ) as PolymorphicPromoCard; export default React.memo(PromoCard);