Repository: react-materialize/react-materialize
Pull request #1079 (2020-03-29 10:02:52)

Tabs tabs hooks
Fixes https://github.com/react-materialize/react-materialize/issues/1071
Fixes https://github.com/react-materialize/react-materialize/issues/1045


Patch:
diff --git a/src/Tab.js b/src/Tab.js
index ab3a112..753af17 100644
--- a/src/Tab.js
+++ b/src/Tab.js
@@ -9,15 +9,15 @@ const Tab = ({ children, idx, className }) => (
     {children}
   </Col>
 );
+
 Tab.propTypes = {
-  children: PropTypes.node,
+  title: PropTypes.string.isRequired,
   idx: PropTypes.string,
-  className: PropTypes.string
-};
-
-Tab.defaultProps = {
-  active: false,
-  disabled: false
+  children: PropTypes.node,
+  className: PropTypes.string,
+  active: PropTypes.bool,
+  disabled: PropTypes.bool,
+  tabWidth: PropTypes.number
 };
 
 export default Tab;
diff --git a/src/Tabs.js b/src/Tabs.js
index 83ff648..c232f65 100644
--- a/src/Tabs.js
+++ b/src/Tabs.js
@@ -1,4 +1,4 @@
-import React, { Component, Fragment, cloneElement } from 'react';
+import React, { useEffect, useRef, cloneElement } from 'react';
 import PropTypes from 'prop-types';
 import idgen from './idgen';
 import cx from 'classnames';
@@ -6,85 +6,52 @@ import cx from 'classnames';
 import Row from './Row';
 import Tab from './Tab';
 
