Skip to content
Permalink
Browse files
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
  • Loading branch information
mlaursen committed Jan 12, 2021
1 parent 695fd2a commit 338d768
Show file tree
Hide file tree
Showing 13 changed files with 516 additions and 134 deletions.
@@ -1,5 +1,5 @@
import React from "react";
import { render } from "@testing-library/react";
import React, { FormEvent } from "react";
import { fireEvent, render } from "@testing-library/react";

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

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

// unable to get this working right now with jsdom even with the suggestions
// in https://github.com/jsdom/jsdom/issues/1937
//
// // this still throws an error
// Object.defineProperty(HTMLFormElement.prototype, "submit", {
// value() {
// this.dispatchEvent(new Event("submit"))
// }
// })
it.todo("should prevent default form submission by default");
it("should prevent default form submission by default", () => {
let isStopped = false;
const onSubmit = jest.fn((event: FormEvent<HTMLFormElement>) => {
isStopped = event.isDefaultPrevented();
});
const { container, rerender } = render(
<Form onSubmit={onSubmit} disablePreventDefault />
);

const form = container.firstElementChild;
if (!form) {
throw new Error();
}
expect(onSubmit).not.toBeCalled();

fireEvent.submit(form);
expect(isStopped).toBe(false);
expect(onSubmit).toBeCalledTimes(1);

rerender(<Form onSubmit={onSubmit} />);

fireEvent.submit(form);
expect(isStopped).toBe(true);
expect(onSubmit).toBeCalledTimes(2);
});
});
@@ -0,0 +1,23 @@
import React from "react";
import { render } from "@testing-library/react";

import { FormThemeOptions, FormThemeProvider } from "../FormThemeProvider";
import { TextField } from "../text-field/TextField";

describe("FormThemeProvider", () => {
it("should default to the outline theme and left direction", () => {
function Test(props: FormThemeOptions) {
return (
<FormThemeProvider {...props}>
<TextField id="field" label="Label" />
</FormThemeProvider>
);
}

const { container, rerender } = render(<Test />);
expect(container).toMatchSnapshot();

rerender(<Test theme="underline" underlineDirection="center" />);
expect(container).toMatchSnapshot();
});
});
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FormThemeProvider should default to the outline theme and left direction 1`] = `
<div>
<div
class="rmd-text-field-container rmd-text-field-container--outline rmd-text-field-container--hoverable rmd-text-field-container--label"
>
<label
class="rmd-label rmd-floating-label"
for="field"
>
Label
</label>
<input
class="rmd-text-field rmd-text-field--floating"
id="field"
type="text"
/>
</div>
</div>
`;

exports[`FormThemeProvider should default to the outline theme and left direction 2`] = `
<div>
<div
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"
>
<label
class="rmd-label rmd-floating-label"
for="field"
>
Label
</label>
<input
class="rmd-text-field rmd-text-field--floating"
id="field"
type="text"
/>
</div>
</div>
`;
@@ -15,8 +15,7 @@ import {
TextFieldContainer,
TextFieldContainerOptions,
} from "../text-field/TextFieldContainer";
import { useValuedState } from "../text-field/useValuedState";
import { useFocusState } from "../useFocusState";
import { useFieldStates } from "../useFieldStates";

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

const icon = useIcon("dropdown", propIcon);
const [focused, onFocus, onBlur] = useFocusState({
const { valued, focused, onBlur, onFocus, onChange } = useFieldStates({
onBlur: propOnBlur,
onFocus: propOnFocus,
});

const [valued, onChange] = useValuedState({
onChange: propOnChange,
value,
defaultValue,
onChange: propOnChange,
});

return (
@@ -43,15 +43,27 @@ describe("Select", () => {
});

it("should update the label and select class names when focused as well as hiding the placeholder text", () => {
const onBlur = jest.fn();
const onFocus = jest.fn();
const { container } = render(
<Select {...PROPS} label="Label" placeholder="Choose..." />
<Select
{...PROPS}
label="Label"
placeholder="Choose..."
onBlur={onBlur}
onFocus={onFocus}
/>
);

const select = getSelect();
expect(container).toMatchSnapshot();

fireEvent.focus(select);
expect(onFocus).toBeCalledTimes(1);
expect(container).toMatchSnapshot();

fireEvent.blur(select);
expect(onBlur).toBeCalledTimes(1);
});

it("should show and focus the listbox when the spacebar is pressed on the select button", () => {
@@ -15,12 +15,11 @@ import { bem, useEnsuredRef, useResizeObserver } from "@react-md/utils";

import { useFormTheme } from "../FormThemeProvider";
import { FloatingLabel } from "../label/FloatingLabel";
import { useFocusState } from "../useFocusState";
import {
TextFieldContainer,
TextFieldContainerOptions,
} from "./TextFieldContainer";
import { useValuedState } from "./useValuedState";
import { useFieldStates } from "../useFieldStates";

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

const [focused, onFocus, onBlur] = useFocusState({
onBlur: propOnBlur,
onFocus: propOnFocus,
});

const [height, setHeight] = useState<number>();
if (resize !== "auto" && typeof height === "number") {
setHeight(undefined);
@@ -185,11 +179,13 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
const [scrollable, setScrollable] = useState(false);
const updateHeight = useCallback(() => {
const mask = maskRef.current;
/* istanbul ignore if */
if (!mask) {
return;
}

let nextHeight = mask.scrollHeight;
/* istanbul ignore if */
if (maxRows > 0) {
const lineHeight = parseFloat(
window.getComputedStyle(mask).lineHeight || DEFAULT_LINE_HEIGHT
@@ -217,16 +213,16 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
ref: maskRef,
disableHeight: true,
});

const [valued, onChange] = useValuedState<HTMLTextAreaElement>({
value,
defaultValue,
const { valued, focused, onBlur, onFocus, onChange } = useFieldStates({
onBlur: propOnBlur,
onFocus: propOnFocus,
onChange: (event) => {
const mask = maskRef.current;
if (propOnChange) {
propOnChange(event);
}

/* istanbul ignore if */
if (!mask || resize !== "auto") {
return;
}
@@ -11,12 +11,11 @@ import { bem } from "@react-md/utils";

import { useFormTheme } from "../FormThemeProvider";
import { FloatingLabel } from "../label/FloatingLabel";
import { useFocusState } from "../useFocusState";
import {
TextFieldContainer,
TextFieldContainerOptions,
} from "./TextFieldContainer";
import { useValuedState } from "./useValuedState";
import { useFieldStates } from "../useFieldStates";

/**
* 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>(
ref
) {
const { id, value, defaultValue } = props;

const [focused, onFocus, handleBlur] = useFocusState({
const { valued, focused, onBlur, onFocus, onChange } = useFieldStates({
onBlur: propOnBlur,
onFocus: propOnFocus,
});

const [valued, onChange, onBlur] = useValuedState<HTMLInputElement>({
onChange: propOnChange,
value,
defaultValue,
onChange: propOnChange,
onBlur: handleBlur,
});

const { theme, underlineDirection } = useFormTheme({

0 comments on commit 338d768

Please sign in to comment.