feat(ui): replace end date field with '...' button toggle
- Hide end date by default; reveal via '...' button in both modes - Keep state/validation/API logic unchanged - Normalize labels to use MUI TextField labels across modes - Align button with fields; constrain widths for expanded mode
This commit is contained in:
committed by
Benoit TELLIER
parent
5078128e10
commit
61358c081a
@@ -38,6 +38,7 @@ import { FieldWithLabel } from "./components/FieldWithLabel";
|
|||||||
import { DateTimeFields } from "./components/DateTimeFields";
|
import { DateTimeFields } from "./components/DateTimeFields";
|
||||||
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
import { useAllDayToggle } from "./hooks/useAllDayToggle";
|
||||||
import { splitDateTime, combineDateTime } from "./utils/dateTimeHelpers";
|
import { splitDateTime, combineDateTime } from "./utils/dateTimeHelpers";
|
||||||
|
import { getEndDateForStartChange } from "./utils/dateRules";
|
||||||
import {
|
import {
|
||||||
formatLocalDateTime,
|
formatLocalDateTime,
|
||||||
formatDateTimeInTimezone,
|
formatDateTimeInTimezone,
|
||||||
@@ -163,6 +164,9 @@ export default function EventFormFields({
|
|||||||
const [endDate, setEndDate] = React.useState("");
|
const [endDate, setEndDate] = React.useState("");
|
||||||
const [endTime, setEndTime] = React.useState("");
|
const [endTime, setEndTime] = React.useState("");
|
||||||
|
|
||||||
|
// UI state for showing/hiding end date field
|
||||||
|
const [showEndDate, setShowEndDate] = React.useState(false);
|
||||||
|
|
||||||
// Use all-day toggle hook
|
// Use all-day toggle hook
|
||||||
const { originalTimeRef, handleAllDayToggle } = useAllDayToggle({
|
const { originalTimeRef, handleAllDayToggle } = useAllDayToggle({
|
||||||
allday,
|
allday,
|
||||||
@@ -268,13 +272,25 @@ export default function EventFormFields({
|
|||||||
(newDate: string) => {
|
(newDate: string) => {
|
||||||
setStartDate(newDate);
|
setStartDate(newDate);
|
||||||
const newStart = combineDateTime(newDate, startTime);
|
const newStart = combineDateTime(newDate, startTime);
|
||||||
|
|
||||||
|
// Update start
|
||||||
if (onStartChange) {
|
if (onStartChange) {
|
||||||
onStartChange(newStart);
|
onStartChange(newStart);
|
||||||
} else {
|
} else {
|
||||||
setStart(newStart);
|
setStart(newStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rule: update end date when start date changes
|
||||||
|
const nextEndDate = getEndDateForStartChange(newDate, allday);
|
||||||
|
setEndDate(nextEndDate);
|
||||||
|
const newEnd = combineDateTime(nextEndDate, endTime || startTime);
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd);
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[startTime, onStartChange, setStart]
|
[startTime, endTime, onStartChange, onEndChange, setStart, setEnd, allday]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleStartTimeChange = React.useCallback(
|
const handleStartTimeChange = React.useCallback(
|
||||||
@@ -343,6 +359,35 @@ export default function EventFormFields({
|
|||||||
onValidationChange?.(validation.isValid);
|
onValidationChange?.(validation.isValid);
|
||||||
}, [validateForm, onValidationChange]);
|
}, [validateForm, onValidationChange]);
|
||||||
|
|
||||||
|
// Auto-calculate end date from start date if not already set
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (startDate && !endDate) {
|
||||||
|
setEndDate(startDate);
|
||||||
|
const newEnd = combineDateTime(startDate, endTime || startTime);
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd);
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [startDate, endDate, startTime, endTime, onEndChange, setEnd]);
|
||||||
|
|
||||||
|
// Auto-calculate end time from start time (+ 1 hour) if not already set
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (startTime && !endTime && !allday) {
|
||||||
|
const [hours, minutes] = startTime.split(":");
|
||||||
|
const endHour = (parseInt(hours) + 1) % 24;
|
||||||
|
const calculatedEndTime = `${endHour.toString().padStart(2, "0")}:${minutes}`;
|
||||||
|
setEndTime(calculatedEndTime);
|
||||||
|
const newEnd = combineDateTime(endDate || startDate, calculatedEndTime);
|
||||||
|
if (onEndChange) {
|
||||||
|
onEndChange(newEnd);
|
||||||
|
} else {
|
||||||
|
setEnd(newEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [startTime, endTime, allday, endDate, startDate, onEndChange, setEnd]);
|
||||||
|
|
||||||
const validation = validateForm();
|
const validation = validateForm();
|
||||||
|
|
||||||
const handleAddVideoConference = () => {
|
const handleAddVideoConference = () => {
|
||||||
@@ -465,6 +510,8 @@ export default function EventFormFields({
|
|||||||
onStartTimeChange={handleStartTimeChange}
|
onStartTimeChange={handleStartTimeChange}
|
||||||
onEndDateChange={handleEndDateChange}
|
onEndDateChange={handleEndDateChange}
|
||||||
onEndTimeChange={handleEndTimeChange}
|
onEndTimeChange={handleEndTimeChange}
|
||||||
|
showEndDate={showEndDate}
|
||||||
|
onToggleEndDate={() => setShowEndDate(!showEndDate)}
|
||||||
/>
|
/>
|
||||||
</FieldWithLabel>
|
</FieldWithLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, IconButton, Tooltip, Typography } from "@mui/material";
|
||||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
|
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
|
||||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||||
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
|
||||||
|
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +17,8 @@ export interface DateTimeFieldsProps {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
allday: boolean;
|
allday: boolean;
|
||||||
showMore: boolean;
|
showMore: boolean;
|
||||||
|
showEndDate: boolean;
|
||||||
|
onToggleEndDate: () => void;
|
||||||
validation: {
|
validation: {
|
||||||
errors: {
|
errors: {
|
||||||
dateTime: string;
|
dateTime: string;
|
||||||
@@ -38,6 +41,8 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
endTime,
|
endTime,
|
||||||
allday,
|
allday,
|
||||||
showMore,
|
showMore,
|
||||||
|
showEndDate,
|
||||||
|
onToggleEndDate,
|
||||||
validation,
|
validation,
|
||||||
onStartDateChange,
|
onStartDateChange,
|
||||||
onStartTimeChange,
|
onStartTimeChange,
|
||||||
@@ -46,18 +51,18 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en">
|
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en">
|
||||||
<Box display="flex" gap={1} flexDirection="column">
|
<Box
|
||||||
|
display="flex"
|
||||||
|
gap={1}
|
||||||
|
flexDirection="column"
|
||||||
|
sx={{ maxWidth: showMore ? "calc(100% - 145px)" : "100%" }}
|
||||||
|
>
|
||||||
{/* First row: 4 fields */}
|
{/* First row: 4 fields */}
|
||||||
<Box display="flex" gap={1} flexDirection="row" alignItems="flex-start">
|
<Box display="flex" gap={1} flexDirection="row" alignItems="center">
|
||||||
{/* Start Date - 30% */}
|
{/* Start Date - 30% */}
|
||||||
<Box sx={{ flexGrow: 0.3, flexBasis: "25%", minWidth: 0 }}>
|
<Box sx={{ flexGrow: 0.3, flexBasis: "25%", maxWidth: "150px" }}>
|
||||||
{showMore && (
|
|
||||||
<Typography variant="caption" display="block" mb={0.5}>
|
|
||||||
Start Date
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label={!showMore ? "Start Date" : ""}
|
label="Start Date"
|
||||||
value={startDate ? moment(startDate) : null}
|
value={startDate ? moment(startDate) : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue: Moment | null) => {
|
||||||
onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
|
onStartDateChange(newValue?.format("YYYY-MM-DD") || "");
|
||||||
@@ -68,22 +73,17 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
margin: "dense" as const,
|
margin: "dense" as const,
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
InputLabelProps: { shrink: true },
|
InputLabelProps: { shrink: true },
|
||||||
"data-testid": "start-date-input",
|
|
||||||
sx: { width: "100%" },
|
sx: { width: "100%" },
|
||||||
|
inputProps: { "data-testid": "start-date-input" },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Start Time - 20% */}
|
{/* Start Time - 20% */}
|
||||||
<Box sx={{ flexGrow: 0.2, flexBasis: "25%", minWidth: 0 }}>
|
<Box sx={{ flexGrow: 0.2, flexBasis: "25%", maxWidth: "150px" }}>
|
||||||
{showMore && (
|
|
||||||
<Typography variant="caption" display="block" mb={0.5}>
|
|
||||||
Start Time
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<TimePicker
|
<TimePicker
|
||||||
label={!showMore ? "Start Time" : ""}
|
label="Start Time"
|
||||||
value={startTime ? moment(startTime, "HH:mm") : null}
|
value={startTime ? moment(startTime, "HH:mm") : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue: Moment | null) => {
|
||||||
onStartTimeChange(newValue?.format("HH:mm") || "");
|
onStartTimeChange(newValue?.format("HH:mm") || "");
|
||||||
@@ -95,22 +95,17 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
margin: "dense" as const,
|
margin: "dense" as const,
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
InputLabelProps: { shrink: true },
|
InputLabelProps: { shrink: true },
|
||||||
"data-testid": "start-time-input",
|
|
||||||
sx: { width: "100%" },
|
sx: { width: "100%" },
|
||||||
|
inputProps: { "data-testid": "start-time-input" },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* End Time - 20% */}
|
{/* End Time - 20% */}
|
||||||
<Box sx={{ flexGrow: 0.2, flexBasis: "25%", minWidth: 0 }}>
|
<Box sx={{ flexGrow: 0.2, flexBasis: "25%", maxWidth: "150px" }}>
|
||||||
{showMore && (
|
|
||||||
<Typography variant="caption" display="block" mb={0.5}>
|
|
||||||
End Time
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<TimePicker
|
<TimePicker
|
||||||
label={!showMore ? "End Time" : ""}
|
label="End Time"
|
||||||
value={endTime ? moment(endTime, "HH:mm") : null}
|
value={endTime ? moment(endTime, "HH:mm") : null}
|
||||||
onChange={(newValue: Moment | null) => {
|
onChange={(newValue: Moment | null) => {
|
||||||
onEndTimeChange(newValue?.format("HH:mm") || "");
|
onEndTimeChange(newValue?.format("HH:mm") || "");
|
||||||
@@ -123,38 +118,53 @@ export const DateTimeFields: React.FC<DateTimeFieldsProps> = ({
|
|||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
InputLabelProps: { shrink: true },
|
InputLabelProps: { shrink: true },
|
||||||
error: !!validation.errors.dateTime,
|
error: !!validation.errors.dateTime,
|
||||||
"data-testid": "end-time-input",
|
|
||||||
sx: { width: "100%" },
|
sx: { width: "100%" },
|
||||||
|
inputProps: { "data-testid": "end-time-input" },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* End Date - 30% */}
|
{/* End Date - Conditional rendering */}
|
||||||
<Box sx={{ flexGrow: 0.3, flexBasis: "25%", minWidth: 0 }}>
|
<Box sx={{ flexGrow: 0.3, flexBasis: "25%", maxWidth: "150px" }}>
|
||||||
{showMore && (
|
{!showEndDate ? (
|
||||||
<Typography variant="caption" display="block" mb={0.5}>
|
// Show "..." button to reveal end date
|
||||||
End Date
|
<Box
|
||||||
</Typography>
|
display="flex"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
sx={{ mt: showMore ? 0 : 0 }}
|
||||||
|
>
|
||||||
|
<Tooltip title="Show end date">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={onToggleEndDate}
|
||||||
|
aria-label="Show end date"
|
||||||
|
>
|
||||||
|
<MoreHorizIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
// Show End Date picker (no hide button)
|
||||||
|
<DatePicker
|
||||||
|
label="End Date"
|
||||||
|
value={endDate ? moment(endDate) : null}
|
||||||
|
onChange={(newValue: Moment | null) => {
|
||||||
|
onEndDateChange(newValue?.format("YYYY-MM-DD") || "");
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
textField: {
|
||||||
|
size: "small",
|
||||||
|
margin: "dense" as const,
|
||||||
|
fullWidth: true,
|
||||||
|
InputLabelProps: { shrink: true },
|
||||||
|
error: !!validation.errors.dateTime,
|
||||||
|
sx: { width: "100%" },
|
||||||
|
inputProps: { "data-testid": "end-date-input" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<DatePicker
|
|
||||||
label={!showMore ? "End Date" : ""}
|
|
||||||
value={endDate ? moment(endDate) : null}
|
|
||||||
onChange={(newValue: Moment | null) => {
|
|
||||||
onEndDateChange(newValue?.format("YYYY-MM-DD") || "");
|
|
||||||
}}
|
|
||||||
slotProps={{
|
|
||||||
textField: {
|
|
||||||
size: "small",
|
|
||||||
margin: "dense" as const,
|
|
||||||
fullWidth: true,
|
|
||||||
InputLabelProps: { shrink: true },
|
|
||||||
error: !!validation.errors.dateTime,
|
|
||||||
"data-testid": "end-date-input",
|
|
||||||
sx: { width: "100%" },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { combineDateTime } from "../utils/dateTimeHelpers";
|
import { combineDateTime } from "../utils/dateTimeHelpers";
|
||||||
|
import { getEndDateForToggle } from "../utils/dateRules";
|
||||||
import { getRoundedCurrentTime } from "../utils/dateTimeFormatters";
|
import { getRoundedCurrentTime } from "../utils/dateTimeFormatters";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,15 +87,15 @@ export function useAllDayToggle(
|
|||||||
setStartTime("");
|
setStartTime("");
|
||||||
setEndTime("");
|
setEndTime("");
|
||||||
newStart = startDate;
|
newStart = startDate;
|
||||||
newEnd = endDate;
|
const nextEndDate = getEndDateForToggle({
|
||||||
|
nextAllDay: true,
|
||||||
// If same day, extend end to next day
|
fromAllDaySlot: !!originalTimeRef.current?.fromAllDaySlot,
|
||||||
if (startDate === endDate) {
|
startDate,
|
||||||
const nextDay = new Date(endDate);
|
previousEndDate: endDate,
|
||||||
nextDay.setDate(nextDay.getDate() + 1);
|
originalEndDate: originalTimeRef.current?.endDate,
|
||||||
newEnd = nextDay.toISOString().split("T")[0];
|
});
|
||||||
setEndDate(newEnd); // Update internal endDate state
|
newEnd = nextEndDate;
|
||||||
}
|
setEndDate(nextEndDate);
|
||||||
} else {
|
} else {
|
||||||
// ON => OFF: Restore original time AND original end date
|
// ON => OFF: Restore original time AND original end date
|
||||||
if (originalTimeRef.current) {
|
if (originalTimeRef.current) {
|
||||||
@@ -123,7 +124,12 @@ export function useAllDayToggle(
|
|||||||
setEndDate(startDate); // Set endDate = startDate
|
setEndDate(startDate); // Set endDate = startDate
|
||||||
} else {
|
} else {
|
||||||
// Normal case: restore original time AND original end date
|
// Normal case: restore original time AND original end date
|
||||||
const restoredEndDate = originalTimeRef.current.endDate || endDate;
|
const restoredEndDate = getEndDateForToggle({
|
||||||
|
nextAllDay: false,
|
||||||
|
startDate,
|
||||||
|
previousEndDate: endDate,
|
||||||
|
originalEndDate: originalTimeRef.current.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
newStart = combineDateTime(startDate, originalTimeRef.current.start);
|
newStart = combineDateTime(startDate, originalTimeRef.current.start);
|
||||||
newEnd = combineDateTime(
|
newEnd = combineDateTime(
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Date rules helpers to normalize end date/time behavior
|
||||||
|
|
||||||
|
import { combineDateTime } from "./dateTimeHelpers";
|
||||||
|
|
||||||
|
/** Adds a number of days to a YYYY-MM-DD string and returns YYYY-MM-DD */
|
||||||
|
export function addDays(dateStr: string, days: number): string {
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
d.setDate(d.getDate() + days);
|
||||||
|
return d.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute endDate (YYYY-MM-DD) when startDate changes
|
||||||
|
* - If all-day: endDate = startDate + 1 day
|
||||||
|
* - Else: endDate = startDate
|
||||||
|
*/
|
||||||
|
export function getEndDateForStartChange(
|
||||||
|
startDate: string,
|
||||||
|
isAllDay: boolean
|
||||||
|
): string {
|
||||||
|
if (!startDate) return "";
|
||||||
|
return isAllDay ? addDays(startDate, 1) : startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute new start/end (ISO-like strings) when toggling all-day
|
||||||
|
* - nextAllDay = true:
|
||||||
|
* - If fromAllDaySlot: endDate = startDate
|
||||||
|
* - Else: endDate = startDate + 1 day
|
||||||
|
* - Times are cleared (date-only)
|
||||||
|
* - nextAllDay = false:
|
||||||
|
* - end restored from originalEndDate if any, else previousEndDate, else start
|
||||||
|
*/
|
||||||
|
export function getEndDateForToggle(params: {
|
||||||
|
nextAllDay: boolean;
|
||||||
|
fromAllDaySlot?: boolean;
|
||||||
|
startDate: string;
|
||||||
|
previousEndDate: string;
|
||||||
|
originalEndDate?: string;
|
||||||
|
}): string {
|
||||||
|
const {
|
||||||
|
nextAllDay,
|
||||||
|
fromAllDaySlot,
|
||||||
|
startDate,
|
||||||
|
previousEndDate,
|
||||||
|
originalEndDate,
|
||||||
|
} = params;
|
||||||
|
if (nextAllDay) {
|
||||||
|
return fromAllDaySlot ? startDate : addDays(startDate, 1);
|
||||||
|
}
|
||||||
|
return originalEndDate || previousEndDate || startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Utility to combine date with a fallback time (HH:mm) safely */
|
||||||
|
export function combineWithFallback(
|
||||||
|
dateStr: string,
|
||||||
|
timeHHmm: string | undefined,
|
||||||
|
fallbackTime: string
|
||||||
|
): string {
|
||||||
|
const time = timeHHmm && timeHHmm.trim() ? timeHHmm : fallbackTime;
|
||||||
|
return combineDateTime(dateStr, time);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user