Skip to content

Commit 0d23300

Browse files
authored
fix(DateField): correctly work with 2-digits years (#22)
1 parent 0872c11 commit 0d23300

File tree

3 files changed

+220
-177
lines changed

3 files changed

+220
-177
lines changed

src/components/DateField/hooks/useDateFieldProps.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -176,40 +176,46 @@ export function useDateFieldProps(
176176
onMouseUp(e: React.MouseEvent) {
177177
e.preventDefault();
178178
},
179-
onBeforeInput(e: React.FormEvent) {
179+
onBeforeInput(e) {
180180
e.preventDefault();
181181
// @ts-expect-error
182182
const key = e.data;
183-
// eslint-disable-next-line no-eq-null, eqeqeq
184-
if (key != null) {
183+
if (key !== undefined && key !== null) {
185184
state.onInput(key);
186185
}
187186
},
188187
onPaste(e: React.ClipboardEvent) {
188+
e.preventDefault();
189189
if (state.readOnly) {
190-
e.preventDefault();
191190
return;
192191
}
193192

194193
const pastedValue = e.clipboardData.getData('text');
195-
if (state.setValueFromString(pastedValue)) {
196-
e.preventDefault();
197-
} else if (
194+
if (
198195
state.selectedSectionIndexes &&
199196
state.selectedSectionIndexes.startIndex ===
200197
state.selectedSectionIndexes.endIndex
201198
) {
202199
const activeSection =
203200
state.sections[state.selectedSectionIndexes.startIndex];
204201

202+
const digitsOnly = /^\d+$/.test(pastedValue);
203+
const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue);
204+
205205
const isValidValue =
206206
Boolean(activeSection) &&
207-
((activeSection.contentType === 'digit' && /^\d+$/.test(pastedValue)) ||
208-
activeSection.contentType === 'letter');
209-
if (!isValidValue) {
210-
e.preventDefault();
207+
((activeSection.contentType === 'digit' && digitsOnly) ||
208+
(activeSection.contentType === 'letter' && lettersOnly));
209+
if (isValidValue) {
210+
state.onInput(pastedValue);
211+
return;
212+
}
213+
if (digitsOnly || lettersOnly) {
214+
return;
211215
}
212216
}
217+
218+
state.setValueFromString(pastedValue);
213219
},
214220
},
215221
},

src/components/DateField/hooks/useDateFieldState.ts

Lines changed: 23 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ import type {
1212
DateFieldSectionType,
1313
DateFieldSectionWithoutPosition,
1414
} from '../types';
15-
import {splitFormatIntoSections} from '../utils';
15+
import {
16+
addSegment,
17+
getDurationUnitFromSectionType,
18+
getSectionLimits,
19+
getSectionValue,
20+
setSegment,
21+
splitFormatIntoSections,
22+
} from '../utils';
1623

1724
export interface DateFieldStateOptions extends DateFieldBase {}
1825

@@ -27,12 +34,6 @@ const EDITABLE_SEGMENTS: Partial<Record<DateFieldSectionType, boolean>> = {
2734
weekday: true,
2835
};
2936

30-
const TYPE_MAPPING = {
31-
weekday: 'day',
32-
day: 'date',
33-
dayPeriod: 'hour',
34-
} as const;
35-
3637
const PAGE_STEP: Partial<Record<DateFieldSectionType, number>> = {
3738
year: 5,
3839
month: 2,
@@ -194,8 +195,13 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
194195
}
195196

196197
if (Object.keys(validSegments).length >= Object.keys(allSegments).length) {
197-
setDate(newValue);
198+
if (!value || !newValue.isSame(value)) {
199+
setDate(newValue);
200+
}
198201
} else {
202+
if (value) {
203+
setDate(null);
204+
}
199205
setPlaceholderDate(newValue);
200206
}
201207
}
@@ -396,9 +402,6 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
396402
currentValue = displayValue.set(type, placeholder[type]());
397403
}
398404

399-
if (value) {
400-
setDate(null);
401-
}
402405
setValue(currentValue);
403406
},
404407
clearAll() {
@@ -534,6 +537,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
534537
}
535538
},
536539
setValueFromString(str: string) {
540+
enteredKeys.current = '';
537541
let date = parseDate({input: str, format, timeZone: props.timeZone});
538542
if (isValid(date)) {
539543
if (props.timeZone && !isDateStringWithTimeZone(str)) {
@@ -560,65 +564,6 @@ function isDateStringWithTimeZone(str: string) {
560564
return /z$/i.test(str) || /[+-]\d\d:\d\d$/.test(str);
561565
}
562566

563-
function addSegment(section: DateFieldSection, date: DateTime, amount: number) {
564-
let val = section.value ?? 0;
565-
if (section.type === 'dayPeriod') {
566-
val = date.hour() + (date.hour() > 12 ? -12 : 12);
567-
} else {
568-
val = val + amount;
569-
const min = section.minValue;
570-
const max = section.maxValue;
571-
if (typeof min === 'number' && typeof max === 'number') {
572-
const length = max - min + 1;
573-
val = ((val - min + length) % length) + min;
574-
}
575-
}
576-
const type = getDurationUnitFromSectionType(section.type);
577-
return date.set(type, val);
578-
}
579-
580-
function setSegment(section: DateFieldSection, date: DateTime, amount: number) {
581-
const type = section.type;
582-
switch (type) {
583-
case 'day':
584-
case 'weekday':
585-
case 'month':
586-
case 'year': {
587-
return date.set(getDurationUnitFromSectionType(type), amount);
588-
}
589-
case 'dayPeriod': {
590-
const hours = date.hour();
591-
const wasPM = hours >= 12;
592-
const isPM = amount >= 12;
593-
if (isPM === wasPM) {
594-
return date;
595-
}
596-
return date.set('hour', wasPM ? hours - 12 : hours + 12);
597-
}
598-
case 'hour': {
599-
// In 12 hour time, ensure that AM/PM does not change
600-
let sectionAmount = amount;
601-
if (section.minValue === 12 || section.maxValue === 11) {
602-
const hours = date.hour();
603-
const wasPM = hours >= 12;
604-
if (!wasPM && sectionAmount === 12) {
605-
sectionAmount = 0;
606-
}
607-
if (wasPM && sectionAmount < 12) {
608-
sectionAmount += 12;
609-
}
610-
}
611-
return date.set('hour', sectionAmount);
612-
}
613-
case 'minute':
614-
case 'second': {
615-
return date.set(type, amount);
616-
}
617-
}
618-
619-
return date;
620-
}
621-
622567
function getCurrentEditableSectionIndex(
623568
sections: DateFieldSection[],
624569
selectedSections: 'all' | number,
@@ -690,20 +635,26 @@ function getEditableSections(
690635
let renderedValue = section.placeholder;
691636
if ((isEditable && validSegments[section.type]) || section.type === 'timeZoneName') {
692637
renderedValue = value.format(section.format);
638+
if (
639+
section.contentType === 'digit' &&
640+
renderedValue.length < section.placeholder.length
641+
) {
642+
renderedValue = renderedValue.padStart(section.placeholder.length, '0');
643+
}
693644
}
694645

695646
const sectionLength = renderedValue.length;
696647

697648
const newSection = {
698649
...section,
699-
value: getValue(value, section.type),
650+
value: getSectionValue(section, value),
700651
textValue: renderedValue,
701652
start: position,
702653
end: position + sectionLength,
703654
modified: false,
704655
previousEditableSection,
705656
nextEditableSection: previousEditableSection,
706-
...getSectionLimits(value, section.type, {hour12: isHour12(section)}),
657+
...getSectionLimits(section, value),
707658
};
708659

709660
newSections.push(newSection);
@@ -726,97 +677,3 @@ function getEditableSections(
726677

727678
return newSections;
728679
}
729-
730-
function getValue(date: DateTime, type: DateFieldSectionType) {
731-
switch (type) {
732-
case 'year':
733-
case 'month':
734-
case 'hour':
735-
case 'minute':
736-
case 'second': {
737-
return date[type]();
738-
}
739-
case 'day': {
740-
return date.date();
741-
}
742-
case 'weekday': {
743-
return date.day();
744-
}
745-
case 'dayPeriod': {
746-
return date.hour() >= 12 ? 12 : 0;
747-
}
748-
}
749-
return undefined;
750-
}
751-
752-
function isHour12(section: DateFieldSectionWithoutPosition) {
753-
if (section.type === 'hour') {
754-
return dateTime().set('hour', 15).format(section.format) !== '15';
755-
}
756-
return false;
757-
}
758-
759-
function getSectionLimits(date: DateTime, type: DateFieldSectionType, options: {hour12: boolean}) {
760-
switch (type) {
761-
case 'year': {
762-
return {
763-
minValue: 1,
764-
maxValue: 9999,
765-
};
766-
}
767-
case 'month': {
768-
return {
769-
minValue: 0,
770-
maxValue: 11,
771-
};
772-
}
773-
case 'weekday': {
774-
return {
775-
minValue: 0,
776-
maxValue: 6,
777-
};
778-
}
779-
case 'day': {
780-
return {
781-
minValue: 1,
782-
maxValue: date ? date.daysInMonth() : 31,
783-
};
784-
}
785-
case 'hour': {
786-
if (options.hour12) {
787-
const isPM = date.hour() >= 12;
788-
return {
789-
minValue: isPM ? 12 : 0,
790-
maxValue: isPM ? 23 : 11,
791-
};
792-
}
793-
return {
794-
minValue: 0,
795-
maxValue: 23,
796-
};
797-
}
798-
case 'minute':
799-
case 'second': {
800-
return {
801-
minValue: 0,
802-
maxValue: 59,
803-
};
804-
}
805-
}
806-
return {};
807-
}
808-
809-
function getDurationUnitFromSectionType(type: DateFieldSectionType) {
810-
if (type === 'literal' || type === 'timeZoneName' || type === 'unknown') {
811-
throw new Error(`${type} section does not have duration unit.`);
812-
}
813-
814-
if (type in TYPE_MAPPING) {
815-
return TYPE_MAPPING[type as keyof typeof TYPE_MAPPING];
816-
}
817-
818-
return type as Exclude<
819-
DateFieldSectionType,
820-
keyof typeof TYPE_MAPPING | 'literal' | 'timeZoneName' | 'unknown'
821-
>;
822-
}

0 commit comments

Comments
 (0)