Basic editor, tooltips are in the wrong place

I’ve got a few simple editors on a page. I added really basic tool tips, but they appear in the wrong place:

wrong-position

The code is very simple, looks like this:

function validateCode(view: EditorView): readonly Diagnostic[] {
	const diagnostics: Diagnostic[] = []

	diagnostics.push({
		from: 0,
		to: view.state.doc.length,
		severity: 'error',
		message: "Don't do that!",
	})

	return diagnostics
}

const editorView = new EditorView({
	state: EditorState.create({
		extensions: [
			lineNumbers(),
			history(),
			keymap.of([...defaultKeymap, ...historyKeymap]),

			linter(validateCode),

			EditorView.baseTheme({
				'.cm-tooltip.cm-tooltip-cursor': {
					backgroundColor: '#66b',
					color: 'white',
					border: 'none',
					padding: '2px 7px',
					borderRadius: '4px',
					'& .cm-tooltip-arrow:before': {
						borderTopColor: '#66b',
					},
					'& .cm-tooltip-arrow:after': {
						borderTopColor: 'transparent',
					},
				},
			}),
		],
	}),
})
<style>
.cm-editor {
	height: 100%;
	width: 100%;
	overflow: auto;
}
</style>

I see that the tooltip has a CSS transform applied to it by Codemirror’s logic:

How does it come up with those values?

That’s not a transform, just a position. It computes those from the screen coordinates of the character that the tooltip is at.

Oops, yeah I knew that, I meant to to say “top/left”.

Indeed I would think it is calculating from the top left of the editor based on character positions.

The editor is inside a page. Here’s a more complete screenshot:

Specifically the editor is inside of a flexbox layout. The whole page layout is nested flexbox layouts, including the collapsed panels with editors in each.

I’m not doing much with the editor itself, apart from the above CSS, and flexbox styles apply only to ancestors of .cm-editor. Each cm-editor is wrapped in a div, and those wrapper divs are flex children.

Do you have transforms or contain CSS declarations on any of the parent nodes?

Indeed I have contain higher up for certain flex layouts to keep force restrict content to their areas.


I found a workaround for now, which is to force the position based on pointer position:

<style>
	#wrapperDiv {
		contain: layout /* scope the position:fixed tooltip to each editor */
	}
</style>
<style id="cmTooltipStyle"></style>
wrapperDiv.addEventListener('pointermove', e => {
	const rect = wrapperDiv.getBoundingClientRect()
	const offsetX = e.clientX - rect.left
	const offsetY = e.clientY - rect.top

	cmTooltipStyle.textContent = /*css*/ `
		.cm-tooltip-hover {
			top: ${offsetY + 10 + 'px !important'};
			left: ${offsetX + 5 + 'px !important'};
			pointer-events: none;
		}
	`
})

I don’t currently have transforms, but I would like to use those in another project to position editors in 3D space, which I know will be tricky.

You might be able to work around the contain problem by passing position: "absolute" in your tooltip configuration.

2 Likes

Ah, thanks.

The parent option ended up solving everything. The code is really simple now, the above pointermove and <style> stuff removed:

const editorView = new EditorView({
	state: EditorState.create({
		extensions: [
			lineNumbers(),
			history(),
			keymap.of([...defaultKeymap, ...historyKeymap]),
			linter(props.validateCode),
			tooltips({parent: document.body}), // 👈😀
		],
	}),
})

And now the spacing for tooltips is big too, so they don’t wrap within each editor. :+1:

1 Like