Follow-up to #438, which replaced ChatScrollButton's ComponentPropsWithoutRef<'div'> extension with a curated ButtonPassthroughProps set. The rest of the Chat directory still has the wide-open pattern:
ChatLayout.tsx:38 — ComponentPropsWithoutRef<'div'>
ChatComposer.tsx:35 — Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'onChange' | 'onSubmit'>
ChatComposerInput.tsx:24 — Omit<ComponentPropsWithoutRef<'textarea'>, 'onChange' | 'value'>
ChatMessage.tsx:20 — ComponentPropsWithoutRef<'article'>
ChatMessageList.tsx:22 — ComponentPropsWithoutRef<'div'>
ChatMessageBubble.tsx:18 — ComponentPropsWithoutRef<'div'>
ChatMessageMetadata.tsx:30 — ComponentPropsWithoutRef<'div'>
ChatSystemMessage.tsx:14 — ComponentPropsWithoutRef<'div'>
Each spreads ...rest onto its root element, so the entire DOM prop surface is accepted and forwarded — including props that fight the component's own contract (role, aria-live, data-sender, data-chat-message) with no indication which wins.
Narrow each to the props a consumer might reasonably need, matching the approach in #438.
Two cases need more than a mechanical swap:
ChatMessage computes aria-label/aria-labelledby from name/sender and spreads ...rest before them, so a consumer label is silently dropped today. The curated version should let the consumer's value win and fall back to the generated one.
ChatMessageList hardcodes aria-live="polite". A static transcript that should not be announced has no way to opt out.
Follow-up to #438, which replaced
ChatScrollButton'sComponentPropsWithoutRef<'div'>extension with a curatedButtonPassthroughPropsset. The rest of the Chat directory still has the wide-open pattern:ChatLayout.tsx:38—ComponentPropsWithoutRef<'div'>ChatComposer.tsx:35—Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'onChange' | 'onSubmit'>ChatComposerInput.tsx:24—Omit<ComponentPropsWithoutRef<'textarea'>, 'onChange' | 'value'>ChatMessage.tsx:20—ComponentPropsWithoutRef<'article'>ChatMessageList.tsx:22—ComponentPropsWithoutRef<'div'>ChatMessageBubble.tsx:18—ComponentPropsWithoutRef<'div'>ChatMessageMetadata.tsx:30—ComponentPropsWithoutRef<'div'>ChatSystemMessage.tsx:14—ComponentPropsWithoutRef<'div'>Each spreads
...restonto its root element, so the entire DOM prop surface is accepted and forwarded — including props that fight the component's own contract (role,aria-live,data-sender,data-chat-message) with no indication which wins.Narrow each to the props a consumer might reasonably need, matching the approach in #438.
Two cases need more than a mechanical swap:
ChatMessagecomputesaria-label/aria-labelledbyfromname/senderand spreads...restbefore them, so a consumer label is silently dropped today. The curated version should let the consumer's value win and fall back to the generated one.ChatMessageListhardcodesaria-live="polite". A static transcript that should not be announced has no way to opt out.