From e798cdc50a81898b8a6cf69a8e55f7c37c56b974 Mon Sep 17 00:00:00 2001 From: supergrecko Date: Sat, 21 Dec 2019 21:39:50 +0100 Subject: [PATCH 1/9] Translate EuiFormRow to TS --- src/components/form/form_row/form_row.js | 272 --------------- src/components/form/form_row/form_row.tsx | 319 ++++++++++++++++++ src/components/form/form_row/index.d.ts | 2 +- .../form/form_row/{index.js => index.ts} | 0 4 files changed, 320 insertions(+), 273 deletions(-) delete mode 100644 src/components/form/form_row/form_row.js create mode 100644 src/components/form/form_row/form_row.tsx rename src/components/form/form_row/{index.js => index.ts} (100%) diff --git a/src/components/form/form_row/form_row.js b/src/components/form/form_row/form_row.js deleted file mode 100644 index b67d4bcb793..00000000000 --- a/src/components/form/form_row/form_row.js +++ /dev/null @@ -1,272 +0,0 @@ -import React, { cloneElement, Component, Children } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { get } from '../../../services/objects'; -import { withRequiredProp } from '../../../utils/prop_types/with_required_prop'; - -import { EuiFormHelpText } from '../form_help_text'; -import { EuiFormErrorText } from '../form_error_text'; -import { EuiFormLabel } from '../form_label'; - -import makeId from './make_id'; - -const displayToClassNameMap = { - row: null, - rowCompressed: 'euiFormRow--compressed', - columnCompressed: 'euiFormRow--compressed euiFormRow--horizontal', - center: null, - centerCompressed: 'euiFormRow--compressed', - columnCompressedSwitch: - 'euiFormRow--compressed euiFormRow--horizontal euiFormRow--hasSwitch', -}; - -export const DISPLAYS = Object.keys(displayToClassNameMap); - -export class EuiFormRow extends Component { - constructor(props) { - super(props); - - this.state = { - isFocused: false, - id: props.id || makeId(), - }; - - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - } - - onFocus(...args) { - // Doing this to allow onFocus to be called correctly from the child input element as this component overrides it - const onChildFocus = get(this.props, 'children.props.onFocus'); - if (onChildFocus) { - onChildFocus(...args); - } - - this.setState({ - isFocused: true, - }); - } - - onBlur(...args) { - // Doing this to allow onBlur to be called correctly from the child input element as this component overrides it - const onChildBlur = get(this.props, 'children.props.onBlur'); - if (onChildBlur) { - onChildBlur(...args); - } - - this.setState({ - isFocused: false, - }); - } - - render() { - const { - children, - helpText, - isInvalid, - error, - label, - labelType, - labelAppend, - hasEmptyLabelSpace, - fullWidth, - className, - describedByIds, - compressed, - display, - displayOnly, - hasChildLabel, - id: propsId, - ...rest - } = this.props; - - const { id } = this.state; - - /** - * Remove when `compressed` is deprecated - */ - let shimDisplay; - if (compressed && display === 'row') { - shimDisplay = 'rowCompressed'; - } else { - shimDisplay = display; - } - - /** - * Remove when `displayOnly` is deprecated - */ - if (compressed && displayOnly) { - shimDisplay = 'centerCompressed'; - } else if (displayOnly && display === 'row') { - shimDisplay = 'center'; - } - - const classes = classNames( - 'euiFormRow', - { - 'euiFormRow--hasEmptyLabelSpace': hasEmptyLabelSpace, - 'euiFormRow--fullWidth': fullWidth, - }, - displayToClassNameMap[shimDisplay], - className - ); - - let optionalHelpText; - - if (helpText) { - optionalHelpText = ( - - {helpText} - - ); - } - - let optionalErrors; - - if (error && isInvalid) { - const errorTexts = Array.isArray(error) ? error : [error]; - optionalErrors = errorTexts.map((error, i) => { - const key = typeof error === 'string' ? error : i; - return ( - - {error} - - ); - }); - } - - let optionalLabel; - const isLegend = label && labelType === 'legend' ? true : false; - - if (label || labelAppend) { - optionalLabel = ( -
- - {label} - - {labelAppend && ' '} - {labelAppend} -
- ); - } - - const optionalProps = {}; - const describingIds = [...describedByIds]; - - if (optionalHelpText) { - describingIds.push(optionalHelpText.props.id); - } - - if (optionalErrors) { - optionalErrors.forEach(error => describingIds.push(error.props.id)); - } - - if (describingIds.length > 0) { - optionalProps['aria-describedby'] = describingIds.join(' '); - } - - const field = cloneElement(Children.only(children), { - id, - onFocus: this.onFocus, - onBlur: this.onBlur, - ...optionalProps, - }); - - const fieldWrapperClasses = classNames('euiFormRow__fieldWrapper', { - euiFormRow__fieldWrapperDisplayOnly: - displayOnly || display.startsWith('center'), - }); - - const Element = labelType === 'legend' ? 'fieldset' : 'div'; - - return ( - - {optionalLabel} -
- {field} - {optionalErrors} - {optionalHelpText} -
-
- ); - } -} - -EuiFormRow.propTypes = { - children: PropTypes.element.isRequired, - className: PropTypes.string, - /** - * Escape hatch to not render duplicate labels if the child also renders a label - */ - hasChildLabel: PropTypes.bool, - label: PropTypes.node, - /** - * Sets the type of html element the label should be based - * on the form row contents. For instance checkbox groups - * should use 'legend' instead of the default 'label' - */ - labelType: PropTypes.oneOf(['label', 'legend']), - /** - * Adds an extra node to the right of the form label without - * being contained inside the form label. Good for things - * like documentation links. - */ - labelAppend: withRequiredProp( - PropTypes.node, - 'label', - 'appending to the label requires that the label also exists' - ), - id: PropTypes.string, - isInvalid: PropTypes.bool, - error: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.arrayOf(PropTypes.node), - ]), - helpText: PropTypes.node, - hasEmptyLabelSpace: PropTypes.bool, - fullWidth: PropTypes.bool, - /** - * IDs of additional elements that should be part of children's `aria-describedby` - */ - describedByIds: PropTypes.array, - /** - * **DEPRECATED: use `display: rowCompressed` instead.** - * When `true`, tightens up the spacing. - */ - compressed: PropTypes.bool, - /** - * When `rowCompressed`, just tightens up the spacing; - * Set to `columnCompressed` if compressed - * and horizontal layout is needed. - * Set to `center` or `centerCompressed` to align non-input - * content better with inline rows. - * Set to `columnCompressedSwitch` if the form control being passed - * as the child is a switch. - */ - display: PropTypes.oneOf(DISPLAYS), - /** - * **DEPRECATED: use `display: center` instead.** - * Vertically centers non-input style content so it aligns - * better with input style content. - */ - displayOnly: PropTypes.bool, -}; - -EuiFormRow.defaultProps = { - display: 'row', - hasEmptyLabelSpace: false, - fullWidth: false, - describedByIds: [], - labelType: 'label', - hasChildLabel: true, -}; diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx new file mode 100644 index 00000000000..5b03ca39789 --- /dev/null +++ b/src/components/form/form_row/form_row.tsx @@ -0,0 +1,319 @@ +import React, { cloneElement, Component, Children, HTMLAttributes, ReactNode } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' +import { CommonProps } from '../../common' + +import { get } from '../../../services/objects' +import { withRequiredProp } from '../../../utils/prop_types/with_required_prop' + +import { EuiFormHelpText } from '../form_help_text' +import { EuiFormErrorText } from '../form_error_text' +import { EuiFormLabel } from '../form_label' + +import makeId from './make_id' + +export type FormRowDisplayKeys = 'row' + | 'rowCompressed' + | 'columnCompressed' + | 'center' + | 'centerCompressed' + | 'columnCompressedSwitch'; +type ClassNameMap = { + [key in FormRowDisplayKeys]: string | null +} +const displayToClassNameMap: ClassNameMap = { + row: null, + rowCompressed: 'euiFormRow--compressed', + columnCompressed: 'euiFormRow--compressed euiFormRow--horizontal', + center: null, + centerCompressed: 'euiFormRow--compressed', + columnCompressedSwitch: + 'euiFormRow--compressed euiFormRow--horizontal euiFormRow--hasSwitch', +} + +export const DISPLAYS = Object.keys(displayToClassNameMap) + +interface EuiFormRowState { + isFocused: boolean; + id: string; +} + +export type EuiFormRowProps = CommonProps + & HTMLAttributes + & { + display?: FormRowDisplayKeys; + hasEmptyLabelSpace?: boolean; + fullWidth?: boolean; + describedByIds?: string[]; + labelType?: 'label' | 'legend'; + hasChildLabel?: boolean; + children: ReactNode; + className?: string; + label?: ReactNode; + labelAppend?: any; + id?: string; + isInvalid?: boolean; + error?: ReactNode | Array; + helpText?: ReactNode; + // Deprecated + compressed?: boolean; + displayOnly?: boolean; +}; + +export class EuiFormRow extends Component { + static propTypes = { + children: PropTypes.element.isRequired, + className: PropTypes.string, + /** + * Escape hatch to not render duplicate labels if the child also renders a label + */ + hasChildLabel: PropTypes.bool, + label: PropTypes.node, + /** + * Sets the type of html element the label should be based + * on the form row contents. For instance checkbox groups + * should use 'legend' instead of the default 'label' + */ + labelType: PropTypes.oneOf(['label', 'legend']), + /** + * Adds an extra node to the right of the form label without + * being contained inside the form label. Good for things + * like documentation links. + */ + labelAppend: withRequiredProp( + PropTypes.node, + 'label', + 'appending to the label requires that the label also exists' + ), + id: PropTypes.string, + isInvalid: PropTypes.bool, + error: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.arrayOf(PropTypes.node), + ]), + helpText: PropTypes.node, + hasEmptyLabelSpace: PropTypes.bool, + fullWidth: PropTypes.bool, + /** + * IDs of additional elements that should be part of children's `aria-describedby` + */ + describedByIds: PropTypes.array, + /** + * **DEPRECATED: use `display: rowCompressed` instead.** + * When `true`, tightens up the spacing. + */ + compressed: PropTypes.bool, + /** + * When `rowCompressed`, just tightens up the spacing; + * Set to `columnCompressed` if compressed + * and horizontal layout is needed. + * Set to `center` or `centerCompressed` to align non-input + * content better with inline rows. + * Set to `columnCompressedSwitch` if the form control being passed + * as the child is a switch. + */ + display: PropTypes.oneOf(DISPLAYS), + /** + * **DEPRECATED: use `display: center` instead.** + * Vertically centers non-input style content so it aligns + * better with input style content. + */ + displayOnly: PropTypes.bool, + } + + static defaultProps = { + display: 'row', + hasEmptyLabelSpace: false, + fullWidth: false, + describedByIds: [], + labelType: 'label', + hasChildLabel: true, + } + + constructor(props: EuiFormRowProps) { + super(props) + + this.state = { + isFocused: false, + id: props.id || makeId(), + } + + this.onFocus = this.onFocus.bind(this) + this.onBlur = this.onBlur.bind(this) + } + + onFocus(...args: any[]) { + // Doing this to allow onFocus to be called correctly from the child input element as this component overrides it + const onChildFocus = get(this.props, 'children.props.onFocus') + if (onChildFocus) { + onChildFocus(...args) + } + + this.setState({ + isFocused: true, + }) + } + + onBlur(...args: any[]) { + // Doing this to allow onBlur to be called correctly from the child input element as this component overrides it + const onChildBlur = get(this.props, 'children.props.onBlur') + if (onChildBlur) { + onChildBlur(...args) + } + + this.setState({ + isFocused: false, + }) + } + + render() { + const { + children, + helpText, + isInvalid, + error, + label, + labelType, + labelAppend, + hasEmptyLabelSpace, + fullWidth, + className, + describedByIds, + compressed, + display, + displayOnly, + hasChildLabel, + id: propsId, + ...rest + } = this.props + + const { id } = this.state + + /** + * Remove when `compressed` is deprecated + */ + let shimDisplay: keyof ClassNameMap + if (compressed && display === 'row') { + shimDisplay = 'rowCompressed' + } else { + /** + * Safe use of ! as prop default is 'row' + */ + shimDisplay = display! + } + + /** + * Remove when `displayOnly` is deprecated + */ + if (compressed && displayOnly) { + shimDisplay = 'centerCompressed' + } else if (displayOnly && display === 'row') { + shimDisplay = 'center' + } + + const classes = classNames( + 'euiFormRow', + { + 'euiFormRow--hasEmptyLabelSpace': hasEmptyLabelSpace, + 'euiFormRow--fullWidth': fullWidth, + }, + displayToClassNameMap[shimDisplay], + className + ) + + let optionalHelpText + + if (helpText) { + optionalHelpText = ( + + {helpText} + + ) + } + + let optionalErrors + + if (error && isInvalid) { + const errorTexts = Array.isArray(error) ? error : [error] + optionalErrors = errorTexts.map((error, i) => { + const key = typeof error === 'string' ? error : i + return ( + + {error} + + ) + }) + } + + let optionalLabel + const isLegend = label && labelType === 'legend' ? true : false + + if (label || labelAppend) { + optionalLabel = ( +
+ + {label} + + {labelAppend && ' '} + {labelAppend} +
+ ) + } + + const optionalProps: React.AriaAttributes = {} + /** + * Safe use of ! as default prop is [] + */ + const describingIds = [...describedByIds!] + + if (optionalHelpText) { + describingIds.push(optionalHelpText.props.id) + } + + if (optionalErrors) { + optionalErrors.forEach(error => describingIds.push(error.props.id)) + } + + if (describingIds.length > 0) { + optionalProps['aria-describedby'] = describingIds.join(' ') + } + + // @ts-ignore + const field = cloneElement(Children.only(children), { + id, + onFocus: this.onFocus, + onBlur: this.onBlur, + ...optionalProps, + }) + + const fieldWrapperClasses = classNames('euiFormRow__fieldWrapper', { + euiFormRow__fieldWrapperDisplayOnly: + /** + * Safe use of ! as default prop is 'row' + */ + displayOnly || display!.startsWith('center'), + }) + + const Element = labelType === 'legend' ? 'fieldset' : 'div' + + return ( + + {optionalLabel} +
+ {field} + {optionalErrors} + {optionalHelpText} +
+
+ ) + } +} diff --git a/src/components/form/form_row/index.d.ts b/src/components/form/form_row/index.d.ts index 963a1b10aed..b32f4fe4397 100644 --- a/src/components/form/form_row/index.d.ts +++ b/src/components/form/form_row/index.d.ts @@ -9,7 +9,7 @@ import { declare module '@elastic/eui' { /** - * @see './form_row.js' + * @see './form_row.ts' */ export type EuiFormRowCommonProps = CommonProps & { error?: ReactNode | ReactNode[]; diff --git a/src/components/form/form_row/index.js b/src/components/form/form_row/index.ts similarity index 100% rename from src/components/form/form_row/index.js rename to src/components/form/form_row/index.ts From ff148e40c23c64fb6f329bb903834d077d223f98 Mon Sep 17 00:00:00 2001 From: supergrecko Date: Sat, 21 Dec 2019 21:48:12 +0100 Subject: [PATCH 2/9] EuiFormRow: Translate Tests to TS --- .../form/form_row/{form_row.test.js => form_row.test.tsx} | 1 + src/components/form/form_row/form_row.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) rename src/components/form/form_row/{form_row.test.js => form_row.test.tsx} (99%) diff --git a/src/components/form/form_row/form_row.test.js b/src/components/form/form_row/form_row.test.tsx similarity index 99% rename from src/components/form/form_row/form_row.test.js rename to src/components/form/form_row/form_row.test.tsx index 69d7e270667..48b79af0dca 100644 --- a/src/components/form/form_row/form_row.test.js +++ b/src/components/form/form_row/form_row.test.tsx @@ -19,6 +19,7 @@ describe('EuiFormRow', () => { }); test('no children is an error', () => { + // @ts-ignore expect(() => ).toThrow(); }); diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index 5b03ca39789..5d8236e2129 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -31,7 +31,8 @@ const displayToClassNameMap: ClassNameMap = { 'euiFormRow--compressed euiFormRow--horizontal euiFormRow--hasSwitch', } -export const DISPLAYS = Object.keys(displayToClassNameMap) +// @ts-ignore +export const DISPLAYS: Array = Object.keys(displayToClassNameMap) interface EuiFormRowState { isFocused: boolean; From b33223358f739b98ccb3036239da71de92a53ae5 Mon Sep 17 00:00:00 2001 From: supergrecko Date: Sat, 21 Dec 2019 22:00:12 +0100 Subject: [PATCH 3/9] Dependencies: include typings for sinon --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 138dbe51cc8..a06e6dd1149 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@types/react-is": "^16.7.1", "@types/react-virtualized": "^9.18.7", "@types/resize-observer-browser": "^0.1.1", + "@types/sinon": "^7.5.1", "@types/tabbable": "^3.1.0", "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^1.13.0", From ad8e323cd8c0a3900347544e1dba6542bff27a4f Mon Sep 17 00:00:00 2001 From: supergrecko Date: Sat, 21 Dec 2019 22:05:41 +0100 Subject: [PATCH 4/9] Add Changelog change for EuiFormRow translation to TS --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f1773902de..2293e1315f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Converted `EuiFormRow` to Typescript ([#2712](https://github.com/elastic/eui/pull/2712)) - Added `nested` glyph to `EuiIcon` ([#2707](https://github.com/elastic/eui/pull/2707)) - Added `tableLayout` prop to `EuiTable`, `EuiBasicTable` and `EuiInMemoryTable` to provide the option of auto layout ([#2697](https://github.com/elastic/eui/pull/2697)) From 01cb2d6c2272ee693b5ceebf101035282aebdd30 Mon Sep 17 00:00:00 2001 From: supergrecko Date: Thu, 26 Dec 2019 19:42:06 +0100 Subject: [PATCH 5/9] EuiFormRow: make use of keysOf for object type --- src/components/form/form_row/form_row.tsx | 186 +++++++--------------- 1 file changed, 57 insertions(+), 129 deletions(-) diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index 5d8236e2129..d118ae835df 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -1,27 +1,16 @@ -import React, { cloneElement, Component, Children, HTMLAttributes, ReactNode } from 'react' -import PropTypes from 'prop-types' -import classNames from 'classnames' -import { CommonProps } from '../../common' - -import { get } from '../../../services/objects' -import { withRequiredProp } from '../../../utils/prop_types/with_required_prop' - -import { EuiFormHelpText } from '../form_help_text' -import { EuiFormErrorText } from '../form_error_text' -import { EuiFormLabel } from '../form_label' - -import makeId from './make_id' - -export type FormRowDisplayKeys = 'row' - | 'rowCompressed' - | 'columnCompressed' - | 'center' - | 'centerCompressed' - | 'columnCompressedSwitch'; -type ClassNameMap = { - [key in FormRowDisplayKeys]: string | null -} -const displayToClassNameMap: ClassNameMap = { +import React, {cloneElement, Component, Children, HTMLAttributes, ReactNode} from 'react'; +import classNames from 'classnames'; +import {CommonProps, keysOf} from '../../common'; + +import {get} from '../../../services/objects'; + +import {EuiFormHelpText} from '../form_help_text'; +import {EuiFormErrorText} from '../form_error_text'; +import {EuiFormLabel} from '../form_label'; + +import makeId from './make_id'; + +const displayToClassNameMap = { row: null, rowCompressed: 'euiFormRow--compressed', columnCompressed: 'euiFormRow--compressed euiFormRow--horizontal', @@ -29,99 +18,38 @@ const displayToClassNameMap: ClassNameMap = { centerCompressed: 'euiFormRow--compressed', columnCompressedSwitch: 'euiFormRow--compressed euiFormRow--horizontal euiFormRow--hasSwitch', -} +}; -// @ts-ignore -export const DISPLAYS: Array = Object.keys(displayToClassNameMap) +export const DISPLAYS = keysOf(displayToClassNameMap); + +export type EuiFormRowDisplayKeys = keyof typeof displayToClassNameMap; interface EuiFormRowState { isFocused: boolean; id: string; -} +}; export type EuiFormRowProps = CommonProps & HTMLAttributes & { - display?: FormRowDisplayKeys; + display?: EuiFormRowDisplayKeys; hasEmptyLabelSpace?: boolean; fullWidth?: boolean; describedByIds?: string[]; labelType?: 'label' | 'legend'; hasChildLabel?: boolean; children: ReactNode; - className?: string; label?: ReactNode; labelAppend?: any; id?: string; isInvalid?: boolean; error?: ReactNode | Array; helpText?: ReactNode; - // Deprecated compressed?: boolean; displayOnly?: boolean; }; export class EuiFormRow extends Component { - static propTypes = { - children: PropTypes.element.isRequired, - className: PropTypes.string, - /** - * Escape hatch to not render duplicate labels if the child also renders a label - */ - hasChildLabel: PropTypes.bool, - label: PropTypes.node, - /** - * Sets the type of html element the label should be based - * on the form row contents. For instance checkbox groups - * should use 'legend' instead of the default 'label' - */ - labelType: PropTypes.oneOf(['label', 'legend']), - /** - * Adds an extra node to the right of the form label without - * being contained inside the form label. Good for things - * like documentation links. - */ - labelAppend: withRequiredProp( - PropTypes.node, - 'label', - 'appending to the label requires that the label also exists' - ), - id: PropTypes.string, - isInvalid: PropTypes.bool, - error: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.arrayOf(PropTypes.node), - ]), - helpText: PropTypes.node, - hasEmptyLabelSpace: PropTypes.bool, - fullWidth: PropTypes.bool, - /** - * IDs of additional elements that should be part of children's `aria-describedby` - */ - describedByIds: PropTypes.array, - /** - * **DEPRECATED: use `display: rowCompressed` instead.** - * When `true`, tightens up the spacing. - */ - compressed: PropTypes.bool, - /** - * When `rowCompressed`, just tightens up the spacing; - * Set to `columnCompressed` if compressed - * and horizontal layout is needed. - * Set to `center` or `centerCompressed` to align non-input - * content better with inline rows. - * Set to `columnCompressedSwitch` if the form control being passed - * as the child is a switch. - */ - display: PropTypes.oneOf(DISPLAYS), - /** - * **DEPRECATED: use `display: center` instead.** - * Vertically centers non-input style content so it aligns - * better with input style content. - */ - displayOnly: PropTypes.bool, - } - static defaultProps = { display: 'row', hasEmptyLabelSpace: false, @@ -129,42 +57,42 @@ export class EuiFormRow extends Component { describedByIds: [], labelType: 'label', hasChildLabel: true, - } + }; constructor(props: EuiFormRowProps) { - super(props) + super(props); this.state = { isFocused: false, id: props.id || makeId(), - } + }; - this.onFocus = this.onFocus.bind(this) - this.onBlur = this.onBlur.bind(this) + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); } onFocus(...args: any[]) { // Doing this to allow onFocus to be called correctly from the child input element as this component overrides it - const onChildFocus = get(this.props, 'children.props.onFocus') + const onChildFocus = get(this.props, 'children.props.onFocus'); if (onChildFocus) { - onChildFocus(...args) + onChildFocus(...args); } this.setState({ isFocused: true, - }) + }); } onBlur(...args: any[]) { // Doing this to allow onBlur to be called correctly from the child input element as this component overrides it - const onChildBlur = get(this.props, 'children.props.onBlur') + const onChildBlur = get(this.props, 'children.props.onBlur'); if (onChildBlur) { - onChildBlur(...args) + onChildBlur(...args); } this.setState({ isFocused: false, - }) + }); } render() { @@ -186,30 +114,30 @@ export class EuiFormRow extends Component { hasChildLabel, id: propsId, ...rest - } = this.props + } = this.props; - const { id } = this.state + const {id} = this.state; /** * Remove when `compressed` is deprecated */ - let shimDisplay: keyof ClassNameMap + let shimDisplay: EuiFormRowDisplayKeys; if (compressed && display === 'row') { - shimDisplay = 'rowCompressed' + shimDisplay = 'rowCompressed'; } else { /** * Safe use of ! as prop default is 'row' */ - shimDisplay = display! + shimDisplay = display!; } /** * Remove when `displayOnly` is deprecated */ if (compressed && displayOnly) { - shimDisplay = 'centerCompressed' + shimDisplay = 'centerCompressed'; } else if (displayOnly && display === 'row') { - shimDisplay = 'center' + shimDisplay = 'center'; } const classes = classNames( @@ -220,24 +148,24 @@ export class EuiFormRow extends Component { }, displayToClassNameMap[shimDisplay], className - ) + ); - let optionalHelpText + let optionalHelpText; if (helpText) { optionalHelpText = ( {helpText} - ) + ); } - let optionalErrors + let optionalErrors; if (error && isInvalid) { - const errorTexts = Array.isArray(error) ? error : [error] + const errorTexts = Array.isArray(error) ? error : [error]; optionalErrors = errorTexts.map((error, i) => { - const key = typeof error === 'string' ? error : i + const key = typeof error === 'string' ? error : i; return ( { className="euiFormRow__text"> {error} - ) - }) + ); + }); } - let optionalLabel - const isLegend = label && labelType === 'legend' ? true : false + let optionalLabel; + const isLegend = label && labelType === 'legend' ? true : false; if (label || labelAppend) { optionalLabel = ( @@ -267,25 +195,25 @@ export class EuiFormRow extends Component { {labelAppend && ' '} {labelAppend} - ) + ); } - const optionalProps: React.AriaAttributes = {} + const optionalProps: React.AriaAttributes = {}; /** * Safe use of ! as default prop is [] */ - const describingIds = [...describedByIds!] + const describingIds = [...describedByIds!]; if (optionalHelpText) { - describingIds.push(optionalHelpText.props.id) + describingIds.push(optionalHelpText.props.id); } if (optionalErrors) { - optionalErrors.forEach(error => describingIds.push(error.props.id)) + optionalErrors.forEach(error => describingIds.push(error.props.id)); } if (describingIds.length > 0) { - optionalProps['aria-describedby'] = describingIds.join(' ') + optionalProps['aria-describedby'] = describingIds.join(' '); } // @ts-ignore @@ -294,7 +222,7 @@ export class EuiFormRow extends Component { onFocus: this.onFocus, onBlur: this.onBlur, ...optionalProps, - }) + }); const fieldWrapperClasses = classNames('euiFormRow__fieldWrapper', { euiFormRow__fieldWrapperDisplayOnly: @@ -302,9 +230,9 @@ export class EuiFormRow extends Component { * Safe use of ! as default prop is 'row' */ displayOnly || display!.startsWith('center'), - }) + }); - const Element = labelType === 'legend' ? 'fieldset' : 'div' + const Element = labelType === 'legend' ? 'fieldset' : 'div'; return ( @@ -315,6 +243,6 @@ export class EuiFormRow extends Component { {optionalHelpText} - ) + ); } } From b4312f2822f6b32e53f1b868a601560fc2cd976d Mon Sep 17 00:00:00 2001 From: supergrecko Date: Fri, 3 Jan 2020 14:48:58 +0100 Subject: [PATCH 6/9] EuiTabs: remove sinon from testing and replace with Jest --- package.json | 1 - src/components/form/form_row/form_row.test.tsx | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a06e6dd1149..138dbe51cc8 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "@types/react-is": "^16.7.1", "@types/react-virtualized": "^9.18.7", "@types/resize-observer-browser": "^0.1.1", - "@types/sinon": "^7.5.1", "@types/tabbable": "^3.1.0", "@types/uuid": "^3.4.4", "@typescript-eslint/eslint-plugin": "^1.13.0", diff --git a/src/components/form/form_row/form_row.test.tsx b/src/components/form/form_row/form_row.test.tsx index 48b79af0dca..40f5c37925f 100644 --- a/src/components/form/form_row/form_row.test.tsx +++ b/src/components/form/form_row/form_row.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { shallow, render, mount } from 'enzyme'; import { requiredProps } from '../../../test'; -import sinon from 'sinon'; import { EuiFormRow, DISPLAYS } from './form_row'; @@ -232,7 +231,7 @@ describe('EuiFormRow', () => { describe('behavior', () => { describe('onFocus', () => { test('is called in child', () => { - const focusMock = sinon.stub(); + const focusMock = jest.fn(); const component = mount( Label}> @@ -242,7 +241,7 @@ describe('EuiFormRow', () => { component.find('input').simulate('focus'); - sinon.assert.calledOnce(focusMock); + expect(focusMock).toBeCalledTimes(1); // Ensure the focus event is properly fired on the parent // which will pass down to the EuiFormLabel @@ -266,7 +265,7 @@ describe('EuiFormRow', () => { describe('onBlur', () => { test('is called in child', () => { - const blurMock = sinon.stub(); + const blurMock = jest.fn(); const component = mount( Label}> @@ -276,7 +275,7 @@ describe('EuiFormRow', () => { component.find('input').simulate('blur'); - sinon.assert.calledOnce(blurMock); + expect(blurMock).toBeCalledTimes(1); // Ensure the blur event is properly fired on the parent // which will pass down to the EuiFormLabel From a6bca2297813eb861c46eb9bc8b9a54f7f796e73 Mon Sep 17 00:00:00 2001 From: supergrecko Date: Fri, 3 Jan 2020 14:59:35 +0100 Subject: [PATCH 7/9] EuiTabs: add previously removed prop type documentation --- src/components/form/form_row/form_row.tsx | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index d118ae835df..de93e2c495b 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -32,20 +32,54 @@ interface EuiFormRowState { export type EuiFormRowProps = CommonProps & HTMLAttributes & { + /** + * When `rowCompressed`, just tightens up the spacing; + * Set to `columnCompressed` if compressed + * and horizontal layout is needed. + * Set to `center` or `centerCompressed` to align non-input + * content better with inline rows. + * Set to `columnCompressedSwitch` if the form control being passed + * as the child is a switch. + */ display?: EuiFormRowDisplayKeys; hasEmptyLabelSpace?: boolean; fullWidth?: boolean; + /** + * IDs of additional elements that should be part of children's `aria-describedby` + */ describedByIds?: string[]; + /** + * Sets the type of html element the label should be based + * on the form row contents. For instance checkbox groups + * should use 'legend' instead of the default 'label' + */ labelType?: 'label' | 'legend'; + /** + * Escape hatch to not render duplicate labels if the child also renders a label + */ hasChildLabel?: boolean; children: ReactNode; label?: ReactNode; + /** + * Adds an extra node to the right of the form label without + * being contained inside the form label. Good for things + * like documentation links. + */ labelAppend?: any; id?: string; isInvalid?: boolean; error?: ReactNode | Array; helpText?: ReactNode; + /** + * **DEPRECATED: use `display: rowCompressed` instead.** + * When `true`, tightens up the spacing. + */ compressed?: boolean; + /** + * **DEPRECATED: use `display: center` instead.** + * Vertically centers non-input style content so it aligns + * better with input style content. + */ displayOnly?: boolean; }; From d26a2661e692c19aa8da576932dcbb115553e3ff Mon Sep 17 00:00:00 2001 From: supergrecko Date: Fri, 3 Jan 2020 15:01:32 +0100 Subject: [PATCH 8/9] EuiFormRow: run eslint --- src/components/form/form_row/form_row.tsx | 133 +++++++++++----------- 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index de93e2c495b..ac732b1b590 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -1,12 +1,18 @@ -import React, {cloneElement, Component, Children, HTMLAttributes, ReactNode} from 'react'; +import React, { + cloneElement, + Component, + Children, + HTMLAttributes, + ReactNode, +} from 'react'; import classNames from 'classnames'; -import {CommonProps, keysOf} from '../../common'; +import { CommonProps, keysOf } from '../../common'; -import {get} from '../../../services/objects'; +import { get } from '../../../services/objects'; -import {EuiFormHelpText} from '../form_help_text'; -import {EuiFormErrorText} from '../form_error_text'; -import {EuiFormLabel} from '../form_label'; +import { EuiFormHelpText } from '../form_help_text'; +import { EuiFormErrorText } from '../form_error_text'; +import { EuiFormLabel } from '../form_label'; import makeId from './make_id'; @@ -27,61 +33,60 @@ export type EuiFormRowDisplayKeys = keyof typeof displayToClassNameMap; interface EuiFormRowState { isFocused: boolean; id: string; -}; +} -export type EuiFormRowProps = CommonProps - & HTMLAttributes - & { - /** - * When `rowCompressed`, just tightens up the spacing; - * Set to `columnCompressed` if compressed - * and horizontal layout is needed. - * Set to `center` or `centerCompressed` to align non-input - * content better with inline rows. - * Set to `columnCompressedSwitch` if the form control being passed - * as the child is a switch. - */ - display?: EuiFormRowDisplayKeys; - hasEmptyLabelSpace?: boolean; - fullWidth?: boolean; - /** - * IDs of additional elements that should be part of children's `aria-describedby` - */ - describedByIds?: string[]; - /** - * Sets the type of html element the label should be based - * on the form row contents. For instance checkbox groups - * should use 'legend' instead of the default 'label' - */ - labelType?: 'label' | 'legend'; - /** - * Escape hatch to not render duplicate labels if the child also renders a label - */ - hasChildLabel?: boolean; - children: ReactNode; - label?: ReactNode; - /** - * Adds an extra node to the right of the form label without - * being contained inside the form label. Good for things - * like documentation links. - */ - labelAppend?: any; - id?: string; - isInvalid?: boolean; - error?: ReactNode | Array; - helpText?: ReactNode; - /** - * **DEPRECATED: use `display: rowCompressed` instead.** - * When `true`, tightens up the spacing. - */ - compressed?: boolean; - /** - * **DEPRECATED: use `display: center` instead.** - * Vertically centers non-input style content so it aligns - * better with input style content. - */ - displayOnly?: boolean; -}; +export type EuiFormRowProps = CommonProps & + HTMLAttributes & { + /** + * When `rowCompressed`, just tightens up the spacing; + * Set to `columnCompressed` if compressed + * and horizontal layout is needed. + * Set to `center` or `centerCompressed` to align non-input + * content better with inline rows. + * Set to `columnCompressedSwitch` if the form control being passed + * as the child is a switch. + */ + display?: EuiFormRowDisplayKeys; + hasEmptyLabelSpace?: boolean; + fullWidth?: boolean; + /** + * IDs of additional elements that should be part of children's `aria-describedby` + */ + describedByIds?: string[]; + /** + * Sets the type of html element the label should be based + * on the form row contents. For instance checkbox groups + * should use 'legend' instead of the default 'label' + */ + labelType?: 'label' | 'legend'; + /** + * Escape hatch to not render duplicate labels if the child also renders a label + */ + hasChildLabel?: boolean; + children: ReactNode; + label?: ReactNode; + /** + * Adds an extra node to the right of the form label without + * being contained inside the form label. Good for things + * like documentation links. + */ + labelAppend?: any; + id?: string; + isInvalid?: boolean; + error?: ReactNode | ReactNode[]; + helpText?: ReactNode; + /** + * **DEPRECATED: use `display: rowCompressed` instead.** + * When `true`, tightens up the spacing. + */ + compressed?: boolean; + /** + * **DEPRECATED: use `display: center` instead.** + * Vertically centers non-input style content so it aligns + * better with input style content. + */ + displayOnly?: boolean; + }; export class EuiFormRow extends Component { static defaultProps = { @@ -150,7 +155,7 @@ export class EuiFormRow extends Component { ...rest } = this.props; - const {id} = this.state; + const { id } = this.state; /** * Remove when `compressed` is deprecated @@ -260,9 +265,9 @@ export class EuiFormRow extends Component { const fieldWrapperClasses = classNames('euiFormRow__fieldWrapper', { euiFormRow__fieldWrapperDisplayOnly: - /** - * Safe use of ! as default prop is 'row' - */ + /** + * Safe use of ! as default prop is 'row' + */ displayOnly || display!.startsWith('center'), }); From 9363c6b3798d469b4f5b9677b545114b0d3e235e Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Thu, 16 Jan 2020 13:07:44 -0600 Subject: [PATCH 9/9] ts clean up --- ...ow.test.js.snap => form_row.test.tsx.snap} | 1 - .../form/form_row/form_row.test.tsx | 14 -- src/components/form/form_row/form_row.tsx | 152 +++++++++++------- src/components/form/form_row/index.d.ts | 51 ------ src/components/form/form_row/index.ts | 2 +- src/components/form/index.d.ts | 1 - 6 files changed, 92 insertions(+), 129 deletions(-) rename src/components/form/form_row/__snapshots__/{form_row.test.js.snap => form_row.test.tsx.snap} (99%) delete mode 100644 src/components/form/form_row/index.d.ts diff --git a/src/components/form/form_row/__snapshots__/form_row.test.js.snap b/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap similarity index 99% rename from src/components/form/form_row/__snapshots__/form_row.test.js.snap rename to src/components/form/form_row/__snapshots__/form_row.test.tsx.snap index 02e6ad2c932..a20b07174f3 100644 --- a/src/components/form/form_row/__snapshots__/form_row.test.js.snap +++ b/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap @@ -590,7 +590,6 @@ exports[`EuiFormRow props label renders as a legend and subsquently a fieldset w > label diff --git a/src/components/form/form_row/form_row.test.tsx b/src/components/form/form_row/form_row.test.tsx index 40f5c37925f..94edb3db718 100644 --- a/src/components/form/form_row/form_row.test.tsx +++ b/src/components/form/form_row/form_row.test.tsx @@ -17,20 +17,6 @@ describe('EuiFormRow', () => { expect(component).toMatchSnapshot(); }); - test('no children is an error', () => { - // @ts-ignore - expect(() => ).toThrow(); - }); - - test('two children is an error', () => { - expect(() => ( - -
-
- - )).toThrow(); - }); - test('ties together parts for accessibility', () => { const props = { label: 'Label', diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index ac732b1b590..7e22ef4e4b5 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -3,10 +3,11 @@ import React, { Component, Children, HTMLAttributes, + ReactElement, ReactNode, } from 'react'; import classNames from 'classnames'; -import { CommonProps, keysOf } from '../../common'; +import { ExclusiveUnion, CommonProps, keysOf } from '../../common'; import { get } from '../../../services/objects'; @@ -35,58 +36,63 @@ interface EuiFormRowState { id: string; } -export type EuiFormRowProps = CommonProps & - HTMLAttributes & { - /** - * When `rowCompressed`, just tightens up the spacing; - * Set to `columnCompressed` if compressed - * and horizontal layout is needed. - * Set to `center` or `centerCompressed` to align non-input - * content better with inline rows. - * Set to `columnCompressedSwitch` if the form control being passed - * as the child is a switch. - */ - display?: EuiFormRowDisplayKeys; - hasEmptyLabelSpace?: boolean; - fullWidth?: boolean; - /** - * IDs of additional elements that should be part of children's `aria-describedby` - */ - describedByIds?: string[]; - /** - * Sets the type of html element the label should be based - * on the form row contents. For instance checkbox groups - * should use 'legend' instead of the default 'label' - */ - labelType?: 'label' | 'legend'; - /** - * Escape hatch to not render duplicate labels if the child also renders a label - */ - hasChildLabel?: boolean; - children: ReactNode; - label?: ReactNode; - /** - * Adds an extra node to the right of the form label without - * being contained inside the form label. Good for things - * like documentation links. - */ - labelAppend?: any; - id?: string; - isInvalid?: boolean; - error?: ReactNode | ReactNode[]; - helpText?: ReactNode; - /** - * **DEPRECATED: use `display: rowCompressed` instead.** - * When `true`, tightens up the spacing. - */ - compressed?: boolean; - /** - * **DEPRECATED: use `display: center` instead.** - * Vertically centers non-input style content so it aligns - * better with input style content. - */ - displayOnly?: boolean; - }; +type EuiFormRowCommonProps = CommonProps & { + /** + * When `rowCompressed`, just tightens up the spacing; + * Set to `columnCompressed` if compressed + * and horizontal layout is needed. + * Set to `center` or `centerCompressed` to align non-input + * content better with inline rows. + * Set to `columnCompressedSwitch` if the form control being passed + * as the child is a switch. + */ + display?: EuiFormRowDisplayKeys; + hasEmptyLabelSpace?: boolean; + fullWidth?: boolean; + /** + * IDs of additional elements that should be part of children's `aria-describedby` + */ + describedByIds?: string[]; + /** + * Escape hatch to not render duplicate labels if the child also renders a label + */ + hasChildLabel?: boolean; + children: ReactElement; + label?: ReactNode; + /** + * Adds an extra node to the right of the form label without + * being contained inside the form label. Good for things + * like documentation links. + */ + labelAppend?: any; + id?: string; + isInvalid?: boolean; + error?: ReactNode | ReactNode[]; + helpText?: ReactNode; + /** + * **DEPRECATED: use `display: rowCompressed` instead.** + * When `true`, tightens up the spacing. + */ + compressed?: boolean; + /** + * **DEPRECATED: use `display: center` instead.** + * Vertically centers non-input style content so it aligns + * better with input style content. + */ + displayOnly?: boolean; +}; + +type LabelProps = { + labelType?: 'label'; +} & EuiFormRowCommonProps & + HTMLAttributes; + +type LegendProps = { + labelType?: 'legend'; +} & EuiFormRowCommonProps & + HTMLAttributes; + +export type EuiFormRowProps = ExclusiveUnion; export class EuiFormRow extends Component { static defaultProps = { @@ -220,15 +226,25 @@ export class EuiFormRow extends Component { const isLegend = label && labelType === 'legend' ? true : false; if (label || labelAppend) { + let labelProps = {}; + if (isLegend) { + labelProps = { + type: labelType, + }; + } else { + labelProps = { + htmlFor: hasChildLabel ? id : undefined, + isFocused: this.state.isFocused, + type: labelType, + }; + } optionalLabel = (
+ {...labelProps}> {label} {labelAppend && ' '} @@ -255,7 +271,6 @@ export class EuiFormRow extends Component { optionalProps['aria-describedby'] = describingIds.join(' '); } - // @ts-ignore const field = cloneElement(Children.only(children), { id, onFocus: this.onFocus, @@ -271,17 +286,32 @@ export class EuiFormRow extends Component { displayOnly || display!.startsWith('center'), }); - const Element = labelType === 'legend' ? 'fieldset' : 'div'; + const sharedProps = { + className: classes, + id: `${id}-row`, + }; - return ( - + const contents = ( + {optionalLabel}
{field} {optionalErrors} {optionalHelpText}
-
+ + ); + + return labelType === 'legend' ? ( +
}> + {contents} +
+ ) : ( +
}> + {contents} +
); } } diff --git a/src/components/form/form_row/index.d.ts b/src/components/form/form_row/index.d.ts deleted file mode 100644 index b32f4fe4397..00000000000 --- a/src/components/form/form_row/index.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { CommonProps, ExclusiveUnion } from '../../common'; - -import { - FunctionComponent, - ReactNode, - ReactElement, - HTMLAttributes, -} from 'react'; - -declare module '@elastic/eui' { - /** - * @see './form_row.ts' - */ - export type EuiFormRowCommonProps = CommonProps & { - error?: ReactNode | ReactNode[]; - fullWidth?: boolean; - hasEmptyLabelSpace?: boolean; - helpText?: ReactNode; - isInvalid?: boolean; - label?: ReactNode; - labelAppend?: ReactNode; - describedByIds?: string[]; - display?: - | 'row' - | 'rowCompressed' - | 'columnCompressed' - | 'center' - | 'centerCompressed' - | 'columnCompressedSwitch'; - // **DEPRECATED: use `display: rowCompressed` instead.** - compressed?: boolean; - // **DEPRECATED: use `display: center` instead.** - displayOnly?: boolean; - }; - - type LabelProps = { - labelType?: 'label'; - } & EuiFormRowCommonProps & - HTMLAttributes; - - type LegendProps = { - labelType?: 'legend'; - } & EuiFormRowCommonProps & - HTMLAttributes; - - export type EuiFormRowProps = ExclusiveUnion & { - children: ReactElement; - }; - - export const EuiFormRow: FunctionComponent; -} diff --git a/src/components/form/form_row/index.ts b/src/components/form/form_row/index.ts index 91534008135..fb59d7d1315 100644 --- a/src/components/form/form_row/index.ts +++ b/src/components/form/form_row/index.ts @@ -1 +1 @@ -export { EuiFormRow } from './form_row'; +export { EuiFormRow, EuiFormRowProps } from './form_row'; diff --git a/src/components/form/index.d.ts b/src/components/form/index.d.ts index c61ad673ada..bcd8f9e672e 100644 --- a/src/components/form/index.d.ts +++ b/src/components/form/index.d.ts @@ -1,6 +1,5 @@ import { CommonProps } from '../common'; /// -/// /// /// ///