← All proposals

Chat Attachment

Net-new component: one attached item for chat.

Net-new componentOpen in Storybook

proposals/ChatAttachment/HANDOFF.md93 lines · View on GitHub

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's fieldHeader stays the container.
  • Named ChatAttachment, not ChatFileUpload, to avoid confusion with PDS FileUpload.
  • Fixed size: file chips are one width (spacing-9xl) and height (spacing-3xl) with the name clamped to two lines; images are a spacing-3xl square, 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 a visually-hidden span and the short text is aria-hidden.
  • Full name in the PDS Tooltip with 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 when onRemove is set. No tooltip on it (hasTooltip={false}, title={undefined}); the aria-label stays. 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, diamondExclamation icon; a failed image has no thumbnail, just the image icon in critical color. status, not a boolean, so an uploading value can join it. The chip does not announce its own error (one live message is owned by the screen, for example ChatInput's helper message).
  • Type uses the semantic pds-copy-2xs class; 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 (with customTrigger), local Icon (file, image, diamondExclamation) and local IconButton (xmark, xs), the pds-copy-2xs and visually-hidden classes.
  • 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 in custom.md.

Accessibility

  • The full file name is always in a visually-hidden span; the shortened visible text is aria-hidden. A thumbnail has alt set to the file name.
  • The error reason is visually-hidden plain text (no role='alert'), read when the user reaches the chip; the screen owns the single live announcement.
  • Remove is a real button with an aria-label from removeLabel. 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: Icon and IconButton are marked ready for dev, so they can port first. The PDS Tooltip is already in the package. No dependency on an unready proposal.
  • Confirm the name ChatAttachment (Figma says ChatFileUpload).
  • 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 an errorType needed (retry for network, remove-only for too large or unsupported)? An uploading status is not built.
  • removeLabel falls back to an empty aria-label when missing, so it is unlabelled; the port may want to make it required with onRemove (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 a div; 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.