diff --git a/package.json b/package.json index 3bcd5075747ea..2c71bd4dd679f 100644 --- a/package.json +++ b/package.json @@ -1064,7 +1064,6 @@ "react-popper-tooltip": "^3.1.1", "react-redux": "^7.2.8", "react-resizable": "^3.0.4", - "react-resize-detector": "^7.1.1", "react-reverse-portal": "^2.1.0", "react-router": "^5.3.4", "react-router-config": "^5.1.1", diff --git a/packages/shared-ux/code_editor/impl/BUILD.bazel b/packages/shared-ux/code_editor/impl/BUILD.bazel index ad571cb379afd..24f18820496a4 100644 --- a/packages/shared-ux/code_editor/impl/BUILD.bazel +++ b/packages/shared-ux/code_editor/impl/BUILD.bazel @@ -25,7 +25,6 @@ BUNDLER_DEPS = [ "@npm//react", "@npm//tslib", "@npm//react-monaco-editor", - "@npm//react-resize-detector", ] js_library( diff --git a/packages/shared-ux/code_editor/impl/code_editor.stories.tsx b/packages/shared-ux/code_editor/impl/code_editor.stories.tsx index 38c063e4ebe2b..e4a78b328cbe3 100644 --- a/packages/shared-ux/code_editor/impl/code_editor.stories.tsx +++ b/packages/shared-ux/code_editor/impl/code_editor.stories.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { monaco as monacoEditor } from '@kbn/monaco'; @@ -32,7 +32,13 @@ const argTypes = mock.getArgumentTypes(); export const Basic = (params: CodeEditorStorybookParams) => { return ( - + ); }; @@ -199,3 +205,39 @@ export const HoverProvider = () => { ); }; + +export const AutomaticResize = (params: CodeEditorStorybookParams) => { + return ( +
+ +
+ ); +}; + +AutomaticResize.argTypes = argTypes; + +export const FitToContent = (params: CodeEditorStorybookParams) => { + const [value, setValue] = useState('hello'); + return ( + { + setValue(newValue); + action('on change'); + }} + value={value} + fitToContent={{ minLines: 3, maxLines: 5 }} + options={{ automaticLayout: true }} + /> + ); +}; + +FitToContent.argTypes = argTypes; diff --git a/packages/shared-ux/code_editor/impl/code_editor.tsx b/packages/shared-ux/code_editor/impl/code_editor.tsx index b41906d5ed456..54be81c2df4b5 100644 --- a/packages/shared-ux/code_editor/impl/code_editor.tsx +++ b/packages/shared-ux/code_editor/impl/code_editor.tsx @@ -7,7 +7,6 @@ */ import React, { useState, useRef, useCallback, useMemo, useEffect, KeyboardEvent } from 'react'; -import { useResizeDetector } from 'react-resize-detector'; import ReactMonacoEditor, { type MonacoEditorProps as ReactMonacoEditorProps, } from 'react-monaco-editor'; @@ -140,6 +139,15 @@ export interface CodeEditorProps { * Alternate text to display, when an attempt is made to edit read only content. (Defaults to "Cannot edit in read-only editor") */ readOnlyMessage?: string; + + /** + * Enables the editor to grow vertically to fit its content. + * This option overrides the `height` option. + */ + fitToContent?: { + minLines?: number; + maxLines?: number; + }; } export const CodeEditor: React.FC = ({ @@ -168,6 +176,7 @@ export const CodeEditor: React.FC = ({ readOnlyMessage = i18n.translate('sharedUXPackages.codeEditor.readOnlyMessage', { defaultMessage: 'Cannot edit in read-only editor', }), + fitToContent, }) => { const { colorMode, euiTheme } = useEuiTheme(); const useDarkTheme = useDarkThemeProp ?? colorMode === 'DARK'; @@ -189,7 +198,7 @@ export const CodeEditor: React.FC = ({ const isReadOnly = options?.readOnly ?? false; - const _editor = useRef(null); + const [_editor, setEditor] = useState(null); const _placeholderWidget = useRef(null); const isSuggestionMenuOpen = useRef(false); const editorHint = useRef(null); @@ -197,21 +206,10 @@ export const CodeEditor: React.FC = ({ const [isHintActive, setIsHintActive] = useState(true); - const _updateDimensions = useCallback(() => { - _editor.current?.layout(); - }, []); - - useResizeDetector({ - handleWidth: true, - handleHeight: true, - onResize: _updateDimensions, - refreshMode: 'debounce', - }); - const startEditing = useCallback(() => { setIsHintActive(false); - _editor.current?.focus(); - }, []); + _editor?.focus(); + }, [_editor]); const stopEditing = useCallback(() => { setIsHintActive(true); @@ -391,8 +389,6 @@ export const CodeEditor: React.FC = ({ remeasureFonts(); - _editor.current = editor; - const textbox = editor.getDomNode()?.getElementsByTagName('textarea')[0]; if (textbox) { // Make sure the textarea is not directly accessible with TAB @@ -435,6 +431,7 @@ export const CodeEditor: React.FC = ({ } editorDidMount?.(editor); + setEditor(editor); }, [editorDidMount, onBlurMonaco, onKeydownMonaco, readOnlyMessage] ); @@ -454,16 +451,18 @@ export const CodeEditor: React.FC = ({ }, []); useEffect(() => { - if (placeholder && !value && _editor.current) { + if (placeholder && !value && _editor) { // Mounts editor inside constructor - _placeholderWidget.current = new PlaceholderWidget(placeholder, euiTheme, _editor.current); + _placeholderWidget.current = new PlaceholderWidget(placeholder, euiTheme, _editor); } return () => { _placeholderWidget.current?.dispose(); _placeholderWidget.current = null; }; - }, [placeholder, value, euiTheme]); + }, [placeholder, value, euiTheme, _editor]); + + useFitToContent({ editor: _editor, fitToContent, isFullScreen }); const { CopyButton } = useCopy({ isCopyable, value }); @@ -512,7 +511,7 @@ export const CodeEditor: React.FC = ({ value={value} onChange={onChange} width={isFullScreen ? '100vw' : width} - height={isFullScreen ? '100vh' : height} + height={isFullScreen ? '100vh' : fitToContent ? undefined : height} editorWillMount={_editorWillMount} editorDidMount={_editorDidMount} editorWillUnmount={_editorWillUnmount} @@ -640,3 +639,40 @@ const useCopy = ({ isCopyable, value }: { isCopyable: boolean; value: string }) return { showCopyButton, CopyButton }; }; + +const useFitToContent = ({ + editor, + fitToContent, + isFullScreen, +}: { + editor: monaco.editor.IStandaloneCodeEditor | null; + isFullScreen: boolean; + fitToContent?: { minLines?: number; maxLines?: number }; +}) => { + const isFitToContent = !!fitToContent; + const minLines = fitToContent?.minLines; + const maxLines = fitToContent?.maxLines; + useEffect(() => { + if (!editor) return; + if (isFullScreen) return; + if (!isFitToContent) return; + + const updateHeight = () => { + const contentHeight = editor.getContentHeight(); + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const minHeight = (minLines ?? 1) * lineHeight; + let maxHeight = maxLines ? maxLines * lineHeight : contentHeight; + maxHeight = Math.max(minHeight, maxHeight); + editor.layout({ + height: Math.min(maxHeight, Math.max(minHeight, contentHeight)), + width: editor.getLayoutInfo().width, + }); + }; + updateHeight(); + const disposable = editor.onDidContentSizeChange(updateHeight); + return () => { + disposable.dispose(); + editor.layout(); // reset the layout that was controlled by the fitToContent + }; + }, [editor, isFitToContent, minLines, maxLines, isFullScreen]); +}; diff --git a/packages/shared-ux/code_editor/mocks/monaco_mock/index.tsx b/packages/shared-ux/code_editor/mocks/monaco_mock/index.tsx index f5b2fc9ab4108..d9b2d4093e67f 100644 --- a/packages/shared-ux/code_editor/mocks/monaco_mock/index.tsx +++ b/packages/shared-ux/code_editor/mocks/monaco_mock/index.tsx @@ -106,7 +106,9 @@ export const MockedMonacoEditor = ({ className?: string; ['data-test-subj']?: string; }) => { - editorWillMount?.(monaco); + useComponentWillMount(() => { + editorWillMount?.(monaco); + }); useEffect(() => { editorDidMount?.( @@ -133,3 +135,11 @@ export const MockedMonacoEditor = ({ ); }; + +const useComponentWillMount = (cb: Function) => { + const willMount = React.useRef(true); + + if (willMount.current) cb(); + + willMount.current = false; +}; diff --git a/src/plugins/kibana_react/public/url_template_editor/styles.scss b/src/plugins/kibana_react/public/url_template_editor/styles.scss index 99379b21454ec..1bff881958076 100644 --- a/src/plugins/kibana_react/public/url_template_editor/styles.scss +++ b/src/plugins/kibana_react/public/url_template_editor/styles.scss @@ -1,5 +1,5 @@ .urlTemplateEditor__container { .monaco-editor .lines-content.monaco-editor-background { - margin: $euiSizeS; + margin: 0 $euiSizeS; } } diff --git a/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx b/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx index 770753bc8c35c..13773396eba76 100644 --- a/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx +++ b/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx @@ -22,6 +22,7 @@ export interface UrlTemplateEditorVariable { export interface UrlTemplateEditorProps { value: string; height?: CodeEditorProps['height']; + fitToContent?: CodeEditorProps['fitToContent']; variables?: UrlTemplateEditorVariable[]; onChange: CodeEditorProps['onChange']; onEditor?: (editor: monaco.editor.IStandaloneCodeEditor) => void; @@ -31,6 +32,7 @@ export interface UrlTemplateEditorProps { export const UrlTemplateEditor: React.FC = ({ height = 105, + fitToContent, value, variables, onChange, @@ -127,6 +129,7 @@ export const UrlTemplateEditor: React.FC = ({ = ({ }, wordWrap: 'on', wrappingIndent: 'none', + automaticLayout: true, + scrollBeyondLastLine: false, + overviewRulerLanes: 0, + padding: { top: 8, bottom: 8 }, }} /> diff --git a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx index 0495f2d61063c..4db2510ba22e5 100644 --- a/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx +++ b/src/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -88,6 +88,7 @@ export const UrlDrilldownCollectConfig: React.FC labelAppend={variablesDropdown} >