[all components] Type render callback props to the rendered element - #5104
Conversation
Specialize the render callback's props (the third generic of BaseUIComponentProps) to ComponentPropsWithRef<'tag'> for parts locked to a single element: Avatar.Image, Form, Fieldset.Root, NumberField.Input, OTPField.Input, and the anchor links (PreviewCard.Trigger, Menu.LinkItem, NavigationMenu.Link, Toolbar.Link). Keep the loose HTMLProps default for polymorphic parts (Field.Control, Toolbar.Input, Combobox/Autocomplete Input, Field.Label, Slider.Value, *.Button parts) so they can still be rendered as a different element. Also drop the injected type='text' from Combobox/Autocomplete Input (native default already; was invalid when rendered as a <textarea>).
commit: |
Bundle size
PerformanceTotal duration: 1,297.28 ms -96.59 ms(-6.9%) | Renders: 78 (+0) No significant changes — details Check out the code infra dashboard for more information about this PR. |
✅ Deploy Preview for base-ui ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
The render prop type column now resolves to the element-specific props for the parts whose render callback was specialized (Avatar.Image, Form, Fieldset.Root, NumberField.Input, OTPField.Input, and the anchor links incl. Menu/ContextMenu LinkItem).
Pins two things for rendering Combobox/Autocomplete Input as a <textarea> via the render prop: it no longer receives an invalid `type` attribute (guards the removed injected type='text'), and the combobox ARIA attributes (role, aria-expanded, aria-controls) are applied only while the popup is open — a non-input control omits them when closed, unlike a native <input> which always exposes them.
7e508eb to
50ceaee
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves Base UI’s type safety for render callback props by specializing the third generic of BaseUIComponentProps to the actual rendered intrinsic element for selected components (e.g. <a>, <img>, <input>, <form>, <fieldset>), while keeping looser typing where polymorphism is intended. It also removes an injected type="text" from Combobox.Input to support rendering as <textarea> without invalid attributes, and updates type-level checks and generated docs accordingly.
Changes:
- Specialized
BaseUIComponentProps<..., ..., RenderFunctionProps>for several parts sorender={(props) => ...}exposes the correct intrinsic props (e.g.href,src,disabled). - Removed injected
type: 'text'fromCombobox.Inputand added runtime tests ensuring<textarea>rendering stays valid and editable. - Added/expanded type-level
*.spec.tsxchecks and updated generated docs to reflect the new callback prop types.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/toolbar/link/ToolbarLink.tsx | Specializes render callback props to native <a> props. |
| packages/react/src/toolbar/link/ToolbarLink.spec.tsx | Adds type-level check that render props include href. |
| packages/react/src/preview-card/trigger/PreviewCardTrigger.tsx | Specializes render callback props to native <a> props. |
| packages/react/src/preview-card/trigger/PreviewCardTrigger.spec.tsx | Adds type-level check that render props include href. |
| packages/react/src/otp-field/input/OTPFieldInput.tsx | Specializes render callback props to native <input> props. |
| packages/react/src/otp-field/input/OTPFieldInput.spec.tsx | Adds type-level checks for <input>-specific props in render. |
| packages/react/src/number-field/input/NumberFieldInput.tsx | Specializes render callback props to native <input> props. |
| packages/react/src/number-field/input/NumberFieldInput.spec.tsx | Adds type-level checks for <input>-specific props in render. |
| packages/react/src/navigation-menu/link/NavigationMenuLink.tsx | Specializes render callback props to native <a> props. |
| packages/react/src/navigation-menu/link/NavigationMenuLink.spec.tsx | Adds type-level check that render props include href. |
| packages/react/src/menu/link-item/MenuLinkItem.tsx | Specializes render callback props to native <a> props. |
| packages/react/src/menu/link-item/MenuLinkItem.spec.tsx | Adds type-level check that render props include href. |
| packages/react/src/form/Form.tsx | Specializes render callback props to native <form> props. |
| packages/react/src/form/Form.spec.tsx | Adds type-level check that render props include noValidate. |
| packages/react/src/fieldset/root/FieldsetRoot.tsx | Specializes render callback props to native <fieldset> props. |
| packages/react/src/fieldset/root/FieldsetRoot.spec.tsx | Adds type-level check that render props include disabled. |
| packages/react/src/combobox/input/ComboboxInput.tsx | Removes injected type="text" to allow <textarea> rendering without invalid attributes. |
| packages/react/src/combobox/input/ComboboxInput.test.tsx | Adds runtime coverage for rendering Combobox.Input as <textarea>. |
| packages/react/src/avatar/image/AvatarImage.tsx | Specializes render callback props to native <img> props. |
| packages/react/src/avatar/Avatar.spec.tsx | Adds type-level checks that Avatar.Image render props include src/alt. |
| docs/src/app/(docs)/react/components/toolbar/types.md | Updates generated API docs for specialized <a> render prop typing. |
| docs/src/app/(docs)/react/components/preview-card/types.md | Updates generated API docs for specialized <a> render prop typing. |
| docs/src/app/(docs)/react/components/otp-field/types.md | Updates generated API docs for specialized <input> render prop typing. |
| docs/src/app/(docs)/react/components/number-field/types.md | Updates generated API docs for specialized <input> render prop typing. |
| docs/src/app/(docs)/react/components/navigation-menu/types.md | Updates generated API docs for specialized <a> render prop typing. |
| docs/src/app/(docs)/react/components/menu/types.md | Updates generated API docs for specialized <a> render prop typing. |
| docs/src/app/(docs)/react/components/form/types.md | Updates generated API docs for specialized <form> render prop typing. |
| docs/src/app/(docs)/react/components/fieldset/types.md | Updates generated API docs for specialized <fieldset> render prop typing. |
| docs/src/app/(docs)/react/components/context-menu/types.md | Updates generated API docs impacted by specialized <a> render prop typing. |
| docs/src/app/(docs)/react/components/avatar/types.md | Updates generated API docs for specialized <img> render prop typing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
flaviendelangle
left a comment
There was a problem hiding this comment.
PR review
This PR is type-only except for one safe runtime change (dropping the injected type="text" on Combobox.Input/Autocomplete.Input). The native-input-vs-other-element ARIA branch keys off tagName === 'INPUT' (AriaCombobox.tsx:992), not the type attribute, so the removal doesn't disturb it, and the new tests pin both the editability and the open-only ARIA behavior. The render-callback specialization only propagates each component's already-declared element type ('img', 'form', 'input', 'a') to the callback param, so it can't tighten anything beyond what the component's own props already required. Nothing is merge-blocking.
Bugs (0)
No findings.
Tests (0)
No findings.
Simplifications (0)
No findings.
Docs (2)
1. ℹ️ Auto-generated render type is now noisy in public API tables
Location: docs/src/app/(docs)/react/components/form/types.md:11 (and the avatar/number-field/otp-field/preview-card/menu/navigation-menu/toolbar tables)
| render | `ReactElement \| ((props: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, state: Form.State) => ReactElement)` | ... |The specialized third generic causes pnpm docs:api to emit the fully-expanded React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> in place of the prior clean HTMLProps. It's accurate but considerably harder to read in the published prop table.
Failure scenario: A user scanning the Form/Avatar/Input prop tables sees a long leaky type for render instead of a concise one; purely a readability regression in the generated docs.
Fix: Optional — if the docs generator supports a type alias/display override, map these back to a short name; otherwise accept it as the cost of the more precise type.
2. ℹ️ Combobox.Input no longer emits a type="text" attribute
Location: packages/react/src/combobox/input/ComboboxInput.tsx:205
// removed:
type: 'text',Behaviorally inert for a native <input> (text is the default), and the PR documents it as intentional. The only observable effect is that the rendered DOM element no longer carries type="text", so an app styling the input via an input[type="text"] selector would stop matching. The in-repo demos that rely on type="text" pass it explicitly (autocomplete/demos/grid/...), so they're unaffected.
Failure scenario: A consumer with CSS like .MyCombobox input[type="text"] { ... } loses those styles after upgrading.
Fix: None needed; consider a one-line changelog/migration note since it's a visible DOM-attribute change.
Verdict
Approve — type-only specialization plus a safe, well-tested type="text" removal; no correctness or test gaps, only minor cosmetic notes on generated docs.
🤖 Review generated with Claude Code
Fixes #5102
The third generic of
BaseUIComponentPropstypes therendercallback's props. It defaults toHTMLPropsso the callback can spread onto any element. Specializing it toComponentPropsWithRef<'tag'>exposes element-specific props (src,href,disabled, …) but blocks rendering as a different element. So we specialize only parts locked to one element, and keepHTMLPropsfor parts that can legitimately render as something else.Specialized
Avatar.Image<img>Form<form>NumberField.Input<input>OTPField.Input<input>PreviewCard.Trigger<a>Menu.LinkItem<a>NavigationMenu.Link<a>Toolbar.Link<a>Kept loose (
HTMLProps)Combobox.Input/Autocomplete.Input<textarea>Field.Control,Toolbar.Input<textarea>Field.LabelnativeLabel={false}supports<div>Fieldset.RootRadioGroup,CheckboxGroup, orSlider.RootSlider.Value<span>/<div>; shouldn't forcehtmlForNavigationMenu.List<ul>has no element-specific props*.ButtonpartsnativeButtonAlso
Combobox.Input/Autocomplete.Input: dropped the injectedtype="text"(native default already; was invalid when rendered as a<textarea>).*.spec.tsxchecks for the specialized callbacks.