Skip to content

Commit fe23ca5

Browse files
authored
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)
1 parent 8fdd88b commit fe23ca5

File tree

4 files changed

+61
-84
lines changed

4 files changed

+61
-84
lines changed

packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22
import type { LexicalCommand, LexicalEditor, TextNode } from 'lexical'
3-
import type { JSX, MutableRefObject, ReactPortal } from 'react'
3+
import type { JSX, ReactPortal, RefObject } from 'react'
44

55
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js'
66
import { mergeRegister } from '@lexical/utils'
@@ -28,7 +28,7 @@ export type MenuResolution = {
2828
const baseClass = 'slash-menu-popup'
2929

3030
export type MenuRenderFn = (
31-
anchorElementRef: MutableRefObject<HTMLElement | null>,
31+
anchorElementRef: RefObject<HTMLElement | null>,
3232
itemProps: {
3333
groups: Array<SlashMenuGroupInternal>
3434
selectItemAndCleanUp: (selectedItem: SlashMenuItem) => void
@@ -206,7 +206,7 @@ export function LexicalMenu({
206206
resolution,
207207
shouldSplitNodeWithQuery = false,
208208
}: {
209-
anchorElementRef: MutableRefObject<HTMLElement>
209+
anchorElementRef: RefObject<HTMLElement>
210210
close: () => void
211211
editor: LexicalEditor
212212
groups: Array<SlashMenuGroupInternal>
@@ -434,7 +434,7 @@ export function useMenuAnchorRef(
434434
resolution: MenuResolution | null,
435435
setResolution: (r: MenuResolution | null) => void,
436436
className?: string,
437-
): MutableRefObject<HTMLElement> {
437+
): RefObject<HTMLElement> {
438438
const [editor] = useLexicalComposerContext()
439439
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'))
440440
const positionMenu = useCallback(() => {

packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/index.tsx

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import * as React from 'react'
2121

2222
import type { MenuTextMatch, TriggerFn } from '../useMenuTriggerMatch.js'
2323
import type { MenuRenderFn, MenuResolution } from './LexicalMenu.js'
24-
import type { SlashMenuGroup, SlashMenuGroupInternal, SlashMenuItem } from './types.js'
24+
import type { SlashMenuGroupInternal, SlashMenuItem } from './types.js'
2525

2626
import { LexicalMenu, useMenuAnchorRef } from './LexicalMenu.js'
2727

@@ -100,29 +100,6 @@ function startTransition(callback: () => void) {
100100
}
101101
}
102102

103-
// Got from https://stackoverflow.com/a/42543908/2013580
104-
export function getScrollParent(
105-
element: HTMLElement,
106-
includeHidden: boolean,
107-
): HTMLBodyElement | HTMLElement {
108-
let style = getComputedStyle(element)
109-
const excludeStaticParent = style.position === 'absolute'
110-
const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/
111-
if (style.position === 'fixed') {
112-
return document.body
113-
}
114-
for (let parent: HTMLElement | null = element; (parent = parent.parentElement); ) {
115-
style = getComputedStyle(parent)
116-
if (excludeStaticParent && style.position === 'static') {
117-
continue
118-
}
119-
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
120-
return parent
121-
}
122-
}
123-
return document.body
124-
}
125-
126103
export { useDynamicPositioning } from './LexicalMenu.js'
127104

128105
export type TypeaheadMenuPluginProps = {
@@ -190,7 +167,7 @@ export function LexicalTypeaheadMenuPlugin({
190167
matchingString: '',
191168
replaceableString: '',
192169
}
193-
if (match !== null && !isSelectionOnEntityBoundary(editor, match.leadOffset)) {
170+
if (!isSelectionOnEntityBoundary(editor, match.leadOffset)) {
194171
if (node !== null) {
195172
const editorWindow = editor._window ?? window
196173
const range = editorWindow.document.createRange()

packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { I18nClient } from '@payloadcms/translations'
22
import type { LexicalEditor, Spread } from 'lexical'
3-
import type { MutableRefObject } from 'react'
43
import type React from 'react'
4+
import type { RefObject } from 'react'
55

66
export type SlashMenuItem = {
77
/** The icon which is rendered in your slash menu item. */
@@ -34,7 +34,7 @@ export type SlashMenuGroup = {
3434
}
3535

3636
export type SlashMenuItemInternal = {
37-
ref: MutableRefObject<HTMLButtonElement | null>
37+
ref: RefObject<HTMLButtonElement | null>
3838
} & SlashMenuItem
3939

4040
export type SlashMenuGroupInternal = Spread<

packages/richtext-lexical/src/lexical/plugins/SlashMenu/index.tsx

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ function SlashMenuItem({
5757
key={item.key}
5858
onClick={onClick}
5959
onMouseEnter={onMouseEnter}
60-
ref={item.ref}
60+
ref={(element) => {
61+
item.ref = { current: element }
62+
}}
6163
role="option"
6264
tabIndex={-1}
6365
type="button"
@@ -178,57 +180,55 @@ export function SlashMenuPlugin({
178180
)
179181

180182
return (
181-
<React.Fragment>
182-
<LexicalTypeaheadMenuPlugin
183-
anchorElem={anchorElem}
184-
groups={groups as SlashMenuGroupInternal[]}
185-
menuRenderFn={(
186-
anchorElementRef,
187-
{ selectItemAndCleanUp, selectedItemKey, setSelectedItemKey },
188-
) =>
189-
anchorElementRef.current && groups.length
190-
? ReactDOM.createPortal(
191-
<div className={baseClass}>
192-
{groups.map((group) => {
193-
let groupTitle = group.key
194-
if (group.label) {
195-
groupTitle =
196-
typeof group.label === 'function' ? group.label({ i18n }) : group.label
197-
}
198-
199-
return (
200-
<div
201-
className={`${baseClass}__group ${baseClass}__group-${group.key}`}
202-
key={group.key}
203-
>
204-
<div className={`${baseClass}__group-title`}>{groupTitle}</div>
205-
{group.items.map((item, oi: number) => (
206-
<SlashMenuItem
207-
index={oi}
208-
isSelected={selectedItemKey === item.key}
209-
item={item as SlashMenuItemInternal}
210-
key={item.key}
211-
onClick={() => {
212-
setSelectedItemKey(item.key)
213-
selectItemAndCleanUp(item)
214-
}}
215-
onMouseEnter={() => {
216-
setSelectedItemKey(item.key)
217-
}}
218-
/>
219-
))}
220-
</div>
221-
)
222-
})}
223-
</div>,
224-
anchorElementRef.current,
225-
)
226-
: null
227-
}
228-
onQueryChange={setQueryString}
229-
onSelectItem={onSelectItem}
230-
triggerFn={checkForTriggerMatch}
231-
/>
232-
</React.Fragment>
183+
<LexicalTypeaheadMenuPlugin
184+
anchorElem={anchorElem}
185+
groups={groups as SlashMenuGroupInternal[]}
186+
menuRenderFn={(
187+
anchorElementRef,
188+
{ selectItemAndCleanUp, selectedItemKey, setSelectedItemKey },
189+
) =>
190+
anchorElementRef.current && groups.length
191+
? ReactDOM.createPortal(
192+
<div className={baseClass}>
193+
{groups.map((group) => {
194+
let groupTitle = group.key
195+
if (group.label) {
196+
groupTitle =
197+
typeof group.label === 'function' ? group.label({ i18n }) : group.label
198+
}
199+
200+
return (
201+
<div
202+
className={`${baseClass}__group ${baseClass}__group-${group.key}`}
203+
key={group.key}
204+
>
205+
<div className={`${baseClass}__group-title`}>{groupTitle}</div>
206+
{group.items.map((item, oi: number) => (
207+
<SlashMenuItem
208+
index={oi}
209+
isSelected={selectedItemKey === item.key}
210+
item={item as SlashMenuItemInternal}
211+
key={item.key}
212+
onClick={() => {
213+
setSelectedItemKey(item.key)
214+
selectItemAndCleanUp(item)
215+
}}
216+
onMouseEnter={() => {
217+
setSelectedItemKey(item.key)
218+
}}
219+
/>
220+
))}
221+
</div>
222+
)
223+
})}
224+
</div>,
225+
anchorElementRef.current,
226+
)
227+
: null
228+
}
229+
onQueryChange={setQueryString}
230+
onSelectItem={onSelectItem}
231+
triggerFn={checkForTriggerMatch}
232+
/>
233233
)
234234
}

0 commit comments

Comments
 (0)