Skip to content

Commit

Permalink
feat(pf4-nav): add nav component (patternfly#626)
Browse files Browse the repository at this point in the history
feat(pf4-nav): adds the nav component

affects: @patternfly/react-core, @patternfly/react-docs

ISSUES CLOSED: patternfly#547
  • Loading branch information
jschuler committed Oct 2, 2018
1 parent 7dbc7bb commit 677587c
Show file tree
Hide file tree
Showing 42 changed files with 3,024 additions and 50 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"cz-lerna-changelog": "^1.2.1",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-context-patch": "^0.0.9",
"enzyme-to-json": "^3.3.3",
"fs-extra": "^6.0.1",
"glob": "^7.1.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Avatar, AvatarProps } from './Avatar';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Brand, BrandProps } from './Brand';
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default as Checkbox } from './Checkbox';
export { default as Checkbox, CheckboxProps } from './Checkbox';
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export {
default as Dropdown,
DropdownPosition,
DropdownDirection
DropdownDirection,
DropdownProps
} from './Dropdown';
export { default as DropdownItem } from './Item';
export { default as DropdownSeparator } from './Separator';
export { default as KebabToggle } from './KebabToggle';
export { default as DropdownToggle } from './DropdownToggle';
export { default as DropdownItem, ItemProps } from './Item';
export { default as DropdownSeparator, SeparatorProps } from './Separator';
export { default as KebabToggle, KebabProps } from './KebabToggle';
export { default as DropdownToggle, DropdownToggleProps } from './DropdownToggle';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as List, ListProps } from './List';
export { default as ListItem, ListItemProps } from './ListItem';
14 changes: 14 additions & 0 deletions packages/patternfly-4/react-core/src/components/Nav/Nav.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SFC, HTMLProps, ReactNode, FormEvent } from 'react';
import { Omit } from '../../typeUtils';

export interface NavProps extends Omit<HTMLProps<HTMLDivElement>, 'onSelect'> {
children?: ReactNode;
className?: string;
onSelect(groupId: number, itemId: number, event: FormEvent<HTMLInputElement>): void;
onToggle(groupId: number, expanded: boolean, event: FormEvent<HTMLInputElement>): void;
'aria-label': string;
}

declare const Nav: SFC<NavProps>;

export default Nav;
33 changes: 33 additions & 0 deletions packages/patternfly-4/react-core/src/components/Nav/Nav.docs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Nav, NavGroup, NavList, NavItem, NavExpandable, NavVariants } from '@patternfly/react-core';
import SimpleList from './examples/NavSimpleList';
import GroupedList from './examples/NavGroupedList';
import ExpandableList from './examples/NavExpandableList';
import ExpandableTitlesList from './examples/NavExpandableTitlesList';
import MixedList from './examples/NavMixedList';
import DefaultList from './examples/NavDefaultList';
import HorizontalList from './examples/NavHorizontalList';
import TertiaryList from './examples/NavTertiaryList';

export default {
title: 'Nav',
components: {
Nav,
NavGroup,
NavList,
NavExpandable,
NavItem
},
enumValues: {
'Object.values(NavVariants)': Object.values(NavVariants)
},
examples: [
SimpleList,
GroupedList,
DefaultList,
ExpandableList,
ExpandableTitlesList,
MixedList,
HorizontalList,
TertiaryList
]
};
70 changes: 70 additions & 0 deletions packages/patternfly-4/react-core/src/components/Nav/Nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import styles from '@patternfly/patternfly-next/components/Nav/styles.css';
import { css } from '@patternfly/react-styles';
import PropTypes from 'prop-types';

const propTypes = {
/** Anything that can be rendered inside of the nav */
children: PropTypes.node,
/** Additional classes added to the container */
className: PropTypes.string,
/** Callback for updating when item selection changes */
onSelect: PropTypes.func,
/** Callback for when a list is expanded or collapsed */
onToggle: PropTypes.func,
/** Accessibility label */
'aria-label': PropTypes.string.isRequired
};

const defaultProps = {
children: null,
className: '',
onSelect: () => undefined,
onToggle: () => undefined
};

export const NavContext = React.createContext();

