Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | 14x 14x 14x 14x 14x 14x 14x 14x 192x 192x 192x 192x 16x 1673x 761x 14x 842x 842x 842x 215x 215x 215x 1020x | import { InputProps, TextField } from "@mui/material";
import {
DatePicker,
DatePickerProps,
usePickerAdapter,
usePickerContext,
} from "@mui/x-date-pickers";
import { useTranslation } from "i18n";
import { Control, Controller, UseControllerProps } from "react-hook-form";
import { Temporal } from "temporal-polyfill";
import { getMuiDateFormat } from "utils/date";
import dayjs, { Dayjs } from "utils/dayjs";
interface DatepickerProps {
className?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: Control<any>;
defaultValue?: Temporal.PlainDate;
error: boolean;
helperText: React.ReactNode;
id: string;
rules?: UseControllerProps["rules"];
label?: string;
name: string;
minValue?: Temporal.PlainDate;
maxValue?: Temporal.PlainDate;
openTo?: "year" | "month" | "day";
onPostChange?(value: Temporal.PlainDate | null): void;
testId?: string;
variant?: "standard" | "outlined" | "filled";
inputProps?: InputProps;
// When true the field can only be set through the picker: no text input, no
// mask placeholder, clicking anywhere (not just the icon) opens the calendar,
// and the chosen date is shown as a localized long date with the month name
// (e.g. "May 25, 1990"). Otherwise the field is an editable, parseable numeric
// date in the locale's format.
pickerInputOnly?: boolean;
}
interface ReadOnlyDateFieldProps {
className?: string;
id?: string;
label?: React.ReactNode;
error?: boolean;
helperText?: React.ReactNode;
variant?: "standard" | "outlined" | "filled";
ariaLabel: string;
inputProps?: InputProps;
}
// Read-only field used by `pickerInputOnly` pickers. It renders the selected
// date as a localized long date (month name) and is blank when no date is set,
// so there is never an editable section mask or format placeholder. Clicking it
// opens the calendar, which is the only way to set the value.
const ReadOnlyDateField = ({
className,
id,
label,
error,
helperText,
variant,
ariaLabel,
inputProps,
}: ReadOnlyDateFieldProps) => {
const pickerContext = usePickerContext<Dayjs | null>();
const adapter = usePickerAdapter();
const value = pickerContext.value;
// Format through the picker adapter so the long date respects the picker's
// adapterLocale (e.g. German "8. April 1990"), not just dayjs's global locale.
const display = value ? adapter.formatByString(value, "LL") : "";
return (
<TextField
ref={pickerContext.triggerRef}
className={className}
id={id}
label={label}
error={error}
helperText={helperText}
variant={variant}
fullWidth
value={display}
onClick={() => pickerContext.setOpen(true)}
slotProps={{
inputLabel: { shrink: true },
htmlInput: { readOnly: true, "aria-label": ariaLabel },
input: inputProps,
}}
/>
);
};
// Convert between our API's Temporal.PlainDate and MUI's expected Dayjs values.
// Use the browser timezone in case we compare to now, aka dayjs().
function temporalToDayjs(value: Temporal.PlainDate): Dayjs {
return dayjs(value.toString(), "YYYY-MM-DD");
}
function dayjsToTemporal(value: Dayjs): Temporal.PlainDate {
return Temporal.PlainDate.from(value.format("YYYY-MM-DD"));
}
const Datepicker = ({
className,
control,
defaultValue,
error,
helperText,
id,
rules,
label,
minValue = dayjsToTemporal(dayjs()),
maxValue,
name,
openTo = "day",
onPostChange,
testId,
variant = "standard",
inputProps = {},
pickerInputOnly = false,
}: DatepickerProps) => {
const { t, i18n } = useTranslation();
const ariaLabel = t("components.datepicker.change_date");
const helperNode = (
<span data-testid={`${name}-helper-text`}>{helperText}</span>
);
// `pickerInputOnly` swaps the editable masked field for a read-only field slot
// (ReadOnlyDateField) so there is never a format placeholder. ReadOnlyDateField
// deliberately doesn't conform to MUI's injected field props (it reads value
// and open-state from the picker context), so the slot wiring is cast through
// `unknown` — MUI's own custom-field pattern requires this.
const pickerOnlyProps = {
slots: { field: ReadOnlyDateField },
slotProps: {
field: {
className,
id,
label,
error,
helperText: helperNode,
variant,
ariaLabel,
inputProps,
},
},
} as unknown as Partial<DatePickerProps>;
return (
<Controller
control={control}
defaultValue={defaultValue ?? null}
name={name}
rules={rules}
render={({ field }) => (
<DatePicker
data-testid={testId}
{...field}
label={label}
value={field.value ? temporalToDayjs(field.value) : null}
minDate={minValue ? temporalToDayjs(minValue) : undefined}
maxDate={maxValue ? temporalToDayjs(maxValue) : undefined}
onChange={(valueDayjs: Dayjs | null) => {
const valueTemporal =
valueDayjs && valueDayjs.isValid()
? dayjsToTemporal(valueDayjs)
: null;
field.onChange(valueTemporal);
onPostChange?.(valueTemporal);
}}
openTo={openTo}
views={["year", "month", "day"]}
// Picker-only fields show a localized long date with the month name;
// editable fields use the parseable numeric locale format.
format={pickerInputOnly ? "LL" : getMuiDateFormat(i18n.language)}
{...(pickerInputOnly
? pickerOnlyProps
: {
slotProps: {
textField: {
// Apply the consumer className to the field root (FormControl)
// so layout styles like margins wrap the whole field, not the
// input box (which would push the helper text away).
className,
fullWidth: true,
id,
error,
helperText: helperNode,
variant,
slotProps: {
inputLabel: { shrink: true },
},
InputProps: {
...(inputProps || {}),
"aria-label": ariaLabel,
},
},
// Shrink the calendar button so its circular hover/ripple stays
// within the input's content box instead of overlapping the
// (standard variant) underline.
openPickerButton: {
size: "small",
},
},
})}
/>
)}
/>
);
};
export default Datepicker;
|