← All proposals

Milestone

Net-new, in two parts, in their own family so "timeline" keeps one meaning (the log).

Net-new componentOpen in Storybook

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

import type { MilestoneVariant } from './Milestone';
import { MilestoneListContext } from './milestone-list-context';

import './milestone-list.css';

export type MilestoneListSubtitlePosition = 'below' | 'start';

export interface MilestoneListProps extends ComponentPropsWithoutRef<'ol'> {
	/**
	 * The milestones. Each child becomes a list item.
	 */
	children?: ReactNode;
	/**
	 * Additional class names
	 */
	className?: string;
	/**
	 * Where each milestone's subtitle goes: `below` the title (default) or
	 * `start`, at the left of the line, beside the dot. At narrow widths `start`
	 * falls back to below the title.
	 */
	subtitlePosition?: MilestoneListSubtitlePosition;
	/**
	 * Put each milestone on a card (`card`, default) or leave the text on the
	 * line (`flat`). A milestone can override it.
	 */
	variant?: MilestoneVariant;
}

// 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(' ');

/**
 * MilestoneList UI component. A list of `Milestone`s hung off one line that
 * runs the full height of the list. It owns the list semantics, the spacing,
 * the line and the card style that milestones follow.
 */
export const MilestoneList = ({
	children,
	className,
	subtitlePosition = 'below',
	variant,
	...props
}: MilestoneListProps) => {
	const baseClass = 'pds-milestone-list';
	const nodes = Children.toArray(children);

	return (
		<MilestoneListContext.Provider value={{ variant }}>
			<ol
				className={mergeClasses([
					baseClass,
					subtitlePosition === 'start' ? `${baseClass}--subtitle-start` : null,
					className,
				])}
				{...props}
			>
				{nodes.map((child, index) => (
					<li
						className={`${baseClass}__entry`}
						key={isValidElement(child) ? (child.key ?? index) : index}
					>
						{child}
					</li>
				))}
			</ol>
		</MilestoneListContext.Provider>
	);
};