Skip to content

Commit 2eb8690

Browse files
authored
Merge pull request #2052 from lindapaiste/refactor/keydown
Move keydown handling out of IDEView
2 parents 4bae3e1 + be5f4eb commit 2eb8690

File tree

8 files changed

+170
-205
lines changed

8 files changed

+170
-205
lines changed

client/components/Nav/NavBar.jsx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React, {
66
useRef,
77
useState
88
} from 'react';
9+
import useKeyDownHandlers from '../../modules/IDE/hooks/useKeyDownHandlers';
910
import { MenuOpenContext, NavBarContext } from './contexts';
1011

1112
function NavBar({ children, className }) {
@@ -31,18 +32,9 @@ function NavBar({ children, className }) {
3132
};
3233
}, [nodeRef, setDropdownOpen]);
3334

34-
// TODO: replace with `useKeyDownHandlers` after #2052 is merged
35-
useEffect(() => {
36-
function handleKeyDown(e) {
37-
if (e.keyCode === 27) {
38-
setDropdownOpen('none');
39-
}
40-
}
41-
document.addEventListener('keydown', handleKeyDown, false);
42-
return () => {
43-
document.removeEventListener('keydown', handleKeyDown, false);
44-
};
45-
}, [setDropdownOpen]);
35+
useKeyDownHandlers({
36+
escape: () => setDropdownOpen('none')
37+
});
4638

