Repository: react-materialize/react-materialize
Pull request #1076 (2020-03-28 13:28:08)

Pagination hooks
Continuation of #962


Patch:
diff --git a/src/Button.js b/src/Button.js
index 85918da..b2a4932 100644
--- a/src/Button.js
+++ b/src/Button.js
@@ -1,109 +1,124 @@
-import React, { Children, useRef, useEffect } from 'react';
+import React, { Component } from 'react';
 import Icon from './Icon';
 import PropTypes from 'prop-types';
 import constants from './constants';
 import cx from 'classnames';
 import idgen from './idgen';
 
-const Button = ({
-  children,
-  className,
-  node,
-  fab,
-  modal,
-  flat,
-  floating,
-  large,
-  small,
-  disabled,
-  waves,
-  tooltip,
-  tooltipOptions = {},
-  icon,
-  ...props
-}) => {
-  const _btn = useRef(null);
-  const _fab = useRef(null);
-
-  useEffect(() => {
-    if (tooltip) {
-      const instance = M.Tooltip.init(_btn.current, tooltipOptions);
+class Button extends Component {
+  constructor(props) {
+    super(props);
+    this.renderFab = this.renderFab.bind(this);
+  }
 
-      return () => {
-        instance && instance.destroy();
-      };
+  componentDidMount() {
+    if (typeof M === 'undefined') return;
+
+    const { tooltip, tooltipOptions = {}, fab } = this.props;
+    if (tooltip) {
+      this.instance = M.Tooltip.init(this._btnEl, tooltipOptions);
     }
-  }, [tooltip, tooltipOptions]);
 
-  useEffect(() => {
     if (fab) {
-      const instance = M.FloatingActionButton.init(_fab.current, fab);
+      this.instance = M.FloatingActionButton.init(this._floatingActionBtn, fab);
+    }
+  }
 
-      return () => {
-        instance && instance.destroy();
-      };
+  componentWillUnmount() {
+    if (this.instance) {
+      this.instance.destroy();
     }
-  }, [fab]);
+  }
 
-  const renderFab = classes => {
-    return (
-      <div
-        {...props}
-        ref={_fab.current}
-        className={cx('fixed-action-btn', {
-          toolbar: fab.toolbarEnabled
-        })}
-      >
-        <a className={classes}>{icon || <Icon>edit</Icon>}</a>
-        <ul>
-          {Children.map(children, child => (
-            <li key={idgen()}>{child}</li>
-          ))}
-        </ul>
-      </div>
-    );
-  };
+  render() {
+    const {
+      children,
+      className,
+      node,
+      fab,
+      modal,
+      flat,
+      floating,
+      large,
+      small,
+      disabled,
+      waves,
+      tooltip,
+      icon,
+      ...other
+    } = this.props;
 
-  let C = node;
-  let classes = {
-    btn: true,
-    disabled,
-    'waves-effect': waves
-  };
+    delete other.tooltipOptions;
 
-  if (constants.WAVES.indexOf(waves) > -1) {
-    classes['waves-' + waves] = true;
-  }
+    let C = node;
+    let classes = {
+      btn: true,
+      disabled,
+      'waves-effect': waves
+    };
 
-  let styles = { flat, floating, large, small };
-  constants.STYLES.forEach(style => {
-    if (styles[style]) {
-      classes.btn = false;
-      classes['btn-' + style] = true;
+    if (constants.WAVES.indexOf(waves) > -1) {
+      classes['waves-' + waves] = true;
     }
-  });
 
-  if (modal) {
-    classes['modal-' + modal] = true;
+    let styles = { flat, floating, large, small };
+    constants.STYLES.forEach(style => {
+      if (styles[style]) {
+        classes.btn = false;
+        classes['btn-' + style] = true;
+      }
+    });
+
+    if (modal) {
+      classes['modal-' + modal] = true;
+    }
+    if (fab) {
+      return this.renderFab(cx(classes, className));
+    } else {
+      return (
+        <C
+          {...other}
+          disabled={!!disabled}
+          onClick={this.props.onClick}
+          className={cx(classes, className)}
+          ref={el => (this._btnEl = el)}
+          data-tooltip={tooltip}
+        >
+          {icon}
+          {children}
+        </C>
+      );
+    }
   }
 
-  if (fab) {
-    return renderFab(cx(classes, className));
-  } else {
+  renderFab(classes) {
+    const {
+      fab,
+      floating,
+      large,
+      className,
+      icon = <Icon>edit</Icon>,
+      ...other
+    } = this.props;
+
     return (
-      <C
-        {...props}
-        disabled={disabled}
-        className={cx(classes, className)}
-        ref={_btn}
-        data-tooltip={tooltip}
+      <div
+        {...other}
+        ref={el => (this._floatingActionBtn = el)}
+        className={cx('fixed-action-btn', {
+          toolbar: fab && fab.toolbarEnabled
+        })}
       >
-        {icon}
-        {children}
-      </C>
+        <a className={classes}>{icon}</a>
+        <ul>
+          {React.Children.map(this.props.children, child => {
+            return <li key={idgen()}>{child}</li>;
+          })}
+        </ul>
+      </div>
     );
   }
-};
+}
 
 Button.propTypes = {
   children: PropTypes.node,
diff --git a/src/CardTitle.js b/src/CardTitle.js
index dffe9f9..0a3956f 100644
--- a/src/CardTitle.js
+++ b/src/CardTitle.js
@@ -1,24 +1,27 @@
-import React from 'react';
+import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
 
 import constants from './constants';
 
-const CardTitle = ({ image, reveal, waves, children, ...props }) => {
-  const classes = cx({
-    'card-image': true,
-    'waves-effect': waves,
-    'waves-block': waves,
-    [`waves-${waves}`]: waves
-  });
+class CardTitle extends Component {
+  render() {
+    let { image, reveal, waves, children, ...props } = this.props;
+    const classes = cx({
+      'card-image': true,
+      'waves-effect': waves,
+      'waves-block': waves,
+      [`waves-${waves}`]: waves
+    });
 
-  return (
-    <div className={cx(classes)} {...props}>
-      <img className={cx({ activator: reveal })} src={image} />
-      <span className={cx('card-title')}>{children}</span>
-    </div>
-  );
-};
+    return (
+      <div className={cx(classes)} {...props}>
+        <img className={cx({ activator: reveal })} src={image} />
+        <span className={cx('card-title')}>{children}</span>
+      </div>
+    );
+  }
+}
 
 CardTitle.propTypes = {
   children: PropTypes.node,
diff --git a/src/Pagination.js b/src/Pagination.js
index d35e45a..b12324b 100644
--- a/src/Pagination.js
+++ b/src/Pagination.js
@@ -1,51 +1,39 @@
-import React, { Component } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
+
 import Icon from './Icon';
 import PaginationButton from './PaginationButton';
 
-class Pagination extends Component {
-  constructor(props) {
-    super(props);
-    const { activePage, items } = props;
-
-    this.state = {
-      activePage: activePage > 0 && activePage <= items ? activePage : 1
-    };
-
-    this.renderButtons = this.renderButtons.bind(this);
-    this._onClick = this._onClick.bind(this);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    if (nextProps.activePage !== this.props.activePage) {
-      this.setState({ activePage: nextProps.activePage });
-    }
-  }
-
-  _onClick(i) {
-    const { items, onSelect } = this.props;
-
-    return () => {
-      if (i > 0 && i <= items) {
-        if (onSelect) {
-          onSelect(i);
-        }
-        this.setState({ activePage: i });
+const Pagination = ({
+  items,
+  className,
+  children,
+  maxButtons = items,
+  leftBtn,
+  rightBtn,
+  onSelect,
+  activePage: activePageProp
+}) => {
+  const [activePage, setActivePage] = useState(
+    activePageProp > 0 && activePageProp <= items ? activePageProp : 1
+  );
+
+  useEffect(() => {
+    setActivePage(activePageProp);
+  }, [activePageProp]);
+
+  const onClick = useCallback(
+    pageIdx => {
+      if (pageIdx > 0 && pageIdx <= items) {
+        if (onSelect) onSelect(pageIdx);
+        setActivePage(pageIdx);
       }
-    };
-  }
-
-  renderButtons() {
-    const {
-      items,
-      children,
-      maxButtons = items,
-      leftBtn,
-      rightBtn
-    } = this.props;
-    const { activePage } = this.state;
+    },
+    [onSelect, items]
+  );
 
+  const renderButtons = useMemo(() => {
     if (children) return children;
 
     const buttonsCount = Math.min(maxButtons, items);
@@ -65,7 +53,7 @@ class Pagination extends Component {
       <PaginationButton
         disabled={activePage === 1}
         key={'pagination-0'}
-        onSelect={this._onClick(activePage - 1)}
+        onSelect={() => onClick(activePage - 1)}
       >
         {leftBtn}
       </PaginationButton>
@@ -76,7 +64,7 @@ class Pagination extends Component {
         <PaginationButton
           active={i === activePage}
           key={`pagination-${i}`}
-          onSelect={this._onClick(i)}
+          onSelect={() => onClick(i)}
         >
           {i}
         </PaginationButton>
@@ -87,23 +75,17 @@ class Pagination extends Component {
       <PaginationButton
         key={`pagination-${items + 1}`}
         disabled={activePage === items}
-        onSelect={this._onClick(activePage + 1)}
+        onSelect={() => onClick(activePage + 1)}
       >
         {rightBtn}
       </PaginationButton>
     );
 
     return buttons;
-  }
+  }, [children, maxButtons, items, activePage, onClick, leftBtn, rightBtn]);
 
-  render() {
-    return (
-      <ul className={cx('pagination', this.props.className)}>
-        {this.renderButtons()}
-      </ul>
-    );
-  }
-}
+  return <ul className={cx('pagination', className)}>{renderButtons}</ul>;
+};
 
 Pagination.propTypes = {
   /**
diff --git a/src/Table.js b/src/Table.js
index ff1d966..da764c3 100644
--- a/src/Table.js
+++ b/src/Table.js
@@ -1,29 +1,33 @@
-import React from 'react';
+import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import cx from 'classnames';
 
-const Table = ({
-  centered,
-  children,
-  className,
-  hoverable,
-  responsive,
-  striped,
-  ...props
-}) => {
-  const classes = {
-    centered: centered,
-    highlight: hoverable,
-    'responsive-table': responsive,
-    striped: striped
-  };
+class Table extends Component {
+  render() {
+    const {
+      centered,
+      children,
+      className,
+      hoverable,
+      responsive,
+      striped,
+      ...props
+    } = this.props;
 
-  return (
-    <table className={cx(classes, className)} {...props}>
-      {children}
-    </table>
-  );
-};
+    const classes = {
+      centered: centered,
+      highlight: hoverable,
+      'responsive-table': responsive,
+      striped: striped
+    };
+
+    return (
+      <table className={cx(classes, className)} {...props}>
+        {children}
+      </table>
+    );
+  }
+}
 
 Table.propTypes = {
   className: PropTypes.string,
diff --git a/stories/Pagination.stories.js b/stories/Pagination.stories.js
index 70c9177..88935bb 100644
--- a/stories/Pagination.stories.js
+++ b/stories/Pagination.stories.js
@@ -10,6 +10,6 @@ stories.addParameters({
   }
 });
 
-stories.add('Default', () => (
-  <Pagination items={10} activePage={2} maxButtons={8} />
-));
+stories.add('Default', () => <Pagination items={10} maxButtons={8} />);
+
+stories.add('with activePage', () => <Pagination items={5} activePage={3} />);


Test patch:
diff --git a/test/Button.spec.js b/test/Button.spec.js
index cb4bc42..9f63091 100644
--- a/test/Button.spec.js
+++ b/test/Button.spec.js
@@ -100,12 +100,12 @@ describe('Button', () => {
     });
 
     test('initializes Button with tooltip', () => {
-      wrapper = mount(<Button tooltip={tooltip}>Stuff</Button>);
+      wrapper = shallow(<Button tooltip={tooltip}>Stuff</Button>);
       expect(tooltipInitMock).toHaveBeenCalled();
     });
 
     test('initializes Button with tooltip options', () => {
-      wrapper = mount(
+      wrapper = shallow(
         <Button tooltip={tooltip} tooltipOptions={tooltipOptions}>
           Stuff
         </Button>
@@ -114,12 +114,29 @@ describe('Button', () => {
     });
 
     test('destroyed when unmounted', () => {
-      wrapper = mount(<Button tooltip={tooltip}>Stuff</Button>);
+      wrapper = shallow(<Button tooltip={tooltip}>Stuff</Button>);
       wrapper.unmount();
       expect(tooltipInstanceDestroyMock).toHaveBeenCalled();
     });
   });
 
+  describe('undefined M', () => {
+    let __M;
+    beforeEach(() => {
+      __M = global.M;
+      global.M = undefined;
+    });
+    afterEach(() => {
+      global.M = __M;
+    });
+    test('doesnt throw without M', () => {
+      const tooltip = {};
+      expect(() =>
+        mount(<Button tooltip={tooltip}>Stuff</Button>)
+      ).not.toThrow();
+    });
+  });
+
   describe('with fixed action button', () => {
     const fabInitMock = jest.fn();
     const fabInstanceDestroyMock = jest.fn();
diff --git a/test/Pagination.spec.js b/test/Pagination.spec.js
index b0ddb3c..8bc7c01 100644
--- a/test/Pagination.spec.js
+++ b/test/Pagination.spec.js
@@ -1,6 +1,7 @@
-import React from 'react';
-import { shallow, mount } from 'enzyme';
+import { mount, shallow } from 'enzyme';
+
 import Pagination from '../src/Pagination';
+import React from 'react';
 
 describe('<Pagination />', () => {
   let wrapper;
@@ -21,21 +22,8 @@ describe('<Pagination />', () => {
     expect(wrapper.childAt(2).props().active).toBeTruthy();
   });
 
-  test('accepts active prop', () => {
-    wrapper.setState({ activePage: 4 });
-    expect(wrapper).toMatchSnapshot();
-  });
-
   test("doesn't update state with wrong activePage prop", () => {
     wrapper = shallow(<Pagination items={1} activePage={2} />);
     expect(wrapper).toMatchSnapshot();
   });
-
-  test('updates state with passed activePage prop', () => {
-    const newActive = 5;
-    wrapper = mount(<Pagination items={10} activePage={2} />);
-    wrapper.setProps({ activePage: newActive });
-
-    expect(wrapper.state().activePage).toEqual(newActive);
-  });
 });
diff --git a/test/__snapshots__/Button.spec.js.snap b/test/__snapshots__/Button.spec.js.snap
index 7583163..27a58bd 100644
--- a/test/__snapshots__/Button.spec.js.snap
+++ b/test/__snapshots__/Button.spec.js.snap
@@ -3,6 +3,7 @@
 exports[`Button renders 1`] = `
 <button
   className="btn waves-effect waves-light"
+  disabled={false}
 >
   Stuff
 </button>
diff --git a/test/__snapshots__/Modal.spec.js.snap b/test/__snapshots__/Modal.spec.js.snap
index df1ba2f..7a53e6d 100644
--- a/test/__snapshots__/Modal.spec.js.snap
+++ b/test/__snapshots__/Modal.spec.js.snap
@@ -96,6 +96,7 @@ exports[`<Modal /> renders a modal has children 1`] = `
         >
           <button
             className="waves-effect waves-green btn-flat modal-close"
+            disabled={false}
           >
             Close
           </button>
@@ -202,6 +203,7 @@ exports[`<Modal /> renders a trigger renders 1`] = `
         >
           <button
             className="waves-effect waves-green btn-flat modal-close"
+            disabled={false}
           >
             Close
           </button>
@@ -293,6 +295,7 @@ exports[`<Modal /> without a trigger renders 1`] = `
         >
           <button
             className="waves-effect waves-green btn-flat modal-close"
+            disabled={false}
           >
             Close
           </button>
diff --git a/test/__snapshots__/Pagination.spec.js.snap b/test/__snapshots__/Pagination.spec.js.snap
index bbbf6b8..c14ebf7 100644
--- a/test/__snapshots__/Pagination.spec.js.snap
+++ b/test/__snapshots__/Pagination.spec.js.snap
@@ -1,72 +1,5 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`<Pagination /> accepts active prop 1`] = `
-<ul
-  className="pagination"
->
-  <PaginationButton
-    disabled={false}
-    key="pagination-0"
-    onSelect={[Function]}
-  >
-    <Icon>
-      chevron_left
-    </Icon>
-  </PaginationButton>
-  <PaginationButton
-    active={false}
-    key="pagination-1"
-    onSelect={[Function]}
-  >
-    1
-  </PaginationButton>
-  <PaginationButton
-    active={false}
-    key="pagination-2"
-    onSelect={[Function]}
-  >
-    2
-  </PaginationButton>
-  <PaginationButton
-    active={false}
-    key="pagination-3"
-    onSelect={[Function]}
-  >
-    3
-  </PaginationButton>
-  <PaginationButton
-    active={true}
-    key="pagination-4"
-    onSelect={[Function]}
-  >
-    4
-  </PaginationButton>
-  <PaginationButton
-    active={false}
-    key="pagination-5"
-    onSelect={[Function]}
-  >
-    5
-  </PaginationButton>
-  <PaginationButton
-    active={false}
-    key="pagination-6"
-    onSelect={[Function]}
-  >
-    6
-  </PaginationButton>
-  <PaginationButton
-    disabled={false}
-    key="pagination-11"
-    onSelect={[Function]}
-  >
-    <Icon>
-      chevron_right
-    </Icon>
-  </PaginationButton>
-</ul>
-`;
-
 exports[`<Pagination /> doesn't update state with wrong activePage prop 1`] = `
 <ul
   className="pagination"