feat: make date fields read only
This commit is contained in:
committed by
Benoit TELLIER
parent
d68b4146d3
commit
3025672b49
@@ -84,8 +84,7 @@ describe("DateTimeFields", () => {
|
|||||||
showMore: true,
|
showMore: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarButton = screen.getAllByRole("button");
|
await userEvent.click(screen.getByTestId("start-date-input"));
|
||||||
await userEvent.click(calendarButton[0]);
|
|
||||||
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
||||||
await userEvent.click(dayButton);
|
await userEvent.click(dayButton);
|
||||||
|
|
||||||
@@ -129,8 +128,7 @@ describe("DateTimeFields", () => {
|
|||||||
showMore: true,
|
showMore: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarButton = screen.getAllByRole("button");
|
await userEvent.click(screen.getByTestId("end-date-input"));
|
||||||
await userEvent.click(calendarButton[2]);
|
|
||||||
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
const dayButton = screen.getByRole("gridcell", { name: "3" });
|
||||||
await userEvent.click(dayButton);
|
await userEvent.click(dayButton);
|
||||||
|
|
||||||
@@ -151,8 +149,7 @@ describe("DateTimeFields", () => {
|
|||||||
showEndDate: true,
|
showEndDate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarButton = screen.getAllByRole("button");
|
await userEvent.click(screen.getByTestId("start-date-input"));
|
||||||
await userEvent.click(calendarButton[0]);
|
|
||||||
const dayButton = screen.getByRole("gridcell", { name: "10" });
|
const dayButton = screen.getByRole("gridcell", { name: "10" });
|
||||||
await userEvent.click(dayButton);
|
await userEvent.click(dayButton);
|
||||||
|
|
||||||
@@ -173,8 +170,7 @@ describe("DateTimeFields", () => {
|
|||||||
showEndDate: true,
|
showEndDate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarButton = screen.getAllByRole("button");
|
await userEvent.click(screen.getByTestId("end-date-input"));
|
||||||
await userEvent.click(calendarButton[1]);
|
|
||||||
const dayButton = screen.getByRole("gridcell", { name: "1" });
|
const dayButton = screen.getByRole("gridcell", { name: "1" });
|
||||||
await userEvent.click(dayButton);
|
await userEvent.click(dayButton);
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import "dayjs/locale/vi";
|
|||||||
|
|
||||||
import { PickerValue } from "@mui/x-date-pickers/internals";
|
import { PickerValue } from "@mui/x-date-pickers/internals";
|
||||||
import { dtDate, dtTime, toDateTime } from "../utils/dateTimeHelpers";
|
import { dtDate, dtTime, toDateTime } from "../utils/dateTimeHelpers";
|
||||||
|
import { ReadOnlyDateField } from "./ReadOnlyPickerField";
|
||||||
|
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
@@ -256,6 +257,16 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getFieldSlotProps = (testId: string, hasError = false) => ({
|
||||||
|
size: "small" as const,
|
||||||
|
margin: "dense" as const,
|
||||||
|
fullWidth: true,
|
||||||
|
InputLabelProps: { shrink: true },
|
||||||
|
error: hasError,
|
||||||
|
sx: { width: "100%" },
|
||||||
|
inputProps: { "data-testid": testId },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider
|
<LocalizationProvider
|
||||||
dateAdapter={AdapterDayjs}
|
dateAdapter={AdapterDayjs}
|
||||||
@@ -280,7 +291,11 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDateValue}
|
value={startDateValue}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
slotProps={getSlotProps("start-date-input")}
|
slots={{ field: ReadOnlyDateField }}
|
||||||
|
slotProps={{
|
||||||
|
...getSlotProps("start-date-input"),
|
||||||
|
field: getFieldSlotProps("start-date-input") as any,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{!allday && (
|
{!allday && (
|
||||||
@@ -302,10 +317,14 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={endDateValue}
|
value={endDateValue}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
slotProps={getSlotProps(
|
slots={{ field: ReadOnlyDateField }}
|
||||||
"end-date-input",
|
slotProps={{
|
||||||
!!validation.errors.dateTime
|
...getSlotProps("end-date-input", !!validation.errors.dateTime),
|
||||||
)}
|
field: getFieldSlotProps(
|
||||||
|
"end-date-input",
|
||||||
|
!!validation.errors.dateTime
|
||||||
|
) as any,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{!allday && (
|
{!allday && (
|
||||||
@@ -332,7 +351,11 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDateValue}
|
value={startDateValue}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
slotProps={getSlotProps("start-date-input")}
|
slots={{ field: ReadOnlyDateField }}
|
||||||
|
slotProps={{
|
||||||
|
...getSlotProps("start-date-input"),
|
||||||
|
field: getFieldSlotProps("start-date-input") as any,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
@@ -341,10 +364,14 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={endDateValue}
|
value={endDateValue}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
slotProps={getSlotProps(
|
slots={{ field: ReadOnlyDateField }}
|
||||||
"end-date-input",
|
slotProps={{
|
||||||
!!validation.errors.dateTime
|
...getSlotProps("end-date-input", !!validation.errors.dateTime),
|
||||||
)}
|
field: getFieldSlotProps(
|
||||||
|
"end-date-input",
|
||||||
|
!!validation.errors.dateTime
|
||||||
|
) as any,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -356,7 +383,11 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDateValue}
|
value={startDateValue}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
slotProps={getSlotProps("start-date-input")}
|
slots={{ field: ReadOnlyDateField }}
|
||||||
|
slotProps={{
|
||||||
|
...getSlotProps("start-date-input"),
|
||||||
|
field: getFieldSlotProps("start-date-input") as any,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ maxWidth: "110px" }}>
|
<Box sx={{ maxWidth: "110px" }}>
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import React from "react";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import {
|
||||||
|
PickerFieldAdapter,
|
||||||
|
PickerValidationScope,
|
||||||
|
useValidation,
|
||||||
|
validateDate,
|
||||||
|
} from "@mui/x-date-pickers/validation";
|
||||||
|
import {
|
||||||
|
useSplitFieldProps,
|
||||||
|
useParsedFormat,
|
||||||
|
usePickerContext,
|
||||||
|
} from "@mui/x-date-pickers/hooks";
|
||||||
|
import { PickerFieldProps } from "@mui/x-date-pickers/models";
|
||||||
|
import { DatePickerFieldProps } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
|
||||||
|
type FieldType = "date" | "time" | "date-time";
|
||||||
|
|
||||||
|
type GenericPickerFieldProps = PickerFieldProps<any, any, any> & {
|
||||||
|
fieldType: FieldType;
|
||||||
|
validator: (
|
||||||
|
value: any,
|
||||||
|
context: PickerValidationScope,
|
||||||
|
adapter: PickerFieldAdapter<any>
|
||||||
|
) => string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared read-only field for date/time pickers. Disables typing, removes icon,
|
||||||
|
* and opens the picker when clicking anywhere in the field.
|
||||||
|
*/
|
||||||
|
function ReadOnlyPickerField(props: GenericPickerFieldProps) {
|
||||||
|
const { fieldType, validator, ...fieldProps } = props;
|
||||||
|
const { internalProps, forwardedProps } = useSplitFieldProps(
|
||||||
|
fieldProps,
|
||||||
|
fieldType
|
||||||
|
);
|
||||||
|
|
||||||
|
const pickerContext = usePickerContext();
|
||||||
|
const parsedFormat = useParsedFormat();
|
||||||
|
|
||||||
|
const { hasValidationError } = useValidation({
|
||||||
|
validator,
|
||||||
|
value: pickerContext.value,
|
||||||
|
timezone: pickerContext.timezone,
|
||||||
|
props: internalProps,
|
||||||
|
});
|
||||||
|
|
||||||
|
const valueToDisplay =
|
||||||
|
pickerContext.value == null
|
||||||
|
? ""
|
||||||
|
: pickerContext.value.isValid()
|
||||||
|
? pickerContext.value.format(pickerContext.fieldFormat)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const mergedInputProps = {
|
||||||
|
...forwardedProps.InputProps,
|
||||||
|
ref: pickerContext.triggerRef,
|
||||||
|
readOnly: true,
|
||||||
|
sx: {
|
||||||
|
cursor: "pointer",
|
||||||
|
"& *": { cursor: "inherit" },
|
||||||
|
...forwardedProps.InputProps?.sx,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
{...forwardedProps}
|
||||||
|
value={valueToDisplay}
|
||||||
|
placeholder={parsedFormat as string}
|
||||||
|
InputProps={mergedInputProps}
|
||||||
|
error={hasValidationError}
|
||||||
|
focused={pickerContext.open}
|
||||||
|
onClick={() => pickerContext.setOpen((prev) => !prev)}
|
||||||
|
className={pickerContext.rootClassName}
|
||||||
|
sx={pickerContext.rootSx}
|
||||||
|
ref={pickerContext.rootRef}
|
||||||
|
name={pickerContext.name}
|
||||||
|
label={pickerContext.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadOnlyDateField(props: DatePickerFieldProps) {
|
||||||
|
return (
|
||||||
|
<ReadOnlyPickerField
|
||||||
|
{...props}
|
||||||
|
fieldType="date"
|
||||||
|
validator={validateDate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user