import { ComponentPropsWithoutRef, ReactNode, useContext } from 'react';
import { Panel } from '../Panel/Panel';
import { MilestoneListContext } from './milestone-list-context';
import './milestone.css';
export type MilestoneHeadingLevel = 2 | 3 | 4 | 5 | 6;
export type MilestoneVariant = 'card' | 'flat';
export interface MilestoneProps extends Omit<
ComponentPropsWithoutRef<'div'>,
'title'
> {
/**
* The body: text, links, anything.
*/
children?: ReactNode;
/**
* Additional class names
*/
className?: string;
/**
* Level of the title's heading element. Pick the one that fits the page's
* outline.
*/
headingLevel?: MilestoneHeadingLevel;
/**
* Is this the milestone in focus (the current step, or the one being
* read)? The dot takes the accent color with a ring; the others are neutral.
* Sets `aria-current='step'`.
*/
isActive?: boolean;
/**
* Quiet line in the secondary color, under the title or, when the
* `MilestoneList` has `subtitlePosition='start'`, beside the dot: a date, a range, a
* stage or a short description. Wrap a date in a `time` element for a
* machine-readable value.
*/
subtitle?: ReactNode;
/**
* What the milestone is called.
*/
title: ReactNode;
/**
* `card` puts the milestone on a card (the default Panel); `flat` leaves the
* text alone on the line. Leave it out to follow the `MilestoneList`'s
* `variant` (`card` unless set).
*/
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(' ');
/**
* Milestone UI component. One milestone in a `MilestoneList`: a dot on the line, a title, a subtitle and a body, on a
* card (the default Panel) or flat.
*/
export const Milestone = ({
children,
className,
headingLevel = 3,
isActive = false,
subtitle,
title,
variant: variantProp,
...props
}: MilestoneProps) => {
const baseClass = 'pds-milestone';
const context = useContext(MilestoneListContext);
const variant = variantProp ?? context.variant ?? 'card';
const Heading = `h${headingLevel}` as const;
const content = (
<div className={`${baseClass}__content`}>
<div className={`${baseClass}__header`}>
<Heading className={`pds-heading-m ${baseClass}__title`}>
<span aria-hidden='true' className={`${baseClass}__marker`} />
<span>{title}</span>
</Heading>
{subtitle && (
<div className={`pds-copy-xs ${baseClass}__subtitle`}>{subtitle}</div>
)}
</div>
{children && <div className={`${baseClass}__body`}>{children}</div>}
</div>
);
return (
<div
className={mergeClasses([
baseClass,
isActive ? `${baseClass}--active` : null,
variant === 'card' ? `${baseClass}--card` : null,
className,
])}
aria-current={isActive ? 'step' : undefined}
{...props}
>
{variant === 'card' ? <Panel padding='none'>{content}</Panel> : content}
</div>
);
};
Milestone
Net-new, in two parts, in their own family so "timeline" keeps one meaning (the log).
Net-new componentOpen in Storybook