Repository: react-materialize/react-materialize
Pull request #1066 (2020-03-27 10:20:19)

Dropdown hooks
Continuation from https://github.com/react-materialize/react-materialize/pull/956



Patch:
diff --git a/src/Dropdown.js b/src/Dropdown.js
index 6905277..3d3272f 100644
--- a/src/Dropdown.js
+++ b/src/Dropdown.js
@@ -1,77 +1,58 @@
-import React, { Component, Fragment, Children, cloneElement } from 'react';
+import React, {
+  Fragment,
+  Children,
+  cloneElement,
+  useRef,
+  useEffect
+} from 'react';
 import PropTypes from 'prop-types';
 import idgen from './idgen';
 import cx from 'classnames';
 
-class Dropdown extends Component {
-  constructor(props) {
-    super(props);
-    this.renderTrigger = this.renderTrigger.bind(this);
-    this.renderItems = this.renderItems.bind(this);
-  }
-
-  componentDidMount() {
-    const { options } = this.props;
-
-    if (typeof M !== undefined) {
-      this.instance = M.Dropdown.init(
-        document.querySelectorAll(this._trigger),
-        options
-      )[0];
-    }
-  }
-
-  componentWillUnmount() {
-    if (this.instance) {
-      this.instance.destroy();
-    }
-  }
-
-  render() {
-    const { className, ...props } = this.props;
-    delete props.trigger;
-    delete props.options;
+const Dropdown = ({ children, className, trigger, options, ...props }) => {
+  const _dropdownContent = useRef(null);
 
-    return (
-      <Fragment>
-        {this.renderTrigger()}
-        <ul {...props} className={cx('dropdown-content', className)}>
-          {this.renderItems()}
-        </ul>
-      </Fragment>
+  useEffect(() => {
+    const instance = M.Dropdown.init(
+      document.querySelector(`[data-target="${props.id}"]`),
+      options
     );
-  }
 
-  renderTrigger() {
-    const { trigger, id } = this.props;
+    return () => {
+      instance && instance.destroy();
+    };
+  }, [options, props.id]);
 
-    return cloneElement(trigger, {
-      'data-target': id,
-      ref: t => (this._trigger = `[data-target=${id}]`),
+  const renderTrigger = () =>
+    cloneElement(trigger, {
+      'data-target': props.id,
       className: cx(trigger.props.className, 'dropdown-trigger')
     });
-  }
 
-  renderItems() {
-    const { children } = this.props;
-
-    return Children.map(children, element => {
-      if (element) {
-        if (element.type.name === 'Divider') {
-          return <li key={idgen()} className="divider" tabIndex="-1" />;
-        } else {
-          return <li key={idgen()}>{element}</li>;
-        }
+  const renderItems = () =>
+    Children.map(children, element => {
+      if (element.type.name === 'Divider') {
+        return <li key={idgen()} className="divider" tabIndex="-1" />;
+      } else {
+        return <li key={idgen()}>{element}</li>;
       }
     });
-  }
-}
+
+  return (
+    <Fragment>
+      {renderTrigger()}
+      <ul
+        {...props}
+        className={cx('dropdown-content', className)}
+        ref={_dropdownContent}
+      >
+        {renderItems()}
+      </ul>
+    </Fragment>
+  );
+};
 
 Dropdown.propTypes = {
-  /*
-   * override id
-   * @default idgen()
-   */
   id: PropTypes.string,
   /**
    * The node to trigger the dropdown
@@ -102,7 +83,7 @@ Dropdown.propTypes = {
 };
 
 Dropdown.defaultProps = {
-  id: `Dropdown-${idgen()}`,
+  id: `Dropdown_${idgen()}`,
   options: {
     alignment: 'left',
     autoTrigger: true,


Test patch:
diff --git a/test/DropdownButton.spec.js b/test/DropdownButton.spec.js
index 1fa9c41..0552e68 100644
--- a/test/DropdownButton.spec.js
+++ b/test/DropdownButton.spec.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
 import Dropdown from '../src/Dropdown';
 import Divider from '../src/Divider';
 import mocker from './helper/new-mocker';
@@ -12,13 +12,6 @@ describe('<Dropdown />', () => {
     expect(wrapper).toMatchSnapshot();
   });
 
-  test('does not update ID reference with each render', () => {
-    wrapper = shallow(<Dropdown trigger={<span>hi</span>} />);
-    const { idx } = wrapper.instance();
-    wrapper.render();
-    expect(wrapper.instance().idx).toEqual(idx);
-  });
-
   test('does correctly render dividers', () => {
     wrapper = shallow(
       <Dropdown trigger={<span>hi</span>}>
@@ -43,7 +36,7 @@ describe('<Dropdown />', () => {
     const dropdownMock = {
       init: (el, options) => {
         dropdownInitMock(options);
-        return [{ destroy: dropdownInstanceDestroyMock }];
+        return { destroy: dropdownInstanceDestroyMock };
       }
     };
 
@@ -59,7 +52,7 @@ describe('<Dropdown />', () => {
     });
 
     test('with default options if none are given', () => {
-      wrapper = shallow(<Dropdown trigger={<button>Drop me!</button>} />);
+      wrapper = mount(<Dropdown trigger={<button>Drop me!</button>} />);
       expect(dropdownInitMock).toHaveBeenCalledWith({
         alignment: 'left',
         autoTrigger: true,
@@ -79,7 +72,7 @@ describe('<Dropdown />', () => {
 
     test('handles options prop', () => {
       const options = { constrainWidth: true };
-      wrapper = shallow(
+      wrapper = mount(
         <Dropdown trigger={<button>Drop me!</button>} options={options} />
       );
       expect(dropdownInitMock).toHaveBeenCalledWith(options);
@@ -87,7 +80,7 @@ describe('<Dropdown />', () => {
 
     test('should destroy when unmounted', () => {
       const options = { constrainWidth: true };
-      wrapper = shallow(
+      wrapper = mount(
         <Dropdown trigger={<button>Drop me!</button>} options={options} />
       );
       wrapper.unmount();
diff --git a/test/__snapshots__/DropdownButton.spec.js.snap b/test/__snapshots__/DropdownButton.spec.js.snap
index ddc34f0..c0d330e 100644
--- a/test/__snapshots__/DropdownButton.spec.js.snap
+++ b/test/__snapshots__/DropdownButton.spec.js.snap
@@ -4,13 +4,13 @@ exports[`<Dropdown /> renders 1`] = `
 <Fragment>
   <button
     className="dropdown-trigger"
-    data-target="Dropdown-mockId"
+    data-target="Dropdown_mockId"
   >
     Drop me!
   </button>
   <ul
     className="dropdown-content"
-    id="Dropdown-mockId"
+    id="Dropdown_mockId"
   />
 </Fragment>
 `;