diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e575739b3..7207fef1efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Added optional `key` attribute to `EuiContextMenu` items and relaxed `name` attribute to allow any React node ([#2817](https://github.com/elastic/eui/pull/2817)) - Converted `EuiColorPicker` color conversion functions to `chroma-js` methods ([#2805](https://github.com/elastic/eui/pull/2805)) - Added `direction` parameter to `euiPaletteColorBlind()` for specifiying lighter or darker (or both) alternates ([#2822](https://github.com/elastic/eui/pull/2822)) +- Converted `EuiSideNav` to TypeScript ([#2818](https://github.com/elastic/eui/issues/2818)) **Bug fixes** diff --git a/src-docs/src/views/side_nav/props.tsx b/src-docs/src/views/side_nav/props.tsx new file mode 100644 index 00000000000..050c4fab8a7 --- /dev/null +++ b/src-docs/src/views/side_nav/props.tsx @@ -0,0 +1,6 @@ +import React, { FunctionComponent } from 'react'; +import { EuiSideNavItemType } from '../../../../src/components/side_nav/side_nav_types'; + +export const SideNavItem: FunctionComponent> = () => ( +
+); diff --git a/src-docs/src/views/side_nav/side_nav_example.js b/src-docs/src/views/side_nav/side_nav_example.js index 3c677bebefd..3665c350ce6 100644 --- a/src-docs/src/views/side_nav/side_nav_example.js +++ b/src-docs/src/views/side_nav/side_nav_example.js @@ -18,6 +18,8 @@ import SideNavForceOpen from './side_nav_force_open'; const sideNavForceOpenSource = require('!!raw-loader!./side_nav_force_open'); const sideNavForceOpenHtml = renderToHtml(SideNavForceOpen); +import { SideNavItem } from './props'; + export const SideNavExample = { title: 'Side Nav', sections: [ @@ -48,7 +50,7 @@ export const SideNavExample = {

), - props: { EuiSideNav }, + props: { EuiSideNav, EuiSideNavItem: SideNavItem }, demo: , }, { diff --git a/src/components/side_nav/__snapshots__/side_nav.test.js.snap b/src/components/side_nav/__snapshots__/side_nav.test.tsx.snap similarity index 100% rename from src/components/side_nav/__snapshots__/side_nav.test.js.snap rename to src/components/side_nav/__snapshots__/side_nav.test.tsx.snap diff --git a/src/components/side_nav/__snapshots__/side_nav_item.test.js.snap b/src/components/side_nav/__snapshots__/side_nav_item.test.tsx.snap similarity index 88% rename from src/components/side_nav/__snapshots__/side_nav_item.test.js.snap rename to src/components/side_nav/__snapshots__/side_nav_item.test.tsx.snap index fbd52c9f2f5..6c9c73dc1b4 100644 --- a/src/components/side_nav/__snapshots__/side_nav_item.test.js.snap +++ b/src/components/side_nav/__snapshots__/side_nav_item.test.tsx.snap @@ -2,7 +2,7 @@ exports[`EuiSideNavItem is rendered 1`] = `
{ - // The developer can force the item to be open. - if (item.forceOpen) { - return true; - } - - // Of course a selected item is open. - if (item.isSelected) { - return true; - } - - // The item has to be open if it has a child that's open. - if (item.items) { - return item.items.some(this.isItemOpen); - } - }; - - renderTree = (items, depth = 0) => { - const { renderItem } = this.props; - - return items.map(item => { - const { - id, - name, - isSelected, - items: childItems, - icon, - onClick, - href, - forceOpen, - ...rest - } = item; - - // Root items are always open. - const isOpen = depth === 0 ? true : this.isItemOpen(item); - - let renderedItems; - - if (childItems) { - renderedItems = this.renderTree(childItems, depth + 1); - } - - return ( - - {name} - - ); - }); - }; - - render() { - const { - className, - items, - toggleOpenOnMobile, - isOpenOnMobile, - mobileTitle, - // Extract this one out so it isn't passed to + ); + } +} diff --git a/src/components/side_nav/side_nav_item.test.js b/src/components/side_nav/side_nav_item.test.tsx similarity index 100% rename from src/components/side_nav/side_nav_item.test.js rename to src/components/side_nav/side_nav_item.test.tsx diff --git a/src/components/side_nav/side_nav_item.js b/src/components/side_nav/side_nav_item.tsx similarity index 52% rename from src/components/side_nav/side_nav_item.js rename to src/components/side_nav/side_nav_item.tsx index e006c774643..e2e47a80ff0 100644 --- a/src/components/side_nav/side_nav_item.js +++ b/src/components/side_nav/side_nav_item.tsx @@ -1,10 +1,60 @@ -import React, { cloneElement } from 'react'; -import PropTypes from 'prop-types'; +import React, { + cloneElement, + ReactNode, + ReactElement, + MouseEventHandler, +} from 'react'; import classNames from 'classnames'; +import { CommonProps } from '../common'; + import { EuiIcon } from '../icon'; -const defaultRenderItem = ({ href, onClick, className, children, ...rest }) => { +type ItemProps = CommonProps & { + href?: string; + onClick?: MouseEventHandler; + children: ReactNode; +}; + +interface SideNavItemProps { + isOpen?: boolean; + isSelected?: boolean; + isParent?: boolean; + icon?: ReactElement; + items?: ReactNode; + depth?: number; +} + +type ExcludeEuiSideNavItemProps = Pick< + T, + Exclude +>; +type OmitEuiSideNavItemProps = { + [K in keyof ExcludeEuiSideNavItemProps]: T[K] +}; + +interface GuaranteedRenderItemProps { + href?: string; + onClick?: ItemProps['onClick']; + className: string; + children: ReactNode; +} +export type RenderItem = ( + // argument is the set of extra component props + GuaranteedRenderItemProps + props: OmitEuiSideNavItemProps & GuaranteedRenderItemProps +) => JSX.Element; + +export type EuiSideNavItemProps = T extends { renderItem: Function } + ? T & { renderItem: RenderItem } + : T; + +const DefaultRenderItem = ({ + href, + onClick, + className, + children, + ...rest +}: ItemProps) => { if (href) { return ( @@ -28,7 +78,10 @@ const defaultRenderItem = ({ href, onClick, className, children, ...rest }) => { ); }; -export const EuiSideNavItem = ({ +export function EuiSideNavItem< + T extends ItemProps & + SideNavItemProps & { renderItem?: (props: any) => JSX.Element } +>({ isOpen, isSelected, isParent, @@ -37,10 +90,10 @@ export const EuiSideNavItem = ({ href, items, children, - depth, - renderItem = defaultRenderItem, + renderItem: RenderItem = DefaultRenderItem, + depth = 0, ...rest -}) => { +}: EuiSideNavItemProps) { let childItems; if (items && isOpen) { @@ -85,29 +138,16 @@ export const EuiSideNavItem = ({ ); + const renderItemProps: GuaranteedRenderItemProps = { + href, + onClick, + className: buttonClasses, + children: buttonContent, + }; return (
- {renderItem({ - href, - onClick, - className: buttonClasses, - children: buttonContent, - ...rest, - })} + {childItems}
); -}; - -EuiSideNavItem.propTypes = { - isOpen: PropTypes.bool, - isSelected: PropTypes.bool, - isParent: PropTypes.bool, - icon: PropTypes.node, - onClick: PropTypes.func, - href: PropTypes.string, - items: PropTypes.node, - children: PropTypes.node, - depth: PropTypes.number, - renderItem: PropTypes.func, -}; +} diff --git a/src/components/side_nav/side_nav_types.ts b/src/components/side_nav/side_nav_types.ts new file mode 100644 index 00000000000..a06de80710c --- /dev/null +++ b/src/components/side_nav/side_nav_types.ts @@ -0,0 +1,42 @@ +import { ReactElement, ReactNode, MouseEventHandler } from 'react'; + +import { RenderItem } from './side_nav_item'; + +export interface EuiSideNavItemType { + /** + * A value that is passed to React as the `key` for this item + */ + id: string | number; + /** + * If set to true it will force the item to display in an "open" state at all times. + */ + forceOpen?: boolean; + /** + * Is an optional string to be passed as the navigation item's `href` prop, and by default it will force rendering of the item as an `
`. + */ + href?: string; + /** + * React node which will be rendered as a small icon to the left of the navigation item text. + */ + icon?: ReactElement; + /** + * If set to true it will render the item in a visible "selected" state, and will force all ancestor navigation items to render in an "open" state. + */ + isSelected?: boolean; + /** + * Array containing additional item objects, representing nested children of this navigation item. + */ + items?: Array>; + /** + * React node representing the text to render for this item (usually a string will suffice). + */ + name: ReactNode; + /** + * Callback function to be passed as the navigation item's `onClick` prop, and by default it will force rendering of the item as a `