import React, { useRef, useState, useCallback, useEffect } from 'react'; import clsx from 'clsx'; import { CardOffgrid, CardOffgridProps } from '../../components/CardOffgrid'; /** * Configuration for a single card in the CarouselCardList pattern * Extends CardOffgridProps but removes variant (controlled by carousel) */ export type CarouselCardConfig = Omit; /** * Props for the CarouselCardList pattern component */ export interface CarouselCardListProps extends React.ComponentPropsWithoutRef<'section'> { /** Color variant of the cards */ variant?: 'neutral' | 'green'; /** Color variant of the navigation buttons (independent of card color). Defaults to 'neutral' */ buttonVariant?: 'neutral' | 'green' | 'black'; /** Section heading text */ heading: React.ReactNode; /** Section description text */ description: React.ReactNode; /** Array of card configurations */ cards: readonly CarouselCardConfig[]; } /** * Generates a stable key for a card based on its properties. */ const getCardKey = (card: CarouselCardConfig, index: number): string | number => { if (card.href) return card.href; if (card.title) return `${card.title}-${index}`; return index; }; /** * CarouselCardList Pattern Component * * A horizontal scrolling carousel that displays CardOffgrid components. * Features navigation buttons that scroll cards in/out of view. * The navigation button colors can be set independently of the card colors * using the `buttonVariant` prop. * * @example * ```tsx * , title: "Feature 1", description: "..." }, * { icon: , title: "Feature 2", description: "..." }, * ]} * /> * ``` */ export const CarouselCardList = React.forwardRef( (props, ref) => { const { variant = 'neutral', buttonVariant = 'neutral', heading, description, cards, className, ...rest } = props; const scrollContainerRef = useRef(null); const [canScrollPrev, setCanScrollPrev] = useState(false); const [canScrollNext, setCanScrollNext] = useState(true); // Check scroll position and update button states const updateScrollButtons = useCallback(() => { const container = scrollContainerRef.current; if (!container) return; const { scrollLeft, scrollWidth, clientWidth } = container; setCanScrollPrev(scrollLeft > 0); setCanScrollNext(scrollLeft + clientWidth < scrollWidth - 1); }, []); // Initialize and listen for scroll events useEffect(() => { const container = scrollContainerRef.current; if (!container) return; updateScrollButtons(); container.addEventListener('scroll', updateScrollButtons, { passive: true }); window.addEventListener('resize', updateScrollButtons); return () => { container.removeEventListener('scroll', updateScrollButtons); window.removeEventListener('resize', updateScrollButtons); }; }, [updateScrollButtons, cards.length]); // Scroll by one card width const scroll = useCallback((direction: 'prev' | 'next') => { const container = scrollContainerRef.current; if (!container) return; // Get the first card to determine scroll amount const card = container.querySelector('.bds-carousel-card-list__card') as HTMLElement; if (!card) return; const cardWidth = card.offsetWidth; const gap = 8; // 8px gap between cards const scrollAmount = cardWidth + gap; container.scrollBy({ left: direction === 'next' ? scrollAmount : -scrollAmount, behavior: 'smooth', }); }, []); // Early return for empty cards if (cards.length === 0) { console.warn('CarouselCardList: No cards provided'); return null; } return (
{/* Header with title, description, and navigation buttons */}

{heading}

{description}

scroll('prev')} aria-label="Previous cards" /> scroll('next')} aria-label="Next cards" />
{/* Cards scroll container - full bleed */}
{cards.map((card, index) => (
))}
); } ); CarouselCardList.displayName = 'CarouselCardList'; /** * Props for the CarouselButton component */ interface CarouselButtonProps extends React.ButtonHTMLAttributes { /** Arrow direction */ direction: 'prev' | 'next'; /** Color variant (independent of card color) */ variant: 'neutral' | 'green' | 'black'; } /** * Internal CarouselButton component for navigation */ const CarouselButton: React.FC = ({ direction, variant, disabled, className, ...buttonProps }) => { return ( ); }; /** * SVG Arrow Icon for carousel navigation - Right arrow */ const CarouselArrowIconRight: React.FC = () => ( ); /** * SVG Arrow Icon for carousel navigation - Left arrow */ const CarouselArrowIconLeft: React.FC = () => ( ); export default CarouselCardList;