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

Convert SideNav to Typescript #2818

Merged
merged 9 commits into from
Feb 6, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Changed SASS comments to non-compiled comments in invisibles files ([#2807](https://github.com/elastic/eui/pull/2807))
- Added `rowHeader` prop to `EuiBasicTable` to allow consumers to set the identifying cell in a row ([#2802](https://github.com/elastic/eui/pull/2802))
- Improved `EuiDescribedFormGroup` accessibility by avoiding duplicated output in screen readers ([#2783](https://github.com/elastic/eui/pull/2783))
- Converted `EuiSideNav` to TypeScript ([#2818](https://github.com/elastic/eui/issues/2818))

**Bug fixes**

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"lint-sass-fix": "sass-lint-auto-fix -c ./.sass-lint-fix.yml",
"test": "yarn lint && yarn test-unit",
"test-unit": "cross-env NODE_ENV=test jest --config ./scripts/jest/config.json",
"test-staged": "yarn lint && node scripts/test-staged.js",
"test-staged": "yarn lint",
chandlerprall marked this conversation as resolved.
Show resolved Hide resolved
"start-test-server": "webpack-dev-server --config src-docs/webpack.config.js --port 9999",
"test-visual": "wdio test/wdio.conf.js",
"yo-component": "yo ./generator-eui/app/component.js",
Expand Down
5 changes: 5 additions & 0 deletions src-docs/src/views/side_nav/props.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React, { FunctionComponent } from 'react';
import { EuiSideNavItemType } from '../../../../src/components/side_nav/side_nav_types';
// import { EuiDataGridColumn } from '../../../../src/components/datagrid/data_grid_types';

export const SideNavItem: FunctionComponent<EuiSideNavItemType> = () => <div />;
4 changes: 3 additions & 1 deletion src-docs/src/views/side_nav/side_nav_example.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -48,7 +50,7 @@ export const SideNavExample = {
</p>
</div>
),
props: { EuiSideNav },
props: { EuiSideNav, EuiSideNavItem: SideNavItem },
demo: <SideNav />,
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { EuiSideNav } from './side_nav';

export * from './side_nav_types';
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { render } from 'enzyme';
import { requiredProps } from '../../test/required_props';

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

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

const renderItem = ({ href, className, children }) => (
const renderItem: EuiSideNavProps['renderItem'] = ({
href,
className,
children,
}) => (
<a data-test-id="my-custom-element" href={href} className={className}>
{children}
</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { Component, ReactNode, MouseEventHandler } from 'react';
import classNames from 'classnames';

import { CommonProps } from '../common';

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

import { EuiSideNavItem } from './side_nav_item';
import { EuiSideNavItem, EuiSideNavItemProps } 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 #EuiSideNavItemType objects. Lists navigation menu items.
Copy link
Contributor

Choose a reason for hiding this comment

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

EuiSideNavItem instead of EuiSideNavItemType (matching the parameter name from https://github.com/elastic/eui/pull/2818/files#diff-e5a3e909f17d33572c3c3cc48dbb6ae3R53)

Copy link
Contributor Author

@rlesniak rlesniak Feb 4, 2020

Choose a reason for hiding this comment

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

There will be a name conflict between component <-> type there :/

Copy link
Contributor

Choose a reason for hiding this comment

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

This change would only be in this comment; #EuiSideNavItem needs to match the rendered heading on the Props tab,

EuiSideNavItem heading

*/
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> {
static defaultProps = {
items: [],
};

export class EuiSideNav extends Component {
isItemOpen = item => {
isItemOpen = (item: EuiSideNavItemType) => {
// The developer can force the item to be open.
if (item.forceOpen) {
return true;
Expand All @@ -22,9 +59,11 @@ export class EuiSideNav extends Component {
if (item.items) {
return item.items.some(this.isItemOpen);
}

return false;
};

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

return items.map(item => {
Expand Down Expand Up @@ -111,55 +150,3 @@ export class EuiSideNav extends Component {
);
}
}

EuiSideNav.propTypes = {
/**
* `children` are not rendered. Use `items` to specify navigation items instead.
*/
children: PropTypes.node,
/**
* Class names to be merged into the final `className` property.
*/
className: PropTypes.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: PropTypes.func,
/**
* If `true`, the navigation menu will be open at mobile device widths. Use in conjunction with the `toggleOpenOnMobile` prop.
*/
isOpenOnMobile: PropTypes.bool,
/**
* A React node to render at mobile responsive widths, representing the title of this navigation menu.
*/
mobileTitle: PropTypes.node,
/**
* `items` is an array of objects (navigation menu `item`s).
* Each `item` may contain the following properties (this is an incomplete list):
* `item.id` is a required value that is passed to React as the `key` for this item
* `item.forceOpen` is an optional boolean; if set to true it will force the item to display in an "open" state at all times.
* `item.href` 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 `<a>`.
* `item.icon` is an optional React node which will be rendered as a small icon to the left of the navigation item text.
* `item.isSelected` is an optional boolean; 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.
* `item.items` is an optional array containing additional item objects, representing nested children of this navigation item.
* `item.name` is a required React node representing the text to render for this item (usually a string will suffice).
* `item.onClick` is an optional callback function to be passed as the navigation item's `onClick` prop, and by default it will force rendering of the item as a `<button>` instead of a link.
* `item.renderItem` is an optional function overriding default rendering for this navigation item — when called, it should return a React node representing a replacement navigation item.
*/
items: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([
PropTypes.string.isRequired,
PropTypes.number.isRequired,
]).isRequired,
}).isRequired
),
/**
* Overrides default navigation menu item rendering. When called, it should return a React node representing a replacement navigation item.
*/
renderItem: PropTypes.func,
};

EuiSideNav.defaultProps = {
items: [],
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
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<HTMLButtonElement | HTMLElement>;
children: ReactNode;
};

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

const DefaultRenderItem = ({
href,
onClick,
className,
children,
...rest
}: ItemProps) => {
if (href) {
return (
<a className={className} href={href} onClick={onClick} {...rest}>
Expand Down Expand Up @@ -37,10 +65,10 @@ export const EuiSideNavItem = ({
href,
items,
children,
depth,
renderItem = defaultRenderItem,
depth = 0,
renderItem: RenderItem = DefaultRenderItem,
...rest
}) => {
}: EuiSideNavItemProps) => {
let childItems;

if (items && isOpen) {
Expand Down Expand Up @@ -87,27 +115,15 @@ export const EuiSideNavItem = ({

return (
<div className={classes}>
{renderItem({
href,
onClick,
className: buttonClasses,
children: buttonContent,
...rest,
})}
<RenderItem
href={href}
onClick={onClick}
className={buttonClasses}
{...rest}>
{buttonContent}
</RenderItem>

{childItems}
</div>
);
};

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,
};
42 changes: 42 additions & 0 deletions src/components/side_nav/side_nav_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ReactElement, ReactNode, MouseEventHandler } from 'react';

// import { EuiSideNavItemProps } 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 `<a>`.
*/
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?: EuiSideNavItemType[];
/**
* 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 `<button>` instead of a link.
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
/**
* Function overriding default rendering for this navigation item — when called, it should return a React node representing a replacement navigation item.
*/
renderItem?: any;
}