Repository: gpbl/react-day-picker (ts)

Issue:
The daypickerinput doesn't work on IE11 anymore.
The selected date isn't populated in the inputbox

Pull request description:
Fix browser issues with blur/focus by adding timeouts
This undo #700 and #691 as the changes weren’t working as expected. Should also fix #712 and #706

Patch:
diff --git a/docs/src/code-samples/examples/input-custom-overlay.js b/docs/src/code-samples/examples/input-custom-overlay.js
index 3e62c1e3e..a74c264b3 100644
--- a/docs/src/code-samples/examples/input-custom-overlay.js
+++ b/docs/src/code-samples/examples/input-custom-overlay.js
@@ -4,11 +4,16 @@ import PropTypes from 'prop-types';
 import DayPickerInput from 'react-day-picker/DayPickerInput';
 import 'react-day-picker/lib/style.css';
 
-function CustomOverlay({ classNames, selectedDay, children }) {
+function CustomOverlay({ classNames, selectedDay, children, ...props }) {
   return (
-    <div className={classNames.overlayWrapper} style={{ marginLeft: -100 }}>
+    <div
+      className={classNames.overlayWrapper}
+      style={{ marginLeft: -100 }}
+      {...props}
+    >
       <div className={classNames.overlay}>
         <h3>Hello day picker!</h3>
+        <button onClick={() => console.log('clicked!')}>button</button>
         <p>
           {selectedDay
             ? `You picked: ${selectedDay.toLocaleDateString()}`
@@ -22,8 +27,8 @@ function CustomOverlay({ classNames, selectedDay, children }) {
 
 CustomOverlay.propTypes = {
   classNames: PropTypes.object.isRequired,
-  selectedDay: PropTypes.oneOfType([Date]),
-  children: PropTypes.number.isRequired,
+  selectedDay: PropTypes.instanceOf(Date),
+  children: PropTypes.node.isRequired,
 };
 
 export default function Example() {
diff --git a/src/DayPickerInput.js b/src/DayPickerInput.js
index 1ea28fbd2..386313b04 100644
--- a/src/DayPickerInput.js
+++ b/src/DayPickerInput.js
@@ -2,7 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import DayPicker from './DayPicker';
-import { isIE } from './Helpers';
 import { isSameMonth, isDate } from './DateUtils';
 import { getModifiersForDay } from './ModifiersUtils';
 import { ESC, TAB } from './keys';
@@ -11,7 +10,36 @@ import { ESC, TAB } from './keys';
 export const HIDE_TIMEOUT = 100;
 
 /**
- * The default function used to format a Date to String, passed to the `format` prop.
+ * The default component used as Overlay.
+ *
+ * @param {Object} props
+ */
+export function OverlayComponent({
+  input,
+  selectedDay,
+  month,
+  children,
+  classNames,
+  ...props
+}) {
+  return (
+    <div className={classNames.overlayWrapper} {...props}>
+      <div className={classNames.overlay}>{children}</div>
+    </div>
+  );
+}
+
+OverlayComponent.propTypes = {
+  input: PropTypes.any,
+  selectedDay: PropTypes.any,
+  month: PropTypes.instanceOf(Date),
+  children: PropTypes.node,
+  classNames: PropTypes.object,
+};
+
+/**
+ * The default function used to format a Date to String, passed to the `format`
+ * prop.
  * @param {Date} d
  * @return {String}
  */
@@ -26,7 +54,8 @@ export function defaultFormat(d) {
 }
 
 /**
- * The default function used to parse a String as Date, passed to the `parse` prop.
+ * The default function used to parse a String as Date, passed to the `parse`
+ * prop.
  * @param {String} str
  * @return {Date}
  */
@@ -105,11 +134,7 @@ export default class DayPickerInput extends React.Component {
     keepFocus: true,
     component: 'input',
     inputProps: {},
-    overlayComponent: ({ children, classNames }) => (
-      <div className={classNames.overlayWrapper}>
-        <div className={classNames.overlay}>{children}</div>
-      </div>
-    ),
+    overlayComponent: OverlayComponent,
     classNames: {
       container: 'DayPickerInput',
       overlayWrapper: 'DayPickerInput-OverlayWrapper',
@@ -175,8 +200,9 @@ export default class DayPickerInput extends React.Component {
   componentWillUnmount() {
     clearTimeout(this.clickTimeout);
     clearTimeout(this.hideTimeout);
-    clearTimeout(this.ieInputFocusTimeout);
-    clearTimeout(this.ieInputBlurTimeout);
+    clearTimeout(this.inputFocusTimeout);
+    clearTimeout(this.inputBlurTimeout);
+    clearTimeout(this.overlayBlurTimeout);
   }
 
   getInitialMonthFromProps(props) {
@@ -217,15 +243,14 @@ export default class DayPickerInput extends React.Component {
 
   input = null;
   daypicker = null;
-  overlayNode = null;
   clickTimeout = null;
   hideTimeout = null;
-  ieInputFocusTimeout = null;
-  ieInputBlutTimeout = null;
+  inputBlurTimeout = null;
+  inputFocusTimeout = null;
 
   /**
-   * Update the component's state and fire the `onDayChange` event
-   * passing the day's modifiers to it.
+   * Update the component's state and fire the `onDayChange` event passing the
+   * day's modifiers to it.
    *
    * @param {Date} day - Will be used for changing the month
    * @param {String} value - Input field value
@@ -265,13 +290,13 @@ export default class DayPickerInput extends React.Component {
   showDayPicker() {
     const { parseDate, format, dayPickerProps } = this.props;
     const { value, showOverlay } = this.state;
-    let month;
-    if (showOverlay === false) {
-      // Reset the current displayed month when showing the overlay
-      month = value
-        ? parseDate(value, format, dayPickerProps.locale) // Use the month in the input field
-        : this.getInitialMonthFromProps(this.props); // Restore the month from the props
+    if (showOverlay) {
+      return;
     }
+    // Reset the current displayed month when showing the overlay
+    const month = value
+      ? parseDate(value, format, dayPickerProps.locale) // Use the month in the input field
+      : this.getInitialMonthFromProps(this.props); // Restore the month from the props
     this.setState({
       showOverlay: true,
       month: month || this.state.month,
@@ -284,15 +309,10 @@ export default class DayPickerInput extends React.Component {
    * @memberof DayPickerInput
    */
   hideDayPicker() {
-    this.setState({ showOverlay: false });
-  }
-
-  showOverlayBasedOnTargetNode(node) {
-    if (this.overlayNode && this.overlayNode.contains(node)) {
-      this.showDayPicker();
-    } else {
-      this.hideDayPicker();
+    if (this.state.showOverlay === false) {
+      return;
     }
+    this.setState({ showOverlay: false });
   }
 
   hideAfterDayClick() {
@@ -312,22 +332,29 @@ export default class DayPickerInput extends React.Component {
 
   handleInputFocus(e) {
     this.showDayPicker();
+    // Set `overlayHasFocus` after a timeout so the overlay can be hidden when
+    // the input is blurred
+    this.inputFocusTimeout = setTimeout(() => {
+      this.overlayHasFocus = false;
+    }, 2);
     if (this.props.inputProps.onFocus) {
       e.persist();
       this.props.inputProps.onFocus(e);
     }
   }
 
+  // When the input is blurred, the overlay should disappear. However the input
+  // is blurred also when the user interacts with the overlay (e.g. the overlay
+  // get the focus by clicking it). In these cases, the overlay should not be
+  // hidden. There are different approaches to avoid hiding the overlay when
+  // this happens, but the only cross-browser hack we’ve found is to set all
+  // these timeouts in code before changing `overlayHasFocus`.
   handleInputBlur(e) {
-    const target = e.relatedTarget;
-    if (!isIE()) {
-      this.showOverlayBasedOnTargetNode(target);
-    } else {
-      this.ieInputBlurTimeout = setTimeout(
-        () => this.showOverlayBasedOnTargetNode(target),
-        HIDE_TIMEOUT
-      );
-    }
+    this.inputBlurTimeout = setTimeout(() => {
+      if (!this.overlayHasFocus) {
+        this.hideDayPicker();
+      }
+    }, 1);
     if (this.props.inputProps.onBlur) {
       e.persist();
       this.props.inputProps.onBlur(e);
@@ -339,30 +366,16 @@ export default class DayPickerInput extends React.Component {
       return;
     }
     e.preventDefault();
-    if (!isIE()) {
-      this.input.focus();
-    } else {
-      // Fix behavior in Internet Explorer
-      // See https://github.com/gpbl/react-day-picker/pull/691
-      this.ieInputFocusTimeout = setTimeout(() => {
-        this.input.focus();
-        // Reset the hide timeout for reasons
-        // TODO: add a comment specifying why we need this
-        if (this.hideTimeout) {
-          this.hideTimeout = setTimeout(() => {
-            this.hideDayPicker();
-            this.hideTimeout = null;
-          }, HIDE_TIMEOUT);
-        }
-      }, HIDE_TIMEOUT);
-    }
+    this.input.focus();
+    this.overlayHasFocus = true;
   }
 
-  handleOverlayBlur(e) {
-    this.setState({
-      showOverlay:
-        this.overlayNode && this.overlayNode.contains(e.relatedTarget),
-    });
+  handleOverlayBlur() {
+    // We need to set a timeout otherwise IE11 will hide the overlay when
+    // focusing it
+    this.overlayBlurTimeout = setTimeout(() => {
+      this.overlayHasFocus = false;
+    }, 3);
   }
 
   handleInputChange(e) {
@@ -395,6 +408,8 @@ export default class DayPickerInput extends React.Component {
   handleInputKeyDown(e) {
     if (e.keyCode === TAB) {
       this.hideDayPicker();
+    } else {
+      this.showDayPicker();
     }
     if (this.props.inputProps.onKeyDown) {
       e.persist();
@@ -403,9 +418,10 @@ export default class DayPickerInput extends React.Component {
   }
 
   handleInputKeyUp(e) {
-    // Hide the overlay if the ESC key is pressed
     if (e.keyCode === ESC) {
       this.hideDayPicker();
+    } else {
+      this.showDayPicker();
     }
     if (this.props.inputProps.onKeyUp) {
       e.persist();
@@ -498,28 +514,25 @@ export default class DayPickerInput extends React.Component {
     }
     const Overlay = this.props.overlayComponent;
     return (
-      <span
+      <Overlay
+        classNames={classNames}
+        month={this.state.month}
+        selectedDay={selectedDay}
+        input={this.input}
+        tabIndex={0} // tabIndex is necessary to catch focus/blur events on Safari
         onFocus={this.handleOverlayFocus}
-        ref={el => (this.overlayNode = el)}
         onBlur={this.handleOverlayBlur}
       >
-        <Overlay
-          classNames={classNames}
+        <DayPicker
+          ref={el => (this.daypicker = el)}
+          onTodayButtonClick={onTodayButtonClick}
+          {...dayPickerProps}
           month={this.state.month}
-          selectedDay={selectedDay}
-          input={this.input}
-        >
-          <DayPicker
-            ref={el => (this.daypicker = el)}
-            onTodayButtonClick={onTodayButtonClick}
-            {...dayPickerProps}
-            month={this.state.month}
-            selectedDays={selectedDay}
-            onDayClick={this.handleDayClick}
-            onMonthChange={this.handleMonthChange}
-          />
-        </Overlay>
-      </span>
+          selectedDays={selectedDay}
+          onDayClick={this.handleDayClick}
+          onMonthChange={this.handleMonthChange}
+        />
+      </Overlay>
     );
   }
 
diff --git a/src/Helpers.js b/src/Helpers.js
index a7741e180..8636ea19b 100644
--- a/src/Helpers.js
+++ b/src/Helpers.js
@@ -144,8 +144,3 @@ export function nodeListToArray(nodeList) {
 export function hasOwnProp(obj, prop) {
   return Object.prototype.hasOwnProperty.call(obj, prop);
 }
-
-export function isIE() {
-  // eslint-disable-next-line no-undef
-  return /* @cc_on!@ */ false || !!document.documentMode;
-}


Test patch:
diff --git a/test/daypickerinput/events.js b/test/daypickerinput/events.js
index 76ab8caeb..174a27751 100644
--- a/test/daypickerinput/events.js
+++ b/test/daypickerinput/events.js
@@ -64,13 +64,29 @@ describe('DayPickerInput', () => {
         expect(document.activeElement).not.toEqual(instance.input);
       });
     });
+    describe('overlayblur', () => {
+      it('should set overlayHasFocus to false', done => {
+        const wrapper = mount(<DayPickerInput showOverlay keepFocus />);
+        wrapper.find('.DayPickerInput-Overlay').simulate('focus');
+        wrapper.find('.DayPickerInput-Overlay').simulate('blur');
+        setTimeout(() => {
+          wrapper.update();
+          expect(wrapper.instance().overlayHasFocus).toBe(false);
+          done();
+        }, 100);
+      });
+    });
 
     describe('blur', () => {
-      it('should hide the overlay when the input is blurred', () => {
+      it('should hide the overlay when the input is blurred', done => {
         const wrapper = mount(<DayPickerInput value="12/15/2017" />);
         wrapper.find('input').simulate('focus');
         wrapper.find('input').simulate('blur');
-        expect(wrapper.find('.DayPicker')).toHaveLength(0);
+        setTimeout(() => {
+          wrapper.update();
+          expect(wrapper.find('.DayPicker')).toHaveLength(0);
+          done();
+        }, 100);
       });
       it('should call `onBlur` event handler', () => {
         const onBlur = jest.fn();
@@ -79,16 +95,6 @@ describe('DayPickerInput', () => {
         expect(onBlur).toHaveBeenCalledTimes(1);
       });
     });
-
-    describe('overlayblur', () => {
-      it('should hide the overlay', () => {
-        const wrapper = mount(<DayPickerInput showOverlay keepFocus />);
-        wrapper.find('.DayPickerInput-Overlay').simulate('focus');
-        wrapper.find('.DayPickerInput-Overlay').simulate('blur');
-        expect(wrapper.find('.DayPicker')).toHaveLength(0);
-      });
-    });
-
     describe('change', () => {
       it('should call `onChange` event handler', () => {
         const onChange = jest.fn();
diff --git a/test/daypickerinput/rendering.js b/test/daypickerinput/rendering.js
index d81c3b7f1..754863008 100644
--- a/test/daypickerinput/rendering.js
+++ b/test/daypickerinput/rendering.js
@@ -72,6 +72,7 @@ describe('DayPickerInput', () => {
     });
     it('should hide the DayPicker', () => {
       const wrapper = shallow(<DayPickerInput />);
+      wrapper.setState({ showOverlay: false });
       wrapper.instance().showDayPicker();
       wrapper.update();
       expect(wrapper.find(DayPicker)).toHaveLength(1);
@@ -79,6 +80,17 @@ describe('DayPickerInput', () => {
       wrapper.instance().hideDayPicker();
       expect(wrapper.find('.DayPicker')).toHaveLength(0);
     });
+    it('should hide the DayPicker', () => {
+      const wrapper = shallow(<DayPickerInput />);
+      wrapper.setState({ showOverlay: true });
+      wrapper.instance().showDayPicker();
+      wrapper.update();
+      expect(wrapper.find(DayPicker)).toHaveLength(1);
+      wrapper.update();
+      wrapper.setState({ showOverlay: false });
+      wrapper.instance().hideDayPicker();
+      expect(wrapper.find('.DayPicker')).toHaveLength(0);
+    });
     it('should pass props to the DayPicker', () => {
       const instance = mount(
         <DayPickerInput
@@ -114,7 +126,7 @@ describe('DayPickerInput', () => {
       mount(<DayPickerInput />, { attachTo: container });
       const spy = jest.spyOn(window, 'clearTimeout');
       ReactDOM.unmountComponentAtNode(container);
-      expect(spy).toHaveBeenCalledTimes(4);
+      expect(spy).toHaveBeenCalledTimes(5);
       spy.mockRestore();
     });
     it('should set today when clicking on today button', () => {