chore(richtext-lexical): improve anchor handling for slash menu and floating select menu

This commit is contained in:
Alessio Gravili
2023-10-12 21:01:47 +02:00
parent 69af8d9c83
commit 15e23a3adc
6 changed files with 56 additions and 27 deletions

View File

@@ -87,13 +87,17 @@ export const LexicalEditor: React.FC<LexicalProviderProps> = (props) => {
return <plugin.Component anchorElem={floatingAnchorElem} key={plugin.key} />
}
})}
{editor.isEditable() && (
<React.Fragment>
<FloatingSelectToolbarPlugin anchorElem={floatingAnchorElem} />
<SlashMenuPlugin anchorElem={floatingAnchorElem} />
</React.Fragment>
)}
</React.Fragment>
)}
{editor.isEditable() && (
<React.Fragment>
<HistoryPlugin />
<FloatingSelectToolbarPlugin />
<SlashMenuPlugin />
<MarkdownShortcutPlugin />
</React.Fragment>
)}

View File

@@ -15,7 +15,6 @@ import { createPortal } from 'react-dom'
import { useEditorConfigContext } from '../../config/EditorConfigProvider'
import { getDOMRangeRect } from '../../utils/getDOMRangeRect'
import { getSelectedNode } from '../../utils/getSelectedNode'
import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition'
import { ToolbarButton } from './ToolbarButton'
import { ToolbarDropdown } from './ToolbarDropdown'
@@ -33,28 +32,41 @@ function FloatingSelectToolbar({
const { editorConfig } = useEditorConfigContext()
function mouseMoveListener(e: MouseEvent) {
if (popupCharStylesEditorRef?.current && (e.buttons === 1 || e.buttons === 3)) {
const closeFloatingToolbar = useCallback(() => {
if (popupCharStylesEditorRef?.current) {
const isOpacityZero = popupCharStylesEditorRef.current.style.opacity === '0'
const isPointerEventsNone = popupCharStylesEditorRef.current.style.pointerEvents === 'none'
if (!isOpacityZero || !isPointerEventsNone) {
// Check if the mouse is not over the popup
const x = e.clientX
const y = e.clientY
const elementUnderMouse = document.elementFromPoint(x, y)
if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) {
// Mouse is not over the target element => not a normal click, but probably a drag
if (!isOpacityZero) {
popupCharStylesEditorRef.current.style.opacity = '0'
}
if (!isPointerEventsNone) {
popupCharStylesEditorRef.current.style.pointerEvents = 'none'
if (!isOpacityZero) {
popupCharStylesEditorRef.current.style.opacity = '0'
}
if (!isPointerEventsNone) {
popupCharStylesEditorRef.current.style.pointerEvents = 'none'
}
}
}, [popupCharStylesEditorRef])
const mouseMoveListener = useCallback(
(e: MouseEvent) => {
if (popupCharStylesEditorRef?.current && (e.buttons === 1 || e.buttons === 3)) {
const isOpacityZero = popupCharStylesEditorRef.current.style.opacity === '0'
const isPointerEventsNone = popupCharStylesEditorRef.current.style.pointerEvents === 'none'
if (!isOpacityZero || !isPointerEventsNone) {
// Check if the mouse is not over the popup
const x = e.clientX
const y = e.clientY
const elementUnderMouse = document.elementFromPoint(x, y)
if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) {
// Mouse is not over the target element => not a normal click, but probably a drag
closeFloatingToolbar()
}
}
}
}
}
function mouseUpListener(e: MouseEvent) {
},
[closeFloatingToolbar],
)
const mouseUpListener = useCallback(() => {
if (popupCharStylesEditorRef?.current) {
if (popupCharStylesEditorRef.current.style.opacity !== '1') {
popupCharStylesEditorRef.current.style.opacity = '1'
@@ -63,7 +75,7 @@ function FloatingSelectToolbar({
popupCharStylesEditorRef.current.style.pointerEvents = 'auto'
}
}
}
}, [popupCharStylesEditorRef])
useEffect(() => {
document.addEventListener('mousemove', mouseMoveListener)
@@ -73,7 +85,7 @@ function FloatingSelectToolbar({
document.removeEventListener('mousemove', mouseMoveListener)
document.removeEventListener('mouseup', mouseUpListener)
}
}, [popupCharStylesEditorRef])
}, [popupCharStylesEditorRef, mouseMoveListener, mouseUpListener])
const updateTextFormatFloatingToolbar = useCallback(() => {
const selection = $getSelection()
@@ -197,6 +209,7 @@ function useFloatingTextFormatToolbar(
editor: LexicalEditor,
anchorElem: HTMLElement,
): JSX.Element | null {
console.log('useFloatingTextFormatToolbar')
const [isText, setIsText] = useState(false)
const updatePopup = useCallback(() => {

View File

@@ -503,6 +503,7 @@ export function LexicalMenu({
}
export function useMenuAnchorRef(
anchorElem: HTMLElement,
resolution: MenuResolution | null,
setResolution: (r: MenuResolution | null) => void,
className?: string,
@@ -517,7 +518,9 @@ export function useMenuAnchorRef(
const menuEle = containerDiv.firstChild as Element
if (rootElement !== null && resolution !== null) {
const { height, left, top, width } = resolution.getRect()
let { height, left, top, width } = resolution.getRect()
top -= anchorElem.getBoundingClientRect().top + window.scrollY
left -= anchorElem.getBoundingClientRect().left + window.scrollX
containerDiv.style.top = `${top + window.scrollY + VERTICAL_OFFSET}px`
containerDiv.style.left = `${left + window.scrollX}px`
containerDiv.style.height = `${height}px`
@@ -558,12 +561,12 @@ export function useMenuAnchorRef(
containerDiv.setAttribute('role', 'listbox')
containerDiv.style.display = 'block'
containerDiv.style.position = 'absolute'
document.body.append(containerDiv)
anchorElem.append(containerDiv)
}
anchorElementRef.current = containerDiv
rootElement.setAttribute('aria-controls', 'typeahead-menu')
}
}, [editor, resolution, className])
}, [editor, resolution, className, anchorElem])
useEffect(() => {
const rootElement = editor.getRootElement()

View File

@@ -168,6 +168,7 @@ export function useBasicTypeaheadTriggerMatch(
export type TypeaheadMenuPluginProps = {
anchorClassName?: string
anchorElem: HTMLElement
groupsWithOptions: Array<SlashMenuGroup>
menuRenderFn: MenuRenderFn
onClose?: () => void
@@ -188,6 +189,7 @@ export const ENABLE_SLASH_MENU_COMMAND: LexicalCommand<{
export function LexicalTypeaheadMenuPlugin({
anchorClassName,
anchorElem,
groupsWithOptions,
menuRenderFn,
onClose,
@@ -198,7 +200,7 @@ export function LexicalTypeaheadMenuPlugin({
}: TypeaheadMenuPluginProps): JSX.Element | null {
const [editor] = useLexicalComposerContext()
const [resolution, setResolution] = useState<MenuResolution | null>(null)
const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName)
const anchorElementRef = useMenuAnchorRef(anchorElem, resolution, setResolution, anchorClassName)
const closeTypeahead = useCallback(() => {
setResolution(null)

View File

@@ -15,6 +15,8 @@ html[data-theme='light'] {
font-family: var(--font-body);
max-height: 300px;
overflow-y: scroll;
z-index: 10;
position: absolute;
.group {
padding-bottom: 8px;

View File

@@ -64,7 +64,11 @@ function SlashMenuItem({
)
}
export function SlashMenuPlugin(): JSX.Element {
export function SlashMenuPlugin({
anchorElem = document.body,
}: {
anchorElem?: HTMLElement
}): JSX.Element {
const [editor] = useLexicalComposerContext()
const [queryString, setQueryString] = useState<null | string>(null)
const { editorConfig } = useEditorConfigContext()
@@ -162,6 +166,7 @@ export function SlashMenuPlugin(): JSX.Element {
return (
<React.Fragment>
<LexicalTypeaheadMenuPlugin
anchorElem={anchorElem}
groupsWithOptions={groups}
menuRenderFn={(
anchorElementRef,