-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: Don't reset form fields if reset event is cancelled. #7603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Thanks for the PR! |
Thank you for your follow-up! I agree with supporting bubble phase case and stopPropagation case. However, creating a new event causes infinite recursion when there are more than one diff --git a/packages/@react-aria/utils/src/useFormReset.ts b/packages/@react-aria/utils/src/useFormReset.ts
index b4bf47c3a..d0777c125 100644
--- a/packages/@react-aria/utils/src/useFormReset.ts
+++ b/packages/@react-aria/utils/src/useFormReset.ts
@@ -19,56 +19,24 @@ export function useFormReset<T>(
initialValue: T,
onReset: (value: T) => void
): void {
- let currEvent = useRef<Event | null>(null);
let resetValue = useRef(initialValue);
- let handleReset = useEffectEvent((e: Event) => {
- currEvent.current = null;
- if (
- e.target === ref?.current?.form
- && onReset
- && !e.defaultPrevented
- ) {
- onReset(resetValue.current);
- }
- });
-
- /**
- * Because event.stopPropagation() does not preventDefault and because we attach directly to the form element unlike React
- * we need to create a new event which lets us monitor stopPropagation and preventDefault. This allows us to call onReset
- * as the browser would natively.
- */
let formListener = useEffectEvent((e: Event) => {
- if (currEvent.current !== null || e.target !== ref?.current?.form) {
- // This is the re-dispatched event. Or it's for a different form.
+ if (e.target !== ref?.current?.form) {
return;
}
- let alreadyPrevented = e.defaultPrevented;
- e.stopPropagation();
- e.preventDefault();
- let event = new Event('reset', {bubbles: true, cancelable: true});
- currEvent.current = event;
- let originalStopPropagation = event.stopPropagation;
- let stopPropagation = () => {
- currEvent.current = null;
- originalStopPropagation.call(event);
- if (!alreadyPrevented && !event.defaultPrevented && onReset) {
+ queueMicrotask(() => {
+ if (!e.defaultPrevented && onReset) {
onReset(resetValue.current);
}
- };
- event.stopPropagation = stopPropagation;
-
- e.target?.dispatchEvent(event);
+ });
});
useEffect(() => {
- let form = ref?.current?.form;
- let document = form?.ownerDocument;
+ let document = ref?.current?.form?.ownerDocument;
document?.addEventListener('reset', formListener, true);
- document?.addEventListener('reset', handleReset);
return () => {
document?.removeEventListener('reset', formListener, true);
- document?.removeEventListener('reset', handleReset);
};
- }, [ref, handleReset, formListener]);
+ }, [ref, formListener]);
}
diff --git a/packages/@react-aria/utils/test/useFormReset.test.tsx b/packages/@react-aria/utils/test/useFormReset.test.tsx
index 49c1151c4..91dd3ef3d 100644
--- a/packages/@react-aria/utils/test/useFormReset.test.tsx
+++ b/packages/@react-aria/utils/test/useFormReset.test.tsx
@@ -10,12 +10,12 @@
* governing permissions and limitations under the License.
*/
-import {fireEvent, render} from '@react-spectrum/test-utils-internal';
+import {act, fireEvent, render} from '@react-spectrum/test-utils-internal';
import React, {useRef} from 'react';
import {useFormReset} from '../';
describe('useFormReset', () => {
- it('should call onReset on reset', () => {
+ it('should call onReset on reset', async () => {
const onReset = jest.fn();
const Form = () => {
const ref = useRef<HTMLInputElement>(null);
@@ -29,11 +29,11 @@ describe('useFormReset', () => {
};
const {getByRole} = render(<Form />);
const button = getByRole('button');
- fireEvent.click(button);
+ await act(async () => fireEvent.click(button));
expect(onReset).toHaveBeenCalled();
});
- it('should call onReset on reset even if event is stopped', () => {
+ it('should call onReset on reset even if event is stopped', async () => {
const onReset = jest.fn();
const Form = () => {
const ref = useRef<HTMLInputElement>(null);
@@ -47,7 +47,7 @@ describe('useFormReset', () => {
};
const {getByRole} = render(<Form />);
const button = getByRole('button');
- fireEvent.click(button);
+ await act(async () => fireEvent.click(button));
expect(onReset).toHaveBeenCalled();
});
@@ -65,7 +65,7 @@ describe('useFormReset', () => {
};
const {getByRole} = render(<Form />);
const button = getByRole('button');
- fireEvent.click(button);
+ await act(async () => fireEvent.click(button));
expect(onReset).not.toHaveBeenCalled();
});
@@ -83,7 +83,30 @@ describe('useFormReset', () => {
};
const {getByRole} = render(<Form />);
const button = getByRole('button');
- fireEvent.click(button);
+ await act(async () => fireEvent.click(button));
expect(onReset).not.toHaveBeenCalled();
});
+
+ it('should not call any onReset if reset is cancelled', async () => {
+ const onReset1 = jest.fn();
+ const onReset2 = jest.fn();
+ const Form = () => {
+ const ref1 = useRef<HTMLInputElement>(null);
+ useFormReset(ref1, '', onReset1);
+ const ref2 = useRef<HTMLInputElement>(null);
+ useFormReset(ref2, '', onReset2);
+ return (
+ <form onReset={(e) => e.preventDefault()}>
+ <input ref={ref1} type="text" />
+ <input ref={ref2} type="text" />
+ <button type="reset">Reset</button>
+ </form>
+ );
+ };
+ const {getByRole} = render(<Form />);
+ const button = getByRole('button');
+ await act(async () => fireEvent.click(button));
+ expect(onReset1).not.toHaveBeenCalled();
+ expect(onReset2).not.toHaveBeenCalled();
+ });
}); |
Right, I didn't want to use |
OK, I try to handle reset event with custom properties. |
Native form fields don't get reset if a
reset
event is cancelled. I think it is good that React Aria components behave as the same as native form fields.Closes #7602
✅ Pull Request Checklist:
📝 Test Instructions:
🧢 Your Project: