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'
|
'use client'
|
||||||
import type { LexicalCommand, LexicalEditor, TextNode } from 'lexical'
|
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 { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
|
||||||
import { mergeRegister } from '@lexical/utils'
|
import { mergeRegister } from '@lexical/utils'
|
||||||
@@ -28,7 +28,7 @@ export type MenuResolution = {
|
|||||||
const baseClass = 'slash-menu-popup'
|
const baseClass = 'slash-menu-popup'
|
||||||
|
|
||||||
export type MenuRenderFn = (
|
export type MenuRenderFn = (
|
||||||
anchorElementRef: MutableRefObject<HTMLElement | null>,
|
anchorElementRef: RefObject<HTMLElement | null>,
|
||||||
itemProps: {
|
itemProps: {
|
||||||
groups: Array<SlashMenuGroupInternal>
|
groups: Array<SlashMenuGroupInternal>
|
||||||
selectItemAndCleanUp: (selectedItem: SlashMenuItem) => void
|
selectItemAndCleanUp: (selectedItem: SlashMenuItem) => void
|
||||||
@@ -206,7 +206,7 @@ export function LexicalMenu({
|
|||||||
resolution,
|
resolution,
|
||||||
shouldSplitNodeWithQuery = false,
|
shouldSplitNodeWithQuery = false,
|
||||||
}: {
|
}: {
|
||||||
anchorElementRef: MutableRefObject<HTMLElement>
|
anchorElementRef: RefObject<HTMLElement>
|
||||||
close: () => void
|
close: () => void
|
||||||
editor: LexicalEditor
|
editor: LexicalEditor
|
||||||
groups: Array<SlashMenuGroupInternal>
|
groups: Array<SlashMenuGroupInternal>
|
||||||
@@ -434,7 +434,7 @@ export function useMenuAnchorRef(
|
|||||||
resolution: MenuResolution | null,
|
resolution: MenuResolution | null,
|
||||||
setResolution: (r: MenuResolution | null) => void,
|
setResolution: (r: MenuResolution | null) => void,
|
||||||
className?: string,
|
className?: string,
|
||||||
): MutableRefObject<HTMLElement> {
|
): RefObject<HTMLElement> {
|
||||||
const [editor] = useLexicalComposerContext()
|
const [editor] = useLexicalComposerContext()
|
||||||
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'))
|
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'))
|
||||||
const positionMenu = useCallback(() => {
|
const positionMenu = useCallback(() => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import * as React from 'react'
|
|||||||
|
|
||||||
import type { MenuTextMatch, TriggerFn } from '../useMenuTriggerMatch.js'
|
import type { MenuTextMatch, TriggerFn } from '../useMenuTriggerMatch.js'
|
||||||
import type { MenuRenderFn, MenuResolution } from './LexicalMenu.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'
|
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 { useDynamicPositioning } from './LexicalMenu.js'
|
||||||
|
|
||||||
export type TypeaheadMenuPluginProps = {
|
export type TypeaheadMenuPluginProps = {
|
||||||
@@ -190,7 +167,7 @@ export function LexicalTypeaheadMenuPlugin({
|
|||||||
matchingString: '',
|
matchingString: '',
|
||||||
replaceableString: '',
|
replaceableString: '',
|
||||||
}
|
}
|
||||||
if (match !== null && !isSelectionOnEntityBoundary(editor, match.leadOffset)) {
|
if (!isSelectionOnEntityBoundary(editor, match.leadOffset)) {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
const editorWindow = editor._window ?? window
|
const editorWindow = editor._window ?? window
|
||||||
const range = editorWindow.document.createRange()
|
const range = editorWindow.document.createRange()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { I18nClient } from '@payloadcms/translations'
|
import type { I18nClient } from '@payloadcms/translations'
|
||||||
import type { LexicalEditor, Spread } from 'lexical'
|
import type { LexicalEditor, Spread } from 'lexical'
|
||||||
import type { MutableRefObject } from 'react'
|
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
import type { RefObject } from 'react'
|
||||||
|
|
||||||
export type SlashMenuItem = {
|
export type SlashMenuItem = {
|
||||||
/** The icon which is rendered in your slash menu item. */
|
/** The icon which is rendered in your slash menu item. */
|
||||||
@@ -34,7 +34,7 @@ export type SlashMenuGroup = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SlashMenuItemInternal = {
|
export type SlashMenuItemInternal = {
|
||||||
ref: MutableRefObject<HTMLButtonElement | null>
|
ref: RefObject<HTMLButtonElement | null>
|
||||||
} & SlashMenuItem
|
} & SlashMenuItem
|
||||||
|
|
||||||
export type SlashMenuGroupInternal = Spread<
|
export type SlashMenuGroupInternal = Spread<
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ function SlashMenuItem({
|
|||||||
key={item.key}
|
key={item.key}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
ref={item.ref}
|
ref={(element) => {
|
||||||
|
item.ref = { current: element }
|
||||||
|
}}
|
||||||
role="option"
|
role="option"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -178,57 +180,55 @@ export function SlashMenuPlugin({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<LexicalTypeaheadMenuPlugin
|
||||||
<LexicalTypeaheadMenuPlugin
|
anchorElem={anchorElem}
|
||||||
anchorElem={anchorElem}
|
groups={groups as SlashMenuGroupInternal[]}
|
||||||
groups={groups as SlashMenuGroupInternal[]}
|
menuRenderFn={(
|
||||||
menuRenderFn={(
|
anchorElementRef,
|
||||||
anchorElementRef,
|
{ selectItemAndCleanUp, selectedItemKey, setSelectedItemKey },
|
||||||
{ selectItemAndCleanUp, selectedItemKey, setSelectedItemKey },
|
) =>
|
||||||
) =>
|
anchorElementRef.current && groups.length
|
||||||
anchorElementRef.current && groups.length
|
? ReactDOM.createPortal(
|
||||||
? ReactDOM.createPortal(
|
<div className={baseClass}>
|
||||||
<div className={baseClass}>
|
{groups.map((group) => {
|
||||||
{groups.map((group) => {
|
let groupTitle = group.key
|
||||||
let groupTitle = group.key
|
if (group.label) {
|
||||||
if (group.label) {
|
groupTitle =
|
||||||
groupTitle =
|
typeof group.label === 'function' ? group.label({ i18n }) : group.label
|
||||||
typeof group.label === 'function' ? group.label({ i18n }) : group.label
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${baseClass}__group ${baseClass}__group-${group.key}`}
|
className={`${baseClass}__group ${baseClass}__group-${group.key}`}
|
||||||
key={group.key}
|
key={group.key}
|
||||||
>
|
>
|
||||||
<div className={`${baseClass}__group-title`}>{groupTitle}</div>
|
<div className={`${baseClass}__group-title`}>{groupTitle}</div>
|
||||||
{group.items.map((item, oi: number) => (
|
{group.items.map((item, oi: number) => (
|
||||||
<SlashMenuItem
|
<SlashMenuItem
|
||||||
index={oi}
|
index={oi}
|
||||||
isSelected={selectedItemKey === item.key}
|
isSelected={selectedItemKey === item.key}
|
||||||
item={item as SlashMenuItemInternal}
|
item={item as SlashMenuItemInternal}
|
||||||
key={item.key}
|
key={item.key}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedItemKey(item.key)
|
setSelectedItemKey(item.key)
|
||||||
selectItemAndCleanUp(item)
|
selectItemAndCleanUp(item)
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setSelectedItemKey(item.key)
|
setSelectedItemKey(item.key)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>,
|
</div>,
|
||||||
anchorElementRef.current,
|
anchorElementRef.current,
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
onQueryChange={setQueryString}
|
onQueryChange={setQueryString}
|
||||||
onSelectItem={onSelectItem}
|
onSelectItem={onSelectItem}
|
||||||
triggerFn={checkForTriggerMatch}
|
triggerFn={checkForTriggerMatch}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user