Skip to content

Commit cad9a7e

Browse files
committed
fix(DateField): correctly manage format changing
1 parent ca3ae5d commit cad9a7e

File tree

6 files changed

+88
-77
lines changed

6 files changed

+88
-77
lines changed

src/components/DateField/hooks/useDateFieldProps.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
TextInputProps as DateFieldTextInputProps,
1010
DomProps,
1111
FocusableProps,
12+
InputDOMProps,
1213
KeyboardEvents,
1314
StyleProps,
1415
TextInputExtendProps,
@@ -22,6 +23,7 @@ export interface DateFieldProps<T = DateTime>
2223
DateFieldTextInputProps,
2324
TextInputExtendProps,
2425
DomProps,
26+
InputDOMProps,
2527
FocusableProps,
2628
KeyboardEvents,
2729
StyleProps,
@@ -58,14 +60,16 @@ export function useDateFieldProps<T = DateTime>(
5860

5961
const firstSelectedSection = state.sections[state.selectedSectionIndexes.startIndex];
6062
const lastSelectedSection = state.sections[state.selectedSectionIndexes.endIndex];
61-
const selectionStart = firstSelectedSection.start;
62-
const selectionEnd = lastSelectedSection.end;
63-
64-
if (
65-
selectionStart !== inputElement.selectionStart ||
66-
selectionEnd !== inputElement.selectionEnd
67-
) {
68-
inputElement.setSelectionRange(selectionStart, selectionEnd);
63+
if (firstSelectedSection && lastSelectedSection) {
64+
const selectionStart = firstSelectedSection.start;
65+
const selectionEnd = lastSelectedSection.end;
66+
67+
if (
68+
selectionStart !== inputElement.selectionStart ||
69+
selectionEnd !== inputElement.selectionEnd
70+
) {
71+
inputElement.setSelectionRange(selectionStart, selectionEnd);
72+
}
6973
}
7074
});
7175

