Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs] New recipe of a read-only field #14606

Merged
merged 40 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
ffda378
[docs] New recipe of a read-only field
flaviendelangle Sep 13, 2024
81099d2
Fix
flaviendelangle Sep 13, 2024
95075dd
Fix
flaviendelangle Sep 13, 2024
92da0c7
Fix
flaviendelangle Sep 13, 2024
b79b5f4
Add onOpen prop
flaviendelangle Sep 13, 2024
c7a1032
Fix types
flaviendelangle Sep 13, 2024
66574c3
Fix types
flaviendelangle Sep 13, 2024
00c6329
Fix
flaviendelangle Sep 13, 2024
cb32485
Merge branch 'master' into readonly-field
flaviendelangle Sep 17, 2024
15fbfec
Improve doc
flaviendelangle Sep 17, 2024
5768e56
Merge branch 'master' into readonly-field
flaviendelangle Sep 20, 2024
26a2135
Try to move onOpen to a context
flaviendelangle Sep 24, 2024
676aa65
Remove TS changes
flaviendelangle Sep 24, 2024
9f5f302
Fix
flaviendelangle Sep 24, 2024
9837ebf
Fix
flaviendelangle Sep 24, 2024
c9b0906
Work
flaviendelangle Sep 24, 2024
9ca0721
Move logic to usePicker
flaviendelangle Sep 24, 2024
1f185cf
Improve typing
flaviendelangle Sep 24, 2024
5f7f8e5
Work
flaviendelangle Sep 24, 2024
7bd2d32
Review Arthur
flaviendelangle Sep 30, 2024
f345cba
Merge branch 'master' into readonly-field
flaviendelangle Sep 30, 2024
299a96c
Work
flaviendelangle Sep 30, 2024
164a1de
Work
flaviendelangle Oct 1, 2024
7adf029
Fix
flaviendelangle Oct 1, 2024
77ef242
Review Lukas
flaviendelangle Oct 4, 2024
a1bc87f
[pickers] Move the DateFieldInPickerProps interface to the picker fol…
flaviendelangle Oct 4, 2024
3642e3c
Fix
flaviendelangle Oct 7, 2024
e6ea1a5
Merge branch 'master' into readonly-field
flaviendelangle Oct 7, 2024
cb91ec7
Merge
flaviendelangle Oct 7, 2024
2365f69
Merge
flaviendelangle Oct 7, 2024
07a0285
Review Lukas + JSDoc for useParsedFormat
flaviendelangle Oct 7, 2024
9d5e300
Replace toggling method with onClose and onOpen
flaviendelangle Oct 7, 2024
065dbfc
Merge branch 'master' into readonly-field
flaviendelangle Oct 7, 2024
3e976f8
Update packages/x-date-pickers/src/internals/components/PickersProvid…
flaviendelangle Oct 7, 2024
f5b950e
Update packages/x-date-pickers/src/internals/components/PickersProvid…
flaviendelangle Oct 7, 2024
80e16b4
Update packages/x-date-pickers/src/internals/hooks/usePicker/usePicke…
flaviendelangle Oct 7, 2024
2fd6a5a
Update packages/x-date-pickers/src/hooks/useParsedFormat.ts
flaviendelangle Oct 7, 2024
9443b65
Update packages/x-date-pickers/src/hooks/usePickersContext.ts
flaviendelangle Oct 7, 2024
018158d
Improve demo
flaviendelangle Oct 7, 2024
70b1bef
Fix
flaviendelangle Oct 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,47 +1,61 @@
import * as React from 'react';
import dayjs from 'dayjs';

import TextField from '@mui/material/TextField';
import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
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 { useValidation, validateDate } from '@mui/x-date-pickers/validation';
import { useSplitFieldProps } from '@mui/x-date-pickers/hooks';
import {
useSplitFieldProps,
useParsedFormat,
usePickersContext,
} from '@mui/x-date-pickers/hooks';

function ReadOnlyField(props) {
function ReadOnlyDateField(props) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');

const { value, timezone, format } = internalProps;
const { InputProps, slotProps, slots, ...other } = forwardedProps;

const pickersContext = usePickersContext();

const parsedFormat = useParsedFormat(internalProps);
const { hasValidationError } = useValidation({
validator: validateDate,
value,
timezone,
props: internalProps,
});

const handleTogglePicker = (event) => {
if (pickersContext.open) {
pickersContext.onClose(event);
} else {
pickersContext.onOpen(event);
}
};

return (
<TextField
{...other}
value={value == null ? '' : value.format(format)}
placeholder={parsedFormat}
InputProps={{ ...InputProps, readOnly: true }}
error={hasValidationError}
onClick={handleTogglePicker}
/>
);
}

