fix(ui): code field adjusts its height to its content dynamically. Scrolling over the container is not prevented. (#8209)

Closes #8051.

- The scrolling problem reported in the issue is solved with Monaco's
`alwaysConsumeMouseWheel` property.
- In addition to that, it is necessary to dynamically adjust the height
of the editor so that it fits its content and does not require
scrolling.
- Additionally, I disabled the `overviewRuler` which is the indicator
strip on the side (above the scrollbar) that makes no sense when there
is no scroll.

**Gotchas**

- Unfortunately, there is a bit of CLS since the editor doesn't know the
height of its content before rendering. In Lexical these things are
possible since it has a lifecycle that allows interaction before or
after rendering, but this is not the case with Monaco.
- I've noticed that sometimes when I press enter the letters in the
editor flicker or move with a small, rapid shake. Maybe it has to do
with the new height being calculated as an effect.


## Before


https://github.com/user-attachments/assets/0747f79d-a3ac-42ae-8454-0bf46dc43f34


## After


https://github.com/user-attachments/assets/976ab97c-9d20-4e93-afb5-023083a6608b
This commit is contained in:
Germán Jabloñski
2024-09-17 16:55:09 -03:00
committed by GitHub
parent c0aad3cccb
commit f011e4fa26
6 changed files with 69 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import EditorImport from '@monaco-editor/react'
import React from 'react'
import React, { useState } from 'react'
import type { Props } from './types.js'
@@ -13,8 +13,8 @@ const Editor = (EditorImport.default || EditorImport) as unknown as typeof Edito
const baseClass = 'code-editor'
const CodeEditor: React.FC<Props> = (props) => {
const { className, height, options, readOnly, ...rest } = props
const { className, options, readOnly, ...rest } = props
const [dynamicHeight, setDynamicHeight] = useState(20)
const { theme } = useTheme()
const classes = [
@@ -29,14 +29,18 @@ const CodeEditor: React.FC<Props> = (props) => {
return (
<Editor
className={classes}
height={height}
loading={<ShimmerEffect height={height} />}
loading={<ShimmerEffect height={dynamicHeight} />}
options={{
detectIndentation: true,
hideCursorInOverviewRuler: true,
minimap: {
enabled: false,
},
overviewRulerBorder: false,
readOnly: Boolean(readOnly),
scrollbar: {
alwaysConsumeMouseWheel: false,
},
scrollBeyondLastLine: false,
tabSize: 2,
wordWrap: 'on',
@@ -44,6 +48,19 @@ const CodeEditor: React.FC<Props> = (props) => {
}}
theme={theme === 'dark' ? 'vs-dark' : 'vs'}
{...rest}
// Since we are not building an IDE and the container
// can already have scrolling, we want the height of the
// editor to fit its content.
// See: https://github.com/microsoft/monaco-editor/discussions/3677
height={dynamicHeight}
onChange={(value, ev) => {
rest.onChange?.(value, ev)
setDynamicHeight(value.split('\n').length * 18 + 2)
}}
onMount={(editor, monaco) => {
rest.onMount?.(editor, monaco)
setDynamicHeight(editor.getValue().split('\n').length * 18 + 2)
}}
/>
)
}

View File

@@ -10,11 +10,9 @@ const LazyEditor = lazy(() => import('./CodeEditor.js'))
export type { Props }
export const CodeEditor: React.FC<Props> = (props) => {
const { height = '35vh' } = props
return (
<Suspense fallback={<ShimmerEffect height={height} />}>
<LazyEditor {...props} height={height} />
<Suspense fallback={<ShimmerEffect />}>
<LazyEditor {...props} />
</Suspense>
)
}

View File

@@ -281,3 +281,13 @@ export const TabBlock: Block = {
},
],
}
export const CodeBlock: Block = {
fields: [
{
name: 'code',
type: 'code',
},
],
slug: 'code',
}

View File

@@ -1016,5 +1016,27 @@ describe('lexicalBlocks', () => {
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('dynamic height of code editor is correctly calculated', async () => {
await navigateToLexicalFields()
const codeEditor = page.locator('.code-editor')
await codeEditor.scrollIntoViewIfNeeded()
await expect(codeEditor).toBeVisible()
const height = (await codeEditor.boundingBox()).height
await expect(() => {
expect(height).toBe(56)
}).toPass()
await codeEditor.click()
await page.keyboard.press('Enter')
const height2 = (await codeEditor.boundingBox()).height
await expect(() => {
expect(height2).toBe(74)
}).toPass()
})
})
})

View File

@@ -303,6 +303,17 @@ export function generateLexicalRichText(): TypedEditorState<
blockType: 'tabBlock',
},
},
{
format: '',
type: 'block',
version: 2,
fields: {
id: '666c9e0b189d72626ea301fa',
blockName: '',
blockType: 'code',
code: 'Some code\nhello\nworld',
},
},
],
direction: 'ltr',
},

View File

@@ -20,6 +20,7 @@ import {
import { lexicalFieldsSlug } from '../../slugs.js'
import {
CodeBlock,
ConditionalLayoutBlock,
RadioButtonsBlock,
RelationshipBlock,
@@ -80,6 +81,7 @@ const editorConfig: ServerEditorConfig = {
RadioButtonsBlock,
ConditionalLayoutBlock,
TabBlock,
CodeBlock,
],
inlineBlocks: [
{