Skip to content

[pickers] Refactor PickersTextField to use slotProps approach#22002

Merged
LukasTy merged 28 commits into
mui:masterfrom
LukasTy:fields/refactor-slot-props
Apr 8, 2026
Merged

[pickers] Refactor PickersTextField to use slotProps approach#22002
LukasTy merged 28 commits into
mui:masterfrom
LukasTy:fields/refactor-slot-props

Conversation

@LukasTy
Copy link
Copy Markdown
Member

@LukasTy LukasTy commented Apr 7, 2026

Follow-up on #21966.

Summary

Aligns PickersTextField (and every Picker / Field consumer) with the Material UI v9 TextField slots API by removing the legacy InputProps, inputProps, InputLabelProps, and FormHelperTextProps props in favor of a nested slotProps shape.

This finally lets us delete the cleanFieldResponse / PickerFieldUI remapping logic that branched on which prop shape the user passed — PickersTextField now natively understands the new shape, so the field/picker layer just passes things through.

Changes

PickersTextField API

  • Removed InputProps, inputProps, InputLabelProps, FormHelperTextProps from PickersTextField, PickersInputBase, PickersInput, PickersFilledInput, and PickersOutlinedInput — including the internal inputProps identifier in PickersInputBase.
  • Added Material v9-style slots / slotProps on PickersTextField:
    • slotProps.input → forwarded to the variant input (PickersInput / PickersFilledInput / PickersOutlinedInput)
    • slotProps.htmlInput → forwarded as the underlying <input> props
    • slotProps.inputLabel → forwarded to <InputLabel />
    • slotProps.formHelperText → forwarded to <FormHelperText />
    • slotProps.root → forwarded to the FormControl root
  • Slot props are intentionally typed as plain Partial<T> objects (no callback form), matching the previous InputProps ergonomics and avoiding fragile type casts.
  • BuiltInFieldTextFieldProps now omits slots / slotProps from PickersTextFieldProps so that field-level slotProps only exposes field-level slot keys (textField, inputAdornment, …) — passing slotProps={{ input: … }} directly on a DateField is now a TypeScript error, with the correct shape being slotProps={{ textField: { slotProps: { input: … } } }}.

PickerFieldUI simplification

  • cleanFieldResponse no longer juggles InputPropsslotProps.input or inputPropsslotProps.htmlInput. It just folds readOnly into slotProps.input and forwards everything else.
  • PickerFieldUI builds an additionalInputSlotProps object (start/end adornments, trigger ref) and merges it into textFieldProps.slotProps.input once before rendering — the manual resolveComponentProps + delete textFieldProps.slotProps dance and the // TODO v9 workaround are gone.
  • PickerFieldUISlotProps['textField'] is now a plain SlotComponentPropsFromProps<PickersTextFieldProps, {}, FieldOwnerState>.
  • useFieldTextFieldProps no longer reads InputProps / inputProps from externalForwardedProps.

Field-level call sites

  • TimeField, DateField, DateTimeField, SingleInputDate/Time/DateTimeRangeField no longer destructure or forward legacy props.
  • createMultiInputRangeField/useTextFieldProps writes triggerRef and data-multi-input into slotProps.input instead of InputProps.
  • Tests under DateField and DesktopDatePicker updated to use the new shape.

Migration tooling

  • New codemod v9.0.0/pickers/migrate-text-field-props that rewrites the legacy props into the new shape on PickersTextField directly and inside slotProps.field / slotProps.textField of Picker/Field components. Built on top of the existing transformNestedProp / addItemToObject / removeProps helpers.
  • Wired into the v9.0.0/pickers/preset-safe preset and covered by both the standalone and the preset-safe fixtures.
  • Documented in packages/x-codemod/README.md.

Migration guide

Docs demos

Changelog

  • Removed the legacy Pickers and Field TextField props (for example: InputProps) in favor of the nested slotProps. Read more

@LukasTy LukasTy self-assigned this Apr 7, 2026
@LukasTy LukasTy added breaking change Introduces changes that are not backward compatible. scope: pickers Changes related to the date/time pickers. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. v9.x labels Apr 7, 2026
@mui-bot
Copy link
Copy Markdown

mui-bot commented Apr 7, 2026

Deploy preview: https://deploy-preview-22002--material-ui-x.netlify.app/

Updated pages:

