← All proposals

Feedback Bar

A small atomic component: a thumbs-up and thumbs-down pair with one piece of state, built from the local IconButton (its existing isPressed toggle, which maps to aria-pressed).

Net-new componentOpen in Storybook

proposals/FeedbackBar/FeedbackBar.tsx103 lines · View on GitHub
import { ComponentPropsWithoutRef, ReactNode } from 'react';

import { mergeClasses } from '../_base/Button/utils';
import { IconButton } from '../IconButton/IconButton';

import './feedback-bar.css';

export type FeedbackValue = 'down' | 'up';

export interface FeedbackBarProps extends Omit<
	ComponentPropsWithoutRef<'div'>,
	'onChange'
> {
	/**
	 * Extra actions after the thumbs (for example copy or retry buttons).
	 */
	children?: ReactNode;
	/**
	 * Additional class names.
	 */
	className?: string;
	/**
	 * Screen-reader text and tooltip for the thumbs-down button.
	 */
	downLabel: string;
	/**
	 * Name of the group of buttons, read by screen readers (for example "Rate
	 * this response").
	 */
	groupLabel: string;
	/**
	 * Called with the new choice. Pressing the chosen thumb again passes
	 * `null`.
	 */
	onValueChange?: (value: FeedbackValue | null) => void;
	/**
	 * Replaces the thumbs with text (for example a thank-you). Announced to
	 * screen readers when it appears.
	 */
	statusLabel?: string;
	/**
	 * Screen-reader text and tooltip for the thumbs-up button.
	 */
	upLabel: string;
	/**
	 * The current choice: `up`, `down` or none.
	 */
	value?: FeedbackValue | null;
}

/**
 * FeedbackBar UI component. Thumbs up and down for rating agent output. It
 * reports the choice and does nothing else.
 */
export const FeedbackBar = ({
	children,
	className,
	downLabel,
	groupLabel,
	onValueChange,
	statusLabel,
	upLabel,
	value = null,
	...props
}: FeedbackBarProps) => {
	const baseClass = 'pds-feedback-bar';

	const choose = (next: FeedbackValue) =>
		onValueChange?.(value === next ? null : next);

	return (
		<div
			aria-label={groupLabel}
			className={mergeClasses([baseClass, className])}
			role='group'
			{...props}
		>
			{!statusLabel && (
				<span className={`${baseClass}__actions`}>
					<IconButton
						ariaLabel={upLabel}
						iconName='thumbsUp'
						isPressed={value === 'up'}
						onClick={() => choose('up')}
						size='s'
					/>
					<IconButton
						ariaLabel={downLabel}
						iconName='thumbsDown'
						isPressed={value === 'down'}
						onClick={() => choose('down')}
						size='s'
					/>
					{children}
				</span>
			)}
			<span className={`pds-copy-xs ${baseClass}__status`} role='status'>
				{statusLabel}
			</span>
		</div>
	);
};