diff --git a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js index 2c7f4751d7e87..bcae377944961 100644 --- a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js +++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.js @@ -131,7 +131,7 @@ function MaskedDateField(props) { return ( diff --git a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx index 918c1c302ef32..30d42fcf110d3 100644 --- a/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx +++ b/docs/data/date-pickers/custom-field/behavior-masked-text-field/MaskedMaterialTextField.tsx @@ -135,7 +135,7 @@ function MaskedDateField(props: DatePickerFieldProps) { return ( diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js new file mode 100644 index 0000000000000..3c6631b19ee00 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.js @@ -0,0 +1,88 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import TextField from '@mui/material/TextField'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { + useSplitFieldProps, + useParsedFormat, + usePickerContext, +} from '@mui/x-date-pickers/hooks'; +import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; + +function CustomDateField(props) { + // TextField does not support slots and slotProps before `@mui/material` v6.0 + const { slots, slotProps, ...other } = props; + const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date'); + + const pickerContext = usePickerContext(); + const placeholder = useParsedFormat(); + const [inputValue, setInputValue] = useInputValue(); + + // Check if the current value is valid or not. + const { hasValidationError } = useValidation({ + value: pickerContext.value, + timezone: pickerContext.timezone, + props: internalProps, + validator: validateDate, + }); + + const handleChange = (event) => { + const newInputValue = event.target.value; + const newValue = dayjs(newInputValue, pickerContext.fieldFormat); + setInputValue(newInputValue); + pickerContext.setValue(newValue); + }; + + return ( + + ); +} + +function useInputValue() { + const pickerContext = usePickerContext(); + const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value); + const [inputValue, setInputValue] = React.useState(() => + createInputValue(pickerContext.value, pickerContext.fieldFormat), + ); + + if (lastValueProp !== pickerContext.value) { + setLastValueProp(pickerContext.value); + if (pickerContext.value && pickerContext.value.isValid()) { + setInputValue( + createInputValue(pickerContext.value, pickerContext.fieldFormat), + ); + } + } + + return [inputValue, setInputValue]; +} + +function createInputValue(value, format) { + if (value == null) { + return ''; + } + + return value.isValid() ? value.format(format) : ''; +} + +function CustomFieldDatePicker(props) { + return ( + + ); +} + +export default function MaterialDatePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx new file mode 100644 index 0000000000000..9d91655805fc5 --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import TextField from '@mui/material/TextField'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { + DatePicker, + DatePickerProps, + DatePickerFieldProps, +} from '@mui/x-date-pickers/DatePicker'; +import { + useSplitFieldProps, + useParsedFormat, + usePickerContext, +} from '@mui/x-date-pickers/hooks'; +import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; + +function CustomDateField(props: DatePickerFieldProps) { + // TextField does not support slots and slotProps before `@mui/material` v6.0 + const { slots, slotProps, ...other } = props; + const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date'); + + const pickerContext = usePickerContext(); + const placeholder = useParsedFormat(); + const [inputValue, setInputValue] = useInputValue(); + + // Check if the current value is valid or not. + const { hasValidationError } = useValidation({ + value: pickerContext.value, + timezone: pickerContext.timezone, + props: internalProps, + validator: validateDate, + }); + + const handleChange = (event: React.ChangeEvent) => { + const newInputValue = event.target.value; + const newValue = dayjs(newInputValue, pickerContext.fieldFormat); + setInputValue(newInputValue); + pickerContext.setValue(newValue); + }; + + return ( + + ); +} + +function useInputValue() { + const pickerContext = usePickerContext(); + const [lastValueProp, setLastValueProp] = React.useState(pickerContext.value); + const [inputValue, setInputValue] = React.useState(() => + createInputValue(pickerContext.value, pickerContext.fieldFormat), + ); + + if (lastValueProp !== pickerContext.value) { + setLastValueProp(pickerContext.value); + if (pickerContext.value && pickerContext.value.isValid()) { + setInputValue( + createInputValue(pickerContext.value, pickerContext.fieldFormat), + ); + } + } + + return [inputValue, setInputValue] as const; +} + +function createInputValue(value: Dayjs | null, format: string) { + if (value == null) { + return ''; + } + + return value.isValid() ? value.format(format) : ''; +} + +function CustomFieldDatePicker(props: DatePickerProps) { + return ( + + ); +} + +export default function MaterialDatePicker() { + return ( + + + + ); +} diff --git a/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx.preview b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx.preview new file mode 100644 index 0000000000000..63be53d3e536f --- /dev/null +++ b/docs/data/date-pickers/custom-field/behavior-tutorial/MaterialDatePicker.tsx.preview @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/data/date-pickers/custom-field/custom-field.md b/docs/data/date-pickers/custom-field/custom-field.md index e30850de8502d..44d13054ceb40 100644 --- a/docs/data/date-pickers/custom-field/custom-field.md +++ b/docs/data/date-pickers/custom-field/custom-field.md @@ -154,3 +154,157 @@ and you don't want the UI to look like a Text Field, you can replace the field w The same logic can be applied to any Range Picker: {{"demo": "behavior-button/MaterialDateRangePicker.js", "defaultCodeOpen": false}} + +## Build your own custom field + +:::success +The sections below show how to build a field for your Picker. +Unlike the field components exposed by `@mui/x-date-pickers` and `@mui/x-date-pickers-pro`, those fields are not suitable for a standalone usage. +::: + +### Typing + +Each Picker component exposes an interface describing the props it passes to its field. +You can import it from the same endpoint as the Picker component and use it to type the props of your field: + +```tsx +import { DatePickerFieldProps } from '@mui/x-date-pickers/DatePicker'; +import { DateRangePickerFieldProps } from '@mui/x-date-pickers-pro/DateRangePicker'; + +function CustomDateField(props: DatePickerFieldProps) { + // Your custom field +} + +function CustomDateRangeField(props: DateRangePickerFieldProps) { + // Your custom field +} +``` + +#### Import + +| Picker component | Field props interface | +| ---------------------: | :------------------------------ | +| Date Picker | `DatePickerFieldProps` | +| Time Picker | `TimePickerFieldProps` | +| Date Time Picker | `DateTimePickerFieldProps` | +| Date Range Picker | `DateRangePickerFieldProps` | +| Date Time Range Picker | `DateTimeRangePickerFieldProps` | + +### Validation + +You can use the `useValidation` hook to check if the current value passed to your field is valid or not: + +```ts +import { useValidation, validateDate } from '@mui/x-date-pickers/validation'; + +const { + // The error associated with the current value. + // For example: "minDate" if `props.value < props.minDate`. + validationError, + // `true` if the value is invalid. + // On range Pickers it is true if the start date or the end date is invalid. + hasValidationError, + // Imperatively get the error of a value. + getValidationErrorForNewValue, +} = useValidation({ + // If you have a value in an internal state, you should pass it here. + // Otherwise, you can pass the value returned by `usePickerContext()`. + value, + timezone, + props, + validator: validateDate, +}); +``` + +#### Import + +Each Picker component has a validator adapted to its value type: + +| Picker component | Import validator | +| ---------------------: | :--------------------------------------------------------------------------- | +| Date Picker | `import { validateDate } from '@mui/x-date-pickers/validation'` | +| Time Picker | `import { validateTime } from '@mui/x-date-pickers/validation'` | +| Date Time Picker | `import { validateDateTime } from '@mui/x-date-pickers/validation'` | +| Date Range Picker | `import { validateDateRange } from '@mui/x-date-pickers-pro/validation'` | +| Date Time Range Picker | `import { validateDateTimeRange } from '@mui/x-date-pickers-pro/validation'` | + +### Localized placeholder + +You can use the `useParsedFormat` to get a clean placeholder. +This hook applies two main transformations on the format: + +1. It replaces all the localized tokens (for example `L` for a date with `dayjs`) with their expanded value (`DD/MM/YYYY` for the same date with `dayjs`). +2. It replaces each token with its token from the localization object (for example `YYYY` remains `YYYY` for the English locale but becomes `AAAA` for the French locale). + +:::warning +The format returned by `useParsedFormat` cannot be parsed by your date library. +::: + +```js +import { useParsedFormat } from '@mui/x-date-pickers/hooks'; + +// Uses the format defined by your Picker +const parsedFormat = useParsedFormat(); + +// Uses the custom format provided +const parsedFormat = useParsedFormat({ format: 'MM/DD/YYYY' }); +``` + +### Spread props to the DOM + +The field receives a lot of props that cannot be forwarded to the DOM element without warnings. +You can use the `useSplitFieldProps` hook to get the props that can be forwarded safely to the DOM: + +```tsx +const { internalProps, forwardedProps } = useSplitFieldProps( + // The props received by the field component + props, + // The value type ("date", "time" or "date-time") + 'date', +); + +return ( + +) +``` + +:::success +The `forwardedProps` contain props like `slots`, `slotProps` and `sx` that are specific to MUI. +You can omit them if the component your are forwarding the props to does not support those concepts: + +```jsx +const { slots, slotProps, sx, ...other } = props; +const { internalProps, forwardedProps } = useSplitFieldProps(other, 'date'); + +return ( + +) +``` + +::: + +### Pass the field to the Picker + +You can pass your custom field to your Picker using the `field` slot: + +```tsx +function DatePickerWithCustomField() { + return ( + + ) +} + +// Also works with the other variants of the component +function DesktopDatePickerWithCustomField() { + return ( + + ) +} + +``` + +### Full custom example + +Here is a live demo of the example created in all the previous sections: + +{{"demo": "behavior-tutorial/MaterialDatePicker.js", "defaultCodeOpen": false}}