chore(richtext-lexical): improve anchor handling for slash menu and floating select menu
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user