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]
|
||||
);
|
||||
|
||||
const getSlotProps = (testId: string, hasError = false) => ({
|
||||
const getSlotProps = (testId: string, hasError = false, testLabel?: string) => ({
|
||||
textField: {
|
||||
size: "small" as const,
|
||||
margin: "dense" as const,
|
||||
@@ -253,18 +253,24 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
InputLabelProps: { shrink: true },
|
||||
error: hasError,
|
||||
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,
|
||||
margin: "dense" as const,
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
error: hasError,
|
||||
sx: { width: "100%" },
|
||||
inputProps: { "data-testid": testId },
|
||||
inputProps: {
|
||||
"data-testid": testId,
|
||||
...(testLabel ? { "aria-label": testLabel } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
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 (
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDayjs}
|
||||
@@ -301,33 +321,41 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.startDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={startDateValue}
|
||||
onChange={handleStartDateChange}
|
||||
slots={{ field: ReadOnlyDateField }}
|
||||
slotProps={{
|
||||
...getSlotProps("start-date-input"),
|
||||
field: getFieldSlotProps("start-date-input") as any,
|
||||
...getSlotProps("start-date-input", false, t("dateTimeFields.startDate")),
|
||||
field: getFieldSlotProps("start-date-input", false, t("dateTimeFields.startDate")) as any,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{!allday && (
|
||||
<Box sx={{ width: "110px" }}>
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.startTime")}
|
||||
ampm={false}
|
||||
value={startTimeValue}
|
||||
onChange={handleStartTimeChange}
|
||||
onChange={handleStartTimeChangeWithClose}
|
||||
open={isStartTimeOpen}
|
||||
onClose={() => setIsStartTimeOpen(false)}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{ actionBar: () => null }}
|
||||
slotProps={{
|
||||
...getSlotProps("start-time-input"),
|
||||
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")),
|
||||
openPickerButton: { sx: { display: "none" } },
|
||||
popper: {
|
||||
sx: {
|
||||
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
},
|
||||
},
|
||||
},
|
||||
textField: {
|
||||
...getSlotProps("start-time-input").textField,
|
||||
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")).textField,
|
||||
onClick: (e) =>
|
||||
handleTimeInputClick(e, setIsStartTimeOpen),
|
||||
sx: {
|
||||
@@ -344,7 +372,6 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.endDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={endDateValue}
|
||||
onChange={handleEndDateChange}
|
||||
@@ -352,11 +379,13 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
slotProps={{
|
||||
...getSlotProps(
|
||||
"end-date-input",
|
||||
!!validation.errors.dateTime
|
||||
!!validation.errors.dateTime,
|
||||
t("dateTimeFields.endDate")
|
||||
),
|
||||
field: getFieldSlotProps(
|
||||
"end-date-input",
|
||||
!!validation.errors.dateTime
|
||||
!!validation.errors.dateTime,
|
||||
t("dateTimeFields.endDate")
|
||||
) as any,
|
||||
}}
|
||||
/>
|
||||
@@ -364,24 +393,35 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
{!allday && (
|
||||
<Box sx={{ width: "110px" }}>
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.endTime")}
|
||||
ampm={false}
|
||||
value={endTimeValue}
|
||||
onChange={handleEndTimeChange}
|
||||
onChange={handleEndTimeChangeWithClose}
|
||||
open={isEndTimeOpen}
|
||||
onClose={() => setIsEndTimeOpen(false)}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{ actionBar: () => null }}
|
||||
slotProps={{
|
||||
...getSlotProps(
|
||||
"end-time-input",
|
||||
!!validation.errors.dateTime
|
||||
!!validation.errors.dateTime,
|
||||
t("dateTimeFields.endTime")
|
||||
),
|
||||
openPickerButton: { sx: { display: "none" } },
|
||||
popper: {
|
||||
sx: {
|
||||
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
},
|
||||
},
|
||||
},
|
||||
textField: {
|
||||
...getSlotProps(
|
||||
"end-time-input",
|
||||
!!validation.errors.dateTime
|
||||
!!validation.errors.dateTime,
|
||||
t("dateTimeFields.endTime")
|
||||
).textField,
|
||||
onClick: (e) =>
|
||||
handleTimeInputClick(e, setIsEndTimeOpen),
|
||||
@@ -401,20 +441,18 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.startDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={startDateValue}
|
||||
onChange={handleStartDateChange}
|
||||
slots={{ field: ReadOnlyDateField }}
|
||||
slotProps={{
|
||||
...getSlotProps("start-date-input"),
|
||||
field: getFieldSlotProps("start-date-input") as any,
|
||||
...getSlotProps("start-date-input", false, t("dateTimeFields.startDate")),
|
||||
field: getFieldSlotProps("start-date-input", false, t("dateTimeFields.startDate")) as any,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||
<DatePicker
|
||||
label={t("dateTimeFields.endDate")}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={endDateValue}
|
||||
onChange={handleEndDateChange}
|
||||
@@ -422,11 +460,13 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
slotProps={{
|
||||
...getSlotProps(
|
||||
"end-date-input",
|
||||
!!validation.errors.dateTime
|
||||
!!validation.errors.dateTime,
|
||||
t("dateTimeFields.endDate")
|
||||
),
|
||||
field: getFieldSlotProps(
|
||||
"end-date-input",
|
||||
!!validation.errors.dateTime
|
||||
!!validation.errors.dateTime,
|
||||
t("dateTimeFields.endDate")
|
||||
) as any,
|
||||
}}
|
||||
/>
|
||||
@@ -436,33 +476,41 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||
<Box sx={{ maxWidth: "300px", width: "48%" }}>
|
||||
<DatePicker
|
||||
label={startDateLabel}
|
||||
format={LONG_DATE_FORMAT}
|
||||
value={startDateValue}
|
||||
onChange={handleStartDateChange}
|
||||
slots={{ field: ReadOnlyDateField }}
|
||||
slotProps={{
|
||||
...getSlotProps("start-date-input"),
|
||||
field: getFieldSlotProps("start-date-input") as any,
|
||||
...getSlotProps("start-date-input", false, startDateLabel),
|
||||
field: getFieldSlotProps("start-date-input", false, startDateLabel) as any,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ maxWidth: "110px" }}>
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.startTime")}
|
||||
ampm={false}
|
||||
value={startTimeValue}
|
||||
onChange={handleStartTimeChange}
|
||||
onChange={handleStartTimeChangeWithClose}
|
||||
disabled={allday}
|
||||
open={isStartTimeOpen}
|
||||
onClose={() => setIsStartTimeOpen(false)}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{ actionBar: () => null }}
|
||||
slotProps={{
|
||||
...getSlotProps("start-time-input"),
|
||||
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")),
|
||||
openPickerButton: { sx: { display: "none" } },
|
||||
popper: {
|
||||
sx: {
|
||||
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
},
|
||||
},
|
||||
},
|
||||
textField: {
|
||||
...getSlotProps("start-time-input").textField,
|
||||
...getSlotProps("start-time-input", false, t("dateTimeFields.startTime")).textField,
|
||||
onClick: (e) => handleTimeInputClick(e, setIsStartTimeOpen),
|
||||
sx: {
|
||||
"& .MuiPickersSectionList-section": {
|
||||
@@ -475,15 +523,15 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
</Box>
|
||||
<Box sx={{ maxWidth: "110px" }}>
|
||||
<TimePicker
|
||||
label={t("dateTimeFields.endTime")}
|
||||
ampm={false}
|
||||
value={endTimeValue}
|
||||
onChange={handleEndTimeChange}
|
||||
onChange={handleEndTimeChangeWithClose}
|
||||
disabled={allday}
|
||||
open={isEndTimeOpen}
|
||||
onClose={() => setIsEndTimeOpen(false)}
|
||||
thresholdToRenderTimeInASingleColumn={48}
|
||||
timeSteps={{ minutes: 30 }}
|
||||
slots={{ actionBar: () => null }}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: "small",
|
||||
@@ -491,7 +539,10 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
fullWidth: true,
|
||||
InputLabelProps: { shrink: true },
|
||||
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),
|
||||
sx: {
|
||||
width: "100%",
|
||||
@@ -501,6 +552,15 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
||||
},
|
||||
},
|
||||
openPickerButton: { sx: { display: "none" } },
|
||||
popper: {
|
||||
sx: {
|
||||
"& .MuiMultiSectionDigitalClockSection-item": {
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -77,7 +77,6 @@ function ReadOnlyPickerField(props: GenericPickerFieldProps) {
|
||||
sx={pickerContext.rootSx}
|
||||
ref={pickerContext.rootRef}
|
||||
name={pickerContext.name}
|
||||
label={pickerContext.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user