@@ -221,10 +225,11 @@ export function useDateFieldProps<T = DateTime>(
221225
const digitsOnly = /^\d+$/.test(pastedValue);
222226
const lettersOnly = /^[a-zA-Z]+$/.test(pastedValue);
223227

224-
const isValidValue =
225-
Boolean(activeSection) &&
226-
((activeSection.contentType === 'digit' && digitsOnly) ||
227-
(activeSection.contentType === 'letter' && lettersOnly));
228+
const isValidValue = Boolean(
229+
activeSection &&
230+
((activeSection.contentType === 'digit' && digitsOnly) ||
231+
(activeSection.contentType === 'letter' && lettersOnly)),
232+
);
228233
if (isValidValue) {
229234
state.onInput(pastedValue);
230235
return;

src/components/DateField/hooks/useDateFieldState.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
EDITABLE_SEGMENTS,
1313
addSegment,
1414
getEditableSections,
15+
isAllSegmentsValid,
1516
parseDateFromString,
1617
setSegment,
17-
splitFormatIntoSections,
18+
useFormatSections,
1819
} from '../utils';
1920

2021
import {useBaseDateFieldState} from './useBaseDateFieldState';
@@ -64,13 +65,13 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
6465
let validSegments = validSegmentsState[0];
6566
const setValidSegments = validSegmentsState[1];
6667

67-
if (value && Object.keys(validSegments).length < Object.keys(allSegments).length) {
68+
if (value && !isAllSegmentsValid(allSegments, validSegments)) {
6869
setValidSegments({...allSegments});
6970
}
7071

7172
if (
7273
!value &&
73-
Object.keys(validSegments).length > 0 &&
74+
isAllSegmentsValid(allSegments, validSegments) &&
7475
Object.keys(validSegments).length === Object.keys(allSegments).length
7576
) {
7677
validSegments = {};
@@ -84,9 +85,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
8485
}
8586

8687
const displayValue =
87-
value &&
88-
isValid(value) &&
89-
Object.keys(validSegments).length >= Object.keys(allSegments).length
88+
value && isValid(value) && isAllSegmentsValid(allSegments, validSegments)
9089
? value.timeZone(timeZone)
9190
: placeholderDate.timeZone(timeZone);
9291
const sectionsState = useSectionsState(sections, displayValue, validSegments);
@@ -128,7 +127,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
128127
return;
129128
}
130129

131-
if (Object.keys(validSegments).length >= Object.keys(allSegments).length) {
130+
if (isAllSegmentsValid(allSegments, validSegments)) {
132131
if (!value || !newValue.isSame(value)) {
133132
handleUpdateDate(newValue);
134133
}
@@ -153,24 +152,31 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
153152

154153
function setSection(sectionIndex: number, amount: number) {
155154
const section = sectionsState.editableSections[sectionIndex];
156-
markValid(section.type);
157-
setValue(setSegment(section, displayValue, amount));
155+
if (section) {
156+
markValid(section.type);
157+
setValue(setSegment(section, displayValue, amount));
158+
}
158159
}
159160

160161
function adjustSection(sectionIndex: number, amount: number) {
161162
const section = sectionsState.editableSections[sectionIndex];
162-
if (validSegments[section.type]) {
163-
setValue(addSegment(section, displayValue, amount));
164-
} else {
165-
markValid(section.type);
166-
if (Object.keys(validSegments).length >= Object.keys(allSegments).length) {
167-
setValue(displayValue);
163+
if (section) {
164+
if (validSegments[section.type]) {
165+
setValue(addSegment(section, displayValue, amount));
166+
} else {
167+
markValid(section.type);
168+
if (Object.keys(validSegments).length >= Object.keys(allSegments).length) {
169+
setValue(displayValue);
170+
}
168171
}
169172
}
170173
}
171174

172175
function flushValidSection(sectionIndex: number) {
173-
delete validSegments[sectionsState.editableSections[sectionIndex].type];
176+
const section = sectionsState.editableSections[sectionIndex];
177+
if (section) {
178+
delete validSegments[section.type];
179+
}
174180
setValidSegments({...validSegments});
175181
}
176182

@@ -208,7 +214,7 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
208214
(isInvalid(value, props.minValue, props.maxValue) ? 'invalid' : undefined) ||
209215
(value && props.isDateUnavailable?.(value) ? 'invalid' : undefined);
210216

211-
return useBaseDateFieldState<DateTime>({
217+
return useBaseDateFieldState({
212218
value,
213219
displayValue,
214220
placeholderValue: props.placeholderValue,
@@ -234,19 +240,6 @@ export function useDateFieldState(props: DateFieldStateOptions): DateFieldState
234240
});
235241
}
236242

237-
function useFormatSections(format: string) {
238-
const usedFormat = format;
239-
const [sections, setSections] = React.useState(() => splitFormatIntoSections(usedFormat));
240-
241-
const [previousFormat, setFormat] = React.useState(usedFormat);
242-
if (usedFormat !== previousFormat) {
243-
setFormat(usedFormat);
244-
setSections(splitFormatIntoSections(usedFormat));
245-
}
246-
247-
return sections;
248-
}
249-
250243
function useSectionsState(
251244
sections: DateFieldSectionWithoutPosition[],
252245
value: DateTime,

src/components/DateField/utils.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import React from 'react';
2+
13
import {dateTime, isValid, settings} from '@gravity-ui/date-utils';
24
import type {DateTime} from '@gravity-ui/date-utils';
35

@@ -414,7 +416,7 @@ export function splitFormatIntoSections(format: string) {
414416
let isSeparator = false;
415417
let isInEscapeBoundary = false;
416418
for (let i = 0; i < expandedFormat.length; i++) {
417-
const char = expandedFormat[i];
419+
const char = expandedFormat[i] || '';
418420
if (isInEscapeBoundary) {
419421
if (char === escapedCharacters.end) {
420422
isInEscapeBoundary = false;
@@ -541,6 +543,9 @@ export function getEditableSections(
541543
let previousEditableSection = -1;
542544
for (let i = 0; i < sections.length; i++) {
543545
const section = sections[i];
546+
if (!section) {
547+
continue;
548+
}
544549

545550
const newSection = toEditableSection(
546551
section,
@@ -652,3 +657,25 @@ export function parseDateFromString(str: string, format: string, timeZone?: stri
652657

653658
return date;
654659
}
660+
661+
export function isAllSegmentsValid(
662+
allSegments: typeof EDITABLE_SEGMENTS,
663+
validSegments: typeof EDITABLE_SEGMENTS,
664+
) {
665+
return Object.keys(allSegments).every(
666+
(key) => validSegments[key as keyof typeof EDITABLE_SEGMENTS],
667+
);
668+
}
669+
670+
export function useFormatSections(format: string) {
671+
const usedFormat = format;
672+
const [sections, setSections] = React.useState(() => splitFormatIntoSections(usedFormat));
673+
674+
const [previousFormat, setFormat] = React.useState(usedFormat);
675+
if (usedFormat !== previousFormat) {
676+
setFormat(usedFormat);
677+
setSections(splitFormatIntoSections(usedFormat));
678+
}
679+
680+
return sections;
681+
}

src/components/DatePicker/hooks/useDatePickerProps.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,6 @@ export function useDatePickerProps(
4141

4242
const {inputProps} = useDateFieldProps(state.dateFieldState, props);
4343

44-
let error: string | boolean | undefined;
45-
let validationState = props.validationState;
46-
if (validationState) {
47-
error = validationState === 'invalid' ? (props.errorMessage as string) || true : undefined;
48-
} else {
49-
validationState = state.dateFieldState.validationState;
50-
error = validationState === 'invalid';
51-
}
52-
5344
const inputRef = React.useRef<HTMLInputElement>(null);
5445

5546
const handleRef = useForkRef(inputRef, inputProps.controlRef);
@@ -85,7 +76,7 @@ export function useDatePickerProps(
8576
state.dateFieldState.isEmpty && !isActive && props.placeholder
8677
? {value: ''}
8778
: undefined,
88-
{controlRef: handleRef, error},
79+
{controlRef: handleRef},
8980
),
9081
calendarButtonProps: {
9182
ref: calendarButtonRef,

src/components/DatePicker/hooks/useDatePickerState.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,17 @@ export function useDatePickerState(props: DatePickerStateOptions): DatePickerSta
142142
return;
143143
}
144144

145-
if (selectedDate && newValue) {
146-
commitValue(selectedDate, newValue);
145+
const newTime =
146+
newValue ??
147+
createPlaceholderValue({
148+
placeholderValue: props.placeholderValue,
149+
timeZone,
150+
});
151+
152+
if (selectedDate) {
153+
commitValue(selectedDate, newTime);
147154
} else {
148-
setSelectedTime(newValue);
155+
setSelectedTime(newTime);
149156
}
150157
};
151158

src/components/RangeDateField/hooks/useRangeDateFieldState.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import type {DateFieldSectionType, DateFieldSectionWithoutPosition} from '../../
1111
import {
1212
EDITABLE_SEGMENTS,
1313
addSegment,
14+
isAllSegmentsValid,
1415
parseDateFromString,
1516
setSegment,
16-
splitFormatIntoSections,
17+
useFormatSections,
1718
} from '../../DateField/utils';
1819
import type {DateFieldBase} from '../../types/datePicker';
1920
import type {RangeValue} from '../../types/inputs';
@@ -71,17 +72,17 @@ export function useRangeDateFieldState(props: RangeDateFieldStateOptions): Range
7172

7273
if (
7374
value &&
74-
(Object.keys(validSegments.start).length < Object.keys(allSegments).length ||
75-
Object.keys(validSegments.end).length < Object.keys(allSegments).length)
75+
(!isAllSegmentsValid(allSegments, validSegments.start) ||
76+
!isAllSegmentsValid(allSegments, validSegments.end))
7677
) {
7778
setValidSegments({start: {...allSegments}, end: {...allSegments}});
7879
}
7980

8081
if (
8182
!value &&
82-
Object.keys(validSegments.start).length > 0 &&
83+
isAllSegmentsValid(allSegments, validSegments.start) &&
8384
Object.keys(validSegments.start).length === Object.keys(allSegments).length &&
84-
Object.keys(validSegments.end).length > 0 &&
85+
isAllSegmentsValid(allSegments, validSegments.end) &&
8586
Object.keys(validSegments.end).length === Object.keys(allSegments).length
8687
) {
8788
validSegments = {start: {}, end: {}};
@@ -145,8 +146,8 @@ export function useRangeDateFieldState(props: RangeDateFieldStateOptions): Range
145146
}
146147

147148
if (
148-
Object.keys(validSegments.start).length >= Object.keys(allSegments).length &&
149-
Object.keys(validSegments.end).length >= Object.keys(allSegments).length
149+
isAllSegmentsValid(allSegments, validSegments.start) &&
150+
isAllSegmentsValid(allSegments, validSegments.end)
150151
) {
151152
if (!value || !(newValue.start.isSame(value.start) && newValue.end.isSame(value.end))) {
152153
handleUpdateRange(newValue);
@@ -249,7 +250,7 @@ export function useRangeDateFieldState(props: RangeDateFieldStateOptions): Range
249250
(value && props.isDateUnavailable?.(value.start) ? 'invalid' : undefined) ||
250251
(value && props.isDateUnavailable?.(value.end) ? 'invalid' : undefined);
251252

252-
return useBaseDateFieldState<RangeValue<DateTime>>({
253+
return useBaseDateFieldState({
253254
value,
254255
displayValue,
255256
placeholderValue: props.placeholderValue,
@@ -277,19 +278,6 @@ export function useRangeDateFieldState(props: RangeDateFieldStateOptions): Range
277278
});
278279
}
279280

280-
function useFormatSections(format: string) {
281-
const usedFormat = format;
282-
const [sections, setSections] = React.useState(() => splitFormatIntoSections(usedFormat));
283-
284-
const [previousFormat, setFormat] = React.useState(usedFormat);
285-
if (usedFormat !== previousFormat) {
286-
setFormat(usedFormat);
287-
setSections(splitFormatIntoSections(usedFormat));
288-
}
289-
290-
return sections;
291-
}
292-
293281
function useSectionsState(
294282
sections: DateFieldSectionWithoutPosition[],
295283
value: RangeValue<DateTime>,

0 commit comments

Comments
 (0)