Skip to content

Commit 8068345

Browse files
committed
update components
1 parent 76a75cb commit 8068345

File tree

15 files changed

+297
-61
lines changed

15 files changed

+297
-61
lines changed

packages/react/src/autocomplete/root/AutocompleteRoot.test.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ describe('<Autocomplete.Root />', () => {
10551055

10561056
it('prop: validate', async () => {
10571057
await render(
1058-
<Field.Root validate={() => 'error'}>
1058+
<Field.Root validationMode="onBlur" validate={() => 'error'}>
10591059
<Autocomplete.Root>
10601060
<Autocomplete.Input data-testid="input" />
10611061
<Autocomplete.Portal>
@@ -1066,13 +1066,56 @@ describe('<Autocomplete.Root />', () => {
10661066
);
10671067

10681068
const input = screen.getByTestId('input');
1069+
expect(input).not.to.have.attribute('aria-invalid');
1070+
1071+
fireEvent.focus(input);
1072+
fireEvent.blur(input);
1073+
await flushMicrotasks();
1074+
expect(input).to.have.attribute('aria-invalid', 'true');
1075+
});
10691076

1077+
it('prop: validationMode=onSubmit', async () => {
1078+
const { user } = await render(
1079+
<Form>
1080+
<Field.Root
1081+
validate={(value) => {
1082+
return value === 'one' ? 'error' : null;
1083+
}}
1084+
>
1085+
<Autocomplete.Root required>
1086+
<Autocomplete.Input data-testid="input" />
1087+
<Autocomplete.Portal>
1088+
<Autocomplete.Positioner>
1089+
<Autocomplete.Popup>
1090+
<Autocomplete.List>
1091+
<Autocomplete.Item value="one">Option 1</Autocomplete.Item>
1092+
<Autocomplete.Item value="two">Option 2</Autocomplete.Item>
1093+
</Autocomplete.List>
1094+
</Autocomplete.Popup>
1095+
</Autocomplete.Positioner>
1096+
</Autocomplete.Portal>
1097+
</Autocomplete.Root>
1098+
</Field.Root>
1099+
<button type="submit">submit</button>
1100+
</Form>,
1101+
);
1102+
1103+
const input = screen.getByTestId('input');
10701104
expect(input).not.to.have.attribute('aria-invalid');
10711105

1072-
await act(async () => input.focus());
1073-
await act(async () => input.blur());
1106+
await user.click(screen.getByText('submit'));
1107+
expect(input).to.have.attribute('aria-invalid', 'true');
10741108

1109+
await user.type(input, 'two');
1110+
expect(input).not.to.have.attribute('aria-invalid');
1111+
1112+
await user.clear(input);
1113+
await user.type(input, 'one');
10751114
expect(input).to.have.attribute('aria-invalid', 'true');
1115+
1116+
await user.clear(input);
1117+
await user.type(input, 'three');
1118+
expect(input).not.to.have.attribute('aria-invalid');
10761119
});
10771120

10781121
// flaky in real browser

packages/react/src/checkbox/root/CheckboxRoot.test.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -583,22 +583,31 @@ describe('<Checkbox.Root />', () => {
583583
expect(button).not.to.have.attribute('data-invalid');
584584
});
585585

586-
it('prop: validate', async () => {
586+
it('prop: validationMode=onSubmit', async () => {
587587
await render(
588-
<Field.Root validate={() => 'error'}>
589-
<Checkbox.Root data-testid="button" />
590-
<Field.Error data-testid="error" />
591-
</Field.Root>,
588+
<Form>
589+
<Field.Root>
590+
<Checkbox.Root required />
591+
<Field.Error data-testid="error" />
592+
</Field.Root>
593+
<button type="submit">submit</button>
594+
</Form>,
592595
);
593596

594-
const button = screen.getByTestId('button');
597+
const checkbox = screen.getByRole('checkbox');
595598

596-
expect(button).not.to.have.attribute('aria-invalid');
599+
expect(checkbox).not.to.have.attribute('aria-invalid');
597600

598-
fireEvent.focus(button);
599-
fireEvent.blur(button);
601+
fireEvent.click(screen.getByText('submit'));
602+
expect(checkbox).to.have.attribute('aria-invalid', 'true');
600603

601-
expect(button).to.have.attribute('aria-invalid', 'true');
604+
fireEvent.click(checkbox);
605+
expect(checkbox).to.have.attribute('data-checked', '');
606+
expect(checkbox).not.to.have.attribute('aria-invalid');
607+
608+
fireEvent.click(checkbox);
609+
expect(checkbox).to.have.attribute('data-unchecked');
610+
expect(checkbox).to.have.attribute('aria-invalid');
602611
});
603612

604613
it('props: validationMode=onChange', async () => {

packages/react/src/checkbox/root/CheckboxRoot.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const CheckboxRoot = React.forwardRef(function CheckboxRoot(
6969
state: fieldState,
7070
validationMode,
7171
validityData,
72+
shouldValidateOnChange,
7273
} = useFieldRootContext();
7374
const fieldItemContext = useFieldItemContext();
7475
const { labelId, controlId, setControlId, getDescriptionProps } = useLabelableContext();
@@ -239,7 +240,7 @@ export const CheckboxRoot = React.forwardRef(function CheckboxRoot(
239240
if (!groupContext) {
240241
setFilled(nextChecked);
241242

242-
if (validationMode === 'onChange') {
243+
if (shouldValidateOnChange()) {
243244
fieldControlValidation.commitValidation(nextChecked);
244245
} else {
245246
fieldControlValidation.commitValidation(nextChecked, true);
@@ -254,7 +255,7 @@ export const CheckboxRoot = React.forwardRef(function CheckboxRoot(
254255
setGroupValue(nextGroupValue, details);
255256
setFilled(nextGroupValue.length > 0);
256257

257-
if (validationMode === 'onChange') {
258+
if (shouldValidateOnChange()) {
258259
fieldControlValidation.commitValidation(nextGroupValue);
259260
} else {
260261
fieldControlValidation.commitValidation(nextGroupValue, true);

packages/react/src/combobox/root/AriaCombobox.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
112112
const {
113113
setDirty,
114114
validityData,
115-
validationMode,
115+
shouldValidateOnChange,
116116
setFilled,
117117
name: fieldName,
118118
disabled: fieldDisabled,
@@ -560,7 +560,7 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
560560
clearErrors(name);
561561
commitValidation?.(selectedValue, true);
562562

563-
if (validationMode === 'onChange') {
563+
if (shouldValidateOnChange()) {
564564
commitValidation?.(selectedValue);
565565
}
566566

@@ -575,7 +575,7 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
575575
clearErrors(name);
576576
commitValidation?.(inputValue, true);
577577

578-
if (validationMode === 'onChange') {
578+
if (shouldValidateOnChange()) {
579579
commitValidation?.(inputValue);
580580
}
581581

@@ -1152,7 +1152,7 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
11521152
setDirty(nextValue !== validityData.initialValue);
11531153
setInputValue(nextValue, details);
11541154

1155-
if (validationMode === 'onChange') {
1155+
if (shouldValidateOnChange()) {
11561156
fieldControlValidation.commitValidation(nextValue);
11571157
}
11581158
return;
@@ -1170,7 +1170,7 @@ export function AriaCombobox<Value = any, Mode extends SelectionMode = 'none'>(
11701170
setDirty(matchingValue !== validityData.initialValue);
11711171
setSelectedValue?.(matchingValue, details);
11721172

1173-
if (validationMode === 'onChange') {
1173+
if (shouldValidateOnChange()) {
11741174
fieldControlValidation.commitValidation(matchingValue);
11751175
}
11761176
}

packages/react/src/combobox/root/ComboboxRoot.test.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3452,7 +3452,7 @@ describe('<Combobox.Root />', () => {
34523452

34533453
it('prop: validate', async () => {
34543454
await render(
3455-
<Field.Root validate={() => 'error'}>
3455+
<Field.Root validationMode="onBlur" validate={() => 'error'}>
34563456
<Combobox.Root>
34573457
<Combobox.Input data-testid="input" />
34583458
<Combobox.Portal>
@@ -3474,6 +3474,52 @@ describe('<Combobox.Root />', () => {
34743474
expect(input).to.have.attribute('aria-invalid', 'true');
34753475
});
34763476

3477+
it('prop: validationMode=onSubmit', async () => {
3478+
const { user } = await render(
3479+
<Form>
3480+
<Field.Root validate={(val) => (val === 'a' ? 'error' : null)}>
3481+
<Combobox.Root required>
3482+
<Combobox.Input data-testid="input" />
3483+
<Combobox.Clear data-testid="clear" />
3484+
<Combobox.Portal>
3485+
<Combobox.Positioner>
3486+
<Combobox.Popup>
3487+
<Combobox.List>
3488+
<Combobox.Item value="a">a</Combobox.Item>
3489+
<Combobox.Item value="b">b</Combobox.Item>
3490+
</Combobox.List>
3491+
</Combobox.Popup>
3492+
</Combobox.Positioner>
3493+
</Combobox.Portal>
3494+
</Combobox.Root>
3495+
</Field.Root>
3496+
<button type="submit">submit</button>
3497+
</Form>,
3498+
);
3499+
3500+
const input = screen.getByTestId('input');
3501+
expect(input).not.to.have.attribute('aria-invalid');
3502+
3503+
await user.click(screen.getByText('submit'));
3504+
expect(input).to.have.attribute('aria-invalid', 'true');
3505+
3506+
await user.click(input);
3507+
3508+
await user.keyboard('{ArrowDown}');
3509+
await user.keyboard('{ArrowDown}');
3510+
await user.keyboard('{Enter}');
3511+
3512+
expect(input).not.to.have.attribute('aria-invalid');
3513+
3514+
const clear = screen.getByTestId('clear');
3515+
await user.click(clear);
3516+
3517+
expect(document.activeElement).to.equal(input);
3518+
await user.keyboard('{Tab}');
3519+
3520+
expect(input).to.have.attribute('aria-invalid', 'true');
3521+
});
3522+
34773523
// flaky in real browser
34783524
it.skipIf(!isJSDOM)('prop: validationMode=onChange', async () => {
34793525
const { user } = await render(

packages/react/src/number-field/input/NumberFieldInput.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ export const NumberFieldInput = React.forwardRef(function NumberFieldInput(
8787
} = useNumberFieldRootContext();
8888

8989
const { clearErrors } = useFormContext();
90-
const { validationMode, setTouched, setFocused, invalid } = useFieldRootContext();
90+
const { validationMode, setTouched, setFocused, invalid, shouldValidateOnChange } =
91+
useFieldRootContext();
9192
const { labelId } = useLabelableContext();
9293

9394
const {
@@ -118,10 +119,10 @@ export const NumberFieldInput = React.forwardRef(function NumberFieldInput(
118119

119120
clearErrors(name);
120121

121-
if (validationMode === 'onChange') {
122+
if (shouldValidateOnChange()) {
122123
commitValidation(value);
123124
}
124-
}, [value, inputValue, name, clearErrors, validationMode, commitValidation]);
125+
}, [value, inputValue, name, clearErrors, shouldValidateOnChange, commitValidation]);
125126

126127
useIsoLayoutEffect(() => {
127128
if (prevValueRef.current === value || validationMode === 'onChange') {

packages/react/src/number-field/root/NumberFieldRoot.test.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ describe('<NumberField />', () => {
10321032

10331033
it('prop: validate', async () => {
10341034
await render(
1035-
<Field.Root validate={() => 'error'}>
1035+
<Field.Root validationMode="onBlur" validate={() => 'error'}>
10361036
<NumberFieldBase.Root>
10371037
<NumberFieldBase.Input />
10381038
</NumberFieldBase.Root>
@@ -1050,6 +1050,37 @@ describe('<NumberField />', () => {
10501050
expect(input).to.have.attribute('aria-invalid', 'true');
10511051
});
10521052

1053+
it('prop: validationMode=onSubmit', async () => {
1054+
await render(
1055+
<Form>
1056+
<Field.Root validate={(value) => (value === 1 ? 'error' : null)}>
1057+
<NumberFieldBase.Root required>
1058+
<NumberFieldBase.Input data-testid="input" />
1059+
</NumberFieldBase.Root>
1060+
</Field.Root>
1061+
<button type="submit">submit</button>
1062+
</Form>,
1063+
);
1064+
1065+
const input = screen.getByRole('textbox');
1066+
expect(input).not.to.have.attribute('aria-invalid');
1067+
1068+
fireEvent.click(screen.getByText('submit'));
1069+
expect(input).to.have.attribute('aria-invalid', 'true');
1070+
1071+
fireEvent.change(input, { target: { value: '2' } });
1072+
expect(input).not.to.have.attribute('aria-invalid');
1073+
// re-invalidate the field value
1074+
fireEvent.change(input, { target: { value: '1' } });
1075+
expect(input).to.have.attribute('aria-invalid', 'true');
1076+
1077+
fireEvent.change(input, { target: { value: '3' } });
1078+
expect(input).not.to.have.attribute('aria-invalid');
1079+
1080+
fireEvent.change(input, { target: { value: '' } });
1081+
expect(input).to.have.attribute('aria-invalid', 'true');
1082+
});
1083+
10531084
it('prop: validationMode=onChange', async () => {
10541085
await render(
10551086
<Field.Root

packages/react/src/radio-group/RadioGroup.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const RadioGroup = React.forwardRef(function RadioGroup(
5252
const {
5353
setTouched: setFieldTouched,
5454
setFocused,
55+
shouldValidateOnChange,
5556
validationMode,
5657
name: fieldName,
5758
disabled: fieldDisabled,
@@ -111,12 +112,19 @@ export const RadioGroup = React.forwardRef(function RadioGroup(
111112

112113
clearErrors(name);
113114

114-
if (validationMode === 'onChange') {
115+
if (shouldValidateOnChange()) {
115116
fieldControlValidation.commitValidation(checkedValue);
116117
} else {
117118
fieldControlValidation.commitValidation(checkedValue, true);
118119
}
119-
}, [name, clearErrors, validationMode, checkedValue, fieldControlValidation]);
120+
}, [
121+
name,
122+
clearErrors,
123+
shouldValidateOnChange,
124+
validationMode,
125+
checkedValue,
126+
fieldControlValidation,
127+
]);
120128

121129
useIsoLayoutEffect(() => {
122130
prevValueRef.current = checkedValue;

0 commit comments

Comments
 (0)