Skip to content

Commit f149209

Browse files
authored
feat: Add support for automatic reset after React 19 form actions (#8444)
1 parent f398fcc commit f149209

File tree

58 files changed

+898
-82
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+898
-82
lines changed

packages/@react-aria/button/src/useToggleButtonGroup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function useToggleButtonGroupItem(props: AriaToggleButtonGroupItemOptions
6868
export function useToggleButtonGroupItem(props: AriaToggleButtonGroupItemOptions<ElementType>, state: ToggleGroupState, ref: RefObject<any>): ToggleButtonAria<HTMLAttributes<any>> {
6969
let toggleState: ToggleState = {
7070
isSelected: state.selectedKeys.has(props.id),
71+
defaultSelected: false,
7172
setSelected(isSelected) {
7273
state.setSelected(props.id, isSelected);
7374
},

packages/@react-aria/checkbox/src/useCheckboxGroupItem.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function useCheckboxGroupItem(props: AriaCheckboxGroupItemProps, state: C
3030
const toggleState = useToggleState({
3131
isReadOnly: props.isReadOnly || state.isReadOnly,
3232
isSelected: state.isSelected(props.value),
33+
defaultSelected: state.defaultValue.includes(props.value),
3334
onChange(isSelected) {
3435
if (isSelected) {
3536
state.addValue(props.value);

packages/@react-aria/color/src/useColorArea.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,7 @@ export function useColorArea(props: AriaColorAreaOptions, state: ColorAreaState)
6969
}
7070
}, [inputXRef]);
7171

72-
useFormReset(inputXRef, [state.xValue, state.yValue], ([x, y]) => {
73-
let newColor = state.value
74-
.withChannelValue(state.channels.xChannel, x)
75-
.withChannelValue(state.channels.yChannel, y);
76-
state.setValue(newColor);
77-
});
72+
useFormReset(inputXRef, state.defaultValue, state.setValue);
7873

7974
let [valueChangedViaKeyboard, setValueChangedViaKeyboard] = useState(false);
8075
let [valueChangedViaInputChangeEvent, setValueChangedViaInputChangeEvent] = useState(false);

packages/@react-aria/color/src/useColorField.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
useCallback,
2121
useState
2222
} from 'react';
23-
import {mergeProps, useId} from '@react-aria/utils';
23+
import {mergeProps, useFormReset, useId} from '@react-aria/utils';
2424
import {privateValidationStateProp} from '@react-stately/form';
2525
import {useFocusWithin, useScrollWheel} from '@react-aria/interactions';
2626
import {useFormattedTextField} from '@react-aria/textfield';
@@ -108,14 +108,18 @@ export function useColorField(
108108
...props,
109109
id: inputId,
110110
value: inputValue,
111-
defaultValue: undefined,
111+
// Intentionally invalid value that will be ignored by onChange during form reset
112+
// This is handled separately below.
113+
defaultValue: '!',
112114
validate: undefined,
113115
[privateValidationStateProp]: state,
114116
type: 'text',
115117
autoComplete: 'off',
116118
onChange
117119
}, state, ref);
118120

121+
useFormReset(ref, state.defaultColorValue, state.setColorValue);
122+
119123
inputProps = mergeProps(inputProps, spinButtonProps, focusWithinProps, {
120124
role: 'textbox',
121125
'aria-valuemax': null,

packages/@react-aria/color/src/useColorWheel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function useColorWheel(props: AriaColorWheelOptions, state: ColorWheelSta
5858
}
5959
}, [inputRef]);
6060

61-
useFormReset(inputRef, state.hue, state.setHue);
61+
useFormReset(inputRef, state.defaultValue, state.setValue);
6262

6363
let currentPosition = useRef<{x: number, y: number} | null>(null);
6464

packages/@react-aria/combobox/src/useComboBox.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
210210
onKeyDown: !isReadOnly ? chain(state.isOpen && collectionProps.onKeyDown, onKeyDown, props.onKeyDown) : props.onKeyDown,
211211
onBlur,
212212
value: state.inputValue,
213+
defaultValue: state.defaultInputValue,
213214
onFocus,
214215
autoComplete: 'off',
215216
validate: undefined,

packages/@react-aria/datepicker/src/useDateField.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function useDateField<T extends DateValue>(props: AriaDateFieldOptions<T>
138138
autoFocusRef.current = false;
139139
}, [focusManager]);
140140

141-
useFormReset(props.inputRef, state.value, state.setValue);
141+
useFormReset(props.inputRef, state.defaultValue, state.setValue);
142142
useFormValidation({
143143
...props,
144144
focus() {

packages/@react-aria/datepicker/src/useDatePicker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
136136
[roleSymbol]: 'presentation',
137137
'aria-describedby': ariaDescribedBy,
138138
value: state.value,
139+
defaultValue: state.defaultValue,
139140
onChange: state.setValue,
140141
placeholderValue: props.placeholderValue,
141142
hideTimeZone: props.hideTimeZone,

packages/@react-aria/datepicker/src/useDateRangePicker.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
183183
...startFieldProps,
184184
...commonFieldProps,
185185
value: state.value?.start ?? null,
186+
defaultValue: state.defaultValue?.start,
186187
onChange: start => state.setDateTime('start', start),
187188
autoFocus: props.autoFocus,
188189
name: props.startName,
@@ -201,6 +202,7 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
201202
...endFieldProps,
202203
...commonFieldProps,
203204
value: state.value?.end ?? null,
205+
defaultValue: state.defaultValue?.end,
204206
onChange: end => state.setDateTime('end', end),
205207
name: props.endName,
206208
[privateValidationStateProp]: {

packages/@react-aria/form/src/useFormValidation.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {FormValidationState} from '@react-stately/form';
1414
import {RefObject, Validation, ValidationResult} from '@react-types/shared';
1515
import {setInteractionModality} from '@react-aria/interactions';
16-
import {useEffect} from 'react';
16+
import {useEffect, useRef} from 'react';
1717
import {useEffectEvent, useLayoutEffect} from '@react-aria/utils';
1818

1919
type ValidatableElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
@@ -43,8 +43,11 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
4343
}
4444
});
4545

46+
let isIgnoredReset = useRef(false);
4647
let onReset = useEffectEvent(() => {
47-
state.resetValidation();
48+
if (!isIgnoredReset.current) {
49+
state.resetValidation();
50+
}
4851
});
4952

5053
let onInvalid = useEffectEvent((e: Event) => {
@@ -82,13 +85,32 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
8285
}
8386

8487
let form = input.form;
88+
89+
let reset = form?.reset;
90+
if (form) {
91+
// Try to detect React's automatic form reset behavior so we don't clear
92+
// validation errors that are returned by server actions.
93+
// To do this, we ignore programmatic form resets that occur outside a user event.
94+
// This is best-effort. There may be false positives, e.g. setTimeout.
95+
form.reset = () => {
96+
// React uses MessageChannel for scheduling, so ignore 'message' events.
97+
isIgnoredReset.current = !window.event || (window.event.type === 'message' && window.event.target instanceof MessagePort);
98+
reset?.call(form);
99+
isIgnoredReset.current = false;
100+
};
101+
}
102+
85103
input.addEventListener('invalid', onInvalid);
86104
input.addEventListener('change', onChange);
87105
form?.addEventListener('reset', onReset);
88106
return () => {
89107
input!.removeEventListener('invalid', onInvalid);
90108
input!.removeEventListener('change', onChange);
91109
form?.removeEventListener('reset', onReset);
110+
if (form) {
111+
// @ts-ignore
112+
form.reset = reset;
113+
}
92114
};
93115
}, [ref, onInvalid, onChange, onReset, validationBehavior]);
94116
}

0 commit comments

Comments
 (0)