Repository: lightdash/lightdash
Pull request #14342: fix: monaco custom tooltip UI issues
Author: login: rephus
Labels: name: released
Created: 2025-04-09T15:12:10Z | Merged: 2025-04-10T08:54:20Z

<!-- Thanks so much for your PR, your contribution is appreciated! ❤️ -->

Closes: <!-- reference the related issue e.g. #150 -->

### Description:

Before:

![Screenshot from 2025-04-09 14-12-31](https://github.com/user-attachments/assets/d45a29a7-5274-4da3-9976-948beaa2a264)

After

<!-- Add a description of the changes proposed in the pull request. -->
![Screenshot from 2025-04-09 17-08-11](https://github.com/user-attachments/assets/498c8a2a-b178-45bf-bda2-5521af4727bf)

<!-- Even better add a screenshot / gif / loom -->
![Screenshot from 2025-04-09 17-08-21](https://github.com/user-attachments/assets/09ed7931-eaf0-4f2c-8a48-f138098e718a)

### Reviewer actions

- [ ] I have manually tested the changes in the preview environment
- [ ] I have reviewed the code
- [ ] I understand that "request changes" will block this PR from merging



Diff:
diff --git a/packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx b/packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
index c56463569e020..3ef42fd74d58f 100644
--- a/packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
+++ b/packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
@@ -1,4 +1,12 @@
-import { Collapse, Group, Switch } from '@mantine/core';
+import {
+    Box,
+    Collapse,
+    getDefaultZIndex,
+    Group,
+    Switch,
+    Text,
+    Tooltip,
+} from '@mantine/core';
 import { useDebouncedValue } from '@mantine/hooks';
 import {
     Editor,
@@ -6,8 +14,11 @@ import {
     type EditorProps,
     type Monaco,
 } from '@monaco-editor/react';
+import { IconHelpCircle } from '@tabler/icons-react';
 import { type IDisposable, type languages } from 'monaco-editor';
 import { useCallback, useEffect, useState, type FC } from 'react';
+import { useDeepCompareEffect } from 'react-use';
+import MantineIcon from '../../../common/MantineIcon';
 import { isCartesianVisualizationConfig } from '../../../LightdashVisualization/types';
 import { useVisualizationContext } from '../../../LightdashVisualization/useVisualizationContext';
 import { Config } from '../../common/Config';
@@ -26,6 +37,7 @@ const MONACO_DEFAULT_OPTIONS: EditorProps['options'] = {
     glyphMargin: false,
     lineDecorationsWidth: 0,
     lineNumbersMinChars: 0,
+    fixedOverflowWidgets: true,
 };
 let completionProviderDisposable: IDisposable | null = null;
 
@@ -55,10 +67,21 @@ const registerCustomCompletionProvider = (
 
                 const suggestions: languages.CompletionItem[] = fields.map(
                     (field) => {
+                        const textBeforeCursor = model.getValueInRange({
+                            startLineNumber: position.lineNumber,
+                            endLineNumber: position.lineNumber,
+                            startColumn: position.column - 1,
+                            endColumn: position.column,
+                        });
+                        const insertText =
+                            textBeforeCursor === '$'
+                                ? `{${field}} `
+                                : `\${${field}}`;
+
                         return {
-                            label: field,
+                            label: `${field}`,
                             kind: monaco.languages.CompletionItemKind.Class,
-                            insertText: `\${${field}\}`,
+                            insertText,
                             range,
                         };
                     },
@@ -66,6 +89,7 @@ const registerCustomCompletionProvider = (
 
                 return { suggestions };
             },
+            triggerCharacters: ['$'],
         });
 };
 export const TooltipConfig: FC<Props> = ({ fields }) => {
@@ -90,9 +114,37 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         visualizationConfig.chartConfig,
     ]);
 
+    const handleEditorOnChange = (value: string | undefined) => {
+        setTooltipValue(value ?? '');
+    };
+
     const [show, setShow] = useState<boolean>(
         isCartesianChart ? !!visualizationConfig.chartConfig.tooltip : false,
     );
+    const [monacoOptions, setMonacoOptions] = useState<
+        EditorProps['options'] | undefined
+    >();
+
+    useDeepCompareEffect(() => {
+        /** Creates a container that belongs to body, outside of the sidebar
+         * so we can place the autocomplete tooltip and it doesn't overflow
+         * CSS for this component is set on `monaco.css`
+         */
+        const containerId = 'monaco-overflow-container';
+        let container = document.getElementById(containerId);
+        if (!container) {
+            const wrapper = document.createElement('div');
+            wrapper.className = 'monaco-editor';
+            container = document.createElement('div');
+            container.id = containerId;
+            wrapper.appendChild(container);
+            document.getElementById('root')?.appendChild(wrapper);
+        }
+        setMonacoOptions({
+            ...MONACO_DEFAULT_OPTIONS,
+            overflowWidgetsDomNode: container,
+        });
+    }, [monacoOptions]);
 
     const beforeMount: BeforeMount = useCallback(
         (monaco) => {
@@ -101,27 +153,72 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         [fields],
     );
 
+    if (!monacoOptions) return null; // we should not load monaco before options are set with the overflowWidgetsDomNode
     return (
         <Config>
             <Config.Section>
                 <Group spacing="xs" align="center">
                     <Config.Heading>Custom Tooltip</Config.Heading>
                     <Switch checked={show} onChange={() => setShow(!show)} />
+                    <Tooltip
+                        withinPortal={true}
+                        maw={350}
+                        variant="xs"
+                        multiline
+                        label="Use this input to enhance chart tooltips with additional content. You can incorporate HTML code and include dynamic values using the format ${variable_name}. 
+                                    Click here to read more about this on our docs."
+                    >
+                        <MantineIcon
+                            onClick={() => {
+                                window.open(
+                                    'https://docs.lightdash.com/references/chart-types#custom-tooltip',
+                                    '_blank',
+                                );
+                            }}
+                            icon={IconHelpCircle}
+                            size="md"
+                            display="inline"
+                            color="gray"
+                        />
+                    </Tooltip>
                 </Group>
 
                 <Collapse in={show}>
-                    <Editor
-                        beforeMount={beforeMount}
-                        value={tooltipValue}
-                        onChange={(value) => setTooltipValue(value ?? '')}
-                        options={MONACO_DEFAULT_OPTIONS}
-                        language={'html'}
-                        height="400px"
-                        width="100%"
-                        wrapperProps={{
-                            className: 'tooltip-editor',
-                        }}
-                    />
+                    {/* Monaco does not support placeholders, so this is a workaround to show the example tooltip 
+                    we show some text, by giving position absolute, it is placed on top of the editor*/}
+                    {tooltipValue?.length === 0 ? (
+                        <Text
+                            ml="xxs"
+                            pos="absolute"
+                            width="400px"
+                            color="gray.5"
+                            sx={{
+                                pointerEvents: 'none',
+                                zIndex: getDefaultZIndex('overlay'),
+                                fontFamily: 'monospace',
+                            }}
+                        >
+                            {`- Total orders: \${orders_total_amount}`}
+                        </Text>
+                    ) : null}
+                    <Box
+                        sx={(theme) => ({
+                            boxShadow: theme.shadows.subtle,
+                        })}
+                    >
+                        <Editor
+                            beforeMount={beforeMount}
+                            value={tooltipValue}
+                            options={monacoOptions}
+                            onChange={handleEditorOnChange}
+                            language={'html'}
+                            height="76px"
+                            width="100%"
+                            wrapperProps={{
+                                id: 'tooltip-editor-wrapper',
+                            }}
+                        />
+                    </Box>
                 </Collapse>
             </Config.Section>
         </Config>
diff --git a/packages/frontend/src/styles/monaco.css b/packages/frontend/src/styles/monaco.css
index bb5b35867f851..6914a0d190103 100644
--- a/packages/frontend/src/styles/monaco.css
+++ b/packages/frontend/src/styles/monaco.css
@@ -3,10 +3,65 @@
     border-bottom: 2px solid red;
 }
 
-.tooltip-editor {
-    border: 1px solid #ccc;
-    overflow: visible;
+.monaco-editor #monaco-overflow-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 1000;
+    border-radius: 4px;
 }
-.tooltip-editor .suggest-widget {
-    left: 0px !important;
+
+#tooltip-editor-wrapper .view-lines.monaco-mouse-cursor-text {
+    border: 1px solid #e6e6e6;
+    border-radius: 4px;
+}
+
+#monaco-overflow-container .suggest-widget {
+    border-radius: 4px;
+    background-color: white;
+    box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+    border: 1px solid #e5e7eb;
+    width: 700px !important;
+}
+
+#monaco-overflow-container .suggest-widget .monaco-list .monaco-list-row {
+    transition: background-color 0.15s ease;
+    border-radius: 4px;
+}
+
+#monaco-overflow-container .suggest-widget .monaco-list .monaco-list-row:hover {
+    background-color: #f3f4f6;
+}
+
+#monaco-overflow-container
+    .suggest-widget
+    .monaco-list
+    .monaco-list-row.focused {
+    background-color: #f3f4f6;
+    color: #111827;
+}
+
+#monaco-overflow-container
+    .suggest-widget
+    .monaco-list
+    .monaco-list-row
+    .highlight {
+    color: #2563eb;
+    font-weight: 600;
+}
+
+#monaco-overflow-container
+    .suggest-widget
+    .monaco-list
+    .monaco-list-row
+    .monaco-highlighted-label
+    .highlight {
+    color: #2563eb;
+    font-weight: 600;
+}
+
+#monaco-overflow-container .suggest-widget .details {
+    border-top: 1px solid #e5e7eb;
+    background-color: #f9fafb;
+    border-radius: 0 0 8px 8px;
 }


