[pickers] Refactor PickersTextField to use slotProps approach#22002
Conversation
|
Deploy preview: https://deploy-preview-22002--material-ui-x.netlify.app/ Updated pages: Bundle size report
|
OverviewFollow-up to #21966. Removes the already-deprecated legacy Ships with a Strengths
Issues🔴 Potential bug — user
|
There was a problem hiding this comment.
Pull request overview
This PR refactors the pickers’ built-in PickersTextField (and its consumers) to align with the Material UI v9 slots API by removing legacy InputProps / inputProps / InputLabelProps / FormHelperTextProps and replacing them with a slots / slotProps structure, simplifying the picker/field UI layer and providing migration tooling.
Changes:
- Refactors
PickersTextField+ internal input components to support Material v9-styleslots/slotProps. - Simplifies
PickerFieldUI+ field/picker call sites to forwardslotPropsdirectly. - Adds a
v9.0.0/pickers/migrate-text-field-propscodemod and updates docs/tests/fixtures accordingly.
Reviewed changes
Copilot reviewed 50 out of 50 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/x-date-pickers.exports.json | Exports new PickersTextFieldSlots / PickersTextFieldSlotProps types. |
| scripts/x-date-pickers-pro.exports.json | Mirrors new PickersTextField* slot type exports for Pro. |
| packages/x-date-pickers/src/TimeField/TimeField.tsx | Stops destructuring legacy text-field props; updates PropTypes. |
| packages/x-date-pickers/src/PickersTextField/PickersTextField.types.ts | Introduces slots / slotProps typings for PickersTextField. |
| packages/x-date-pickers/src/PickersTextField/PickersTextField.tsx | Implements slot-based rendering; removes legacy prop plumbing. |
| packages/x-date-pickers/src/PickersTextField/PickersOutlinedInput/PickersOutlinedInput.tsx | Plumbs slots/slotProps through to PickersInputBase. |
| packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.types.ts | Adds htmlInput slot typing and shifts base prop typing away from BoxProps. |
| packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.tsx | Implements htmlInput as a slot; resolves html input props via useSlotProps. |
| packages/x-date-pickers/src/PickersTextField/PickersInput/PickersInput.tsx | Forwards slots/slotProps; merges root slot props. |
| packages/x-date-pickers/src/PickersTextField/PickersFilledInput/PickersFilledInput.tsx | Forwards slots/slotProps; merges root/input slot props. |
| packages/x-date-pickers/src/PickersTextField/index.ts | Re-exports new slot type interfaces. |
| packages/x-date-pickers/src/PickersTextField/describe.PickersTextField.test.tsx | Adds conformance coverage for PickersTextField. |
| packages/x-date-pickers/src/models/fields.ts | Omits slots / slotProps from BuiltInFieldTextFieldProps. |
| packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx | Removes legacy prop remapping; merges adornments via slotProps.input. |
| packages/x-date-pickers/src/DesktopDatePicker/tests/DesktopDatePicker.test.tsx | Updates tests to assert new slotProps.textField.slotProps.* behavior. |
| packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx | Removes legacy text-field prop PropTypes entries. |
| packages/x-date-pickers/src/DateField/tests/DateField.test.tsx | Updates field tests to use nested slotProps shape. |
| packages/x-date-pickers/src/DateField/DateField.tsx | Removes legacy text-field prop PropTypes entries. |
| packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx | Removes legacy text-field prop PropTypes entries. |
| packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx | Removes legacy text-field prop PropTypes entries. |
| packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx | Removes legacy text-field prop PropTypes entries. |
| packages/x-date-pickers-pro/src/internals/utils/createMultiInputRangeField/useTextFieldProps.ts | Writes trigger ref + multi-input marker to slotProps.input. |
| packages/x-codemod/src/v9.0.0/pickers/preset-safe/index.ts | Adds migrate-text-field-props to the v9 preset-safe pipeline. |
| packages/x-codemod/src/v9.0.0/pickers/preset-safe/expected.spec.tsx | Adds preset-safe expected output for migrated text-field props. |
| packages/x-codemod/src/v9.0.0/pickers/preset-safe/actual.spec.tsx | Adds preset-safe input fixture using legacy text-field props. |
| packages/x-codemod/src/v9.0.0/pickers/migrate-text-field-props/migrate-text-field-props.test.ts | Adds tests for the new codemod + idempotency check. |
| packages/x-codemod/src/v9.0.0/pickers/migrate-text-field-props/index.ts | Implements codemod to rewrite legacy props into slotProps.* shape. |
| packages/x-codemod/src/v9.0.0/pickers/migrate-text-field-props/expected.spec.tsx | Codemod expected fixture for multiple nesting scenarios. |
| packages/x-codemod/src/v9.0.0/pickers/migrate-text-field-props/actual.spec.tsx | Codemod input fixture (legacy props + nested slotProps). |
| packages/x-codemod/README.md | Documents the new migrate-text-field-props codemod and usage. |
| docs/translations/api-docs/date-pickers/time-field/time-field.json | Removes legacy prop entries from translated API docs. |
| docs/translations/api-docs/date-pickers/single-input-time-range-field/single-input-time-range-field.json | Removes legacy prop entries from translated API docs. |
| docs/translations/api-docs/date-pickers/single-input-date-time-range-field/single-input-date-time-range-field.json | Removes legacy prop entries from translated API docs. |
| docs/translations/api-docs/date-pickers/single-input-date-range-field/single-input-date-range-field.json | Removes legacy prop entries from translated API docs. |
| docs/translations/api-docs/date-pickers/pickers-text-field/pickers-text-field.json | Adds slots descriptions + removes legacy prop entries in translated docs. |
| docs/translations/api-docs/date-pickers/date-time-field/date-time-field.json | Removes legacy prop entries from translated API docs. |
| docs/translations/api-docs/date-pickers/date-field/date-field.json | Removes legacy prop entries from translated API docs. |
| docs/src/modules/components/overview/pickers/mainDemo/Birthday.tsx | Migrates demo from InputProps to nested slotProps. |
| docs/pages/x/api/date-pickers/time-field.json | Removes legacy prop docs from generated API page JSON. |
| docs/pages/x/api/date-pickers/single-input-time-range-field.json | Removes legacy prop docs from generated API page JSON. |
| docs/pages/x/api/date-pickers/single-input-date-time-range-field.json | Removes legacy prop docs from generated API page JSON. |
| docs/pages/x/api/date-pickers/single-input-date-range-field.json | Removes legacy prop docs from generated API page JSON. |
| docs/pages/x/api/date-pickers/pickers-text-field.json | Adds slots / slotProps API docs + slot list for PickersTextField. |
| docs/pages/x/api/date-pickers/date-time-field.json | Removes legacy prop docs from generated API page JSON. |
| docs/pages/x/api/date-pickers/date-field.json | Removes legacy prop docs from generated API page JSON. |
| docs/data/migration/migration-pickers-v8/migration-pickers-v8.md | Adds migration section + codemod command for dropped legacy props. |
| docs/data/date-pickers/custom-opening-button/AddWarningIconWhenInvalidRange.tsx | Migrates demo from InputProps to slotProps.input. |
| docs/data/date-pickers/custom-opening-button/AddWarningIconWhenInvalidRange.js | Migrates demo from InputProps to slotProps.input. |
| docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx | Migrates Data Grid demo from InputProps to slotProps.input. |
| docs/data/data-grid/custom-columns/EditingWithDatePickers.js | Migrates Data Grid demo from InputProps to slotProps.input. |
Comments suppressed due to low confidence (1)
packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.tsx:466
className={classes.input}can be accidentally removed becausehtmlInputPropsis spread afterwards (and may include its ownclassName). This risks breaking the visually-hidden styling on the underlying<input>. Consider merging class names viauseSlotProps({ ..., className: classes.input })(and relying on the returnedclassName) so internal and external classes are combined.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 50 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Claude Opus 4.6 reviewStrengths
Issues Addressed During Review
Risk Assessment
Merge Readiness Score: 9.5 / 10No outstanding issues. Ready to merge. |
flaviendelangle
left a comment
There was a problem hiding this comment.
PR #22002 Review: [pickers] Refactor PickersTextField to use slotProps approach
Author: LukasTy
Branch: fields/refactor-slot-props -> master
Stats: 51 files changed, +867 / -663
Overview
This PR removes the legacy InputProps, inputProps, InputLabelProps, and FormHelperTextProps props from PickersTextField and all Picker/Field components in favor of a nested slotProps shape that mirrors Material UI v9's TextField API. It also simplifies the cleanFieldResponse / PickerFieldUI logic, adds a new codemod, migration docs, and updated tests.
Overall Assessment: The architecture is sound and the migration is well-structured. There are a few critical issues to address before merge, primarily around the codemod's field slot handling and silent failure scenarios during the transition period.
Critical Issues (must fix before merge)
1. Codemod incorrectly transforms legacy props on the field slot
File: packages/x-codemod/src/v9.0.0/pickers/migrate-text-field-props/index.ts (step 2, ~lines 65-121)
The codemod treats slotProps.field the same as slotProps.textField, but they are at different nesting levels. When a Picker receives slotProps.field.InputProps, the field component (e.g., DateField) would have forwarded that to the text field. The correct transformation needs one additional nesting level.
Current (incorrect) transform:
// Input
<DatePicker slotProps={{ field: { InputProps: { name: 'field-input' } } }} />
// Output (wrong)
<DatePicker slotProps={{ field: { slotProps: { input: { name: 'field-input' } } } }} />Correct transform:
<DatePicker slotProps={{ field: { slotProps: { textField: { slotProps: { input: { name: 'field-input' } } } } } }} />The addItemToObject call for the field case should use slotProps.textField.slotProps.${newKey} instead of slotProps.${newKey}.
2. ref overwrite in PickerFieldUI merge logic
File: packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx (~line 245-256)
The new merge:
textFieldProps.slotProps = {
...textFieldProps.slotProps,
input: { ...externalInputSlotProps, ...additionalInputSlotProps },
};additionalInputSlotProps is spread after externalInputSlotProps, so pickerContext.triggerRef always overwrites any user-provided ref in slotProps.input. Unlike PickersInputBase (which correctly uses useForkRef), there is no ref forking here.
Fix: Use useForkRef to merge the user-provided ref with the picker's trigger ref:
additionalInputSlotProps.ref = useForkRef(externalInputSlotProps?.ref, pickerContext.triggerRef);3. cleanFieldResponse silently leaks InputProps into ...other
File: packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx (~line 25-50)
InputProps was removed from the destructuring of fieldResponse. If any internal or third-party code still produces InputProps in a field response, it now leaks into ...other -> textFieldProps -> gets spread as an unknown prop on the FormControl root. The prop is silently lost with no dev-mode warning.
Fix: Explicitly destructure and discard legacy props, or add a dev-mode warning:
const {
InputProps: _InputProps, // removed - discard explicitly
readOnly,
...other
} = fieldResponse;
if (process.env.NODE_ENV !== 'production' && _InputProps) {
console.warn('MUI X: InputProps is no longer supported. Use slotProps.input instead.');
}Important Issues (should fix)
4. No dev-mode warnings for removed props on PickersTextField
File: packages/x-date-pickers/src/PickersTextField/PickersTextField.tsx
TypeScript users will get compilation errors, but JavaScript users passing legacy props (InputProps, inputProps, etc.) will have them silently ignored. Adding process.env.NODE_ENV !== 'production' warnings would greatly improve the developer experience during migration.
5. useFieldTextFieldProps silently drops legacy props from externalForwardedProps
File: packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx (~line 447)
Before the PR, InputProps and inputProps were destructured from externalForwardedProps and explicitly merged. Now they pass straight through to PickersTextField, which ignores them. JavaScript users of <DateField InputProps={{...}} /> will see their configuration vanish silently.
6. Missing test coverage for direct PickersTextField slot forwarding
The new describe.PickersTextField.test.tsx only does conformance testing. There are no tests verifying that:
slotProps.inputprops reach the underlying input componentslotProps.htmlInputprops reach the<input>elementslotProps.inputLabelandslotProps.formHelperTextare forwarded correctly- Custom
slotsreplacements are actually rendered
The existing DateField/DesktopDatePicker tests cover these paths indirectly (at 2 levels of nesting), but a regression in PickersTextField.tsx itself would not be caught.
7. No test for multi-input range field's slotProps.input forwarding
File: packages/x-date-pickers-pro/src/internals/utils/createMultiInputRangeField/useTextFieldProps.ts
The data-multi-input attribute and pickerContext.triggerRef are now set on slotProps.input instead of InputProps. This path has no unit test.
Suggestions (nice to have)
8. Codemod should warn about untransformable patterns
The codemod silently skips variable references (InputProps={someVar}) and spread patterns. Adding a log message or TODO comment for patterns it cannot transform would help users identify what needs manual migration.
9. Codemod merge logic edge case with spread elements
File: packages/x-codemod/src/util/addComponentsSlots.js
The newKeys set is built via value.properties.map((p) => p.key?.name ?? p.key?.value). If value contains SpreadElement nodes, undefined enters the set, which could cause existing spread elements to be filtered out during deduplication. Consider guarding against this.
10. PickersInputBaseProps slot types
File: packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.types.ts
The new PickersInputBaseSlotProps types are well-structured with concrete interfaces (PickersInputBaseRootSlotProps, PickersInputBaseInputSlotProps). This is a significant improvement over the previous any types. The PickersTextFieldSlotProps<InputPropsType> generic is well-designed -- consider adding a constraint like InputPropsType extends PickersInputBaseProps to prevent nonsensical instantiations.
11. Birthday demo uses wrong nesting level
File: docs/src/modules/components/overview/pickers/mainDemo/Birthday.tsx
The demo wraps InputProps migration inside slotProps.textField.slotProps.input, which is correct for a DateTimePicker. However, the extra nesting depth makes it a less compelling demo. Consider if the demo could use PickersTextField directly to show the simpler slotProps.input path, then show the Picker nesting separately.
Strengths
-
Clean architecture: The slot/slotProps forwarding chain through
PickersTextField-> variant inputs ->PickersInputBaseis correctly implemented with proper merge ordering. -
Proper ref handling in
PickersInputBase: The newhtmlInputslot usesuseSlotPropsanduseForkRefcorrectly -- this is the gold standard thatPickerFieldUIshould follow (see Issue #2). -
Well-structured types:
PickersTextFieldSlots,PickersTextFieldSlotProps<InputPropsType>, andPickersTextFieldSlotsAndSlotProps<InputPropsType>are variant-aware and properly typed. -
Major simplification of
PickerFieldUI: The removal of theresolveComponentProps+delete textFieldProps.slotPropsdance andTODO v9workarounds is a significant reduction in complexity (~70 lines removed fromPickerFieldUI). -
BuiltInFieldTextFieldPropscorrectly omitsslots/slotProps: This prevents field-level slot props from leaking into the text field, fixing a long-standing type safety gap. -
Thorough codemod: Handles both direct JSX attributes and nested
slotProps.textField/slotProps.fieldpatterns, with idempotency testing and preset-safe integration. -
Complete migration guide: Clear mapping table, before/after examples for both
PickersTextFieldand Picker components, codemod instructions, and a caveat about spread/variable patterns. -
Consistent doc and API updates: All 7 field component API docs, translation files, and demo files are properly updated.
Recommended Action
- Fix Critical #1 (codemod
fieldslot nesting) -- this would silently lose user props after migration - Fix Critical #2 (ref forking in PickerFieldUI) -- user refs silently overwritten
- Fix Critical #3 (explicit discard of legacy props in cleanFieldResponse)
- Address Important #4-5 (dev-mode warnings) to smooth the migration experience
- Consider Important #6-7 (test coverage) for confidence in the forwarding chain
- Re-run review after fixes
|
Comments on @flaviendelangle review: Summary of changesCodemod (packages/x-codemod)
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 53 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
packages/x-codemod/src/util/addComponentsSlots.js:81
addItemToObjectstill assumes everyobject.propertiesentry has akey(e.g.property.key.name), which will throw when the object contains spread elements ({ ...rest }) or other non-Property nodes. Since this helper is used by codemods on user code, encountering spreads is common and would crash the codemod. Please guard these accesses (e.g. optional chaining / filtering byProperty/ObjectProperty) in the.find/.filterbranches (both thesplittedPath.length === 1and nested-path cases).
const propertyToAdd = j.objectProperty(j.identifier(path), value);
return j.objectExpression([
...(object.properties ?? []).filter((property) => property.key.name !== path),
propertyToAdd,
]);
}
const remainingPath = splittedPath.slice(1).join('.');
const targetKey = splittedPath[0];
if (object === null) {
// Simplest case, no object to take into consideration
const propertyToAdd = j.objectProperty(
j.identifier(targetKey),
addItemToObject(remainingPath, value, null, j),
);
return j.objectExpression([propertyToAdd]);
}
// Look if the object we got already contains the property we have to use.
const correspondingObject = (object.properties ?? []).find(
(property) => property.key.name === targetKey,
);
const propertyToAdd = j.objectProperty(
j.identifier(targetKey),
// Here we use recursion to mix the new value with the current one
addItemToObject(remainingPath, value, correspondingObject?.value ?? null, j),
);
return j.objectExpression([
...(object.properties ?? []).filter((property) => property.key.name !== targetKey),
propertyToAdd,
]);
packages/x-date-pickers/src/PickersTextField/PickersInputBase/PickersInputBase.types.ts:102
PickersInputBasePropsno longer extendsBoxPropsandcomponentwas removed from the runtime PropTypes. SincePickersInputBase(and the variant inputs that extend it) are exported, this is a potential breaking API change beyond the stated removal ofInputProps/inputProps. If this is intentional, it should be called out in the migration notes/codemod; if not, consider restoringcomponentsupport (and the corresponding typing) to avoid an accidental breaking change.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
It's hard to know if it will actually break someones code |
| textField: { | ||
| slotProps: { | ||
| htmlInput: { 'data-testid': 'html-input' }, | ||
|
|
There was a problem hiding this comment.
We might be missing a prettier run to handle those.
The current actual/expected passes tests.
I'd need to fight with formatting or look at running prettier to handle this.
WDYT about keeping it as is?
|
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
Follow-up on #21966.
Summary
Aligns
PickersTextField(and every Picker / Field consumer) with the Material UI v9TextFieldslots API by removing the legacyInputProps,inputProps,InputLabelProps, andFormHelperTextPropsprops in favor of a nestedslotPropsshape.This finally lets us delete the
cleanFieldResponse/PickerFieldUIremapping logic that branched on which prop shape the user passed —PickersTextFieldnow natively understands the new shape, so the field/picker layer just passes things through.Changes
PickersTextFieldAPIInputProps,inputProps,InputLabelProps,FormHelperTextPropsfromPickersTextField,PickersInputBase,PickersInput,PickersFilledInput, andPickersOutlinedInput— including the internalinputPropsidentifier inPickersInputBase.slots/slotPropsonPickersTextField:slotProps.input→ forwarded to the variant input (PickersInput/PickersFilledInput/PickersOutlinedInput)slotProps.htmlInput→ forwarded as the underlying<input>propsslotProps.inputLabel→ forwarded to<InputLabel />slotProps.formHelperText→ forwarded to<FormHelperText />slotProps.root→ forwarded to theFormControlrootPartial<T>objects (no callback form), matching the previousInputPropsergonomics and avoiding fragile type casts.BuiltInFieldTextFieldPropsnow omitsslots/slotPropsfromPickersTextFieldPropsso that field-levelslotPropsonly exposes field-level slot keys (textField,inputAdornment, …) — passingslotProps={{ input: … }}directly on aDateFieldis now a TypeScript error, with the correct shape beingslotProps={{ textField: { slotProps: { input: … } } }}.PickerFieldUIsimplificationcleanFieldResponseno longer jugglesInputProps↔slotProps.inputorinputProps↔slotProps.htmlInput. It just foldsreadOnlyintoslotProps.inputand forwards everything else.PickerFieldUIbuilds anadditionalInputSlotPropsobject (start/end adornments, trigger ref) and merges it intotextFieldProps.slotProps.inputonce before rendering — the manualresolveComponentProps+delete textFieldProps.slotPropsdance and the// TODO v9workaround are gone.PickerFieldUISlotProps['textField']is now a plainSlotComponentPropsFromProps<PickersTextFieldProps, {}, FieldOwnerState>.useFieldTextFieldPropsno longer readsInputProps/inputPropsfromexternalForwardedProps.Field-level call sites
TimeField,DateField,DateTimeField,SingleInputDate/Time/DateTimeRangeFieldno longer destructure or forward legacy props.createMultiInputRangeField/useTextFieldPropswritestriggerRefanddata-multi-inputintoslotProps.inputinstead ofInputProps.DateFieldandDesktopDatePickerupdated to use the new shape.Migration tooling
v9.0.0/pickers/migrate-text-field-propsthat rewrites the legacy props into the new shape onPickersTextFielddirectly and insideslotProps.field/slotProps.textFieldof Picker/Field components. Built on top of the existingtransformNestedProp/addItemToObject/removePropshelpers.v9.0.0/pickers/preset-safepreset and covered by both the standalone and the preset-safe fixtures.Migration guide
PickersTextFieldprops" section in docs/data/migration/migration-pickers-v8/migration-pickers-v8.md with the prop mapping table, before/after snippets for bothPickersTextFieldand Picker/Field components, and the codemod command.Docs demos
InputProps.Changelog
InputProps) in favor of the nestedslotProps. Read more