export default function CustomField() {
function ReadOnlyFieldDatePicker(props) {
return (
<DatePicker {...props} slots={{ ...props.slots, field: ReadOnlyDateField }} />
);
}

export default function ReadOnlyMaterialTextField() {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DemoContainer components={['DatePicker']}>
<DatePicker
label="Date Picker"
slots={{ field: ReadOnlyField }}
maxDate={dayjs('2022-04-17')}
defaultValue={dayjs('2022-04-18')}
/>
</DemoContainer>
<ReadOnlyFieldDatePicker />
</LocalizationProvider>
);
}
Original file line number Diff line number Diff line change
@@ -1,47 +1,65 @@
import * as React from 'react';
import dayjs, { Dayjs } from 'dayjs';
import { Dayjs } from 'dayjs';
import TextField from '@mui/material/TextField';
import { DemoContainer } from '@mui/x-date-pickers/internals/demo';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { DatePicker, DatePickerFieldProps } from '@mui/x-date-pickers/DatePicker';
import {
DatePicker,
DatePickerProps,
DatePickerFieldProps,
} from '@mui/x-date-pickers/DatePicker';
import { useValidation, validateDate } from '@mui/x-date-pickers/validation';
import { useSplitFieldProps } from '@mui/x-date-pickers/hooks';
import {
useSplitFieldProps,
useParsedFormat,
usePickersContext,
} from '@mui/x-date-pickers/hooks';

