Repository: react-dates/react-dates (language: javascript)

Issue:
Feature Request: Support unselecting a selected date by clicking on it
**react-dates version**
react-dates@15.5.3

**Describe the bug**
This is mostly a feature request. When using the single date picker, it is not possible to unselect a currently selected date by clicking on the already-selected date. This FR was initially [filed in AMP's repo](https://github.com/ampproject/amphtml/issues/25609). [AMP uses the `DayPickerSingleDateController`](https://github.com/ampproject/amphtml/blob/b6313e372fdd1298928e2417dcc616b03288e051/extensions/amp-date-picker/0.1/single-date-picker.js#L33) for building the single date component. 

We would like to support the use-case of unselecting a date by clicking it again. However, instead of building this on our side, we thought it would be a good idea to fully support it on the `react-dates` side since that would be more sustainable and would potentially benefit others.

I've already explored a possible solution for this (see my comments in the [original FR](https://github.com/ampproject/amphtml/issues/25609)). I'd be happy to contribute with a PR for this.

**Is the issue reproducible in Storybook?**
Select a date on the [demo DayPickerSingleDateController](http://airbnb.io/react-dates/?path=/story/daypickersingledatecontroller--default) and then clicking that same date again. The date is not unselected on the second click.


Discussion / hints:
As I mentioned in the issue's description, I'm happy to tackle this. Let me know what you think :)
Thanks, a PR sounds great.
Perfect. If possible, could you please assign me to the issue then? 
Sure, done, altho that’s not usually needed.
Ah good to know, thanks for the heads up.

Fix patch:
diff --git a/examples/DayPickerSingleDateControllerWrapper.jsx b/examples/DayPickerSingleDateControllerWrapper.jsx
index 02d94e931b..44a0a5e1fb 100644
--- a/examples/DayPickerSingleDateControllerWrapper.jsx
+++ b/examples/DayPickerSingleDateControllerWrapper.jsx
@@ -19,6 +19,7 @@ const propTypes = forbidExtraProps({
   initialDate: momentPropTypes.momentObj,
   showInput: PropTypes.bool,
 
+  allowUnselect: PropTypes.bool,
   keepOpenOnDateSelect: PropTypes.bool,
   isOutsideRange: PropTypes.func,
   isDayBlocked: PropTypes.func,
@@ -56,6 +57,7 @@ const defaultProps = {
   showInput: false,
 
   // day presentation and interaction related props
+  allowUnselect: false,
   renderCalendarDay: undefined,
   renderDayContents: null,
   isDayBlocked: () => false,
diff --git a/src/components/DayPickerSingleDateController.jsx b/src/components/DayPickerSingleDateController.jsx
index 89be9f6ed8..de492c3a24 100644
--- a/src/components/DayPickerSingleDateController.jsx
+++ b/src/components/DayPickerSingleDateController.jsx
@@ -34,12 +34,18 @@ import {
 import DayPicker from './DayPicker';
 import getPooledMoment from '../utils/getPooledMoment';
 
+// Default value of the date property. Represents the state
+// when there is no date selected.
+// TODO: use null
+const DATE_UNSET_VALUE = undefined;
+
 const propTypes = forbidExtraProps({
   date: momentPropTypes.momentObj,
   minDate: momentPropTypes.momentObj,
   maxDate: momentPropTypes.momentObj,
   onDateChange: PropTypes.func,
 
+  allowUnselect: PropTypes.bool,
   focused: PropTypes.bool,
   onFocusChange: PropTypes.func,
   onClose: PropTypes.func,
@@ -102,11 +108,12 @@ const propTypes = forbidExtraProps({
 });
 
 const defaultProps = {
-  date: undefined, // TODO: use null
+  date: DATE_UNSET_VALUE,
   minDate: null,
   maxDate: null,
   onDateChange() {},
 
+  allowUnselect: false,
   focused: false,
   onFocusChange() {},
   onClose() {},
@@ -352,16 +359,19 @@ export default class DayPickerSingleDateController extends React.PureComponent {
     if (e) e.preventDefault();
     if (this.isBlocked(day)) return;
     const {
+      allowUnselect,
       onDateChange,
       keepOpenOnDateSelect,
       onFocusChange,
       onClose,
     } = this.props;
 
-    onDateChange(day);
+    const clickedDay = allowUnselect && this.isSelected(day) ? DATE_UNSET_VALUE : day;
+
+    onDateChange(clickedDay);
     if (!keepOpenOnDateSelect) {
       onFocusChange({ focused: false });
-      onClose({ date: day });
+      onClose({ date: clickedDay });
     }
   }
 
diff --git a/stories/DayPickerSingleDateController.js b/stories/DayPickerSingleDateController.js
index e7fd81a5f0..ef05c08305 100644
--- a/stories/DayPickerSingleDateController.js
+++ b/stories/DayPickerSingleDateController.js
@@ -145,6 +145,14 @@ storiesOf('DayPickerSingleDateController', module)
       onNextMonthClick={action('DayPickerSingleDateController::onNextMonthClick')}
     />
   )))
+  .add('with day unselection', withInfo()(() => (
+    <DayPickerSingleDateControllerWrapper
+      onOutsideClick={action('DayPickerSingleDateController::onOutsideClick')}
+      onPrevMonthClick={action('DayPickerSingleDateController::onPrevMonthClick')}
+      onNextMonthClick={action('DayPickerSingleDateController::onNextMonthClick')}
+      allowUnselect
+    />
+  )))
   .add('with custom input', withInfo()(() => (
     <DayPickerSingleDateControllerWrapper
       onOutsideClick={action('DayPickerSingleDateController::onOutsideClick')}


Test patch:
diff --git a/test/components/DayPickerSingleDateController_spec.jsx b/test/components/DayPickerSingleDateController_spec.jsx
index 3a55baa0d7..3bc892f635 100644
--- a/test/components/DayPickerSingleDateController_spec.jsx
+++ b/test/components/DayPickerSingleDateController_spec.jsx
@@ -539,6 +539,40 @@ describe('DayPickerSingleDateController', () => {
         expect(onDateChangeStub.callCount).to.equal(1);
       });
 
+      it('props.onDateChange receives undefined when day selected', () => {
+        const date = moment();
+        const onDateChangeStub = sinon.stub();
+        const wrapper = shallow((
+          <DayPickerSingleDateController
+            onDateChange={onDateChangeStub}
+            onFocusChange={() => {}}
+            date={date}
+            allowUnselect
+          />
+        ));
+        // Click same day as the provided date.
+        wrapper.instance().onDayClick(date);
+        expect(onDateChangeStub.callCount).to.equal(1);
+        expect(onDateChangeStub.getCall(0).args[0]).to.equal(undefined);
+      });
+
+      it('props.onDateChange receives day when allowUnselect is disabled', () => {
+        const date = moment();
+        const onDateChangeStub = sinon.stub();
+        const wrapper = shallow((
+          <DayPickerSingleDateController
+            onDateChange={onDateChangeStub}
+            onFocusChange={() => {}}
+            date={date}
+            allowUnselect={false}
+          />
+        ));
+        // Click same day as the provided date.
+        wrapper.instance().onDayClick(date);
+        expect(onDateChangeStub.callCount).to.equal(1);
+        expect(onDateChangeStub.getCall(0).args[0]).to.equal(date);
+      });
+
       describe('props.keepOpenOnDateSelect is false', () => {
         it('props.onFocusChange is called', () => {
           const onFocusChangeStub = sinon.stub();