Handoff: ChatAttachment
What and why
One attached file or image in a chat, with an optional remove button. ChatInput has a fieldHeader slot for attached files but PDS had nothing to put in it, and the same item belongs in a sent ChatMessage. PDS FileUpload is a drop zone, which is a different thing. Built from PDS Tooltip plus the local Icon and IconButton. Figma: Chat Attachment page (the designer's hand-built component set; the Figma name is ChatFileUpload, the code name is ChatAttachment). Context: context.md.
Exploration and decisions
- Own component, not part of
ChatInput: an attachment also appears in a sent message.ChatInput'sfieldHeaderstays the container. - Named
ChatAttachment, notChatFileUpload, to avoid confusion with PDSFileUpload. - Fixed size: file chips are one width (
spacing-9xl) and height (spacing-3xl) with the name clamped to two lines; images are aspacing-3xlsquare, so rows wrap predictably. - Long names are shortened in the string, not with CSS (CSS cannot keep the end of a name over two lines). Over 24 characters it keeps the start and the extension (
verylong-filename-exam….md); with no extension, the last 6 characters. The full name is in avisually-hiddenspan and the short text isaria-hidden. - Full name in the PDS
Tooltipwith the tile as custom trigger (hover and keyboard focus; tap only on error chips so it does not fight the remove button). - Remove button: shown on hover (designer's call), on
:focus-within, and always on devices that cannot hover. Only rendered whenonRemoveis set. No tooltip on it (hasTooltip={false},title={undefined}); thearia-labelstays. Its hover and pressed fill sits on an opaque surface so an image does not show through. - Error (
status='error', first try): critical background, border and text,diamondExclamationicon; a failed image has no thumbnail, just the image icon in critical color.status, not a boolean, so anuploadingvalue can join it. The chip does not announce its own error (one live message is owned by the screen, for exampleChatInput's helper message). - Type uses the semantic
pds-copy-2xsclass; colors are semantic tokens.
Full list and reasons: decisions.md.
API
ChatAttachment (extends div props except children; they are spread onto the Tooltip):
| Prop | Type | Default | Purpose |
|---|---|---|---|
className |
string |
none | Additional class names. |
errorMessage |
string |
none | Why it failed (for example "Upload failed"); in the tooltip and read by screen readers. Used with status='error'. |
fileName |
string |
none (required) | Name of the file. Full name in the tooltip and for screen readers; the visible text is shortened when long. |
fileType |
'file' | 'image' |
'file' |
image shows a square thumbnail (or an image icon without previewUrl); file shows an icon and the name. |
onRemove |
() => void |
none | Called when the remove button is pressed. The button only renders when this is set. |
previewUrl |
string |
none | Thumbnail URL. Only used when fileType is image and the status is not error. |
removeLabel |
string |
none | Accessible label of the remove button, for example "Remove report.md". Required in practice when onRemove is set. |
status |
'default' | 'error' |
'default' |
error marks the attachment as failed. |
Exported types: ChatAttachmentFileType, ChatAttachmentStatus, ChatAttachmentProps.
Variants and states: file and image; with and without a preview; default; hover or focus (remove button visible); error (file and image); long name. Stories: Default, Image.
Tokens used
--pds-border-radius-input--pds-border-width-default--pds-color-border-default--pds-color-foreground-default-secondary--pds-color-status-critical-background--pds-color-status-critical-border--pds-color-status-critical-foreground--pds-color-surface-default--pds-color-surface-default-secondary--pds-spacing-2xs--pds-spacing-3xl--pds-spacing-9xl--pds-spacing-xs
The check tool's report also lists --pds-spacing-l, --pds-spacing-xl, --pds-typography-ff-default and --pds-typography-size-xs; those come from chat-attachment-story.css (story layout and captions only), not the component. npm run check:tokens -- ChatAttachment passes with 0 problems.
Impact
From impact.md: no existing PDS component is changed and no --pds-* token is redefined. One opt-in addition: a hover and pressed rule on .pds-icon-button scoped to .pds-chat-attachment__remove, so existing IconButtons are unaffected. The generated report says no other proposal imports or uses ChatAttachment, but ChatInput (stories, in fieldHeader) and ChatWithAgent (stories) import it; they only use it in stories, so nothing has to port with it.
Reused vs net-new
- Reused: PDS
Tooltip(withcustomTrigger), localIcon(file,image,diamondExclamation) and localIconButton(xmark,xs), thepds-copy-2xsandvisually-hiddenclasses. - Net-new:
.pds-chat-attachment(__icon,__image,__item,__item--error,__item--image,__name,__remove); all props. No custom CSS variables, no raw values kept. Details incustom.md.
Accessibility
- The full file name is always in a
visually-hiddenspan; the shortened visible text isaria-hidden. A thumbnail hasaltset to the file name. - The error reason is
visually-hiddenplain text (norole='alert'), read when the user reaches the chip; the screen owns the single live announcement. - Remove is a real button with an
aria-labelfromremoveLabel. It is revealed on hover, on:focus-within(keyboard) and always on touch, so it is keyboard and touch operable. The tooltip opens on focus, which lands on the remove button inside the trigger. - The chip itself is not focusable when there is no
onRemove, so the tooltip with the full name is then hover-only for keyboard users; the full name is still in the DOM text. - The a11y panel has not been run, and light and dark were not checked by eye in a session.
Decisions and open questions
For the engineer or designer to settle:
- Dependencies:
IconandIconButtonare marked ready for dev, so they can port first. The PDSTooltipis already in the package. No dependency on an unready proposal. - Confirm the name
ChatAttachment(Figma saysChatFileUpload). - Image without a preview: built as "no preview"; is it really "loading"?
- Error: first try. Is the reason shown on the chip, is retry needed (
onRetry), should Remove always show on an error, and is anerrorTypeneeded (retry for network, remove-only for too large or unsupported)? Anuploadingstatus is not built. removeLabelfalls back to an emptyaria-labelwhen missing, so it is unlabelled; the port may want to make it required withonRemove(for example a union type).- Name shortening counts characters (24, extension kept), not pixels; it can fit slightly more or less in other fonts.
- Extra props are spread onto the
Tooltip, not adiv; check what the Tooltip does with them.
Port notes
Would live in src/components/ (a chat or conversation category folder; generator npm run generate:component). Not yet matching v2: the local mergeClasses stand-in (use @libs/components/utils); the local Icon and IconButton wrappers (use the ported ones); title={undefined} and hasTooltip={false} on IconButton are needed only if it still falls back to title; pds-copy-2xs is !important, so a rule on the same element cannot override size or weight; the -webkit-box two-line clamp on the name.
Preview
npm run storybook, story Component Proposals / AI Chat / Chat Attachment. Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=291-6953, page ↳ Chat Attachment. The designer built this page by hand as components (set ChatFileUpload: FileType Image/Doc, State Default/Hover/Error), not captures from the workflow; its Figma name is ChatFileUpload while the code name is ChatAttachment.