Skip to content

Commit

Permalink
fix: remove resetting time value to 00:00 in DateTimeSelector
Browse files Browse the repository at this point in the history
When invalid value was entered in time part of DateTimeSelector,
it got resetted to 00:00. This was problematic as it happened e.g. while
the user was typing so it interrupted the user flow.

The DateTimeSelector was changed in a way that in set NaN value when
either of date or time parts are invalid + it no longer automatically
set any other part while the input is still not fully valid - making the
user the enter everything and thus behaving more naturally.

Allow to clear value in IpaCalendar and DateTimeSelector:

Previously it was not possible to undo setting a date. Now when
both date and time parts are empty, the value is returned as null or
"" (depending on component).

Fixes: #373

Signed-off-by: Petr Vobornik <pvoborni@redhat.com>
  • Loading branch information
pvoborni committed Sep 12, 2024
1 parent 6428635 commit a638d73
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 61 deletions.
126 changes: 67 additions & 59 deletions src/components/Form/DateTimeSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,121 @@
import React from "react";
import React, { useEffect, useState } from "react";
// PatternFly
import {
DatePicker,
InputGroup,
TimePicker,
isValidDate,
yyyyMMddFormat,
} from "@patternfly/react-core";
// Utils
import {
parseFullDateStringToUTCFormat,
toGeneralizedTime,
} from "src/utils/utils";

interface PropsToDateTimeSelector {
datetime: Date | null;
onChange?: (timeValue: Date) => void;
onChange?: (timeValue: Date | null) => void;
name: string;
ariaLabel?: string;
isDisabled?: boolean;
}

export const yyyyMMddFormat = (date: Date) =>
`${date.getUTCFullYear()}-${(date.getUTCMonth() + 1)
.toString()
.padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`;

const hhMMFormat = (date: Date) => {
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const hours = date.getUTCHours().toString().padStart(2, "0");
const minutes = date.getUTCMinutes().toString().padStart(2, "0");

return hours + ":" + minutes;
};

function cloneDate(date: Date): Date {
return parseFullDateStringToUTCFormat(toGeneralizedTime(date)) as Date;
}

const DateTimeSelector = (props: PropsToDateTimeSelector) => {
// On change date handler
const onDateChange = (
_event: React.FormEvent<HTMLInputElement>,
inputDate: string,
newFromDate: Date | undefined
) => {
let updatedFromDate = new Date();
const [dateText, setDateText] = useState(
props.datetime ? yyyyMMddFormat(props.datetime) : ""
);
const [dateValid, setDateValid] = useState(true);

const [timeText, setTimeText] = useState(
props.datetime ? hhMMFormat(props.datetime) : ""
);
const [timeValid, setTimeValid] = useState(true);

useEffect(() => {
if (props.datetime && isValidDate(props.datetime)) {
updatedFromDate = cloneDate(props.datetime);
const newTimeText = hhMMFormat(props.datetime);
if (newTimeText !== timeText) {
setTimeText(newTimeText);
}

const newDateText = yyyyMMddFormat(props.datetime);
if (newDateText !== dateText) {
setDateText(newDateText);
}
} else if (props.datetime === null) {
setDateText("");
// the TimePicker component will unfornately reset the time to current
// time if the time is set to an empty string
setTimeText("");
}
}, [props.datetime]);

const onDateTimeChange = (
date: string,
time: string,
dateValid: boolean,
timeValid: boolean
) => {
if (!props.onChange) return;

if (!newFromDate) {
props.onChange(new Date(NaN));
// both date and time are empty -> clear the value
if (date === "" && time === "") {
props.onChange(null);
return;
}

if (newFromDate && !isValidDate(newFromDate)) {
props.onChange(newFromDate);
// one part is invalid -> skip the update as invalid datetime might be set
// to null which represents an empty value.
// The limitation is that revert might not reset the value as the source
// might still have the original value.
if (!dateValid || !timeValid) {
return;
}

updatedFromDate.setFullYear(newFromDate.getFullYear());
updatedFromDate.setMonth(newFromDate.getMonth());
updatedFromDate.setDate(newFromDate.getDate());
// valid date -> update the datetime
props.onChange(new Date(date + "T" + time + ":00Z"));
};

props.onChange(updatedFromDate);
const onDateChange = (
_event: React.FormEvent<HTMLInputElement>,
inputDate: string,
newFromDate: Date | undefined
) => {
const newDateValid = isValidDate(newFromDate);
setDateText(inputDate);
setDateValid(newDateValid);
onDateTimeChange(inputDate, timeText, newDateValid, timeValid);
};

// On change time handler
const onTimeChange = (_event, time, hour, minute) => {
let updatedFromDate = new Date();
if (props.datetime && isValidDate(props.datetime)) {
updatedFromDate = cloneDate(props.datetime);
}

if (!props.onChange) return;

// If no valid time is provided, then set hour and minutes to default values : 00:00
// - This is needed to activate the buttons and not losing the date data
if (!hour || !minute) {
updatedFromDate.setFullYear(updatedFromDate.getFullYear());
updatedFromDate.setMonth(updatedFromDate.getMonth());
updatedFromDate.setDate(updatedFromDate.getDate());
updatedFromDate.setHours(0);
updatedFromDate.setMinutes(0);
props.onChange(updatedFromDate);
return;
}

updatedFromDate.setHours(hour);
updatedFromDate.setMinutes(minute);

if (props.onChange) {
props.onChange(updatedFromDate);
}
const newTimeValid = Number.isInteger(hour) && Number.isInteger(minute);
setTimeText(time);
setTimeValid(newTimeValid);
onDateTimeChange(dateText, time, dateValid, newTimeValid);
};

return (
<InputGroup>
<DatePicker
name={props.name}
value={props.datetime ? yyyyMMddFormat(props.datetime) : ""}
value={dateText}
onChange={onDateChange}
aria-label={props.ariaLabel || props.name}
placeholder="YYYY-MM-DD"
isDisabled={props.isDisabled || false}
/>
<TimePicker
name={props.name}
time={props.datetime ? hhMMFormat(props.datetime) : ""}
time={timeText}
aria-label={props.ariaLabel || props.name}
onChange={onTimeChange}
placeholder="HH:MM"
is24Hour={true}
isDisabled={props.isDisabled || false}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Form/IpaCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function getParamPropertiesDateTime(
const IpaCalendar = (props: IPAParamDefinition) => {
const { readOnly, value } = getParamPropertiesDateTime(props);

const onDateChange = (date: Date) => {
const onDateChange = (date: Date | null) => {
if (props.ipaObject !== undefined && props.onChange !== undefined) {
updateIpaObject(
props.ipaObject,
Expand Down
3 changes: 2 additions & 1 deletion src/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,8 @@ const formatDate = (date, format, local) => {
return str;
};

export const toGeneralizedTime = (date: Date) => {
export const toGeneralizedTime = (date: Date | null) => {
if (!date) return "";
const generalizedTimeDate = formatDate(date, templates.generalized, false);
return generalizedTimeDate;
};
Expand Down

0 comments on commit a638d73

Please sign in to comment.