question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Show & Hide BubbleMenu programmatically

See original GitHub issue

What problem are you facing?

I am trying to make a prompt for link input and want to reuse the BubbleMenu plugin. I am using BubbleMenu to open a toolbar on selection, which contains a button for creating a link. I want to open a second BubbleMenu positioned at the selection when the user clicks the link toolbar button but I haven’t been able to come up with a way to do this. I know BubbleMenu has the shouldShow option but I can’t figure out a way to use this to be toggled by a button press.

What’s the solution you would like to see?

It would be nice to expose the tooltip’s show & hide methods as editor commands so I could do something like this, editor.commands.showBubbleMenu(id).

What alternatives did you consider?

I am trying to get it to work with shouldShow but haven’t gotten anywhere. I am considering taking BubbleMenu and making a similar extension but with the ability to programmatically trigger the tooltip but I wanted to open an issue first.

Anything to add? (optional)

No response

Are you sponsoring us?

  • Yes, I’m a sponsor. 💖

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:7
  • Comments:15 (3 by maintainers)

github_iconTop GitHub Comments

4reactions
sjdemartinicommented, Jan 24, 2022

I ended up implementing my own simple “Bubble Menu” to get it to follow React paradigms more closely (so that changes to “should show” are always rendered/reflected). If anyone is seeing this and happens to be using MaterialUI (or would want to use https://github.com/atomiks/tippyjs-react/, since that could easily be swapped for the Popper here), I’ve pasted my example below in case it helps.

The implementation below has two main advantages:

  1. The BubbleMenu visibility can be controlled programmatically using the open prop. Any updates to the prop will re-render as you’d intend. (Resolves this issue.)
  2. The popper is rendered via React Portal under the hood under the document body, so it can’t get visually clipped by the editor boundaries. The Tiptap BubbleMenuPlugin places its tippy DOM element within the editor DOM structure, so it will get clipped/hidden by the edges of the editor, especially noticeable when there is no content in the editor yet (so it’ll get sliced off at the top of the editor). It’s not possible to use a React Portal or appendTo: () => document.body there as a workaround due to the way in which the element is dynamically created/destroyed via tippy inside Tiptap, thereby preventing interactivity (see https://github.com/ueberdosis/tiptap/issues/2292).

Minimal version of the component code:

import { Popper } from "@mui/material";
import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core";

type Props = {
  editor: Editor;
  open: boolean;
  children: React.ReactNode;
};

const ControlledBubbleMenu: React.FC<Props> = ({
  editor,
  open,
  children,
}: Props) => (
  <Popper
    open={open}
    placement="top"
    modifiers={[
      {
        name: "offset",
        options: {
          // Add a slight vertical offset for the popper from the current selection
          offset: [0, 4],
        },
      },
      {
        name: "flip",
        enabled: true,
        options: {
          // We'll reposition (to one of the below fallback placements) whenever our Popper goes
          // outside of the editor. (This is necessary since our children aren't actually rendered
          // here, but instead with a portal, so the editor DOM node isn't a parent.)
          boundary: editor.options.element,
          fallbackPlacements: [
            "bottom",
            "top-start",
            "bottom-start",
            "top-end",
            "bottom-end",
          ],
          padding: 8,
        },
      },
    ]}
    anchorEl={() => {
      // The logic here is taken from the positioning implementation in Tiptap's BubbleMenuPlugin
      // https://github.com/ueberdosis/tiptap/blob/16bec4e9d0c99feded855b261edb6e0d3f0bad21/packages/extension-bubble-menu/src/bubble-menu-plugin.ts#L183-L193
      const { ranges } = editor.state.selection;
      const from = Math.min(...ranges.map((range) => range.$from.pos));
      const to = Math.max(...ranges.map((range) => range.$to.pos));

      return {
        getBoundingClientRect: () => {
          if (isNodeSelection(editor.state.selection)) {
            const node = editor.view.nodeDOM(from) as HTMLElement;

            if (node) {
              return node.getBoundingClientRect();
            }
          }

          return posToDOMRect(editor.view, from, to);
        },
      };
    }}
  >
    {children}
  </Popper>
);

export default ControlledBubbleMenu;

which can be used nearly identically to the BubbleMenu from @tiptap/react, like:

<div>
  {editor && (
    <ControlledBubbleMenu editor={editor} open={shouldShow}>
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        className={editor.isActive('bold') ? 'is-active' : ''}
      >
        bold
      </button>
    </ControlledBubbleMenu>
  )}
</div>

where shouldShow is whatever you want it to be (e.g. based on some React state variable and/or editor state)

3reactions
ehyndscommented, Aug 1, 2022

Here’s a floating-ui implementation of @sjdemartini’s ControlledBubbleMenu:

import { useFloating, autoUpdate, offset, flip } from '@floating-ui/react-dom';
import { Editor, isNodeSelection, posToDOMRect } from '@tiptap/core';
import { ReactNode, useLayoutEffect } from 'react';

type Props = {
  editor: Editor;
  open: boolean;
  children: ReactNode;
};

// Adapted from https://github.com/ueberdosis/tiptap/issues/2305#issuecomment-1020665146
export const ControlledBubbleMenu = ({ editor, open, children }: Props) => {
  const { x, y, strategy, reference, floating } = useFloating({
    strategy: 'fixed',
    whileElementsMounted: autoUpdate,
    placement: 'top',
    middleware: [
      offset({ mainAxis: 8 }),
      flip({
        padding: 8,
        boundary: editor.options.element,
        fallbackPlacements: [
          'bottom',
          'top-start',
          'bottom-start',
          'top-end',
          'bottom-end',
        ],
      }),
    ],
  });

  useLayoutEffect(() => {
    const { ranges } = editor.state.selection;
    const from = Math.min(...ranges.map((range) => range.$from.pos));
    const to = Math.max(...ranges.map((range) => range.$to.pos));

    reference({
      getBoundingClientRect() {
        if (isNodeSelection(editor.state.selection)) {
          const node = editor.view.nodeDOM(from) as HTMLElement;

          if (node) {
            return node.getBoundingClientRect();
          }
        }

        return posToDOMRect(editor.view, from, to);
      },
    });
  }, [reference, editor]);

  if (!open) {
    return null;
  }

  return (
    <div
      ref={floating}
      style={{
        position: strategy,
        top: y ?? 0,
        left: x ?? 0,
      }}
    >
      {children}
    </div>
  );
};

Usage:

<ControlledBubbleMenu open={!editor.view.state.selection.empty} editor={editor}>
  // your custom toolbar
</ControlledBubbleMenu>
Read more comments on GitHub >

github_iconTop Results From Across the Web

How do I hide a menu item in the actionbar? - android
In onCreateOptionsMenu() , check for the flag/condition and show or hide it the following way: MenuItem item = menu.findItem(R.id.
Read more >
Bubble Menu – Tiptap Editor
The BubbleMenu debounces the update method to allow the bubble menu to not be updated on every selection update. This can be controlled...
Read more >
Programmatically show and hide menu bar items - aka switch ...
Hello, I'm currently working on a more complex LabVIEW-Project and would like to programmatically show and hide items in the menu bar.
Read more >
CHANGELOG.md ... - GitLab
... Allow to hide personalization questions on New Group page by @wwwjon (merge ... Show blocked status label in deployments view (merge request)...
Read more >
Diff - 2fd987c9e8..bccf0827db - chromium/src - Git at Google
Used when the bubble menu's settings button is tapped. ... + // + // This gives 3 actions: + // - Animate showing,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found