Repository: react-materialize/react-materialize
Pull request #632 (2018-08-13 23:41:21)

3.0.0 Button.js
# Description
It closes react-materialize/react-materialize#583 (I hope :smile: )

This PR updates `Button` component to work with materializecss 1.0.0-rc.2. It includes:

- New class `.btn-small`
- Now if your element has a class like `.btn-large` or` .btn-small` you no longer need the `.btn` class
- A update of `Button` component docs (used materializecss new Button docs page as example)
- A page for documenting Floating Action Button component (used materializecss new Button docs page as example)
- A helper file called `string-helper`. It is used to captalize and add spaces to titles (for the navbar and header of the page). It was necessary since the Floating Action Component docs page had its title rendered as "FloatingActionPage" (in the header of the page and in the navbar)
- A fix for Floating Action Button. Now it is initialized when `fab` prop is passed
- A test for new class `.btn-small`

There is some items I would like the opinion from the others contributors:

- I have used materializecss docs as base for those changes in our docs. Was it ok?
- What you think about my solution for nav items with more than one word (Floating Action Button)? Any sugestion of how it could be better?
- The fix for Floating Action Button was valid? Is it ok to initialize the component the way I have done?
- There is any other unit tests you guys want me to add?

@petergarnaes, sorry but could you review this PR again? I have created a new clean branch. And thanks for helping me.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

- Manual tests using the docs
- Unit tests

# 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/docs/src/pages/ButtonsPage.js b/docs/src/pages/ButtonsPage.js
index 441c7aa..9481b33 100644
--- a/docs/src/pages/ButtonsPage.js
+++ b/docs/src/pages/ButtonsPage.js
@@ -7,10 +7,9 @@ import Samples from './Samples';
 import ButtonCode from '!raw-loader!Button';
 
 import raisedButton from '../../../examples/RaisedButton';
-import fixedActionButton from '../../../examples/FixedActionButton';
 import floatingButton from '../../../examples/FloatingButton';
+import verticalFAB from '../../../examples/VerticalFAB';
 import horizontalFab from '../../../examples/HorizontalFAB';
