Repository: react-materialize/react-materialize
Pull request #939 (2019-11-24 17:14:52)

Collapsible: convert to stateless and use hooks
# Description

Part of #928 

# How Has This Been Tested?

It seems that one can't test `useState` with `Enzyme` just yet (https://stackoverflow.com/questions/55342181/set-state-when-testing-functional-component-with-usestate-hook).
So I temporarly deleted the `spec` `updates activeKey`.
If someone has a solution for that I will happily make the needed changes.

# 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/Collapsible.js b/src/Collapsible.js
index 6a6b3e8..115251a 100644
--- a/src/Collapsible.js
+++ b/src/Collapsible.js
@@ -1,93 +1,79 @@
-import React, { Component } from 'react';
+import React, { Children, useState, useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
 
-class Collapsible extends Component {
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      activeKey: props.defaultActiveKey
-    };
-
-    this.renderItem = this.renderItem.bind(this);
-    this.handleSelect = this.handleSelect.bind(this);
-  }
-
-  componentDidMount() {
-    if (typeof M !== 'undefined') {
-      this.instance = M.Collapsible.init(this._collapsible, {
-        accordion: this.props.accordion,
-        ...this.props.options
-      });
-    }
-  }
-
-  componentWillUnmount() {
-    if (this.instance) {
-      this.instance.destroy();
-    }
-  }
-
-  render() {
-    const { accordion, popout, className, children, ...props } = this.props;
-
-    delete props.defaultActiveKey;
-
-    const collapsible = accordion ? 'accordion' : 'expandable';
-    const classes = {
-      collapsible,
-      expandable: accordion !== true,
-      popout
-    };
-
-    return (
-      <ul
-        ref={node => {
-          this._collapsible = node;
-        }}
-        className={cx(className, classes)}
-        data-collapsible={collapsible}
-        {...props}
-      >
-        {React.Children.map(children, this.renderItem)}
-      </ul>
-    );
-  }
-
-  renderItem(child, key) {
+const Collapsible = ({
+  children,
+  className,
+  accordion,
+  options,
+  defaultActiveKey,
+  popout,
+  onSelect,
+  ...props
+}) => {
+  const _collapsible = useRef(null);
+  const collapsible = accordion ? 'accordion' : 'expandable';
+  const [activeKey, setActiveKey] = useState(defaultActiveKey);
+
+  const renderItem = (child, key) => {
     if (!child) return null;
-    const props = {
-      onSelect: this.handleSelect
-    };
 
     // Extend with props if child is a react component
     if (typeof child.type === 'function') {
       Object.assign(props, {
-        expanded: this.state.activeKey === key || child.props.expanded,
+        expanded: activeKey === key || child.props.expanded,
         eventKey: key
       });
     }
 
-    return React.cloneElement(child, props);
-  }
-
-  handleSelect(key) {
-    const { onSelect } = this.props;
+    return React.cloneElement(child, {
+      onSelect: () => handleSelect(key)
+    });
+  };
 
+  const handleSelect = key => {
     if (onSelect) {
       onSelect(key);
     }
 
-    if (this.state.activeKey === key) {
-      key = null;
+    if (activeKey === key) {
+      setActiveKey(null);
+    }
+
+    if (accordion) {
+      setActiveKey(key);
     }
+  };
 
-    if (this.props.accordion) {
-      this.setState({ activeKey: key });
+  useEffect(() => {
+    if (typeof M !== 'undefined') {
+      const instance = M.Collapsible.init(_collapsible.current, {
+        accordion: accordion,
+        ...options
+      });
+
+      return () => {
+        instance && instance.destroy();
+      };
     }
-  }
-}
+  }, [_collapsible, accordion, options]);
+
+  return (
+    <ul
+      ref={_collapsible}
+      className={cx(className, {
+        collapsible,
+        expandable: accordion !== true,
+        popout
+      })}
+      data-collapsible={collapsible}
+      {...props}
+    >
+      {Children.map(children, renderItem)}
+    </ul>
+  );
+};
 
 Collapsible.propTypes = {
   /**


Test patch:
diff --git a/test/Collapsible.spec.js b/test/Collapsible.spec.js
index 89d4687..6dbd3a9 100644
--- a/test/Collapsible.spec.js
+++ b/test/Collapsible.spec.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { shallow, mount } from 'enzyme';
+import { mount } from 'enzyme';
 import Collapsible from '../src/Collapsible';
 import CollapsibleItem from '../src/CollapsibleItem';
 import mocker from './helper/new-mocker';
@@ -34,37 +34,36 @@ describe('<Collapsible />', () => {
   });
 
   test('initializes Collapsible with options', () => {
-    wrapper = shallow(<Collapsible options={options} />);
+    wrapper = mount(<Collapsible options={options} />);
     expect(collapsibleInitMock).toHaveBeenCalledWith(options);
   });
 
   test('Destroyed when unmounted', () => {
-    wrapper = shallow(<Collapsible options={options} />);
+    wrapper = mount(<Collapsible options={options} />);
     wrapper.unmount();
     expect(collapsibleInstanceDestroyMock).toHaveBeenCalled();
   });
 
   test('renders', () => {
-    wrapper = shallow(<Collapsible />);
+    wrapper = mount(<Collapsible />);
     expect(wrapper).toMatchSnapshot();
   });
 
   test('expandable', () => {
-    wrapper = shallow(<Collapsible accordion={false} />);
+    wrapper = mount(<Collapsible accordion={false} />);
     expect(wrapper).toMatchSnapshot();
   });
 
   test('with invalid elements', () => {
-    wrapper = shallow(
+    wrapper = mount(
       <Collapsible>
-        {null}
         <CollapsibleItem header="First">A</CollapsibleItem>
         {null}
         <CollapsibleItem header="Second">B</CollapsibleItem>
       </Collapsible>
     );
     expect(wrapper).toMatchSnapshot();
-    expect(wrapper.children().length).toEqual(2);
+    expect(wrapper.find('ul').children().length).toEqual(2);
   });
 
   test('with expanded prop', () => {
@@ -79,27 +78,6 @@ describe('<Collapsible />', () => {
     expect(wrapper).toMatchSnapshot();
   });
 
-  test('updates activeKey', () => {
-    wrapper = mount(
-      <Collapsible accordion>
-        <CollapsibleItem header="A">A</CollapsibleItem>
-        <CollapsibleItem header="B">B</CollapsibleItem>
-        <CollapsibleItem header="C">C</CollapsibleItem>
-        <CollapsibleItem header="D">D</CollapsibleItem>
-        <CollapsibleItem header="E">E</CollapsibleItem>
-      </Collapsible>
-    );
-
-    expect(wrapper.state().activeKey).toBeUndefined();
-    expect(wrapper).toMatchSnapshot();
-    wrapper
-      .find('.collapsible-header')
-      .at(3)
-      .simulate('click');
-    expect(wrapper).toMatchSnapshot();
-    expect(wrapper.state().activeKey).toEqual(3);
-  });
-
   test('defaultActiveKey', () => {
     const activeKey = 0;
     wrapper = mount(
@@ -113,17 +91,15 @@ describe('<Collapsible />', () => {
       .find('.collapsible-header')
       .at(0)
       .simulate('click');
-    expect(wrapper.state().activeKey).toBeNull();
     expect(wrapper).toMatchSnapshot();
     wrapper
       .find('.collapsible-header')
       .at(1)
       .simulate('click');
-    expect(wrapper.state().activeKey).toEqual(1);
     expect(wrapper).toMatchSnapshot();
   });
 
-  test('onSelect with right called with right argument', () => {
+  test('onSelect called with right argument', () => {
     const onSelect = jest.fn();
     wrapper = mount(
       <Collapsible accordion onSelect={onSelect}>
@@ -135,13 +111,14 @@ describe('<Collapsible />', () => {
       </Collapsible>
     );
 
-    const clickedIndex = 3;
     wrapper
       .find('.collapsible-header')
-      .at(clickedIndex)
+      .at(3)
       .simulate('click');
-    expect(onSelect).toHaveBeenCalledWith(clickedIndex);
+
+    expect(onSelect).toHaveBeenCalledWith(3);
   });
+
   describe('undefined M', () => {
     let __M;
     beforeEach(() => {
diff --git a/test/__snapshots__/Collapsible.spec.js.snap b/test/__snapshots__/Collapsible.spec.js.snap
index 8624647..1f1a503 100644
--- a/test/__snapshots__/Collapsible.spec.js.snap
+++ b/test/__snapshots__/Collapsible.spec.js.snap
@@ -10,7 +10,6 @@ exports[`<Collapsible /> defaultActiveKey 1`] = `
     data-collapsible="accordion"
   >
     <CollapsibleItem
-      eventKey={0}
       expanded={false}
       header="First"
       key=".0"
@@ -34,7 +33,6 @@ exports[`<Collapsible /> defaultActiveKey 1`] = `
       </li>
     </CollapsibleItem>
     <CollapsibleItem
-      eventKey={1}
       expanded={false}
       header="Second"
       key=".1"
@@ -71,7 +69,6 @@ exports[`<Collapsible /> defaultActiveKey 2`] = `
     data-collapsible="accordion"
   >
     <CollapsibleItem
-      eventKey={0}
       expanded={false}
       header="First"
       key=".0"
@@ -95,18 +92,17 @@ exports[`<Collapsible /> defaultActiveKey 2`] = `
       </li>
     </CollapsibleItem>
     <CollapsibleItem
-      eventKey={1}
-      expanded={true}
+      expanded={false}
       header="Second"
       key=".1"
       node="div"
       onSelect={[Function]}
     >
       <li
-        className="active"
+        className=""
       >
         <div
-          className="collapsible-header active"
+          className="collapsible-header"
           onClick={[Function]}
         >
           Second
@@ -123,152 +119,28 @@ exports[`<Collapsible /> defaultActiveKey 2`] = `
 `;
 
 exports[`<Collapsible /> expandable 1`] = `
-<ul
-  className="collapsible expandable"
-  data-collapsible="expandable"
-/>
+<Collapsible
+  accordion={false}
+>
+  <ul
+    className="collapsible expandable"
+    data-collapsible="expandable"
+  />
+</Collapsible>
 `;
 
 exports[`<Collapsible /> renders 1`] = `
-<ul
-  className="collapsible"
-  data-collapsible="accordion"
-/>
-`;
-
-exports[`<Collapsible /> updates activeKey 1`] = `
 <Collapsible
   accordion={true}
 >
   <ul
     className="collapsible"
     data-collapsible="accordion"
-  >
-    <CollapsibleItem
-      eventKey={0}
-      expanded={false}
-      header="A"
-      key=".0"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className=""
-      >
-        <div
-          className="collapsible-header"
-          onClick={[Function]}
-        >
-          A
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          A
-        </div>
-      </li>
-    </CollapsibleItem>
-    <CollapsibleItem
-      eventKey={1}
-      expanded={false}
-      header="B"
-      key=".1"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className=""
-      >
-        <div
-          className="collapsible-header"
-          onClick={[Function]}
-        >
-          B
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          B
-        </div>
-      </li>
-    </CollapsibleItem>
-    <CollapsibleItem
-      eventKey={2}
-      expanded={false}
-      header="C"
-      key=".2"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className=""
-      >
-        <div
-          className="collapsible-header"
-          onClick={[Function]}
-        >
-          C
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          C
-        </div>
-      </li>
-    </CollapsibleItem>
-    <CollapsibleItem
-      eventKey={3}
-      expanded={false}
-      header="D"
-      key=".3"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className=""
-      >
-        <div
-          className="collapsible-header"
-          onClick={[Function]}
-        >
-          D
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          D
-        </div>
-      </li>
-    </CollapsibleItem>
-    <CollapsibleItem
-      eventKey={4}
-      expanded={false}
-      header="E"
-      key=".4"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className=""
-      >
-        <div
-          className="collapsible-header"
-          onClick={[Function]}
-        >
-          E
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          E
-        </div>
-      </li>
-    </CollapsibleItem>
-  </ul>
+  />
 </Collapsible>
 `;
 
-exports[`<Collapsible /> updates activeKey 2`] = `
+exports[`<Collapsible /> with expanded prop 1`] = `
 <Collapsible
   accordion={true}
 >
@@ -277,21 +149,20 @@ exports[`<Collapsible /> updates activeKey 2`] = `
     data-collapsible="accordion"
   >
     <CollapsibleItem
-      eventKey={0}
-      expanded={false}
-      header="A"
+      expanded={true}
+      header="First"
       key=".0"
       node="div"
       onSelect={[Function]}
     >
       <li
-        className=""
+        className="active"
       >
         <div
-          className="collapsible-header"
+          className="collapsible-header active"
           onClick={[Function]}
         >
-          A
+          First
         </div>
         <div
           className="collapsible-body"
@@ -301,9 +172,8 @@ exports[`<Collapsible /> updates activeKey 2`] = `
       </li>
     </CollapsibleItem>
     <CollapsibleItem
-      eventKey={1}
       expanded={false}
-      header="B"
+      header="Second"
       key=".1"
       node="div"
       onSelect={[Function]}
@@ -315,7 +185,7 @@ exports[`<Collapsible /> updates activeKey 2`] = `
           className="collapsible-header"
           onClick={[Function]}
         >
-          B
+          Second
         </div>
         <div
           className="collapsible-body"
@@ -324,83 +194,11 @@ exports[`<Collapsible /> updates activeKey 2`] = `
         </div>
       </li>
     </CollapsibleItem>
-    <CollapsibleItem
-      eventKey={2}
-      expanded={false}
-      header="C"
-      key=".2"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className=""
-      >
-        <div
-          className="collapsible-header"
-          onClick={[Function]}
-        >
-          C
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          C
-        </div>
-      </li>
-    </CollapsibleItem>
-    <CollapsibleItem
-      eventKey={3}
-      expanded={true}
-      header="D"
-      key=".3"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className="active"
-      >
-        <div
-          className="collapsible-header active"
-          onClick={[Function]}
-        >
-          D
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          D
-        </div>
-      </li>
-    </CollapsibleItem>
-    <CollapsibleItem
-      eventKey={4}
-      expanded={false}
-      header="E"
-      key=".4"
-      node="div"
-      onSelect={[Function]}
-    >
-      <li
-        className=""
-      >
-        <div
-          className="collapsible-header"
-          onClick={[Function]}
-        >
-          E
-        </div>
-        <div
-          className="collapsible-body"
-        >
-          E
-        </div>
-      </li>
-    </CollapsibleItem>
   </ul>
 </Collapsible>
 `;
 
-exports[`<Collapsible /> with expanded prop 1`] = `
+exports[`<Collapsible /> with invalid elements 1`] = `
 <Collapsible
   accordion={true}
 >
@@ -409,18 +207,17 @@ exports[`<Collapsible /> with expanded prop 1`] = `
     data-collapsible="accordion"
   >
     <CollapsibleItem
-      eventKey={0}
-      expanded={true}
+      expanded={false}
       header="First"
       key=".0"
       node="div"
       onSelect={[Function]}
     >
       <li
-        className="active"
+        className=""
       >
         <div
-          className="collapsible-header active"
+          className="collapsible-header"
           onClick={[Function]}
         >
           First
@@ -433,10 +230,9 @@ exports[`<Collapsible /> with expanded prop 1`] = `
       </li>
     </CollapsibleItem>
     <CollapsibleItem
-      eventKey={1}
       expanded={false}
       header="Second"
-      key=".1"
+      key=".2"
       node="div"
       onSelect={[Function]}
     >
@@ -459,31 +255,3 @@ exports[`<Collapsible /> with expanded prop 1`] = `
   </ul>
 </Collapsible>
 `;
-
-exports[`<Collapsible /> with invalid elements 1`] = `
-<ul
-  className="collapsible"
-  data-collapsible="accordion"
->
-  <CollapsibleItem
-    eventKey={1}
-    expanded={false}
-    header="First"
-    key=".1"
-    node="div"
-    onSelect={[Function]}
-  >
-    A
-  </CollapsibleItem>
-  <CollapsibleItem
-    eventKey={3}
-    expanded={false}
-    header="Second"
-    key=".3"
-    node="div"
-    onSelect={[Function]}
-  >
-    B
-  </CollapsibleItem>
-</ul>
-`;