refactor(client): render lower jaw via React portal (#55785)

pull/55788/head
Oliver Eyton-Williams 2024-08-08 09:38:52 +02:00 committed by GitHub
parent 274b7bc377
commit fec2315447
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 41 additions and 61 deletions

View File

@ -10,8 +10,8 @@ import type {
import { OS } from 'monaco-editor/esm/vs/base/common/platform.js';
import Prism from 'prismjs';
import React, { useEffect, Suspense, MutableRefObject, useRef } from 'react';
import ReactDOM from 'react-dom';
import { Provider, connect, useStore } from 'react-redux';
import { createPortal } from 'react-dom';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import store from 'store';
@ -127,7 +127,6 @@ interface EditorProperties {
outputZoneTop: number;
outputZoneId: string;
descriptionNode?: HTMLDivElement;
outputNode?: HTMLDivElement;
descriptionWidget?: editor.IContentWidget;
outputWidget?: editor.IOverlayWidget;
}
@ -243,7 +242,6 @@ const initialData: EditorProperties = {
};
const Editor = (props: EditorProps): JSX.Element => {
const reduxStore = useStore();
const { t } = useTranslation();
const { editorRef, initTests, resetAttempts, isMobileLayout } = props;
// These refs are used during initialisation of the editor as well as by
@ -255,6 +253,8 @@ const Editor = (props: EditorProps): JSX.Element => {
const monacoRef: MutableRefObject<typeof monacoEditor | null> =
useRef<typeof monacoEditor>(null);
const dataRef = useRef<EditorProperties>({ ...initialData });
const [lowerJawContainer, setLowerJawContainer] =
React.useState<HTMLDivElement | null>(null);
const submitChallengeDebounceRef = useRef(
debounce(props.submitChallenge, 1000, { leading: true, trailing: false })
@ -713,56 +713,23 @@ const Editor = (props: EditorProps): JSX.Element => {
const tryToSubmitChallenge = submitChallengeDebounceRef.current;
function createLowerJaw(
outputNode: HTMLDivElement,
editor: editor.IStandaloneCodeEditor
) {
const { output } = props;
const isChallengeComplete = challengeIsComplete();
ReactDOM.render(
<Provider store={reduxStore}>
<LowerJaw
openHelpModal={props.openHelpModal}
openResetModal={props.openResetModal}
tryToExecuteChallenge={tryToExecuteChallenge}
hint={output[1]}
testsLength={props.tests.length}
attempts={attemptsRef.current}
challengeIsCompleted={isChallengeComplete}
tryToSubmitChallenge={tryToSubmitChallenge}
isSignedIn={props.isSignedIn}
updateContainer={() => updateOutputViewZone(outputNode, editor)}
/>
</Provider>,
outputNode
);
}
const updateOutputZone = () => {
const editor = dataRef.current.editor;
if (!editor || !dataRef.current.outputNode) return;
const outputNode = dataRef.current.outputNode;
createLowerJaw(outputNode, editor);
};
// TODO: there's a potential performance gain to be had by only updating when
// the outputViewZone has actually changed.
const updateOutputViewZone = (
outputNode: HTMLDivElement,
editor: editor.IStandaloneCodeEditor
lowerJawContainer: HTMLDivElement,
editor?: editor.IStandaloneCodeEditor
) => {
if (!editor) return;
// make sure the overlayWidget has resized before using it to set the height
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
lowerJawContainer.style.width = `${getEditorContentWidth(editor)}px`;
// We have to wait for the viewZone to finish rendering before adjusting the
// position of the overlayWidget (i.e. trigger it via onComputedHeight). If
// not the editor may report the wrong value for position of the lines.
editor?.changeViewZones(changeAccessor => {
editor.changeViewZones(changeAccessor => {
changeAccessor.removeZone(dataRef.current.outputZoneId);
const viewZone = {
afterLineNumber: getLastLineOfEditableRegion(),
heightInPx: outputNode.offsetHeight,
heightInPx: lowerJawContainer.offsetHeight,
domNode: document.createElement('div'),
onComputedHeight: () =>
dataRef.current.outputWidget &&
@ -829,16 +796,16 @@ const Editor = (props: EditorProps): JSX.Element => {
return editor.getLayoutInfo().contentWidth - getScrollbarWidth();
}
function createOutputNode(editor: editor.IStandaloneCodeEditor) {
if (dataRef.current.outputNode) return dataRef.current.outputNode;
const outputNode = document.createElement('div');
outputNode.classList.add('editor-lower-jaw');
outputNode.setAttribute('id', 'editor-lower-jaw');
outputNode.style.left = `${editor.getLayoutInfo().contentLeft}px`;
outputNode.style.width = `${getEditorContentWidth(editor)}px`;
outputNode.style.top = getOutputZoneTop();
dataRef.current.outputNode = outputNode;
return outputNode;
function createLowerJawContainer(editor: editor.IStandaloneCodeEditor) {
if (lowerJawContainer) return lowerJawContainer;
const container = document.createElement('div');
container.classList.add('editor-lower-jaw');
container.setAttribute('id', 'editor-lower-jaw');
container.style.left = `${editor.getLayoutInfo().contentLeft}px`;
container.style.width = `${getEditorContentWidth(editor)}px`;
container.style.top = getOutputZoneTop();
setLowerJawContainer(container);
return container;
}
function createScrollGutterNode(
@ -1099,7 +1066,7 @@ const Editor = (props: EditorProps): JSX.Element => {
function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
const descriptionNode = createDescription(editor);
const outputNode = createOutputNode(editor);
const lowerJawNode = createLowerJawContainer(editor);
if (!dataRef.current.descriptionWidget) {
dataRef.current.descriptionWidget = createWidget(
@ -1125,11 +1092,10 @@ const Editor = (props: EditorProps): JSX.Element => {
dataRef.current.outputWidget = createWidget(
editor,
'output.widget',
outputNode,
lowerJawNode,
getOutputZoneTop
);
editor.addOverlayWidget(dataRef.current.outputWidget);
editor.changeViewZones(updateOutputZone);
}
editor.onDidScrollChange(() => {
@ -1157,7 +1123,6 @@ const Editor = (props: EditorProps): JSX.Element => {
// ask monaco to update regardless.
redecorateEditableRegion();
updateDescriptionZone();
updateOutputZone();
});
}
@ -1263,7 +1228,6 @@ const Editor = (props: EditorProps): JSX.Element => {
useEffect(() => {
const { model, insideEditDecId } = dataRef.current;
const lowerJawElement = dataRef.current.outputNode;
const isChallengeComplete = challengeIsComplete();
const range = model?.getDecorationRange(insideEditDecId);
if (range && isChallengeComplete) {
@ -1275,8 +1239,6 @@ const Editor = (props: EditorProps): JSX.Element => {
}
);
}
dataRef.current.outputNode = lowerJawElement;
updateOutputZone();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.tests]);
@ -1290,7 +1252,6 @@ const Editor = (props: EditorProps): JSX.Element => {
}
if (hasEditableRegion()) {
updateDescriptionZone();
updateOutputZone();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.dimensions]);
@ -1315,6 +1276,7 @@ const Editor = (props: EditorProps): JSX.Element => {
: theme === Themes.Default
? 'vs-custom'
: editorSystemTheme;
return (
<Suspense fallback={<Loader loaderDelay={600} />}>
<span className='notranslate'>
@ -1326,6 +1288,24 @@ const Editor = (props: EditorProps): JSX.Element => {
theme={editorTheme}
/>
</span>
{lowerJawContainer !== null &&
createPortal(
<LowerJaw
openHelpModal={props.openHelpModal}
openResetModal={props.openResetModal}
tryToExecuteChallenge={tryToExecuteChallenge}
hint={props.output[1]}
testsLength={props.tests.length}
attempts={attemptsRef.current}
challengeIsCompleted={challengeIsComplete()}
tryToSubmitChallenge={tryToSubmitChallenge}
isSignedIn={props.isSignedIn}
updateContainer={() =>
updateOutputViewZone(lowerJawContainer, dataRef.current.editor)
}
/>,
lowerJawContainer
)}
</Suspense>
);
};