From 8b88faffe24503284bda754da7095b2eb4a2d797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Nh=C3=A2n=20Ph=E1=BB=A5ng?= Date: Fri, 5 Dec 2025 16:25:50 +0700 Subject: [PATCH] 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 --- __test__/components/DateTimeField.test.tsx | 20 +++ .../Event/components/DateTimeFields.tsx | 128 +++++++++++++----- .../Event/components/ReadOnlyPickerField.tsx | 1 - 3 files changed, 114 insertions(+), 35 deletions(-) diff --git a/__test__/components/DateTimeField.test.tsx b/__test__/components/DateTimeField.test.tsx index 95e4de8..3ad2448 100644 --- a/__test__/components/DateTimeField.test.tsx +++ b/__test__/components/DateTimeField.test.tsx @@ -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"); + }); }); diff --git a/src/components/Event/components/DateTimeFields.tsx b/src/components/Event/components/DateTimeFields.tsx index 86fccfe..496b19a 100644 --- a/src/components/Event/components/DateTimeFields.tsx +++ b/src/components/Event/components/DateTimeFields.tsx @@ -245,7 +245,7 @@ export const DateTimeFields: React.FC = ({ [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 = ({ 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 = ({ } }; + const handleStartTimeChangeWithClose = (value: PickerValue) => { + setIsStartTimeOpen(false); + setTimeout(() => { + handleStartTimeChange(value); + }, 100); + }; + + const handleEndTimeChangeWithClose = (value: PickerValue) => { + setIsEndTimeOpen(false); + setTimeout(() => { + handleEndTimeChange(value); + }, 100); + }; + return ( = ({ {!allday && ( 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 = ({ = ({ 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 = ({ {!allday && ( 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 = ({ = ({ 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 = ({ 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 = ({ setIsEndTimeOpen(false)} thresholdToRenderTimeInASingleColumn={48} timeSteps={{ minutes: 30 }} + slots={{ actionBar: () => null }} slotProps={{ textField: { size: "small", @@ -491,7 +539,10 @@ export const DateTimeFields: React.FC = ({ 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 = ({ }, }, openPickerButton: { sx: { display: "none" } }, + popper: { + sx: { + "& .MuiMultiSectionDigitalClockSection-item": { + justifyContent: "flex-start", + width: "100%", + textAlign: "left", + }, + }, + }, }} /> diff --git a/src/components/Event/components/ReadOnlyPickerField.tsx b/src/components/Event/components/ReadOnlyPickerField.tsx index 9ddd4cb..2c7a0b3 100644 --- a/src/components/Event/components/ReadOnlyPickerField.tsx +++ b/src/components/Event/components/ReadOnlyPickerField.tsx @@ -77,7 +77,6 @@ function ReadOnlyPickerField(props: GenericPickerFieldProps) { sx={pickerContext.rootSx} ref={pickerContext.rootRef} name={pickerContext.name} - label={pickerContext.label} /> ); }