Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proper typing for EuiSubNav's renderItem #1

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src-docs/src/views/side_nav/props.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { FunctionComponent } from 'react';
import { EuiSideNavItemType } from '../../../../src/components/side_nav/side_nav_types';

export const SideNavItem: FunctionComponent<EuiSideNavItemType> = () => <div />;
export const SideNavItem: FunctionComponent<EuiSideNavItemType<any>> = () => (
<div />
);
9 changes: 3 additions & 6 deletions src/components/side_nav/side_nav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React from 'react';
import { render } from 'enzyme';
import { requiredProps } from '../../test/required_props';

import { EuiSideNav, EuiSideNavProps } from './side_nav';
import { EuiSideNav } from './side_nav';
import { RenderItem } from './side_nav_item';

describe('EuiSideNav', () => {
test('is rendered', () => {
Expand Down Expand Up @@ -109,11 +110,7 @@ describe('EuiSideNav', () => {
},
];

const renderItem: EuiSideNavProps['renderItem'] = ({
href,
className,
children,
}) => (
const renderItem: RenderItem<{}> = ({ href, className, children }) => (
<a data-test-id="my-custom-element" href={href} className={className}>
{children}
</a>
Expand Down
71 changes: 36 additions & 35 deletions src/components/side_nav/side_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,47 @@ import { CommonProps } from '../common';

import { EuiIcon } from '../icon';

import { EuiSideNavItem, EuiSideNavItemProps } from './side_nav_item';
import { EuiSideNavItem, RenderItem } from './side_nav_item';
import { EuiSideNavItemType } from './side_nav_types';

export type EuiSideNavProps = CommonProps & {
/**
* `children` are not rendered. Use `items` to specify navigation items instead.
*/
children?: never;
/**
* Class names to be merged into the final `className` property.
*/
className?: string;
/**
* When called, toggles visibility of the navigation menu at mobile responsive widths. The callback should set the `isOpenOnMobile` prop to actually toggle navigation visibility.
*/
toggleOpenOnMobile?: MouseEventHandler<HTMLButtonElement>;
/**
* If `true`, the navigation menu will be open at mobile device widths. Use in conjunction with the `toggleOpenOnMobile` prop.
*/
isOpenOnMobile?: boolean;
/**
* A React node to render at mobile responsive widths, representing the title of this navigation menu.
*/
mobileTitle?: ReactNode;
/**
* An array of #EuiSideNavItem objects. Lists navigation menu items.
*/
items: EuiSideNavItemType[];
/**
* Overrides default navigation menu item rendering. When called, it should return a React node representing a replacement navigation item.
*/
renderItem?: EuiSideNavItemProps['renderItem'];
};

export class EuiSideNav extends Component<EuiSideNavProps> {
export type EuiSideNavProps<T> = T &
CommonProps & {
/**
* `children` are not rendered. Use `items` to specify navigation items instead.
*/
children?: never;
/**
* Class names to be merged into the final `className` property.
*/
className?: string;
/**
* When called, toggles visibility of the navigation menu at mobile responsive widths. The callback should set the `isOpenOnMobile` prop to actually toggle navigation visibility.
*/
toggleOpenOnMobile?: MouseEventHandler<HTMLButtonElement>;
/**
* If `true`, the navigation menu will be open at mobile device widths. Use in conjunction with the `toggleOpenOnMobile` prop.
*/
isOpenOnMobile?: boolean;
/**
* A React node to render at mobile responsive widths, representing the title of this navigation menu.
*/
mobileTitle?: ReactNode;
/**
* An array of #EuiSideNavItem objects. Lists navigation menu items.
*/
items: Array<EuiSideNavItemType<T>>;
/**
* Overrides default navigation menu item rendering. When called, it should return a React node representing a replacement navigation item.
*/
renderItem?: RenderItem<T>;
};

export class EuiSideNav<T> extends Component<EuiSideNavProps<T>> {
static defaultProps = {
items: [],
};

isItemOpen = (item: EuiSideNavItemType) => {
isItemOpen = (item: EuiSideNavItemType<T>) => {
// The developer can force the item to be open.
if (item.forceOpen) {
return true;
Expand All @@ -63,7 +64,7 @@ export class EuiSideNav extends Component<EuiSideNavProps> {
return false;
};

renderTree = (items: EuiSideNavItemType[], depth = 0) => {
renderTree = (items: Array<EuiSideNavItemType<T>>, depth = 0) => {
const { renderItem } = this.props;

return items.map(item => {
Expand Down
52 changes: 38 additions & 14 deletions src/components/side_nav/side_nav_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,38 @@ type ItemProps = CommonProps & {
children: ReactNode;
};

export type EuiSideNavItemProps = ItemProps & {
interface SideNavItemProps {
isOpen?: boolean;
isSelected?: boolean;
isParent?: boolean;
icon?: ReactElement;
items?: ReactNode;
depth?: number;
renderItem?: (props: ItemProps) => JSX.Element;
}

type ExcludeEuiSideNavItemProps<T> = Pick<
T,
Exclude<keyof T, keyof SideNavItemProps | 'renderItem'>
>;
type OmitEuiSideNavItemProps<T> = {
[K in keyof ExcludeEuiSideNavItemProps<T>]: T[K]
};

interface GuaranteedRenderItemProps {
href?: string;
onClick?: ItemProps['onClick'];
className: string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
className: string;
className?: string;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

className always exists when renderItem is called (it is built up as buttonClasses in the method)

children: ReactNode;
}
export type RenderItem<T> = (
// argument is the set of extra component props + GuaranteedRenderItemProps
props: OmitEuiSideNavItemProps<T> & GuaranteedRenderItemProps
) => JSX.Element;

export type EuiSideNavItemProps<T> = T extends { renderItem: Function }
? T & { renderItem: RenderItem<T> }
: T;

const DefaultRenderItem = ({
href,
onClick,
Expand Down Expand Up @@ -56,7 +78,10 @@ const DefaultRenderItem = ({
);
};

export const EuiSideNavItem = ({
export function EuiSideNavItem<
T extends ItemProps &
SideNavItemProps & { renderItem?: (props: any) => JSX.Element }
>({
isOpen,
isSelected,
isParent,
Expand All @@ -65,10 +90,10 @@ export const EuiSideNavItem = ({
href,
items,
children,
depth = 0,
renderItem: RenderItem = DefaultRenderItem,
depth = 0,
...rest
}: EuiSideNavItemProps) => {
}: EuiSideNavItemProps<T>) {
let childItems;

if (items && isOpen) {
Expand Down Expand Up @@ -113,17 +138,16 @@ export const EuiSideNavItem = ({
</span>
);

const renderItemProps: GuaranteedRenderItemProps = {
href,
onClick,
className: buttonClasses,
children: buttonContent,
};
return (
<div className={classes}>
<RenderItem
href={href}
onClick={onClick}
className={buttonClasses}
{...rest}>
{buttonContent}
</RenderItem>

<RenderItem {...renderItemProps} {...rest} />
{childItems}
</div>
);
};
}
8 changes: 4 additions & 4 deletions src/components/side_nav/side_nav_types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ReactElement, ReactNode, MouseEventHandler } from 'react';

import { EuiSideNavItemProps } from './side_nav_item';
import { RenderItem } from './side_nav_item';

export interface EuiSideNavItemType {
export interface EuiSideNavItemType<T> {
/**
* A value that is passed to React as the `key` for this item
*/
Expand All @@ -26,7 +26,7 @@ export interface EuiSideNavItemType {
/**
* Array containing additional item objects, representing nested children of this navigation item.
*/
items?: EuiSideNavItemType[];
items?: Array<EuiSideNavItemType<T>>;
/**
* React node representing the text to render for this item (usually a string will suffice).
*/
Expand All @@ -38,5 +38,5 @@ export interface EuiSideNavItemType {
/**
* Function overriding default rendering for this navigation item — when called, it should return a React node representing a replacement navigation item.
*/
renderItem?: EuiSideNavItemProps['renderItem'];
renderItem?: RenderItem<T>;
}