diff --git a/CHANGELOG.md b/CHANGELOG.md index 50aa6b9407c..383ee93e15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added a `column` direction option to `EuiFlexGrid` ([#2073](https://github.com/elastic/eui/pull/2073)) - Updated `EuiSuperDatePicker`'s commonly used date/times to display as columns. ([#2073](https://github.com/elastic/eui/pull/2073)) +- Added TypeScript definition for `EuiFormControlLayout` ([#2086](https://github.com/elastic/eui/pull/2086)) **Bug fixes** diff --git a/src/components/date_picker/__snapshots__/date_picker.test.js.snap b/src/components/date_picker/__snapshots__/date_picker.test.js.snap index cdef94b8f47..defdf6f9126 100644 --- a/src/components/date_picker/__snapshots__/date_picker.test.js.snap +++ b/src/components/date_picker/__snapshots__/date_picker.test.js.snap @@ -6,7 +6,6 @@ exports[`EuiDatePicker is rendered 1`] = ` className="euiDatePicker euiDatePicker--shadow" > - {prependNodes} -
- {clonedChildren || children} - - -
- {appendNodes} - - ); - } - - renderPrepends() { - const { prepend } = this.props; - - if (!prepend) { - return; - } - - let prependNodes; - - if (Array.isArray(prepend)) { - prependNodes = prepend.map((item, index) => { - return this.createSideNode(item, 'prepend', index); - }); - } else { - prependNodes = this.createSideNode(prepend, 'prepend'); - } - - return prependNodes; - } - - renderAppends() { - const { append } = this.props; - - if (!append) { - return; - } - - let appendNodes; - - if (Array.isArray(append)) { - appendNodes = append.map((item, index) => { - return this.createSideNode(item, 'append', index); - }); - } else { - appendNodes = this.createSideNode(append, 'append'); - } - - return appendNodes; - } - - createSideNode(node, side, key) { - return cloneElement(node, { - className: `euiFormControlLayout__${side}`, - key: key, - }); - } -} - -EuiFormControlLayout.propTypes = { - children: PropTypes.node, - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - type: PropTypes.string, - side: PropTypes.oneOf(ICON_SIDES), - onClick: PropTypes.func, - }), - ]), - clear: PropTypes.shape({ - onClick: PropTypes.func, - }), - fullWidth: PropTypes.bool, - isLoading: PropTypes.bool, - className: PropTypes.string, - compressed: PropTypes.bool, - /** - * Creates an input group with element(s) coming before children - */ - prepend: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.arrayOf(PropTypes.node), - ]), - /** - * Creates an input group with element(s) coming after children - */ - append: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.arrayOf(PropTypes.node), - ]), -}; - -EuiFormControlLayout.defaultProps = { - isLoading: false, - compressed: false, -}; diff --git a/src/components/form/form_control_layout/form_control_layout.test.js b/src/components/form/form_control_layout/form_control_layout.test.tsx similarity index 95% rename from src/components/form/form_control_layout/form_control_layout.test.js rename to src/components/form/form_control_layout/form_control_layout.test.tsx index a6eed4ee343..130cb2ff9a1 100644 --- a/src/components/form/form_control_layout/form_control_layout.test.js +++ b/src/components/form/form_control_layout/form_control_layout.test.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { render, mount } from 'enzyme'; -import sinon from 'sinon'; import { findTestSubject, requiredProps } from '../../../test'; @@ -63,7 +62,7 @@ describe('EuiFormControlLayout', () => { test('is called when clicked', () => { const icon = { type: 'alert', - onClick: sinon.spy(), + onClick: jest.fn(), 'data-test-subj': 'myIcon', }; @@ -71,7 +70,7 @@ describe('EuiFormControlLayout', () => { const closeButton = findTestSubject(component, 'myIcon'); closeButton.simulate('click'); - expect(icon.onClick.called).toBe(true); + expect(icon.onClick).toBeCalled(); }); }); }); @@ -92,7 +91,7 @@ describe('EuiFormControlLayout', () => { test('is called when clicked', () => { const clear = { - onClick: sinon.spy(), + onClick: jest.fn(), 'data-test-subj': 'clearButton', }; @@ -100,7 +99,7 @@ describe('EuiFormControlLayout', () => { const closeButton = findTestSubject(component, 'clearButton'); closeButton.simulate('click'); - expect(clear.onClick.called).toBe(true); + expect(clear.onClick).toBeCalled(); }); }); }); diff --git a/src/components/form/form_control_layout/form_control_layout.tsx b/src/components/form/form_control_layout/form_control_layout.tsx new file mode 100644 index 00000000000..328f099693e --- /dev/null +++ b/src/components/form/form_control_layout/form_control_layout.tsx @@ -0,0 +1,161 @@ +import React, { + cloneElement, + Component, + HTMLAttributes, + ReactElement, + ReactNode, +} from 'react'; +import classNames from 'classnames'; + +import { + EuiFormControlLayoutIcons, + EuiFormControlLayoutIconsProps, +} from './form_control_layout_icons'; +import { CommonProps } from '../../common'; + +export { ICON_SIDES } from './form_control_layout_icons'; + +type ReactElements = ReactElement | ReactElement[]; + +// if `prepend` and/or `append` is specified then `children` must be undefined or a single ReactElement +interface AppendWithChildren { + append: ReactElements; + children?: ReactElement; +} +interface PrependWithChildren { + prepend: ReactElements; + children?: ReactElement; +} +type SiblingsWithChildren = AppendWithChildren | PrependWithChildren; + +type ChildrenOptions = + | SiblingsWithChildren + | { + append?: undefined | null; + prepend?: undefined | null; + children?: ReactNode; + }; + +type EuiFormControlLayoutProps = CommonProps & + HTMLAttributes & + ChildrenOptions & { + /** + * Creates an input group with element(s) coming before children + */ + prepend?: ReactElements; + /** + * Creates an input group with element(s) coming after children + */ + append?: ReactElements; + icon?: EuiFormControlLayoutIconsProps['icon']; + clear?: EuiFormControlLayoutIconsProps['clear']; + fullWidth?: boolean; + isLoading?: boolean; + className?: string; + compressed?: boolean; + readOnly?: boolean; + }; + +function isChildrenIsReactElement( + append: EuiFormControlLayoutProps['append'], + prepend: EuiFormControlLayoutProps['prepend'], + children: EuiFormControlLayoutProps['children'] +): children is ReactElement { + return (!!append || !!prepend) && children != null; +} + +export class EuiFormControlLayout extends Component { + render() { + const { + children, + icon, + clear, + fullWidth, + isLoading, + compressed, + className, + prepend, + append, + readOnly, + ...rest + } = this.props; + + const classes = classNames( + 'euiFormControlLayout', + { + 'euiFormControlLayout--fullWidth': fullWidth, + 'euiFormControlLayout--compressed': compressed, + 'euiFormControlLayout--readOnly': readOnly, + 'euiFormControlLayout--group': prepend || append, + }, + className + ); + + const prependNodes = this.renderPrepends(); + const appendNodes = this.renderAppends(); + + let clonedChildren; + if (isChildrenIsReactElement(append, prepend, children)) { + clonedChildren = cloneElement(children, { + className: `${ + children.props.className + } euiFormControlLayout__child--noStyle`, + }); + } + + return ( +
+ {prependNodes} +
+ {clonedChildren || children} + + +
+ {appendNodes} +
+ ); + } + + renderPrepends() { + const { prepend } = this.props; + + if (!prepend) { + return; + } + + const prependNodes = React.Children.map(prepend, (item, index) => + this.createSideNode(item, 'prepend', index) + ); + + return prependNodes; + } + + renderAppends() { + const { append } = this.props; + + if (!append) { + return; + } + + const appendNodes = React.Children.map(append, (item, index) => + this.createSideNode(item, 'append', index) + ); + + return appendNodes; + } + + createSideNode( + node: ReactElement, + side: 'append' | 'prepend', + key: React.Key + ) { + return cloneElement(node, { + className: `euiFormControlLayout__${side}`, + key: key, + }); + } +} diff --git a/src/components/form/form_control_layout/form_control_layout_clear_button.tsx b/src/components/form/form_control_layout/form_control_layout_clear_button.tsx index f9a712a90ce..f5a5318e723 100644 --- a/src/components/form/form_control_layout/form_control_layout_clear_button.tsx +++ b/src/components/form/form_control_layout/form_control_layout_clear_button.tsx @@ -5,8 +5,11 @@ import { CommonProps } from '../../common'; import { EuiIcon } from '../../icon'; import { EuiI18n } from '../../i18n'; +export type EuiFormControlLayoutClearButtonProps = CommonProps & + ButtonHTMLAttributes; + export const EuiFormControlLayoutClearButton: FunctionComponent< - CommonProps & ButtonHTMLAttributes + EuiFormControlLayoutClearButtonProps > = ({ className, onClick, ...rest }) => { const classes = classNames('euiFormControlLayoutClearButton', className); diff --git a/src/components/form/form_control_layout/form_control_layout_custom_icon.tsx b/src/components/form/form_control_layout/form_control_layout_custom_icon.tsx index 5d34bb1da43..279ba83034a 100644 --- a/src/components/form/form_control_layout/form_control_layout_custom_icon.tsx +++ b/src/components/form/form_control_layout/form_control_layout_custom_icon.tsx @@ -6,20 +6,21 @@ import React, { import classNames from 'classnames'; import { EuiIcon, IconType } from '../../icon'; -import { CommonProps, ExclusiveUnion } from '../../common'; +import { CommonProps, ExclusiveUnion, Omit } from '../../common'; -export interface EuiFormControlLayoutCustomIconProps { - type: IconType; - iconRef?: string | ((el: HTMLButtonElement | HTMLSpanElement | null) => void); -} +export type EuiFormControlLayoutCustomIconProps = CommonProps & + ExclusiveUnion< + Omit, 'type'>, + HTMLAttributes + > & { + type: IconType; + iconRef?: + | string + | ((el: HTMLButtonElement | HTMLSpanElement | null) => void); + }; export const EuiFormControlLayoutCustomIcon: FunctionComponent< - CommonProps & - ExclusiveUnion< - ButtonHTMLAttributes, - HTMLAttributes - > & - EuiFormControlLayoutCustomIconProps + EuiFormControlLayoutCustomIconProps > = ({ className, onClick, type, iconRef, ...rest }) => { const classes = classNames('euiFormControlLayoutCustomIcon', className, { 'euiFormControlLayoutCustomIcon--clickable': onClick, diff --git a/src/components/form/form_control_layout/form_control_layout_icons.js b/src/components/form/form_control_layout/form_control_layout_icons.tsx similarity index 58% rename from src/components/form/form_control_layout/form_control_layout_icons.js rename to src/components/form/form_control_layout/form_control_layout_icons.tsx index c3e5aa85318..407e4e4c4d9 100644 --- a/src/components/form/form_control_layout/form_control_layout_icons.js +++ b/src/components/form/form_control_layout/form_control_layout_icons.tsx @@ -1,17 +1,46 @@ import React, { Fragment, Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiLoadingSpinner } from '../../loading'; -import { EuiFormControlLayoutClearButton } from './form_control_layout_clear_button'; -import { EuiFormControlLayoutCustomIcon } from './form_control_layout_custom_icon'; +import { + EuiFormControlLayoutClearButton, + EuiFormControlLayoutClearButtonProps, +} from './form_control_layout_clear_button'; +import { + EuiFormControlLayoutCustomIcon, + EuiFormControlLayoutCustomIconProps, +} from './form_control_layout_custom_icon'; +import { IconType } from '../../icon'; +import { Omit } from '../../common'; + +export const ICON_SIDES: ['left', 'right'] = ['left', 'right']; + +type IconShape = Partial< + Omit +> & { + type: IconType; + side?: typeof ICON_SIDES[number]; + ref?: EuiFormControlLayoutCustomIconProps['iconRef']; +}; + +function isIconShape( + icon: EuiFormControlLayoutIconsProps['icon'] +): icon is IconShape { + return !!icon && icon.hasOwnProperty('type'); +} -export const ICON_SIDES = ['left', 'right']; +export interface EuiFormControlLayoutIconsProps { + icon?: IconType | IconShape; + clear?: EuiFormControlLayoutClearButtonProps; + isLoading?: boolean; +} -export class EuiFormControlLayoutIcons extends Component { +export class EuiFormControlLayoutIcons extends Component< + EuiFormControlLayoutIconsProps +> { render() { const { icon } = this.props; - const iconSide = icon && icon.side ? icon.side : 'left'; + const iconSide = isIconShape(icon) && icon.side ? icon.side : 'left'; const customIcon = this.renderCustomIcon(); const loadingSpinner = this.renderLoadingSpinner(); const clearButton = this.renderClearButton(); @@ -51,12 +80,11 @@ export class EuiFormControlLayoutIcons extends Component { } // Normalize the icon to an object if it's a string. - const iconProps = - typeof icon === 'string' - ? { - type: icon, - } - : icon; + const iconProps: IconShape = isIconShape(icon) + ? icon + : { + type: icon, + }; const { ref: iconRef, side, ...iconRest } = iconProps; @@ -83,18 +111,3 @@ export class EuiFormControlLayoutIcons extends Component { return ; } } - -EuiFormControlLayoutIcons.propTypes = { - icon: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - type: PropTypes.string, - side: PropTypes.oneOf(ICON_SIDES), - onClick: PropTypes.func, - }), - ]), - clear: PropTypes.shape({ - onClick: PropTypes.func, - }), - isLoading: PropTypes.bool, -}; diff --git a/src/components/form/form_control_layout/index.d.ts b/src/components/form/form_control_layout/index.d.ts deleted file mode 100644 index 2396d672345..00000000000 --- a/src/components/form/form_control_layout/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EuiFormControlLayoutClearButton as FormControlLayoutClearButton } from './form_control_layout_clear_button'; -import { EuiFormControlLayoutCustomIcon as FormControlLayoutCustomIcon } from './form_control_layout_custom_icon'; - -declare module '@elastic/eui' { - /** - * @see './form_control_layout_clear_button.js' - */ - export const EuiFormControlLayoutClearButton: typeof FormControlLayoutClearButton; - - /** - * @see './form_control_layout_custom_icon.js' - */ - export const EuiFormControlLayoutCustomIcon: typeof FormControlLayoutCustomIcon; -} diff --git a/src/components/form/form_control_layout/index.js b/src/components/form/form_control_layout/index.js deleted file mode 100644 index 8657421f437..00000000000 --- a/src/components/form/form_control_layout/index.js +++ /dev/null @@ -1 +0,0 @@ -export { EuiFormControlLayout, ICON_SIDES } from './form_control_layout'; diff --git a/src/components/form/form_control_layout/index.ts b/src/components/form/form_control_layout/index.ts new file mode 100644 index 00000000000..79e0774a1f0 --- /dev/null +++ b/src/components/form/form_control_layout/index.ts @@ -0,0 +1,7 @@ +export { EuiFormControlLayout, ICON_SIDES } from './form_control_layout'; +export { + EuiFormControlLayoutClearButton, +} from './form_control_layout_clear_button'; +export { + EuiFormControlLayoutCustomIcon, +} from './form_control_layout_custom_icon'; diff --git a/src/components/form/index.d.ts b/src/components/form/index.d.ts index f2c781079f8..366419d36f9 100644 --- a/src/components/form/index.d.ts +++ b/src/components/form/index.d.ts @@ -4,8 +4,6 @@ import { CommonProps } from '../common'; /// /// /// -/// -/// /// /// ///