class Nav extends React.Component {
// Callback from NavItem
onSelect(event, groupId, itemId) {
event.stopPropagation();
this.props.onSelect({
event,
itemId,
groupId
});
}

// Callback from NavExpandable
onToggle(event, groupId, isExpanded) {
event.stopPropagation();
this.props.onToggle({
event,
groupId,
isExpanded
});
}

render() {
const { children, className, ...props } = this.props;

return (
<NavContext.Provider
value={{
onSelect: (event, groupId, itemId) => this.onSelect(event, groupId, itemId),
onToggle: (event, groupId, expanded) => this.onToggle(event, groupId, expanded)
}}
>
<nav className={css(styles.nav, className)} {...props}>
{children}
</nav>
</NavContext.Provider>
);
}
}

Nav.propTypes = propTypes;
Nav.defaultProps = defaultProps;

export default Nav;
211 changes: 211 additions & 0 deletions packages/patternfly-4/react-core/src/components/Nav/Nav.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import React from 'react';
import { mount } from 'enzyme';
import Nav from './Nav';
import NavList from './NavList';
import NavGroup from './NavGroup';
import NavItem from './NavItem';
import NavExpandable from './NavExpandable';

const props = {
items: [
{ to: '#link1', label: 'Link 1' },
{ to: '#link2', label: 'Link 2' },
{ to: '#link3', label: 'Link 3' },
{ to: '#link4', label: 'Link 4' }
]
};

beforeEach(() => {
window.location.hash = '#link1';
});

const context = {
onSelect: () => undefined,
onToggle: () => undefined
};

test('Default Nav List', () => {
const view = mount(
<Nav aria-label="Test">
<NavList>
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavList>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});

test('Default Nav List - Trigger item active update', () => {
window.location.hash = '#link2';
const view = mount(
<Nav aria-label="Test">
<NavList>
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavList>
</Nav>,
{ context }
);
view
.find({ href: '#link2' })
.first()
.simulate('click');
expect(view).toMatchSnapshot();
});

test('Simple Nav List', () => {
const view = mount(
<Nav aria-label="Test">
<NavList variant="simple">
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavList>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});

test('Expandable Nav List', () => {
const view = mount(
<Nav aria-label="Test">
<NavList>
<NavExpandable id="grp-1" title="Section 1">
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavExpandable>
</NavList>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});

test('Expandable Nav List - Trigger toggle', () => {
window.location.hash = '#link2';
const view = mount(
<Nav aria-label="Test">
<NavList>
<NavExpandable id="grp-1" title="Section 1" className="expandable-group" isExpanded>
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavExpandable>
</NavList>
</Nav>,
{ context }
);
view
.find('li.expandable-group')
.first()
.simulate('click');
expect(view).toMatchSnapshot();
});

test('Expandable Nav List with aria label', () => {
const view = mount(
<Nav aria-label="Test">
<NavList>
<NavExpandable id="grp-1" title="Section 1" srText="Section 1 - Example sub-navigation">
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavExpandable>
</NavList>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});

test('Nav Grouped List', () => {
const view = mount(
<Nav aria-label="Test">
<NavGroup id="grp-1" title="Section 1">
<NavList>
{props.items.map(item => (
<NavItem to={item.to} key={`section1_${item.to}`}>
{item.label}
</NavItem>
))}
</NavList>
</NavGroup>
<NavGroup id="grp-2" title="Section 2">
<NavList>
{props.items.map(item => (
<NavItem to={item.to} key={`section2_${item.to}`}>
{item.label}
</NavItem>
))}
</NavList>
</NavGroup>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});

test('Horizontal Nav List', () => {
const view = mount(
<Nav aria-label="Test">
<NavList variant="horizontal">
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavList>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});

test('Tertiary Nav List', () => {
const view = mount(
<Nav aria-label="Test">
<NavList variant="tertiary">
{props.items.map(item => (
<NavItem to={item.to} key={item.to}>
{item.label}
</NavItem>
))}
</NavList>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});

test('Nav List with custom item nodes', () => {
const CustomNode = () => <div>My custom node</div>;
const view = mount(
<Nav aria-label="Test">
<NavList variant="tertiary">
<NavItem to="/components/nav#link1">
<CustomNode />
</NavItem>
</NavList>
</Nav>,
{ context }
);
expect(view).toMatchSnapshot();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SFC, HTMLProps, ReactNode } from 'react';

export interface NavExpandableProps extends HTMLProps<HTMLDivElement> {
title: string;
srText?: string;
isExpanded?: boolean;
children?: ReactNode;
className?: string;
groupId?: string | number;
isActive?: boolean;
id?: string;
}

declare const NavExpandable: SFC<NavExpandableProps>;

export default NavExpandable;

0 comments on commit 677587c

Please sign in to comment.