Skip to content

Commit 07214be

Browse files
nantosnowystinger
andauthored
fix: Don't reset form fields if reset event is cancelled. (#7603)
* fix: Don't reset form fields if reset event is cancelled. * tests, handle bubble preventdefault and stoppropagation * fix: Allow multiple form elements with useFormReset * simplify case again --------- Co-authored-by: Robert Snow <rsnow@adobe.com> Co-authored-by: Robert Snow <snowystinger@gmail.com>
1 parent 7902702 commit 07214be

2 files changed

Lines changed: 140 additions & 2 deletions

File tree

packages/@react-aria/utils/src/useFormReset.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ export function useFormReset<T>(
1919
initialValue: T,
2020
onReset: (value: T) => void
2121
): void {
22-
let handleReset = useEffectEvent(() => {
23-
if (onReset) {
22+
23+
let handleReset = useEffectEvent((e: Event) => {
24+
if (onReset && !e.defaultPrevented) {
2425
onReset(initialValue);
2526
}
2627
});
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2025 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
import {fireEvent, render} from '@react-spectrum/test-utils-internal';
14+
import React, {useRef} from 'react';
15+
import {useFormReset} from '../';
16+
17+
describe('useFormReset', () => {
18+
it('should call onReset on reset', () => {
19+
const onReset = jest.fn();
20+
const Form = () => {
21+
const ref = useRef<HTMLInputElement>(null);
22+
useFormReset(ref, '', onReset);
23+
return (
24+
<form>
25+
<input ref={ref} type="text" />
26+
<button type="reset">Reset</button>
27+
</form>
28+
);
29+
};
30+
const {getByRole} = render(<Form />);
31+
const button = getByRole('button');
32+
fireEvent.click(button);
33+
expect(onReset).toHaveBeenCalled();
34+
});
35+
36+
it('should call onReset on reset even if event is stopped', () => {
37+
const onReset = jest.fn();
38+
const Form = () => {
39+
const ref = useRef<HTMLInputElement>(null);
40+
useFormReset(ref, '', onReset);
41+
return (
42+
<form onReset={(e) => e.stopPropagation()}>
43+
<input ref={ref} type="text" />
44+
<button type="reset">Reset</button>
45+
</form>
46+
);
47+
};
48+
const {getByRole} = render(<Form />);
49+
const button = getByRole('button');
50+
fireEvent.click(button);
51+
expect(onReset).toHaveBeenCalled();
52+
});
53+
54+
it('should call every onReset on reset', () => {
55+
const onReset1 = jest.fn();
56+
const onReset2 = jest.fn();
57+
const Form = () => {
58+
const ref1 = useRef<HTMLInputElement>(null);
59+
useFormReset(ref1, '', onReset1);
60+
const ref2 = useRef<HTMLInputElement>(null);
61+
useFormReset(ref2, '', onReset2);
62+
return (
63+
<form>
64+
<input ref={ref1} type="text" />
65+
<input ref={ref2} type="text" />
66+
<button type="reset">Reset</button>
67+
</form>
68+
);
69+
};
70+
const {getByRole} = render(<Form />);
71+
const button = getByRole('button');
72+
fireEvent.click(button);
73+
expect(onReset1).toHaveBeenCalled();
74+
expect(onReset2).toHaveBeenCalled();
75+
});
76+
77+
it.skip('should not call onReset if reset is cancelled', async () => {
78+
// Simpler case at the moment, but you have to setup a capture listener to prevent the default behavior.
79+
// Matching native behavior is too much of a change until someone asks for it.
80+
const onReset = jest.fn();
81+
const Form = () => {
82+
const ref = useRef<HTMLInputElement>(null);
83+
useFormReset(ref, '', onReset);
84+
return (
85+
<form onReset={(e) => e.preventDefault()}>
86+
<input ref={ref} type="text" />
87+
<button type="reset">Reset</button>
88+
</form>
89+
);
90+
};
91+
const {getByRole} = render(<Form />);
92+
const button = getByRole('button');
93+
fireEvent.click(button);
94+
expect(onReset).not.toHaveBeenCalled();
95+
});
96+
97+
it('should not call onReset if reset is cancelled in capture phase', async () => {
98+
const onReset = jest.fn();
99+
const Form = () => {
100+
const ref = useRef<HTMLInputElement>(null);
101+
useFormReset(ref, '', onReset);
102+
return (
103+
<form onResetCapture={(e) => e.preventDefault()}>
104+
<input ref={ref} type="text" />
105+
<button type="reset">Reset</button>
106+
</form>
107+
);
108+
};
109+
const {getByRole} = render(<Form />);
110+
const button = getByRole('button');
111+
fireEvent.click(button);
112+
expect(onReset).not.toHaveBeenCalled();
113+
});
114+
115+
it('should not call any onReset if reset is cancelled', () => {
116+
const onReset1 = jest.fn();
117+
const onReset2 = jest.fn();
118+
const Form = () => {
119+
const ref1 = useRef<HTMLInputElement>(null);
120+
useFormReset(ref1, '', onReset1);
121+
const ref2 = useRef<HTMLInputElement>(null);
122+
useFormReset(ref2, '', onReset2);
123+
return (
124+
<form onResetCapture={(e) => e.preventDefault()}>
125+
<input ref={ref1} type="text" />
126+
<input ref={ref2} type="text" />
127+
<button type="reset">Reset</button>
128+
</form>
129+
);
130+
};
131+
const {getByRole} = render(<Form />);
132+
const button = getByRole('button');
133+
fireEvent.click(button);
134+
expect(onReset1).not.toHaveBeenCalled();
135+
expect(onReset2).not.toHaveBeenCalled();
136+
});
137+
});

0 commit comments

Comments
 (0)