Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: date and time picker issues #742

Merged
merged 1 commit into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading