import { useEffect, useRef, useState } from 'react';
import { Meta, StoryObj } from '@storybook/react-vite';
import { ClipboardButton } from '@pantheon-systems/pds-toolkit-react';
import { IconButton } from '../IconButton/IconButton';
import { FeedbackBar, FeedbackBarProps, FeedbackValue } from './FeedbackBar';
import './feedback-bar-story.css';
const THANKS_MS = 4500;
// The consumer owns the choice and the timing, so the story keeps them in
// state: after a choice the thumbs are replaced by the status text for a few
// seconds, then come back with the chosen one pressed. Pressing it again clears
// the choice.
const InteractiveFeedbackBar = ({
statusLabel,
value: initial,
...args
}: FeedbackBarProps) => {
const [value, setValue] = useState<FeedbackValue | null>(initial ?? null);
const [showStatus, setShowStatus] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => () => clearTimeout(timer.current), []);
const handleChange = (next: FeedbackValue | null) => {
setValue(next);
clearTimeout(timer.current);
setShowStatus(next !== null && Boolean(statusLabel));
if (next !== null) {
timer.current = setTimeout(() => setShowStatus(false), THANKS_MS);
}
};
return (
<FeedbackBar
{...args}
onValueChange={handleChange}
statusLabel={showStatus ? statusLabel : undefined}
value={value}
/>
);
};
const meta: Meta<typeof FeedbackBar> = {
tags: ['new-pattern', 'ready-for-dev'],
title: 'Component Proposals/Feedback Bar',
component: FeedbackBar,
parameters: {
docs: {
description: {
component:
'**New pattern:** not in PDS v2 today. Thumbs up and down for rating agent output. It reports the choice and does nothing else; what the screen does with it (analytics, a reason, a thank-you) is up to the screen.',
},
},
},
argTypes: {
children: { control: false, table: { category: 'Slots' } },
className: { table: { category: 'Base attributes' } },
downLabel: { control: 'text', table: { category: 'Content' } },
groupLabel: { control: 'text', table: { category: 'Content' } },
onValueChange: { table: { category: 'Events' } },
statusLabel: { control: 'text', table: { category: 'Content' } },
upLabel: { control: 'text', table: { category: 'Content' } },
value: {
control: 'inline-radio',
options: [null, 'up', 'down'],
table: { category: 'State' },
},
},
args: {
downLabel: 'Bad response',
groupLabel: 'Rate this response',
upLabel: 'Good response',
value: null,
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Interactive: Story = {
args: { statusLabel: 'Thanks for the feedback' },
parameters: {
docs: {
description: {
story:
'Press a thumb: the status text shows for a few seconds, then the thumbs come back with your choice pressed. Press the chosen thumb again to clear it. Story-only wiring: the screen owns the choice and the timing.',
},
},
},
render: (args) => <InteractiveFeedbackBar {...args} />,
};
const LABELS = {
downLabel: 'Bad response',
groupLabel: 'Rate this response',
upLabel: 'Good response',
};
const EXAMPLES: Array<{ caption: string; props: Partial<FeedbackBarProps> }> = [
{ caption: 'No choice', props: {} },
{ caption: 'Thumbs up chosen', props: { value: 'up' } },
{ caption: 'Thumbs down chosen', props: { value: 'down' } },
{
caption: 'With status text',
props: { statusLabel: 'Thanks for the feedback' },
},
{
caption: 'With extra actions',
props: {
children: (
<>
<ClipboardButton
ariaLabel='Copy'
clipboardText='Sample response text'
size='s'
/>
<IconButton ariaLabel='Try again' iconName='rotate' size='s' />
</>
),
},
},
];
export const AllStates: Story = {
parameters: {
docs: {
description: {
story:
'Every state, static. The status text (`statusLabel`) replaces the thumbs and is announced to screen readers; the screen decides when to show it and for how long. The `children` slot takes extra actions after the thumbs.',
},
},
},
render: () => (
<div className='feedback-bar-story__stack'>
{EXAMPLES.map(({ caption, props }) => (
<div className='feedback-bar-story__example' key={caption}>
<p className='pds-copy-xs feedback-bar-story__caption'>{caption}</p>
<FeedbackBar {...LABELS} {...props} />
</div>
))}
</div>
),
};
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