Skip to content

Commit

Permalink
feat: support form reset for inputs (#1660)
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS authored Jun 18, 2024
1 parent db15851 commit 38fe431
Show file tree
Hide file tree
Showing 12 changed files with 420 additions and 50 deletions.
93 changes: 93 additions & 0 deletions src/components/Checkbox/__tests__/Checkbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<form data-qa="form" onSubmit={onSubmit}>
<Checkbox name="checkbox-field" value="value" />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
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(
<form data-qa="form" onSubmit={onSubmit}>
<Checkbox name="checkbox-field" value="value" defaultChecked />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
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(
<form data-qa="form" onSubmit={onSubmit}>
<Checkbox name="checkbox-field" value="value" checked />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
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 (
<form data-qa="form">
<Checkbox
name="checkbox-field"
value="value"
checked={value}
onUpdate={setValue}
/>
<input type="reset" data-qa="reset" />
</form>
);
}

render(<Test />);
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});
});
});
});
8 changes: 4 additions & 4 deletions src/components/Palette/Palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,7 +33,7 @@ export type PaletteOption = Pick<ButtonProps, 'disabled' | 'title'> & {
};

export interface PaletteProps
extends Pick<ControlGroupProps, 'aria-label' | 'aria-labelledby'>,
extends AriaLabelingProps,
Pick<ButtonProps, 'disabled' | 'size'>,
DOMProps,
QAProps {
Expand Down Expand Up @@ -165,9 +166,8 @@ export const Palette = React.forwardRef<HTMLDivElement, PaletteProps>(function P

return (
<div
{...filterDOMProps(props, {labelable: true})}
{...gridProps}
aria-label={props['aria-label']}
aria-labelledby={props['aria-labelledby']}
ref={handleRef}
className={b({size}, className)}
style={style}
Expand Down
93 changes: 93 additions & 0 deletions src/components/RadioGroup/__tests__/RadioGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,97 @@ describe('RadioGroup', () => {
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(
<form data-qa="form" onSubmit={onSubmit}>
<RadioGroup name="radio-field" options={options} />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
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(
<form data-qa="form" onSubmit={onSubmit}>
<RadioGroup name="radio-field" options={options} defaultValue="Value 2" />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
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(
<form data-qa="form" onSubmit={onSubmit}>
<RadioGroup name="radio-field" options={options} value="Value 2" />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
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 (
<form data-qa="form">
<RadioGroup
name="radio-field"
options={options}
value={value}
onUpdate={setValue}
/>
<input type="reset" data-qa="reset" />
</form>
);
}

render(<Test />);
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'});
});
});
});
5 changes: 3 additions & 2 deletions src/components/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -58,7 +58,6 @@ export type SelectRenderCounter = (
) => React.ReactElement;

export type SelectProps<T = any> = QAProps &
Pick<ControlGroupProps, 'name' | 'disabled'> &
UseOpenProps & {
onUpdate?: (value: string[]) => void;
onFilterChange?: (filter: string) => void;
Expand Down Expand Up @@ -122,7 +121,9 @@ export type SelectProps<T = any> = QAProps &
/**Shows selected options count if multiple selection is avalable */
hasCounter?: boolean;
title?: string;
name?: string;
form?: string;
disabled?: boolean;
};

export type SelectOption<T = any> = QAProps &
Expand Down
4 changes: 3 additions & 1 deletion src/components/controls/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,9 +72,10 @@ export const TextArea = React.forwardRef<HTMLSpanElement, TextAreaProps>(

const [inputValue, setInputValue] = useControlledState(value, defaultValue ?? '', onUpdate);
const innerControlRef = React.useRef<HTMLTextAreaElement | HTMLInputElement>(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);
Expand Down
90 changes: 90 additions & 0 deletions src/components/controls/TextArea/__tests__/TextArea.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<form data-qa="form" onSubmit={onSubmit}>
<TextArea name="text-field" />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
await userEvent.click(screen.getByTestId('submit'));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(value).toEqual([['text-field', '']]);
});

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(
<form data-qa="form" onSubmit={onSubmit}>
<TextArea name="text-field" defaultValue="default value" />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
await userEvent.click(screen.getByTestId('submit'));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(value).toEqual([['text-field', 'default 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(
<form data-qa="form" onSubmit={onSubmit}>
<TextArea name="text-field" value="value" />
<button type="submit" data-qa="submit">
submit
</button>
</form>,
);
await userEvent.click(screen.getByTestId('submit'));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(value).toEqual([['text-field', 'value']]);
});

test('supports form reset', async () => {
function Test() {
const [value, setValue] = React.useState('value');
return (
<form>
<TextArea name="text-field" value={value} onUpdate={setValue} />
<input type="reset" data-qa="reset" />
</form>
);
}

render(<Test />);
// eslint-disable-next-line testing-library/no-node-access
const inputs = document.querySelectorAll('[name=text-field]');
expect(inputs.length).toBe(1);
expect(inputs[0]).toHaveValue('value');

await userEvent.tab();
await userEvent.keyboard('text');

expect(inputs[0]).toHaveValue('text');

const button = screen.getByTestId('reset');
await userEvent.click(button);
expect(inputs[0]).toHaveValue('value');
});
});
});
5 changes: 3 additions & 2 deletions src/components/controls/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import {TriangleExclamation} from '@gravity-ui/icons';

import {useControlledState, useForkRef, useUniqId} from '../../../hooks';
import {useElementSize} from '../../../hooks/private';
import {useElementSize, useFormResetHandler} from '../../../hooks/private';
import {Icon} from '../../Icon';
import {Popover} from '../../Popover';
import {block} from '../../utils/cn';
Expand Down Expand Up @@ -99,7 +99,8 @@ export const TextInput = React.forwardRef<HTMLSpanElement, TextInputProps>(

const [inputValue, setInputValue] = useControlledState(value, defaultValue ?? '', onUpdate);
const innerControlRef = React.useRef<HTMLTextAreaElement | HTMLInputElement>(null);
const handleRef = useForkRef(props.controlRef, innerControlRef);
const fieldRef = useFormResetHandler({initialValue: inputValue, onReset: setInputValue});
const handleRef = useForkRef(props.controlRef, innerControlRef, fieldRef);
const labelRef = React.useRef<HTMLLabelElement>(null);
const startContentRef = React.useRef<HTMLDivElement>(null);
const state = getInputControlState(validationState);
Expand Down
Loading

0 comments on commit 38fe431

Please sign in to comment.