Skip to content

Commit 338d768

Browse files
committed
fix(form): Fixed floating state for controlled text fields
chore(form): refactored TextField to use new useFieldStates hook chore(form): Updated NativeSelect to use new useFieldStates hook chore(form): Moved useFieldStates to parent folder and added documentation chore(form): Increased test coverage Closes #1043
1 parent 695fd2a commit 338d768

File tree

13 files changed

+516
-134
lines changed

13 files changed

+516
-134
lines changed

packages/form/src/__tests__/Form.tsx

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from "react";
2-
import { render } from "@testing-library/react";
1+
import React, { FormEvent } from "react";
2+
import { fireEvent, render } from "@testing-library/react";
33

44
import { Form } from "../Form";
55

@@ -14,14 +14,29 @@ describe("Form", () => {
1414
expect(container).toMatchSnapshot();
1515
});
1616

17-
// unable to get this working right now with jsdom even with the suggestions
18-
// in https://github.com/jsdom/jsdom/issues/1937
19-
//
20-
// // this still throws an error
21-
// Object.defineProperty(HTMLFormElement.prototype, "submit", {
22-
// value() {
23-
// this.dispatchEvent(new Event("submit"))
24-
// }
25-
// })
26-
it.todo("should prevent default form submission by default");
17+
it("should prevent default form submission by default", () => {
18+
let isStopped = false;
19+
const onSubmit = jest.fn((event: FormEvent<HTMLFormElement>) => {
20+
isStopped = event.isDefaultPrevented();
21+
});
22+
const { container, rerender } = render(
23+
<Form onSubmit={onSubmit} disablePreventDefault />
24+
);
25+
26+
const form = container.firstElementChild;
27+
if (!form) {
28+
throw new Error();
29+
}
30+
expect(onSubmit).not.toBeCalled();
31+
32+
fireEvent.submit(form);
33+
expect(isStopped).toBe(false);
34+
expect(onSubmit).toBeCalledTimes(1);
35+
36+
rerender(<Form onSubmit={onSubmit} />);
37+
38+
fireEvent.submit(form);
39+
expect(isStopped).toBe(true);
40+
expect(onSubmit).toBeCalledTimes(2);
41+
});
2742
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from "react";
2+
import { render } from "@testing-library/react";
3+
4+
import { FormThemeOptions, FormThemeProvider } from "../FormThemeProvider";
5+
import { TextField } from "../text-field/TextField";
6+
7+
describe("FormThemeProvider", () => {
8+
it("should default to the outline theme and left direction", () => {
9+
function Test(props: FormThemeOptions) {
10+
return (
11+
<FormThemeProvider {...props}>
12+
<TextField id="field" label="Label" />
13+
</FormThemeProvider>
14+
);
15+
}
16+
17+
const { container, rerender } = render(<Test />);
18+
expect(container).toMatchSnapshot();
19+
20+
rerender(<Test theme="underline" underlineDirection="center" />);
21+
expect(container).toMatchSnapshot();
22+
});
23+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`FormThemeProvider should default to the outline theme and left direction 1`] = `
4+
<div>
5+
<div
6+
class="rmd-text-field-container rmd-text-field-container--outline rmd-text-field-container--hoverable rmd-text-field-container--label"
7+
>
8+
<label
9+
class="rmd-label rmd-floating-label"
10+
for="field"
11+
>
12+
Label
13+
</label>
14+
<input
15+
class="rmd-text-field rmd-text-field--floating"
16+
id="field"
17+
type="text"
18+
/>
19+
</div>
20+
</div>
21+
`;
22+
23+
exports[`FormThemeProvider should default to the outline theme and left direction 2`] = `
24+
<div>
25+
<div
26+
class="rmd-text-field-container rmd-text-field-container--hoverable rmd-text-field-container--label rmd-text-field-container--underline rmd-text-field-container--underline-labelled rmd-text-field-container--underline-center"
27+
>
28+
<label
29+
class="rmd-label rmd-floating-label"
30+
for="field"
31+
>
32+
Label
33+
</label>
34+
<input
35+
class="rmd-text-field rmd-text-field--floating"
36+
id="field"
37+
type="text"
38+
/>
39+
</div>
40+
</div>
41+
`;

packages/form/src/select/NativeSelect.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {
1515
TextFieldContainer,
1616
TextFieldContainerOptions,
1717
} from "../text-field/TextFieldContainer";
18-
import { useValuedState } from "../text-field/useValuedState";
19-
import { useFocusState } from "../useFocusState";
18+
import { useFieldStates } from "../useFieldStates";
2019

2120
export interface NativeSelectProps
2221
extends SelectHTMLAttributes<HTMLSelectElement>,
@@ -139,15 +138,12 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
139138
const underline = theme === "underline" || theme === "filled";
140139

141140
const icon = useIcon("dropdown", propIcon);
142-
const [focused, onFocus, onBlur] = useFocusState({
141+
const { valued, focused, onBlur, onFocus, onChange } = useFieldStates({
143142
onBlur: propOnBlur,
144143
onFocus: propOnFocus,
145-
});
146-
147-
const [valued, onChange] = useValuedState({
144+
onChange: propOnChange,
148145
value,
149146
defaultValue,
150-
onChange: propOnChange,
151147
});
152148

153149
return (

packages/form/src/select/__tests__/Select.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,27 @@ describe("Select", () => {
4343
});
4444

4545
it("should update the label and select class names when focused as well as hiding the placeholder text", () => {
46+
const onBlur = jest.fn();
47+
const onFocus = jest.fn();
4648
const { container } = render(
47-
<Select {...PROPS} label="Label" placeholder="Choose..." />
49+
<Select
50+
{...PROPS}
51+
label="Label"
52+
placeholder="Choose..."
53+
onBlur={onBlur}
54+
onFocus={onFocus}
55+
/>
4856
);
4957

5058
const select = getSelect();
5159
expect(container).toMatchSnapshot();
5260

5361
fireEvent.focus(select);
62+
expect(onFocus).toBeCalledTimes(1);
5463
expect(container).toMatchSnapshot();
64+
65+
fireEvent.blur(select);
66+
expect(onBlur).toBeCalledTimes(1);
5567
});
5668

5769
it("should show and focus the listbox when the spacebar is pressed on the select button", () => {

packages/form/src/text-field/TextArea.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ import { bem, useEnsuredRef, useResizeObserver } from "@react-md/utils";
1515

1616
import { useFormTheme } from "../FormThemeProvider";
1717
import { FloatingLabel } from "../label/FloatingLabel";
18-
import { useFocusState } from "../useFocusState";
1918
import {
2019
TextFieldContainer,
2120
TextFieldContainerOptions,
2221
} from "./TextFieldContainer";
23-
import { useValuedState } from "./useValuedState";
22+
import { useFieldStates } from "../useFieldStates";
2423

2524
export type TextAreaResize =
2625
| "none"
@@ -171,11 +170,6 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
171170
underlineDirection: propUnderlineDirection,
172171
});
173172

174-
const [focused, onFocus, onBlur] = useFocusState({
175-
onBlur: propOnBlur,
176-
onFocus: propOnFocus,
177-
});
178-
179173
const [height, setHeight] = useState<number>();
180174
if (resize !== "auto" && typeof height === "number") {
181175
setHeight(undefined);
@@ -185,11 +179,13 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
185179
const [scrollable, setScrollable] = useState(false);
186180
const updateHeight = useCallback(() => {
187181
const mask = maskRef.current;
182+
/* istanbul ignore if */
188183
if (!mask) {
189184
return;
190185
}
191186

192187
let nextHeight = mask.scrollHeight;
188+
/* istanbul ignore if */
193189
if (maxRows > 0) {
194190
const lineHeight = parseFloat(
195191
window.getComputedStyle(mask).lineHeight || DEFAULT_LINE_HEIGHT
@@ -217,16 +213,16 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
217213
ref: maskRef,
218214
disableHeight: true,
219215
});
220-
221-
const [valued, onChange] = useValuedState<HTMLTextAreaElement>({
222-
value,
223-
defaultValue,
216+
const { valued, focused, onBlur, onFocus, onChange } = useFieldStates({
217+
onBlur: propOnBlur,
218+
onFocus: propOnFocus,
224219
onChange: (event) => {
225220
const mask = maskRef.current;
226221
if (propOnChange) {
227222
propOnChange(event);
228223
}
229224

225+
/* istanbul ignore if */
230226
if (!mask || resize !== "auto") {
231227
return;
232228
}

packages/form/src/text-field/TextField.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ import { bem } from "@react-md/utils";
1111

1212
import { useFormTheme } from "../FormThemeProvider";
1313
import { FloatingLabel } from "../label/FloatingLabel";
14-
import { useFocusState } from "../useFocusState";
1514
import {
1615
TextFieldContainer,
1716
TextFieldContainerOptions,
1817
} from "./TextFieldContainer";
19-
import { useValuedState } from "./useValuedState";
18+
import { useFieldStates } from "../useFieldStates";
2019

2120
/**
2221
* These are all the "supported" input types for react-md so that they at least
@@ -163,17 +162,12 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
163162
ref
164163
) {
165164
const { id, value, defaultValue } = props;
166-
167-
const [focused, onFocus, handleBlur] = useFocusState({
165+
const { valued, focused, onBlur, onFocus, onChange } = useFieldStates({
168166
onBlur: propOnBlur,
169167
onFocus: propOnFocus,
170-
});
171-
172-
const [valued, onChange, onBlur] = useValuedState<HTMLInputElement>({
168+
onChange: propOnChange,
173169
value,
174170
defaultValue,
175-
onChange: propOnChange,
176-
onBlur: handleBlur,
177171
});
178172

179173
const { theme, underlineDirection } = useFormTheme({

0 commit comments

Comments
 (0)