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

Issue:
Clicking the today button should set the field value to today
We can make that clicking the today button in the day picker input will set automatically the field's value to the today. See https://github.com/gpbl/react-day-picker/issues/528.

### Checklist

- [x] Close #529 
- [x] Add feature
- [x] Update input button example

Pull request:
Clicking the today button should set the field value to today
Close #530 

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8806285a2..a85faf7b6 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -86,6 +86,5 @@ workflows:
                 - master   
       - build-docs:
           filters:
-            branches:
-              only: 
-                - master   
+            tags:
+              only: /^v.*/  
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1124df5c3..5acf4a7df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@ _The following changes are not released yet, but are on the master branch and wi
 
 **DayPickerInput**
 
+* New: clicking the Today Button will set the input value to today ([#561](https://github.com/gpbl/react-day-picker/issues/561))
+
 * Fixed: allow to change `numberOfMonths`, `selectedDays` props from `dayPickerProps` ([#513](https://github.com/gpbl/react-day-picker/issues/513), [#531](https://github.com/gpbl/react-day-picker/issues/531) by [hydrognomik](https://github.com/hydrognomik))
 * Fixed: some modifiers were not passed down when using a custom `classNames` ([#517](https://github.com/gpbl/react-day-picker/issues/517), [#504](https://github.com/gpbl/react-day-picker/issues/504) by [tume](https://github.com/tume))
 * Fixed: focus behavior on Firefox ([#525](https://github.com/gpbl/react-day-picker/issues/525) by [martinmosko](https://github.com/martinmosko))
diff --git a/src/.eslintrc b/src/.eslintrc
index 35b40b17b..3a729adc1 100644
--- a/src/.eslintrc
+++ b/src/.eslintrc
@@ -1,6 +1,7 @@
 {
   "rules": {
-    "jsx-a11y/no-static-element-interactions" : 0,
-    "react/forbid-prop-types": 0
+    "class-methods-use-this": "off",
+    "jsx-a11y/no-static-element-interactions": "off",
+    "react/forbid-prop-types": "off"
   }
-}
\ No newline at end of file
+}
diff --git a/src/DayPickerInput.js b/src/DayPickerInput.js
index 299d16a8d..46fe361f3 100644
--- a/src/DayPickerInput.js
+++ b/src/DayPickerInput.js
@@ -7,30 +7,11 @@ import { getModifiersForDay } from './ModifiersUtils';
 import MomentLocaleUtils from './addons/MomentLocaleUtils';
 import { ESC } from './keys';
 
+// When clicking on a day cell, overlay will be hidden after this timeout
 export const HIDE_TIMEOUT = 100;
 
-function getStateFromProps(props) {
-  const { dayPickerProps, format, value } = props;
-  let month;
-  if (value) {
-    const m = moment(value, format, true);
-    if (m.isValid()) {
-      month = m.toDate();
-    }
-  } else {
-    month = dayPickerProps.initialMonth || dayPickerProps.month || new Date();
-  }
-
-  return {
-    value,
-    month,
-    selectedDays: dayPickerProps.selectedDays,
-  };
-}
-
 export default class DayPickerInput extends React.Component {
   static propTypes = {
-    // eslint-disable-next-line react/no-unused-prop-types
     value: PropTypes.string,
 
     format: PropTypes.oneOfType([
@@ -73,33 +54,34 @@ export default class DayPickerInput extends React.Component {
 
   constructor(props) {
     super(props);
-    this.state = getStateFromProps(props);
+    this.state = this.getStateFromProps(props);
     this.state.showOverlay = false;
+    this.hideAfterDayClick = this.hideAfterDayClick.bind(this);
+    this.handleContainerMouseDown = this.handleContainerMouseDown.bind(this);
+    this.handleInputClick = this.handleInputClick.bind(this);
+    this.handleInputFocus = this.handleInputFocus.bind(this);
+    this.handleInputBlur = this.handleInputBlur.bind(this);
+    this.handleInputChange = this.handleInputChange.bind(this);
+    this.handleInputOnKeyUp = this.handleInputOnKeyUp.bind(this);
+    this.handleDayClick = this.handleDayClick.bind(this);
   }
 
   componentWillReceiveProps(nextProps) {
     const { dayPickerProps, value } = this.props;
-    const { dayPickerProps: nextDayPickerProps, value: nextValue } = nextProps;
-
-    const hasDifferentValue = nextValue !== value;
-
-    const willUpdateDayPickerMonth =
-      nextDayPickerProps.month && dayPickerProps.month;
-
-    const newMonthProp = nextDayPickerProps.month && !dayPickerProps.month;
-
-    const shouldDisplayAnotherMonth =
-      newMonthProp ||
-      (willUpdateDayPickerMonth &&
-        (nextDayPickerProps.month.getFullYear() !==
-          dayPickerProps.month.getFullYear() ||
-          nextDayPickerProps.month.getMonth() !==
-            dayPickerProps.month.getMonth()));
-
-    if (hasDifferentValue && !shouldDisplayAnotherMonth) {
-      this.setState(getStateFromProps(nextProps));
-    } else if (shouldDisplayAnotherMonth) {
-      this.setState({ month: nextDayPickerProps.month });
+    const currentMonth = dayPickerProps.month;
+    const nextMonth = nextProps.dayPickerProps.month;
+    const nextValue = nextProps.value;
+
+    const shouldUpdateMonth =
+      (nextMonth && !currentMonth) ||
+      (nextMonth &&
+        (nextMonth.getFullYear() !== currentMonth.getFullYear() ||
+          nextMonth.getMonth() !== currentMonth.getMonth()));
+
+    if (nextValue !== value && !shouldUpdateMonth) {
+      this.setState(this.getStateFromProps(nextProps));
+    } else if (shouldUpdateMonth) {
+      this.setState({ month: nextMonth });
     }
   }
 
@@ -109,12 +91,62 @@ export default class DayPickerInput extends React.Component {
     clearTimeout(this.blurTimeout);
   }
 
+  getStateFromProps(props) {
+    const { dayPickerProps, format, value } = props;
+    let month;
+    if (value) {
+      // Display the month specified in the value prop
+      const m = moment(value, format, true);
+      if (m.isValid()) {
+        month = m.toDate();
+      }
+    } else {
+      // Otherwise display the month coming from `dayPickerProps` or the current month
+      month = dayPickerProps.initialMonth || dayPickerProps.month || new Date();
+    }
+    return {
+      value,
+      month,
+      selectedDays: dayPickerProps.selectedDays,
+    };
+  }
+
   input = null;
   daypicker = null;
   clickedInside = false;
   clickTimeout = null;
   hideTimeout = null;
 
+  /**
+   * 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
+   * @private
+   */
+  updateState(day, value) {
+    const { dayPickerProps, onDayChange } = this.props;
+    this.setState({ month: day, value }, () => {
+      if (!onDayChange) {
+        return;
+      }
+      const modifiersObj = {
+        disabled: dayPickerProps.disabledDays,
+        selected: dayPickerProps.selectedDays,
+        ...dayPickerProps.modifiers,
+      };
+      const modifiers = getModifiersForDay(
+        day,
+        modifiersObj
+      ).reduce((obj, modifier) => {
+        const newObj = { ...obj };
+        newObj[modifier] = true;
+        return newObj;
+      }, {});
+      onDayChange(moment(day), modifiers);
+    });
+  }
+
   /**
    * Show the Day Picker overlay.
    * 
@@ -137,123 +169,103 @@ export default class DayPickerInput extends React.Component {
     });
   }
 
-  hideAfterDayClick = () => {
+  hideAfterDayClick() {
     if (!this.props.hideOnDayClick) {
       return;
     }
-    this.hideTimeout = setTimeout(
-      () => this.hideDayPicker(),
-      HIDE_TIMEOUT // give a timeout to show the clicked day
-    );
-  };
+    this.hideTimeout = setTimeout(() => this.hideDayPicker(), HIDE_TIMEOUT);
+  }
 
-  handleContainerMouseDown = () => {
+  handleContainerMouseDown() {
     this.clickedInside = true;
-    // The input's onBlur method is called from a queue right after onMouseDown event.
-    // setTimeout adds another callback in the queue, but is called later than onBlur event
+    // The input's onBlur method is called from a queue right after the onMouseDown event.
+    // setTimeout adds another callback in the queue, which is called after the onBlur event.
     this.clickTimeout = setTimeout(() => {
       this.clickedInside = false;
     }, 0);
-  };
+  }
 
-  handleClick = e => {
+  handleInputClick(e) {
     this.showDayPicker();
     if (this.props.onClick) {
       e.persist();
       this.props.onClick(e);
     }
-  };
+  }
 
-  handleFocus = e => {
+  handleInputFocus(e) {
     this.showDayPicker();
     if (this.props.onFocus) {
       e.persist();
       this.props.onFocus(e);
     }
-  };
+  }
 
-  handleBlur = e => {
+  handleInputBlur(e) {
     this.setState({
       showOverlay: this.clickedInside,
     });
-
     // Force input's focus if blur event was caused by clicking inside the overlay
     if (this.clickedInside) {
       this.blurTimeout = setTimeout(() => this.input.focus(), 0);
     }
-
     if (this.props.onBlur) {
       e.persist();
       this.props.onBlur(e);
     }
-  };
-
-  handleChange = e => {
-    const { value } = e.target;
-    const { format, dayPickerProps, onDayChange, onChange } = this.props;
-    const m = moment(value, format, true);
+  }
 
+  handleInputChange(e) {
+    const { onChange, onDayChange, format } = this.props;
     if (onChange) {
       e.persist();
       onChange(e);
     }
-
+    const { value } = e.target;
+    const m = moment(value, format, true);
     if (value.trim() === '') {
       this.setState({ value });
-      if (this.props.onDayChange) {
-        this.props.onDayChange(undefined, {});
+      if (onDayChange) {
+        onDayChange(undefined, {});
       }
       return;
     }
-
     if (!m.isValid()) {
       this.setState({ value });
       return;
     }
-
     const day = m.toDate();
-    this.setState({ month: day, value }, () => {
-      if (!onDayChange) {
-        return;
-      }
-      const modifiersObj = {
-        disabled: dayPickerProps.disabledDays,
-        selected: dayPickerProps.selectedDays,
-        ...dayPickerProps.modifiers,
-      };
-      const modifiers = getModifiersForDay(
-        day,
-        modifiersObj
-      ).reduce((obj, modifier) => {
-        const newObj = { ...obj };
-        newObj[modifier] = true;
-        return newObj;
-      }, {});
-      this.props.onDayChange(m, modifiers);
-    });
-  };
+    this.updateState(day, value);
+  }
 
-  handleOnKeyUp = e => {
-    this.setState({
-      showOverlay: e.keyCode !== ESC,
-    });
+  handleInputOnKeyUp(e) {
+    // Hide the overlay if the ESC key is pressed
+    this.setState({ showOverlay: e.keyCode !== ESC });
     if (this.props.onKeyUp) {
       e.persist();
       this.props.onKeyUp(e);
     }
-  };
+  }
 
-  handleDayClick = (day, modifiers, e) => {
-    if (this.props.dayPickerProps.onDayClick) {
-      this.props.dayPickerProps.onDayClick(day, modifiers, e);
+  handleDayClick(day, modifiers, e) {
+    const {
+      clickUnselectsDay,
+      dayPickerProps,
+      onDayChange,
+      format,
+    } = this.props;
+    if (dayPickerProps.onDayClick) {
+      dayPickerProps.onDayClick(day, modifiers, e);
     }
 
+    // Do nothing if the day is disabled
     if (modifiers.disabled) {
-      // Do nothing if the day is disabled
       return;
     }
-    if (modifiers.selected && this.props.clickUnselectsDay) {
-      // Unselect the day
+
+    // If the clicked day is already selected, remove the clicked day
+    // from the selected days and empty the field value
+    if (modifiers.selected && clickUnselectsDay) {
       let { selectedDays } = this.state;
       if (Array.isArray(selectedDays)) {
         selectedDays = selectedDays.slice(0);
@@ -263,30 +275,24 @@ export default class DayPickerInput extends React.Component {
         selectedDays = null;
       }
       this.setState({ value: '', selectedDays }, this.hideAfterDayClick);
-      if (this.props.onDayChange) {
-        this.props.onDayChange(undefined, modifiers);
+      if (onDayChange) {
+        onDayChange(undefined, modifiers);
       }
       return;
     }
 
-    const { format } = this.props;
     const m = moment(day);
-    this.setState(
-      {
-        value: m.format(typeof format === 'string' ? format : format[0]),
-        month: day,
-      },
-      () => {
-        if (this.props.onDayChange) {
-          this.props.onDayChange(m, modifiers);
-        }
-        this.hideAfterDayClick();
+    const value = m.format(typeof format === 'string' ? format : format[0]);
+    this.setState({ value, month: day }, () => {
+      if (onDayChange) {
+        onDayChange(m, modifiers);
       }
-    );
-  };
+      this.hideAfterDayClick();
+    });
+  }
 
   renderOverlay() {
-    const { format } = this.props;
+    const { format, classNames, dayPickerProps } = this.props;
     const { selectedDays, value } = this.state;
     let selectedDay;
     if (!selectedDays && value) {
@@ -298,14 +304,22 @@ export default class DayPickerInput extends React.Component {
       selectedDay = selectedDays;
     }
 
+    let onTodayButtonClick;
+    if (dayPickerProps.todayButton) {
+      // Set the current day when clicking the today button
+      onTodayButtonClick = () =>
+        this.updateState(new Date(), moment().format(this.props.format));
+    }
+
     return (
-      <div className={this.props.classNames.overlayWrapper}>
-        <div className={this.props.classNames.overlay}>
+      <div className={classNames.overlayWrapper}>
+        <div className={classNames.overlay}>
           <DayPicker
             ref={el => (this.daypicker = el)}
             localeUtils={MomentLocaleUtils}
             fixedWeeks
-            {...this.props.dayPickerProps}
+            onTodayButtonClick={onTodayButtonClick}
+            {...dayPickerProps}
             month={this.state.month}
             selectedDays={selectedDay}
             onDayClick={this.handleDayClick}
@@ -333,11 +347,11 @@ export default class DayPickerInput extends React.Component {
           ref: el => (this.input = el),
           ...inputProps,
           value: this.state.value,
-          onChange: this.handleChange,
-          onFocus: this.handleFocus,
-          onBlur: this.handleBlur,
-          onKeyUp: this.handleOnKeyUp,
-          onClick: this.handleClick,
+          onChange: this.handleInputChange,
+          onFocus: this.handleInputFocus,
+          onBlur: this.handleInputBlur,
+          onKeyUp: this.handleInputOnKeyUp,
+          onClick: this.handleInputClick,
         })}
         {this.state.showOverlay && this.renderOverlay()}
       </div>


Test patch:
diff --git a/test/daypickerinput/rendering.js b/test/daypickerinput/rendering.js
index 943c1f62c..03d0a0fde 100644
--- a/test/daypickerinput/rendering.js
+++ b/test/daypickerinput/rendering.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import { shallow, mount } from 'enzyme';
+import moment from 'moment';
 
 import DayPickerInput from '../../src/DayPickerInput';
 import DayPicker from '../../src/DayPicker';
@@ -124,6 +125,25 @@ describe('DayPickerInput', () => {
       expect(wrapper.find('.DayPicker-Day--selected')).toHaveLength(1);
       expect(wrapper.find('.DayPicker-Day--selected')).toHaveText('15');
     });
+    it('should clear timeouts when component unmounts', () => {
+      const container = document.createElement('div');
+      mount(<DayPickerInput />, { attachTo: container });
+      const spy = jest.spyOn(window, 'clearTimeout');
+      ReactDOM.unmountComponentAtNode(container);
+      expect(spy).toHaveBeenCalledTimes(3);
+      spy.mockRestore();
+    });
+    it('should set today when clicking on today button', () => {
+      const wrapper = mount(
+        <DayPickerInput dayPickerProps={{ todayButton: 'Today' }} />
+      );
+      wrapper.instance().showDayPicker();
+      wrapper.update();
+      wrapper.find('.DayPicker-TodayButton').simulate('click');
+      expect(wrapper.find('input')).toHaveProp('value', moment().format('L'));
+    });
+  });
+  describe('updating props', () => {
     it('should update the current value when `value` prop is updated', () => {
       const wrapper = mount(<DayPickerInput value="12/15/2017" />);
       wrapper.setProps({ value: '01/10/2018' });
@@ -135,7 +155,12 @@ describe('DayPickerInput', () => {
       expect(wrapper.instance().state.value).toBe('12/15/2017');
     });
     it("should update the displayed month when `dayPickerProps.month`'s month is updated", () => {
-      const wrapper = mount(<DayPickerInput value="12/15/2017" />);
+      const wrapper = mount(
+        <DayPickerInput
+          dayPickerProps={{ month: new Date(2017, 9) }}
+          value="12/15/2017"
+        />
+      );
       wrapper.instance().showDayPicker();
       wrapper.update();
       expect(wrapper.find('.DayPicker-Caption').first()).toHaveText(
@@ -148,7 +173,12 @@ describe('DayPickerInput', () => {
       );
     });
     it("should update the displayed month when `dayPickerProps.month`'s year is updated", () => {
-      const wrapper = mount(<DayPickerInput value="12/15/2017" />);
+      const wrapper = mount(
+        <DayPickerInput
+          dayPickerProps={{ month: new Date(2018, 10) }}
+          value="12/15/2017"
+        />
+      );
       wrapper.instance().showDayPicker();
       wrapper.update();
       expect(wrapper.find('.DayPicker-Caption').first()).toHaveText(
@@ -160,19 +190,23 @@ describe('DayPickerInput', () => {
         'November 2016'
       );
     });
-    it('should not update the current value when other props are updated', () => {
+    it('should not change state if month did not change', () => {
       const wrapper = mount(<DayPickerInput value="12/15/2017" />);
-      wrapper.setProps({ dayPickerProps: {} });
+      wrapper.instance().showDayPicker();
+      wrapper.update();
+      expect(wrapper.find('.DayPicker-Caption').first()).toHaveText(
+        'December 2017'
+      );
+      wrapper.setProps({ dayPickerProps: { month: new Date(2017, 11) } });
       expect(wrapper.instance().state.value).toBe('12/15/2017');
+      expect(wrapper.find('.DayPicker-Caption').first()).toHaveText(
+        'December 2017'
+      );
     });
-
-    it('should clear timeouts when component unmounts', () => {
-      const container = document.createElement('div');
-      mount(<DayPickerInput />, { attachTo: container });
-      const spy = jest.spyOn(window, 'clearTimeout');
-      ReactDOM.unmountComponentAtNode(container);
-      expect(spy).toHaveBeenCalledTimes(3);
-      spy.mockRestore();
+    it('should not update the current value when other props are updated', () => {
+      const wrapper = mount(<DayPickerInput value="12/15/2017" />);
+      wrapper.setProps({ dayPickerProps: {}, value: '12/15/2017' });
+      expect(wrapper.instance().state.value).toBe('12/15/2017');
     });
   });
 });