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:
@@ -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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -281,3 +281,13 @@ export const TabBlock: Block = {
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const CodeBlock: Block = {
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'code',
|
||||
},
|
||||
],
|
||||
slug: 'code',
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user