Reviews:
user: login: joaoviana
body: Left some suggestions and a fix to a useEffect bug 👍 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:09:59Z
user: login: joaoviana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:10:24Z
user: login: rephus
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T07:09:09Z
user: login: joaoviana
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T08:19:14Z

Review comments:
diff_hunk: @@ -104,22 +135,61 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         [fields],
     );
 
+    if (!monacoOptions) return null; // we should not load monaco before options are set with the overflowWidgetsDomNode
     return (
         <Config>
             <Config.Section>
                 <Group spacing="xs" align="center">
                     <Config.Heading>Custom Tooltip</Config.Heading>
                     <Switch checked={show} onChange={() => setShow(!show)} />
+                    <Tooltip
+                        withinPortal={true}
+                        maw={350}
+                        variant="s"
path: packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
user: login: joaoviana
body: ```suggestion
                        variant="xs"
```
created_at: 2025-04-09T15:39:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,22 +135,61 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         [fields],
     );
 
+    if (!monacoOptions) return null; // we should not load monaco before options are set with the overflowWidgetsDomNode
     return (
         <Config>
             <Config.Section>
                 <Group spacing="xs" align="center">
                     <Config.Heading>Custom Tooltip</Config.Heading>
                     <Switch checked={show} onChange={() => setShow(!show)} />
+                    <Tooltip
+                        withinPortal={true}
+                        maw={350}
+                        variant="s"
+                        multiline
+                        label="Use this input to enhance chart tooltips with additional content. You can incorporate HTML code and include dynamic values using the format ${variable_name}. 
+                                    Click here to read more about this on our docs."
+                    >
+                        <MantineIcon
+                            onClick={() => {
+                                window.open(
+                                    'https://docs.lightdash.com/references/chart-types#custom-tooltip',
+                                    '_blank',
+                                );
+                            }}
+                            icon={IconHelpCircle}
+                            size="md"
+                            display="inline"
+                            color="gray"
+                        />
+                    </Tooltip>
                 </Group>
 
                 <Collapse in={show}>
+                    {/* Monaco does not support placeholders, so this is a workaround to show the example tooltip 
+                    we show some text, by giving position absolute, it is placed on top of the editor*/}
+                    {tooltipValue?.length === 0 ? (
+                        <Text
+                            sx={{
+                                position: 'absolute',
+                                width: '400px',
+                                zIndex: 10,
+                                marginLeft: 3,
+                                pointerEvents: 'none',
+                            }}
+                            color="gray.5"
+                        >
path: packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
user: login: joaoviana
body: ```
<Text
                            ml="xs"
                            pos="absolute"
                            width="400px"
                            color="gray.5"
                            sx={{
                                pointerEvents: 'none',
                                zIndex: getDefaultZIndex('overlay'),
                                fontFamily: 'monospace',
                            }}
                        >
                            {`- Total orders: \${orders_total_amount}`}
                        </Text>
```
created_at: 2025-04-09T15:43:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,9 +96,37 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         visualizationConfig.chartConfig,
     ]);
 
+    const handleEditorOnChange = (value: string | undefined) => {
+        setTooltipValue(value ?? '');
+    };
+
     const [show, setShow] = useState<boolean>(
         isCartesianChart ? !!visualizationConfig.chartConfig.tooltip : false,
     );
+    const [monacoOptions, setMonacoOptions] = useState<
+        EditorProps['options'] | undefined
+    >();
+
+    useEffect(() => {
+        /** Creates a container that belongs to body, outside of the sidebar
+         * so we can place the autocomplete tooltip and it doesn't overflow
+         * CSS for this component is set on `monaco.css`
+         */
+        const containerId = 'monaco-overflow-container';
+        let container = document.getElementById(containerId);
+        if (!container) {
+            const wrapper = document.createElement('div');
+            wrapper.className = 'monaco-editor';
+            container = document.createElement('div');
+            container.id = containerId;
+            wrapper.appendChild(container);
+            document.getElementById('root')?.appendChild(wrapper);
+        }
+        setMonacoOptions({
+            ...MONACO_DEFAULT_OPTIONS,
+            overflowWidgetsDomNode: container,
+        });
+    }, [monacoOptions]);
path: packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
user: login: joaoviana
body: this causes an infinite render, can you replace `useEffect` with `useDeepCompareEffect` from `react-use`?
created_at: 2025-04-09T16:01:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,22 +135,61 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         [fields],
     );
 
