Repository: react-materialize/react-materialize
Pull request #702 (2018-11-25 16:16:50)

3.0.0-Navbar
# Description

This pull request is for #600 

Dropping _jQuery_ initialization, now using `M.Sidenav.init()`

For more information about the `Sidenav` please refer to #670.

Initializing the `Sidenav` component with the default options (as per[ the documentation](https://materializecss.com/sidenav.html#optons)):

Also dropping the use of `NavItem` component.
This was necessary to really make use of `React Router` `Link` component or any other custom component one may wish to use to fulfill routing purposes.

For the same reason, I'm dropping the `href` props, instead fully relying on `brand` props which in turns make it possible (again) to use `Link` component from `React Router` or whatever you are using for routing purposes.

For example:
```javascript
<Navbar brand='Logo' href='/' />
     ...
</Navbar>
```
Is now:

```javascript
<Navbar brand={<a href="/">Logo</a>} />
     ...
</Navbar>
```

With `Link` component from `React Router`:

```javascript
<Navbar brand={<Link to="/">Logo</Link>} />
     ...
</Navbar>
```
I think this approach is more flexible and resolve some edges cases, as in #650  

Moreover It's now possible to add extended components to the `NavBar`[ as per the doc](https://materializecss.com/navbar.html#navbar-tabs).

To completely use the `extended component` functionality I think we have to refactor, for example `Tabs` component, maybe using `Portals`

## Type of change

- [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update

# How Has This Been Tested?

Used it in a project of mine and tested running `yarn test`.
I've updated the snapshot accordingly to the changes.

Also added some new tests too check 
- if the sidenav component initializes with default options if none are given
- if the mobile sidenav is rendered.
- it it's possible to center the logo using `centerLogo`
- can be extended with custom elements
# Checklist:

- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have not generated a new package version. (the maintainers will handle that)


Patch:
diff --git a/src/Navbar.js b/src/Navbar.js
index d441c4f..26bbbdc 100644
--- a/src/Navbar.js
+++ b/src/Navbar.js
@@ -1,76 +1,82 @@
-import React, { Component } from 'react';
+import React, { Component, Fragment, Children } from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
-import Col from './Col';
 import Icon from './Icon';
-
 class Navbar extends Component {
-  constructor(props) {
-    super(props);
-    this.renderSideNav = this.renderSideNav.bind(this);
-  }
-
   componentDidMount() {
-    if (typeof $ !== 'undefined') {
-      $('.button-collapse').sideNav(this.props.options);
+    const { options } = this.props;
+
+    if (typeof M !== 'undefined') {
+      this.instance = M.Sidenav.init(this._sidenav, options);
     }
   }
 
-  renderSideNav() {
-    return (
-      <ul id="nav-mobile" className="side-nav">
-        {this.props.children}
-      </ul>
-    );
+  componentWillUnmount() {
+    if (this.instance) {
+      this.instance.destroy();
+    }
   }
 
   render() {
     const {
+      children,
       brand,
       className,
+      extendWith,
       fixed,
-      left,
-      right,
-      centerLogo,
-      href,
-      ...other
+      alignLinks,
+      centerLogo
     } = this.props;
 
-    delete other.options;
-
-    let classes = {
-      right: right,
-      'hide-on-med-and-down': true
-    };
-
-    let brandClasses = {
+    const brandClasses = cx({
       'brand-logo': true,
-      right: left,
       center: centerLogo
-    };
+    });
+
+    const navCSS = cx({ 'nav-extended': extendWith }, className);
 
-    let content = (
-      <nav {...other} className={className}>
+    const navMobileCSS = cx('hide-on-med-and-down', [alignLinks]);
+
+    const links = Children.map(children, (link, index) => (
+      <li key={index}>{link}</li>
+    ));
+
+    let navbar = (
+      <nav className={navCSS}>
         <div className="nav-wrapper">
-          <Col s={12}>
-            <a href={href} className={cx(brandClasses)}>
-              {brand}
-            </a>
-            <ul className={cx(className, classes)}>{this.props.children}</ul>
-            {this.renderSideNav()}
-            <a className="button-collapse" href="#" data-activates="nav-mobile">
-              <Icon>view_headline</Icon>
-            </a>
-          </Col>
+          {brand &&
+            React.cloneElement(brand, {
+              className: cx(brand.props.className, brandClasses)
+            })}
+
+          <a href="#!" data-target="mobile-nav" className="sidenav-trigger">
+            <Icon>menu</Icon>
+          </a>
+          <ul className={navMobileCSS}>{links}</ul>
         </div>
+        {extendWith && <div className="nav-content">{extendWith}</div>}
       </nav>
     );
 
     if (fixed) {
-      content = <div className="navbar-fixed">{content}</div>;
+      navbar = <div className="navbar-fixed">{navbar}</div>;
     }
 
-    return content;
+    return (
+      <Fragment>
+        {navbar}
+
+        <ul
+          id="mobile-nav"
+          className={cx('sidenav', [alignLinks])}
+          ref={ul => {
+            this._sidenav = ul;
+          }}
+        >
+          {links}
+        </ul>
+      </Fragment>
+    );
   }
 }
 
@@ -78,39 +84,58 @@ Navbar.propTypes = {
   brand: PropTypes.node,
   children: PropTypes.node,
   className: PropTypes.string,
+  extendWith: PropTypes.node,
   /**
-   * Makes the navbar links left aligned
-   */
-  left: PropTypes.bool,
-  /**
-   * Makes the navbar links right aligned
+   * left makes the navbar links left aligned, right makes them right aligned
    */
-  right: PropTypes.bool,
+  alignLinks: PropTypes.oneOf(['left', 'right']),
   /**
    * The logo will center itself on medium and down screens.
    * Specifying centerLogo as a prop the logo will always be centered
    */
   centerLogo: PropTypes.bool,
-  href: PropTypes.string,
   /**
    * Makes the navbar fixed
    */
   fixed: PropTypes.bool,
   /**
    * Options hash for the sidenav.
-   * More info: http://materializecss.com/side-nav.html#options
+   * More info: https://materializecss.com/sidenav.html#options
    */
   options: PropTypes.shape({
-    menuWidth: PropTypes.number,
+    // Side of screen on which Sidenav appears.
     edge: PropTypes.oneOf(['left', 'right']),
-    closeOnClick: PropTypes.bool,
-    draggable: PropTypes.bool
+    // Allow swipe gestures to open / close Sidenav.
+    draggable: PropTypes.bool,
+    // Length in ms of enter transition.
+    inDuration: PropTypes.number,
+    // Length in ms of exit transition.
+    outDuration: PropTypes.number,
+    // Function called when sidenav starts entering.
+    onOpenStart: PropTypes.func,
+    // Function called when sidenav finishes entering.
+    onOpenEnd: PropTypes.func,
+    // Function called when sidenav starts exiting.
+    onCloseStart: PropTypes.func,
+    // Function called when sidenav finishes exiting.
+    onCloseEnd: PropTypes.func,
+    // Prevent page from scrolling while sidenav is open.
+    preventScrolling: PropTypes.bool
   })
 };
 
 Navbar.defaultProps = {
-  href: '/',
-  options: {}
+  options: {
+    edge: 'left',
+    draggable: true,
+    inDuration: 250,
+    outDuration: 200,
+    onOpenStart: null,
+    onOpenEnd: null,
+    onCloseStart: null,
+    onCloseEnd: null,
+    preventScrolling: true
+  }
 };
 
 export default Navbar;


Test patch:
diff --git a/test/Navbar.spec.js b/test/Navbar.spec.js
index dfa827a..6008168 100644
--- a/test/Navbar.spec.js
+++ b/test/Navbar.spec.js
@@ -1,13 +1,26 @@
 import React from 'react';
 import { shallow, mount } from 'enzyme';
 import Navbar from '../src/Navbar';
-import NavItem from '../src/NavItem';
-import mocker from './helper/mocker';
+import mocker from './helper/new-mocker';
 
 describe('<Navbar />', () => {
   let wrapper;
-  const sideNavMock = jest.fn();
-  const restore = mocker('sideNav', sideNavMock);
+  const sidenavInitMock = jest.fn();
+  const sidenavInstanceDestroyMock = jest.fn();
+  const sidenavMock = {
+    init: (el, options) => {
+      sidenavInitMock(options);
+      return {
+        destroy: sidenavInstanceDestroyMock
+      };
+    }
+  };
+  const restore = mocker('Sidenav', sidenavMock);
+
+  beforeEach(() => {
+    sidenavInitMock.mockClear();
+    sidenavInstanceDestroyMock.mockClear();
+  });
 
   afterAll(() => {
     restore();
@@ -15,9 +28,9 @@ describe('<Navbar />', () => {
 
   test('renders', () => {
     wrapper = shallow(
-      <Navbar brand="Logo" right>
-        <NavItem href="get-started.html">Getting started</NavItem>
-        <NavItem href="components.html">Components</NavItem>
+      <Navbar brand={<a href="/">Logo</a>} alignLinks="right">
+        <a href="get-started.html">Getting started</a>
+        <a href="components.html">Components</a>
       </Navbar>
     );
 
@@ -25,10 +38,33 @@ describe('<Navbar />', () => {
     expect(wrapper.find('nav')).toHaveLength(1);
   });
 
+  test('sidenav get initialized with default options if none are given', () => {
+    mount(<Navbar />);
+    expect(sidenavInitMock).toHaveBeenCalledWith({
+      edge: 'left',
+      draggable: true,
+      inDuration: 250,
+      outDuration: 200,
+      onOpenStart: null,
+      onOpenEnd: null,
+      onCloseStart: null,
+      onCloseEnd: null,
+      preventScrolling: true
+    });
+  });
+
+  test('can center the brand logo', () => {
+    wrapper = shallow(<Navbar brand={<a href="/">Logo</a>} centerLogo />);
+    expect(wrapper).toMatchSnapshot();
+    expect(wrapper.find('a.brand-logo').hasClass('center'));
+  });
+
   test('places brand on right if navbar is left aligned', () => {
-    wrapper = shallow(<Navbar brand="logo" left />);
+    wrapper = shallow(
+      <Navbar brand={<a href="/">Logo</a>} alignLinks="left" />
+    );
     expect(wrapper).toMatchSnapshot();
-    expect(wrapper.find('a.brand-logo.right')).toHaveLength(1);
+    expect(wrapper.find('a.brand-logo').hasClass('right'));
   });
 
   test('adds a brand node', () => {
@@ -36,12 +72,35 @@ describe('<Navbar />', () => {
 
     wrapper = mount(<Navbar brand={brandNode} />);
     expect(wrapper).toMatchSnapshot();
-    expect(wrapper.find('a.brand-logo').contains(brandNode)).toBeTruthy();
+    expect(wrapper.find('span.brand-logo.brandp')).toBeTruthy();
   });
 
   test('can be fixed', () => {
     wrapper = shallow(<Navbar fixed />);
     expect(wrapper).toMatchSnapshot();
-    expect(wrapper.hasClass('navbar-fixed')).toBeTruthy();
+    expect(wrapper.find('.navbar-fixed')).toHaveLength(1);
+  });
+
+  test('can be extended with custom elements', () => {
+    wrapper = shallow(
+      <Navbar
+        extendWith={
+          <div className="col s12">
+            <a href="#!" className="breadcrumb">
+              First
+            </a>
+            <a href="#!" className="breadcrumb">
+              Second
+            </a>
+            <a href="#!" className="breadcrumb">
+              Third
+            </a>
+          </div>
+        }
+      />
+    );
+
+    expect(wrapper).toMatchSnapshot();
+    expect(wrapper.find('.breadcrumb')).toHaveLength(3);
   });
 });
diff --git a/test/__snapshots__/Navbar.spec.js.snap b/test/__snapshots__/Navbar.spec.js.snap
index 9838c88..f054be7 100644
--- a/test/__snapshots__/Navbar.spec.js.snap
+++ b/test/__snapshots__/Navbar.spec.js.snap
@@ -9,135 +9,221 @@ exports[`<Navbar /> adds a brand node 1`] = `
       I AM BRAND
     </span>
   }
-  href="/"
-  options={Object {}}
+  options={
+    Object {
+      "draggable": true,
+      "edge": "left",
+      "inDuration": 250,
+      "onCloseEnd": null,
+      "onCloseStart": null,
+      "onOpenEnd": null,
+      "onOpenStart": null,
+      "outDuration": 200,
+      "preventScrolling": true,
+    }
+  }
 >
-  <nav>
+  <nav
+    className=""
+  >
     <div
       className="nav-wrapper"
     >
-      <Col
-        s={12}
+      <span
+        className="brando brand-logo"
       >
-        <div
-          className="col s12"
-        >
-          <a
-            className="brand-logo"
-            href="/"
-          >
-            <span
-              className="brando"
-            >
-              I AM BRAND
-            </span>
-          </a>
-          <ul
-            className="hide-on-med-and-down"
-          />
-          <ul
-            className="side-nav"
-            id="nav-mobile"
-          />
-          <a
-            className="button-collapse"
-            data-activates="nav-mobile"
-            href="#"
+        I AM BRAND
+      </span>
+      <a
+        className="sidenav-trigger"
+        data-target="mobile-nav"
+        href="#!"
+      >
+        <Icon>
+          <i
+            className="material-icons"
           >
-            <Icon>
-              <i
-                className="material-icons"
-              >
-                view_headline
-              </i>
-            </Icon>
-          </a>
-        </div>
-      </Col>
+            menu
+          </i>
+        </Icon>
+      </a>
+      <ul
+        className="hide-on-med-and-down "
+      />
     </div>
   </nav>
+  <ul
+    className="sidenav "
+    id="mobile-nav"
+  />
 </Navbar>
 `;
 
-exports[`<Navbar /> can be fixed 1`] = `
-<div
-  className="navbar-fixed"
->
-  <nav>
+exports[`<Navbar /> can be extended with custom elements 1`] = `
+<Fragment>
+  <nav
+    className="nav-extended"
+  >
     <div
       className="nav-wrapper"
     >
-      <Col
-        s={12}
+      <a
+        className="sidenav-trigger"
+        data-target="mobile-nav"
+        href="#!"
+      >
+        <Icon>
+          menu
+        </Icon>
+      </a>
+      <ul
+        className="hide-on-med-and-down "
+      />
+    </div>
+    <div
+      className="nav-content"
+    >
+      <div
+        className="col s12"
       >
         <a
-          className="brand-logo"
-          href="/"
-        />
-        <ul
-          className="hide-on-med-and-down"
-        />
-        <ul
-          className="side-nav"
-          id="nav-mobile"
-        />
+          className="breadcrumb"
+          href="#!"
+        >
+          First
+        </a>
         <a
-          className="button-collapse"
-          data-activates="nav-mobile"
-          href="#"
+          className="breadcrumb"
+          href="#!"
         >
-          <Icon>
-            view_headline
-          </Icon>
+          Second
+        </a>
+        <a
+          className="breadcrumb"
+          href="#!"
+        >
+          Third
         </a>
-      </Col>
+      </div>
     </div>
   </nav>
-</div>
+  <ul
+    className="sidenav "
+    id="mobile-nav"
+  />
+</Fragment>
 `;
 
-exports[`<Navbar /> places brand on right if navbar is left aligned 1`] = `
-<nav>
+exports[`<Navbar /> can be fixed 1`] = `
+<Fragment>
   <div
-    className="nav-wrapper"
+    className="navbar-fixed"
+  >
+    <nav
+      className=""
+    >
+      <div
+        className="nav-wrapper"
+      >
+        <a
+          className="sidenav-trigger"
+          data-target="mobile-nav"
+          href="#!"
+        >
+          <Icon>
+            menu
+          </Icon>
+        </a>
+        <ul
+          className="hide-on-med-and-down "
+        />
+      </div>
+    </nav>
+  </div>
+  <ul
+    className="sidenav "
+    id="mobile-nav"
+  />
+</Fragment>
+`;
+
+exports[`<Navbar /> can center the brand logo 1`] = `
+<Fragment>
+  <nav
+    className=""
   >
-    <Col
-      s={12}
+    <div
+      className="nav-wrapper"
     >
       <a
-        className="brand-logo right"
+        className="brand-logo center"
         href="/"
       >
-        logo
+        Logo
+      </a>
+      <a
+        className="sidenav-trigger"
+        data-target="mobile-nav"
+        href="#!"
+      >
+        <Icon>
+          menu
+        </Icon>
       </a>
       <ul
-        className="hide-on-med-and-down"
-      />
-      <ul
-        className="side-nav"
-        id="nav-mobile"
+        className="hide-on-med-and-down "
       />
+    </div>
+  </nav>
+  <ul
+    className="sidenav "
+    id="mobile-nav"
+  />
+</Fragment>
+`;
+
+exports[`<Navbar /> places brand on right if navbar is left aligned 1`] = `
+<Fragment>
+  <nav
+    className=""
+  >
+    <div
+      className="nav-wrapper"
+    >
+      <a
+        className="brand-logo"
+        href="/"
+      >
+        Logo
+      </a>
       <a
-        className="button-collapse"
-        data-activates="nav-mobile"
-        href="#"
+        className="sidenav-trigger"
+        data-target="mobile-nav"
+        href="#!"
       >
         <Icon>
-          view_headline
+          menu
         </Icon>
       </a>
-    </Col>
-  </div>
-</nav>
+      <ul
+        className="hide-on-med-and-down left"
+      />
+    </div>
+  </nav>
+  <ul
+    className="sidenav left"
+    id="mobile-nav"
+  />
+</Fragment>
 `;
 
 exports[`<Navbar /> renders 1`] = `
-<nav>
-  <div
-    className="nav-wrapper"
+<Fragment>
+  <nav
+    className=""
   >
-    <Col
-      s={12}
+    <div
+      className="nav-wrapper"
     >
       <a
         className="brand-logo"
@@ -145,45 +231,61 @@ exports[`<Navbar /> renders 1`] = `
       >
         Logo
       </a>
-      <ul
-        className="right hide-on-med-and-down"
+      <a
+        className="sidenav-trigger"
+        data-target="mobile-nav"
+        href="#!"
       >
-        <NavItem
-          href="get-started.html"
-        >
-          Getting started
-        </NavItem>
-        <NavItem
-          href="components.html"
-        >
-          Components
-        </NavItem>
-      </ul>
+        <Icon>
+          menu
+        </Icon>
+      </a>
       <ul
-        className="side-nav"
-        id="nav-mobile"
+        className="hide-on-med-and-down right"
       >
-        <NavItem
-          href="get-started.html"
+        <li
+          key="0/.0"
         >
-          Getting started
-        </NavItem>
-        <NavItem
-          href="components.html"
+          <a
+            href="get-started.html"
+          >
+            Getting started
+          </a>
+        </li>
+        <li
+          key="1/.1"
         >
-          Components
-        </NavItem>
+          <a
+            href="components.html"
+          >
+            Components
+          </a>
+        </li>
       </ul>
+    </div>
+  </nav>
+  <ul
+    className="sidenav right"
+    id="mobile-nav"
+  >
+    <li
+      key="0/.0"
+    >
       <a
-        className="button-collapse"
-        data-activates="nav-mobile"
-        href="#"
+        href="get-started.html"
       >
-        <Icon>
-          view_headline
-        </Icon>
+        Getting started
       </a>
-    </Col>
-  </div>
-</nav>
+    </li>
+    <li
+      key="1/.1"
+    >
+      <a
+        href="components.html"
+      >
+        Components
+      </a>
+    </li>
+  </ul>
+</Fragment>
 `;