4739
const clearHideTimeout = useCallback(() => {
4840
if (timerRef.current) {

client/modules/App/components/Overlay.jsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@ import { withTranslation } from 'react-i18next';
44

55
import browserHistory from '../../../browserHistory';
66
import ExitIcon from '../../../images/exit.svg';
7+
import { DocumentKeyDown } from '../../IDE/hooks/useKeyDownHandlers';
78

89
class Overlay extends React.Component {
910
constructor(props) {
1011
super(props);
1112
this.close = this.close.bind(this);
1213
this.handleClick = this.handleClick.bind(this);
1314
this.handleClickOutside = this.handleClickOutside.bind(this);
14-
this.keyPressHandle = this.keyPressHandle.bind(this);
1515
}
1616

1717
componentWillMount() {
1818
document.addEventListener('mousedown', this.handleClick, false);
19-
document.addEventListener('keydown', this.keyPressHandle);
2019
}
2120

2221
componentDidMount() {
@@ -25,7 +24,6 @@ class Overlay extends React.Component {
2524

2625
componentWillUnmount() {
2726
document.removeEventListener('mousedown', this.handleClick, false);
28-
document.removeEventListener('keydown', this.keyPressHandle);
2927
}
3028

3129
handleClick(e) {
@@ -40,14 +38,6 @@ class Overlay extends React.Component {
4038
this.close();
4139
}
4240

43-
keyPressHandle(e) {
44-
// escape key code = 27.
45-
// So here we are checking if the key pressed was Escape key.
46-
if (e.keyCode === 27) {
47-
this.close();
48-
}
49-
}
50-
5141
close() {
5242
// Only close if it is the last (and therefore the topmost overlay)
5343
const overlays = document.getElementsByClassName('overlay');
@@ -90,6 +80,7 @@ class Overlay extends React.Component {
9080
</div>
9181
</header>
9282
{children}
83+
<DocumentKeyDown handlers={{ escape: () => this.close() }} />
9384
</section>
9485
</div>
9586
</div>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import PropTypes from 'prop-types';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
import { updateFileContent } from '../actions/files';
4+
import {
5+
collapseConsole,
6+
collapseSidebar,
7+
expandConsole,
8+
expandSidebar,
9+
showErrorModal,
10+
startSketch,
11+
stopSketch
12+
} from '../actions/ide';
13+
import { setAllAccessibleOutput } from '../actions/preferences';
14+
import { cloneProject, saveProject } from '../actions/project';
15+
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';
16+
import {
17+
getAuthenticated,
18+
getIsUserOwner,
19+
getSketchOwner
20+
} from '../selectors/users';
21+
22+
export const useIDEKeyHandlers = ({ getContent }) => {
23+
const dispatch = useDispatch();
24+
25+
const sidebarIsExpanded = useSelector((state) => state.ide.sidebarIsExpanded);
26+
const consoleIsExpanded = useSelector((state) => state.ide.consoleIsExpanded);
27+
28+
const isUserOwner = useSelector(getIsUserOwner);
29+
const isAuthenticated = useSelector(getAuthenticated);
30+
const sketchOwner = useSelector(getSketchOwner);
31+
32+
const syncFileContent = () => {
33+
const file = getContent();
34+
dispatch(updateFileContent(file.id, file.content));
35+
};
36+
37+
useKeyDownHandlers({
38+
'ctrl-s': (e) => {
39+
e.preventDefault();
40+
e.stopPropagation();
41+
if (isUserOwner || (isAuthenticated && !sketchOwner)) {
42+
dispatch(saveProject(getContent()));
43+
} else if (isAuthenticated) {
44+
dispatch(cloneProject());
45+
} else {
46+
dispatch(showErrorModal('forceAuthentication'));
47+
}
48+
},
49+
'ctrl-shift-enter': (e) => {
50+
e.preventDefault();
51+
e.stopPropagation();
52+
dispatch(stopSketch());
53+
},
54+
'ctrl-enter': (e) => {
55+
e.preventDefault();
56+
e.stopPropagation();
57+
syncFileContent();
58+
dispatch(startSketch());
59+
},
60+
'ctrl-shift-1': (e) => {
61+
e.preventDefault();
62+
dispatch(setAllAccessibleOutput(true));
63+
},
64+
'ctrl-shift-2': (e) => {
65+
e.preventDefault();
66+
dispatch(setAllAccessibleOutput(false));
67+
},
68+
'ctrl-b': (e) => {
69+
e.preventDefault();
70+
dispatch(
71+
// TODO: create actions 'toggleConsole', 'toggleSidebar', etc.
72+
sidebarIsExpanded ? collapseSidebar() : expandSidebar()
73+
);
74+
},
75+
'ctrl-`': (e) => {
76+
e.preventDefault();
77+
dispatch(consoleIsExpanded ? collapseConsole() : expandConsole());
78+
}
79+
});
80+
};
81+
82+
const IDEKeyHandlers = ({ getContent }) => {
83+
useIDEKeyHandlers({ getContent });
84+
return null;
85+
};
86+
87+
// Most actions can be accessed via redux, but those involving the cmController
88+
// must be provided via props.
89+
IDEKeyHandlers.propTypes = {
90+
getContent: PropTypes.func.isRequired
91+
};
92+
93+
export default IDEKeyHandlers;

client/modules/IDE/components/Modal.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import classNames from 'classnames';
22
import PropTypes from 'prop-types';
33
import React, { useEffect, useRef } from 'react';
44
import ExitIcon from '../../../images/exit.svg';
5+
import useKeyDownHandlers from '../hooks/useKeyDownHandlers';
56

67
// Common logic from NewFolderModal, NewFileModal, UploadFileModal
78

@@ -30,6 +31,8 @@ const Modal = ({
3031
};
3132
}, []);
3233

34+
useKeyDownHandlers({ escape: onClose });
35+
3336
return (
3437
<section className="modal" ref={modalRef}>
3538
<div className={classNames('modal-content', contentClassName)}>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import mapKeys from 'lodash/mapKeys';
2+
import PropTypes from 'prop-types';
3+
import { useCallback, useEffect, useRef } from 'react';
4+
5+
/**
6+
* Attaches keydown handlers to the global document.
7+
*
8+
* Handles Mac/PC switching of Ctrl to Cmd.
9+
*
10+
* @param {Record<string, (e: KeyboardEvent) => void>} keyHandlers - an object
11+
* which maps from the key to its event handler. The object keys are a combination
12+
* of the key and prefixes `ctrl-` `shift-` (ie. 'ctrl-f', 'ctrl-shift-f')
13+
* and the values are the function to call when that specific key is pressed.
14+
*/
15+
export default function useKeyDownHandlers(keyHandlers) {
16+
/**
17+
* Instead of memoizing the handlers, use a ref and call the current
18+
* handler at the time of the event.
19+
*/
20+
const handlers = useRef(keyHandlers);
21+
22+
useEffect(() => {
23+
handlers.current = mapKeys(keyHandlers, (value, key) => key.toLowerCase());
24+
}, [keyHandlers]);
25+
26+
/**
27+
* Will call all matching handlers, starting with the most specific: 'ctrl-shift-f' => 'ctrl-f' => 'f'.
28+
* Can use e.stopPropagation() to prevent subsequent handlers.
29+
* @type {(function(KeyboardEvent): void)}
30+
*/
31+
const handleEvent = useCallback((e) => {
32+
const isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
33+
const isCtrl = isMac ? e.metaKey : e.ctrlKey;
34+
if (e.shiftKey && isCtrl) {
35+
handlers.current[`ctrl-shift-${e.key.toLowerCase()}`]?.(e);
36+
} else if (isCtrl) {
37+
handlers.current[`ctrl-${e.key.toLowerCase()}`]?.(e);
38+
}
39+
handlers.current[e.key.toLowerCase()]?.(e);
40+
}, []);
41+
42+
useEffect(() => {
43+
document.addEventListener('keydown', handleEvent);
44+
45+
return () => document.removeEventListener('keydown', handleEvent);
46+
}, [handleEvent]);
47+
}
48+
49+
/**
50+
* Component version can be used in class components where hooks can't be used.
51+
*
52+
* @param {Record<string, (e: KeyboardEvent) => void>} handlers
53+
*/
54+
export const DocumentKeyDown = ({ handlers }) => {
55+
useKeyDownHandlers(handlers);
56+
return null;
57+
};
58+
DocumentKeyDown.propTypes = {
59+
handlers: PropTypes.objectOf(PropTypes.func)
60+
};

0 commit comments

Comments
 (0)