Skip to content

Commit a6d4909

Browse files
authored
refactor(experience): improve date field backspace behavior and tests (#7708)
1 parent d07e90d commit a6d4909

File tree

2 files changed

+102
-24
lines changed

2 files changed

+102
-24
lines changed

packages/experience/src/components/InputFields/DateField/index.test.tsx

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe('DateField Component', () => {
8181
expect(inputs[2]!.value).toBe('2023');
8282
});
8383

84-
test('backspace clears previous field when empty', () => {
84+
test('backspace deletes one character; moves to previous field when caret at start', () => {
8585
const handleChange = jest.fn();
8686
const { container } = render(
8787
<Controlled
@@ -98,28 +98,93 @@ describe('DateField Component', () => {
9898
fireEvent.click(labelNode!);
9999
});
100100
const inputs = Array.from(container.querySelectorAll('input'));
101+
const month = inputs[0]!;
102+
const day = inputs[1]!;
103+
const year = inputs[2]!;
101104

102-
// Focus last input and clear it with backspace until it moves to previous
103-
act(() => {
104-
inputs[2]!.focus();
105-
fireEvent.keyDown(inputs[2]!, { key: 'Backspace' }); // Clears year
106-
});
105+
// Initial assertions
106+
expect(month.value).toBe('08');
107+
expect(day.value).toBe('20');
108+
expect(year.value).toBe('2023');
109+
110+
const deleteLastChar = (input: HTMLInputElement) => {
111+
const current = input.value;
112+
const next = current.slice(0, -1);
113+
act(() => {
114+
fireEvent.keyDown(input, { key: 'Backspace' });
115+
// Simulate browser applying new value (Backspace default behavior)
116+
fireEvent.input(input, { target: { value: next } });
117+
});
118+
};
119+
120+
// Delete year digits one by one
121+
year.focus();
122+
deleteLastChar(year); // 2023 -> 202
123+
expect(handleChange).toHaveBeenLastCalledWith('08/20/202');
124+
deleteLastChar(year); // 202 -> 20
125+
expect(handleChange).toHaveBeenLastCalledWith('08/20/20');
126+
deleteLastChar(year); // 20 -> 2
127+
expect(handleChange).toHaveBeenLastCalledWith('08/20/2');
128+
deleteLastChar(year); // 2 -> '' (becomes 08/20/)
107129
expect(handleChange).toHaveBeenLastCalledWith('08/20/');
108130

109-
// Now last input is empty, backspace again should move to previous and clear day
131+
// Current (year) empty; Backspace now should move to previous (day) and delete its last char
110132
act(() => {
111-
fireEvent.keyDown(inputs[2]!, { key: 'Backspace' });
133+
fireEvent.keyDown(year, { key: 'Backspace' });
112134
});
135+
expect(handleChange).toHaveBeenLastCalledWith('08/2/');
136+
expect(document.activeElement).toBe(day);
137+
138+
// Delete remaining day digit via single-char deletion
139+
deleteLastChar(day); // 2 -> '' => 08//
113140
expect(handleChange).toHaveBeenLastCalledWith('08//');
114141

115-
// Finally clear the month field so that all parts empty => '' (not '//')
142+
// Backspace on empty day moves to month and deletes one char (08 -> 0)
116143
act(() => {
117-
inputs[0]!.focus();
118-
fireEvent.keyDown(inputs[0]!, { key: 'Backspace' });
144+
fireEvent.keyDown(day, { key: 'Backspace' });
119145
});
146+
expect(handleChange).toHaveBeenLastCalledWith('0//');
147+
expect(document.activeElement).toBe(month);
148+
149+
// Delete last month digit
150+
deleteLastChar(month); // 0 -> '' => all empty -> '' (not '//')
120151
expect(handleChange).toHaveBeenLastCalledWith('');
121-
const emittedValuesAfterFullClear = handleChange.mock.calls.map((call) => call[0]);
122-
expect(emittedValuesAfterFullClear.includes('//')).toBe(false);
152+
153+
const emittedValues = handleChange.mock.calls.map((call) => call[0]);
154+
// Ensure no final dangling '//' value
155+
expect(emittedValues.filter((value) => value === '//')).toHaveLength(0);
156+
});
157+
158+
test('backspace at start of non-empty field moves to previous and deletes last char there', () => {
159+
const handleChange = jest.fn();
160+
const { container } = render(
161+
<Controlled
162+
dateFormat={SupportedDateFormat.US}
163+
value="08/20/2023"
164+
label="Date of birth"
165+
onChange={handleChange}
166+
/>
167+
);
168+
const labelNode = Array.from(container.querySelectorAll('*')).find(
169+
(element) => element.textContent === 'Date of birth'
170+
);
171+
act(() => {
172+
fireEvent.click(labelNode!);
173+
});
174+
const inputs = Array.from(container.querySelectorAll('input'));
175+
const day = inputs[1]!; // Value '20'
176+
const month = inputs[0]!; // Value '08'
177+
// Place caret at start of day input
178+
act(() => {
179+
day.focus();
180+
// Simulate caret at start
181+
day.setSelectionRange(0, 0);
182+
fireEvent.keyDown(day, { key: 'Backspace' });
183+
});
184+
// Should have deleted last char of month (08 -> 0) keeping day intact
185+
expect(handleChange).toHaveBeenLastCalledWith('0/20/2023');
186+
expect(month.value).toBe('0');
187+
expect(day.value).toBe('20');
123188
});
124189

125190
test('paste distributes digits across inputs', () => {

packages/experience/src/components/InputFields/DateField/index.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,13 @@ const DateField = (props: Props) => {
108108

109109
const updateValue = useCallback(
110110
(data: string, targetId: number) => {
111-
if (!formatConfig || !isNumeric(data)) {
111+
if (data === '') {
112+
const clearedParts = dateParts.map((part, index) => (index === targetId ? '' : part));
113+
handleOnChange(clearedParts);
114+
return;
115+
}
116+
117+
if (!isNumeric(data)) {
112118
return;
113119
}
114120

@@ -121,7 +127,7 @@ const DateField = (props: Props) => {
121127
return currentParts;
122128
}
123129

124-
const fieldMaxLength = formatConfig.maxLengths[currentTargetId] ?? 0;
130+
const fieldMaxLength = formatConfig?.maxLengths[currentTargetId] ?? 0;
125131
const fieldData = remainingData.slice(0, fieldMaxLength);
126132
const updatedParts = currentParts.map((part, index) =>
127133
index === currentTargetId ? fieldData : part
@@ -200,17 +206,24 @@ const DateField = (props: Props) => {
200206

201207
switch (key) {
202208
case 'Backspace': {
203-
event.preventDefault();
209+
const caretAtStart = target.selectionStart === 0 && target.selectionEnd === 0;
204210

205-
if (value) {
206-
const clearedParts = dateParts.map((part, index) => (index === targetId ? '' : part));
207-
handleOnChange(clearedParts);
208-
} else if (previousTarget) {
209-
previousTarget.focus();
210-
const clearedParts = dateParts.map((part, index) =>
211-
index === targetId - 1 ? '' : part
211+
if (caretAtStart && previousTarget) {
212+
event.preventDefault();
213+
const previousValue = previousTarget.value;
214+
215+
if (!previousValue) {
216+
previousTarget.focus();
217+
return;
218+
}
219+
220+
const newPreviousValue = previousValue.slice(0, -1);
221+
const updatedParts = dateParts.map((part, index) =>
222+
index === targetId - 1 ? newPreviousValue : part
212223
);
213-
handleOnChange(clearedParts);
224+
handleOnChange(updatedParts);
225+
previousTarget.focus();
226+
previousTarget.setSelectionRange(newPreviousValue.length, newPreviousValue.length);
214227
}
215228
break;
216229
}

0 commit comments

Comments
 (0)