← All proposals

Code Diff

A Panel List of changed files.

Net-new componentOpen in Storybook

proposals/CodeDiff/CodeDiff.tsx141 lines · View on GitHub
import { ComponentPropsWithoutRef } from 'react';

import {
	CodeBlock,
	FileDiff,
	PanelList,
	PanelListProps,
	PanelRow,
} from '@pantheon-systems/pds-toolkit-react';

import { Icon } from '../Icon/Icon';

import './code-diff.css';

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

/**
 * One changed file in a CodeDiff.
 */
export interface CodeDiffFile {
	/**
	 * Number of added lines. Shows the PDS File Diff (counts and bar) at the end
	 * of the row when set together with `deletions`.
	 */
	additions?: number;
	/**
	 * Number of removed lines. See `additions`.
	 */
	deletions?: number;
	/**
	 * The diff text, in unified diff format.
	 */
	diff: string;
	/**
	 * Aria-label for the expand button of this row, for example "Show changes
	 * to composer.json". Provide it translated.
	 */
	expandButtonLabel?: string;
	/**
	 * Unique id for the file.
	 */
	id: string;
	/**
	 * File name shown in the row.
	 */
	name: string;
}

/**
 * Prop types for CodeDiff
 */
export interface CodeDiffProps extends ComponentPropsWithoutRef<'div'> {
	/**
	 * Additional class names
	 */
	className?: string;
	/**
	 * Aria-label and tooltip text for each diff's copy button.
	 */
	copyButtonLabel?: string;
	/**
	 * The changed files, one expandable row each.
	 */
	files: CodeDiffFile[];
	/**
	 * Whether the list has an outside border. Passed to the PDS Panel List.
	 */
	hasBorder?: PanelListProps['hasBorder'];
	/**
	 * Show a copy button on each diff.
	 */
	hasCopyButton?: boolean;
	/**
	 * Block padding of each row. Passed to the PDS Panel List's `paddingSize`.
	 * `2xs` (default) is the compact setting for this component; the Panel
	 * List's own default is `m`.
	 */
	paddingSize?: PanelListProps['paddingSize'];
}

/**
 * CodeDiff UI component. A PDS Panel List of changed files. Each row expands to
 * show its diff in a PDS Code Block.
 */
export const CodeDiff = ({
	className,
	copyButtonLabel,
	files,
	hasBorder = true,
	hasCopyButton = false,
	paddingSize = '2xs',
	...props
}: CodeDiffProps) => {
	// Set up classes.
	const baseClass = 'pds-code-diff';

	// Render the output
	return (
		<div className={mergeClasses([baseClass, className])} {...props}>
			<PanelList hasBorder={hasBorder} paddingSize={paddingSize}>
				{files.map((file) => {
					const hasStats =
						file.additions !== undefined && file.deletions !== undefined;

					return (
						<PanelRow
							expandButtonAriaLabel={file.expandButtonLabel}
							isExpandable
							key={file.id}
						>
							<div className={`${baseClass}__file`} slot='items-left'>
								<Icon iconName='fileDiff' size='s' />
								<span className={`${baseClass}__name`}>{file.name}</span>
							</div>
							{hasStats ? (
								<div className={`${baseClass}__stats`} slot='items-right'>
									<FileDiff
										additions={file.additions as number}
										deletions={file.deletions as number}
									/>
								</div>
							) : null}
							<div slot='items-below'>
								<CodeBlock
									code={file.diff}
									copyButtonLabel={copyButtonLabel}
									hasCopyButton={hasCopyButton}
									language='diff'
								/>
							</div>
						</PanelRow>
					);
				})}
			</PanelList>
		</div>
	);
};