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

Issue:
Next and Previous buttons cannot be operated via keyboard.
The [next and previous buttons](https://github.com/gpbl/react-day-picker/blob/b8184a65b39f74cbbed5bfaaba693812a4bdb972/src/Navbar.js#L38-L66) are implemented using `<span>`, thus they are not operable via `enter` or `space` press. Please consider it replacing with `<button>`.

Pros
- `tabindex=0` is no more needed, `<button>` is a focusable element
- `role=button` is no more needed, `<button>` exposes the role by default
-  `previousClickHandler` and `nextClickHandler` will get invoked when the buttons are activated using keyboard.

Cons
- might need to update the styles


Pull request description:
Make Next/Previous button working with keyboard
Closes #430 

Patch:
diff --git a/src/DayPicker.js b/src/DayPicker.js
index 3a8cd07de..111d8c851 100644
--- a/src/DayPicker.js
+++ b/src/DayPicker.js
@@ -9,7 +9,7 @@ import * as DateUtils from './DateUtils';
 import * as LocaleUtils from './LocaleUtils';
 import classNames from './classNames';
 
-import keys from './keys';
+import { ENTER, SPACE, LEFT, UP, DOWN, RIGHT } from './keys';
 import PropTypes, { ModifierPropType } from './PropTypes';
 
 export default class DayPicker extends Component {
@@ -335,16 +335,16 @@ export default class DayPicker extends Component {
     e.persist();
 
     switch (e.keyCode) {
-      case keys.LEFT:
+      case LEFT:
         this.showPreviousMonth();
         break;
-      case keys.RIGHT:
+      case RIGHT:
         this.showNextMonth();
         break;
-      case keys.UP:
+      case UP:
         this.showPreviousYear();
         break;
-      case keys.DOWN:
+      case DOWN:
         this.showNextYear();
         break;
       default:
@@ -359,24 +359,24 @@ export default class DayPicker extends Component {
   handleDayKeyDown = (day, modifiers, e) => {
     e.persist();
     switch (e.keyCode) {
-      case keys.LEFT:
+      case LEFT:
         Helpers.cancelEvent(e);
         this.focusPreviousDay(e.target);
         break;
-      case keys.RIGHT:
+      case RIGHT:
         Helpers.cancelEvent(e);
         this.focusNextDay(e.target);
         break;
-      case keys.UP:
+      case UP:
         Helpers.cancelEvent(e);
         this.focusPreviousWeek(e.target);
         break;
-      case keys.DOWN:
+      case DOWN:
         Helpers.cancelEvent(e);
         this.focusNextWeek(e.target);
         break;
-      case keys.ENTER:
-      case keys.SPACE:
+      case ENTER:
+      case SPACE:
         Helpers.cancelEvent(e);
         if (this.props.onDayClick) {
           this.handleDayClick(day, modifiers, e);
diff --git a/src/DayPickerInput.js b/src/DayPickerInput.js
index 673ccc9dd..ead97914f 100644
--- a/src/DayPickerInput.js
+++ b/src/DayPickerInput.js
@@ -4,7 +4,7 @@ import moment from 'moment'; // eslint-disable-line import/no-extraneous-depende
 
 import DayPicker from './DayPicker';
 import { getModifiersForDay } from './ModifiersUtils';
-import keys from './keys';
+import { ESC } from './keys';
 
 export const HIDE_TIMEOUT = 100;
 
@@ -214,7 +214,7 @@ export default class DayPickerInput extends React.Component {
 
   handleOnKeyUp = e => {
     this.setState({
-      showOverlay: e.keyCode !== keys.ESC,
+      showOverlay: e.keyCode !== ESC,
     });
     if (this.props.onKeyUp) {
       e.persist();
diff --git a/src/Navbar.js b/src/Navbar.js
index 9f377b0bd..7bd28728b 100644
--- a/src/Navbar.js
+++ b/src/Navbar.js
@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import PropTypes from './PropTypes';
 import defaultClassNames from './classNames';
+import { SPACE, ENTER } from './keys';
 
 export default class Navbar extends Component {
   static defaultProps = {
@@ -13,6 +14,7 @@ export default class Navbar extends Component {
     showPreviousButton: true,
     showNextButton: true,
   };
+
   shouldComponentUpdate(nextProps) {
     return (
       nextProps.labels !== this.props.labels ||
@@ -21,19 +23,66 @@ export default class Navbar extends Component {
     );
   }
 
+  handleNextClick = () => {
+    if (this.props.onNextClick) {
+      this.props.onNextClick();
+    }
+  };
+
+  handlePreviousClick = () => {
+    if (this.props.onPreviousClick) {
+      this.props.onPreviousClick();
+    }
+  };
+
+  handleNextKeyDown = e => {
+    if (e.keyCode !== ENTER && e.keyCode !== SPACE) {
+      return;
+    }
+    this.handleNextClick();
+  };
+
+  handlePreviousKeyDown = e => {
+    if (e.keyCode !== ENTER && e.keyCode !== SPACE) {
+      return;
+    }
+    this.handlePreviousClick();
+  };
+
   render() {
     const {
       classNames,
       className,
       showPreviousButton,
       showNextButton,
-      onPreviousClick,
-      onNextClick,
       labels,
       dir,
     } = this.props;
-    const previousClickHandler = dir === 'rtl' ? onNextClick : onPreviousClick;
-    const nextClickHandler = dir === 'rtl' ? onPreviousClick : onNextClick;
+
+    let previousClickHandler;
+    let nextClickHandler;
+    let previousKeyDownHandler;
+    let nextKeyDownHandler;
+
+    if (dir === 'rtl') {
+      previousClickHandler = this.handleNextClick;
+      nextClickHandler = this.handlePreviousClick;
+      previousKeyDownHandler = this.handleNextKeyDown;
+      nextKeyDownHandler = this.handlePreviousKeyDown;
+    } else {
+      previousClickHandler = this.handlePreviousClick;
+      nextClickHandler = this.handleNextClick;
+      previousKeyDownHandler = this.handlePreviousKeyDown;
+      nextKeyDownHandler = this.handleNextKeyDown;
+    }
+
+    const previousClassName = showPreviousButton
+      ? classNames.navButtonPrev
+      : `${classNames.navButtonPrev} ${classNames.navButtonInteractionDisabled}`;
+
+    const nextClassName = showNextButton
+      ? classNames.navButtonNext
+      : `${classNames.navButtonNext} ${classNames.navButtonInteractionDisabled}`;
 
     const previousButton = (
       <span
@@ -41,12 +90,9 @@ export default class Navbar extends Component {
         role="button"
         aria-label={labels.previousMonth}
         key="previous"
-        className={
-          showPreviousButton
-            ? classNames.navButtonPrev
-            : `${classNames.navButtonPrev} ${classNames.navButtonInteractionDisabled}`
-        }
-        onClick={showPreviousButton ? () => previousClickHandler() : undefined}
+        className={previousClassName}
+        onKeyDown={showPreviousButton ? previousKeyDownHandler : undefined}
+        onClick={showPreviousButton ? previousClickHandler : undefined}
       />
     );
 
@@ -56,12 +102,9 @@ export default class Navbar extends Component {
         role="button"
         aria-label={labels.nextMonth}
         key="right"
-        className={
-          showNextButton
-            ? classNames.navButtonNext
-            : `${classNames.navButtonNext} ${classNames.navButtonInteractionDisabled}`
-        }
-        onClick={showNextButton ? () => nextClickHandler() : undefined}
+        className={nextClassName}
+        onKeyDown={showNextButton ? nextKeyDownHandler : undefined}
+        onClick={showNextButton ? nextClickHandler : undefined}
       />
     );
 
diff --git a/src/keys.js b/src/keys.js
index e32793a81..232f8e887 100644
--- a/src/keys.js
+++ b/src/keys.js
@@ -1,9 +1,7 @@
-export default {
-  LEFT: 37,
-  UP: 38,
-  RIGHT: 39,
-  DOWN: 40,
-  ENTER: 13,
-  SPACE: 32,
-  ESC: 27,
-};
+export const LEFT = 37;
+export const UP = 38;
+export const RIGHT = 39;
+export const DOWN = 40;
+export const ENTER = 13;
+export const SPACE = 32;
+export const ESC = 27;


Test patch:
diff --git a/test/Navbar.js b/test/Navbar.js
index 34fc32e72..e218a4827 100644
--- a/test/Navbar.js
+++ b/test/Navbar.js
@@ -2,6 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 
 import Navbar from '../src/Navbar';
+import { SPACE, ENTER } from '../src/keys';
 
 describe('<Navbar />', () => {
   it('should have default props', () => {
@@ -79,12 +80,58 @@ describe('<Navbar />', () => {
     wrapper.find('.DayPicker-NavButton--next').simulate('click');
     expect(handleNextClick).toHaveBeenCalledTimes(1);
   });
+  it('should work without `onNextClick`', () => {
+    const wrapper = shallow(<Navbar />);
+    expect(() =>
+      wrapper.find('.DayPicker-NavButton--next').simulate('click')
+    ).not.toThrow();
+  });
+  it('should call `onNextClick` when pressing ENTER or SPACE on the next button', () => {
+    const handleNextClick = jest.fn();
+    const wrapper = shallow(<Navbar onNextClick={handleNextClick} />);
+    wrapper
+      .find('.DayPicker-NavButton--next')
+      .simulate('keydown', { keyCode: ENTER })
+      .simulate('keydown', { keyCode: SPACE });
+    expect(handleNextClick).toHaveBeenCalledTimes(2);
+  });
+  it('should not call `onNextClick` when pressing a key different than ENTER or SPACE on the next button', () => {
+    const handleNextClick = jest.fn();
+    const wrapper = shallow(<Navbar onNextClick={handleNextClick} />);
+    wrapper
+      .find('.DayPicker-NavButton--next')
+      .simulate('keydown', { keyCode: 1 });
+    expect(handleNextClick).not.toHaveBeenCalled();
+  });
   it('should call `onPreviousClick` when clicking the prev button', () => {
     const handlePreviousClick = jest.fn();
     const wrapper = shallow(<Navbar onPreviousClick={handlePreviousClick} />);
     wrapper.find('.DayPicker-NavButton--prev').simulate('click');
     expect(handlePreviousClick).toHaveBeenCalledTimes(1);
   });
+  it('should work without `onPreviousClick`', () => {
+    const wrapper = shallow(<Navbar />);
+    expect(() =>
+      wrapper.find('.DayPicker-NavButton--prev').simulate('click')
+    ).not.toThrow();
+  });
+  it('should call `onPreviousClick` when pressing ENTER or SPACE on the prev button', () => {
+    const handlePreviousClick = jest.fn();
+    const wrapper = shallow(<Navbar onPreviousClick={handlePreviousClick} />);
+    wrapper
+      .find('.DayPicker-NavButton--prev')
+      .simulate('keydown', { keyCode: ENTER })
+      .simulate('keydown', { keyCode: SPACE });
+    expect(handlePreviousClick).toHaveBeenCalledTimes(2);
+  });
+  it('should not call `onPreviousClick` when pressing a key different than ENTER or SPACE on the prev button', () => {
+    const handlePreviousClick = jest.fn();
+    const wrapper = shallow(<Navbar onPreviousClick={handlePreviousClick} />);
+    wrapper
+      .find('.DayPicker-NavButton--prev')
+      .simulate('keydown', { keyCode: 1 });
+    expect(handlePreviousClick).not.toHaveBeenCalled();
+  });
   it('should call `onNextClick` when clicking the prev button for RTL', () => {
     const handleNextClick = jest.fn();
     const wrapper = shallow(<Navbar dir="rtl" onNextClick={handleNextClick} />);
diff --git a/test/daypicker/events.js b/test/daypicker/events.js
index 8d0051791..4ada20aff 100644
--- a/test/daypicker/events.js
+++ b/test/daypicker/events.js
@@ -4,7 +4,7 @@ import SyntheticEvent from 'react-dom/lib/SyntheticEvent';
 import { mount } from 'enzyme';
 
 import DayPicker from '../../src/DayPicker';
-import keys from '../../src/keys';
+import * as keys from '../../src/keys';
 import { formatMonthTitle } from '../../src/LocaleUtils';
 
 describe('DayPicker’s events handlers', () => {
diff --git a/test/daypicker/navigation.js b/test/daypicker/navigation.js
index f6222062c..a0463d154 100644
--- a/test/daypicker/navigation.js
+++ b/test/daypicker/navigation.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { shallow, mount } from 'enzyme';
 
 import DayPicker from '../../src/DayPicker';
-import keys from '../../src/keys';
+import * as keys from '../../src/keys';
 
 describe('DayPicker’s navigation', () => {
   it('should not allow the previous month when the first month is the first allowed one', () => {
diff --git a/test/daypickerinput/events.js b/test/daypickerinput/events.js
index eee38a268..63db8dbbc 100644
--- a/test/daypickerinput/events.js
+++ b/test/daypickerinput/events.js
@@ -2,7 +2,7 @@ import React from 'react';
 
 import { mount } from 'enzyme';
 
-import keys from '../../src/keys';
+import * as keys from '../../src/keys';
 
 import DayPickerInput, { HIDE_TIMEOUT } from '../../src/DayPickerInput';
 import DayPicker from '../../src/DayPicker';