feat: remove labels from date/time fields and auto-close TimePicker dropdown
- Remove label prop from all DatePicker and TimePicker components - Add aria-label to inputProps for accessibility and testing - Auto-close TimePicker dropdown when selecting an option - Add 0.1s delay before calling time change handlers after closing dropdown - Add test case to verify aria-label attributes
This commit is contained in:
committed by
Benoit TELLIER
parent
e4d3722992
commit
8b88faffe2
@@ -241,4 +241,24 @@ describe("DateTimeFields", () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should have aria-label for accessibility and testing", async () => {
|
||||||
|
await renderField({
|
||||||
|
startDate: "2025-01-01",
|
||||||
|
startTime: "10:00",
|
||||||
|
endDate: "2025-01-01",
|
||||||
|
endTime: "11:00",
|
||||||
|
showMore: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const startDateInput = screen.getByTestId("start-date-input");
|
||||||
|
const startTimeInput = screen.getByTestId("start-time-input");
|
||||||
|
const endDateInput = screen.getByTestId("end-date-input");
|
||||||
|
const endTimeInput = screen.getByTestId("end-time-input");
|
||||||
|
|
||||||
|
expect(startDateInput).toHaveAttribute("aria-label", "dateTimeFields.startDate");
|
||||||
|
expect(startTimeInput).toHaveAttribute("aria-label", "dateTimeFields.startTime");
|
||||||
|
expect(endDateInput).toHaveAttribute("aria-label", "dateTimeFields.endDate");
|
||||||
|
expect(endTimeInput).toHaveAttribute("aria-label", "dateTimeFields.endTime");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
[endTime]
|
[endTime]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getSlotProps = (testId: string, hasError = false) => ({
|
const getSlotProps = (testId: string, hasError = false, testLabel?: string) => ({
|
||||||
textField: {
|
textField: {
|
||||||
size: "small" as const,
|
size: "small" as const,
|
||||||
margin: "dense" as const,
|
margin: "dense" as const,
|
||||||
@@ -253,18 +253,24 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
InputLabelProps: { shrink: true },
|
InputLabelProps: { shrink: true },
|
||||||
error: hasError,
|
error: hasError,
|
||||||
sx: { width: "100%" },
|
sx: { width: "100%" },
|
||||||
inputProps: { "data-testid": testId },
|
inputProps: {
|
||||||
|
"data-testid": testId,
|
||||||
|
...(testLabel ? { "aria-label": testLabel } : {}),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getFieldSlotProps = (testId: string, hasError = false) => ({
|
const getFieldSlotProps = (testId: string, hasError = false, testLabel?: string) => ({
|
||||||
size: "small" as const,
|
size: "small" as const,
|
||||||
margin: "dense" as const,
|
margin: "dense" as const,
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
InputLabelProps: { shrink: true },
|
InputLabelProps: { shrink: true },
|
||||||
error: hasError,
|
error: hasError,
|
||||||
sx: { width: "100%" },
|
sx: { width: "100%" },
|
||||||
inputProps: { "data-testid": testId },
|
inputProps: {
|
||||||
|
"data-testid": testId,
|
||||||
|
...(testLabel ? { "aria-label": testLabel } : {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isStartTimeOpen, setIsStartTimeOpen] = React.useState(false);
|
const [isStartTimeOpen, setIsStartTimeOpen] = React.useState(false);
|
||||||
@@ -281,6 +287,20 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStartTimeChangeWithClose = (value: PickerValue) => {
|
||||||
|
setIsStartTimeOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
handleStartTimeChange(value);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndTimeChangeWithClose = (value: PickerValue) => {
|
||||||
|
setIsEndTimeOpen(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
handleEndTimeChange(value);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider
|
<LocalizationProvider
|
||||||
dateAdapter={AdapterDayjs}
|
dateAdapter={AdapterDayjs}
|
||||||
@@ -301,33 +321,41 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label={t("dateTimeFields.startDate")}
|
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDateValue}
|
value={startDateValue}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
slots={{ field: ReadOnlyDateField }}
|
slots={{ field: ReadOnlyDateField }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps("start-date-input"),
|
...getSlotProps("start-date-input", false, t("dateTimeFields.startDate")),
|
||||||
field: getFieldSlotProps("start-date-input") as any,
|
field: getFieldSlotProps("start-date-input", false, t("dateTimeFields.startDate")) as any,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{!allday && (
|
{!allday && (
|
||||||
<Box sx={{ width: "110px" }}>
|
<Box sx={{ width: "110px" }}>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
label={t("dateTimeFields.startTime")}
|
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={startTimeValue}
|
value={startTimeValue}
|
||||||
onChange={handleStartTimeChange}
|
onChange={handleStartTimeChangeWithClose}
|
||||||
open={isStartTimeOpen}
|
open={isStartTimeOpen}
|
||||||
onClose={() => setIsStartTimeOpen(false)}
|
onClose={() => setIsStartTimeOpen(false)}
|
||||||
thresholdToRenderTimeInASingleColumn={48}
|
thresholdToRenderTimeInASingleColumn={48}
|
||||||
timeSteps={{ minutes: 30 }}
|
timeSteps={{ minutes: 30 }}
|
||||||
|
slots={{ actionBar: () => null }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps("start-time-input"),
|
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")),
|
||||||
openPickerButton: { sx: { display: "none" } },
|
openPickerButton: { sx: { display: "none" } },
|
||||||
|
popper: {
|
||||||
|
sx: {
|
||||||
|
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
textField: {
|
textField: {
|
||||||
...getSlotProps("start-time-input").textField,
|
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")).textField,
|
||||||
onClick: (e) =>
|
onClick: (e) =>
|
||||||
handleTimeInputClick(e, setIsStartTimeOpen),
|
handleTimeInputClick(e, setIsStartTimeOpen),
|
||||||
sx: {
|
sx: {
|
||||||
@@ -344,7 +372,6 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label={t("dateTimeFields.endDate")}
|
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={endDateValue}
|
value={endDateValue}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
@@ -352,11 +379,13 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps(
|
...getSlotProps(
|
||||||
"end-date-input",
|
"end-date-input",
|
||||||
!!validation.errors.dateTime
|
!!validation.errors.dateTime,
|
||||||
|
t("dateTimeFields.endDate")
|
||||||
),
|
),
|
||||||
field: getFieldSlotProps(
|
field: getFieldSlotProps(
|
||||||
"end-date-input",
|
"end-date-input",
|
||||||
!!validation.errors.dateTime
|
!!validation.errors.dateTime,
|
||||||
|
t("dateTimeFields.endDate")
|
||||||
) as any,
|
) as any,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -364,24 +393,35 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
{!allday && (
|
{!allday && (
|
||||||
<Box sx={{ width: "110px" }}>
|
<Box sx={{ width: "110px" }}>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
label={t("dateTimeFields.endTime")}
|
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={endTimeValue}
|
value={endTimeValue}
|
||||||
onChange={handleEndTimeChange}
|
onChange={handleEndTimeChangeWithClose}
|
||||||
open={isEndTimeOpen}
|
open={isEndTimeOpen}
|
||||||
onClose={() => setIsEndTimeOpen(false)}
|
onClose={() => setIsEndTimeOpen(false)}
|
||||||
thresholdToRenderTimeInASingleColumn={48}
|
thresholdToRenderTimeInASingleColumn={48}
|
||||||
timeSteps={{ minutes: 30 }}
|
timeSteps={{ minutes: 30 }}
|
||||||
|
slots={{ actionBar: () => null }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps(
|
...getSlotProps(
|
||||||
"end-time-input",
|
"end-time-input",
|
||||||
!!validation.errors.dateTime
|
!!validation.errors.dateTime,
|
||||||
|
t("dateTimeFields.endTime")
|
||||||
),
|
),
|
||||||
openPickerButton: { sx: { display: "none" } },
|
openPickerButton: { sx: { display: "none" } },
|
||||||
|
popper: {
|
||||||
|
sx: {
|
||||||
|
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
textField: {
|
textField: {
|
||||||
...getSlotProps(
|
...getSlotProps(
|
||||||
"end-time-input",
|
"end-time-input",
|
||||||
!!validation.errors.dateTime
|
!!validation.errors.dateTime,
|
||||||
|
t("dateTimeFields.endTime")
|
||||||
).textField,
|
).textField,
|
||||||
onClick: (e) =>
|
onClick: (e) =>
|
||||||
handleTimeInputClick(e, setIsEndTimeOpen),
|
handleTimeInputClick(e, setIsEndTimeOpen),
|
||||||
@@ -401,20 +441,18 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label={t("dateTimeFields.startDate")}
|
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDateValue}
|
value={startDateValue}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
slots={{ field: ReadOnlyDateField }}
|
slots={{ field: ReadOnlyDateField }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps("start-date-input"),
|
...getSlotProps("start-date-input", false, t("dateTimeFields.startDate")),
|
||||||
field: getFieldSlotProps("start-date-input") as any,
|
field: getFieldSlotProps("start-date-input", false, t("dateTimeFields.startDate")) as any,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label={t("dateTimeFields.endDate")}
|
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={endDateValue}
|
value={endDateValue}
|
||||||
onChange={handleEndDateChange}
|
onChange={handleEndDateChange}
|
||||||
@@ -422,11 +460,13 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps(
|
...getSlotProps(
|
||||||
"end-date-input",
|
"end-date-input",
|
||||||
!!validation.errors.dateTime
|
!!validation.errors.dateTime,
|
||||||
|
t("dateTimeFields.endDate")
|
||||||
),
|
),
|
||||||
field: getFieldSlotProps(
|
field: getFieldSlotProps(
|
||||||
"end-date-input",
|
"end-date-input",
|
||||||
!!validation.errors.dateTime
|
!!validation.errors.dateTime,
|
||||||
|
t("dateTimeFields.endDate")
|
||||||
) as any,
|
) as any,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -436,33 +476,41 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label={startDateLabel}
|
|
||||||
format={LONG_DATE_FORMAT}
|
format={LONG_DATE_FORMAT}
|
||||||
value={startDateValue}
|
value={startDateValue}
|
||||||
onChange={handleStartDateChange}
|
onChange={handleStartDateChange}
|
||||||
slots={{ field: ReadOnlyDateField }}
|
slots={{ field: ReadOnlyDateField }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps("start-date-input"),
|
...getSlotProps("start-date-input", false, startDateLabel),
|
||||||
field: getFieldSlotProps("start-date-input") as any,
|
field: getFieldSlotProps("start-date-input", false, startDateLabel) as any,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ maxWidth: "110px" }}>
|
<Box sx={{ maxWidth: "110px" }}>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
label={t("dateTimeFields.startTime")}
|
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={startTimeValue}
|
value={startTimeValue}
|
||||||
onChange={handleStartTimeChange}
|
onChange={handleStartTimeChangeWithClose}
|
||||||
disabled={allday}
|
disabled={allday}
|
||||||
open={isStartTimeOpen}
|
open={isStartTimeOpen}
|
||||||
onClose={() => setIsStartTimeOpen(false)}
|
onClose={() => setIsStartTimeOpen(false)}
|
||||||
thresholdToRenderTimeInASingleColumn={48}
|
thresholdToRenderTimeInASingleColumn={48}
|
||||||
timeSteps={{ minutes: 30 }}
|
timeSteps={{ minutes: 30 }}
|
||||||
|
slots={{ actionBar: () => null }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
...getSlotProps("start-time-input"),
|
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")),
|
||||||
openPickerButton: { sx: { display: "none" } },
|
openPickerButton: { sx: { display: "none" } },
|
||||||
|
popper: {
|
||||||
|
sx: {
|
||||||
|
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
textField: {
|
textField: {
|
||||||
...getSlotProps("start-time-input").textField,
|
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")).textField,
|
||||||
onClick: (e) => handleTimeInputClick(e, setIsStartTimeOpen),
|
onClick: (e) => handleTimeInputClick(e, setIsStartTimeOpen),
|
||||||
sx: {
|
sx: {
|
||||||
"& .MuiPickersSectionList-section": {
|
"& .MuiPickersSectionList-section": {
|
||||||
@@ -475,15 +523,15 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ maxWidth: "110px" }}>
|
<Box sx={{ maxWidth: "110px" }}>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
label={t("dateTimeFields.endTime")}
|
|
||||||
ampm={false}
|
ampm={false}
|
||||||
value={endTimeValue}
|
value={endTimeValue}
|
||||||
onChange={handleEndTimeChange}
|
onChange={handleEndTimeChangeWithClose}
|
||||||
disabled={allday}
|
disabled={allday}
|
||||||
open={isEndTimeOpen}
|
open={isEndTimeOpen}
|
||||||
onClose={() => setIsEndTimeOpen(false)}
|
onClose={() => setIsEndTimeOpen(false)}
|
||||||
thresholdToRenderTimeInASingleColumn={48}
|
thresholdToRenderTimeInASingleColumn={48}
|
||||||
timeSteps={{ minutes: 30 }}
|
timeSteps={{ minutes: 30 }}
|
||||||
|
slots={{ actionBar: () => null }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
textField: {
|
textField: {
|
||||||
size: "small",
|
size: "small",
|
||||||
@@ -491,7 +539,10 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
InputLabelProps: { shrink: true },
|
InputLabelProps: { shrink: true },
|
||||||
error: !!validation.errors.dateTime,
|
error: !!validation.errors.dateTime,
|
||||||
inputProps: { "data-testid": "end-time-input" },
|
inputProps: {
|
||||||
|
"data-testid": "end-time-input",
|
||||||
|
"aria-label": t("dateTimeFields.endTime"),
|
||||||
|
},
|
||||||
onClick: (e) => handleTimeInputClick(e, setIsEndTimeOpen),
|
onClick: (e) => handleTimeInputClick(e, setIsEndTimeOpen),
|
||||||
sx: {
|
sx: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -501,6 +552,15 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
openPickerButton: { sx: { display: "none" } },
|
openPickerButton: { sx: { display: "none" } },
|
||||||
|
popper: {
|
||||||
|
sx: {
|
||||||
|
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ function ReadOnlyPickerField(props: GenericPickerFieldProps) {
|
|||||||
sx={pickerContext.rootSx}
|
sx={pickerContext.rootSx}
|
||||||
ref={pickerContext.rootRef}
|
ref={pickerContext.rootRef}
|
||||||
name={pickerContext.name}
|
name={pickerContext.name}
|
||||||
label={pickerContext.label}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user