-class Tabs extends Component {
-  constructor(props) {
-    super(props);
-    this.scope = `${idgen()}`;
-  }
-
-  _onSelect(idx, e) {
-    const { onChange } = this.props;
-
-    if (onChange) onChange(idx, e);
-  }
-
-  componentDidMount() {
-    const { options } = this.props;
-
-    if (typeof M !== 'undefined') {
-      this.instance = M.Tabs.init(this._tabsEl, options);
-    }
-  }
-
-  componentDidUpdate() {
-    const { options } = this.props;
+const scope = `tabs-${idgen()}`;
 
-    if (typeof M !== 'undefined') {
-      this.instance.destroy();
-      this.instance = M.Tabs.init(this._tabsEl, options);
-    }
-  }
+const Tabs = ({ children, className, defaultValue, options, onChange }) => {
+  const _tabsRef = useRef(null);
 
-  componentWillUnmount() {
-    if (this.instance) {
-      this.instance.destroy();
-    }
-  }
+  useEffect(() => {
+    const instance = M.Tabs.init(_tabsRef.current, options);
+    return () => instance.destroy();
+  }, [options]);
 
-  render() {
-    const { children, className, defaultValue } = this.props;
-    return (
-      <Fragment>
-        <ul className={cx('tabs', className)} ref={el => (this._tabsEl = el)}>
-          {React.Children.map(children, (child, id) => {
-            const idx = `${this.scope}${id}`;
-            const { active, disabled, tabWidth, title } = child.props;
+  return (
+    <React.Fragment>
+      <ul className={cx('tabs', className)} ref={_tabsRef}>
+        {React.Children.map(children, (child, id) => {
+          const idx = `${scope}${id}`;
+          const { active, disabled, tabWidth, title } = child.props;
 
-            const classes = {
-              [`s${tabWidth}`]: tabWidth,
-              tab: true,
-              disabled,
-              col: true
-            };
+          const classes = {
+            [`s${tabWidth}`]: tabWidth,
+            tab: true,
+            disabled,
+            col: true
+          };
 
-            return (
-              <li className={cx(classes)} key={idx}>
-                <a
-                  href={`#tab_${idx}`}
-                  className={active || defaultValue === idx ? 'active' : ''}
-                  {...(disabled
-                    ? {}
-                    : { onClick: this._onSelect.bind(this, idx) })}
-                >
-                  {title}
-                </a>
-              </li>
-            );
-          })}
-        </ul>
-        <Row>
-          {React.Children.map(children, (child, id) => {
-            const idx = `${this.scope}${id}`;
-            return cloneElement(child, {
-              defaultValue,
-              idx
-            });
-          })}
-        </Row>
-      </Fragment>
-    );
-  }
-}
+          return (
+            <li className={cx(classes)} key={idx}>
+              <a
+                href={`#tab_${idx}`}
+                className={active || defaultValue === idx ? 'active' : ''}
+                {...(disabled ? {} : { onClick: onChange })}
+              >
+                {title}
+              </a>
+            </li>
+          );
+        })}
+      </ul>
+      <Row>
+        {React.Children.map(children, (child, id) => {
+          const idx = `${scope}${id}`;
+          return cloneElement(child, { idx });
+        })}
+      </Row>
+    </React.Fragment>
+  );
+};
 
 Tabs.propTypes = {
   children: PropTypes.node.isRequired,
diff --git a/stories/Tabs.stories.js b/stories/Tabs.stories.js
index c17a959..d7f10e9 100644
--- a/stories/Tabs.stories.js
+++ b/stories/Tabs.stories.js
@@ -54,3 +54,36 @@ stories.add('Fixed Width Tabs', () => (
     <Tab title="Test 4">Test 5</Tab>
   </Tabs>
 ));
+
+stories.add('Swipeable Tabs', () => (
+  <Tabs
+    className="tab-demo z-depth-1"
+    options={{
+      swipeable: true
+    }}
+  >
+    <Tab title="Test 1" className="blue">
+      Test 1
+    </Tab>
+    <Tab title="Test 2" active className="red">
+      Test 2
+    </Tab>
+    <Tab title="Test 3" className="green">
+      Test 3
+    </Tab>
+  </Tabs>
+));
+
+stories.add('Fixed Width Tabs', () => (
+  <Tabs className="tab-demo z-depth-1 tabs-fixed-width">
+    <Tab title="Test 1">Test 1</Tab>
+    <Tab title="Test 2" active>
+      Test 2
+    </Tab>
+    <Tab title="Test 3" disabled>
+      Test 3
+    </Tab>
+    <Tab title="Test 4">Test 4</Tab>
+    <Tab title="Test 4">Test 5</Tab>
+  </Tabs>
+));
diff --git a/types/components/Autocomplete.d.ts b/types/components/Autocomplete.d.ts
index 281718d..6148e0c 100644
--- a/types/components/Autocomplete.d.ts
+++ b/types/components/Autocomplete.d.ts
@@ -5,27 +5,28 @@ interface AutocompleteData {
   [key: string]: string | null;
 }
 
-declare namespace Autocomplete {
-  interface AutocompleteOptions {
-    data: AutocompleteData;
-    limit?: number;
-    onAutocomplete?: () => void;
-    minLength?: number;
-    sortFunction?: <T>(a: T, b: T, inputString: string) => number;
-  }
+interface AutocompleteOptions {
+  data: AutocompleteData;
+  limit?: number;
+  onAutocomplete?: () => void;
+  minLength?: number;
+  sortFunction?: <T>(a: T, b: T, inputString: string) => number;
+}
 
-  interface AutocompleteProps extends Responsive {
-    id?: string;
-    className?: string;
-    title?: string;
-    icon?: React.ReactNode;
-    placeholder?: string;
-    onChange?: (e: React.ChangeEvent) => void;
-    value?: string;
-    options: AutocompleteOptions;
-  }
+export interface AutocompleteProps extends Responsive {
+  id?: string;
+  className?: string;
+  title?: string;
+  icon?: React.ReactNode;
+  placeholder?: string;
+  onChange?: (e: React.ChangeEvent) => void;
+  value?: string;
+  options: AutocompleteOptions;
 }
 
-declare class Autocomplete extends React.Component<Autocomplete.AutocompleteProps> {}
+/**
+ * React Materialize: Autocomplete
+ */
+declare const Autocomplete: React.FC<AutocompleteProps>
 
-export = Autocomplete;
+export default Autocomplete;
diff --git a/types/components/Tab.ts b/types/components/Tab.ts
index 3c6bcd4..eab5332 100644
--- a/types/components/Tab.ts
+++ b/types/components/Tab.ts
@@ -1,8 +1,17 @@
 import * as React from 'react';
 
-declare namespace Checkbox {
+export interface TabProps {
+  title: string;
+  idx?: string;
+  className?: string;
+  active?: boolean;
+  disabled?: boolean;
+  tabWidth?: number;
 }
 
-declare class Checkbox extends React.Component {}
+/**
+ * React Materialize: Tab
+ */
+declare const Tab: React.FC<TabProps>
 
-export = Checkbox;
+export default Tab;
diff --git a/types/components/Tabs.ts b/types/components/Tabs.ts
index 3c6bcd4..9abc98b 100644
--- a/types/components/Tabs.ts
+++ b/types/components/Tabs.ts
@@ -1,8 +1,22 @@
 import * as React from 'react';
 
-declare namespace Checkbox {
+interface TabsOptions {
+  duration: number;
+  onShow: () => void;
+  swipeable: boolean;
+  responsiveThreshold: number;
 }
 
-declare class Checkbox extends React.Component {}
+export interface TabsProps {
+  options?: TabsOptions;
+  className?: string;
+  defaultValue?: string;
+  onChange?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
+}
+
+/**
+ * React Materialize: Tabs
+ */
+declare const Tabs: React.FC<TabsProps>
 
-export = Checkbox;
+export default Tabs;


Test patch:
diff --git a/test/Tabs.spec.js b/test/Tabs.spec.js
index 8982f52..c26be8c 100644
--- a/test/Tabs.spec.js
+++ b/test/Tabs.spec.js
@@ -59,26 +59,6 @@ describe('Tabs', () => {
     });
   });
 
-  describe('when updated', () => {
-    beforeEach(() => {
-      wrapper = mount(
-        <Tabs options={options}>
-          <Tab title="one">One</Tab>
-          <Tab title="Two">Two</Tab>
-        </Tabs>
-      );
-    });
-
-    test('should re-initialize with options', () => {
-      expect(tabsInitMock).toHaveBeenCalledWith(options);
-      tabsInitMock.mockClear();
-      tabInstanceDestroyMock.mockClear();
-      wrapper.setProps({ className: 'test' });
-      expect(tabInstanceDestroyMock).toHaveBeenCalled();
-      expect(tabsInitMock).toHaveBeenCalledWith(options);
-    });
-  });
-
   describe('when unmounted', () => {
     beforeEach(() => {
       tabInstanceDestroyMock.mockClear();
diff --git a/test/__snapshots__/Tabs.spec.js.snap b/test/__snapshots__/Tabs.spec.js.snap
index a01ef3a..09f7bee 100644
--- a/test/__snapshots__/Tabs.spec.js.snap
+++ b/test/__snapshots__/Tabs.spec.js.snap
@@ -7,24 +7,22 @@ exports[`Tabs should create list of Tab items 1`] = `
   >
     <li
       className="tab col"
-      key="mockId0/.0"
+      key="tabs-mockId0/.0"
     >
       <a
         className=""
-        href="#tab_mockId0"
-        onClick={[Function]}
+        href="#tab_tabs-mockId0"
       >
         one
       </a>
     </li>
     <li
       className="tab col"
-      key="mockId1/.1"
+      key="tabs-mockId1/.1"
     >
       <a
         className=""
-        href="#tab_mockId1"
-        onClick={[Function]}
+        href="#tab_tabs-mockId1"
       >
         Two
       </a>
@@ -32,7 +30,7 @@ exports[`Tabs should create list of Tab items 1`] = `
   </ul>
   <Row>
     <Tab
-      idx="mockId0"
+      idx="tabs-mockId0"
       key=".0"
       options={
         Object {
@@ -47,7 +45,7 @@ exports[`Tabs should create list of Tab items 1`] = `
       One
     </Tab>
     <Tab
-      idx="mockId1"
+      idx="tabs-mockId1"
       key=".1"
       options={
         Object {
diff --git a/types/sample.test.tsx b/types/sample.test.tsx
index 3ba5485..6d8a328 100644
--- a/types/sample.test.tsx
+++ b/types/sample.test.tsx
@@ -2,6 +2,12 @@ import React from 'react';
 
 import {
   Autocomplete,
+  Tabs,
+  Tab
 } from 'react-materialize';
 
-<Autocomplete options={{ data: { asd: '32' }, onAutocomplete: () => 32 }} s={3} />;
+<>
+  <Autocomplete options={{ data: { asd: '32' }, onAutocomplete: () => 32 }} s={3} />;
+  <Tabs className="hello" defaultValue="what" options={{ duration: 32, onShow: () => {}, swipeable: true, responsiveThreshold: 32 }} onChange={() => {}} />
+  <Tab idx="as" title="Hello" />
+</>