diff --git a/src/components/DateField/hooks/useDateFieldProps.ts b/src/components/DateField/hooks/useDateFieldProps.ts index 98d3d2f..b861cf5 100644 --- a/src/components/DateField/hooks/useDateFieldProps.ts +++ b/src/components/DateField/hooks/useDateFieldProps.ts @@ -176,25 +176,22 @@ export function useDateFieldProps( onMouseUp(e: React.MouseEvent) { e.preventDefault(); }, - onBeforeInput(e: React.FormEvent) { + onBeforeInput(e) { e.preventDefault(); // @ts-expect-error const key = e.data; - // eslint-disable-next-line no-eq-null, eqeqeq - if (key != null) { + if (key !== undefined && key !== null) { state.onInput(key); } }, onPaste(e: React.ClipboardEvent) { + e.preventDefault(); if (state.readOnly) { - e.preventDefault(); return; } const pastedValue = e.clipboardData.getData('text'); - if (state.setValueFromString(pastedValue)) { - e.preventDefault(); - } else if ( + if ( state.selectedSectionIndexes && state.selectedSectionIndexes.startIndex === state.selectedSectionIndexes.endIndex @@ -202,14 +199,23 @@ export function useDateFieldProps( const activeSection = state.sections[state.selectedSectionIndexes.startIndex]; + const digitsOnly = /^\d+$/.test(pastedValue); + const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue); + const isValidValue = Boolean(activeSection) && - ((activeSection.contentType === 'digit' && /^\d+$/.test(pastedValue)) || - activeSection.contentType === 'letter'); - if (!isValidValue) { - e.preventDefault(); + ((activeSection.contentType === 'digit' && digitsOnly) || + (activeSection.contentType === 'letter' && lettersOnly)); + if (isValidValue) { + state.onInput(pastedValue); + return; + } + if (digitsOnly || lettersOnly) { + return; } } + + state.setValueFromString(pastedValue); }, }, }, diff --git a/src/components/DateField/hooks/useDateFieldState.ts b/src/components/DateField/hooks/useDateFieldState.ts index b239101..a56317d 100644 --- a/src/components/DateField/hooks/useDateFieldState.ts +++ b/src/components/DateField/hooks/useDateFieldState.ts @@ -12,7 +12,14 @@ import type { DateFieldSectionType, DateFieldSectionWithoutPosition, } from '../types'; -import {splitFormatIntoSections} from '../utils'; +import { + addSegment, + getDurationUnitFromSectionType, + getSectionLimits, + getSectionValue, + setSegment, + splitFormatIntoSections, +} from '../utils'; export interface DateFieldStateOptions extends DateFieldBase {} @@ -27,12 +34,6 @@ const EDITABLE_SEGMENTS: Partial> = { weekday: true, }; -const TYPE_MAPPING = { - weekday: 'day', - day: 'date', - dayPeriod: 'hour', -} as const; - const PAGE_STEP: Partial> = { year: 5, month: 2, @@ -194,8 +195,13 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState } if (Object.keys(validSegments).length >= Object.keys(allSegments).length) { - setDate(newValue); + if (!value || !newValue.isSame(value)) { + setDate(newValue); + } } else { + if (value) { + setDate(null); + } setPlaceholderDate(newValue); } } @@ -396,9 +402,6 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState currentValue = displayValue.set(type, placeholder[type]()); } - if (value) { - setDate(null); - } setValue(currentValue); }, clearAll() { @@ -534,6 +537,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState } }, setValueFromString(str: string) { + enteredKeys.current = ''; let date = parseDate({input: str, format, timeZone: props.timeZone}); if (isValid(date)) { if (props.timeZone && !isDateStringWithTimeZone(str)) { @@ -560,65 +564,6 @@ function isDateStringWithTimeZone(str: string) { return /z$/i.test(str) || /[+-]\d\d:\d\d$/.test(str); } -function addSegment(section: DateFieldSection, date: DateTime, amount: number) { - let val = section.value ?? 0; - if (section.type === 'dayPeriod') { - val = date.hour() + (date.hour() > 12 ? -12 : 12); - } else { - val = val + amount; - const min = section.minValue; - const max = section.maxValue; - if (typeof min === 'number' && typeof max === 'number') { - const length = max - min + 1; - val = ((val - min + length) % length) + min; - } - } - const type = getDurationUnitFromSectionType(section.type); - return date.set(type, val); -} - -function setSegment(section: DateFieldSection, date: DateTime, amount: number) { - const type = section.type; - switch (type) { - case 'day': - case 'weekday': - case 'month': - case 'year': { - return date.set(getDurationUnitFromSectionType(type), amount); - } - case 'dayPeriod': { - const hours = date.hour(); - const wasPM = hours >= 12; - const isPM = amount >= 12; - if (isPM === wasPM) { - return date; - } - return date.set('hour', wasPM ? hours - 12 : hours + 12); - } - case 'hour': { - // In 12 hour time, ensure that AM/PM does not change - let sectionAmount = amount; - if (section.minValue === 12 || section.maxValue === 11) { - const hours = date.hour(); - const wasPM = hours >= 12; - if (!wasPM && sectionAmount === 12) { - sectionAmount = 0; - } - if (wasPM && sectionAmount < 12) { - sectionAmount += 12; - } - } - return date.set('hour', sectionAmount); - } - case 'minute': - case 'second': { - return date.set(type, amount); - } - } - - return date; -} - function getCurrentEditableSectionIndex( sections: DateFieldSection[], selectedSections: 'all' | number, @@ -690,20 +635,26 @@ function getEditableSections( let renderedValue = section.placeholder; if ((isEditable && validSegments[section.type]) || section.type === 'timeZoneName') { renderedValue = value.format(section.format); + if ( + section.contentType === 'digit' && + renderedValue.length < section.placeholder.length + ) { + renderedValue = renderedValue.padStart(section.placeholder.length, '0'); + } } const sectionLength = renderedValue.length; const newSection = { ...section, - value: getValue(value, section.type), + value: getSectionValue(section, value), textValue: renderedValue, start: position, end: position + sectionLength, modified: false, previousEditableSection, nextEditableSection: previousEditableSection, - ...getSectionLimits(value, section.type, {hour12: isHour12(section)}), + ...getSectionLimits(section, value), }; newSections.push(newSection); @@ -726,97 +677,3 @@ function getEditableSections( return newSections; } - -function getValue(date: DateTime, type: DateFieldSectionType) { - switch (type) { - case 'year': - case 'month': - case 'hour': - case 'minute': - case 'second': { - return date[type](); - } - case 'day': { - return date.date(); - } - case 'weekday': { - return date.day(); - } - case 'dayPeriod': { - return date.hour() >= 12 ? 12 : 0; - } - } - return undefined; -} - -function isHour12(section: DateFieldSectionWithoutPosition) { - if (section.type === 'hour') { - return dateTime().set('hour', 15).format(section.format) !== '15'; - } - return false; -} - -function getSectionLimits(date: DateTime, type: DateFieldSectionType, options: {hour12: boolean}) { - switch (type) { - case 'year': { - return { - minValue: 1, - maxValue: 9999, - }; - } - case 'month': { - return { - minValue: 0, - maxValue: 11, - }; - } - case 'weekday': { - return { - minValue: 0, - maxValue: 6, - }; - } - case 'day': { - return { - minValue: 1, - maxValue: date ? date.daysInMonth() : 31, - }; - } - case 'hour': { - if (options.hour12) { - const isPM = date.hour() >= 12; - return { - minValue: isPM ? 12 : 0, - maxValue: isPM ? 23 : 11, - }; - } - return { - minValue: 0, - maxValue: 23, - }; - } - case 'minute': - case 'second': { - return { - minValue: 0, - maxValue: 59, - }; - } - } - return {}; -} - -function getDurationUnitFromSectionType(type: DateFieldSectionType) { - if (type === 'literal' || type === 'timeZoneName' || type === 'unknown') { - throw new Error(`${type} section does not have duration unit.`); - } - - if (type in TYPE_MAPPING) { - return TYPE_MAPPING[type as keyof typeof TYPE_MAPPING]; - } - - return type as Exclude< - DateFieldSectionType, - keyof typeof TYPE_MAPPING | 'literal' | 'timeZoneName' | 'unknown' - >; -} diff --git a/src/components/DateField/utils.ts b/src/components/DateField/utils.ts index ac899b5..f983d8d 100644 --- a/src/components/DateField/utils.ts +++ b/src/components/DateField/utils.ts @@ -1,4 +1,5 @@ import {dateTime, settings} from '@gravity-ui/date-utils'; +import type {DateTime} from '@gravity-ui/date-utils'; import {i18n} from './i18n'; import type { @@ -120,6 +121,185 @@ function getDateSectionConfigFromFormatToken(formatToken: string): { function isFourDigitYearFormat(format: string) { return dateTime().format(format).length === 4; } + +function isHour12(format: string) { + return dateTime().set('hour', 15).format(format) !== '15'; +} + +export function getSectionLimits(section: DateFieldSectionWithoutPosition, date: DateTime) { + const {type, format} = section; + switch (type) { + case 'year': { + const isFourDigit = isFourDigitYearFormat(format); + return { + minValue: isFourDigit ? 1 : 0, + maxValue: isFourDigit ? 9999 : 99, + }; + } + case 'month': { + return { + minValue: 0, + maxValue: 11, + }; + } + case 'weekday': { + return { + minValue: 0, + maxValue: 6, + }; + } + case 'day': { + return { + minValue: 1, + maxValue: date ? date.daysInMonth() : 31, + }; + } + case 'hour': { + if (isHour12(format)) { + const isPM = date.hour() >= 12; + return { + minValue: isPM ? 12 : 0, + maxValue: isPM ? 23 : 11, + }; + } + return { + minValue: 0, + maxValue: 23, + }; + } + case 'minute': + case 'second': { + return { + minValue: 0, + maxValue: 59, + }; + } + } + return {}; +} + +export function getSectionValue(sections: DateFieldSectionWithoutPosition, date: DateTime) { + const type = sections.type; + switch (type) { + case 'year': { + return isFourDigitYearFormat(sections.format) + ? date.year() + : Number(date.format(sections.format)); + } + case 'month': + case 'hour': + case 'minute': + case 'second': { + return date[type](); + } + case 'day': { + return date.date(); + } + case 'weekday': { + return date.day(); + } + case 'dayPeriod': { + return date.hour() >= 12 ? 12 : 0; + } + } + return undefined; +} + +const TYPE_MAPPING = { + weekday: 'day', + day: 'date', + dayPeriod: 'hour', +} as const; + +export function getDurationUnitFromSectionType(type: DateFieldSectionType) { + if (type === 'literal' || type === 'timeZoneName' || type === 'unknown') { + throw new Error(`${type} section does not have duration unit.`); + } + + if (type in TYPE_MAPPING) { + return TYPE_MAPPING[type as keyof typeof TYPE_MAPPING]; + } + + return type as Exclude< + DateFieldSectionType, + keyof typeof TYPE_MAPPING | 'literal' | 'timeZoneName' | 'unknown' + >; +} + +export function addSegment(section: DateFieldSection, date: DateTime, amount: number) { + let val = section.value ?? 0; + if (section.type === 'dayPeriod') { + val = date.hour() + (date.hour() > 12 ? -12 : 12); + } else { + val = val + amount; + const min = section.minValue; + const max = section.maxValue; + if (typeof min === 'number' && typeof max === 'number') { + const length = max - min + 1; + val = ((val - min + length) % length) + min; + } + } + + if (section.type === 'year' && !isFourDigitYearFormat(section.format)) { + val = dateTime({input: `${val}`.padStart(2, '0'), format: section.format}).year(); + } + + const type = getDurationUnitFromSectionType(section.type); + return date.set(type, val); +} + +export function setSegment(section: DateFieldSection, date: DateTime, amount: number) { + const type = section.type; + switch (type) { + case 'year': { + return date.set( + 'year', + isFourDigitYearFormat(section.format) + ? amount + : dateTime({ + input: `${amount}`.padStart(2, '0'), + format: section.format, + }).year(), + ); + } + case 'day': + case 'weekday': + case 'month': { + return date.set(getDurationUnitFromSectionType(type), amount); + } + case 'dayPeriod': { + const hours = date.hour(); + const wasPM = hours >= 12; + const isPM = amount >= 12; + if (isPM === wasPM) { + return date; + } + return date.set('hour', wasPM ? hours - 12 : hours + 12); + } + case 'hour': { + // In 12 hour time, ensure that AM/PM does not change + let sectionAmount = amount; + if (section.minValue === 12 || section.maxValue === 11) { + const hours = date.hour(); + const wasPM = hours >= 12; + if (!wasPM && sectionAmount === 12) { + sectionAmount = 0; + } + if (wasPM && sectionAmount < 12) { + sectionAmount += 12; + } + } + return date.set('hour', sectionAmount); + } + case 'minute': + case 'second': { + return date.set(type, amount); + } + } + + return date; +} + function doesSectionHaveLeadingZeros( contentType: 'digit' | 'letter', sectionType: DateFieldSectionType,