steven-tey / novel

Notion-style WYSIWYG editor with AI-powered autocompletion.
https://novel.sh
Apache License 2.0
12.7k stars 1.04k forks source link

bug: Can't close bubble menu #409

Open eleviven opened 3 months ago

eleviven commented 3 months ago

Provide environment information

System: OS: macOS 14.5 CPU: (8) arm64 Apple M1 Memory: 76.72 MB / 8.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 21.7.2 - /opt/homebrew/bin/node Yarn: 1.22.19 - ~/.nvm/versions/node/v20.5.1/bin/yarn npm: 10.8.1 - ~/Desktop/Document/Folder/frontend/node_modules/.bin/npm pnpm: 9.1.0 - ~/.nvm/versions/node/v20.5.1/bin/pnpm

Describe the bug

When I open the popover from the bubble menu and click outside, the popover closes, but the bubble menu remains open. It works as expected when I click options on the bubble menu that are not popovers. You can observe the same issue on the demo site of Novel.

Link to reproduction

https://novel.sh/

To reproduce

It's the wrapper of options which is Bubble Root component

const EditorBubbles: React.FC<EditorBubblesProps> = () => {
  return (
    <Novel.EditorBubble
      tippyOptions={{
        placement: 'top',
      }}
      className={twMerge(
        'flex items-center w-fit max-w-[90vw] overflow-hidden rounded-md border p-1',
        'border-neutral-2 bg-white shadow-xl',
        'dark:(bg-neutral-8 border-neutral-7)'
      )}
    >
      <NodeSelector />
      <Separator orientation="vertical" />
      <LinkSelector />
      <Separator orientation="vertical" />
      <TextButtons />
      <Separator orientation="vertical" />
      <ColorSelector />
    </Novel.EditorBubble>
  );
};

And it's the Popover element on bubble menu

export const NodeSelector = () => {
  const { editor } = useEditor();
  if (!editor) return null;

  const activeItem = items.filter((item) => item.isActive(editor)).pop() ?? {
    name: 'Multiple',
  };

  return (
    <Popover.Root modal={true}>
      <Popover.Trigger>
        <EditorBubble.Button>
          {activeItem.name}
          <LuChevronDown className="h-4 w-4" />
        </EditorBubble.Button>
      </Popover.Trigger>

      <Popover.Content
        sideOffset={5}
        align="start"
        className="w-48 p-1 rounded-lg dark:(bg-neutral-8 border-neutral-7)"
      >
        {items.map((item, index) => (
          <EditorBubble.Item
            key={index}
            onSelect={(editor) => {
              item.command(editor);
            }}
          >
            <div className="flex items-center space-x-2">
              <div className="rounded-sm border p-1">
                <item.icon className="h-3 w-3" />
              </div>
              <span>{item.name}</span>
            </div>
            {activeItem.name === item.name && <LuCheck className="h-4 w-4" />}
          </EditorBubble.Item>
        ))}
      </Popover.Content>
    </Popover.Root>
  );
};

Additional information

No response

momueh commented 1 month ago

I have the same issue. Did you find a fix or workaround for this?

eleviven commented 1 month ago

@momueh I did. It's not ideal but it works.

const EditorBubbles: React.FC<EditorBubblesProps> = () => {
  // Added state to keep random value. I'm using current date here
  const [key, setKey] = useState(Date.now());

  // We will trigger this function when some popover closes. This will update key
  const handleOpenChange = (value: boolean) => {
    if (value) return;
    setKey(Date.now());
  };

  return (
    <Novel.EditorBubble
      // And when we update key, this part will be rendered and closed.
      key={key}
      tippyOptions={{
        placement: "top",
      }}
    >
      <NodeSelector onOpenChange={handleOpenChange} />
      <Separator orientation="vertical" />
      <LinkSelector onOpenChange={handleOpenChange} />
      <Separator orientation="vertical" />
      <TextButtons />
      <Separator orientation="vertical" />
      <ColorSelector onOpenChange={handleOpenChange} />
    </Novel.EditorBubble>
  );
};