From 9bf255ea1b50ca2a9178a92d2a06314a3b058427 Mon Sep 17 00:00:00 2001 From: Moe Shaaban Date: Fri, 13 Nov 2020 22:11:10 +0100 Subject: [PATCH 1/9] refactor(form-controls): use functional components and react hooks --- .../src/components/Button/Button.tsx | 197 +++++++++--------- .../src/components/Checkbox/Checkbox.tsx | 12 +- .../CheckboxField/CheckboxField.tsx | 26 ++- .../ControlledInput/ControlledInput.tsx | 117 +++++------ .../ControlledInputField.tsx | 144 +++++++------ .../components/Form/FieldGroup/FieldGroup.tsx | 32 ++- .../src/components/Form/Form.tsx | 76 ++++--- .../src/components/FormLabel/FormLabel.tsx | 68 +++--- .../src/components/IconButton/IconButton.tsx | 108 +++++----- .../components/RadioButton/RadioButton.tsx | 12 +- .../RadioButtonField/RadioButtonField.tsx | 18 +- .../src/components/Select/Option/Option.tsx | 22 +- .../src/components/Select/Select.tsx | 155 +++++++------- .../components/SelectField/SelectField.tsx | 164 +++++++-------- .../src/components/TextField/TextField.tsx | 193 ++++++++--------- .../TextInput/TextInput.stories.tsx | 2 +- .../src/components/TextInput/TextInput.tsx | 177 ++++++++-------- .../src/components/Textarea/Textarea.tsx | 138 ++++++------ .../components/ToggleButton/ToggleButton.tsx | 111 +++++----- 19 files changed, 850 insertions(+), 922 deletions(-) diff --git a/packages/forma-36-react-components/src/components/Button/Button.tsx b/packages/forma-36-react-components/src/components/Button/Button.tsx index 209075d45b..2890ea5d0e 100644 --- a/packages/forma-36-react-components/src/components/Button/Button.tsx +++ b/packages/forma-36-react-components/src/components/Button/Button.tsx @@ -1,10 +1,10 @@ import React, { - Component, CSSProperties, FocusEvent, MouseEvent as ReactMouseEvent, FocusEventHandler, MouseEventHandler, + ElementType, } from 'react'; import cn from 'classnames'; import { CSSTransition } from 'react-transition-group'; @@ -49,109 +49,106 @@ const defaultProps: Partial = { type: 'button', }; -export class Button extends Component { - static defaultProps = defaultProps; +export const Button = (props: ButtonProps) => { + const { + className, + children, + icon, + buttonType, + size, + isFullWidth, + onBlur, + testId, + onClick, + loading, + disabled, + indicateDropdown, + href, + type, + isActive, + ...otherProps + } = props; - render() { - const { - className, - children, - icon, - buttonType, - size, - isFullWidth, - onBlur, - testId, - onClick, - loading, - disabled, - indicateDropdown, - href, - type, - isActive, - ...otherProps - } = this.props; + const classNames = cn( + styles.Button, + className, + styles[`Button--${buttonType}`], + { + [styles['Button--disabled']]: disabled, + [styles[`Button--${size}`]]: size, + [styles['Button--full-width']]: isFullWidth, + [styles['Button--is-active']]: isActive, + }, + ); - const classNames = cn( - styles.Button, - className, - styles[`Button--${buttonType}`], - { - [styles['Button--disabled']]: disabled, - [styles[`Button--${size}`]]: size, - [styles['Button--full-width']]: isFullWidth, - [styles['Button--is-active']]: isActive, - }, - ); + const iconColor = + buttonType === 'muted' || buttonType === 'naked' ? 'secondary' : 'white'; - const iconColor = - buttonType === 'muted' || buttonType === 'naked' ? 'secondary' : 'white'; + const Element: ElementType = href ? 'a' : 'button'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const Element: any = href ? 'a' : 'button'; + return ( + { + if (onBlur && !disabled) { + onBlur(e); + } + }} + onClick={(e: ReactMouseEvent) => { + if (onClick && !disabled && !loading) { + onClick(e); + } + }} + data-test-id={testId} + className={classNames} + disabled={disabled} + href={!disabled ? href : undefined} + type={type} + {...otherProps} + > + + {icon && ( + + )} + {children && {children}} + {indicateDropdown && ( + + )} + + + + + + ); +}; - return ( - { - if (onBlur && !disabled) { - onBlur(e); - } - }} - onClick={(e: ReactMouseEvent) => { - if (onClick && !disabled && !loading) { - onClick(e); - } - }} - data-test-id={testId} - className={classNames} - disabled={disabled} - href={!disabled ? href : null} - type={type} - {...otherProps} - > - - {icon && ( - - )} - {children && {children}} - {indicateDropdown && ( - - )} - - - - - - ); - } -} +Button.defaultProps = defaultProps; export default Button; diff --git a/packages/forma-36-react-components/src/components/Checkbox/Checkbox.tsx b/packages/forma-36-react-components/src/components/Checkbox/Checkbox.tsx index b0ed21b0c7..fd294bad88 100644 --- a/packages/forma-36-react-components/src/components/Checkbox/Checkbox.tsx +++ b/packages/forma-36-react-components/src/components/Checkbox/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import ControlledInput, { ControlledInputPropTypes } from '../ControlledInput'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -12,13 +12,9 @@ const defaultProps: Partial = { willBlurOnEsc: true, }; -export class Checkbox extends Component { - static defaultProps = defaultProps; - - render() { - return ; - } -} +export const Checkbox = (props: CheckboxProps) => { + return ; +}; Checkbox.defaultProps = defaultProps; diff --git a/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.tsx b/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.tsx index c41c9b1a97..4766addb32 100644 --- a/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.tsx +++ b/packages/forma-36-react-components/src/components/CheckboxField/CheckboxField.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import ControlledInputField, { ControlledInputFieldPropTypes, } from '../ControlledInputField'; @@ -13,20 +13,18 @@ const defaultProps: Partial = { testId: 'cf-ui-checkbox-field', }; -export class CheckboxField extends Component { - static defaultProps = defaultProps; +export const CheckboxField = (props: CheckboxFieldProps) => { + const { testId, ...otherProps } = props; - render() { - const { testId, ...otherProps } = this.props; + return ( + + ); +}; - return ( - - ); - } -} +CheckboxField.defaultProps = defaultProps; export default CheckboxField; diff --git a/packages/forma-36-react-components/src/components/ControlledInput/ControlledInput.tsx b/packages/forma-36-react-components/src/components/ControlledInput/ControlledInput.tsx index 391e96c021..e566fbe01e 100644 --- a/packages/forma-36-react-components/src/components/ControlledInput/ControlledInput.tsx +++ b/packages/forma-36-react-components/src/components/ControlledInput/ControlledInput.tsx @@ -1,5 +1,4 @@ import React, { - Component, EventHandler, ChangeEvent, FocusEvent, @@ -34,72 +33,70 @@ const defaultProps: Partial = { willBlurOnEsc: true, }; -export class ControlledInput extends Component { - static defaultProps = defaultProps; +export const ControlledInput = (props: ControlledInputPropTypes) => { + const { + className, + id, + testId, + required, + disabled, + onFocus, + onBlur, + name, + onChange, + checked, + value, + type, + labelText, + willBlurOnEsc, + ...otherProps + } = props; - handleKeyDown = (e: KeyboardEvent) => { + const classNames = cn(styles['ControlledInput'], className, { + [styles['ControlledInput--disabled']]: disabled, + }); + + const handleKeyDown = (e: KeyboardEvent) => { const ESC = 27; - if (e.keyCode === ESC && this.props.willBlurOnEsc) { + if (e.keyCode === ESC && willBlurOnEsc) { e.currentTarget.blur(); } }; - render() { - const { - className, - id, - testId, - required, - disabled, - onFocus, - onBlur, - name, - onChange, - checked, - value, - type, - labelText, - willBlurOnEsc, - ...otherProps - } = this.props; - - const classNames = cn(styles['ControlledInput'], className, { - [styles['ControlledInput--disabled']]: disabled, - }); + return ( + { + if (onChange) { + onChange(e); + } + }} + onBlur={(e) => { + if (onBlur) { + onBlur(e); + } + }} + onFocus={(e) => { + if (onFocus) { + onFocus(e); + } + }} + aria-label={labelText} + id={id} + required={required} + disabled={disabled} + onKeyDown={handleKeyDown} + {...otherProps} + /> + ); +}; - return ( - { - if (onChange) { - onChange(e); - } - }} - onBlur={(e) => { - if (onBlur) { - onBlur(e); - } - }} - onFocus={(e) => { - if (onFocus) { - onFocus(e); - } - }} - aria-label={labelText} - id={id} - required={required} - disabled={disabled} - onKeyDown={this.handleKeyDown} - {...otherProps} - /> - ); - } -} +ControlledInput.defaultProps = defaultProps; export default ControlledInput; diff --git a/packages/forma-36-react-components/src/components/ControlledInputField/ControlledInputField.tsx b/packages/forma-36-react-components/src/components/ControlledInputField/ControlledInputField.tsx index 3a647fc445..1694ff6bb6 100644 --- a/packages/forma-36-react-components/src/components/ControlledInputField/ControlledInputField.tsx +++ b/packages/forma-36-react-components/src/components/ControlledInputField/ControlledInputField.tsx @@ -1,4 +1,4 @@ -import React, { Component, ChangeEventHandler } from 'react'; +import React, { ChangeEventHandler } from 'react'; import cn from 'classnames'; import FormLabel from '../FormLabel'; import HelpText from '../HelpText'; @@ -34,83 +34,79 @@ const defaultProps: Partial = { inputType: 'checkbox', }; -export class ControlledInputField extends Component< - ControlledInputFieldPropTypes -> { - static defaultProps = defaultProps; +export const ControlledInputField = (props: ControlledInputFieldPropTypes) => { + const { + id, + labelIsLight, + testId, + required, + helpText, + disabled, + labelText, + helpTextProps, + formLabelProps, + className, + checked, + value, + validationMessage, + onChange, + children, + inputType, + inputProps, + name, + ...otherProps + } = props; - render() { - const { - id, - labelIsLight, - testId, - required, - helpText, - disabled, - labelText, - helpTextProps, - formLabelProps, - className, - checked, - value, - validationMessage, - onChange, - children, - inputType, - inputProps, - name, - ...otherProps - } = this.props; + const classNames = cn(styles['ControlledInputField'], className, { + [styles['ControlledInputField--disabled']]: disabled, + }); - const classNames = cn(styles['ControlledInputField'], className, { - [styles['ControlledInputField--disabled']]: disabled, - }); - - return ( -
- + +
+ -
- + {labelText} + + {helpText && ( + + {helpText} + + )} + {validationMessage && ( + - {labelText} - - {helpText && ( - - {helpText} - - )} - {validationMessage && ( - - {validationMessage} - - )} -
+ {validationMessage} + + )}
- ); - } -} +
+ ); +}; + +ControlledInputField.defaultProps = defaultProps; export default ControlledInputField; diff --git a/packages/forma-36-react-components/src/components/Form/FieldGroup/FieldGroup.tsx b/packages/forma-36-react-components/src/components/Form/FieldGroup/FieldGroup.tsx index 2b243fca4b..e2f3c92467 100644 --- a/packages/forma-36-react-components/src/components/Form/FieldGroup/FieldGroup.tsx +++ b/packages/forma-36-react-components/src/components/Form/FieldGroup/FieldGroup.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import cn from 'classnames'; import styles from './FieldGroup.css'; @@ -16,24 +16,22 @@ const defaultProps: Partial = { testId: 'cf-ui-field-group', }; -export class FieldGroup extends Component { - static defaultProps = defaultProps; +export const FieldGroup = (props: FieldGroupProps) => { + const { className, children, row, testId, ...otherProps } = props; - render() { - const { className, children, row, testId, ...otherProps } = this.props; + const classNames = cn(styles.FieldGroup, className, { + [styles['FieldGroup--row']]: row, + }); - const classNames = cn(styles.FieldGroup, className, { - [styles['FieldGroup--row']]: row, - }); + return ( +
+ {React.Children.map(children, (child) => ( +
{child}
+ ))} +
+ ); +}; - return ( -
- {React.Children.map(children, (child) => ( -
{child}
- ))} -
- ); - } -} +FieldGroup.defaultProps = defaultProps; export default FieldGroup; diff --git a/packages/forma-36-react-components/src/components/Form/Form.tsx b/packages/forma-36-react-components/src/components/Form/Form.tsx index 10d396a3f8..e8f2eef410 100644 --- a/packages/forma-36-react-components/src/components/Form/Form.tsx +++ b/packages/forma-36-react-components/src/components/Form/Form.tsx @@ -1,4 +1,4 @@ -import React, { Component, FormEventHandler, FormEvent } from 'react'; +import React, { FormEventHandler, FormEvent } from 'react'; import cn from 'classnames'; import styles from './Form.css'; @@ -17,49 +17,47 @@ const defaultProps: Partial = { testId: 'cf-ui-form', }; -export class Form extends Component { - static defaultProps = defaultProps; +export const Form = (props: FormProps) => { + const { + className, + children, + testId, + onSubmit, + spacing, + ...otherProps + } = props; - handleSubmit = (event: FormEvent) => { + const classNames = cn(styles.Form, className); + + const formItemClassNames = cn( + styles.Form__item, + styles[`Form__item--${spacing}`], + ); + + const handleSubmit = (event: FormEvent) => { event.preventDefault(); - if (this.props.onSubmit) { - this.props.onSubmit(event); + if (onSubmit) { + onSubmit(event); } }; - render() { - const { - className, - children, - testId, - onSubmit, - spacing, - ...otherProps - } = this.props; - - const classNames = cn(styles.Form, className); - - const formItemClassNames = cn( - styles.Form__item, - styles[`Form__item--${spacing}`], - ); + return ( +
+ {React.Children.map(children, (child) => { + if (child) { + return
{child}
; + } + return null; + })} +
+ ); +}; - return ( -
- {React.Children.map(children, (child) => { - if (child) { - return
{child}
; - } - return null; - })} -
- ); - } -} +Form.defaultProps = defaultProps; export default Form; diff --git a/packages/forma-36-react-components/src/components/FormLabel/FormLabel.tsx b/packages/forma-36-react-components/src/components/FormLabel/FormLabel.tsx index 3c5cbc60f5..6fbdfb9461 100644 --- a/packages/forma-36-react-components/src/components/FormLabel/FormLabel.tsx +++ b/packages/forma-36-react-components/src/components/FormLabel/FormLabel.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import cn from 'classnames'; import styles from './FormLabel.css'; @@ -18,39 +18,37 @@ const defaultProps: Partial = { required: false, }; -export class FormLabel extends Component { - static defaultProps = defaultProps; - - render() { - const { - className, - children, - testId, - htmlFor, - requiredText, - required, - ...otherProps - } = this.props; - - const classNames = cn(styles.FormLabel, className); - - return ( - - ); - } -} +export const FormLabel = (props: FormLabelProps) => { + const { + className, + children, + testId, + htmlFor, + requiredText, + required, + ...otherProps + } = props; + + const classNames = cn(styles.FormLabel, className); + + return ( + + ); +}; + +FormLabel.defaultProps = defaultProps; export default FormLabel; diff --git a/packages/forma-36-react-components/src/components/IconButton/IconButton.tsx b/packages/forma-36-react-components/src/components/IconButton/IconButton.tsx index e3b2bb7b6b..155775c446 100644 --- a/packages/forma-36-react-components/src/components/IconButton/IconButton.tsx +++ b/packages/forma-36-react-components/src/components/IconButton/IconButton.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import cn from 'classnames'; import Icon, { IconProps } from '../Icon'; import TabFocusTrap from '../TabFocusTrap'; @@ -29,69 +29,67 @@ const defaultProps: Partial = { withDropdown: false, }; -export class IconButton extends Component { - static defaultProps = defaultProps; +export const IconButton = (props: IconButtonProps) => { + const { + label, + iconProps, + href, + testId, + disabled, + onClick, + buttonType, + withDropdown, + className, + ...otherProps + } = props; - render() { - const { - label, - iconProps, - href, - testId, - disabled, - onClick, - buttonType, - withDropdown, - className, - ...otherProps - } = this.props; + const classNames = cn(styles.IconButton, className, { + [styles['IconButton--disabled']]: disabled, + [styles[`IconButton--${buttonType}`]]: buttonType, + }); - const classNames = cn(styles.IconButton, className, { - [styles['IconButton--disabled']]: disabled, - [styles[`IconButton--${buttonType}`]]: buttonType, - }); + const elementProps = { + className: classNames, + onClick: !disabled ? onClick : undefined, + 'data-test-id': testId, + ...otherProps, + }; - const elementProps = { - className: classNames, - onClick: !disabled ? onClick : undefined, - 'data-test-id': testId, - ...otherProps, - }; - - const content = ( - + const content = ( + + + {label} + {withDropdown && ( - {label} - {withDropdown && ( - - )} - - ); + )} + + ); - if (href) { - if (disabled) { - return {content}; - } - return ( - - content - - ); + if (href) { + if (disabled) { + return {content}; } - return ( - + + content + ); } -} + + return ( + + ); +}; + +IconButton.defaultProps = defaultProps; export default IconButton; diff --git a/packages/forma-36-react-components/src/components/RadioButton/RadioButton.tsx b/packages/forma-36-react-components/src/components/RadioButton/RadioButton.tsx index 5e57387592..19e70bb5c6 100644 --- a/packages/forma-36-react-components/src/components/RadioButton/RadioButton.tsx +++ b/packages/forma-36-react-components/src/components/RadioButton/RadioButton.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import ControlledInput, { ControlledInputPropTypes } from '../ControlledInput'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -12,13 +12,9 @@ const defaultProps: Partial = { willBlurOnEsc: true, }; -export class RadioButton extends Component { - static defaultProps = defaultProps; - - render() { - return ; - } -} +export const RadioButton = (props: RadioButtonProps) => { + return ; +}; RadioButton.defaultProps = defaultProps; diff --git a/packages/forma-36-react-components/src/components/RadioButtonField/RadioButtonField.tsx b/packages/forma-36-react-components/src/components/RadioButtonField/RadioButtonField.tsx index 02ff41936d..98956da9b4 100644 --- a/packages/forma-36-react-components/src/components/RadioButtonField/RadioButtonField.tsx +++ b/packages/forma-36-react-components/src/components/RadioButtonField/RadioButtonField.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; import ControlledInputField, { ControlledInputFieldPropTypes, } from '../ControlledInputField'; @@ -13,16 +13,14 @@ const defaultProps: Partial = { testId: 'cf-ui-radio-button-field', }; -export class RadioButtonField extends Component { - static defaultProps = defaultProps; +export const RadioButtonField = (props: RadioButtonFieldProps) => { + const { testId, ...otherProps } = props; - render() { - const { testId, ...otherProps } = this.props; + return ( + + ); +}; - return ( - - ); - } -} +RadioButtonField.defaultProps = defaultProps; export default RadioButtonField; diff --git a/packages/forma-36-react-components/src/components/Select/Option/Option.tsx b/packages/forma-36-react-components/src/components/Select/Option/Option.tsx index 4a2c345c39..e493eccc65 100644 --- a/packages/forma-36-react-components/src/components/Select/Option/Option.tsx +++ b/packages/forma-36-react-components/src/components/Select/Option/Option.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React from 'react'; export interface OptionProps { value: string; @@ -10,18 +10,16 @@ const defaultProps: Partial = { testId: 'cf-ui-select-option', }; -export class Option extends Component { - static defaultProps = defaultProps; +export const Option = (props: OptionProps) => { + const { value, children, testId, ...otherProps } = props; - render() { - const { value, children, testId, ...otherProps } = this.props; + return ( + + ); +}; - return ( - - ); - } -} +Option.defaultProps = defaultProps; export default Option; diff --git a/packages/forma-36-react-components/src/components/Select/Select.tsx b/packages/forma-36-react-components/src/components/Select/Select.tsx index f3f802b9bd..16e38d0587 100644 --- a/packages/forma-36-react-components/src/components/Select/Select.tsx +++ b/packages/forma-36-react-components/src/components/Select/Select.tsx @@ -1,8 +1,10 @@ import React, { - Component, + useState, + useEffect, ChangeEventHandler, FocusEventHandler, KeyboardEvent, + ChangeEvent, } from 'react'; import cn from 'classnames'; import Icon from '../Icon'; @@ -25,10 +27,6 @@ export interface SelectProps { willBlurOnEsc?: boolean; } -export interface SelectState { - value?: string; -} - const defaultProps: Partial = { testId: 'cf-ui-select', required: false, @@ -38,95 +36,84 @@ const defaultProps: Partial = { willBlurOnEsc: true, }; -export class Select extends Component { - static defaultProps = defaultProps; +export const Select = (props: SelectProps) => { + const { + id, + name, + required, + children, + width, + className, + testId, + onChange, + onBlur, + onFocus, + isDisabled, + hasError, + value, + willBlurOnEsc, + ...otherProps + } = props; + const [valueState, setValueState] = useState(); - state = { - value: this.props.value, - }; + const handleKeyDown = (e: KeyboardEvent) => { + const ESC = 27; - UNSAFE_componentWillReceiveProps(nextProps: SelectProps) { - if (this.props.value !== nextProps.value) { - this.setState({ - value: nextProps.value, - }); + if (e.keyCode === ESC && willBlurOnEsc) { + e.currentTarget.blur(); } - } + }; - handleKeyDown = (e: KeyboardEvent) => { - const ESC = 27; + const handleChange = (e: ChangeEvent) => { + if (!isDisabled) { + setValueState(e.target.value); - if (e.keyCode === ESC && this.props.willBlurOnEsc) { - e.currentTarget.blur(); + if (onChange) { + onChange(e); + } } }; - render() { - const { - id, - name, - required, - children, - width, - className, - testId, - onChange, - onBlur, - onFocus, - isDisabled, - hasError, - willBlurOnEsc, - ...otherProps - } = this.props; + useEffect(() => { + setValueState(value); + }, [value]); - const widthClass = `Select--${width}`; - const classNames = cn(styles['Select'], { - [styles['Select--disabled']]: isDisabled, - [styles['Select--negative']]: hasError, - }); + const widthClass = `Select--${width}`; + const classNames = cn(styles['Select'], { + [styles['Select--disabled']]: isDisabled, + [styles['Select--negative']]: hasError, + }); - const wrapperClassNames = cn( - styles['Select__wrapper'], - styles[widthClass], - className, - ); + const wrapperClassNames = cn( + styles['Select__wrapper'], + styles[widthClass], + className, + ); - return ( -
- - -
- ); - } -} + return ( +
+ + +
+ ); +}; + +Select.defaultProps = defaultProps; export default Select; diff --git a/packages/forma-36-react-components/src/components/SelectField/SelectField.tsx b/packages/forma-36-react-components/src/components/SelectField/SelectField.tsx index cfe7ba3c39..5a3e14c5da 100644 --- a/packages/forma-36-react-components/src/components/SelectField/SelectField.tsx +++ b/packages/forma-36-react-components/src/components/SelectField/SelectField.tsx @@ -1,5 +1,6 @@ import React, { - Component, + useState, + useEffect, ChangeEvent, FocusEventHandler, ChangeEventHandler, @@ -39,101 +40,94 @@ const defaultProps: Partial = { required: false, }; -export class SelectField extends Component { - static defaultProps = defaultProps; - - state = { value: this.props.value }; - - UNSAFE_componentWillReceiveProps(nextProps: SelectFieldProps) { - if (this.props.value !== nextProps.value) { - this.setState({ - value: nextProps.value, - }); - } - } +export const SelectField = (props: SelectFieldProps) => { + const { + validationMessage, + className, + children, + selectProps, + testId, + formLabelProps, + textLinkProps, + labelText, + helpText, + required, + onChange, + onBlur, + value, + name, + id, + ...otherProps + } = props; + const [valueState, setValueState] = useState(value); // Store a copy of the value in state. // This is used by this component when the `countCharacters` // option is on - handleOnChange = (evt: ChangeEvent) => { - this.setState({ value: (evt.target as HTMLSelectElement).value }); - if (this.props.onChange) { - this.props.onChange(evt); + const handleOnChange = (e: ChangeEvent) => { + setValueState(e.currentTarget.value); + if (onChange) { + onChange(e); } }; - render() { - const { - validationMessage, - className, - children, - selectProps, - testId, - formLabelProps, - textLinkProps, - labelText, - helpText, - required, - onChange, - onBlur, - value, - name, - id, - ...otherProps - } = this.props; + useEffect(() => { + setValueState(value); + }, [value]); - const classNames = cn(styles['SelectField'], className); + const classNames = cn(styles['SelectField'], className); - return ( -
-
- - {labelText} - - {textLinkProps && ( - - {textLinkProps.text} - - )} -
- - {validationMessage && ( - +
+ + {labelText} + + {textLinkProps && ( + - {validationMessage} - - )} - {helpText && ( -
- {helpText && ( - - {helpText} - - )} -
+ {textLinkProps.text} +
)}
- ); - } -} + + {validationMessage && ( + + {validationMessage} + + )} + {helpText && ( +
+ {helpText && ( + + {helpText} + + )} +
+ )} +
+ ); +}; + +SelectField.defaultProps = defaultProps; export default SelectField; diff --git a/packages/forma-36-react-components/src/components/TextField/TextField.tsx b/packages/forma-36-react-components/src/components/TextField/TextField.tsx index e9e0d07409..dfb8fafd4e 100644 --- a/packages/forma-36-react-components/src/components/TextField/TextField.tsx +++ b/packages/forma-36-react-components/src/components/TextField/TextField.tsx @@ -1,8 +1,9 @@ import React, { - Component, + useState, ChangeEvent, ChangeEventHandler, FocusEventHandler, + ElementType, } from 'react'; import cn from 'classnames'; import ValidationMessage from '../ValidationMessage'; @@ -47,118 +48,104 @@ const defaultProps: Partial = { width: 'full', }; -export class TextField extends Component { - static defaultProps = defaultProps; - - state = { value: this.props.value || '', initialValue: this.props.value }; +export const TextField = (props: TextFieldProps) => { + const { + validationMessage, + className, + textInputProps, + testId, + width, + formLabelProps, + textLinkProps, + labelText, + helpText, + textarea, + countCharacters, + required, + onChange, + onBlur, + onFocus, + value, + name, + id, + ...otherProps + } = props; + const [valueState, setValueState] = useState(value); // Store a copy of the value in state. // This is used by this component when the `countCharacters` // option is on - handleOnChange = (evt: ChangeEvent) => { - this.setState({ value: (evt.target as HTMLInputElement).value }); - if (this.props.onChange) this.props.onChange(evt); + const handleOnChange = ( + e: ChangeEvent, + ) => { + setValueState(e.target.value); + if (onChange) onChange(e); }; - static getDerivedStateFromProps( - props: TextFieldProps, - state: TextFieldState, - ) { - if (props.value !== state.initialValue) { - return { ...state, value: props.value, initialValue: props.value }; - } - return state; - } - - render() { - const { - validationMessage, - className, - textInputProps, - testId, - width, - formLabelProps, - textLinkProps, - labelText, - helpText, - textarea, - countCharacters, - required, - onChange, - onBlur, - onFocus, - value, - name, - id, - ...otherProps - } = this.props; + const widthClass = `TextField--${width}`; + const classNames = cn(styles['TextField'], styles[widthClass], className); - const widthClass = `TextField--${width}`; - const classNames = cn(styles['TextField'], styles[widthClass], className); + const Element: ElementType = textarea ? Textarea : TextInput; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const Element: any = textarea ? Textarea : TextInput; - return ( -
-
- - {labelText} - - {textLinkProps && ( - - {textLinkProps.text} - - )} -
- - {validationMessage && ( - +
+ + {labelText} + + {textLinkProps && ( + - {validationMessage} - - )} - {(helpText || countCharacters) && ( -
- {helpText && ( - - {helpText} - - )} - {countCharacters && textInputProps && textInputProps.maxLength && ( - - {this.state.value ? this.state.value.length : 0}/ - {textInputProps.maxLength} - - )} -
+ {textLinkProps.text} +
)}
- ); - } -} + + {validationMessage && ( + + {validationMessage} + + )} + {(helpText || countCharacters) && ( +
+ {helpText && ( + + {helpText} + + )} + {countCharacters && textInputProps && textInputProps.maxLength && ( + + {valueState ? valueState.length : 0}/{textInputProps.maxLength} + + )} +
+ )} +
+ ); +}; + +TextField.defaultProps = defaultProps; export default TextField; diff --git a/packages/forma-36-react-components/src/components/TextInput/TextInput.stories.tsx b/packages/forma-36-react-components/src/components/TextInput/TextInput.stories.tsx index 04a1dc2d96..4737a3f06a 100644 --- a/packages/forma-36-react-components/src/components/TextInput/TextInput.stories.tsx +++ b/packages/forma-36-react-components/src/components/TextInput/TextInput.stories.tsx @@ -18,7 +18,7 @@ storiesOf('Components/TextInput', module) disabled={boolean('disabled', false)} isReadOnly={boolean('isReadOnly', false)} withCopyButton={boolean('withCopyButton', false)} - value={text('valiue', '123456')} + value={text('value', '123456')} maxLength={number('maxLength', 50)} width={select( 'width', diff --git a/packages/forma-36-react-components/src/components/TextInput/TextInput.tsx b/packages/forma-36-react-components/src/components/TextInput/TextInput.tsx index a452ee2129..2bffc9a7e0 100644 --- a/packages/forma-36-react-components/src/components/TextInput/TextInput.tsx +++ b/packages/forma-36-react-components/src/components/TextInput/TextInput.tsx @@ -1,4 +1,12 @@ -import React, { Component, RefObject, FocusEvent, KeyboardEvent } from 'react'; +import React, { + useState, + useEffect, + RefObject, + FocusEvent, + KeyboardEvent, + ChangeEvent, + ChangeEventHandler, +} from 'react'; import cn from 'classnames'; import CopyButton from '../CopyButton'; import styles from './TextInput.css'; @@ -20,6 +28,7 @@ export type TextInputProps = { className?: string; withCopyButton?: boolean; testId?: string; + onChange?: ChangeEventHandler; onCopy?: (value: string) => void; value?: string; inputRef?: RefObject; @@ -27,10 +36,6 @@ export type TextInputProps = { willBlurOnEsc: boolean; } & JSX.IntrinsicElements['input']; -export interface TextInputState { - value?: string; -} - const defaultProps: Partial = { withCopyButton: false, testId: 'cf-ui-text-input', @@ -41,107 +46,101 @@ const defaultProps: Partial = { willBlurOnEsc: true, }; -export class TextInput extends Component { - static defaultProps = defaultProps; +export const TextInput = (props: TextInputProps) => { + const { + className, + withCopyButton, + placeholder, + maxLength, + disabled, + required, + isReadOnly, + onChange, + testId, + onBlur, + onCopy, + error, + width, + value, + type, + name, + id, + inputRef, + willBlurOnEsc, + ...otherProps + } = props; - state = { - value: this.props.value, - }; + const [valueState, setValueState] = useState(value); - UNSAFE_componentWillReceiveProps(nextProps: TextInputProps) { - if (this.props.value !== nextProps.value) { - this.setState({ - value: nextProps.value, - }); + const handleFocus = (e: FocusEvent) => { + if (disabled) { + (e.target as HTMLInputElement).select(); } - } + }; - handleFocus = (e: FocusEvent) => { - if (this.props.disabled) { - (e.target as HTMLInputElement).select(); + const handleChange = (e: ChangeEvent) => { + if (disabled || isReadOnly) return; + + if (onChange) { + onChange(e); } + setValueState(e.currentTarget.value); }; - handleKeyDown = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { const ESC = 27; - if (this.props.onKeyDown) { - this.props.onKeyDown(e); + if (props.onKeyDown) { + props.onKeyDown(e); } - if (e.keyCode === ESC && this.props.willBlurOnEsc) { + if (e.keyCode === ESC && props.willBlurOnEsc) { e.currentTarget.blur(); } }; - render() { - const { - className, - withCopyButton, - placeholder, - maxLength, - disabled, - required, - isReadOnly, - onChange, - testId, - onBlur, - onCopy, - error, - width, - value, - type, - name, - id, - inputRef, - willBlurOnEsc, - ...otherProps - } = this.props; - - const widthClass = `TextInput--${width}`; - const classNames = cn(styles['TextInput'], className, styles[widthClass], { - [styles['TextInput--disabled']]: disabled, - [styles['TextInput--negative']]: error, - }); + useEffect(() => { + setValueState(value); + }, [value]); - return ( -
- { - if (disabled || isReadOnly) return; + const widthClass = `TextInput--${width}`; + const classNames = cn(styles['TextInput'], className, styles[widthClass], { + [styles['TextInput--disabled']]: disabled, + [styles['TextInput--negative']]: error, + }); - if (onChange) { - onChange(e); - } - this.setState({ value: e.target.value }); - }} - value={this.state.value} - type={type} - ref={inputRef} - {...otherProps} + return ( +
+ + {withCopyButton && ( + - {withCopyButton && ( - - )} -
- ); - } -} + )} +
+ ); +}; + +TextInput.defaultProps = defaultProps; export default TextInput; diff --git a/packages/forma-36-react-components/src/components/Textarea/Textarea.tsx b/packages/forma-36-react-components/src/components/Textarea/Textarea.tsx index 9106d01cb3..2a495ddcea 100644 --- a/packages/forma-36-react-components/src/components/Textarea/Textarea.tsx +++ b/packages/forma-36-react-components/src/components/Textarea/Textarea.tsx @@ -1,10 +1,12 @@ import React, { - Component, + useState, + useEffect, KeyboardEvent, FocusEventHandler, ChangeEventHandler, KeyboardEventHandler, RefObject, + ChangeEvent, } from 'react'; import cn from 'classnames'; import styles from './Textarea.css'; @@ -44,87 +46,81 @@ const defaultProps: Partial = { willBlurOnEsc: true, }; -export class Textarea extends Component { - static defaultProps = defaultProps; +export const Textarea = (props: TextareaProps) => { + const { + className, + testId, + placeholder, + maxLength, + onChange, + disabled, + required, + onBlur, + onKeyDown, + error, + width, + value, + name, + rows, + id, + willBlurOnEsc, + textareaRef, + ...otherProps + } = props; + const [valueState, setValueState] = useState(value); - state = { - value: this.props.value, - }; - - UNSAFE_componentWillReceiveProps(nextProps: TextareaProps) { - if (this.props.value !== nextProps.value) { - this.setState({ - value: nextProps.value, - }); - } - } + useEffect(() => { + setValueState(value); + }, [value]); - handleKeyDown = (e: KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { const ESC = 27; - if (this.props.onKeyDown) { - this.props.onKeyDown(e); + if (onKeyDown) { + onKeyDown(e); } - if (e.keyCode === ESC && this.props.willBlurOnEsc) { + if (e.keyCode === ESC && willBlurOnEsc) { e.currentTarget.blur(); } }; - render() { - const { - className, - testId, - placeholder, - maxLength, - onChange, - disabled, - required, - onBlur, - error, - width, - value, - name, - rows, - id, - willBlurOnEsc, - textareaRef, - ...otherProps - } = this.props; + const handleChange = (e: ChangeEvent) => { + setValueState(e.target.value); + if (onChange) { + onChange(e); + } + }; + + const widthClass = `Textarea--${width}`; + const classNames = cn(styles['Textarea'], className, styles[widthClass], { + [styles['Textarea--disabled']]: disabled, + [styles['Textarea--negative']]: error, + }); - const widthClass = `Textarea--${width}`; - const classNames = cn(styles['Textarea'], className, styles[widthClass], { - [styles['Textarea--disabled']]: disabled, - [styles['Textarea--negative']]: error, - }); + return ( +
+