+    if (!monacoOptions) return null; // we should not load monaco before options are set with the overflowWidgetsDomNode
     return (
         <Config>
             <Config.Section>
                 <Group spacing="xs" align="center">
                     <Config.Heading>Custom Tooltip</Config.Heading>
                     <Switch checked={show} onChange={() => setShow(!show)} />
+                    <Tooltip
+                        withinPortal={true}
+                        maw={350}
+                        variant="s"
+                        multiline
+                        label="Use this input to enhance chart tooltips with additional content. You can incorporate HTML code and include dynamic values using the format ${variable_name}. 
+                                    Click here to read more about this on our docs."
+                    >
+                        <MantineIcon
+                            onClick={() => {
+                                window.open(
+                                    'https://docs.lightdash.com/references/chart-types#custom-tooltip',
+                                    '_blank',
+                                );
+                            }}
+                            icon={IconHelpCircle}
+                            size="md"
+                            display="inline"
+                            color="gray"
+                        />
+                    </Tooltip>
                 </Group>
 
                 <Collapse in={show}>
+                    {/* Monaco does not support placeholders, so this is a workaround to show the example tooltip 
+                    we show some text, by giving position absolute, it is placed on top of the editor*/}
+                    {tooltipValue?.length === 0 ? (
+                        <Text
+                            sx={{
+                                position: 'absolute',
+                                width: '400px',
+                                zIndex: 10,
+                                marginLeft: 3,
+                                pointerEvents: 'none',
+                            }}
+                            color="gray.5"
+                        >
+                            {`- Total orders: \${orders_total_amount}`}
+                        </Text>
+                    ) : null}
+
                     <Editor
                         beforeMount={beforeMount}
                         value={tooltipValue}
-                        onChange={(value) => setTooltipValue(value ?? '')}
-                        options={MONACO_DEFAULT_OPTIONS}
+                        options={monacoOptions}
path: packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
user: login: joaoviana
body: I would replace these lines  with this

```
<Box
                        sx={(theme) => ({
                            boxShadow: theme.shadows.subtle,
                        })}
                    >
                        <Editor
                            beforeMount={beforeMount}
                            value={tooltipValue}
                            options={monacoOptions}
                            onChange={handleEditorOnChange}
                            language={'html'}
                            height="200px"
                            width="100%"
                            wrapperProps={{
                                id: 'tooltip-editor-wrapper',
                            }}
                        />
                    </Box>
```

and if you update your css to look like this you get some pretty suggestions + border in the editor ;) 

