← All proposals

Timeline

Net-new, in two parts.

Net-new componentOpen in Storybook

proposals/Timeline/Timeline.tsx87 lines · View on GitHub
import {
	Children,
	ComponentPropsWithoutRef,
	isValidElement,
	ReactNode,
} from 'react';

import { TimelineItem } from './TimelineItem';

import './timeline.css';

export type TimelineSize = 's' | 'xs';

export interface TimelineProps extends ComponentPropsWithoutRef<'ol'> {
	/**
	 * What goes in the list. Each child becomes a list item. `TimelineItem`s
	 * are joined by the rail; anything else (a `LabeledDivider`, a
	 * `ChainOfThought`, a `ThinkingState`) breaks it.
	 */
	children?: ReactNode;
	/**
	 * Additional class names
	 */
	className?: string;
	/**
	 * Draw a vertical rail joining the icons of neighbouring `TimelineItem`s.
	 * The rail is decorative.
	 */
	hasRail?: boolean;
	/**
	 * Text size of the items, as a `pds-copy-*` class on the list. Items read
	 * it by inheritance.
	 */
	size?: TimelineSize;
}

// Stand-in for the v2 `mergeClasses` util, which the published package does
// not export. The port uses the real one.
const mergeClasses = (classes: Array<string | null | undefined>) =>
	classes.filter(Boolean).join(' ');

const isItem = (node: ReactNode) =>
	isValidElement(node) && node.type === TimelineItem;

/**
 * Timeline UI component. A list of events over time: `TimelineItem`s, with
 * dividers, a Chain of Thought or a Thinking State in between. It owns the list
 * semantics, the spacing and the optional rail.
 */
export const Timeline = ({
	children,
	className,
	hasRail = false,
	size = 'xs',
	...props
}: TimelineProps) => {
	const baseClass = 'pds-timeline';
	const nodes = Children.toArray(children);

	return (
		<ol
			className={mergeClasses([
				baseClass,
				`pds-copy-${size}`,
				hasRail ? `${baseClass}--rail` : null,
				className,
			])}
			{...props}
		>
			{nodes.map((child, index) => {
				const isJoined = isItem(child) && isItem(nodes[index + 1]);
				return (
					<li
						className={mergeClasses([
							`${baseClass}__entry`,
							isJoined ? `${baseClass}__entry--joined` : null,
						])}
						key={isValidElement(child) ? (child.key ?? index) : index}
					>
						{child}
					</li>
				);
			})}
		</ol>
	);
};