function ReadOnlyField(props: DatePickerFieldProps<Dayjs, false>) {
function ReadOnlyDateField(props: DatePickerFieldProps<Dayjs, false>) {
const { internalProps, forwardedProps } = useSplitFieldProps(props, 'date');

const { value, timezone, format } = internalProps;
const { InputProps, slotProps, slots, ...other } = forwardedProps;

const pickersContext = usePickersContext();

const parsedFormat = useParsedFormat(internalProps);
const { hasValidationError } = useValidation({
validator: validateDate,
value,
timezone,
props: internalProps,
});

const handleTogglePicker = (event: React.UIEvent) => {
if (pickersContext.open) {
pickersContext.onClose(event);
} else {
pickersContext.onOpen(event);
}
};

return (
<TextField
{...other}
value={value == null ? '' : value.format(format)}
placeholder={parsedFormat}
InputProps={{ ...InputProps, readOnly: true }}
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
error={hasValidationError}
onClick={handleTogglePicker}
/>
);
}

export default function CustomField() {
function ReadOnlyFieldDatePicker(props: DatePickerProps<Dayjs>) {
return (
<DatePicker {...props} slots={{ ...props.slots, field: ReadOnlyDateField }} />
);
}

export default function ReadOnlyMaterialTextField() {
return (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DemoContainer components={['DatePicker']}>
<DatePicker
label="Date Picker"
slots={{ field: ReadOnlyField }}
maxDate={dayjs('2022-04-17')}
defaultValue={dayjs('2022-04-18')}
/>
</DemoContainer>
<ReadOnlyFieldDatePicker />
</LocalizationProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<ReadOnlyFieldDatePicker />
11 changes: 9 additions & 2 deletions docs/data/date-pickers/custom-field/custom-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,17 @@ you can replace the field with an `Autocomplete` listing those dates:

{{"demo": "PickerWithAutocompleteField.js", "defaultCodeOpen": false}}

### Using a read-only `TextField`

If you want users to select a value exclusively through the views
but you still want the UI to look like a `TextField`, you can replace the field with a read-only `TextField`:

{{"demo": "custom-behavior/ReadOnlyMaterialTextField.js", "defaultCodeOpen": false}}

### Using a `Button`

If you only want to allow the user to pick a value through the views,
you can replace the field with a `Button`:
If you want users to select a value exclusively through the views
and you don't want the UI to look like a `TextField`, you can replace the field with a `Button`:

{{"demo": "PickerWithButtonField.js", "defaultCodeOpen": false}}

Expand Down

This file was deleted.

4 changes: 0 additions & 4 deletions docs/data/date-pickers/experimentation/experimentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,3 @@ productId: x-date-pickers
# Date and Time Pickers experimentation

<p class="description">Demos not accessible through the navbar of the doc</p>

## Custom field

{{"demo": "CustomField.js"}}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import useSlotProps from '@mui/utils/useSlotProps';
import { useLicenseVerifier } from '@mui/x-license';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { PickersLayout, PickersLayoutSlotProps } from '@mui/x-date-pickers/PickersLayout';
import {
executeInTheNextEventLoopTick,
Expand All @@ -11,6 +10,7 @@ import {
ExportedBaseToolbarProps,
DateOrTimeViewWithMeridiem,
ExportedBaseTabsProps,
PickersProvider,
} from '@mui/x-date-pickers/internals';
import { PickerValidDate, FieldRef, InferError } from '@mui/x-date-pickers/models';
import {
Expand Down Expand Up @@ -93,6 +93,7 @@ export const useDesktopRangePicker = <
renderCurrentView,
shouldRestoreFocus,
fieldProps: pickerFieldProps,
contextValue,
} = usePicker<
DateRange<TDate>,
TDate,
Expand Down Expand Up @@ -209,7 +210,7 @@ export const useDesktopRangePicker = <
const Layout = slots?.layout ?? PickersLayout;

const renderPicker = () => (
<LocalizationProvider localeText={localeText}>
<PickersProvider contextValue={contextValue} localeText={localeText}>
<Field {...enrichedFieldProps} />
<PickersPopper
role="tooltip"
Expand All @@ -233,7 +234,7 @@ export const useDesktopRangePicker = <
{renderCurrentView()}
</Layout>
</PickersPopper>
</LocalizationProvider>
</PickersProvider>
);

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as React from 'react';
import useSlotProps from '@mui/utils/useSlotProps';
import { useLicenseVerifier } from '@mui/x-license';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { PickersLayout, PickersLayoutSlotProps } from '@mui/x-date-pickers/PickersLayout';
import {
usePicker,
PickersModalDialog,
ExportedBaseToolbarProps,
DateOrTimeViewWithMeridiem,
ExportedBaseTabsProps,
PickersProvider,
} from '@mui/x-date-pickers/internals';
import { usePickersTranslations } from '@mui/x-date-pickers/hooks';
import { PickerValidDate, FieldRef, InferError } from '@mui/x-date-pickers/models';
Expand Down Expand Up @@ -88,6 +88,7 @@ export const useMobileRangePicker = <
layoutProps,
renderCurrentView,
fieldProps: pickerFieldProps,
contextValue,
} = usePicker<
DateRange<TDate>,
TDate,
Expand Down Expand Up @@ -214,7 +215,7 @@ export const useMobileRangePicker = <
};

const renderPicker = () => (
<LocalizationProvider localeText={localeText}>
<PickersProvider contextValue={contextValue} localeText={localeText}>
<Field {...enrichedFieldProps} />
<PickersModalDialog {...actions} open={open} slots={slots} slotProps={slotProps}>
<Layout
Expand All @@ -226,7 +227,7 @@ export const useMobileRangePicker = <
{renderCurrentView()}
</Layout>
</PickersModalDialog>
</LocalizationProvider>
</PickersProvider>
);

return {
Expand Down
4 changes: 4 additions & 0 deletions packages/x-date-pickers/src/hooks/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ export type {
export { usePickersTranslations } from './usePickersTranslations';

export { useSplitFieldProps } from './useSplitFieldProps';

export { useParsedFormat } from './useParsedFormat';

export { usePickersContext } from './usePickersContext';
61 changes: 61 additions & 0 deletions packages/x-date-pickers/src/hooks/useParsedFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client';
import * as React from 'react';
import { useRtl } from '@mui/system/RtlProvider';
import { useUtils } from '../internals/hooks/useUtils';
import { buildSectionsFromFormat } from '../internals/hooks/useField/buildSectionsFromFormat';
import { getLocalizedDigits } from '../internals/hooks/useField/useField.utils';
import { PickerValidDate } from '../models';
import { usePickersTranslations } from './usePickersTranslations';
import type { UseFieldInternalProps } from '../internals/hooks/useField';

interface UseParsedFormatParameters
extends Pick<
UseFieldInternalProps<any, any, any, any, any>,
'format' | 'formatDensity' | 'shouldRespectLeadingZeros'
> {}

/**
* Returns the parsed format to be rendered in the field when their is no value or in other parts of the picker.
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
* This format is localized (e.g: `AAAA` for the year with the French locale) and cannot be parsed by your date library.
* @param {object} The parameters needed to build the placeholder.
* @param {string} params.format Format of the date to use.
* @param {'dense' | 'spacious'} params.formatDensity Density of the format (setting `formatDensity` to `"spacious"` will add a space before and after each `/`, `-` and `.` character).
* @param {boolean} params.shouldRespectLeadingZeros If `true`, the format will respect the leading zeroes, if `false`, the format will always add leading zeroes.
* @returns
*/
export const useParsedFormat = <TDate extends PickerValidDate>(
parameters: UseParsedFormatParameters,
) => {
const { format, formatDensity = 'dense', shouldRespectLeadingZeros = false } = parameters;
const utils = useUtils<TDate>();
const isRtl = useRtl();
const translations = usePickersTranslations<TDate>();
const localizedDigits = React.useMemo(() => getLocalizedDigits(utils), [utils]);

return React.useMemo(() => {
const sections = buildSectionsFromFormat({
utils,
format,
formatDensity,
isRtl,
shouldRespectLeadingZeros,
localeText: translations,
localizedDigits,
date: null,
// TODO v9: Make sure we still don't reverse in `buildSectionsFromFormat` when using `useParsedFormat`.
enableAccessibleFieldDOMStructure: false,
});

return sections
.map((section) => `${section.startSeparator}${section.placeholder}${section.endSeparator}`)
.join('');
}, [
utils,
isRtl,
translations,
localizedDigits,
format,
formatDensity,
shouldRespectLeadingZeros,
]);
};
19 changes: 19 additions & 0 deletions packages/x-date-pickers/src/hooks/usePickersContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';
import * as React from 'react';
import { PickersContext } from '../internals/components/PickersProvider';

/**
* Returns the context passed by the picker the wraps the current component.
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
*/
export const usePickersContext = () => {
const value = React.useContext(PickersContext);
if (value == null) {
throw new Error(
[
'MUI X: The `usePickersContext` can only be called in fields that are used as a slot of a picker component',
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved
].join('\n'),
);
}

return value;
};
Loading