Bundle size report

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 🔺+390B(+0.18%) 🔺+108B(+0.19%)
@mui/x-date-pickers-pro 🔺+399B(+0.13%) 🔺+132B(+0.17%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against ee871e8

@LukasTy
Copy link
Copy Markdown
Member Author

LukasTy commented Apr 7, 2026

Overview

Follow-up to #21966. Removes the already-deprecated legacy InputProps, inputProps, InputLabelProps, and FormHelperTextProps from PickersTextField and every Picker/Field consumer, replacing them with a nested slots/slotProps API that mirrors Material UI v9 TextField. The big payoff is that cleanFieldResponse and PickerFieldUI no longer juggle two prop shapes — the branching, resolveComponentProps dance, and // TODO v9 workaround are deleted.

Ships with a v9.0.0/pickers/migrate-text-field-props codemod (wired into preset-safe) and a migration-guide section.

Strengths

  • Great cleanup win. PickerFieldUI.tsx loses ~60 lines of conditional shape juggling and three delete statements; cleanFieldResponse is now a straight passthrough plus readOnly. The // TODO v9 comments and the noop = () => {} placeholder are gone.
  • Codemod is thorough. Covers direct JSX attrs on 25+ components, nested slotProps.field / slotProps.textField, the PickersTextField special case (keys at root), and includes an idempotency test. Known limitations (spreads / variable refs) are explicitly documented.
  • Migration guide has a clear mapping table, before/after snippets for both direct PickersTextField and Picker/Field shapes, and the codemod command.
  • Typed slot props at the top level: PickersTextFieldSlotProps<InputPropsType> is nicely parameterized by variant so slotProps.input narrows to the correct underlying input type.
  • BuiltInFieldTextFieldProps now omits slots | slotProps from PickersTextFieldProps — this is the right call, it stops slotProps={{ input: … }} from accidentally type-checking on a DateField.

Issues

🔴 Potential bug — user slots/slotProps under slotProps.input get clobbered

In PickersTextField.tsx (around the new <PickersInputComponent> call):

<PickersInputComponent
  
  {...inputAdditionalProps}
  {...inputSlotProps}
  slots={{ htmlInput: slots?.htmlInput }}
  slotProps={{ htmlInput: slotProps?.htmlInput }}
/>

inputSlotProps is typed as Partial<PickersInputProps> (or Filled/Outlined), which extends PickersInputBaseProps — so it legitimately can carry slots and slotProps keys. The trailing slots={…} / slotProps={…} JSX attributes then fully replace anything spread from inputSlotProps (JSX spread is last-wins, not merged). A caller doing:

<PickersTextField
  slotProps={{
    input: {
      slotProps: { root: { disableUnderline: true } },
    },
  }}
/>

will silently lose their nested slot props. Suggest merging:

slots={{ ...inputSlotProps?.slots, htmlInput: slots?.htmlInput }}
slotProps={{ ...inputSlotProps?.slotProps, htmlInput: slotProps?.htmlInput }}

(or using mergeSlotProps). If this collision is intentional — because you want htmlInput to be a top-level-only concern — the type of slotProps.input should probably Omit slots/slotProps to make the constraint visible.

🟡 PickersInputBaseSlotProps.htmlInput uses the wrong HTML interface

// PickersInputBase.types.ts
export interface PickersInputBaseSlotProps {
  root?: any;
  input?: any;
  htmlInput?: React.HTMLAttributes<HTMLInputElement> & { ref?: React.Ref<HTMLInputElement> };
}

This should be React.InputHTMLAttributes<HTMLInputElement> (as used at the top-level PickersTextFieldSlotProps.htmlInput). HTMLAttributes omits input-specific attrs (value, defaultValue, name, disabled, checked, etc.), so typing slotProps.htmlInput.name on PickersInputBase won't check. The public PickersTextField type is correct; only this internal one drifts.

🟡 Test/type gap — as any had to be added for data-testid

Both DateField.test.tsx and DesktopDatePicker.test.tsx now need:

htmlInput: { 'data-testid': 'test-html-input' } as any,

data-* attributes should be valid on InputHTMLAttributes via React's [key: \data-${string}`]pattern. Theas anyhere is a smell — worth figuring out whether it's thePartialwrapping or the& { ref }` intersection breaking the React index signature, and fixing the type rather than casting in tests (tests are the canonical "does this type feel good" smoke test).

🟡 root?: any / input?: any in PickersInputBaseSlotProps

The PR description explicitly calls these out as "plain Partial<T> objects … avoiding fragile type casts," but what landed in PickersInputBase.types.ts is any, not Partial<SomeProps>. That erases type safety entirely for anyone overriding these through PickersInputBase. Even Partial<React.ComponentProps<typeof PickersInputBaseRoot>> / Partial<PickersInputBaseSectionsContainerProps> would be strictly better, and would surface the test-side data-testid typing issue consistently.

🟢 Minor — no-op spread in PickersOutlinedInput

slotProps={{
  ...incomingSlotProps,
  root: { ...incomingSlotProps?.root },
  input: { ...incomingSlotProps?.input },
}}

The two inner spreads just reconstruct the same object already present via ...incomingSlotProps. You can drop them (or, symmetrically with PickersFilledInput, add the variant-specific defaults that the outlined variant currently has none of).

🟢 Minor — DateField.test.tsx lost direct coverage of slotProps.input

The removed it('should respect the InputProps', …) wasn't replaced with a direct slotProps.textField.slotProps.input assertion on DateField. DesktopDatePicker has it, so it's covered at the picker layer, but adding the field-level case back would be cheap insurance and mirrors the old intent.

🟢 Minor — codemod step 2 is component-agnostic

The second pass (root.find(j.JSXAttribute, { name: { name: 'slotProps' } })) rewrites legacy keys inside any slotProps.field / slotProps.textField regardless of the enclosing JSX element — so a user component that happens to have a slotProps.field.InputProps path gets touched too. Low risk given the key names, but worth a docs note, or gating on the component name as step 1 does.

🟢 Nit — PickersTextField slot order

{...other} {...slotProps?.root} on RootComponent means slotProps.root silently wins over any FormControl prop forwarded via other (e.g. disabled, error). This matches v9 semantics, so it's fine — just worth a sentence in the migration guide so consumers don't get surprised.

Risk Assessment

  • Correctness: one likely-real bug (htmlInput slot clobber) and one type inconsistency (HTMLAttributes vs InputHTMLAttributes). Both are contained and fixable.
  • Breaking change: intentional and well-documented; codemod covers the 95% path.
  • Performance: neutral; if anything slightly better (fewer resolveComponentProps / mergeSlotProps calls in the hot PickerFieldUI path).
  • Security: n/a.
  • Tests: browser + jsdom tests updated; codemod has both standalone and preset-safe fixtures plus an idempotency test. Consider adding the one missing DateField case and removing the as any once the typing is fixed.

Recommendation

Request changes on the htmlInput slot merge (PickersTextField.tsx) and the HTMLAttributesInputHTMLAttributes typing in PickersInputBase.types.ts — both are small, targeted fixes. The rest are polish. The refactor itself is a solid simplification and the migration tooling is in good shape.

@LukasTy LukasTy marked this pull request as ready for review April 7, 2026 12:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-style slots / slotProps.
  • Simplifies PickerFieldUI + field/picker call sites to forward slotProps directly.
  • Adds a v9.0.0/pickers/migrate-text-field-props codemod 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 because htmlInputProps is spread afterwards (and may include its own className). This risks breaking the visually-hidden styling on the underlying <input>. Consider merging class names via useSlotProps({ ..., className: classes.input }) (and relying on the returned className) so internal and external classes are combined.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/x-date-pickers/src/PickersTextField/PickersTextField.tsx Outdated
Comment thread packages/x-date-pickers/src/PickersTextField/PickersTextField.tsx Outdated
Comment thread packages/x-date-pickers/src/PickersTextField/PickersTextField.tsx Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@LukasTy
Copy link
Copy Markdown
Member Author

LukasTy commented Apr 7, 2026

Claude Opus 4.6 review

Strengths

  • Major simplificationcleanFieldResponse is now a clean passthrough (just folds readOnly into slotProps.input). The resolveComponentProps dance, delete textFieldProps.slotProps, noop = () => {}, and all // TODO v9 workarounds in PickerFieldUI are deleted (~60 lines).
  • Correct slot forwarding — In PickersTextField.tsx, user inputSlotProps.slots/inputSlotProps.slotProps are spread first, then the top-level htmlInput overlays. No clobbering.
  • Well-typed slot systemPickersTextFieldSlotProps<InputPropsType> narrows slotProps.input per variant. PickersInputBaseSlotProps uses concrete types (PickersInputBaseRootSlotProps, PickersInputBaseInputSlotProps, React.ComponentPropsWithRef<'input'>). BuiltInFieldTextFieldProps omits slots | slotProps to prevent slotProps={{ input: … }} from type-checking on DateField.
  • Thorough codemod — Covers direct JSX attributes on 25+ components, nested slotProps.field/slotProps.textField rewriting, idempotency test. After our fix, it correctly merges into existing slotProps properties instead of replacing them.
  • Migration docs — Clear mapping table, before/after snippets for both PickersTextField and Picker/Field, codemod command, and known limitations (spread/variable refs).
  • Conformance test — New describe.PickersTextField.test.tsx added.
  • as any for data-testid — Correct. React's TS types don't include an index signature for data-* attributes on element props; the cast is the standard workaround.

Issues Addressed During Review

Issue Resolution
Codemod silently dropped existing slotProps.input on PickersTextField when InputProps was also present Fixed in addItemToObject — leaf-level ObjectExpression merge now preserves existing properties (new wins on key conflicts). Test fixture updated.
Missing field-level slotProps.textField.slotProps.input test after removal of InputProps test Added in DateField.test.tsx — asserts name propagates through the nested path.

Risk Assessment

Dimension Rating
Architecture / design Excellent — clean MUI v9 alignment
Type safety Excellent — concrete slot prop types throughout
Migration tooling Excellent (after fix) — merges, doesn't clobber
Documentation Excellent
Test coverage Excellent (after fix) — field + picker + codemod + conformance
Performance Neutral to slightly better
Security N/A

Merge Readiness Score: 9.5 / 10

No outstanding issues. Ready to merge.

Copy link
Copy Markdown
Member

@flaviendelangle flaviendelangle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.input props reach the underlying input component
  • slotProps.htmlInput props reach the <input> element
  • slotProps.inputLabel and slotProps.formHelperText are forwarded correctly
  • Custom slots replacements 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

  1. Clean architecture: The slot/slotProps forwarding chain through PickersTextField -> variant inputs -> PickersInputBase is correctly implemented with proper merge ordering.

  2. Proper ref handling in PickersInputBase: The new htmlInput slot uses useSlotProps and useForkRef correctly -- this is the gold standard that PickerFieldUI should follow (see Issue #2).

  3. Well-structured types: PickersTextFieldSlots, PickersTextFieldSlotProps<InputPropsType>, and PickersTextFieldSlotsAndSlotProps<InputPropsType> are variant-aware and properly typed.

  4. Major simplification of PickerFieldUI: The removal of the resolveComponentProps + delete textFieldProps.slotProps dance and TODO v9 workarounds is a significant reduction in complexity (~70 lines removed from PickerFieldUI).

  5. BuiltInFieldTextFieldProps correctly omits slots/slotProps: This prevents field-level slot props from leaking into the text field, fixing a long-standing type safety gap.

  6. Thorough codemod: Handles both direct JSX attributes and nested slotProps.textField / slotProps.field patterns, with idempotency testing and preset-safe integration.

  7. Complete migration guide: Clear mapping table, before/after examples for both PickersTextField and Picker components, codemod instructions, and a caveat about spread/variable patterns.

  8. Consistent doc and API updates: All 7 field component API docs, translation files, and demo files are properly updated.


Recommended Action

  1. Fix Critical #1 (codemod field slot nesting) -- this would silently lose user props after migration
  2. Fix Critical #2 (ref forking in PickerFieldUI) -- user refs silently overwritten
  3. Fix Critical #3 (explicit discard of legacy props in cleanFieldResponse)
  4. Address Important #4-5 (dev-mode warnings) to smooth the migration experience
  5. Consider Important #6-7 (test coverage) for confidence in the forwarding chain
  6. Re-run review after fixes

@LukasTy
Copy link
Copy Markdown
Member Author

LukasTy commented Apr 8, 2026

Comments on @flaviendelangle review:

Summary of changes

Codemod (packages/x-codemod)

  • field slot rewrite (migrate-text-field-props/index.ts) — legacy props found under slotProps.field are no longer nested back inside field (TS doesn't allow slotProps.field.slotProps.textField.slotProps.input). They are hoisted to the sibling slotProps.textField.slotProps.<newKey>, and field is dropped if it becomes empty. Test fixture updated.
  • Untransformable-pattern warnings — codemod now console.warns (with the file path) when it sees a spread inside slotProps or a non-literal slotProps.field/textField value, so users know to migrate manually.
  • Spread-element guard in addComponentsSlots.js — undefined keys (from ...rest) no longer poison the dedup Set, so existing spreads survive merges.

PickerFieldUI (PickerFieldUI.tsx)

  • Explicit discard of legacy props in cleanFieldResponse so they can't leak into ...other and end up spread on the FormControl root. Dev-mode console.warn fires if any are present.
  • Ref forkingslotProps.input.ref is now forked with pickerContext.triggerRef via useForkRef instead of being silently overwritten.
  • useFieldTextFieldProps sanitization — legacy props are stripped from externalForwardedProps before reaching PickersTextField, with a dev warning.

PickersTextField (PickersTextField.tsx)

  • Dev-mode console.warn when JS users pass legacy InputProps/inputProps/InputLabelProps/FormHelperTextProps.

Types (PickersTextField.types.ts)

  • PickersTextFieldSlotProps<InputPropsType extends PickersInputBaseProps> constraint added (also propagated to PickersTextFieldSlotsAndSlotProps). tsc clean for both x-date-pickers and x-date-pickers-pro.

Tests

  • PickersTextField.test.tsx — direct forwarding tests for slotProps.htmlInput, slotProps.inputLabel, slotProps.formHelperText, plus a custom slots.formHelperText replacement.
  • MultiInputDateRangeField.slotProps.test.tsx — verifies data-multi-input is set on each input and that slotProps.textField.slotProps.input is forwarded without stripping the internal marker.

Points not addressed (and why)

  • Review point 11 (Birthday demo nesting) — intentionally left as-is. The current slotProps.textField.slotProps.input shape is the expected approach for a field component, so flattening would misrepresent the path users actually need.
  • slots.root test inside PickersTextField.test.tsx — dropped after writing it. PickersInputBase throws MUI X: PickersInputBase should always be used inside a PickersTextField component at runtime via useFormControl(), so a non-FormControl custom root is unsupported by design. Coverage of slots.root would require a FormControl-compatible wrapper, which doesn't add meaningful safety beyond what the conformance test already provides.
  • Custom slots.input test — not added. The variant input (PickersInput/PickersOutlinedInput/PickersFilledInput) is tightly coupled to internal props (elements, sectionListRef, etc.); a stub replacement would either ignore them or duplicate PickersInputBase plumbing. The conformance test plus the new htmlInput/inputLabel/formHelperText tests already cover the forwarding chain at the boundaries that matter.

☝️ WDYT @flaviendelangle, maybe we shouldn't add the slots alongside the slotProps at all, at least until there is enough valid arguments for doing it?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • addItemToObject still assumes every object.properties entry has a key (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 by Property/ObjectProperty) in the .find / .filter branches (both the splittedPath.length === 1 and 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

  • PickersInputBaseProps no longer extends BoxProps and component was removed from the runtime PropTypes. Since PickersInputBase (and the variant inputs that extend it) are exported, this is a potential breaking API change beyond the stated removal of InputProps/inputProps. If this is intentional, it should be called out in the migration notes/codemod; if not, consider restoring component support (and the corresponding typing) to avoid an accidental breaking change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/x-date-pickers/src/PickersTextField/PickersTextField.tsx
Comment thread packages/x-date-pickers/src/internals/components/PickerFieldUI.tsx
@flaviendelangle
Copy link
Copy Markdown
Member

☝️ WDYT @flaviendelangle, maybe we shouldn't add the slots alongside the slotProps at all, at least until there is enough valid arguments for doing it?

It's hard to know if it will actually break someones code
I'm fine moving forward and being reactive with folow ups with needs be

Copy link
Copy Markdown
Member

@michelengelen michelengelen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just 2 nits... LGTM

textField: {
slotProps: {
htmlInput: { 'data-testid': 'html-input' },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Apr 8, 2026
@siriwatknp siriwatknp mentioned this pull request Apr 8, 2026
13 tasks
@LukasTy LukasTy merged commit ea2c9d1 into mui:master Apr 8, 2026
21 checks passed
@LukasTy LukasTy deleted the fields/refactor-slot-props branch April 8, 2026 10:28
arminmeh pushed a commit to arminmeh/mui-x that referenced this pull request Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Introduces changes that are not backward compatible. scope: pickers Changes related to the date/time pickers. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. v9.x

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants