mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-03 03:55:51 +01:00
refactor form fields to enable usage with react-hook-form (#1656)
React Hook Form is a library that makes working with forms easier and reduces boilerplate. For it to be used in our project, some of the form fields had to be adjusted.
This commit is contained in:
committed by
GitHub
parent
7286a62a80
commit
640a270e1d
@@ -21,16 +21,83 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import Checkbox from "./Checkbox";
|
||||
import styled from "styled-components";
|
||||
import Button from "../buttons/Button";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { SubmitButton } from "../buttons";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
const Spacing = styled.div`
|
||||
padding: 2em;
|
||||
`;
|
||||
|
||||
const Ref: FC = () => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<>
|
||||
<Checkbox label={"Ref Checkbox"} checked={false} ref={ref} />
|
||||
<Button
|
||||
action={() => {
|
||||
if (ref.current) {
|
||||
ref.current.checked = !ref.current.checked;
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Toggle Checkbox
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Settings = {
|
||||
rememberMe: string;
|
||||
scramblePassword: string;
|
||||
};
|
||||
|
||||
const ReactHookForm: FC = () => {
|
||||
const { register, handleSubmit } = useForm<Settings>();
|
||||
const [stored, setStored] = useState<Settings>();
|
||||
|
||||
const onSubmit = (settings: Settings) => {
|
||||
setStored(settings);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Checkbox label="Remember Me" {...register("rememberMe")} />
|
||||
<Checkbox label="Scramble Password" {...register("scramblePassword")} />
|
||||
<div className="pt-2">
|
||||
<SubmitButton>Submit</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
{stored ? (
|
||||
<div className="mt-5">
|
||||
<pre>
|
||||
<code>{JSON.stringify(stored, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LegacyEvents: FC = () => {
|
||||
const [value, setValue] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<Checkbox checked={value} onChange={setValue} />
|
||||
<div className="mt-3">{JSON.stringify(value)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Forms|Checkbox", module)
|
||||
.addDecorator(storyFn => <MemoryRouter>{storyFn()}</MemoryRouter>)
|
||||
.add("Default", () => (
|
||||
<Spacing>
|
||||
<Checkbox label="Not checked" checked={false} />
|
||||
@@ -52,4 +119,7 @@ storiesOf("Forms|Checkbox", module)
|
||||
helpText="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
|
||||
/>
|
||||
</Spacing>
|
||||
));
|
||||
))
|
||||
.add("Ref", () => <Ref />)
|
||||
.add("Legacy Events", () => <LegacyEvents />)
|
||||
.add("ReactHookForm", () => <ReactHookForm />);
|
||||
|
||||
@@ -21,69 +21,108 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Help } from "../index";
|
||||
import React, { ChangeEvent, FC, FocusEvent, useEffect } from "react";
|
||||
import { createAttributesForTesting, Help } from "../index";
|
||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||
import TriStateCheckbox from "./TriStateCheckbox";
|
||||
import useInnerRef from "./useInnerRef";
|
||||
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
export interface CheckboxElement extends HTMLElement {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
type BaseProps = {
|
||||
label?: string;
|
||||
onChange?: (value: boolean, name?: string) => void;
|
||||
checked: boolean;
|
||||
checked?: boolean;
|
||||
indeterminate?: boolean;
|
||||
name?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
testId?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default class Checkbox extends React.Component<Props> {
|
||||
onCheckboxChange = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(!this.props.checked, this.props.name);
|
||||
const InnerCheckbox: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({
|
||||
label,
|
||||
name,
|
||||
indeterminate,
|
||||
disabled,
|
||||
testId,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const field = useInnerRef(props.innerRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (field.current) {
|
||||
field.current.indeterminate = indeterminate || false;
|
||||
}
|
||||
}, [field, indeterminate]);
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (props.onChange) {
|
||||
if (isUsingRef<BaseProps, HTMLInputElement, boolean>(props)) {
|
||||
props.onChange(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onChange(event.target.checked, name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onKeyDown = (event: React.KeyboardEvent) => {
|
||||
const SPACE = 32;
|
||||
if (event.keyCode === SPACE) {
|
||||
this.onCheckboxChange();
|
||||
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
||||
if (props.onBlur) {
|
||||
if (isUsingRef<BaseProps, HTMLInputElement, boolean>(props)) {
|
||||
props.onBlur(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onBlur(event.target.checked, name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renderHelp = () => {
|
||||
const { title, helpText } = this.props;
|
||||
const renderHelp = () => {
|
||||
const { title, helpText } = props;
|
||||
if (helpText && !title) {
|
||||
return <Help message={helpText} />;
|
||||
}
|
||||
};
|
||||
|
||||
renderLabelWithHelp = () => {
|
||||
const { title, helpText } = this.props;
|
||||
const renderLabelWithHelp = () => {
|
||||
const { title, helpText } = props;
|
||||
if (title) {
|
||||
return <LabelWithHelpIcon label={title} helpText={helpText} />;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, checked, indeterminate, disabled, testId } = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabelWithHelp()}
|
||||
<div className="control" onClick={this.onCheckboxChange} onKeyDown={this.onKeyDown}>
|
||||
{/*
|
||||
return (
|
||||
<div className="field">
|
||||
{renderLabelWithHelp()}
|
||||
<div className="control">
|
||||
{/*
|
||||
we have to ignore the next line,
|
||||
because jsx label does not the custom disabled attribute
|
||||
but bulma does.
|
||||
// @ts-ignore */}
|
||||
<label className="checkbox" disabled={disabled}>
|
||||
<TriStateCheckbox checked={checked} indeterminate={indeterminate} disabled={disabled} testId={testId} />
|
||||
{label}
|
||||
{this.renderHelp()}
|
||||
</label>
|
||||
</div>
|
||||
<label className="checkbox" disabled={disabled}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={name}
|
||||
className={classNames("checkbox", className)}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
ref={field}
|
||||
checked={props.checked}
|
||||
disabled={disabled}
|
||||
{...createAttributesForTesting(testId)}
|
||||
/>{" "}
|
||||
{label}
|
||||
{renderHelp()}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Checkbox: FieldType<BaseProps, HTMLInputElement, boolean> = createFormFieldWrapper(InnerCheckbox);
|
||||
|
||||
export default Checkbox;
|
||||
|
||||
@@ -38,6 +38,9 @@ const FullWidthSelect = styled.select`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
/**
|
||||
* @deprecated Use `Select` instead
|
||||
*/
|
||||
class DropDown extends React.Component<Props> {
|
||||
render() {
|
||||
const { options, optionValues, preselectedOption, className, disabled } = this.props;
|
||||
|
||||
81
scm-ui/ui-components/src/forms/FormFieldTypes.tsx
Normal file
81
scm-ui/ui-components/src/forms/FormFieldTypes.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent, FC, FocusEvent } from "react";
|
||||
|
||||
export type MinimumBaseProps = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type LegacyProps<Base extends MinimumBaseProps, ValueType> = Base & {
|
||||
onChange?: (value: ValueType, name?: string) => void;
|
||||
onBlur?: (value: ValueType, name?: string) => void;
|
||||
innerRef?: never;
|
||||
};
|
||||
|
||||
export type RefProps<Base extends MinimumBaseProps, ElementType extends HTMLElement> = Base & {
|
||||
onChange?: (event: ChangeEvent<ElementType>) => void;
|
||||
onBlur?: (event: FocusEvent<ElementType>) => void;
|
||||
};
|
||||
|
||||
export type InnerRefProps<Base extends MinimumBaseProps, ElementType extends HTMLElement> = RefProps<
|
||||
Base,
|
||||
ElementType
|
||||
> & {
|
||||
innerRef: React.ForwardedRef<ElementType>;
|
||||
};
|
||||
|
||||
export const isUsingRef = <Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType>(
|
||||
props: Partial<FieldProps<Base, ElementType, ValueType>>
|
||||
): props is InnerRefProps<Base, ElementType> => {
|
||||
return (props as Partial<InnerRefProps<Base, ElementType>>).innerRef !== undefined;
|
||||
};
|
||||
|
||||
export const isLegacy = <Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType>(
|
||||
props: FieldProps<Base, ElementType, ValueType>
|
||||
): props is LegacyProps<Base, ValueType> => {
|
||||
return (props as Partial<InnerRefProps<Base, ElementType>>).innerRef === undefined;
|
||||
};
|
||||
|
||||
export type FieldProps<Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType> =
|
||||
| LegacyProps<Base, ValueType>
|
||||
| InnerRefProps<Base, ElementType>;
|
||||
|
||||
export type OuterProps<Base extends MinimumBaseProps, ElementType extends HTMLElement> = RefProps<Base, ElementType> & {
|
||||
ref: React.Ref<ElementType>;
|
||||
};
|
||||
|
||||
export type FieldType<Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType> = {
|
||||
(props: OuterProps<Base, ElementType>): React.ReactElement<OuterProps<Base, ElementType>> | null;
|
||||
(props: LegacyProps<Base, ValueType>): React.ReactElement<LegacyProps<Base, ValueType>> | null;
|
||||
};
|
||||
|
||||
export const createFormFieldWrapper = <Base extends MinimumBaseProps, ElementType extends HTMLElement, ValueType>(
|
||||
InnerType: FC<FieldProps<Base, ElementType, ValueType>>
|
||||
) =>
|
||||
React.forwardRef<ElementType, LegacyProps<Base, ValueType> | OuterProps<Base, ElementType>>((props, ref) => {
|
||||
if (ref) {
|
||||
return <InnerType innerRef={ref} {...(props as RefProps<Base, ElementType>)} />;
|
||||
}
|
||||
return <InnerType {...(props as LegacyProps<Base, ValueType>)} />;
|
||||
});
|
||||
124
scm-ui/ui-components/src/forms/InputField.stories.tsx
Normal file
124
scm-ui/ui-components/src/forms/InputField.stories.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import styled from "styled-components";
|
||||
import InputField from "./InputField";
|
||||
import Button from "../buttons/Button";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { SubmitButton } from "../buttons";
|
||||
import { Person } from "@scm-manager/ui-types";
|
||||
|
||||
const Decorator = styled.div`
|
||||
padding: 2rem;
|
||||
max-width: 30rem;
|
||||
`;
|
||||
|
||||
const Ref: FC = () => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<>
|
||||
<InputField ref={ref} placeholder="Click the button to focus me" />
|
||||
<Button action={() => ref.current?.focus()} color="primary">
|
||||
Focus InputField
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AutoFocusAndRef: FC = () => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<>
|
||||
<InputField ref={ref} autofocus={true} placeholder="Click the button to focus me" />
|
||||
<InputField placeholder="Click me to switch focus" />
|
||||
<Button action={() => ref.current?.focus()} color="primary">
|
||||
Focus First InputField
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Name = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
const ReactHookForm: FC = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<Name>();
|
||||
const [stored, setStored] = useState<Person>();
|
||||
|
||||
const onSubmit = (person: Person) => {
|
||||
setStored(person);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<InputField label="First Name" autofocus={true} {...register("firstName")} />
|
||||
<InputField
|
||||
label="Last Name"
|
||||
{...register("lastName", { required: true })}
|
||||
validationError={!!errors.lastName}
|
||||
errorMessage={"Last name is required"}
|
||||
/>
|
||||
<div className="pt-2">
|
||||
<SubmitButton>Submit</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
{stored ? (
|
||||
<div className="mt-5">
|
||||
<pre>
|
||||
<code>{JSON.stringify(stored, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LegacyEvents: FC = () => {
|
||||
const [value, setValue] = useState<string>("");
|
||||
return (
|
||||
<>
|
||||
<InputField placeholder="Legacy onChange handler" value={value} onChange={e => setValue(e)} />
|
||||
<div className="mt-3">{value}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Forms|InputField", module)
|
||||
.addDecorator(storyFn => <Decorator>{storyFn()}</Decorator>)
|
||||
.addDecorator(storyFn => <MemoryRouter>{storyFn()}</MemoryRouter>)
|
||||
.add("AutoFocus", () => <InputField label="Field with AutoFocus" autofocus={true} />)
|
||||
.add("Default Value", () => <InputField label="Field with Default Value" defaultValue={"I am a default value"} />)
|
||||
.add("Ref", () => <Ref />)
|
||||
.add("Legacy Events", () => <LegacyEvents />)
|
||||
.add("AutoFocusAndRef", () => <AutoFocusAndRef />)
|
||||
.add("React Hook Form", () => <ReactHookForm />);
|
||||
@@ -21,109 +21,109 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent, KeyboardEvent, FocusEvent } from "react";
|
||||
import React, { ChangeEvent, FC, FocusEvent, KeyboardEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||
import { createAttributesForTesting } from "../devBuild";
|
||||
import useAutofocus from "./useAutofocus";
|
||||
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
|
||||
|
||||
type Props = {
|
||||
type BaseProps = {
|
||||
label?: string;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
type?: string;
|
||||
autofocus?: boolean;
|
||||
onChange: (value: string, name?: string) => void;
|
||||
onReturnPressed?: () => void;
|
||||
validationError?: boolean;
|
||||
errorMessage?: string;
|
||||
errorMessage?: string | string[];
|
||||
informationMessage?: string;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
onBlur?: (value: string, name?: string) => void;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
class InputField extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
type: "text",
|
||||
placeholder: ""
|
||||
};
|
||||
export const InnerInputField: FC<FieldProps<BaseProps, HTMLInputElement, string>> = ({
|
||||
name,
|
||||
onReturnPressed,
|
||||
type,
|
||||
placeholder,
|
||||
value,
|
||||
validationError,
|
||||
errorMessage,
|
||||
informationMessage,
|
||||
disabled,
|
||||
label,
|
||||
helpText,
|
||||
className,
|
||||
testId,
|
||||
autofocus,
|
||||
defaultValue,
|
||||
...props
|
||||
}) => {
|
||||
const field = useAutofocus<HTMLInputElement>(autofocus, props.innerRef);
|
||||
|
||||
field: HTMLInputElement | null | undefined;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.autofocus && this.field) {
|
||||
this.field.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleInput = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onChange(event.target.value, this.props.name);
|
||||
};
|
||||
|
||||
handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(event.target.value, this.props.name);
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (props.onChange) {
|
||||
if (isUsingRef<BaseProps, HTMLInputElement, string>(props)) {
|
||||
props.onChange(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onChange(event.target.value, name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
const onReturnPressed = this.props.onReturnPressed;
|
||||
if (!onReturnPressed) {
|
||||
return;
|
||||
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
||||
if (props.onBlur) {
|
||||
if (isUsingRef<BaseProps, HTMLInputElement, string>(props)) {
|
||||
props.onBlur(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onBlur(event.target.value, name);
|
||||
}
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (onReturnPressed && event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
onReturnPressed();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
type,
|
||||
placeholder,
|
||||
value,
|
||||
validationError,
|
||||
errorMessage,
|
||||
informationMessage,
|
||||
disabled,
|
||||
label,
|
||||
helpText,
|
||||
className,
|
||||
testId
|
||||
} = this.props;
|
||||
const errorView = validationError ? "is-danger" : "";
|
||||
let helper;
|
||||
if (validationError) {
|
||||
helper = <p className="help is-danger">{errorMessage}</p>;
|
||||
} else if (informationMessage) {
|
||||
helper = <p className="help is-info">{informationMessage}</p>;
|
||||
}
|
||||
return (
|
||||
<div className={classNames("field", className)}>
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<input
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
}}
|
||||
className={classNames("input", errorView)}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={this.handleInput}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
disabled={disabled}
|
||||
onBlur={this.handleBlur}
|
||||
{...createAttributesForTesting(testId)}
|
||||
/>
|
||||
</div>
|
||||
{helper}
|
||||
</div>
|
||||
);
|
||||
const errorView = validationError ? "is-danger" : "";
|
||||
let helper;
|
||||
if (validationError) {
|
||||
helper = <p className="help is-danger">{errorMessage}</p>;
|
||||
} else if (informationMessage) {
|
||||
helper = <p className="help is-info">{informationMessage}</p>;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className={classNames("field", className)}>
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<input
|
||||
ref={field}
|
||||
name={name}
|
||||
className={classNames("input", errorView)}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onKeyPress={handleKeyPress}
|
||||
onBlur={handleBlur}
|
||||
defaultValue={defaultValue}
|
||||
{...createAttributesForTesting(testId)}
|
||||
/>
|
||||
</div>
|
||||
{helper}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InputField: FieldType<BaseProps, HTMLInputElement, string> = createFormFieldWrapper(InnerInputField);
|
||||
|
||||
export default InputField;
|
||||
|
||||
@@ -21,10 +21,14 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React from "react";
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import Radio from "./Radio";
|
||||
import styled from "styled-components";
|
||||
import Button from "../buttons/Button";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { SubmitButton } from "../buttons";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
const Spacing = styled.div`
|
||||
padding: 2em;
|
||||
@@ -39,7 +43,73 @@ const RadioList = styled.div`
|
||||
padding: 2em;
|
||||
`;
|
||||
|
||||
const Ref: FC = () => {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<>
|
||||
<Radio label={"Ref Radio Button"} checked={false} ref={ref} />
|
||||
<Button
|
||||
action={() => {
|
||||
if (ref.current) {
|
||||
ref.current.checked = true;
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Check InputField
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Settings = {
|
||||
rememberMe: string;
|
||||
scramblePassword: string;
|
||||
};
|
||||
|
||||
const ReactHookForm: FC = () => {
|
||||
const { register, handleSubmit } = useForm<Settings>();
|
||||
const [stored, setStored] = useState<Settings>();
|
||||
|
||||
const onSubmit = (settings: Settings) => {
|
||||
setStored(settings);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<RadioList>
|
||||
<Radio defaultChecked={true} value={"true"} label="Remember Me" {...register("rememberMe")} />
|
||||
<Radio value={"false"} label="Dont Remember Me" {...register("rememberMe")} />
|
||||
</RadioList>
|
||||
<Radio className="ml-2" value={"false"} label="Scramble Password" {...register("scramblePassword")} />
|
||||
<div className="pt-2">
|
||||
<SubmitButton>Submit</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
{stored ? (
|
||||
<div className="mt-5">
|
||||
<pre>
|
||||
<code>{JSON.stringify(stored, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LegacyEvents: FC = () => {
|
||||
const [value, setValue] = useState<boolean>(false);
|
||||
return (
|
||||
<>
|
||||
<Radio checked={value} onChange={setValue} />
|
||||
<div className="mt-3">{JSON.stringify(value)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Forms|Radio", module)
|
||||
.addDecorator(storyFn => <MemoryRouter>{storyFn()}</MemoryRouter>)
|
||||
.add("Default", () => (
|
||||
<Spacing>
|
||||
<Radio label="Not checked" checked={false} />
|
||||
@@ -60,4 +130,7 @@ storiesOf("Forms|Radio", module)
|
||||
helpText="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
|
||||
/>
|
||||
</RadioList>
|
||||
));
|
||||
))
|
||||
.add("Ref", () => <Ref />)
|
||||
.add("Legacy Events", () => <LegacyEvents />)
|
||||
.add("ReactHookForm", () => <ReactHookForm />);
|
||||
|
||||
@@ -21,61 +21,81 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent } from "react";
|
||||
import React, { ChangeEvent, FC, FocusEvent } from "react";
|
||||
import { Help } from "../index";
|
||||
import styled from "styled-components";
|
||||
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
|
||||
import classNames from "classnames";
|
||||
|
||||
const StyledRadio = styled.label`
|
||||
margin-right: 0.5em;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
type BaseProps = {
|
||||
label?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
checked: boolean;
|
||||
onChange?: (value: boolean, name?: string) => void;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
defaultChecked?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
class Radio extends React.Component<Props> {
|
||||
renderHelp = () => {
|
||||
const helpText = this.props.helpText;
|
||||
const InnerRadio: FC<FieldProps<BaseProps, HTMLInputElement, boolean>> = ({ name, defaultChecked, ...props }) => {
|
||||
const renderHelp = () => {
|
||||
const helpText = props.helpText;
|
||||
if (helpText) {
|
||||
return <Help message={helpText} />;
|
||||
}
|
||||
};
|
||||
|
||||
onValueChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(event.target.checked, this.props.name);
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (props.onChange) {
|
||||
if (isUsingRef<BaseProps, HTMLInputElement, boolean>(props)) {
|
||||
props.onChange(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onChange(Boolean(event.target.checked), name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
we have to ignore the next line,
|
||||
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
|
||||
if (props.onBlur) {
|
||||
if (isUsingRef<BaseProps, HTMLInputElement, boolean>(props)) {
|
||||
props.onBlur(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onBlur(Boolean(event.target.checked), name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
we have to ignore the next line,
|
||||
because jsx label does not the custom disabled attribute
|
||||
but bulma does.
|
||||
// @ts-ignore */}
|
||||
<StyledRadio className="radio" disabled={this.props.disabled}>
|
||||
<input
|
||||
type="radio"
|
||||
name={this.props.name}
|
||||
value={this.props.value}
|
||||
checked={this.props.checked}
|
||||
onChange={this.onValueChange}
|
||||
disabled={this.props.disabled}
|
||||
/>{" "}
|
||||
{this.props.label}
|
||||
{this.renderHelp()}
|
||||
</StyledRadio>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
<StyledRadio className={classNames("radio", props.className)} disabled={props.disabled}>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={props.value}
|
||||
checked={props.checked}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={props.disabled}
|
||||
ref={props.innerRef}
|
||||
defaultChecked={defaultChecked}
|
||||
/>{" "}
|
||||
{props.label}
|
||||
{renderHelp()}
|
||||
</StyledRadio>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Radio: FieldType<BaseProps, HTMLInputElement, boolean> = createFormFieldWrapper(InnerRadio);
|
||||
|
||||
export default Radio;
|
||||
|
||||
130
scm-ui/ui-components/src/forms/Select.stories.tsx
Normal file
130
scm-ui/ui-components/src/forms/Select.stories.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import Button from "../buttons/Button";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { SubmitButton } from "../buttons";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import Select from "./Select";
|
||||
|
||||
const Ref: FC = () => {
|
||||
const ref = useRef<HTMLSelectElement>(null);
|
||||
const [selected, setSelected] = useState("false");
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "foo", value: "true" },
|
||||
{ label: "bar", value: "false" }
|
||||
]}
|
||||
value={selected}
|
||||
label={"Ref Radio Button"}
|
||||
onChange={e => setSelected(e.target.value)}
|
||||
ref={ref}
|
||||
/>
|
||||
<Button
|
||||
action={() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Focus Select Field
|
||||
</Button>
|
||||
{selected}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Settings = {
|
||||
rememberMe: string;
|
||||
scramblePassword: string;
|
||||
};
|
||||
|
||||
const ReactHookForm: FC = () => {
|
||||
const { register, handleSubmit } = useForm<Settings>();
|
||||
const [stored, setStored] = useState<Settings>();
|
||||
|
||||
const onSubmit = (settings: Settings) => {
|
||||
setStored(settings);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "Yes", value: "true" },
|
||||
{ label: "No", value: "false" }
|
||||
]}
|
||||
label="Remember Me"
|
||||
{...register("rememberMe")}
|
||||
/>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "Yes", value: "true" },
|
||||
{ label: "No", value: "false" }
|
||||
]}
|
||||
label="Scramble Password"
|
||||
{...register("scramblePassword")}
|
||||
/>
|
||||
|
||||
<div className="pt-2">
|
||||
<SubmitButton>Submit</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
{stored ? (
|
||||
<div className="mt-5">
|
||||
<pre>
|
||||
<code>{JSON.stringify(stored, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LegacyEvents: FC = () => {
|
||||
const [value, setValue] = useState<string>();
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "Yes", value: "true" },
|
||||
{ label: "No", value: "false" }
|
||||
]}
|
||||
onChange={setValue}
|
||||
/>
|
||||
<div className="mt-3">{JSON.stringify(value)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Forms|Select", module)
|
||||
.addDecorator(storyFn => <MemoryRouter>{storyFn()}</MemoryRouter>)
|
||||
.add("Ref", () => <Ref />)
|
||||
.add("Legacy Events", () => <LegacyEvents />)
|
||||
.add("ReactHookForm", () => <ReactHookForm />);
|
||||
@@ -21,72 +21,107 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent } from "react";
|
||||
import React, { ChangeEvent, FC, FocusEvent, useEffect } from "react";
|
||||
import classNames from "classnames";
|
||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||
import {createAttributesForTesting} from "../devBuild";
|
||||
import { createAttributesForTesting } from "../devBuild";
|
||||
import useInnerRef from "./useInnerRef";
|
||||
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
|
||||
|
||||
export type SelectItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
type BaseProps = {
|
||||
name?: string;
|
||||
label?: string;
|
||||
options: SelectItem[];
|
||||
value?: string;
|
||||
onChange: (value: string, name?: string) => void;
|
||||
loading?: boolean;
|
||||
helpText?: string;
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
class Select extends React.Component<Props> {
|
||||
field: HTMLSelectElement | null | undefined;
|
||||
const InnerSelect: FC<FieldProps<BaseProps, HTMLSelectElement, string>> = ({
|
||||
value,
|
||||
defaultValue,
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
loading,
|
||||
disabled,
|
||||
testId,
|
||||
...props
|
||||
}) => {
|
||||
const field = useInnerRef(props.innerRef);
|
||||
|
||||
componentDidMount() {
|
||||
// trigger change after render, if value is null to set it to the first value
|
||||
// of the given options.
|
||||
if (!this.props.value && this.field && this.field.value) {
|
||||
this.props.onChange(this.field.value);
|
||||
const handleInput = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (props.onChange) {
|
||||
if (isUsingRef<BaseProps, HTMLSelectElement, string>(props)) {
|
||||
props.onChange(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onChange(event.target.value, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleInput = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
this.props.onChange(event.target.value, this.props.name);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { options, value, label, helpText, loading, disabled, testId } = this.props;
|
||||
const loadingClass = loading ? "is-loading" : "";
|
||||
const handleBlur = (event: FocusEvent<HTMLSelectElement>) => {
|
||||
if (props.onBlur) {
|
||||
if (isUsingRef<BaseProps, HTMLSelectElement, string>(props)) {
|
||||
props.onBlur(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onBlur(event.target.value, name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className={classNames("control select", loadingClass)}>
|
||||
<select
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
}}
|
||||
value={value}
|
||||
onChange={this.handleInput}
|
||||
disabled={disabled}
|
||||
{...createAttributesForTesting(testId)}
|
||||
>
|
||||
{options.map(opt => {
|
||||
return (
|
||||
<option value={opt.value} key={"KEY_" + opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
// trigger change after render, if value is null to set it to the first value
|
||||
// of the given options.
|
||||
if (!value && field.current?.value) {
|
||||
if (props.onChange) {
|
||||
if (isUsingRef<BaseProps, HTMLSelectElement, string>(props)) {
|
||||
const event = new Event("change");
|
||||
field.current?.dispatchEvent(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onChange(field.current?.value, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [field, name, props, value]);
|
||||
|
||||
const loadingClass = loading ? "is-loading" : "";
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className={classNames("control select", loadingClass)}>
|
||||
<select
|
||||
name={name}
|
||||
ref={field}
|
||||
value={value}
|
||||
defaultValue={defaultValue}
|
||||
onChange={handleInput}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
{...createAttributesForTesting(testId)}
|
||||
>
|
||||
{props.options.map(opt => {
|
||||
return (
|
||||
<option value={opt.value} key={"KEY_" + opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Select: FieldType<BaseProps, HTMLSelectElement, string> = createFormFieldWrapper(InnerSelect);
|
||||
|
||||
export default Select;
|
||||
|
||||
@@ -21,10 +21,14 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import styled from "styled-components";
|
||||
import Textarea from "./Textarea";
|
||||
import Button from "../buttons/Button";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { SubmitButton } from "../buttons";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
const Spacing = styled.div`
|
||||
padding: 2em;
|
||||
@@ -59,7 +63,7 @@ const OnSubmitTextare = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const OnCancelTextare = () => {
|
||||
const OnCancelTextarea = () => {
|
||||
const [value, setValue] = useState("Use the escape key to clear the textarea");
|
||||
|
||||
const cancel = () => {
|
||||
@@ -73,7 +77,97 @@ const OnCancelTextare = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Ref: FC = () => {
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
return (
|
||||
<>
|
||||
<Textarea ref={ref} />
|
||||
<Button action={() => ref.current?.focus()} color="primary">
|
||||
Focus InputField
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AutoFocusAndRef: FC = () => {
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
return (
|
||||
<>
|
||||
<Textarea ref={ref} autofocus={true} placeholder="Click the button to focus me" />
|
||||
<Textarea placeholder="Click me to switch focus" />
|
||||
<Button action={() => ref.current?.focus()} color="primary">
|
||||
Focus First InputField
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Commit = {
|
||||
message: string;
|
||||
footer: string;
|
||||
};
|
||||
|
||||
const ReactHookForm: FC = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
} = useForm<Commit>();
|
||||
const [stored, setStored] = useState<Commit>();
|
||||
|
||||
const onSubmit = (commit: Commit) => {
|
||||
setStored(commit);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Textarea
|
||||
autofocus={true}
|
||||
label="Message"
|
||||
{...register("message", { required: true })}
|
||||
validationError={!!errors.message}
|
||||
errorMessage={"Message is required"}
|
||||
/>
|
||||
<Textarea label="Footer" {...register("footer")} />
|
||||
<div className="pt-2">
|
||||
<SubmitButton>Submit</SubmitButton>
|
||||
</div>
|
||||
</form>
|
||||
{stored ? (
|
||||
<div className="mt-5">
|
||||
<pre>
|
||||
<code>{JSON.stringify(stored, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LegacyEvents: FC = () => {
|
||||
const [value, setValue] = useState<string>("");
|
||||
return (
|
||||
<>
|
||||
<Textarea placeholder="Legacy onChange handler" value={value} onChange={e => setValue(e)} />
|
||||
<div className="mt-3">{value}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
storiesOf("Forms|Textarea", module)
|
||||
.addDecorator(storyFn => <MemoryRouter>{storyFn()}</MemoryRouter>)
|
||||
.add("OnChange", () => <OnChangeTextarea />)
|
||||
.add("OnSubmit", () => <OnSubmitTextare />)
|
||||
.add("OnCancel", () => <OnCancelTextare />);
|
||||
.add("OnCancel", () => <OnCancelTextarea />)
|
||||
.add("AutoFocus", () => <Textarea label="Field with AutoFocus" autofocus={true} />)
|
||||
.add("Default Value", () => (
|
||||
<Textarea
|
||||
label="Field with Default Value"
|
||||
defaultValue={"I am a text area with so much default value its crazy!"}
|
||||
/>
|
||||
))
|
||||
.add("Ref", () => <Ref />)
|
||||
.add("Legacy Events", () => <LegacyEvents />)
|
||||
.add("AutoFocusAndRef", () => <AutoFocusAndRef />)
|
||||
.add("ReactHookForm", () => <ReactHookForm />);
|
||||
|
||||
@@ -21,70 +21,107 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { ChangeEvent, KeyboardEvent } from "react";
|
||||
import React, { ChangeEvent, FC, FocusEvent, KeyboardEvent } from "react";
|
||||
import LabelWithHelpIcon from "./LabelWithHelpIcon";
|
||||
import useAutofocus from "./useAutofocus";
|
||||
import classNames from "classnames";
|
||||
import { createFormFieldWrapper, FieldProps, FieldType, isLegacy, isUsingRef } from "./FormFieldTypes";
|
||||
|
||||
type Props = {
|
||||
type BaseProps = {
|
||||
name?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
autofocus?: boolean;
|
||||
onChange: (value: string, name?: string) => void;
|
||||
helpText?: string;
|
||||
disabled?: boolean;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
validationError?: boolean;
|
||||
errorMessage?: string | string[];
|
||||
informationMessage?: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
class Textarea extends React.Component<Props> {
|
||||
field: HTMLTextAreaElement | null | undefined;
|
||||
const InnerTextarea: FC<FieldProps<BaseProps, HTMLTextAreaElement, string>> = ({
|
||||
placeholder,
|
||||
value,
|
||||
autofocus,
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
disabled,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
errorMessage,
|
||||
validationError,
|
||||
informationMessage,
|
||||
defaultValue,
|
||||
...props
|
||||
}) => {
|
||||
const ref = useAutofocus<HTMLTextAreaElement>(autofocus, props.innerRef);
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.autofocus && this.field) {
|
||||
this.field.focus();
|
||||
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (props.onChange) {
|
||||
if (isUsingRef<BaseProps, HTMLTextAreaElement, string>(props)) {
|
||||
props.onChange(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onChange(event.target.value, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleInput = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.props.onChange(event.target.value, this.props.name);
|
||||
};
|
||||
|
||||
onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const { onCancel } = this.props;
|
||||
const handleBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
|
||||
if (props.onBlur) {
|
||||
if (isUsingRef<BaseProps, HTMLTextAreaElement, string>(props)) {
|
||||
props.onBlur(event);
|
||||
} else if (isLegacy(props)) {
|
||||
props.onBlur(event.target.value, name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (onCancel && event.key === "Escape") {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const { onSubmit } = this.props;
|
||||
if (onSubmit && event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { placeholder, value, label, helpText, disabled } = this.props;
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
onChange={this.handleInput}
|
||||
value={value}
|
||||
disabled={!!disabled}
|
||||
onKeyDown={this.onKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const errorView = validationError ? "is-danger" : "";
|
||||
let helper;
|
||||
if (validationError) {
|
||||
helper = <p className="help is-danger">{errorMessage}</p>;
|
||||
} else if (informationMessage) {
|
||||
helper = <p className="help is-info">{informationMessage}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="field">
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
<div className="control">
|
||||
<textarea
|
||||
className={classNames("textarea", errorView)}
|
||||
ref={ref}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onKeyDown={onKeyDown}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
</div>
|
||||
{helper}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Textarea: FieldType<BaseProps, HTMLTextAreaElement, string> = createFormFieldWrapper(InnerTextarea);
|
||||
|
||||
export default Textarea;
|
||||
|
||||
50
scm-ui/ui-components/src/forms/useAutofocus.ts
Normal file
50
scm-ui/ui-components/src/forms/useAutofocus.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { useRef } from "react";
|
||||
|
||||
const useAutofocus = <T extends HTMLElement | null>(enabled?: boolean, innerRef?: React.ForwardedRef<T>) => {
|
||||
const ref = useRef<T>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [enabled, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (innerRef) {
|
||||
if (typeof innerRef === "function") {
|
||||
innerRef(ref.current);
|
||||
} else {
|
||||
innerRef.current = ref.current;
|
||||
}
|
||||
}
|
||||
}, [ref, innerRef]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
export default useAutofocus;
|
||||
43
scm-ui/ui-components/src/forms/useInnerRef.ts
Normal file
43
scm-ui/ui-components/src/forms/useInnerRef.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
const useInnerRef = <T extends HTMLElement | null>(innerRef?: React.ForwardedRef<T>) => {
|
||||
const ref = useRef<T>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (innerRef) {
|
||||
if (typeof innerRef === "function") {
|
||||
innerRef(ref.current);
|
||||
} else {
|
||||
innerRef.current = ref.current;
|
||||
}
|
||||
}
|
||||
}, [ref, innerRef]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
export default useInnerRef;
|
||||
Reference in New Issue
Block a user