Repository: mui/material-ui (TypeScript)
Date: 2018-06-09 18:13:38+00:00
Task type: Bug Fix

Issue:
Focus shifted to input element when I click on link in helptext
I added an Input field and in its helptext, I added an link for my documentation. When I click on that link, the focus shifted to input element. It should not happen as I didn't click on input field.
 
<!-- Checked checkbox should look like this: [x] -->
- [x] This is a v1.x issue (v0.x is no longer maintained).
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior
When I click on link in helptext, it should not shift focus to input element.

## Current Behavior
When I click on link in helptext, focus shifted to input element.


## Steps to Reproduce (for bugs)
Bug can be replicated here: https://codesandbox.io/s/8krz9y5q1j
By clicking on 'here' in helper text, you can see focus getting shifted to Input element


## Your Environment
<!--- Include as many relevant details about the environment with which you experienced the bug. -->

| Tech         | Version |
|--------------|---------|
| Material-UI  | latest |
| React        |    latest     |
| browser      |     Chrome    |
| etc          |         |



Issue discussion:

Solution patch:
diff --git a/.size-limit.js b/.size-limit.js
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -27,7 +27,7 @@ module.exports = [
     name: 'The size of all the modules of material-ui.',
     webpack: true,
     path: 'packages/material-ui/build/index.js',
-    limit: '94.7 KB',
+    limit: '94.8 KB',
   },
   {
     name: 'The main bundle of the docs',
diff --git a/docs/src/pages/demos/selection-controls/SwitchLabels.js b/docs/src/pages/demos/selection-controls/SwitchLabels.js
--- a/docs/src/pages/demos/selection-controls/SwitchLabels.js
+++ b/docs/src/pages/demos/selection-controls/SwitchLabels.js
@@ -1,5 +1,4 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 import FormGroup from '@material-ui/core/FormGroup';
 import FormControlLabel from '@material-ui/core/FormControlLabel';
 import Switch from '@material-ui/core/Switch';
@@ -46,8 +45,4 @@ class SwitchLabels extends React.Component {
   }
 }
 
-SwitchLabels.propTypes = {
-  classes: PropTypes.object.isRequired,
-};
-
 export default SwitchLabels;
diff --git a/packages/material-ui/src/FormControl/FormControl.js b/packages/material-ui/src/FormControl/FormControl.js
--- a/packages/material-ui/src/FormControl/FormControl.js
+++ b/packages/material-ui/src/FormControl/FormControl.js
@@ -93,20 +93,11 @@ class FormControl extends React.Component {
     };
   }
 
-  handleFocus = event => {
-    if (this.props.onFocus) {
-      this.props.onFocus(event);
-    }
+  handleFocus = () => {
     this.setState(state => (!state.focused ? { focused: true } : null));
   };
 
-  handleBlur = event => {
-    // The event might be undefined.
-    // For instance, a child component might call this hook
-    // when an input is disabled but still having the focus.
-    if (this.props.onBlur && event) {
-      this.props.onBlur(event);
-    }
+  handleBlur = () => {
     this.setState(state => (state.focused ? { focused: false } : null));
   };
 
@@ -146,8 +137,6 @@ class FormControl extends React.Component {
           className,
         )}
         {...other}
-        onFocus={this.handleFocus}
-        onBlur={this.handleBlur}
       />
     );
   }
