From f1ec3532497fd3252337366366b8d513ecaafe44 Mon Sep 17 00:00:00 2001 From: Mukul Bansal Date: Sun, 23 Jun 2024 23:32:29 +0530 Subject: [PATCH] fix: date and time picker issues affects: @medly-components/core, @medly-components/forms, @medly-components/utils --- .../src/components/DatePicker/DatePicker.tsx | 52 +- .../__snapshots__/DatePicker.test.tsx.snap | 5 + .../DateRangeTextField/types.ts | 1 + .../DateRangeTextFields.tsx | 5 + .../useDateRangeTextFieldsHandlers.ts | 91 ++-- .../DateRangePicker.test.tsx.snap | 6 + .../helpers/getFormattedDate.ts | 5 - .../DateRangePicker/helpers/index.ts | 1 - .../components/TextField/getMaskedValue.ts | 50 +- .../TimePickerTextField.tsx | 60 ++- .../__snapshots__/TimePicker.test.tsx.snap | 342 +++++++------- .../Form/__snapshots__/Form.test.tsx.snap | 444 +++++++++--------- .../{parseToDate.ts => dateHelpers.ts} | 23 +- packages/utils/src/helpers/index.ts | 2 +- packages/utils/src/hooks/index.ts | 1 + .../src/hooks/useRunAfterUpdate/index.ts | 1 + .../useRunAfterUpdate/useRunAfterUpdate.ts | 16 + 17 files changed, 645 insertions(+), 460 deletions(-) delete mode 100644 packages/core/src/components/DateRangePicker/helpers/getFormattedDate.ts rename packages/utils/src/helpers/{parseToDate.ts => dateHelpers.ts} (53%) create mode 100644 packages/utils/src/hooks/useRunAfterUpdate/index.ts create mode 100644 packages/utils/src/hooks/useRunAfterUpdate/useRunAfterUpdate.ts diff --git a/packages/core/src/components/DatePicker/DatePicker.tsx b/packages/core/src/components/DatePicker/DatePicker.tsx index d5dec498b..aa1d75137 100644 --- a/packages/core/src/components/DatePicker/DatePicker.tsx +++ b/packages/core/src/components/DatePicker/DatePicker.tsx @@ -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'; @@ -42,28 +49,44 @@ const Component: FC = memo( const wrapperRef = useRef(null), inputRef = useCombinedRefs(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) => { - 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] @@ -127,7 +150,16 @@ const Component: FC = 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); @@ -138,10 +170,6 @@ const Component: FC = memo( } }, wrapperRef); - useEffect(() => { - date && !showCalendar && setInputKey(key => key + 1); - }, [date, showCalendar]); - const dateIconEl = () => ( @@ -168,7 +196,7 @@ const Component: FC = 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} @@ -176,6 +204,8 @@ const Component: FC = memo( value={textValue} onChange={onTextChange} validator={inputValidator} + onKeyPress={onKeyPress} + maxLength={mask!.length + 1} {...{ ...restProps, onBlur, onFocus, minWidth, onInvalid }} /> diff --git a/packages/core/src/components/DatePicker/__snapshots__/DatePicker.test.tsx.snap b/packages/core/src/components/DatePicker/__snapshots__/DatePicker.test.tsx.snap index 8b67ddde3..eed7b98ac 100644 --- a/packages/core/src/components/DatePicker/__snapshots__/DatePicker.test.tsx.snap +++ b/packages/core/src/components/DatePicker/__snapshots__/DatePicker.test.tsx.snap @@ -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" @@ -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" @@ -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" @@ -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" @@ -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" diff --git a/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextField/types.ts b/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextField/types.ts index 6c5df94ff..d0cf32215 100644 --- a/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextField/types.ts +++ b/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextField/types.ts @@ -4,5 +4,6 @@ export type Props = Omit & { isPrefixPresent?: boolean; dateMaskLabel: string; size: Required['size']; + maxLength: number; variant: Required['variant']; }; diff --git a/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextFields.tsx b/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextFields.tsx index 43a08a664..c72892a1d 100644 --- a/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextFields.tsx +++ b/packages/core/src/components/DateRangePicker/DateRangeTextFields/DateRangeTextFields.tsx @@ -45,6 +45,7 @@ export const DateRangeTextFields: FC = memo(props => { builtInErrorMessage, stopPropagation, onIconClick, + onKeyPress, onTextFieldFocus, handleTextChange, validateOnTextFieldBlur, @@ -125,8 +126,10 @@ export const DateRangeTextFields: FC = memo(props => { value={startDateText} name="START_DATE" isPrefixPresent + maxLength={mask.length + 1} dateMaskLabel={startDateMaskLabel} label={startDateLabel} + onKeyPress={onKeyPress} {...commonTextProps} /> @@ -136,7 +139,9 @@ export const DateRangeTextFields: FC = memo(props => { value={endDateText} name="END_DATE" dateMaskLabel={endDateMaskLabel} + maxLength={mask.length + 1} label={endDateLabel} + onKeyPress={onKeyPress} {...commonTextProps} /> {showTooltipForHelperAndErrorText && ( diff --git a/packages/core/src/components/DateRangePicker/DateRangeTextFields/useDateRangeTextFieldsHandlers.ts b/packages/core/src/components/DateRangePicker/DateRangeTextFields/useDateRangeTextFieldsHandlers.ts index d056d3bd1..886897c37 100644 --- a/packages/core/src/components/DateRangePicker/DateRangeTextFields/useDateRangeTextFieldsHandlers.ts +++ b/packages/core/src/components/DateRangePicker/DateRangeTextFields/useDateRangeTextFieldsHandlers.ts @@ -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'; @@ -32,6 +31,25 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => { [endDateMaskLabel, setEndDateMaskLabel] = useState(mask), isErrorPresent = useMemo(() => !!errorText || !!builtInErrorMessage, [errorText, builtInErrorMessage]); + const getErrorMessage = (event: React.ChangeEvent | React.FocusEvent): 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 => { @@ -49,24 +67,38 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => { event.target.setSelectionRange(event.target.value.length, event.target.value.length); }, []), handleTextChange = useCallback( - (e: React.ChangeEvent) => { - 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) => { + 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 }); } } @@ -74,35 +106,23 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => { [selectedDates, onDateChange] ), validateOnTextFieldBlur = useCallback( - (event: FormEvent) => { + (event: React.FocusEvent) => { 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] @@ -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) : '', @@ -180,6 +206,7 @@ export const useDateRangeTextFieldsHandlers = (props: Props) => { isErrorPresent, stopPropagation, onIconClick, + onKeyPress, onTextFieldFocus, handleTextChange, validateOnTextFieldBlur, diff --git a/packages/core/src/components/DateRangePicker/__snapshots__/DateRangePicker.test.tsx.snap b/packages/core/src/components/DateRangePicker/__snapshots__/DateRangePicker.test.tsx.snap index aab1dac8a..0334cb269 100644 --- a/packages/core/src/components/DateRangePicker/__snapshots__/DateRangePicker.test.tsx.snap +++ b/packages/core/src/components/DateRangePicker/__snapshots__/DateRangePicker.test.tsx.snap @@ -2334,6 +2334,7 @@ exports[`DateRangePicker Custom date range options should render properly with c - format(date, displayFormat).replace(new RegExp('\\/|\\-', 'g'), ' $& '); diff --git a/packages/core/src/components/DateRangePicker/helpers/index.ts b/packages/core/src/components/DateRangePicker/helpers/index.ts index 497ba4401..3c1e6c608 100644 --- a/packages/core/src/components/DateRangePicker/helpers/index.ts +++ b/packages/core/src/components/DateRangePicker/helpers/index.ts @@ -1,2 +1 @@ export * from './dateRangeHelpers'; -export * from './getFormattedDate'; diff --git a/packages/core/src/components/TextField/getMaskedValue.ts b/packages/core/src/components/TextField/getMaskedValue.ts index d192cde5b..8b1806914 100644 --- a/packages/core/src/components/TextField/getMaskedValue.ts +++ b/packages/core/src/components/TextField/getMaskedValue.ts @@ -1,4 +1,4 @@ -const applyMasking = (value: string, mask: string, selectionStart: number): string => { +const applyMasking = (value: string, mask: string, selectionStart: number, data: string | null): string => { const { length } = value, lastChar = value.charAt(length - 1), alphaRegex = /[a-zA-Z]/, //NOSONAR @@ -11,6 +11,7 @@ const applyMasking = (value: string, mask: string, selectionStart: number): stri // if user types more char then mask length newValue = value.slice(0, -1); } else if ( + data === null && specialCharsRegex.test(mask.charAt(selectionStart)) && !specialCharsRegex.test(mask.charAt(selectionStart + 1)) && mask.slice(0, selectionStart).replace(/[^a-zA-Z0-9]+$/, '').length === length //NOSONAR @@ -35,19 +36,48 @@ export const getMaskedValue = (event: React.ChangeEvent, mask: // @ts-expect-error { data } = event.nativeEvent; - if (selectionStart !== null && selectionStart < value.length && value.length !== mask.length) { - maskedValue = - data === null - ? `${value.slice(0, selectionStart)} ${value.slice(selectionStart)}` - : value[selectionStart] === ' ' - ? `${value.slice(0, selectionStart - 1)}${data}${value.slice(selectionStart + 1)}` - : `${value.slice(0, selectionStart)}${data}${value.slice(selectionStart)}`; - return { maskedValue, selectionStart: specialCharsRegex.test(value[selectionStart]) ? selectionStart : selectionStart + 1 }; + if (selectionStart !== null && selectionStart < value.length) { + const cursorText = value[selectionStart ? selectionStart - 1 : 0]; + + if (selectionStart === 2 && data !== null && value[0] === ' ') { + return { + maskedValue: value.trim().replace(/\s{3}/g, ' '), + selectionStart: selectionStart - 1 + }; + } + if (selectionStart === 0 && data === null) { + return { + maskedValue: `${value.slice(0, selectionStart)} ${value.slice(selectionStart)}`.replace(/\s{3}/g, ' '), + selectionStart: selectionStart + 1 + }; + } else if (cursorText === ' ' && data === null) { + const postCursorText = value[selectionStart]; + const updatedText = `${value.slice(0, selectionStart)} ${value.slice(selectionStart)}`; + return { + maskedValue: postCursorText === ' ' ? updatedText.replace(/\s{3}/g, ' ') : updatedText, + selectionStart: postCursorText === ' ' ? selectionStart : selectionStart + 1 + }; + } else if (cursorText === ' ' && data !== null) { + return { + maskedValue: value.replace(/\s{3}/g, ' '), + selectionStart + }; + } else if (cursorText !== ' ' && data === null) { + return { + maskedValue: `${value.slice(0, selectionStart)} ${value.slice(selectionStart)}`.replace(/\s{3}/g, ' '), + selectionStart: selectionStart + }; + } else { + return { + maskedValue: value.replace(/\s{3}/g, ' '), + selectionStart + }; + } } else { maskedValue = value .replace(specialCharsRegex, '') .split('') - .reduce((acc: string, c: string) => applyMasking(acc + c, mask, selectionStart ?? 0), ''); + .reduce((acc: string, c: string) => applyMasking(acc + c, mask, selectionStart ?? 0, data), ''); return { maskedValue, selectionStart: maskedValue.length }; } }; diff --git a/packages/core/src/components/TimePicker/TimePickerTextField/TimePickerTextField.tsx b/packages/core/src/components/TimePicker/TimePickerTextField/TimePickerTextField.tsx index f5715fa17..355f67e12 100644 --- a/packages/core/src/components/TimePicker/TimePickerTextField/TimePickerTextField.tsx +++ b/packages/core/src/components/TimePicker/TimePickerTextField/TimePickerTextField.tsx @@ -1,9 +1,9 @@ -import { AccessTimeIcon } from '@medly-components/icons'; -import { WithStyle, useCombinedRefs } from '@medly-components/utils'; +import { WithStyle, useCombinedRefs, useRunAfterUpdate } from '@medly-components/utils'; import type { ChangeEvent, FC, FocusEvent } from 'react'; import { forwardRef, memo, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { PopoverContext } from '../../Popover/Popover.context'; import TextField from '../../TextField'; +import { TimeIcon } from '../TimePicker.styled'; import { TimePickerTextFieldProps } from './types'; const Component: FC = memo( @@ -11,6 +11,7 @@ const Component: FC = memo( const [key, setKey] = useState(0); const [text, setText] = useState(''); const [isDialogOpen] = useContext(PopoverContext); + const runAfterUpdate = useRunAfterUpdate(); const inputRef = useCombinedRefs(ref, useRef(null)); const validator = useCallback( @@ -61,23 +62,44 @@ const Component: FC = memo( }; const onChange = (event: ChangeEvent) => { - setText(event.target.value); - if (event.target.value.length >= 11) { - // @ts-expect-error - const [, hour, minutes, period] = event.target.value.replace(/ /g, '').match(/([0-9]{2}):([0-9]{2})([a-zA-Z]{2})/); - if ( - hour >= '00' && - hour <= '12' && - minutes >= '00' && - minutes <= '59' && - (period.toUpperCase() === 'AM' || period.toUpperCase() === 'PM') - ) { - props.onChange?.(period.toUpperCase() === 'AM' ? `${hour}:${minutes}` : `${Number(hour) + 12}:${minutes}`); - setText(`${`0${Number(hour) % 12}`.slice(-2)} : ${`0${minutes}`.slice(-2)} ${period}`); - } else { - props.onChange?.(''); + const inputValue = event.target.value; + const cursor = event.target.selectionStart || 0; + setText(inputValue); + if (inputValue.length >= 7 && inputRef.current) { + const match = inputValue.replace(/ /g, '').match(/([0-9]{2}):([0-9]{2})([a-zA-Z]{2})/); + if (match) { + const [, hour, minutes, period] = match; + if ( + hour >= '00' && + hour <= '12' && + minutes >= '00' && + minutes <= '59' && + (period.toUpperCase() === 'AM' || period.toUpperCase() === 'PM') + ) { + props.onChange?.(period.toUpperCase() === 'AM' ? `${hour}:${minutes}` : `${Number(hour) + 12}:${minutes}`); + } + const updatedText = `${`0${hour}`.slice(-2)} : ${`0${minutes}`.slice(-2)} ${period}`; + const updatedCursor = + cursor - + (inputValue.slice(0, cursor) === updatedText.slice(0, cursor) ? 0 : inputValue.length - updatedText.length); + setText(updatedText); + runAfterUpdate(() => inputRef.current?.setSelectionRange(updatedCursor, updatedCursor)); + inputRef.current.maxLength = 11; + return; } } + if (inputRef.current) { + inputRef.current.maxLength = 12; + } + }; + + const onKeyPress = (event: React.KeyboardEvent) => { + const target = event.target as HTMLInputElement; + const count = target.value.includes(' ') ? 6 : 5; + const regex = (target.selectionStart || 0) > count ? /[aApPmM]+/g : /[0-9:]+/g; + if (!regex.test(event.key)) { + event.preventDefault(); + } }; useEffect(() => { @@ -100,8 +122,10 @@ const Component: FC = memo( fullWidth mask="HH : MM AM" ref={inputRef} - suffix={AccessTimeIcon} + suffix={TimeIcon} + onKeyPress={onKeyPress} key={key.toString()} + maxLength={12} pattern={'[0-9]{2} : [0-9]{2} [AaPp][Mm]'} {...{ ...props, value: text, onBlur, validator, onChange }} /> diff --git a/packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap b/packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap index 461de40cd..6779388e0 100644 --- a/packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap +++ b/packages/core/src/components/TimePicker/__snapshots__/TimePicker.test.tsx.snap @@ -24,7 +24,7 @@ exports[`TimePicker should render properly 1`] = ` display: inline-flex; } -.c14 { +.c15 { z-index: 4; position: absolute; background-color: white; @@ -32,24 +32,24 @@ exports[`TimePicker should render properly 1`] = ` left: 0%; } -.c13 { +.c14 { pointer-events: none; margin-left: 1.6rem; } -.c13 * { +.c14 * { fill: #546A7F; } -.c13:hover * { +.c14:hover * { fill: #546A7F; } -.c13 * { +.c14 * { fill: #3872D2; } -.c13:hover * { +.c14:hover * { fill: #546A7F; } @@ -343,7 +343,11 @@ exports[`TimePicker should render properly 1`] = ` margin: 0; } -.c22 { +.c13 { + cursor: pointer; +} + +.c23 { margin: 0; color: inherit; font-size: 1.4rem; @@ -357,7 +361,7 @@ exports[`TimePicker should render properly 1`] = ` display: inline-block; } -.c26 { +.c27 { margin: 0; color: inherit; font-size: 1.4rem; @@ -371,7 +375,7 @@ exports[`TimePicker should render properly 1`] = ` display: inline-block; } -.c25 { +.c26 { border: none; position: relative; -webkit-user-select: none; @@ -400,27 +404,27 @@ exports[`TimePicker should render properly 1`] = ` align-items: center; } -.c25, -.c25 .c21, -.c25 .c11, -.c25 .c11 * { +.c26, +.c26 .c22, +.c26 .c11, +.c26 .c11 * { -webkit-transition: all 100ms ease-out; transition: all 100ms ease-out; } -.c25:hover { +.c26:hover { cursor: pointer; } -.c25:focus { +.c26:focus { outline: none; } -.c25:disabled { +.c26:disabled { cursor: not-allowed; } -.c25::after { +.c26::after { content: ''; display: block; position: absolute; @@ -436,71 +440,71 @@ exports[`TimePicker should render properly 1`] = ` transform: translate(-50%); } -.c25:disabled { +.c26:disabled { color: #98A7B7; } -.c25:disabled::after { +.c26:disabled::after { background-color: #98A7B7; } -.c25:disabled .c11 * { +.c26:disabled .c11 * { fill: #98A7B7; } -.c25:not(:disabled):not(:hover) { +.c26:not(:disabled):not(:hover) { color: #3872D2; } -.c25:not(:disabled):not(:hover)::after { +.c26:not(:disabled):not(:hover)::after { background-color: #3872D2; } -.c25:not(:disabled):not(:hover) .c11 * { +.c26:not(:disabled):not(:hover) .c11 * { fill: #3872D2; } -.c25:not(:disabled):active { +.c26:not(:disabled):active { color: #275093; } -.c25:not(:disabled):active::after { +.c26:not(:disabled):active::after { background-color: #275093; } -.c25:not(:disabled):active .c11 * { +.c26:not(:disabled):active .c11 * { fill: #275093; } -.c25:not(:disabled):active::after { +.c26:not(:disabled):active::after { width: calc(100% - 4.8rem); } -.c25:not(:disabled):not(:active):hover { +.c26:not(:disabled):not(:active):hover { color: #3061B3; } -.c25:not(:disabled):not(:active):hover::after { +.c26:not(:disabled):not(:active):hover::after { background-color: #3061B3; } -.c25:not(:disabled):not(:active):hover .c11 * { +.c26:not(:disabled):not(:active):hover .c11 * { fill: #3061B3; } -.c25:not(:disabled):not(:active):hover::after { +.c26:not(:disabled):not(:active):hover::after { width: calc(100% - 4.8rem); } -.c25 .c11 + .c21 { +.c26 .c11 + .c22 { margin-left: 0.8rem; } -.c25 .c21 + .c11 { +.c26 .c22 + .c11 { margin-left: 0.8rem; } -.c27 { +.c28 { border: none; position: relative; -webkit-user-select: none; @@ -528,72 +532,72 @@ exports[`TimePicker should render properly 1`] = ` align-items: center; } -.c27, -.c27 .c21, -.c27 .c11, -.c27 .c11 * { +.c28, +.c28 .c22, +.c28 .c11, +.c28 .c11 * { -webkit-transition: all 100ms ease-out; transition: all 100ms ease-out; } -.c27:hover { +.c28:hover { cursor: pointer; } -.c27:focus { +.c28:focus { outline: none; } -.c27:disabled { +.c28:disabled { cursor: not-allowed; } -.c27:disabled { +.c28:disabled { color: #98A7B7; background: #dfe4e9; } -.c27:disabled .c11 * { +.c28:disabled .c11 * { fill: #98A7B7; } -.c27:not(:disabled):not(:hover) { +.c28:not(:disabled):not(:hover) { color: #ffffff; background: #3872D2; } -.c27:not(:disabled):not(:hover) .c11 * { +.c28:not(:disabled):not(:hover) .c11 * { fill: #ffffff; } -.c27:not(:disabled):active { +.c28:not(:disabled):active { color: #ffffff; background: #275093; } -.c27:not(:disabled):active .c11 * { +.c28:not(:disabled):active .c11 * { fill: #ffffff; } -.c27:not(:disabled):not(:active):hover { +.c28:not(:disabled):not(:active):hover { color: #ffffff; background: #3061B3; box-shadow: 0 0.2rem 0.8rem rgba(48,97,179,0.35); } -.c27:not(:disabled):not(:active):hover .c11 * { +.c28:not(:disabled):not(:active):hover .c11 * { fill: #ffffff; } -.c27 .c11 + .c21 { +.c28 .c11 + .c22 { margin-left: 0.8rem; } -.c27 .c21 + .c11 { +.c28 .c22 + .c11 { margin-left: 0.8rem; } -.c17 { +.c18 { height: 100%; display: -webkit-box; display: -webkit-flex; @@ -615,7 +619,7 @@ exports[`TimePicker should render properly 1`] = ` z-index: 1; } -.c18 { +.c19 { height: 100%; padding: 0; margin: 0; @@ -646,11 +650,11 @@ exports[`TimePicker should render properly 1`] = ` list-style: none; } -.c18::-webkit-scrollbar { +.c19::-webkit-scrollbar { display: none; } -.c19 { +.c20 { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -677,16 +681,16 @@ exports[`TimePicker should render properly 1`] = ` color: #546A7F; } -.c19:hover { +.c20:hover { color: #546A7F; } -.c19:hover { +.c20:hover { -webkit-text-decoration: none; text-decoration: none; } -.c20 { +.c21 { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -713,16 +717,16 @@ exports[`TimePicker should render properly 1`] = ` color: #3872D2; } -.c20:hover { +.c21:hover { color: #3872D2; } -.c20:hover { +.c21:hover { -webkit-text-decoration: underline; text-decoration: underline; } -.c15 { +.c16 { background: #ffffff; box-shadow: 0 0.2rem 0.8rem #b0bcc8; border-radius: 0.4rem; @@ -735,7 +739,7 @@ exports[`TimePicker should render properly 1`] = ` padding-top: 1.2rem; } -.c16 { +.c17 { width: 100%; box-sizing: border-box; height: 20rem; @@ -758,13 +762,13 @@ exports[`TimePicker should render properly 1`] = ` position: relative; } -.c16 > * { +.c17 > * { -webkit-flex: 1; -ms-flex: 1; flex: 1; } -.c16::before { +.c17::before { content: ''; position: absolute; top: 50%; @@ -778,7 +782,7 @@ exports[`TimePicker should render properly 1`] = ` transform: translate(-50%,-50%); } -.c23 { +.c24 { -webkit-flex: 0.5; -ms-flex: 0.5; flex: 0.5; @@ -797,7 +801,7 @@ exports[`TimePicker should render properly 1`] = ` z-index: 1; } -.c24 { +.c25 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -835,6 +839,7 @@ exports[`TimePicker should render properly 1`] = ` aria-describedby="time-helper-text" class="c6" id="time-input" + maxlength="12" pattern="[0-9]{2} : [0-9]{2} [AaPp][Mm]" placeholder="HH : MM AM" type="text" @@ -853,9 +858,10 @@ exports[`TimePicker should render properly 1`] = `
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
:
  • 00
  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • AM
  • PM
@@ -2257,7 +2266,7 @@ exports[`Form should render properly with initial state 1`] = ` class="c11" >
@@ -2345,11 +2354,11 @@ exports[`Form should render properly with initial state 1`] = `

Marks

Marks Information
@@ -4563,7 +4581,7 @@ exports[`Form should render properly without initial state 1`] = ` class="c14" >
@@ -4651,7 +4669,7 @@ exports[`Form should render properly without initial state 1`] = `

Marks

Marks Information