feat: control editor focus (#43882)

* refactor: MultifileEditor to functional component.

* fix: make editor acquire focus once on mount

Now the editors can leave the dom (e.g. if a tab is clicked), but an
editor will only call for focus if the MultifileEditor itself remounts
This commit is contained in:
Oliver Eyton-Williams
2021-10-19 17:52:51 +02:00
committed by GitHub
parent 2bddbbff42
commit 6c20301204
2 changed files with 148 additions and 145 deletions

View File

@ -1,5 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { useRef } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
@ -73,154 +73,153 @@ const mapDispatchToProps = {
updateFile updateFile
}; };
class MultifileEditor extends Component { const MultifileEditor = props => {
focusOnHotkeys() { const {
if (this.props.containerRef.current) { challengeFiles,
this.props.containerRef.current.focus(); containerRef,
description,
editorRef,
initialTests,
theme,
resizeProps,
title,
visibleEditors: { indexcss, indexhtml, indexjs, indexjsx },
usesMultifileEditor
} = props;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
// the in-editor description)
// TODO: the splitters should appear between editors, so logically this
// would be best as
// editors.map(props => <EditorWrapper ...props>).join(<ReflexSplitter>)
// ...probably! As long as we can put keys in the right places.
const reflexProps = {
propagateDimensions: true
};
let splitterJSXRight, splitterHTMLRight, splitterCSSRight;
if (indexjsx) {
if (indexhtml || indexcss || indexjs) {
splitterJSXRight = true;
}
}
if (indexhtml) {
if (indexcss || indexjs) {
splitterHTMLRight = true;
}
}
if (indexcss) {
if (indexjs) {
splitterCSSRight = true;
} }
} }
render() { // TODO: tabs should be dynamically created from the challengeFiles
const { // TODO: the tabs mess up the rendering (scroll doesn't work properly and
challengeFiles, // the in-editor description)
containerRef, const targetEditor = getTargetEditor(challengeFiles);
description,
editorRef,
initialTests,
theme,
resizeProps,
title,
visibleEditors: { indexcss, indexhtml, indexjs, indexjsx },
usesMultifileEditor
} = this.props;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
// the in-editor description)
// TODO: the splitters should appear between editors, so logically this // Only one editor should be focused and that should happen once, after it has
// would be best as // been mounted. This ref allows the editors to co-ordinate, without having to
// editors.map(props => <EditorWrapper ...props>).join(<ReflexSplitter>) // resort to redux.
// ...probably! As long as we can put keys in the right places. const canFocusOnMountRef = useRef(true);
const reflexProps = { return (
propagateDimensions: true <ReflexContainer
}; orientation='horizontal'
{...reflexProps}
{...resizeProps}
className='editor-container'
>
<ReflexElement flex={10} {...reflexProps} {...resizeProps}>
<ReflexContainer orientation='vertical'>
{indexjsx && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Editor
canFocusOnMountRef={canFocusOnMountRef}
challengeFiles={challengeFiles}
containerRef={containerRef}
description={targetEditor === 'indexjsx' ? description : null}
editorRef={editorRef}
fileKey='indexjsx'
initialTests={initialTests}
key='indexjsx'
resizeProps={resizeProps}
theme={editorTheme}
title={title}
usesMultifileEditor={usesMultifileEditor}
/>
</ReflexElement>
)}
{splitterJSXRight && (
<ReflexSplitter propagate={true} {...resizeProps} />
)}
{indexhtml && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Editor
canFocusOnMountRef={canFocusOnMountRef}
challengeFiles={challengeFiles}
containerRef={containerRef}
description={targetEditor === 'indexhtml' ? description : null}
editorRef={editorRef}
fileKey='indexhtml'
initialTests={initialTests}
key='indexhtml'
resizeProps={resizeProps}
theme={editorTheme}
title={title}
usesMultifileEditor={usesMultifileEditor}
/>
</ReflexElement>
)}
{splitterHTMLRight && (
<ReflexSplitter propagate={true} {...resizeProps} />
)}
{indexcss && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Editor
canFocusOnMountRef={canFocusOnMountRef}
challengeFiles={challengeFiles}
containerRef={containerRef}
description={targetEditor === 'indexcss' ? description : null}
editorRef={editorRef}
fileKey='indexcss'
initialTests={initialTests}
key='indexcss'
resizeProps={resizeProps}
theme={editorTheme}
title={title}
usesMultifileEditor={usesMultifileEditor}
/>
</ReflexElement>
)}
{splitterCSSRight && (
<ReflexSplitter propagate={true} {...resizeProps} />
)}
let splitterJSXRight, splitterHTMLRight, splitterCSSRight; {indexjs && (
if (indexjsx) { <ReflexElement {...reflexProps} {...resizeProps}>
if (indexhtml || indexcss || indexjs) { <Editor
splitterJSXRight = true; canFocusOnMountRef={canFocusOnMountRef}
} challengeFiles={challengeFiles}
} containerRef={containerRef}
if (indexhtml) { description={targetEditor === 'indexjs' ? description : null}
if (indexcss || indexjs) { editorRef={editorRef}
splitterHTMLRight = true; fileKey='indexjs'
} initialTests={initialTests}
} key='indexjs'
if (indexcss) { resizeProps={resizeProps}
if (indexjs) { theme={editorTheme}
splitterCSSRight = true; title={title}
} usesMultifileEditor={usesMultifileEditor}
} />
</ReflexElement>
// TODO: tabs should be dynamically created from the challengeFiles )}
// TODO: the tabs mess up the rendering (scroll doesn't work properly and </ReflexContainer>
// the in-editor description) </ReflexElement>
const targetEditor = getTargetEditor(challengeFiles); </ReflexContainer>
return ( );
<ReflexContainer };
orientation='horizontal'
{...reflexProps}
{...resizeProps}
className='editor-container'
>
<ReflexElement flex={10} {...reflexProps} {...resizeProps}>
<ReflexContainer orientation='vertical'>
{indexjsx && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Editor
challengeFiles={challengeFiles}
containerRef={containerRef}
description={targetEditor === 'indexjsx' ? description : null}
editorRef={editorRef}
fileKey='indexjsx'
initialTests={initialTests}
key='indexjsx'
resizeProps={resizeProps}
theme={editorTheme}
title={title}
usesMultifileEditor={usesMultifileEditor}
/>
</ReflexElement>
)}
{splitterJSXRight && (
<ReflexSplitter propagate={true} {...resizeProps} />
)}
{indexhtml && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Editor
challengeFiles={challengeFiles}
containerRef={containerRef}
description={
targetEditor === 'indexhtml' ? description : null
}
editorRef={editorRef}
fileKey='indexhtml'
initialTests={initialTests}
key='indexhtml'
resizeProps={resizeProps}
theme={editorTheme}
title={title}
usesMultifileEditor={usesMultifileEditor}
/>
</ReflexElement>
)}
{splitterHTMLRight && (
<ReflexSplitter propagate={true} {...resizeProps} />
)}
{indexcss && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Editor
challengeFiles={challengeFiles}
containerRef={containerRef}
description={targetEditor === 'indexcss' ? description : null}
editorRef={editorRef}
fileKey='indexcss'
initialTests={initialTests}
key='indexcss'
resizeProps={resizeProps}
theme={editorTheme}
title={title}
usesMultifileEditor={usesMultifileEditor}
/>
</ReflexElement>
)}
{splitterCSSRight && (
<ReflexSplitter propagate={true} {...resizeProps} />
)}
{indexjs && (
<ReflexElement {...reflexProps} {...resizeProps}>
<Editor
challengeFiles={challengeFiles}
containerRef={containerRef}
description={targetEditor === 'indexjs' ? description : null}
editorRef={editorRef}
fileKey='indexjs'
initialTests={initialTests}
key='indexjs'
resizeProps={resizeProps}
theme={editorTheme}
title={title}
usesMultifileEditor={usesMultifileEditor}
/>
</ReflexElement>
)}
</ReflexContainer>
</ReflexElement>
</ReflexContainer>
);
}
}
MultifileEditor.displayName = 'MultifileEditor'; MultifileEditor.displayName = 'MultifileEditor';
MultifileEditor.propTypes = propTypes; MultifileEditor.propTypes = propTypes;

