feat: use local HotKeys and allow editor escape
GlobalHotKeys were abandoned, because they were capturing events that had called stopPropagation. This meant that something needed to be in focus, hence passing a ref of the HotKey DOM element to the Editor. react-hotkeys has been updated to 2.0.0-pre9, because 2.0.0 captured ctrl keypresses when asked for ctrl+enter Currently only classic challenges can be executed by hotkey, but all allow hotkey navigation
This commit is contained in:
committed by
mrugesh
parent
ab3a9076d9
commit
c91393d737
6
client/package-lock.json
generated
6
client/package-lock.json
generated
@ -16613,9 +16613,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-hotkeys": {
|
"react-hotkeys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0-pre9",
|
||||||
"resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0-pre9.tgz",
|
||||||
"integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==",
|
"integrity": "sha512-YujzB+kGB5F6rq6/NkNN2t3uSwYfBsC9qWligGKyDe7roMSmzFYO2N88mwSc+9zmHhy/ZrDyB+aqbzVIaK8haw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"prop-types": "^15.6.1"
|
"prop-types": "^15.6.1"
|
||||||
},
|
},
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
"react-final-form": "^6.3.0",
|
"react-final-form": "^6.3.0",
|
||||||
"react-ga": "^2.6.0",
|
"react-ga": "^2.6.0",
|
||||||
"react-helmet": "^5.2.1",
|
"react-helmet": "^5.2.1",
|
||||||
"react-hotkeys": "^2.0.0",
|
"react-hotkeys": "^2.0.0-pre9",
|
||||||
"react-identicons": "^1.1.7",
|
"react-identicons": "^1.1.7",
|
||||||
"react-instantsearch-dom": "^5.7.0",
|
"react-instantsearch-dom": "^5.7.0",
|
||||||
"react-monaco-editor": "^0.30.1",
|
"react-monaco-editor": "^0.30.1",
|
||||||
|
@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { navigate } from 'gatsby';
|
|
||||||
|
|
||||||
import { executeChallenge, updateFile } from '../redux';
|
import { executeChallenge, updateFile } from '../redux';
|
||||||
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
import { userSelector, isDonationModalOpenSelector } from '../../../redux';
|
||||||
@ -13,13 +12,12 @@ const MonacoEditor = React.lazy(() => import('react-monaco-editor'));
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
canFocus: PropTypes.bool,
|
canFocus: PropTypes.bool,
|
||||||
|
containerRef: PropTypes.any.isRequired,
|
||||||
contents: PropTypes.string,
|
contents: PropTypes.string,
|
||||||
dimensions: PropTypes.object,
|
dimensions: PropTypes.object,
|
||||||
executeChallenge: PropTypes.func.isRequired,
|
executeChallenge: PropTypes.func.isRequired,
|
||||||
ext: PropTypes.string,
|
ext: PropTypes.string,
|
||||||
fileKey: PropTypes.string,
|
fileKey: PropTypes.string,
|
||||||
nextChallengePath: PropTypes.string.isRequired,
|
|
||||||
prevChallengePath: PropTypes.string.isRequired,
|
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
updateFile: PropTypes.func.isRequired
|
updateFile: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
@ -123,26 +121,13 @@ class Editor extends Component {
|
|||||||
run: this.props.executeChallenge
|
run: this.props.executeChallenge
|
||||||
});
|
});
|
||||||
this._editor.addAction({
|
this._editor.addAction({
|
||||||
id: 'navigate-prev',
|
id: 'leave-editor',
|
||||||
label: 'Navigate to previous challenge',
|
label: 'Leave editor',
|
||||||
keybindings: [
|
keybindings: [monaco.KeyCode.Escape],
|
||||||
/* eslint-disable no-bitwise */
|
run: () => {
|
||||||
monaco.KeyMod.chord(
|
if (this.props.containerRef.current)
|
||||||
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.US_COMMA
|
this.props.containerRef.current.focus();
|
||||||
)
|
}
|
||||||
],
|
|
||||||
run: () => navigate(this.props.prevChallengePath)
|
|
||||||
});
|
|
||||||
this._editor.addAction({
|
|
||||||
id: 'navigate-next',
|
|
||||||
label: 'Navigate to next challenge',
|
|
||||||
keybindings: [
|
|
||||||
/* eslint-disable no-bitwise */
|
|
||||||
monaco.KeyMod.chord(
|
|
||||||
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.US_DOT
|
|
||||||
)
|
|
||||||
],
|
|
||||||
run: () => navigate(this.props.nextChallengePath)
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import VideoModal from '../components/VideoModal';
|
|||||||
import ResetModal from '../components/ResetModal';
|
import ResetModal from '../components/ResetModal';
|
||||||
import MobileLayout from './MobileLayout';
|
import MobileLayout from './MobileLayout';
|
||||||
import DesktopLayout from './DesktopLayout';
|
import DesktopLayout from './DesktopLayout';
|
||||||
|
import Hotkeys from '../components/Hotkeys';
|
||||||
|
|
||||||
import { getGuideUrl } from '../utils';
|
import { getGuideUrl } from '../utils';
|
||||||
import { challengeTypes } from '../../../../utils/challengeTypes';
|
import { challengeTypes } from '../../../../utils/challengeTypes';
|
||||||
@ -32,7 +33,8 @@ import {
|
|||||||
initTests,
|
initTests,
|
||||||
updateChallengeMeta,
|
updateChallengeMeta,
|
||||||
challengeMounted,
|
challengeMounted,
|
||||||
consoleOutputSelector
|
consoleOutputSelector,
|
||||||
|
executeChallenge
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
|
|
||||||
import './classic.css';
|
import './classic.css';
|
||||||
@ -51,7 +53,8 @@ const mapDispatchToProps = dispatch =>
|
|||||||
initConsole,
|
initConsole,
|
||||||
initTests,
|
initTests,
|
||||||
updateChallengeMeta,
|
updateChallengeMeta,
|
||||||
challengeMounted
|
challengeMounted,
|
||||||
|
executeChallenge
|
||||||
},
|
},
|
||||||
dispatch
|
dispatch
|
||||||
);
|
);
|
||||||
@ -62,6 +65,7 @@ const propTypes = {
|
|||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
challengeNode: ChallengeNode
|
challengeNode: ChallengeNode
|
||||||
}),
|
}),
|
||||||
|
executeChallenge: PropTypes.func.isRequired,
|
||||||
files: PropTypes.shape({
|
files: PropTypes.shape({
|
||||||
key: PropTypes.string
|
key: PropTypes.string
|
||||||
}),
|
}),
|
||||||
@ -99,6 +103,8 @@ class ShowClassic extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
resizing: false
|
resizing: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.containerRef = React.createRef();
|
||||||
}
|
}
|
||||||
onResize() {
|
onResize() {
|
||||||
this.setState({ resizing: true });
|
this.setState({ resizing: true });
|
||||||
@ -219,19 +225,13 @@ class ShowClassic extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderEditor() {
|
renderEditor() {
|
||||||
const {
|
const { files } = this.props;
|
||||||
files,
|
|
||||||
pageContext: {
|
|
||||||
challengeMeta: { prevChallengePath, nextChallengePath }
|
|
||||||
}
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const challengeFile = first(Object.keys(files).map(key => files[key]));
|
const challengeFile = first(Object.keys(files).map(key => files[key]));
|
||||||
return (
|
return (
|
||||||
challengeFile && (
|
challengeFile && (
|
||||||
<Editor
|
<Editor
|
||||||
nextChallengePath={nextChallengePath}
|
containerRef={this.containerRef}
|
||||||
prevChallengePath={prevChallengePath}
|
|
||||||
{...challengeFile}
|
{...challengeFile}
|
||||||
fileKey={challengeFile.key}
|
fileKey={challengeFile.key}
|
||||||
/>
|
/>
|
||||||
@ -261,42 +261,56 @@ class ShowClassic extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { forumTopicId, title } = this.getChallenge();
|
const { forumTopicId, title } = this.getChallenge();
|
||||||
|
const {
|
||||||
|
executeChallenge,
|
||||||
|
pageContext: {
|
||||||
|
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
||||||
|
}
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<LearnLayout>
|
<Hotkeys
|
||||||
<Helmet
|
executeChallenge={executeChallenge}
|
||||||
title={`Learn ${this.getBlockNameTitle()} | freeCodeCamp.org`}
|
innerRef={this.containerRef}
|
||||||
/>
|
introPath={introPath}
|
||||||
<Media maxWidth={MAX_MOBILE_WIDTH}>
|
nextChallengePath={nextChallengePath}
|
||||||
<MobileLayout
|
prevChallengePath={prevChallengePath}
|
||||||
editor={this.renderEditor()}
|
>
|
||||||
guideUrl={getGuideUrl({ forumTopicId, title })}
|
<LearnLayout>
|
||||||
hasPreview={this.hasPreview()}
|
<Helmet
|
||||||
instructions={this.renderInstructionsPanel({
|
title={`Learn ${this.getBlockNameTitle()} | freeCodeCamp.org`}
|
||||||
showToolPanel: false
|
|
||||||
})}
|
|
||||||
preview={this.renderPreview()}
|
|
||||||
testOutput={this.renderTestOutput()}
|
|
||||||
videoUrl={this.getVideoUrl()}
|
|
||||||
/>
|
/>
|
||||||
</Media>
|
<Media maxWidth={MAX_MOBILE_WIDTH}>
|
||||||
<Media minWidth={MAX_MOBILE_WIDTH + 1}>
|
<MobileLayout
|
||||||
<DesktopLayout
|
editor={this.renderEditor()}
|
||||||
challengeFile={this.getChallengeFile()}
|
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||||
editor={this.renderEditor()}
|
hasPreview={this.hasPreview()}
|
||||||
hasPreview={this.hasPreview()}
|
instructions={this.renderInstructionsPanel({
|
||||||
instructions={this.renderInstructionsPanel({
|
showToolPanel: false
|
||||||
showToolPanel: true
|
})}
|
||||||
})}
|
preview={this.renderPreview()}
|
||||||
preview={this.renderPreview()}
|
testOutput={this.renderTestOutput()}
|
||||||
resizeProps={this.resizeProps}
|
videoUrl={this.getVideoUrl()}
|
||||||
testOutput={this.renderTestOutput()}
|
/>
|
||||||
/>
|
</Media>
|
||||||
</Media>
|
<Media minWidth={MAX_MOBILE_WIDTH + 1}>
|
||||||
<CompletionModal />
|
<DesktopLayout
|
||||||
<HelpModal />
|
challengeFile={this.getChallengeFile()}
|
||||||
<VideoModal videoUrl={this.getVideoUrl()} />
|
editor={this.renderEditor()}
|
||||||
<ResetModal />
|
hasPreview={this.hasPreview()}
|
||||||
</LearnLayout>
|
instructions={this.renderInstructionsPanel({
|
||||||
|
showToolPanel: true
|
||||||
|
})}
|
||||||
|
preview={this.renderPreview()}
|
||||||
|
resizeProps={this.resizeProps}
|
||||||
|
testOutput={this.renderTestOutput()}
|
||||||
|
/>
|
||||||
|
</Media>
|
||||||
|
<CompletionModal />
|
||||||
|
<HelpModal />
|
||||||
|
<VideoModal videoUrl={this.getVideoUrl()} />
|
||||||
|
<ResetModal />
|
||||||
|
</LearnLayout>
|
||||||
|
</Hotkeys>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Link from '../../../components/helpers/Link';
|
import Link from '../../../components/helpers/Link';
|
||||||
import { GlobalHotKeys } from 'react-hotkeys';
|
|
||||||
import { navigate } from 'gatsby';
|
|
||||||
|
|
||||||
import './challenge-title.css';
|
import './challenge-title.css';
|
||||||
import GreenPass from '../../../assets/icons/GreenPass';
|
import GreenPass from '../../../assets/icons/GreenPass';
|
||||||
|
|
||||||
const keyMap = {
|
|
||||||
NAVIGATE_PREV: ['ctrl+shift+<', 'cmd+shift+<'],
|
|
||||||
NAVIGATE_NEXT: ['ctrl+shift+>', 'cmd+shift+>']
|
|
||||||
};
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
children: PropTypes.string,
|
children: PropTypes.string,
|
||||||
introPath: PropTypes.string,
|
introPath: PropTypes.string,
|
||||||
@ -29,13 +22,8 @@ function ChallengeTitle({
|
|||||||
prevChallengePath,
|
prevChallengePath,
|
||||||
showPrevNextBtns
|
showPrevNextBtns
|
||||||
}) {
|
}) {
|
||||||
const handlers = {
|
|
||||||
NAVIGATE_PREV: () => navigate(prevChallengePath),
|
|
||||||
NAVIGATE_NEXT: () => navigate(nextChallengePath)
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div className='challenge-title-wrap'>
|
<div className='challenge-title-wrap'>
|
||||||
<GlobalHotKeys handlers={handlers} keyMap={keyMap} />
|
|
||||||
{showPrevNextBtns ? (
|
{showPrevNextBtns ? (
|
||||||
<Link
|
<Link
|
||||||
aria-label='Previous lesson'
|
aria-label='Previous lesson'
|
||||||
|
46
client/src/templates/Challenges/components/Hotkeys.js
Normal file
46
client/src/templates/Challenges/components/Hotkeys.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
import { navigate } from 'gatsby';
|
||||||
|
|
||||||
|
const keyMap = {
|
||||||
|
EXECUTE_CHALLENGE: 'ctrl+enter',
|
||||||
|
NAVIGATE_PREV: ['ctrl+left', 'cmd+left'],
|
||||||
|
NAVIGATE_NEXT: ['ctrl+right', 'cmd+right']
|
||||||
|
};
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
children: PropTypes.any,
|
||||||
|
executeChallenge: PropTypes.func,
|
||||||
|
innerRef: PropTypes.any,
|
||||||
|
introPath: PropTypes.string,
|
||||||
|
nextChallengePath: PropTypes.string,
|
||||||
|
prevChallengePath: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
function Hotkeys({
|
||||||
|
children,
|
||||||
|
executeChallenge,
|
||||||
|
introPath,
|
||||||
|
innerRef,
|
||||||
|
nextChallengePath,
|
||||||
|
prevChallengePath
|
||||||
|
}) {
|
||||||
|
const handlers = {
|
||||||
|
EXECUTE_CHALLENGE: () => {
|
||||||
|
if (executeChallenge) executeChallenge();
|
||||||
|
},
|
||||||
|
NAVIGATE_PREV: () => navigate(prevChallengePath),
|
||||||
|
NAVIGATE_NEXT: () => navigate(introPath ? introPath : nextChallengePath)
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={handlers} innerRef={innerRef} keyMap={keyMap}>
|
||||||
|
{children}
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Hotkeys.displayName = 'Hotkeys';
|
||||||
|
Hotkeys.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default Hotkeys;
|
@ -33,6 +33,7 @@ import { Form } from '../../../../components/formHelpers';
|
|||||||
import Spacer from '../../../../components/helpers/Spacer';
|
import Spacer from '../../../../components/helpers/Spacer';
|
||||||
import { ChallengeNode } from '../../../../redux/propTypes';
|
import { ChallengeNode } from '../../../../redux/propTypes';
|
||||||
import { isSignedInSelector } from '../../../../redux';
|
import { isSignedInSelector } from '../../../../redux';
|
||||||
|
import Hotkeys from '../../components/Hotkeys';
|
||||||
|
|
||||||
import { backend } from '../../../../../utils/challengeTypes';
|
import { backend } from '../../../../../utils/challengeTypes';
|
||||||
|
|
||||||
@ -186,6 +187,11 @@ export class BackEnd extends Component {
|
|||||||
return (
|
return (
|
||||||
<LearnLayout>
|
<LearnLayout>
|
||||||
<Helmet title={`${blockNameTitle} | Learn | freeCodeCamp.org`} />
|
<Helmet title={`${blockNameTitle} | Learn | freeCodeCamp.org`} />
|
||||||
|
<Hotkeys
|
||||||
|
introPath={introPath}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
|
/>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Row>
|
<Row>
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
|
@ -24,6 +24,7 @@ import ProjectForm from '../ProjectForm';
|
|||||||
import ProjectToolPanel from '../Tool-Panel';
|
import ProjectToolPanel from '../Tool-Panel';
|
||||||
import CompletionModal from '../../components/CompletionModal';
|
import CompletionModal from '../../components/CompletionModal';
|
||||||
import HelpModal from '../../components/HelpModal';
|
import HelpModal from '../../components/HelpModal';
|
||||||
|
import Hotkeys from '../../components/Hotkeys';
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
const mapStateToProps = () => ({});
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
@ -110,6 +111,11 @@ export class Project extends Component {
|
|||||||
const blockNameTitle = `${blockName} - ${title}`;
|
const blockNameTitle = `${blockName} - ${title}`;
|
||||||
return (
|
return (
|
||||||
<LearnLayout>
|
<LearnLayout>
|
||||||
|
<Hotkeys
|
||||||
|
introPath={introPath}
|
||||||
|
nextChallengePath={nextChallengePath}
|
||||||
|
prevChallengePath={prevChallengePath}
|
||||||
|
/>
|
||||||
<Helmet title={`${blockNameTitle} | Learn | freeCodeCamp.org`} />
|
<Helmet title={`${blockNameTitle} | Learn | freeCodeCamp.org`} />
|
||||||
<Grid>
|
<Grid>
|
||||||
<Row>
|
<Row>
|
||||||
|
Reference in New Issue
Block a user