Eric Eastwood 2022-01-27 16:32:12 -06:00 committed by GitHub
parent efa1667d7e
commit 7fa27f5834
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 630 additions and 44 deletions

View file

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react';
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, RefObject } from 'react';
import classNames from 'classnames';
import { debounce } from "lodash";
import * as sdk from '../../../index';
import { IFieldState, IValidationResult } from "./Validation";
import { ComponentClass } from "../../../@types/common";
// Invoke validation from user input (when typing, etc.) at most once every N ms.
const VALIDATION_THROTTLE_MS = 200;
@ -78,26 +79,45 @@ interface IProps {
}
export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
// The ref pass through to the input
inputRef?: RefObject<HTMLInputElement>;
// The element to create. Defaults to "input".
element?: "input";
componentClass?: undefined;
// The input's value. This is a controlled component, so the value is required.
value: string;
}
interface ISelectProps extends IProps, SelectHTMLAttributes<HTMLSelectElement> {
// The ref pass through to the select
inputRef?: RefObject<HTMLSelectElement>;
// To define options for a select, use <Field><option ... /></Field>
element: "select";
componentClass?: undefined;
// The select's value. This is a controlled component, so the value is required.
value: string;
}
interface ITextareaProps extends IProps, TextareaHTMLAttributes<HTMLTextAreaElement> {
// The ref pass through to the textarea
inputRef?: RefObject<HTMLTextAreaElement>;
element: "textarea";
componentClass?: undefined;
// The textarea's value. This is a controlled component, so the value is required.
value: string;
}
type PropShapes = IInputProps | ISelectProps | ITextareaProps;
export interface INativeOnChangeInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
// The ref pass through to the input
inputRef?: RefObject<HTMLInputElement>;
element: "input";
// The custom component to render
componentClass: ComponentClass;
// The input's value. This is a controlled component, so the value is required.
value: string;
}
type PropShapes = IInputProps | ISelectProps | ITextareaProps | INativeOnChangeInputProps;
interface IState {
valid: boolean;
@ -108,7 +128,7 @@ interface IState {
export default class Field extends React.PureComponent<PropShapes, IState> {
private id: string;
private input: HTMLInputElement;
private inputRef: RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
public static readonly defaultProps = {
element: "input",
@ -146,7 +166,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
}
public focus() {
this.input.focus();
this.inputRef.current?.focus();
// programmatic does not fire onFocus handler
this.setState({
focused: true,
@ -197,7 +217,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
if (!this.props.onValidate) {
return;
}
const value = this.input ? this.input.value : null;
const value = this.inputRef.current?.value ?? null;
const { valid, feedback } = await this.props.onValidate({
value,
focused,
@ -228,13 +248,13 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
public render() {
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { element, prefixComponent, postfixComponent, className, onValidate, children,
const { element, componentClass, inputRef, prefixComponent, postfixComponent, className, onValidate, children,
tooltipContent, forceValidity, tooltipClassName, list, validateOnBlur, validateOnChange, validateOnFocus,
usePlaceholderAsHint, forceTooltipVisible,
...inputProps } = this.props;
// Set some defaults for the <input> element
const ref = input => this.input = input;
this.inputRef = inputRef || React.createRef();
inputProps.placeholder = inputProps.placeholder || inputProps.label;
inputProps.id = this.id; // this overwrites the id from props
@ -243,9 +263,9 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
inputProps.onBlur = this.onBlur;
// Appease typescript's inference
const inputProps_ = { ...inputProps, ref, list };
const inputProps_ = { ...inputProps, ref: this.inputRef, list };
const fieldInput = React.createElement(this.props.element, inputProps_, children);
const fieldInput = React.createElement(this.props.componentClass || this.props.element, inputProps_, children);
let prefixContainer = null;
if (prefixComponent) {
@ -257,17 +277,22 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
}
const hasValidationFlag = forceValidity !== null && forceValidity !== undefined;
const fieldClasses = classNames("mx_Field", `mx_Field_${this.props.element}`, className, {
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
mx_Field_placeholderIsHint: usePlaceholderAsHint,
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? !forceValidity
: onValidate && this.state.valid === false,
});
const fieldClasses = classNames(
"mx_Field",
`mx_Field_${this.props.element}`,
className,
{
// If we have a prefix element, leave the label always at the top left and
// don't animate it, as it looks a bit clunky and would add complexity to do
// properly.
mx_Field_labelAlwaysTopLeft: prefixComponent || usePlaceholderAsHint,
mx_Field_placeholderIsHint: usePlaceholderAsHint,
mx_Field_valid: hasValidationFlag ? forceValidity : onValidate && this.state.valid === true,
mx_Field_invalid: hasValidationFlag
? !forceValidity
: onValidate && this.state.valid === false,
},
);
// Handle displaying feedback on validity
// FIXME: Using an import will result in test failures