fix(richtext-lexical): slash menu keyboard navigation not triggering auto-scroll (#7185)

`ref` was not added to internal slash menu items correctly.

Works as expected now:
![CleanShot 2024-07-16 at 22 05
41](https://github.com/user-attachments/assets/cfb32ec8-a449-41a7-a556-1e5ac365c6bc)
This commit is contained in:
Alessio Gravili
2024-07-16 22:30:04 -04:00
committed by GitHub
parent 8fdd88bd66
commit fe23ca5b1a
4 changed files with 60 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import type { LexicalCommand, LexicalEditor, TextNode } from 'lexical'
import type { JSX, MutableRefObject, ReactPortal } from 'react'
import type { JSX, ReactPortal, RefObject } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
import { mergeRegister } from '@lexical/utils'
@@ -28,7 +28,7 @@ export type MenuResolution = {
const baseClass = 'slash-menu-popup'
export type MenuRenderFn = (
anchorElementRef: MutableRefObject<HTMLElement | null>,
anchorElementRef: RefObject<HTMLElement | null>,
itemProps: {
groups: Array<SlashMenuGroupInternal>
selectItemAndCleanUp: (selectedItem: SlashMenuItem) => void
@@ -206,7 +206,7 @@ export function LexicalMenu({
resolution,
shouldSplitNodeWithQuery = false,
}: {
anchorElementRef: MutableRefObject<HTMLElement>
anchorElementRef: RefObject<HTMLElement>
close: () => void
editor: LexicalEditor
groups: Array<SlashMenuGroupInternal>
@@ -434,7 +434,7 @@ export function useMenuAnchorRef(
resolution: MenuResolution | null,
setResolution: (r: MenuResolution | null) => void,
className?: string,
): MutableRefObject<HTMLElement> {
): RefObject<HTMLElement> {
const [editor] = useLexicalComposerContext()
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'))
const positionMenu = useCallback(() => {

View File

@@ -21,7 +21,7 @@ import * as React from 'react'
import type { MenuTextMatch, TriggerFn } from '../useMenuTriggerMatch.js'
import type { MenuRenderFn, MenuResolution } from './LexicalMenu.js'
import type { SlashMenuGroup, SlashMenuGroupInternal, SlashMenuItem } from './types.js'
import type { SlashMenuGroupInternal, SlashMenuItem } from './types.js'
import { LexicalMenu, useMenuAnchorRef } from './LexicalMenu.js'
@@ -100,29 +100,6 @@ function startTransition(callback: () => void) {
}
}
// Got from https://stackoverflow.com/a/42543908/2013580
export function getScrollParent(
element: HTMLElement,
includeHidden: boolean,
): HTMLBodyElement | HTMLElement {
let style = getComputedStyle(element)
const excludeStaticParent = style.position === 'absolute'
const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/
if (style.position === 'fixed') {
return document.body
}
for (let parent: HTMLElement | null = element; (parent = parent.parentElement); ) {
style = getComputedStyle(parent)
if (excludeStaticParent && style.position === 'static') {
continue
}
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
return parent
}
}
return document.body
}
export { useDynamicPositioning } from './LexicalMenu.js'
export type TypeaheadMenuPluginProps = {
@@ -190,7 +167,7 @@ export function LexicalTypeaheadMenuPlugin({
matchingString: '',
replaceableString: '',
}
if (match !== null && !isSelectionOnEntityBoundary(editor, match.leadOffset)) {
if (!isSelectionOnEntityBoundary(editor, match.leadOffset)) {
if (node !== null) {
const editorWindow = editor._window ?? window
const range = editorWindow.document.createRange()

View File

@@ -1,7 +1,7 @@
import type { I18nClient } from '@payloadcms/translations'
import type { LexicalEditor, Spread } from 'lexical'
import type { MutableRefObject } from 'react'
import type React from 'react'
import type { RefObject } from 'react'
export type SlashMenuItem = {
/** The icon which is rendered in your slash menu item. */
@@ -34,7 +34,7 @@ export type SlashMenuGroup = {
}
export type SlashMenuItemInternal = {
ref: MutableRefObject<HTMLButtonElement | null>
ref: RefObject<HTMLButtonElement | null>
} & SlashMenuItem
export type SlashMenuGroupInternal = Spread<

View File

@@ -57,7 +57,9 @@ function SlashMenuItem({
key={item.key}
onClick={onClick}
onMouseEnter={onMouseEnter}
ref={item.ref}
ref={(element) => {
item.ref = { current: element }
}}
role="option"
tabIndex={-1}
type="button"
@@ -178,57 +180,55 @@ export function SlashMenuPlugin({
)
return (
<React.Fragment>
<LexicalTypeaheadMenuPlugin
anchorElem={anchorElem}
groups={groups as SlashMenuGroupInternal[]}
menuRenderFn={(
anchorElementRef,
{ selectItemAndCleanUp, selectedItemKey, setSelectedItemKey },
) =>
anchorElementRef.current && groups.length
? ReactDOM.createPortal(
<div className={baseClass}>
{groups.map((group) => {
let groupTitle = group.key
if (group.label) {
groupTitle =
typeof group.label === 'function' ? group.label({ i18n }) : group.label
}
<LexicalTypeaheadMenuPlugin
anchorElem={anchorElem}
groups={groups as SlashMenuGroupInternal[]}
menuRenderFn={(
anchorElementRef,
{ selectItemAndCleanUp, selectedItemKey, setSelectedItemKey },
) =>
anchorElementRef.current && groups.length
? ReactDOM.createPortal(
<div className={baseClass}>
{groups.map((group) => {
let groupTitle = group.key
if (group.label) {
groupTitle =
typeof group.label === 'function' ? group.label({ i18n }) : group.label
}
return (
<div
className={`${baseClass}__group ${baseClass}__group-${group.key}`}
key={group.key}
>
<div className={`${baseClass}__group-title`}>{groupTitle}</div>
{group.items.map((item, oi: number) => (
<SlashMenuItem
index={oi}
isSelected={selectedItemKey === item.key}
item={item as SlashMenuItemInternal}
key={item.key}
onClick={() => {
setSelectedItemKey(item.key)
selectItemAndCleanUp(item)
}}
onMouseEnter={() => {
setSelectedItemKey(item.key)
}}
/>
))}
</div>
)
})}
</div>,
anchorElementRef.current,
)
: null
}
onQueryChange={setQueryString}
onSelectItem={onSelectItem}
triggerFn={checkForTriggerMatch}
/>
</React.Fragment>
return (
<div
className={`${baseClass}__group ${baseClass}__group-${group.key}`}
key={group.key}
>
<div className={`${baseClass}__group-title`}>{groupTitle}</div>
{group.items.map((item, oi: number) => (
<SlashMenuItem
index={oi}
isSelected={selectedItemKey === item.key}
item={item as SlashMenuItemInternal}
key={item.key}
onClick={() => {
setSelectedItemKey(item.key)
selectItemAndCleanUp(item)
}}
onMouseEnter={() => {
setSelectedItemKey(item.key)
}}
/>
))}
</div>
)
})}
</div>,
anchorElementRef.current,
)
: null
}
onQueryChange={setQueryString}
onSelectItem={onSelectItem}
triggerFn={checkForTriggerMatch}
/>
)
}