```css
.editorError {
    background-color: yellow;
    border-bottom: 2px solid red;
}

.monaco-editor #monaco-overflow-container {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1000;
    border-radius: 4px;
}

#tooltip-editor-wrapper .view-lines.monaco-mouse-cursor-text {
    border: 1px solid #e6e6e6;
    border-radius: 4px;
}

#monaco-overflow-container .suggest-widget {
    max-width: 700px !important;
    border-radius: 4px;
    background-color: white;
    box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
    border: 1px solid #e5e7eb;
}

#monaco-overflow-container .suggest-widget .monaco-list .monaco-list-row {
    transition: background-color 0.15s ease;
    border-radius: 4px;
}

#monaco-overflow-container .suggest-widget .monaco-list .monaco-list-row:hover {
    background-color: #f3f4f6;
}

#monaco-overflow-container
    .suggest-widget
    .monaco-list
    .monaco-list-row.focused {
    background-color: #f3f4f6;
    color: #111827;
}

#monaco-overflow-container
    .suggest-widget
    .monaco-list
    .monaco-list-row
    .highlight {
    color: #2563eb;
    font-weight: 600;
}

#monaco-overflow-container
    .suggest-widget
    .monaco-list
    .monaco-list-row
    .monaco-highlighted-label
    .highlight {
    color: #2563eb;
    font-weight: 600;
}

#monaco-overflow-container .suggest-widget .details {
    border-top: 1px solid #e5e7eb;
    background-color: #f9fafb;
    border-radius: 0 0 8px 8px;
}

```


