From 38fe43155e7f55d9190ac2bcd405612dec598d5e Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko Date: Tue, 18 Jun 2024 23:30:31 +0200 Subject: [PATCH] feat: support form reset for inputs (#1660) --- .../Checkbox/__tests__/Checkbox.test.tsx | 93 +++++++++++++++++++ src/components/Palette/Palette.tsx | 8 +- .../RadioGroup/__tests__/RadioGroup.test.tsx | 93 +++++++++++++++++++ src/components/Select/types.ts | 5 +- src/components/controls/TextArea/TextArea.tsx | 4 +- .../TextArea/__tests__/TextArea.test.tsx | 90 ++++++++++++++++++ .../controls/TextInput/TextInput.tsx | 5 +- .../__tests__/TextInput.input.test.tsx | 90 ++++++++++++++++++ src/components/types.ts | 6 +- src/hooks/private/useCheckbox/useCheckbox.ts | 5 +- src/hooks/private/useRadio/useRadio.ts | 23 +++-- .../private/useRadioGroup/useRadioGroup.ts | 48 +++++----- 12 files changed, 420 insertions(+), 50 deletions(-) diff --git a/src/components/Checkbox/__tests__/Checkbox.test.tsx b/src/components/Checkbox/__tests__/Checkbox.test.tsx index b609f898f8..663f538230 100644 --- a/src/components/Checkbox/__tests__/Checkbox.test.tsx +++ b/src/components/Checkbox/__tests__/Checkbox.test.tsx @@ -191,4 +191,97 @@ describe('Checkbox', () => { checkbox.blur(); expect(handleOnBlur).toHaveBeenCalledTimes(1); }); + + describe('form', () => { + test('should submit no value by default', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([]); + }); + + test('should submit default value', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['checkbox-field', 'value']]); + }); + + test('should submit controlled value', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['checkbox-field', 'value']]); + }); + + test('supports form reset', async () => { + function Test() { + const [value, setValue] = React.useState(true); + return ( +
+ + + + ); + } + + render(); + const form = screen.getByTestId('form'); + expect(form).toHaveFormValues({'checkbox-field': true}); + + await userEvent.tab(); + await userEvent.keyboard('{Enter}'); + + expect(form).toHaveFormValues({}); + + const button = screen.getByTestId('reset'); + await userEvent.click(button); + expect(form).toHaveFormValues({'checkbox-field': true}); + }); + }); }); diff --git a/src/components/Palette/Palette.tsx b/src/components/Palette/Palette.tsx index 7c55673c08..afdfcf0ff7 100644 --- a/src/components/Palette/Palette.tsx +++ b/src/components/Palette/Palette.tsx @@ -6,8 +6,9 @@ import {useSelect} from '../../hooks'; import {useForkRef} from '../../hooks/useForkRef/useForkRef'; import type {ButtonProps} from '../Button'; import {Button} from '../Button'; -import type {ControlGroupProps, DOMProps, QAProps} from '../types'; +import type {AriaLabelingProps, DOMProps, QAProps} from '../types'; import {block} from '../utils/cn'; +import {filterDOMProps} from '../utils/filterDOMProps'; import {usePaletteGrid} from './hooks'; import {getPaletteRows} from './utils'; @@ -32,7 +33,7 @@ export type PaletteOption = Pick & { }; export interface PaletteProps - extends Pick, + extends AriaLabelingProps, Pick, DOMProps, QAProps { @@ -165,9 +166,8 @@ export const Palette = React.forwardRef(function P return (
{ expect(component).toHaveClass(`g-radio-group_direction_${direction}`); }, ); + + describe('form', () => { + test('should submit first value by default', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['radio-field', 'Value 1']]); + }); + + test('should submit default value', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['radio-field', 'Value 2']]); + }); + + test('should submit controlled value', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+ + + , + ); + await userEvent.click(screen.getByTestId('submit')); + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(value).toEqual([['radio-field', 'Value 2']]); + }); + + test('should support form reset', async () => { + function Test() { + const [value, setValue] = React.useState('Value 2'); + return ( +
+ + + + ); + } + + render(); + const form = screen.getByTestId('form'); + expect(form).toHaveFormValues({'radio-field': 'Value 2'}); + + await userEvent.tab(); + await userEvent.keyboard('{ArrowLeft}'); + + expect(form).toHaveFormValues({'radio-field': 'Value 3'}); + + const button = screen.getByTestId('reset'); + await userEvent.click(button); + expect(form).toHaveFormValues({'radio-field': 'Value 2'}); + }); + }); }); diff --git a/src/components/Select/types.ts b/src/components/Select/types.ts index 5b2d14e92f..5b84a7dc89 100644 --- a/src/components/Select/types.ts +++ b/src/components/Select/types.ts @@ -3,7 +3,7 @@ import type React from 'react'; import type {PopperPlacement} from '../../hooks/private'; import type {UseOpenProps} from '../../hooks/useSelect/types'; import type {InputControlPin, InputControlSize, InputControlView} from '../controls'; -import type {ControlGroupOption, ControlGroupProps, QAProps} from '../types'; +import type {ControlGroupOption, QAProps} from '../types'; import type {Option, OptionGroup} from './tech-components'; @@ -58,7 +58,6 @@ export type SelectRenderCounter = ( ) => React.ReactElement; export type SelectProps = QAProps & - Pick & UseOpenProps & { onUpdate?: (value: string[]) => void; onFilterChange?: (filter: string) => void; @@ -122,7 +121,9 @@ export type SelectProps = QAProps & /**Shows selected options count if multiple selection is avalable */ hasCounter?: boolean; title?: string; + name?: string; form?: string; + disabled?: boolean; }; export type SelectOption = QAProps & diff --git a/src/components/controls/TextArea/TextArea.tsx b/src/components/controls/TextArea/TextArea.tsx index 36151ab2d4..ae86d559a5 100644 --- a/src/components/controls/TextArea/TextArea.tsx +++ b/src/components/controls/TextArea/TextArea.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {useControlledState, useForkRef, useUniqId} from '../../../hooks'; +import {useFormResetHandler} from '../../../hooks/private'; import {block} from '../../utils/cn'; import {ClearButton, mapTextInputSizeToButtonSize} from '../common'; import {OuterAdditionalContent} from '../common/OuterAdditionalContent/OuterAdditionalContent'; @@ -71,9 +72,10 @@ export const TextArea = React.forwardRef( const [inputValue, setInputValue] = useControlledState(value, defaultValue ?? '', onUpdate); const innerControlRef = React.useRef(null); + const fieldRef = useFormResetHandler({initialValue: inputValue, onReset: setInputValue}); + const handleRef = useForkRef(props.controlRef, innerControlRef, fieldRef); const [hasVerticalScrollbar, setHasVerticalScrollbar] = React.useState(false); const state = getInputControlState(validationState); - const handleRef = useForkRef(props.controlRef, innerControlRef); const innerId = useUniqId(); const isErrorMsgVisible = validationState === 'invalid' && Boolean(errorMessage); diff --git a/src/components/controls/TextArea/__tests__/TextArea.test.tsx b/src/components/controls/TextArea/__tests__/TextArea.test.tsx index 47aa3f9740..dba43b31dc 100644 --- a/src/components/controls/TextArea/__tests__/TextArea.test.tsx +++ b/src/components/controls/TextArea/__tests__/TextArea.test.tsx @@ -152,4 +152,94 @@ describe('TextArea', () => { expect(input.getAttribute('autocomplete')).toBe('off'); }); }); + + describe('form', () => { + test('should submit empty value by default', async () => { + let value; + const onSubmit = jest.fn((e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + value = [...formData.entries()]; + }); + render( +
+