From b82bbb04e015f0821cc1b34f7f91e822143d35f9 Mon Sep 17 00:00:00 2001 From: Jeffrey Johnson Date: Mon, 4 Feb 2019 20:53:08 -0800 Subject: [PATCH 01/17] Save state --- .../SideNavigation.Component.js | 454 ++++++++++++------ src/SideNavigation/SideNavigation.js | 268 ++++------- src/SideNavigation/SideNavigation.test.js | 379 ++++++++++----- src/index.js | 2 +- 4 files changed, 671 insertions(+), 432 deletions(-) diff --git a/src/SideNavigation/SideNavigation.Component.js b/src/SideNavigation/SideNavigation.Component.js index bc816c6dc..840ea0024 100644 --- a/src/SideNavigation/SideNavigation.Component.js +++ b/src/SideNavigation/SideNavigation.Component.js @@ -1,92 +1,109 @@ import React from 'react'; +import { SideNav } from '../'; import { Description, DocsText, DocsTile, Header, Import, Properties, Separator } from '../_playground'; -import { SideNav, SideNavGroup, SideNavList } from '../'; +import { Link, MemoryRouter } from 'react-router-dom'; export const SideNavigationComponent = () => { const sideNavOneLevelCode = ` - - -`; + + Link Item + + + Link Item + + + Link Item + + + Link Item + + + Link Item + + `; const sideNavWithTitlesCode = ` - - - - - + + + + + + + + + `; + + const sideNavMultiLevelCode = ` - -`; - - const sideNavMultiLevelCode = ` - - -`; + `; const sideNavWithIconsCode = ` - -`; + + `; const sideNavCollapsedCode = ` - - -`; + + + `; return (
@@ -106,14 +123,28 @@ export const SideNavigationComponent = () => {

Side Navigation with one level

- + + + + + + + {sideNavOneLevelCode} @@ -123,28 +154,78 @@ export const SideNavigationComponent = () => {

Side navigation with titles

Use this to group navigation. Titles are not clickable. - - - - - - - - + + + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + + {sideNavWithTitlesCode} @@ -157,36 +238,70 @@ export const SideNavigationComponent = () => { - + + + + + + + + + + + + + + + Item 1 + + + Item 2 + + + Item 3 + + + Item 4 + + + + + {sideNavMultiLevelCode} @@ -196,14 +311,39 @@ export const SideNavigationComponent = () => {

Side navigation with icons

- + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + {sideNavWithIconsCode} @@ -218,14 +358,28 @@ export const SideNavigationComponent = () => { - + + + + + + + {sideNavCollapsedCode} diff --git a/src/SideNavigation/SideNavigation.js b/src/SideNavigation/SideNavigation.js index 403e1dd5b..72103e02a 100644 --- a/src/SideNavigation/SideNavigation.js +++ b/src/SideNavigation/SideNavigation.js @@ -1,90 +1,38 @@ import classnames from 'classnames'; -import PropTypes from 'prop-types'; -import { BrowserRouter, Link } from 'react-router-dom'; +// import PropTypes from 'prop-types'; import React, { Component } from 'react'; -export const SideNav = props => { - const { icons, children, ...rest } = props; - - const sideNavClasses = classnames( - 'fd-side-nav', - { - 'fd-side-nav--icons': icons - } - ); - - return ( - - ); -}; -SideNav.propTypes = { - children: PropTypes.node, - icons: PropTypes.bool -}; - -SideNav.propDescriptions = { - icons: 'Set to **true** enables side navigation collapsed with icons.' -}; - -export class SideNavList extends Component { +export class SideNav extends Component { constructor(props) { super(props); - - let initialState = []; - - props.items.forEach(item => { - if (item.hasChild) { - let id = item.id; - let obj = {}; - - obj[id] = false; - initialState.push(obj); - } - }); - - this.state = { - selectedItem: 'item_2', - itemStates: initialState - }; } - handleSelectChild = (e, id) => { - this.setState({ - selectedItem: id - }); - }; - - handleSelect = (e, id) => { - let iStates = Object.assign({}, this.state.itemStates); - iStates[id] = !iStates[id]; - this.setState({ itemStates: iStates }); - this.setState({ selectedItem: id }); - }; + render() { + const { children, className, icons, ...rest } = this.props; - getLinkClasses = ({id, hasChild}) => { - return classnames( - 'fd-side-nav__link', + const sideNavClasses = classnames( + className, + 'fd-side-nav', { - 'is-selected': this.state.selectedItem === id, - 'has-child': hasChild, - 'is-expanded': this.state.itemStates[id] && hasChild + 'fd-side-nav--icons': icons } ); - } - getSubLinkClasses = (id) => { - return classnames( - 'fd-side-nav__sublink', - { - 'is-selected': this.state.selectedItem === id - } + return ( + ); } +} + +export class SideNavList extends Component { + constructor(props) { + super(props); + } render() { - const { items, className, ...rest } = this.props; + const { children, className, icons, ...rest } = this.props; const sideNavListClasses = classnames( 'fd-side-nav__list', @@ -92,96 +40,16 @@ export class SideNavList extends Component { ); return ( - - - +
    + {children} +
); } } -SideNavList.propTypes = { - items: PropTypes.array.isRequired, - className: PropTypes.string -}; - -SideNavList.propDescriptions = { - items: 'An array of objects with keys \'id\', \'url\', \'name\', \'hasChild\', \'child\', and \'glyph\' setting the attributes of the items.' -}; - -export const SideNavGroup = props => { - const { title, children, className, titleProps, ...rest } = props; +const SideNavGroup = ({title, children, className, titleProps, ...props}) => { const sideNavGroupClasses = classnames( 'fd-side-nav__group', className @@ -189,16 +57,90 @@ export const SideNavGroup = props => { return (
-

{title}

+

+ {title} +

{children}
); }; -SideNavGroup.propTypes = { - title: PropTypes.string.isRequired, - className: PropTypes.string, - titleProps: PropTypes.object +const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, url, ...props}) => { + const getClasses = () => { + return classnames( + { + 'fd-side-nav__link': !isSubItem, + 'fd-side-nav__sublink': isSubItem, + 'is-selected': 'item_3' === id, + 'has-child': hasChild, + 'is-expanded': hasChild + } + ); + }; + + const renderLink = () => { + return ( + + {glyph ? ( + + ) : null} + {name} + + ); + }; + + return ( +
  • + {url && renderLink()} + {React.Children.map(children, (child) => { + if (React.isValidElement(child) && child.type !== 'SubGroup') { + return React.cloneElement(child, { + children: ( + {glyph ? ( + + ) : null} + {child.props.children} + ), + className: getClasses() + }); + } else { + return child; + } + })} +
  • + ); +}; + +const SideNavSubItems = ({children, id, open, ...props}) => { + return ( +
      + { + React.Children.map(children, (child) => { + return React.cloneElement(child, { + isSubItem: true + }); + }) + } +
    + ); }; + +SideNav.Group = SideNavGroup; +SideNav.List = SideNavList; +SideNav.Item = SideNavItem; +SideNav.SubItems = SideNavSubItems; diff --git a/src/SideNavigation/SideNavigation.test.js b/src/SideNavigation/SideNavigation.test.js index c57c3192c..286404f30 100644 --- a/src/SideNavigation/SideNavigation.test.js +++ b/src/SideNavigation/SideNavigation.test.js @@ -1,108 +1,251 @@ +import { Link } from 'react-router-dom'; import { mount } from 'enzyme'; import React from 'react'; import renderer from 'react-test-renderer'; -import { SideNav, SideNavGroup, SideNavList } from './SideNavigation'; +import { SideNavList } from './SideNavigation'; describe('', () => { const subSideNavList = ( - + + + Link Item 1 + + + Link Item 2 + + + Item 1 + + + Item 2 + + + Item 3 + + + Item 4 + + + + + Link Item 3 + + + Link Item 4 + + + Item 1 + + + Item 2 + + + Item 3 + + + Item 4 + + + + + Link Item + + ); const oneLevelSideNav = ( - - - + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + ); const sideNavWithTitle = ( - - - - - - - - + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + ); - const sideNavMultiLevel = {subSideNavList}; + const sideNavMultiLevel = {subSideNavList}; const sideNavWithIcons = ( - - - + + + + Link Item + + + + + Link Item + + + + + Link Item + + + + Link Item + + + Link Item + + ); const sideNavCollapsed = ( - - - + + + + + + + ); test('create side navigation', () => { @@ -195,42 +338,42 @@ describe('', () => { ]); }); - describe('Prop spreading', () => { - test('should allow props to be spread to the SideNav component', () => { - const element = mount(); + // describe('Prop spreading', () => { + // test('should allow props to be spread to the SideNav component', () => { + // const element = mount(); - expect( - element.getDOMNode().attributes['data-sample'].value - ).toBe('Sample'); - }); + // expect( + // element.getDOMNode().attributes['data-sample'].value + // ).toBe('Sample'); + // }); - test('should allow props to be spread to the SideNavList component', () => { - const items = [ - { id: 'item-1', url: '#', name: 'Link Item 1' }, - { id: 'item-2', url: '#', name: 'Link Item 2' }, - { id: 'item-3', url: '#', name: 'Link Item 3' } - ]; - const element = mount(); - - expect( - element.getDOMNode().attributes['data-sample'].value - ).toBe('Sample'); - }); + // test('should allow props to be spread to the SideNavList component', () => { + // const items = [ + // { id: 'item-1', url: '#', name: 'Link Item 1' }, + // { id: 'item-2', url: '#', name: 'Link Item 2' }, + // { id: 'item-3', url: '#', name: 'Link Item 3' } + // ]; + // const element = mount(); - test('should allow props to be spread to the SideNavGroup component', () => { - const element = mount(); + // expect( + // element.getDOMNode().attributes['data-sample'].value + // ).toBe('Sample'); + // }); - expect( - element.getDOMNode().attributes['data-sample'].value - ).toBe('Sample'); - }); + // test('should allow props to be spread to the SideNavGroup component', () => { + // const element = mount(); - test('should allow props to be spread to the SideNavGroup component\'s h1 element', () => { - const element = mount(); + // expect( + // element.getDOMNode().attributes['data-sample'].value + // ).toBe('Sample'); + // }); - expect( - element.find('h1').getDOMNode().attributes['data-sample'].value - ).toBe('Sample'); - }); - }); + // test('should allow props to be spread to the SideNavGroup component\'s h1 element', () => { + // const element = mount(); + + // expect( + // element.find('h1').getDOMNode().attributes['data-sample'].value + // ).toBe('Sample'); + // }); + // }); }); diff --git a/src/index.js b/src/index.js index 3a5ea4494..a0f1e4422 100644 --- a/src/index.js +++ b/src/index.js @@ -50,7 +50,7 @@ export { ProductTile } from './Tile/Tile'; export { ProductTileContent } from './Tile/Tile'; export { ProductTileMedia } from './Tile/Tile'; export { SearchInput } from './SearchInput/SearchInput'; -export { SideNav, SideNavList, SideNavGroup } from './SideNavigation/SideNavigation'; +export { SideNav as SideNav, SideNavList, SideNavGroup } from './SideNavigation/SideNavigation'; export { Table } from './Table/Table'; export { Tab, TabGroup } from './Tabs/Tabs'; export { Token } from './Token/Token'; From 38c36f6f2b594e79da8dd0361d4b3cf6df3132a9 Mon Sep 17 00:00:00 2001 From: Jeffrey Johnson Date: Tue, 5 Feb 2019 09:23:01 -0800 Subject: [PATCH 02/17] saving state --- src/SideNavigation/SideNavigation.js | 56 ++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/SideNavigation/SideNavigation.js b/src/SideNavigation/SideNavigation.js index 72103e02a..94e0976bd 100644 --- a/src/SideNavigation/SideNavigation.js +++ b/src/SideNavigation/SideNavigation.js @@ -5,6 +5,17 @@ import React, { Component } from 'react'; export class SideNav extends Component { constructor(props) { super(props); + + this.state = { + selectedId: props.selectedItem + }; + } + + handleSelect = (e, id) => { + console.log(id); + this.setState({ + selectedId: id + }); } render() { @@ -20,7 +31,15 @@ export class SideNav extends Component { return ( ); } @@ -32,7 +51,7 @@ export class SideNavList extends Component { } render() { - const { children, className, icons, ...rest } = this.props; + const { children, className, icons, onItemSelect, ...rest } = this.props; const sideNavListClasses = classnames( 'fd-side-nav__list', @@ -43,13 +62,21 @@ export class SideNavList extends Component {
      - {children} + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + onItemSelect: onItemSelect + }); + } else { + return child; + } + })}
    ); } } -const SideNavGroup = ({title, children, className, titleProps, ...props}) => { +const SideNavGroup = ({title, children, className, onItemSelect, titleProps, ...props}) => { const sideNavGroupClasses = classnames( 'fd-side-nav__group', className @@ -62,12 +89,20 @@ const SideNavGroup = ({title, children, className, titleProps, ...props}) => {

    {title}

    - {children} + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + onItemSelect: onItemSelect + }); + } else { + return child; + } + })}
    ); }; -const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, url, ...props}) => { +const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, onClick, onItemSelect, url, ...props}) => { const getClasses = () => { return classnames( { @@ -84,7 +119,11 @@ const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, url, ...pr return ( + href={url} + onClick={(e) => { + onClick(e); + onItemSelect(e, id); + }}> {glyph ? ( {url && renderLink()} {React.Children.map(children, (child) => { - if (React.isValidElement(child) && child.type !== 'SubGroup') { + console.warn(child.type); + if (React.isValidElement(child) && child.type !== SideNav.SubItems) { return React.cloneElement(child, { children: ( {glyph ? ( From 3ff7b7a75314d050ac039b1278bebb6a474c5b76 Mon Sep 17 00:00:00 2001 From: Jeffrey Johnson Date: Tue, 5 Feb 2019 16:30:19 -0800 Subject: [PATCH 03/17] Adding interactivity and active item --- .../SideNavigation.Component.js | 36 +++--- src/SideNavigation/SideNavigation.js | 119 +++++++++++------- 2 files changed, 88 insertions(+), 67 deletions(-) diff --git a/src/SideNavigation/SideNavigation.Component.js b/src/SideNavigation/SideNavigation.Component.js index 840ea0024..937ba732b 100644 --- a/src/SideNavigation/SideNavigation.Component.js +++ b/src/SideNavigation/SideNavigation.Component.js @@ -159,11 +159,9 @@ export const SideNavigationComponent = () => { - - Link Item - - + id='item_1' + name='Link Item' + url='#' /> @@ -244,7 +242,7 @@ export const SideNavigationComponent = () => { name='Link Item 1' url='#' /> @@ -277,24 +275,20 @@ export const SideNavigationComponent = () => { - Item 1 - + name='Item 1' + url='#' /> - Item 2 - + id='subitem_42' + name='Item 2' + url='#' /> - Item 3 - + id='subitem_43' + name='Item 3' + url='#' /> - Item 4 - + id='subitem_44' + name='Item 4' + url='#' /> { - console.log(id); + handleSelect = (e, id, hasChild) => { + let expandedIds = this.state.expandedIds; + if (hasChild && expandedIds.includes(id)) { + expandedIds = expandedIds.filter(eId => eId !== id) + } else if (hasChild) { + expandedIds.push(id); + } + this.setState({ + expandedIds: expandedIds, selectedId: id }); } @@ -34,7 +42,9 @@ export class SideNav extends Component { {React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { - onItemSelect: this.handleSelect + expandedIds: this.state.expandedIds, + onItemSelect: this.handleSelect, + selectedId: this.state.selectedId }); } else { return child; @@ -45,45 +55,16 @@ export class SideNav extends Component { } } -export class SideNavList extends Component { - constructor(props) { - super(props); - } - - render() { - const { children, className, icons, onItemSelect, ...rest } = this.props; - - const sideNavListClasses = classnames( - 'fd-side-nav__list', - className - ); - return ( -
      - {React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - return React.cloneElement(child, { - onItemSelect: onItemSelect - }); - } else { - return child; - } - })} -
    - ); - } -} -const SideNavGroup = ({title, children, className, onItemSelect, titleProps, ...props}) => { +const SideNavGroup = ({title, children, className, expandedIds, onItemSelect, selectedId, titleProps, ...props}) => { const sideNavGroupClasses = classnames( 'fd-side-nav__group', className - ); - - return ( -

    @@ -92,7 +73,9 @@ const SideNavGroup = ({title, children, className, onItemSelect, titleProps, ... {React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { - onItemSelect: onItemSelect + expandedIds: expandedIds, + onItemSelect: onItemSelect, + selectedId: selectedId }); } else { return child; @@ -102,15 +85,41 @@ const SideNavGroup = ({title, children, className, onItemSelect, titleProps, ... ); }; -const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, onClick, onItemSelect, url, ...props}) => { +const SideNavList = ({children, className, expandedIds, icons, onItemSelect, selectedId, ...rest}) => { + const sideNavListClasses = classnames( + 'fd-side-nav__list', + className + ); + + return ( +
      + {React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + expandedIds: expandedIds, + onItemSelect: onItemSelect, + selected: selectedId === child.props.id, + selectedId: selectedId + }); + } else { + return child; + } + })} +
    + ); +}; + +const SideNavItem = ({children, expandedIds = [], glyph, id, isSubItem, name, onClick, onItemSelect, selected, selectedId, url, ...props}) => { const getClasses = () => { return classnames( { 'fd-side-nav__link': !isSubItem, 'fd-side-nav__sublink': isSubItem, - 'is-selected': 'item_3' === id, + 'is-selected': selected, 'has-child': hasChild, - 'is-expanded': hasChild + 'is-expanded': expandedIds.includes(id) } ); }; @@ -122,7 +131,7 @@ const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, onClick, o href={url} onClick={(e) => { onClick(e); - onItemSelect(e, id); + onItemSelect(e, id, hasChild); }}> {glyph ? ( { + if (React.isValidElement(child) && child.type === SideNav.SubItems) { + hasChild = true; + } + }) + return (
  • {url && renderLink()} {React.Children.map(children, (child) => { - console.warn(child.type); if (React.isValidElement(child) && child.type !== SideNav.SubItems) { return React.cloneElement(child, { children: ( @@ -153,6 +168,12 @@ const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, onClick, o ), className: getClasses() }); + } else if (React.isValidElement(child) && child.type === SideNav.SubItems) { + return React.cloneElement(child, { + onItemSelect: onItemSelect, + open: expandedIds.includes(id), + selectedId: selectedId + }); } else { return child; } @@ -161,7 +182,11 @@ const SideNavItem = ({children, glyph, hasChild, id, isSubItem, name, onClick, o ); }; -const SideNavSubItems = ({children, id, open, ...props}) => { +SideNavItem.defaultProps = { + onClick: () => {} +}; + +const SideNavSubItems = ({children, id, onItemSelect, open, selectedId, ...props}) => { return (