Skip to content

Commit

Permalink
fix: date and time picker issues (#742)
Browse files Browse the repository at this point in the history
affects: @medly-components/core, @medly-components/forms, @medly-components/utils
  • Loading branch information
gmukul01 authored Jun 23, 2024
1 parent c7b4105 commit e3abf5c
Show file tree
Hide file tree
Showing 17 changed files with 645 additions and 460 deletions.
52 changes: 41 additions & 11 deletions packages/core/src/components/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { DateRangeIcon } from '@medly-components/icons';
import { parseToDate, useCombinedRefs, useOuterClickNotifier, WithStyle } from '@medly-components/utils';
import {
getFormattedDate,
parseToDate,
useCombinedRefs,
useOuterClickNotifier,
useRunAfterUpdate,
WithStyle
} from '@medly-components/utils';
import { format } from 'date-fns';
import type { FC } from 'react';
import { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Calendar from '../Calendar';
import TextField from '../TextField';
import { DateIconWrapper, Wrapper } from './DatePicker.styled';
Expand Down Expand Up @@ -42,28 +49,44 @@ const Component: FC<DatePickerProps> = memo(

const wrapperRef = useRef<HTMLDivElement>(null),
inputRef = useCombinedRefs<HTMLInputElement>(ref, useRef(null)),
runAfterUpdate = useRunAfterUpdate(),
[inputKey, setInputKey] = useState(0),
[textValue, setTextValue] = useState(''),
[isFocused, setFocusedState] = useState(false),
[builtInErrorMessage, setErrorMessage] = useState(''),
[showCalendar, toggleCalendar] = useState(false),
[active, setActive] = useState(false),
isErrorPresent = useMemo(() => !!errorText || !!builtInErrorMessage, [errorText, builtInErrorMessage]);
isErrorPresent = useMemo(() => !!errorText || !!builtInErrorMessage, [errorText, builtInErrorMessage]),
mask = displayFormat!.replace(new RegExp('\\/|\\-', 'g'), ' $& ').toUpperCase();

useEffect(() => {
if (date) {
const cursor = inputRef.current?.selectionStart || 0;
setTextValue(format(date, displayFormat!).replace(new RegExp('\\/|\\-', 'g'), ' $& '));
runAfterUpdate(() => inputRef.current?.setSelectionRange(cursor, cursor));
} else if (!isErrorPresent && !isFocused) {
setTextValue('');
}
}, [date, isFocused, isErrorPresent, displayFormat]);
const onTextChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value,
const inputValue = event.target.value || '',
cursor = event.target.selectionStart || 0,
parsedDate = parseToDate(inputValue, displayFormat!),
isValidDate = parsedDate?.toString() !== 'Invalid Date';
setTextValue(inputValue);

onChange(isValidDate ? parsedDate : null);

const breakdown = getFormattedDate(inputValue, displayFormat!);
if (breakdown) {
setTextValue(breakdown);
event.target.maxLength = mask!.length;
} else {
setTextValue(inputValue);
event.target.maxLength = mask!.length + 1;
}

runAfterUpdate(() => event.target?.setSelectionRange(cursor, cursor));
isValidDate && validate(event);
},
[displayFormat, onChange]
Expand Down Expand Up @@ -127,7 +150,16 @@ const Component: FC<DatePickerProps> = memo(
},
[onChange]
),
inputValidator = useCallback(() => '', []);
inputValidator = useCallback(() => '', []),
onKeyPress = useCallback(
(event: React.KeyboardEvent) => {
const regex = displayFormat?.includes('/') ? /[0-9/]+/g : /[0-9-]+/g;
if (!regex.test(event.key)) {
event.preventDefault();
}
},
[displayFormat]
);

useOuterClickNotifier((event: any) => {
setActive(false);
Expand All @@ -138,10 +170,6 @@ const Component: FC<DatePickerProps> = memo(
}
}, wrapperRef);

useEffect(() => {
date && !showCalendar && setInputKey(key => key + 1);
}, [date, showCalendar]);

const dateIconEl = () => (
<DateIconWrapper variant={restProps.variant!} isErrorPresent={isErrorPresent} isActive={active} disabled={disabled} size={size}>
<DateRangeIcon id={`${id}-calendar-icon`} title={`${id}-calendar-icon`} onClick={onIconClick} size={size} />
Expand All @@ -168,14 +196,16 @@ const Component: FC<DatePickerProps> = memo(
required={required}
{...(showCalendarIcon && (calendarIconPosition === 'left' ? { prefix: dateIconEl } : { suffix: dateIconEl }))}
fullWidth
mask={displayFormat!.replace(new RegExp('\\/|\\-', 'g'), ' $& ').toUpperCase()}
mask={mask}
pattern={datePickerPattern[displayFormat!]}
size={size}
disabled={disabled}
showDecorators={showDecorators}
value={textValue}
onChange={onTextChange}
validator={inputValidator}
onKeyPress={onKeyPress}
maxLength={mask!.length + 1}
{...{ ...restProps, onBlur, onFocus, minWidth, onInvalid }}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ exports[`DatePicker component calendar icon should show calendar icon displayed
aria-describedby="dob-helper-text"
class="c8"
id="dob-input"
maxlength="15"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
type="text"
Expand Down Expand Up @@ -756,6 +757,7 @@ exports[`DatePicker component calendar icon should show calendar icon displayed
aria-describedby="dob-helper-text"
class="c6"
id="dob-input"
maxlength="15"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
type="text"
Expand Down Expand Up @@ -13619,6 +13621,7 @@ exports[`DatePicker component should render properly when hideInput prop is pass
aria-describedby="startdate-helper-text"
class="c7"
id="startdate-input"
maxlength="15"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
type="text"
Expand Down Expand Up @@ -14000,6 +14003,7 @@ exports[`DatePicker component should render properly when value is of date type
class="c6"
disabled=""
id="startdate-input"
maxlength="15"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
type="text"
Expand Down Expand Up @@ -14380,6 +14384,7 @@ exports[`DatePicker component should render properly when value is of string typ
class="c6"
disabled=""
id="medly-datepicker-input"
maxlength="15"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
type="text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export type Props = Omit<TextFieldProps, 'prefix' | 'suffix'> & {
isPrefixPresent?: boolean;
dateMaskLabel: string;
size: Required<TextFieldProps>['size'];
maxLength: number;
variant: Required<TextFieldProps>['variant'];
};
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const DateRangeTextFields: FC<Props> = memo(props => {
builtInErrorMessage,
stopPropagation,
onIconClick,
onKeyPress,
onTextFieldFocus,
handleTextChange,
validateOnTextFieldBlur,
Expand Down Expand Up @@ -125,8 +126,10 @@ export const DateRangeTextFields: FC<Props> = memo(props => {
value={startDateText}
name="START_DATE"
isPrefixPresent
maxLength={mask.length + 1}
dateMaskLabel={startDateMaskLabel}
label={startDateLabel}
onKeyPress={onKeyPress}
{...commonTextProps}
/>
<InputSeparator {...iconProps} />
Expand All @@ -136,7 +139,9 @@ export const DateRangeTextFields: FC<Props> = memo(props => {
value={endDateText}
name="END_DATE"
dateMaskLabel={endDateMaskLabel}
maxLength={mask.length + 1}
label={endDateLabel}
onKeyPress={onKeyPress}
{...commonTextProps}
/>
{showTooltipForHelperAndErrorText && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { parseToDate } from '@medly-components/utils';
import { ChangeEvent, FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { getFormattedDate, parseToDate } from '@medly-components/utils';
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { isValidDate } from '../../Calendar/helper';
import getMaskedValue from '../../TextField/getMaskedValue';
import { getFormattedDate } from '../helpers';
import { FOCUS_ELEMENT } from '../types';
import { Props } from './types';

Expand Down Expand Up @@ -32,6 +31,25 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => {
[endDateMaskLabel, setEndDateMaskLabel] = useState(mask),
isErrorPresent = useMemo(() => !!errorText || !!builtInErrorMessage, [errorText, builtInErrorMessage]);

const getErrorMessage = (event: React.ChangeEvent<HTMLInputElement> | React.FocusEvent<HTMLInputElement>): string => {
const element = event.target as HTMLInputElement,
parsedDate = parseToDate(element.value, displayFormat),
isInvalidDate = element.value && parsedDate.toString() === 'Invalid Date';

if (isInvalidDate) {
return 'Enter valid date';
} else if (parsedDate && element.name === 'START_DATE' && parsedDate < minSelectableDate) {
return `Please select date from allowed range`;
} else if (parsedDate && element.name === 'END_DATE' && parsedDate > maxSelectableDate) {
return `Please select date from allowed range`;
} else if (parsedDate && element.name === 'START_DATE' && selectedDates.endDate && parsedDate > selectedDates.endDate) {
return 'Start date should be less than end date';
} else if (parsedDate && element.name === 'END_DATE' && selectedDates.startDate && parsedDate < selectedDates.startDate) {
return 'End date should be greater than start date';
}
return '';
};

const stopPropagation = useCallback((event: React.MouseEvent) => event.stopPropagation(), []),
onIconClick = useCallback(
event => {
Expand All @@ -49,60 +67,62 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => {
event.target.setSelectionRange(event.target.value.length, event.target.value.length);
}, []),
handleTextChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { maskedValue, selectionStart } = getMaskedValue(e, mask),
parsedDate = parseToDate(e.target.value, displayFormat),
maskedLabel = `${maskedValue}${mask.substr(maskedValue.length)}`;
e.target.value = maskedValue;
e.target.setSelectionRange(selectionStart, selectionStart);
if (e.target.name === 'START_DATE') {
(event: React.ChangeEvent<HTMLInputElement>) => {
const errorMessage = getErrorMessage(event);
const { maskedValue, selectionStart } = getMaskedValue(event, mask),
parsedDate = parseToDate(event.target.value, displayFormat),
maskedLabel = `${maskedValue}${mask.substr(maskedValue.length)}`,
inputValue = event.target.value,
// @ts-expect-error
{ data } = event.nativeEvent;

event.target.value = maskedValue;
event.target.setSelectionRange(selectionStart, selectionStart);
if (getFormattedDate(inputValue, displayFormat)) {
event.target.maxLength = mask!.length;
errorMessage && setErrorMessage(errorMessage);
} else {
event.target.maxLength = mask!.length + 1;
}

if (event.target.name === 'START_DATE') {
setStartDateText(maskedValue);
setStartDateMaskLabel(maskedLabel);
if (parsedDate.toString() !== 'Invalid Date') {
setFocusedElement('END_DATE');
data !== null && !errorMessage && setFocusedElement('END_DATE');
!errorMessage && setErrorMessage('');
onDateChange({ ...selectedDates, startDate: parsedDate });
}
} else {
setEndDateText(maskedValue);
setEndDateMaskLabel(maskedLabel);
if (parsedDate.toString() !== 'Invalid Date') {
setFocusedElement('START_DATE');
data !== null && !errorMessage && setFocusedElement('START_DATE');
!errorMessage && setErrorMessage('');
onDateChange({ ...selectedDates, endDate: parsedDate });
}
}
},
[selectedDates, onDateChange]
),
validateOnTextFieldBlur = useCallback(
(event: FormEvent<HTMLInputElement>) => {
(event: React.FocusEvent<HTMLInputElement>) => {
event.preventDefault();
const element = event.target as HTMLInputElement,
parsedDate = parseToDate(element.value, displayFormat),
isInvalidDate = element.value && parsedDate.toString() === 'Invalid Date',
message = isInvalidDate ? 'Enter valid date' : '';
isInvalidDate = element.value && parsedDate.toString() === 'Invalid Date';
if (isInvalidDate) {
setErrorMessage(message);
onDateChange({
...selectedDates,
...(element.name === 'START_DATE' ? { startDate: null } : { endDate: null })
});
}
if (parsedDate && element.name === 'START_DATE' && parsedDate < minSelectableDate) {
const message = `Please select date from allowed range`;
const message = getErrorMessage(event);
if (message) {
setErrorMessage(message);
startDateRef.current?.setCustomValidity(message);
} else if (parsedDate && element.name === 'END_DATE' && parsedDate > maxSelectableDate) {
const message = `Please select date from allowed range`;
setErrorMessage(message);
endDateRef.current?.setCustomValidity(message);
} else if (parsedDate && element.name === 'START_DATE' && selectedDates.endDate && parsedDate > selectedDates.endDate) {
const message = 'Start date should be less than end date';
setErrorMessage(message);
startDateRef.current?.setCustomValidity(message);
} else if (parsedDate && element.name === 'END_DATE' && selectedDates.startDate && parsedDate < selectedDates.startDate) {
const message = 'End date should be greater than start date';
setErrorMessage(message);
endDateRef.current?.setCustomValidity(message);
element.name === 'START_DATE'
? startDateRef.current?.setCustomValidity(message)
: endDateRef.current?.setCustomValidity(message);
}
},
[selectedDates, onDateChange]
Expand Down Expand Up @@ -134,7 +154,13 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => {
}
},
[validator, selectedDates, required]
);
),
onKeyPress = useCallback((event: React.KeyboardEvent) => {
const regex = displayFormat?.includes('/') ? /[0-9/]+/g : /[0-9-]+/g;
if (!regex.test(event.key)) {
event.preventDefault();
}
}, []);

useEffect(() => {
const formattedStartDate = selectedDates.startDate ? getFormattedDate(selectedDates.startDate, displayFormat) : '',
Expand Down Expand Up @@ -180,6 +206,7 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => {
isErrorPresent,
stopPropagation,
onIconClick,
onKeyPress,
onTextFieldFocus,
handleTextChange,
validateOnTextFieldBlur,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2334,6 +2334,7 @@ exports[`DateRangePicker Custom date range options should render properly with c
<input
class="c6"
id="contract-startDate-input"
maxlength="15"
name="START_DATE"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
Expand Down Expand Up @@ -2361,6 +2362,7 @@ exports[`DateRangePicker Custom date range options should render properly with c
<input
class="c6"
id="contract-endDate-input"
maxlength="15"
name="END_DATE"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
Expand Down Expand Up @@ -25021,6 +25023,7 @@ exports[`DateRangePicker should render properly 1`] = `
<input
class="c6"
id="contract-startDate-input"
maxlength="15"
name="START_DATE"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
Expand Down Expand Up @@ -25048,6 +25051,7 @@ exports[`DateRangePicker should render properly 1`] = `
<input
class="c6"
id="contract-endDate-input"
maxlength="15"
name="END_DATE"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
Expand Down Expand Up @@ -27440,6 +27444,7 @@ exports[`DateRangePicker should render properly with single month 1`] = `
<input
class="c7"
id="contract-startDate-input"
maxlength="15"
name="START_DATE"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
Expand Down Expand Up @@ -27467,6 +27472,7 @@ exports[`DateRangePicker should render properly with single month 1`] = `
<input
class="c7"
id="contract-endDate-input"
maxlength="15"
name="END_DATE"
pattern="\\\\d{2} \\\\/ \\\\d{2} \\\\/ \\\\d{4}"
placeholder="MM / DD / YYYY"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './dateRangeHelpers';
export * from './getFormattedDate';
Loading

0 comments on commit e3abf5c

Please sign in to comment.