-import buttonAsLink from '../../../examples/ButtonAsLink';
 
 const ButtonsPage = () => (
   <Row>
@@ -35,27 +34,67 @@ const ButtonsPage = () => (
         </ReactPlayground>
       </Col>
       <h4 className='col s12'>
-        Fixed Action Button
+        Flat
       </h4>
+      <p className='col s12 caption'>
+        Flat buttons are used to reduce excessive layering. For example, flat buttons are usually used for actions within a card or modal so there aren't too many overlapping shadows.
+      </p>
       <Col s={12}>
-        <ReactPlayground code={Samples.fixedActionButton}>
-          {fixedActionButton}
-        </ReactPlayground>
+        <ReactPlayground code={Samples.flatButton} />
       </Col>
       <h4 className='col s12'>
-        Horizontal FAB
+        Submit Button
       </h4>
+      <p className='col s12 caption'>
+        When you use a button to submit a form, instead of using a input tag, use a button tag with a type submit
+      </p>
       <Col s={12}>
-        <ReactPlayground code={Samples.horizontalFab}>
-          {horizontalFab}
+        <ReactPlayground code={Samples.submitButton} />
+      </Col>
+      <h4 className='col s12'>
+        Large Button
+      </h4>
+      <p className='col s12 caption'>
+        This button has a larger height for buttons that need more attention.
+      </p>
+      <Col s={12}>
+        <ReactPlayground code={Samples.largeButton} />
+      </Col>
+      <h4 className='col s12'>
+        Small Button
+      </h4>
+      <p className='col s12 caption'>
+        When mouse and keyboard are the primary input methods, this smaller button is useful for denser UI layouts.
+      </p>
+      <Col s={12}>
+        <ReactPlayground code={Samples.smallButton} />
+      </Col>
+      <h4 className='col s12'>
+        Disabled Button
+      </h4>
+      <p className='col s12 caption'>
+        This style can be applied to all button types
+      </p>
+      <Col s={12}>
+        <ReactPlayground code={Samples.disabledButton} />
+      </Col>
+      <h4 className='col s12'>
+        Fixed Action Button
+      </h4>
+      <p className='col s12  caption'>
+        If you want a fixed floating action button, you can add multiple actions that will appear on hover. Our demo is in the bottom righthand corner of the page.
+      </p>
+      <Col s={12}>
+        <ReactPlayground code={Samples.verticalFAB}>
+          {verticalFAB}
         </ReactPlayground>
       </Col>
       <h4 className='col s12'>
-        Button as Link
+        Horizontal FAB
       </h4>
       <Col s={12}>
-        <ReactPlayground code={Samples.buttonAsLink}>
-          {buttonAsLink}
+        <ReactPlayground code={Samples.horizontalFab}>
+          {horizontalFab}
         </ReactPlayground>
       </Col>
       <Col s={12}>
diff --git a/docs/src/pages/Samples.js b/docs/src/pages/Samples.js
index be3968e..62a2e20 100644
--- a/docs/src/pages/Samples.js
+++ b/docs/src/pages/Samples.js
@@ -17,7 +17,7 @@ export default {
   collectionWithHref: require('!raw-loader!../../../examples/CollectionWithHref.js'),
   disabledButton: require('!raw-loader!../../../examples/DisabledButton.js'),
   dropdown: require('!raw-loader!../../../examples/DropdownBasic.js'),
-  fixedActionButton: require('!raw-loader!../../../examples/FixedActionButton.js'),
+  verticalFAB: require('!raw-loader!../../../examples/VerticalFAB.js'),
   flatButton: require('!raw-loader!../../../examples/FlatButton.js'),
   floatingButton: require('!raw-loader!../../../examples/FloatingButton.js'),
   buttonAsLink: require('!raw-loader!../../../examples/ButtonAsLink.js'),
@@ -50,6 +50,7 @@ export default {
   sideNav: require('!raw-loader!../../../examples/SideNavBasic.js'),
   sliders: require('!raw-loader!../../../examples/Sliders.js'),
   submitButton: require('!raw-loader!../../../examples/SubmitButton.js'),
+  smallButton: require('!raw-loader!../../../examples/SmallButton.js'),
   tab: require('!raw-loader!../../../examples/Tab.js'),
   table: require('!raw-loader!../../../examples/Table.js'),
   toast: require('!raw-loader!../../../examples/Toast.js'),
diff --git a/examples/SmallButton.js b/examples/SmallButton.js
new file mode 100644
index 0000000..d207b6f
--- /dev/null
+++ b/examples/SmallButton.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import Button from '../src/Button';
+import Icon from '../src/Icon';
+
+export default
+<div>
+    <Button node='a' waves='light' small>Button</Button>
+    <Button node='a' waves='light' small>Button<Icon left>cloud</Icon></Button>
+    <Button node='a' waves='light' small>Button<Icon right>cloud</Icon></Button>
+</div>;
diff --git a/examples/FixedActionButton.js b/examples/VerticalFAB.js
similarity index 73%
rename from examples/FixedActionButton.js
rename to examples/VerticalFAB.js
index 301ffe7..dbbb39e 100644
--- a/examples/FixedActionButton.js
+++ b/examples/VerticalFAB.js
@@ -2,7 +2,7 @@ import React from 'react';
 import Button from '../src/Button';
 
 export default
-  <Button floating fab='vertical' faicon='fa fa-plus' className='red' large style={{bottom: '45px', right: '24px'}}>
+<Button floating fab='vertical' className='red' large style={{bottom: '45px', right: '24px'}} icon='mode_edit'>
   <Button floating icon='insert_chart' className='red'/>
   <Button floating icon='format_quote' className='yellow darken-1'/>
   <Button floating icon='publish' className='green'/>
diff --git a/src/Button.js b/src/Button.js
index 1a4b1dd..4188810 100644
--- a/src/Button.js
+++ b/src/Button.js
@@ -13,12 +13,21 @@ class Button extends Component {
   }
 
   componentDidMount() {
-    const { tooltip, tooltipOptions } = this.props;
-    if (
-      typeof $ !== 'undefined' &&
-      (typeof tooltip !== 'undefined' || typeof tooltipOptions !== 'undefined')
-    ) {
-      $(this._btnEl).tooltip(tooltipOptions);
+    if (!M) return;
+
+    const { tooltip, tooltipOptions = {}, fab } = this.props;
+    if (tooltip) {
+      this.instance = M.Tooltip.init(this._btnEl, tooltipOptions);
+    }
+
+    if (fab) {
+      this.instance = M.FloatingActionButton.init(this._floatingActionBtn);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.instance) {
+      this.instance.destroy();
     }
   }
 
@@ -32,6 +41,7 @@ class Button extends Component {
       flat,
       floating,
       large,
+      small,
       disabled,
       waves,
       tooltip,
@@ -50,9 +60,12 @@ class Button extends Component {
       classes['waves-' + waves] = true;
     }
 
-    let styles = { flat, floating, large };
+    let styles = { flat, floating, large, small };
     constants.STYLES.forEach(style => {
-      classes['btn-' + style] = styles[style];
+      if (styles[style]) {
+        classes.btn = false;
+        classes['btn-' + style] = true;
+      }
     });
 
     if (modal) {
@@ -80,7 +93,10 @@ class Button extends Component {
   renderFab(className, mode, clickOnly) {
     const classes = cx(mode, clickOnly);
     return (
-      <div className={cx('fixed-action-btn', classes)}>
+      <div
+        ref={el => (this._floatingActionBtn = el)}
+        className={cx('fixed-action-btn', classes)}
+      >
         <a className={className}>{this.renderIcon()}</a>
         <ul>
           {React.Children.map(this.props.children, child => {
@@ -108,6 +124,7 @@ Button.propTypes = {
    */
   flat: PropTypes.bool,
   large: PropTypes.bool,
+  small: PropTypes.bool,
   floating: PropTypes.bool,
   /**
    * Fixed action button
diff --git a/src/constants.js b/src/constants.js
index b5dee48..b3684d1 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,5 +1,5 @@
 export default {
-  STYLES: ['large', 'floating', 'flat'],
+  STYLES: ['large', 'small', 'floating', 'flat'],
   WAVES: ['light', 'red', 'yellow', 'orange', 'purple', 'green', 'teal'],
   SIZES: ['s', 'm', 'l', 'xl'],
   PLACEMENTS: ['left', 'center', 'right'],


Test patch:
diff --git a/test/Button.spec.js b/test/Button.spec.js
index ed35495..1a6d181 100644
--- a/test/Button.spec.js
+++ b/test/Button.spec.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import { shallow, mount } from 'enzyme';
 import Button from '../src/Button';
-import mocker from './helper/mocker';
+import mocker from './helper/new-mocker';
 
 describe('Button', () => {
   let wrapper;
@@ -31,6 +31,11 @@ describe('Button', () => {
     expect(wrapper.find('.btn-large')).toHaveLength(1);
   });
 
+  test('should apply small styles', () => {
+    wrapper = shallow(<Button small>Stuff</Button>);
+    expect(wrapper.find('.btn-small')).toHaveLength(1);
+  });
+
   test('should apply floating styles', () => {
     wrapper = shallow(<Button floating>Stuff</Button>);
     expect(wrapper.find('.btn-floating')).toHaveLength(1);
@@ -69,22 +74,98 @@ describe('Button', () => {
   });
 
   describe('with a tooltip', () => {
-    let tooltip = 'Hello';
-    let tooltipOptions = {
+    const tooltipInitMock = jest.fn();
+    const tooltipInstanceDestroyMock = jest.fn();
+    const tooltipMock = {
+      init: (el, options) => {
+        tooltipInitMock(options);
+        return {
+          destroy: tooltipInstanceDestroyMock
+        };
+      }
+    };
+    const restore = mocker('Tooltip', tooltipMock);
+    const tooltip = 'Hello';
+    const tooltipOptions = {
       position: 'top',
       delay: 50
     };
 
-    const tooltipMock = jest.fn();
-    mocker('tooltip', tooltipMock);
-    wrapper = shallow(<Button tooltip={tooltip}>Stuff</Button>);
+    beforeEach(() => {
+      tooltipInitMock.mockClear();
+    });
+
+    afterAll(() => {
+      restore();
+    });
+
     test('initializes Button with tooltip', () => {
-      expect(tooltipMock).toHaveBeenCalled();
+      wrapper = shallow(<Button tooltip={tooltip}>Stuff</Button>);
+      expect(tooltipInitMock).toHaveBeenCalled();
     });
 
-    wrapper = shallow(<Button tooltipOptions={tooltipOptions}>Stuff</Button>);
     test('initializes Button with tooltip options', () => {
-      expect(tooltipMock).toHaveBeenCalledWith(tooltipOptions);
+      wrapper = shallow(
+        <Button tooltip={tooltip} tooltipOptions={tooltipOptions}>
+          Stuff
+        </Button>
+      );
+      expect(tooltipInitMock).toHaveBeenCalledWith(tooltipOptions);
+    });
+
+    test('destroyed when unmounted', () => {
+      wrapper = shallow(<Button tooltip={tooltip}>Stuff</Button>);
+      wrapper.unmount();
+      expect(tooltipInstanceDestroyMock).toHaveBeenCalled();
+    });
+  });
+
+  describe('with fixed action button', () => {
+    const fabInitMock = jest.fn();
+    const fabInstanceDestroyMock = jest.fn();
+    const fabMock = {
+      init: (el, options) => {
+        fabInitMock(options);
+        return {
+          destroy: fabInstanceDestroyMock
+        };
+      }
+    };
+    const restore = mocker('FloatingActionButton', fabMock);
+    const fab = 'vertical';
+    let wrapper;
+
+    beforeEach(() => {
+      fabInitMock.mockClear();
+      wrapper = mount(
+        <Button
+          floating
+          fab={fab}
+          className="red"
+          large
+          style={{
+            bottom: '45px',
+            right: '24px'
+          }}
+        >
+          <Button floating icon="insert_chart" className="red" />
+          <Button floating icon="format_quote" className="yellow darken-1" />
+          <Button floating icon="publish" className="green" />
+          <Button floating icon="attach_file" className="blue" />
+        </Button>
+      );
+    });
+
+    afterAll(() => {
+      restore();
+    });
+
+    test('initializes FloatingActionButton instance', () => {
+      expect(fabInitMock).toHaveBeenCalled();
+    });
+    test('destroys FloatingActionButton instance when unmounted', () => {
+      wrapper.unmount();
+      expect(fabInstanceDestroyMock).toHaveBeenCalled();
     });
   });
 });
diff --git a/test/__snapshots__/Modal.spec.js.snap b/test/__snapshots__/Modal.spec.js.snap
index 29ad48a..bedaefd 100644
--- a/test/__snapshots__/Modal.spec.js.snap
+++ b/test/__snapshots__/Modal.spec.js.snap
@@ -62,7 +62,7 @@ exports[`<Modal /> renders a modal has children 1`] = `
           waves="green"
         >
           <button
-            className="btn waves-effect waves-green btn-flat modal-close"
+            className="waves-effect waves-green btn-flat modal-close"
             disabled={false}
           >
             Close
@@ -136,7 +136,7 @@ exports[`<Modal /> renders a trigger renders 1`] = `
           waves="green"
         >
           <button
-            className="btn waves-effect waves-green btn-flat modal-close"
+            className="waves-effect waves-green btn-flat modal-close"
             disabled={false}
           >
             Close
@@ -197,7 +197,7 @@ exports[`<Modal /> without a trigger renders 1`] = `
           waves="green"
         >
           <button
-            className="btn waves-effect waves-green btn-flat modal-close"
+            className="waves-effect waves-green btn-flat modal-close"
             disabled={false}
           >
             Close