created_at: 2025-04-09T16:09:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,22 +135,61 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         [fields],
     );
 
+    if (!monacoOptions) return null; // we should not load monaco before options are set with the overflowWidgetsDomNode
     return (
         <Config>
             <Config.Section>
                 <Group spacing="xs" align="center">
                     <Config.Heading>Custom Tooltip</Config.Heading>
                     <Switch checked={show} onChange={() => setShow(!show)} />
+                    <Tooltip
+                        withinPortal={true}
+                        maw={350}
+                        variant="s"
+                        multiline
+                        label="Use this input to enhance chart tooltips with additional content. You can incorporate HTML code and include dynamic values using the format ${variable_name}. 
+                                    Click here to read more about this on our docs."
+                    >
+                        <MantineIcon
+                            onClick={() => {
+                                window.open(
+                                    'https://docs.lightdash.com/references/chart-types#custom-tooltip',
+                                    '_blank',
+                                );
+                            }}
+                            icon={IconHelpCircle}
+                            size="md"
+                            display="inline"
+                            color="gray"
+                        />
+                    </Tooltip>
                 </Group>
 
                 <Collapse in={show}>
+                    {/* Monaco does not support placeholders, so this is a workaround to show the example tooltip 
+                    we show some text, by giving position absolute, it is placed on top of the editor*/}
+                    {tooltipValue?.length === 0 ? (
+                        <Text
+                            sx={{
+                                position: 'absolute',
+                                width: '400px',
+                                zIndex: 10,
+                                marginLeft: 3,
+                                pointerEvents: 'none',
+                            }}
+                            color="gray.5"
+                        >
+                            {`- Total orders: \${orders_total_amount}`}
+                        </Text>
+                    ) : null}
+
                     <Editor
                         beforeMount={beforeMount}
                         value={tooltipValue}
-                        onChange={(value) => setTooltipValue(value ?? '')}
-                        options={MONACO_DEFAULT_OPTIONS}
+                        options={monacoOptions}
path: packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
user: login: joaoviana
body: <img width="460" alt="image" src="https://github.com/user-attachments/assets/9ad42ff2-7906-4eca-b73b-69b60f90b476" />

created_at: 2025-04-09T16:10:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -104,22 +135,61 @@ export const TooltipConfig: FC<Props> = ({ fields }) => {
         [fields],
     );
 
+    if (!monacoOptions) return null; // we should not load monaco before options are set with the overflowWidgetsDomNode
     return (
         <Config>
             <Config.Section>
                 <Group spacing="xs" align="center">
                     <Config.Heading>Custom Tooltip</Config.Heading>
                     <Switch checked={show} onChange={() => setShow(!show)} />
+                    <Tooltip
+                        withinPortal={true}
+                        maw={350}
+                        variant="s"
+                        multiline
+                        label="Use this input to enhance chart tooltips with additional content. You can incorporate HTML code and include dynamic values using the format ${variable_name}. 
+                                    Click here to read more about this on our docs."
+                    >
+                        <MantineIcon
+                            onClick={() => {
+                                window.open(
+                                    'https://docs.lightdash.com/references/chart-types#custom-tooltip',
+                                    '_blank',
+                                );
+                            }}
+                            icon={IconHelpCircle}
+                            size="md"
+                            display="inline"
+                            color="gray"
+                        />
+                    </Tooltip>
                 </Group>
 
                 <Collapse in={show}>
+                    {/* Monaco does not support placeholders, so this is a workaround to show the example tooltip 
+                    we show some text, by giving position absolute, it is placed on top of the editor*/}
+                    {tooltipValue?.length === 0 ? (
+                        <Text
+                            sx={{
+                                position: 'absolute',
+                                width: '400px',
+                                zIndex: 10,
+                                marginLeft: 3,
+                                pointerEvents: 'none',
+                            }}
+                            color="gray.5"
+                        >
path: packages/frontend/src/components/VisualizationConfigs/ChartConfigPanel/Legend/TooltipConfig.tsx
user: login: rephus
body: nice
created_at: 2025-04-10T07:09:09Z
author_association: COLLABORATOR

PR comments:
user: login: owlas
created_at: 2025-04-10T07:44:23Z
body: @rephus could we reduce the height of the input box? 3-4 lines should be good!
author_association: COLLABORATOR
user: login: lightdash-bot
created_at: 2025-04-10T08:57:08Z
body: :tada: This PR is included in version 0.1567.4 :tada:

The release is available on:
- `0.1567.4`
- [GitHub release](https://github.com/lightdash/lightdash/releases/tag/0.1567.4)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR