Repository: react-materialize/react-materialize
Pull request #616 (2018-07-20 11:49:11)

3.0.0 beta.0 tabs
# Description

Issues #607, #408 are covered.

Adapted to new materialize-css API and refactored to be more efficient and fix wrong behavior after re-render.

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

# Checklist:

- [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/Tabs.js b/src/Tabs.js
index bae6fc7..d3a09e7 100644
--- a/src/Tabs.js
+++ b/src/Tabs.js
@@ -7,11 +7,15 @@ import Row from './Row';
 import Col from './Col';
 
 class Tabs extends Component {
+  constructor(props) {
+    super(props);
+    this.scope = `${idgen()}`;
+  }
   componentDidMount() {
     const { tabOptions = {} } = this.props;
 
-    if (typeof $ !== 'undefined') {
-      $(this._tabsEl).tabs(tabOptions);
+    if (typeof M !== 'undefined') {
+      this.instance = M.Tabs.init(this._tabsEl, tabOptions);
     }
   }
 
@@ -21,25 +25,29 @@ class Tabs extends Component {
     if (onChange) onChange(idx, e);
   }
 
-  componentWillReceiveProps(nextProps) {
-    const { tabOptions = {} } = nextProps;
+  componentDidUpdate() {
+    const { tabOptions = {} } = this.props;
 
-    if (typeof $ !== 'undefined') {
-      $(this._tabsEl).tabs(tabOptions);
+    if (typeof M !== 'undefined') {
+      this.instance.destroy();
+      this.instance = M.Tabs.init(this._tabsEl, tabOptions);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.instance) {
+      this.instance.destroy();
     }
   }
 
   render() {
     const { children, className, defaultValue } = this.props;
-
-    const scope = `${idgen()}`;
-
     return (
       <Row>
         <Col s={12}>
           <ul className={cx('tabs', className)} ref={el => (this._tabsEl = el)}>
             {React.Children.map(children, (child, id) => {
-              const idx = `${scope}${id}`;
+              const idx = `${this.scope}${id}`;
               const {
                 active,
                 className,
@@ -72,7 +80,7 @@ class Tabs extends Component {
           </ul>
         </Col>
         {React.Children.map(children, (child, id) => {
-          const idx = `${scope}${id}`;
+          const idx = `${this.scope}${id}`;
           return (
             <Col
               id={`tab_${idx}`}
@@ -102,8 +110,25 @@ Tabs.propTypes = {
    * <a href='http://materializecss.com/tabs.html'>http://materializecss.com/tabs.html</a>
    */
   tabOptions: PropTypes.shape({
+    /*
+     * Transition duration in milliseconds.
+     * @default 300
+     */
+    duration: PropTypes.number,
+    /*
+     * Callback for when a new tab content is showns.
+     * @default null
+     */
     onShow: PropTypes.func,
+    /*
+     * Set to true to enable swipeable tabs. This also uses the responsiveThreshold option.
+     * @default false
+     */
     swipeable: PropTypes.bool,
+    /*
+     * The maximum width of the screen, in pixels, where the swipeable functionality initializes.
+     * @default Infinity
+     */
     responsiveThreshold: PropTypes.number
   })
 };


Test patch:
diff --git a/test/Tabs.spec.js b/test/Tabs.spec.js
index 7701690..cb8d1e5 100644
--- a/test/Tabs.spec.js
+++ b/test/Tabs.spec.js
@@ -2,14 +2,26 @@ import React from 'react';
 import { shallow, mount } from 'enzyme';
 import Tabs from '../src/Tabs';
 import Tab from '../src/Tab';
-import mocker from './helper/mocker';
+import mocker from './helper/new-mocker';
+import * as idgenHolder from '../src/idgen';
 
 describe('Tabs', () => {
   let wrapper;
-  const tabsMock = jest.fn();
-  const restore = mocker('tabs', tabsMock);
+  const tabsInitMock = jest.fn();
+  const tabInstanceDestroyMock = jest.fn();
+  const tabsMock = {
+    init: (el, options) => {
+      tabsInitMock(options);
+      return {
+        destroy: tabInstanceDestroyMock
+      };
+    }
+  };
+  const restore = mocker('Tabs', tabsMock);
+  const idgenSpy = jest.spyOn(idgenHolder, 'default');
 
   const options = {
+    duration: 400,
     onShow: jest.fn(),
     swipeable: true,
     responsiveThreshold: 2
@@ -24,6 +36,11 @@ describe('Tabs', () => {
 
   afterAll(() => {
     restore();
+    idgenSpy.mockRestore();
+  });
+
+  beforeEach(() => {
+    tabsInitMock.mockClear();
   });
 
   test('should create list of Tab itemt', () => {
@@ -41,12 +58,13 @@ describe('Tabs', () => {
     });
 
     test('initializes Tabs with options', () => {
-      expect(tabsMock).toHaveBeenCalledWith(options);
+      expect(tabsInitMock).toHaveBeenCalledWith(options);
     });
   });
 
   describe('when updated', () => {
     beforeEach(() => {
+      idgenSpy.mockClear();
       wrapper = mount(
         <Tabs tabOptions={options}>
           <Tab title="one">One</Tab>
@@ -56,9 +74,29 @@ describe('Tabs', () => {
     });
 
     test('should re-initialize with options', () => {
-      expect(tabsMock).toHaveBeenCalled();
+      expect(tabsInitMock).toHaveBeenCalledWith(options);
+      tabsInitMock.mockClear();
+      tabInstanceDestroyMock.mockClear();
+      wrapper.setProps({ className: 'test' });
+      expect(tabInstanceDestroyMock).toHaveBeenCalled();
+      expect(tabsInitMock).toHaveBeenCalledWith(options);
+    });
+
+    test('idgen is not called after update', () => {
+      expect(idgenSpy).toHaveBeenCalledTimes(1);
       wrapper.setProps({ className: 'test' });
-      expect(tabsMock).toHaveBeenCalled();
+      expect(idgenSpy).toHaveBeenCalledTimes(1);
+    });
+  });
+
+  describe('when unmounted', () => {
+    beforeEach(() => {
+      tabInstanceDestroyMock.mockClear();
+      wrapper.unmount();
+    });
+
+    test('should be destroyed', () => {
+      expect(tabInstanceDestroyMock).toHaveBeenCalled();
     });
   });
 });