Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The menu always opens to the bottom from the anchor element and overlaps container. #788

Open
Kost927 opened this issue Jan 8, 2025 · 3 comments
Labels
bug Something isn't working

Comments

@Kost927
Copy link

Kost927 commented Jan 8, 2025

Describe the bug
The menu always opens to the bottom from the anchor element.

To Reproduce
Steps to reproduce the behavior:
add rich editor on the bottom of the screen
open mention menu

Expected behavior
The menu doesn't overlap with the screen at the bottom.

Screenshots

https://github.com/user-attachments/assets/10ad8a64-eba3-4f4c-a75a-78ca8d723ac6
Screenshot 2025-01-08 at 14 03 06

Desktop (please complete the following information):
any device, any browser

Additional context
We would like to choose menu placement, or automatically place it if the menu overlaps the container.

Copy link

dosubot bot commented Jan 8, 2025

Hey @Kost927! 👋 I'm here to help you with bug fixes, answer questions, and assist you in becoming a contributor.

To address the issue of the menu overlapping the screen at the bottom, you can modify the useAnchorRef function to adjust the menu's position dynamically. Here's a solution that ensures the menu doesn't overflow the screen:

useEffect(() => {
  if (!anchor) {
    return;
  }
  if (!render) {
    if (anchorChild) {
      anchorChild.remove();
      setAnchorChild(null);
    }
    return;
  }
  const { height, bottom } = anchor.getBoundingClientRect();
  const newAnchorChild = anchorChild ?? document.createElement("div");
  newAnchorChild.style.position = "absolute";
  newAnchorChild.style.left = "0";
  newAnchorChild.style.right = "0";
  newAnchorChild.style.paddingTop = `${height}px`;

  // Adjust the position if the menu would overflow the screen
  if (bottom + height > window.innerHeight) {
    newAnchorChild.style.bottom = "0";
    newAnchorChild.style.top = "auto";
  } else {
    newAnchorChild.style.top = "0";
    newAnchorChild.style.bottom = "auto";
  }

  anchor.prepend(newAnchorChild);
  if (!anchorChild) {
    setAnchorChild(newAnchorChild);
  }
  const anchorObserver = new ResizeObserver(([entry]) => {
    newAnchorChild.style.paddingTop = `${entry.contentRect.height}px`;
  });
  anchorObserver.observe(anchor);
  setTimeout(() => {
    newAnchorChild.className = comboboxAnchorClassName ?? "";
  });
  return () => {
    anchorObserver.disconnect();
    anchor.removeChild(newAnchorChild);
  };
}, [anchor, render, anchorChild, comboboxAnchorClassName]);

This code checks if the menu would overflow the screen at the bottom and adjusts its position accordingly, ensuring it stays within the viewport [1].

To continue talking to Dosu, mention @dosu.


Help Dosu learn! Give it feedback: Great Response | Irrelevant Answer | Incorrect Sources | Too Verbose | Hallucination | Other

@dosubot dosubot bot added the bug Something isn't working label Jan 8, 2025
@Kost927
Copy link
Author

Kost927 commented Jan 9, 2025

Screenshot 2025-01-09 at 09 23 26 Also, it would be great to have an open state to debug the menu, now, when you type "@" for example, the menu opens, but when you want to check styles or something else, you have to blur and the menu closes. So is it possible to keep it open?

@Kost927
Copy link
Author

Kost927 commented Jan 9, 2025

wrote a huck, if someone will need it in a feature.

import { FC, useEffect, useRef, useState } from 'react';

import { BeautifulMentionsMenuProps } from 'lexical-beautiful-mentions';
import { createPortal } from 'react-dom';
import { fullScreenContainerId } from 'shared/constants/constants';

import styles from './CustomMentionMenu.module.scss';

const CustomMentionMenu: FC<BeautifulMentionsMenuProps & { menuPlacement?: 'top' | 'bottom' }> = ({
	loading,
	menuPlacement = 'bottom',
	editable,
	...props
}): JSX.Element | null => {
	const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
	const anchorRectRef = useRef<DOMRect | null>(null);
	const isFirstRender = useRef<boolean>(true);
	const menuItemHeight = 40;

	useEffect(() => {
		if (isFirstRender.current) {
			const anchor = document.querySelector('.lexical-mention-menu-anchor');

			if (!anchor) return;

			const anchorRect = anchor.getBoundingClientRect();
			anchorRectRef.current = anchorRect;

			if (menuPlacement === 'bottom') {
				const top = anchorRectRef.current.top + anchorRectRef.current.height;
				const left = anchorRectRef.current.left;

				setMenuPosition({ top, left });
			}

			if (menuPlacement === 'top') {
				const top = anchorRectRef.current.top - props.children.length * menuItemHeight;
				const left = anchorRectRef.current.left;

				setMenuPosition({ top, left });
			}
			isFirstRender.current = false;
		}

		if (anchorRectRef?.current && !isFirstRender.current) {
			if (menuPlacement === 'top') {
				const top = anchorRectRef.current.top - props.children.length * menuItemHeight;
				const left = anchorRectRef.current.left;
				setMenuPosition({ top, left });
			}
		}
	}, [props?.children?.length, menuPlacement]);

	return createPortal(
		<div
			className={styles.container}
			data-skip-click-outside="true"
			style={{
				position: 'fixed',
				top: menuPosition?.top,
				left: menuPosition?.left,
				zIndex: 'var(--mentionMenuZIndex)',
				display: editable ? 'flex' : 'none',
			}}
			{...props}
		/>,
		document.getElementById(fullScreenContainerId) || document.body,
	);
};

export default CustomMentionMenu;

I have fullscreen mode, so I need to add a new portal to move the menu inside the fullscreen container, if not, createPortal and logic with bottom placement is not needed as the menu is in the bottom by default.

if (menuPlacement === 'bottom') {
				const top = anchorRectRef.current.top + anchorRectRef.current.height;
				const left = anchorRectRef.current.left;

				setMenuPosition({ top, left });
}
{mentionProps && (
					<BeautifulMentionsPlugin
						items={mentionItems}
						menuComponent={useCallback(
							(props: BeautifulMentionsMenuProps) =>
								CustomMentionMenu({
									...props,
									menuPlacement: mentionProps?.menuPlacement,
									editable: editable,
								}),
							[editable],
						)}
						menuItemComponent={CustomMentionMenuItem}
						menuAnchorClassName="lexical-mention-menu-anchor"
						showMentionsOnDelete={false}
					/>
				)}

Can add an intersection observer if needed, but in my case, the placement "top" and "bottom" is enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant