fix:enable default mouse/touch events on monaco content widget (#46217)

* fix:enable default mouse events on monaco content widget

* add touch event handling

* initial test of scroll gutter

* disable scroll gutter

* re-enable gutter scroll as content widget

* move scroll gutter over line numbers

* fix: bug in moving scroll gutter over line numbers

* CSS adjustments to put scroll gutter over line numbers

* fix: scroll gutter height

* fix: make scroll gutter width responsive

* refactor: consolidate event handlers

* refactor: simplify args to createWidget
This commit is contained in:
Bruce Blaser
2022-06-08 23:37:40 -07:00
committed by GitHub
parent efa788ab26
commit 4f54125e76
3 changed files with 91 additions and 26 deletions
@@ -3,6 +3,15 @@
color: var(--primary-color);
}
.monaco-scrollable-element {
overflow: visible !important;
}
.monaco-editor-background {
overflow: visible !important;
contain: none !important;
}
.vs .monaco-scrollable-element > .scrollbar > .slider {
z-index: 11;
}
@@ -493,6 +493,16 @@ const Editor = (props: EditorProps): JSX.Element => {
if (!getStoredAriaRoledescription()) {
setAriaRoledescription(false);
}
// Add invisible content widget over line numbers so touch users will
// always have a place to vertically scroll the editor.
const scrollGutterNode = createScrollGutterNode(editor);
const scrollGutterWidget = createWidget(
editor,
'scrollgutter.widget',
scrollGutterNode
);
editor.addContentWidget(scrollGutterWidget);
};
const toggleAriaRoledescription = () => {
@@ -700,6 +710,19 @@ const Editor = (props: EditorProps): JSX.Element => {
return outputNode;
}
function createScrollGutterNode(
editor: editor.IStandaloneCodeEditor
): HTMLDivElement {
const scrollGutterNode = document.createElement('div');
const lineGutterWidth = editor.getLayoutInfo().contentLeft;
scrollGutterNode.style.width = `${lineGutterWidth}px`;
scrollGutterNode.style.left = `-${lineGutterWidth}px`;
scrollGutterNode.style.top = '0';
scrollGutterNode.style.height = '10000px';
scrollGutterNode.style.background = 'transparent';
return scrollGutterNode;
}
function resetMarginDecorations() {
const { model, insideEditDecId } = dataRef.current;
const range = model?.getDecorationRange(insideEditDecId);
@@ -880,42 +903,49 @@ const Editor = (props: EditorProps): JSX.Element => {
})[0];
}
function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
const createWidget = (
id: string,
domNode: HTMLDivElement,
getTop: () => string
) => {
const getId = () => id;
const getDomNode = () => domNode;
const getPosition = () => {
const createWidget = (
editor: editor.IStandaloneCodeEditor,
id: string,
domNode: HTMLDivElement,
// If getTop function is not provided then no positioning will be done here.
// This allows scroll gutter to do its positioning elsewhere.
getTop?: () => string
) => {
const getId = () => id;
const getDomNode = () => domNode;
const getPosition = () => {
if (getTop) {
domNode.style.width = `${editor.getLayoutInfo().contentWidth}px`;
domNode.style.top = getTop();
// must return null, so that Monaco knows the widget will position
// itself.
return null;
};
// Only the description content widget uses this method but it
// is harmless to pass it to the overlay widget.
const afterRender = () => {
domNode.style.left = '0';
domNode.style.visibility = 'visible';
};
return {
getId,
getDomNode,
getPosition,
afterRender
};
}
// must return null, so that Monaco knows the widget will position
// itself.
return null;
};
// Only the description content widget uses this method but it
// is harmless to pass it to the overlay widget.
const afterRender = () => {
if (getTop) {
domNode.style.left = '0';
}
domNode.style.visibility = 'visible';
};
return {
getId,
getDomNode,
getPosition,
afterRender
};
};
function addWidgetsToRegions(editor: editor.IStandaloneCodeEditor) {
const descriptionNode = createDescription(editor);
const outputNode = createOutputNode(editor);
if (!dataRef.current.descriptionWidget) {
dataRef.current.descriptionWidget = createWidget(
editor,
'description.widget',
descriptionNode,
getDescriptionZoneTop
@@ -935,6 +965,7 @@ const Editor = (props: EditorProps): JSX.Element => {
}
if (!dataRef.current.outputWidget) {
dataRef.current.outputWidget = createWidget(
editor,
'output.widget',
outputNode,
getOutputZoneTop
@@ -139,6 +139,15 @@ const BASE_LAYOUT = {
testsPane: { flex: 0.3 }
};
// Used to prevent monaco from stealing mouse/touch events on the upper jaw
// content widget so they can trigger their default actions. (Issue #46166)
const handleContentWidgetEvents = (e: MouseEvent | TouchEvent): void => {
const target = e.target as HTMLElement;
if (target?.closest('.editor-upper-jaw')) {
e.stopPropagation();
}
};
// Component
class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
static displayName: string;
@@ -224,6 +233,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
}
} = this.props;
this.initializeComponent(title);
// Bug fix for the monaco content widget and touch devices/right mouse
// click. (Issue #46166)
document.addEventListener('mousedown', handleContentWidgetEvents, true);
document.addEventListener('contextmenu', handleContentWidgetEvents, true);
document.addEventListener('touchstart', handleContentWidgetEvents, true);
document.addEventListener('touchmove', handleContentWidgetEvents, true);
document.addEventListener('touchend', handleContentWidgetEvents, true);
}
componentDidUpdate(prevProps: ShowClassicProps) {
@@ -301,6 +317,15 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
const { createFiles, cancelTests } = this.props;
createFiles([]);
cancelTests();
document.removeEventListener('mousedown', handleContentWidgetEvents, true);
document.removeEventListener(
'contextmenu',
handleContentWidgetEvents,
true
);
document.removeEventListener('touchstart', handleContentWidgetEvents, true);
document.removeEventListener('touchmove', handleContentWidgetEvents, true);
document.removeEventListener('touchend', handleContentWidgetEvents, true);
}
getChallenge = () => this.props.data.challengeNode.challenge;