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: 
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user