diff --git a/CHANGELOG.md b/CHANGELOG.md index a804c3c1d24..30d46e0ce4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Converted `EuiFacetButton` to TypeScript ([#2226](https://github.com/elastic/eui/pull/2226)) - Adds an optional `onClear` prop to the the `EuiDatePicker` component ([#2235](https://github.com/elastic/eui/pull/2235)) +- Added support for `onClick` and `href` props on `EuiListGroupItem` and converted to TypeScript ([#1933](https://github.com/elastic/eui/pull/1933)) **Bug fixes** diff --git a/src/components/list_group/__snapshots__/list_group.test.js.snap b/src/components/list_group/__snapshots__/list_group.test.tsx.snap similarity index 100% rename from src/components/list_group/__snapshots__/list_group.test.js.snap rename to src/components/list_group/__snapshots__/list_group.test.tsx.snap diff --git a/src/components/list_group/__snapshots__/list_group_item.test.js.snap b/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap similarity index 98% rename from src/components/list_group/__snapshots__/list_group_item.test.js.snap rename to src/components/list_group/__snapshots__/list_group_item.test.tsx.snap index 26fc39e957d..3881ae91b0d 100644 --- a/src/components/list_group/__snapshots__/list_group_item.test.js.snap +++ b/src/components/list_group/__snapshots__/list_group_item.test.tsx.snap @@ -49,6 +49,22 @@ exports[`EuiListGroupItem props extraAction is rendered 1`] = ` `; +exports[`EuiListGroupItem props href and onClick is rendered 1`] = ` +
  • + + + +
  • +`; + exports[`EuiListGroupItem props href is rendered 1`] = `
  • `; - -exports[`EuiListGroupItem throws an warning if both onClick and href are provided but still renders 1`] = ` -
  • - - - -
  • -`; diff --git a/src/components/list_group/index.d.ts b/src/components/list_group/index.d.ts deleted file mode 100644 index fc5e3b751ee..00000000000 --- a/src/components/list_group/index.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - EuiButtonIconProps, - EuiButtonPropsForButtonOrLink, -} from '@elastic/eui'; // eslint-disable-line import/no-unresolved -import { IconType } from '../icon'; -import { CommonProps, ExclusiveUnion } from '../common'; -import { - AnchorHTMLAttributes, - ButtonHTMLAttributes, - FunctionComponent, - HTMLAttributes, - MouseEventHandler, - ReactElement, - ReactNode, -} from 'react'; - -declare module '@elastic/eui' { - /** - * list group type defs - * - * @see './list_group.js' - */ - - type EuiListGroupProps = CommonProps & - HTMLAttributes & { - bordered?: boolean; - flush?: boolean; - listItems?: Array>; - maxWidth?: boolean | number | string; - showToolTips?: boolean; - wrapText?: boolean; - }; - - export const EuiListGroup: FunctionComponent; - - /** - * list group item type defs - * - * @see './list_group_item.js' - */ - - interface EuiListGroupItemPropsBasics { - size?: 'xs' | 's' | 'm' | 'l'; - label: ReactNode; - isActive?: boolean; - isDisabled?: boolean; - href?: string; - iconType?: IconType; - icon?: ReactElement; - showToolTip?: boolean; - extraAction?: EuiButtonPropsForButtonOrLink< - CommonProps & - EuiButtonIconProps & { - iconType: IconType; - alwaysShow?: boolean; - } - >; - onClick?: MouseEventHandler; - wrapText?: boolean; - } - - type EuiListGroupItemProps = EuiListGroupItemPropsBasics & - CommonProps & - ExclusiveUnion< - ExclusiveUnion< - ButtonHTMLAttributes, - AnchorHTMLAttributes - >, - HTMLAttributes - >; - - export const EuiListGroupItem: FunctionComponent; -} diff --git a/src/components/list_group/index.js b/src/components/list_group/index.ts similarity index 99% rename from src/components/list_group/index.js rename to src/components/list_group/index.ts index 1fb18ca4a48..69dca4816b4 100644 --- a/src/components/list_group/index.js +++ b/src/components/list_group/index.ts @@ -1,3 +1,2 @@ export { EuiListGroup } from './list_group'; - export { EuiListGroupItem } from './list_group_item'; diff --git a/src/components/list_group/list_group.js b/src/components/list_group/list_group.js deleted file mode 100644 index 19c4e0ad51e..00000000000 --- a/src/components/list_group/list_group.js +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { EuiListGroupItem } from './list_group_item'; - -export const EuiListGroup = ({ - children, - className, - flush, - bordered, - wrapText, - listItems, - maxWidth, - style, - showToolTips, - ...rest -}) => { - let newStyle; - let widthClassName; - if (maxWidth !== true) { - const value = typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth; - newStyle = { ...style, maxWidth: value }; - } else if (maxWidth === true) { - widthClassName = 'euiListGroup-maxWidthDefault'; - } - - const classes = classNames( - 'euiListGroup', - { - 'euiListGroup-flush': flush, - 'euiListGroup-bordered': bordered, - }, - widthClassName, - className - ); - - let childrenOrListItems = null; - if (listItems) { - childrenOrListItems = listItems.map((item, index) => { - return [ - , - ]; - }); - } else { - if (showToolTips) { - childrenOrListItems = React.Children.map(children, child => { - return React.cloneElement(child, { - showToolTip: true, - }); - }); - } else { - childrenOrListItems = children; - } - } - - return ( -
      - {childrenOrListItems} -
    - ); -}; - -EuiListGroup.propTypes = { - listItems: PropTypes.arrayOf(PropTypes.shape(EuiListGroupItem.propTypes)), - children: PropTypes.node, - className: PropTypes.string, - - /** - * Remove container padding, stretching list items to the edges - */ - flush: PropTypes.bool, - - /** - * Add a border to the list container - */ - bordered: PropTypes.bool, - - /** - * Allow link text to wrap - */ - wrapText: PropTypes.bool, - - /** - * Display tooltips on all list items - */ - showToolTips: PropTypes.bool, - - /** - * Sets the max-width of the page, - * set to `true` to use the default size, - * set to `false` to not restrict the width, - * set to a number for a custom width in px, - * set to a string for a custom width in custom measurement. - */ - maxWidth: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.number, - PropTypes.string, - ]), -}; - -EuiListGroup.defaultProps = { - flush: false, - bordered: false, - wrapText: false, - maxWidth: true, - showToolTips: false, -}; diff --git a/src/components/list_group/list_group.test.js b/src/components/list_group/list_group.test.tsx similarity index 100% rename from src/components/list_group/list_group.test.js rename to src/components/list_group/list_group.test.tsx diff --git a/src/components/list_group/list_group.tsx b/src/components/list_group/list_group.tsx new file mode 100644 index 00000000000..c87b74bf513 --- /dev/null +++ b/src/components/list_group/list_group.tsx @@ -0,0 +1,112 @@ +import React, { FunctionComponent, HTMLAttributes, CSSProperties } from 'react'; +import classNames from 'classnames'; + +import { EuiListGroupItem, EuiListGroupItemProps } from './list_group_item'; +import { CommonProps } from '../common'; + +type EuiListGroupProps = CommonProps & + HTMLAttributes & { + /** + * Add a border to the list container + */ + bordered?: boolean; + + /** + * Remove container padding, stretching list items to the edges + */ + flush?: boolean; + + /** + * Items to display in this group + */ + listItems?: EuiListGroupItemProps[]; + + /** + * Sets the max-width of the page, + * set to `true` to use the default size, + * set to `false` to not restrict the width, + * set to a number for a custom width in px, + * set to a string for a custom width in custom measurement. + */ + maxWidth?: boolean | number | string; + + /** + * Display tooltips on all list items + */ + showToolTips?: boolean; + + /** + * Allow link text to wrap + */ + wrapText?: boolean; + }; + +export const EuiListGroup: FunctionComponent = ({ + children, + className, + listItems, + style, + flush = false, + bordered = false, + wrapText = false, + maxWidth = true, + showToolTips = false, + ...rest +}) => { + let newStyle: CSSProperties | undefined; + let widthClassName; + if (maxWidth !== true) { + let value: CSSProperties['maxWidth']; + if (typeof maxWidth === 'number') { + value = `${maxWidth}px`; + } else { + value = typeof maxWidth === 'string' ? maxWidth : undefined; + } + + newStyle = { ...style, maxWidth: value }; + } else if (maxWidth === true) { + widthClassName = 'euiListGroup-maxWidthDefault'; + } + + const classes = classNames( + 'euiListGroup', + { + 'euiListGroup-flush': flush, + 'euiListGroup-bordered': bordered, + }, + widthClassName, + className + ); + + let childrenOrListItems = null; + if (listItems) { + childrenOrListItems = listItems.map((item, index) => { + return [ + , + ]; + }); + } else { + if (showToolTips) { + childrenOrListItems = React.Children.map(children, child => { + if (React.isValidElement(child)) { + return React.cloneElement>(child, { + showToolTip: true, + }); + } + }); + } else { + childrenOrListItems = children; + } + } + + return ( +
      + {childrenOrListItems} +
    + ); +}; diff --git a/src/components/list_group/list_group_item.test.js b/src/components/list_group/list_group_item.test.tsx similarity index 91% rename from src/components/list_group/list_group_item.test.js rename to src/components/list_group/list_group_item.test.tsx index 181a578196e..ef4e9264c32 100644 --- a/src/components/list_group/list_group_item.test.js +++ b/src/components/list_group/list_group_item.test.tsx @@ -112,6 +112,16 @@ describe('EuiListGroupItem', () => { expect(component).toMatchSnapshot(); }); }); + + describe('href and onClick', () => { + test('is rendered', () => { + const component = render( + {}} href="#" /> + ); + + expect(component).toMatchSnapshot(); + }); + }); }); test('renders a disabled button even if provided an href', () => { @@ -132,7 +142,7 @@ describe('EuiListGroupItem', () => { describe('throws an warning', () => { const oldConsoleError = console.warn; - let consoleStub; + let consoleStub: jest.Mock; beforeEach(() => { // We don't use jest.spyOn() here, because EUI's tests apply a global @@ -145,18 +155,6 @@ describe('EuiListGroupItem', () => { console.warn = oldConsoleError; }); - test('if both onClick and href are provided but still renders', () => { - const component = render( - {}} href="#" /> - ); - - expect(consoleStub).toBeCalled(); - expect(consoleStub.mock.calls[0][0]).toMatch( - '`href` and `onClick` were passed' - ); - expect(component).toMatchSnapshot(); - }); - test('if both iconType and icon are provided but still renders', () => { const component = render( } /> diff --git a/src/components/list_group/list_group_item.js b/src/components/list_group/list_group_item.tsx similarity index 52% rename from src/components/list_group/list_group_item.js rename to src/components/list_group/list_group_item.tsx index c24f1e74217..9600cfeea64 100644 --- a/src/components/list_group/list_group_item.js +++ b/src/components/list_group/list_group_item.tsx @@ -1,33 +1,126 @@ -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; +import React, { + Fragment, + HTMLAttributes, + AnchorHTMLAttributes, + ButtonHTMLAttributes, + ReactNode, + ReactElement, + MouseEventHandler, + FunctionComponent, +} from 'react'; import classNames from 'classnames'; +import { + EuiButtonIconProps, + EuiButtonPropsForButtonOrLink, + EuiButtonIcon as EuiButtonIconType, +} from '@elastic/eui'; // eslint-disable-line import/no-unresolved +// @ts-ignore import { EuiButtonIcon } from '../button'; -import { IconPropType, EuiIcon } from '../icon'; +const EuiButtonIconTyped: typeof EuiButtonIconType = EuiButtonIcon; + +import { EuiIcon, IconType } from '../icon'; import { EuiToolTip } from '../tool_tip'; import { useInnerText } from '../inner_text'; +import { ExclusiveUnion, CommonProps } from '../common'; + +type ItemSize = 'xs' | 's' | 'm' | 'l'; -const sizeToClassNameMap = { +const sizeToClassNameMap: { [size in ItemSize]: string } = { xs: 'euiListGroupItem--xSmall', s: 'euiListGroupItem--small', m: 'euiListGroupItem--medium', l: 'euiListGroupItem--large', }; -export const SIZES = Object.keys(sizeToClassNameMap); - -export const EuiListGroupItem = ({ +export const SIZES = Object.keys(sizeToClassNameMap) as ItemSize[]; + +export type EuiListGroupItemProps = CommonProps & + ExclusiveUnion< + ExclusiveUnion< + ButtonHTMLAttributes, + AnchorHTMLAttributes + >, + HTMLAttributes + > & { + /** + * Size of the label text + */ + size?: ItemSize; + + /** + * Content to be displayed in the list item + */ + label: ReactNode; + + /** + * Apply styles indicating an item is active + */ + isActive?: boolean; + + /** + * Apply styles indicating an item is disabled + */ + isDisabled?: boolean; + + /** + * Make the list item label a link. + * While permitted, `href` and `onClick` should not be used together in most cases and may create problems. + */ + href?: string; + + /** + * Adds `EuiIcon` of `EuiIcon.type` + */ + iconType?: IconType; + + /** + * Custom node to pass as the icon. Cannot be used in conjunction + * with `iconType`. + */ + icon?: ReactElement; + + /** + * Display tooltip on list item + */ + showToolTip?: boolean; + + /** + * Adds an `EuiButtonIcon` to the right side of the item; `iconType` is required; + * pass `alwaysShow` if you don't want the default behavior of only showing on hover + */ + extraAction?: EuiButtonPropsForButtonOrLink< + CommonProps & + EuiButtonIconProps & { + iconType: IconType; + alwaysShow?: boolean; + } + >; + + /** + * Make the list item label a button. + * While permitted, `href` and `onClick` should not be used together in most cases and may create problems. + */ + onClick?: MouseEventHandler; + + /** + * Allow link text to wrap + */ + wrapText?: boolean; + }; + +export const EuiListGroupItem: FunctionComponent = ({ label, - isActive, - isDisabled, + isActive = false, + isDisabled = false, href, className, iconType, icon, extraAction, onClick, - size, - showToolTip, + size = 'm', + showToolTip = false, wrapText, ...rest }) => { @@ -70,7 +163,7 @@ export const EuiListGroupItem = ({ }); extraActionNode = ( - + ['onClick']} + className="euiListGroupItem__button" + {...rest as AnchorHTMLAttributes}> {iconNode} {labelContent} ); - - if (onClick) { - console.warn( - 'Both `href` and `onClick` were passed to EuiListGroupItem but only one can exist. The `href` was used.' - ); - } } else if ((href && isDisabled) || onClick) { itemContent = ( @@ -138,8 +229,7 @@ export const EuiListGroupItem = ({ anchorClassName="euiListGroupItem__tooltip" content={label} position="right" - delay="long" - size="s"> + delay="long"> {itemContent} @@ -155,71 +245,3 @@ export const EuiListGroupItem = ({ return {itemContent}; }; - -EuiListGroupItem.propTypes = { - className: PropTypes.string, - - /** - * Set the size of the label text - */ - size: PropTypes.oneOf(SIZES), - - /** - * Content to be displyed in the list item - */ - label: PropTypes.node.isRequired, - - /** - * Apply styles indicating an item is active - */ - isActive: PropTypes.bool, - - /** - * Apply styles indicating an item is disabled - */ - isDisabled: PropTypes.bool, - - /** - * Make the list item label a link - */ - href: PropTypes.string, - - /** - * Adds `EuiIcon` of `EuiIcon.type` - */ - iconType: IconPropType, - - /** - * Custom node to pass as the icon. Cannot be used in conjunction - * with `iconType`. - */ - icon: PropTypes.element, - - /** - * Display tooltip on list item - */ - showToolTip: PropTypes.bool, - - /** - * Adds an `EuiButtonIcon` to the right side of the item; `iconType` is required; - * pass `alwaysShow` if you don't want the default behavior of only showing on hover - */ - extraAction: PropTypes.shape({ - iconType: IconPropType.isRequired, - alwaysShow: PropTypes.bool, - }), - - onClick: PropTypes.func, - - /** - * Allow link text to wrap - */ - wrapText: PropTypes.bool, -}; - -EuiListGroupItem.defaultProps = { - isActive: false, - isDisabled: false, - size: 'm', - showToolTip: false, -};