@@ -188,14 +177,6 @@ FormControl.propTypes = {
    * If `dense` or `normal`, will adjust vertical spacing of this and contained components.
    */
   margin: PropTypes.oneOf(['none', 'dense', 'normal']),
-  /**
-   * @ignore
-   */
-  onBlur: PropTypes.func,
-  /**
-   * @ignore
-   */
-  onFocus: PropTypes.func,
   /**
    * If `true`, the label will indicate that the input is required.
    */
diff --git a/packages/material-ui/src/Input/Input.js b/packages/material-ui/src/Input/Input.js
--- a/packages/material-ui/src/Input/Input.js
+++ b/packages/material-ui/src/Input/Input.js
@@ -289,7 +289,7 @@ class Input extends React.Component {
   input = null; // Holds the input reference
 
   handleFocus = event => {
-    // Fix an bug with IE11 where the focus/blur events are triggered
+    // Fix a bug with IE11 where the focus/blur events are triggered
     // while the input is disabled.
     if (formControlState(this.props, this.context).disabled) {
       event.stopPropagation();
@@ -300,6 +300,11 @@ class Input extends React.Component {
     if (this.props.onFocus) {
       this.props.onFocus(event);
     }
+
+    const { muiFormControl } = this.context;
+    if (muiFormControl && muiFormControl.onFocus) {
+      muiFormControl.onFocus(event);
+    }
   };
 
   handleBlur = event => {
@@ -307,6 +312,11 @@ class Input extends React.Component {
     if (this.props.onBlur) {
       this.props.onBlur(event);
     }
+
+    const { muiFormControl } = this.context;
+    if (muiFormControl && muiFormControl.onBlur) {
+      muiFormControl.onBlur(event);
+    }
   };
 
   handleChange = event => {
diff --git a/packages/material-ui/src/internal/SwitchBase.js b/packages/material-ui/src/internal/SwitchBase.js
--- a/packages/material-ui/src/internal/SwitchBase.js
+++ b/packages/material-ui/src/internal/SwitchBase.js
@@ -47,6 +47,28 @@ class SwitchBase extends React.Component {
   input = null;
   isControlled = null;
 
+  handleFocus = event => {
+    if (this.props.onFocus) {
+      this.props.onFocus(event);
+    }
+
+    const { muiFormControl } = this.context;
+    if (muiFormControl && muiFormControl.onFocus) {
+      muiFormControl.onFocus(event);
+    }
+  };
+
+  handleBlur = event => {
+    if (this.props.onBlur) {
+      this.props.onBlur(event);
+    }
+
+    const { muiFormControl } = this.context;
+    if (muiFormControl && muiFormControl.onBlur) {
+      muiFormControl.onBlur(event);
+    }
+  };
+
   handleInputChange = event => {
     const checked = event.target.checked;
 
@@ -71,7 +93,9 @@ class SwitchBase extends React.Component {
       inputProps,
       inputRef,
       name,
+      onBlur,
       onChange,
+      onFocus,
       tabIndex,
       type,
       value,
@@ -105,6 +129,8 @@ class SwitchBase extends React.Component {
         disabled={disabled}
         tabIndex={null}
         role={undefined}
+        onFocus={this.handleFocus}
+        onBlur={this.handleBlur}
         {...other}
       >
         {checked ? checkedIcon : icon}
@@ -186,6 +212,10 @@ SwitchBase.propTypes = {
    * @ignore
    */
   name: PropTypes.string,
+  /**
+   * @ignore
+   */
+  onBlur: PropTypes.func,
   /**
    * Callback fired when the state is changed.
    *
@@ -194,6 +224,10 @@ SwitchBase.propTypes = {
    * @param {boolean} checked The `checked` value of the switch
    */
   onChange: PropTypes.func,
+  /**
+   * @ignore
+   */
+  onFocus: PropTypes.func,
   /**
    * @ignore
    */


Test patch:
diff --git a/packages/material-ui/src/FormControl/FormControl.test.js b/packages/material-ui/src/FormControl/FormControl.test.js
--- a/packages/material-ui/src/FormControl/FormControl.test.js
+++ b/packages/material-ui/src/FormControl/FormControl.test.js
@@ -1,5 +1,4 @@
 import React from 'react';
-import { spy } from 'sinon';
 import { assert } from 'chai';
 import { createShallow, getClasses } from '../test-utils';
 import Input from '../Input';
@@ -223,38 +222,23 @@ describe('<FormControl />', () => {
 
       describe('handleFocus', () => {
         it('should set the focused state', () => {
-          assert.strictEqual(wrapper.state('focused'), false);
+          assert.strictEqual(wrapper.state().focused, false);
           muiFormControlContext.onFocus();
-          assert.strictEqual(wrapper.state('focused'), true);
+          assert.strictEqual(wrapper.state().focused, true);
           muiFormControlContext.onFocus();
-          assert.strictEqual(wrapper.state('focused'), true);
-        });
-
-        it('should be able to use a onFocus property', () => {
-          const handleFocus = spy();
-          wrapper.setProps({ onFocus: handleFocus });
-          muiFormControlContext.onFocus();
-          assert.strictEqual(handleFocus.callCount, 1);
+          assert.strictEqual(wrapper.state().focused, true);
         });
       });
 
       describe('handleBlur', () => {
         it('should clear the focused state', () => {
-          assert.strictEqual(wrapper.state('focused'), false);
+          assert.strictEqual(wrapper.state().focused, false);
           muiFormControlContext.onFocus();
-          assert.strictEqual(wrapper.state('focused'), true);
+          assert.strictEqual(wrapper.state().focused, true);
           muiFormControlContext.onBlur();
-          assert.strictEqual(wrapper.state('focused'), false);
+          assert.strictEqual(wrapper.state().focused, false);
           muiFormControlContext.onBlur();
-          assert.strictEqual(wrapper.state('focused'), false);
-        });
-
-        it('should be able to use a onBlur property', () => {
-          const handleBlur = spy();
-          wrapper.setProps({ onBlur: handleBlur });
-          muiFormControlContext.onFocus();
-          muiFormControlContext.onBlur({});
-          assert.strictEqual(handleBlur.callCount, 1);
+          assert.strictEqual(wrapper.state().focused, false);
         });
       });
     });
diff --git a/packages/material-ui/src/Input/Input.test.js b/packages/material-ui/src/Input/Input.test.js
--- a/packages/material-ui/src/Input/Input.test.js
+++ b/packages/material-ui/src/Input/Input.test.js
@@ -264,19 +264,22 @@ describe('<Input />', () => {
     });
 
     describe('callbacks', () => {
-      let handleFilled;
-      let handleEmpty;
-
       beforeEach(() => {
-        handleFilled = spy();
-        handleEmpty = spy();
-        // Mock the input ref
-        wrapper.setProps({ onFilled: handleFilled, onEmpty: handleEmpty });
         wrapper.instance().input = { value: '' };
-        setFormControlContext({ onFilled: spy(), onEmpty: spy() });
+        setFormControlContext({
+          onFilled: spy(),
+          onEmpty: spy(),
+          onFocus: spy(),
+          onBlur: spy(),
+        });
       });
 
       it('should fire the onFilled muiFormControl and props callback when dirtied', () => {
+        const handleFilled = spy();
+        wrapper.setProps({
+          onFilled: handleFilled,
+        });
+
         wrapper.instance().input.value = 'hello';
         wrapper.find('input').simulate('change');
         assert.strictEqual(handleFilled.callCount, 1, 'should have called the onFilled props cb');
@@ -288,6 +291,11 @@ describe('<Input />', () => {
       });
 
       it('should fire the onEmpty muiFormControl and props callback when cleaned', () => {
+        const handleEmpty = spy();
+        wrapper.setProps({
+          onEmpty: handleEmpty,
+        });
+
         wrapper.instance().input.value = '';
         wrapper.find('input').simulate('change');
         assert.strictEqual(handleEmpty.callCount, 1, 'should have called the onEmpty props cb');
@@ -297,6 +305,28 @@ describe('<Input />', () => {
           'should have called the onEmpty muiFormControl cb',
         );
       });
+
+      it('should fire the onFocus muiFormControl', () => {
+        const handleFocus = spy();
+        wrapper.setProps({
+          onFocus: handleFocus,
+        });
+
+        wrapper.find('input').simulate('focus');
+        assert.strictEqual(handleFocus.callCount, 1);
+        assert.strictEqual(muiFormControl.onFocus.callCount, 1);
+      });
+
+      it('should fire the onBlur muiFormControl', () => {
+        const handleBlur = spy();
+        wrapper.setProps({
+          onBlur: handleBlur,
+        });
+
+        wrapper.find('input').simulate('blur');
+        assert.strictEqual(handleBlur.callCount, 1);
+        assert.strictEqual(muiFormControl.onBlur.callCount, 1);
+      });
     });
 
     describe('error', () => {
diff --git a/packages/material-ui/src/internal/SwitchBase.test.js b/packages/material-ui/src/internal/SwitchBase.test.js
--- a/packages/material-ui/src/internal/SwitchBase.test.js
+++ b/packages/material-ui/src/internal/SwitchBase.test.js
@@ -256,37 +256,37 @@ describe('<SwitchBase />', () => {
     };
 
     it('should call onChange exactly once with event', () => {
-      const onChangeSpy = spy();
+      const handleChange = spy();
       const wrapper = mount(
-        <SwitchBaseNaked {...defaultProps} classes={{}} onChange={onChangeSpy} />,
+        <SwitchBaseNaked {...defaultProps} classes={{}} onChange={handleChange} />,
       );
       const instance = wrapper.instance();
       instance.handleInputChange(event);
 
-      assert.strictEqual(onChangeSpy.callCount, 1);
-      assert.strictEqual(onChangeSpy.calledWith(event), true);
+      assert.strictEqual(handleChange.callCount, 1);
+      assert.strictEqual(handleChange.calledWith(event), true);
 
-      onChangeSpy.resetHistory();
+      handleChange.resetHistory();
     });
 
     describe('controlled', () => {
       it('should call onChange once', () => {
         const checked = true;
-        const onChangeSpy = spy();
+        const handleChange = spy();
         const wrapper = mount(
           <SwitchBaseNaked
             {...defaultProps}
             classes={{}}
             checked={checked}
-            onChange={onChangeSpy}
+            onChange={handleChange}
           />,
         );
         const instance = wrapper.instance();
         instance.handleInputChange(event);
 
-        assert.strictEqual(onChangeSpy.callCount, 1);
+        assert.strictEqual(handleChange.callCount, 1);
         assert.strictEqual(
-          onChangeSpy.calledWith(event, !checked),
+          handleChange.calledWith(event, !checked),
           true,
           'call onChange with event and !props.checked',
         );
@@ -296,11 +296,11 @@ describe('<SwitchBase />', () => {
     describe('not controlled no input', () => {
       let checkedMock;
       let wrapper;
-      let onChangeSpy;
+      let handleChange;
 
       before(() => {
-        onChangeSpy = spy();
-        wrapper = mount(<SwitchBaseNaked {...defaultProps} classes={{}} onChange={onChangeSpy} />);
+        handleChange = spy();
+        wrapper = mount(<SwitchBaseNaked {...defaultProps} classes={{}} onChange={handleChange} />);
         checkedMock = true;
         const instance = wrapper.instance();
         wrapper.setState({ checked: checkedMock });
@@ -308,11 +308,11 @@ describe('<SwitchBase />', () => {
       });
 
       it('should call onChange exactly once', () => {
-        assert.strictEqual(onChangeSpy.callCount, 1);
+        assert.strictEqual(handleChange.callCount, 1);
       });
 
       it('should call onChange with right params', () => {
-        assert.strictEqual(onChangeSpy.calledWith(event, !checkedMock), true);
+        assert.strictEqual(handleChange.calledWith(event, !checkedMock), true);
       });
 
       it('should change state.checked !checkedMock', () => {
@@ -387,4 +387,44 @@ describe('<SwitchBase />', () => {
       });
     });
   });
+
+  describe('prop: onFocus', () => {
+    it('should work', () => {
+      const handleFocusProps = spy();
+      const handleFocusContext = spy();
+      const wrapper = mount(
+        <SwitchBaseNaked {...defaultProps} classes={{}} onFocus={handleFocusProps} />,
+        {
+          context: {
+            muiFormControl: {
+              onFocus: handleFocusContext,
+            },
+          },
+        },
+      );
+      wrapper.find('input').simulate('focus');
+      assert.strictEqual(handleFocusProps.callCount, 1);
+      assert.strictEqual(handleFocusContext.callCount, 1);
+    });
+  });
+
+  describe('prop: onBlur', () => {
+    it('should work', () => {
+      const handleFocusProps = spy();
+      const handleFocusContext = spy();
+      const wrapper = mount(
+        <SwitchBaseNaked {...defaultProps} classes={{}} onBlur={handleFocusProps} />,
+        {
+          context: {
+            muiFormControl: {
+              onBlur: handleFocusContext,
+            },
+          },
+        },
+      );
+      wrapper.find('input').simulate('blur');
+      assert.strictEqual(handleFocusProps.callCount, 1);
+      assert.strictEqual(handleFocusContext.callCount, 1);
+    });
+  });
 });