View File

@ -57,6 +57,7 @@ interface EditorProps {
executeChallenge: (options?: { showCompletionModal: boolean }) => void; executeChallenge: (options?: { showCompletionModal: boolean }) => void;
ext: ExtTypes; ext: ExtTypes;
fileKey: FileKeyTypes; fileKey: FileKeyTypes;
canFocusOnMountRef: MutableRefObject<boolean>;
initialEditorContent: string; initialEditorContent: string;
initialExt: string; initialExt: string;
initTests: (tests: Test[]) => void; initTests: (tests: Test[]) => void;
@ -631,12 +632,15 @@ const Editor = (props: EditorProps): JSX.Element => {
function focusIfTargetEditor() { function focusIfTargetEditor() {
const { editor } = dataRef.current; const { editor } = dataRef.current;
if (!editor) return; const { canFocusOnMountRef } = props;
if (!editor || !canFocusOnMountRef.current) return;
if (!props.usesMultifileEditor) { if (!props.usesMultifileEditor) {
// Only one editor? Focus it. // Only one editor? Focus it.
editor.focus(); editor.focus();
canFocusOnMountRef.current = false;
} else if (hasEditableRegion()) { } else if (hasEditableRegion()) {
editor.focus(); editor.focus();
canFocusOnMountRef.current = false;
} }
} }