diff --git a/frontend/src/modules/entitydetails/components/EntityNavigation.js b/frontend/src/modules/entitydetails/components/EntityNavigation.js index 6ebdb7225b..daadd8e490 100644 --- a/frontend/src/modules/entitydetails/components/EntityNavigation.js +++ b/frontend/src/modules/entitydetails/components/EntityNavigation.js @@ -18,6 +18,16 @@ type Props = {| * Shows next/previous buttons. */ export default class EntityNavigation extends React.Component { + componentDidMount() { + // $FLOW_IGNORE (errors that I don't understand, no help from the Web) + document.addEventListener('keydown', this.handleShortcuts); + } + + componentWillUnmount() { + // $FLOW_IGNORE (errors that I don't understand, no help from the Web) + document.removeEventListener('keydown', this.handleShortcuts); + } + goToNextEntity = () => { this.props.goToNextEntity(); } @@ -26,6 +36,28 @@ export default class EntityNavigation extends React.Component { this.props.goToPreviousEntity(); } + handleShortcuts = (event: SyntheticKeyboardEvent<>) => { + const key = event.keyCode; + + let handledEvent = false; + + // On Alt + Up, move to the previous entity. + if (key === 38 && event.altKey && !event.ctrlKey && !event.shiftKey) { + handledEvent = true; + this.goToPreviousEntity(); + } + + // On Alt + Down, move to the next entity. + if (key === 40 && event.altKey && !event.ctrlKey && !event.shiftKey) { + handledEvent = true; + this.goToNextEntity(); + } + + if (handledEvent) { + event.preventDefault(); + } + } + render(): React.Node { return
', () => { + function getEntityNav({ create = shallow } = {}) { + const nextMock = sinon.stub(); + const prevMock = sinon.stub(); + const wrapper = create(); + + return { + wrapper, + nextMock, + prevMock, + }; + } + + it('goes to the next entity on click on the Next button', () => { + const { wrapper, nextMock } = getEntityNav(); + + expect(nextMock.calledOnce).toBeFalsy(); + wrapper.find('button.next').simulate('click'); + expect(nextMock.calledOnce).toBeTruthy(); + }); + + it('goes to the next entity on Alt + Down', () => { + // Simulating the key presses on `document`. + // See https://github.com/airbnb/enzyme/issues/426 + const eventsMap = {}; + document.addEventListener = sinon.spy((event, cb) => { + eventsMap[event] = cb; + }); + + const { nextMock } = getEntityNav(mount); + + expect(nextMock.calledOnce).toBeFalsy(); + const event = { + preventDefault: sinon.spy(), + keyCode: 40, // Down + altKey: true, + ctrlKey: false, + shiftKey: false, + }; + eventsMap.keydown(event); + expect(nextMock.calledOnce).toBeTruthy(); + }); + + it('goes to the previous entity on click on the Previous button', () => { + const { wrapper, prevMock } = getEntityNav(); + + expect(prevMock.calledOnce).toBeFalsy(); + wrapper.find('button.previous').simulate('click'); + expect(prevMock.calledOnce).toBeTruthy(); + }); + + it('goes to the previous entity on Alt + Up', () => { + // Simulating the key presses on `document`. + // See https://github.com/airbnb/enzyme/issues/426 + const eventsMap = {}; + document.addEventListener = sinon.spy((event, cb) => { + eventsMap[event] = cb; + }); + + const { prevMock } = getEntityNav(mount); + + expect(prevMock.calledOnce).toBeFalsy(); + const event = { + preventDefault: sinon.spy(), + keyCode: 38, // Up + altKey: true, + ctrlKey: false, + shiftKey: false, + }; + eventsMap.keydown(event); + expect(prevMock.calledOnce).toBeTruthy(); + }); +});