Skip to content

Commit a30eeaf

Browse files
authored
feat(richtext-lexical): backport relevant from lexical playground between 0.18.0 and 0.20.0 (#9129)
1 parent df764db commit a30eeaf

File tree

10 files changed

+74
-25
lines changed

10 files changed

+74
-25
lines changed

packages/richtext-lexical/src/features/experimental_table/client/plugins/TableHoverActionsPlugin/index.tsx

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@lexical/table'
1717
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
1818
import { $getNearestNodeFromDOMNode } from 'lexical'
19-
import { useEffect, useRef, useState } from 'react'
19+
import { useEffect, useMemo, useRef, useState } from 'react'
2020
import * as React from 'react'
2121
import { createPortal } from 'react-dom'
2222

@@ -25,15 +25,19 @@ import { useDebounce } from '../../utils/useDebounce.js'
2525

2626
const BUTTON_WIDTH_PX = 20
2727

28-
function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }): JSX.Element {
28+
function TableHoverActionsContainer({
29+
anchorElem,
30+
}: {
31+
anchorElem: HTMLElement
32+
}): JSX.Element | null {
2933
const [editor] = useLexicalComposerContext()
3034
const editorConfig = useEditorConfigContext()
3135
const [isShownRow, setShownRow] = useState<boolean>(false)
3236
const [isShownColumn, setShownColumn] = useState<boolean>(false)
3337
const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(false)
3438
const [position, setPosition] = useState({})
35-
const codeSetRef = useRef<Set<NodeKey>>(new Set())
36-
const tableDOMNodeRef = useRef<HTMLElement | null>(null)
39+
const tableSetRef = useRef<Set<NodeKey>>(new Set())
40+
const tableCellDOMNodeRef = useRef<HTMLElement | null>(null)
3741

3842
const debouncedOnMouseMove = useDebounce(
3943
(event: MouseEvent) => {
@@ -49,7 +53,7 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement })
4953
return
5054
}
5155

52-
tableDOMNodeRef.current = tableDOMNode
56+
tableCellDOMNodeRef.current = tableDOMNode
5357

5458
let hoveredRowNode: null | TableCellNode = null
5559
let hoveredColumnNode: null | TableCellNode = null
@@ -88,9 +92,9 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement })
8892
const {
8993
bottom: tableElemBottom,
9094
height: tableElemHeight,
95+
left: tableElemLeft,
9196
right: tableElemRight,
9297
width: tableElemWidth,
93-
x: tableElemX,
9498
y: tableElemY,
9599
} = (tableDOMElement as HTMLTableElement).getBoundingClientRect()
96100

@@ -101,7 +105,7 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement })
101105
setShownRow(true)
102106
setPosition({
103107
height: BUTTON_WIDTH_PX,
104-
left: tableElemX - editorElemLeft,
108+
left: tableElemLeft - editorElemLeft,
105109
top: tableElemBottom - editorElemY + 5,
106110
width: tableElemWidth,
107111
})
@@ -121,6 +125,15 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement })
121125
250,
122126
)
123127

128+
// Hide the buttons on any table dimensions change to prevent last row cells
129+
// overlap behind the 'Add Row' button when text entry changes cell height
130+
const tableResizeObserver = useMemo(() => {
131+
return new ResizeObserver(() => {
132+
setShownRow(false)
133+
setShownColumn(false)
134+
})
135+
}, [])
136+
124137
useEffect(() => {
125138
if (!shouldListenMouseMove) {
126139
return
@@ -143,15 +156,28 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement })
143156
(mutations) => {
144157
editor.getEditorState().read(() => {
145158
for (const [key, type] of mutations) {
159+
const tableDOMElement = editor.getElementByKey(key)
160+
146161
switch (type) {
147162
case 'created':
148-
codeSetRef.current.add(key)
149-
setShouldListenMouseMove(codeSetRef.current.size > 0)
163+
tableSetRef.current.add(key)
164+
setShouldListenMouseMove(tableSetRef.current.size > 0)
165+
if (tableDOMElement) {
166+
tableResizeObserver.observe(tableDOMElement)
167+
}
150168
break
151169

152170
case 'destroyed':
153-
codeSetRef.current.delete(key)
154-
setShouldListenMouseMove(codeSetRef.current.size > 0)
171+
tableSetRef.current.delete(key)
172+
setShouldListenMouseMove(tableSetRef.current.size > 0)
173+
// Reset resize observers
174+
tableResizeObserver.disconnect()
175+
tableSetRef.current.forEach((tableKey: NodeKey) => {
176+
const tableElement = editor.getElementByKey(tableKey)
177+
if (tableElement) {
178+
tableResizeObserver.observe(tableElement)
179+
}
180+
})
155181
break
156182

157183
default:
@@ -163,12 +189,12 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement })
163189
{ skipInitialization: false },
164190
),
165191
)
166-
}, [editor])
192+
}, [editor, tableResizeObserver])
167193

168194
const insertAction = (insertRow: boolean) => {
169195
editor.update(() => {
170-
if (tableDOMNodeRef.current) {
171-
const maybeTableNode = $getNearestNodeFromDOMNode(tableDOMNodeRef.current)
196+
if (tableCellDOMNodeRef.current) {
197+
const maybeTableNode = $getNearestNodeFromDOMNode(tableCellDOMNodeRef.current)
172198
maybeTableNode?.selectEnd()
173199
if (insertRow) {
174200
$insertTableRow__EXPERIMENTAL()
@@ -181,6 +207,10 @@ function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement })
181207
})
182208
}
183209

210+
if (!editor?.isEditable()) {
211+
return null
212+
}
213+
184214
return (
185215
<>
186216
{isShownRow && (
@@ -233,5 +263,10 @@ export function TableHoverActionsPlugin({
233263
}: {
234264
anchorElem?: HTMLElement
235265
}): null | React.ReactPortal {
266+
const [editor] = useLexicalComposerContext()
267+
if (!editor?.isEditable()) {
268+
return null
269+
}
270+
236271
return createPortal(<TableHoverActionsContainer anchorElem={anchorElem} />, anchorElem)
237272
}

packages/richtext-lexical/src/features/format/bold/feature.client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use client'
2+
import { $isTableSelection } from '@lexical/table'
23
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
34

45
import type { ToolbarGroup } from '../../toolbars/types.js'
@@ -18,7 +19,7 @@ const toolbarGroups: ToolbarGroup[] = [
1819
{
1920
ChildComponent: BoldIcon,
2021
isActive: ({ selection }) => {
21-
if ($isRangeSelection(selection)) {
22+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
2223
return selection.hasFormat('bold')
2324
}
2425
return false

packages/richtext-lexical/src/features/format/inlineCode/feature.client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { $isTableSelection } from '@lexical/table'
34
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
45

56
import type { ToolbarGroup } from '../../toolbars/types.js'
@@ -14,7 +15,7 @@ const toolbarGroups: ToolbarGroup[] = [
1415
{
1516
ChildComponent: CodeIcon,
1617
isActive: ({ selection }) => {
17-
if ($isRangeSelection(selection)) {
18+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
1819
return selection.hasFormat('code')
1920
}
2021
return false

packages/richtext-lexical/src/features/format/italic/feature.client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { $isTableSelection } from '@lexical/table'
34
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
45

56
import type { ToolbarGroup } from '../../toolbars/types.js'
@@ -14,7 +15,7 @@ const toolbarGroups: ToolbarGroup[] = [
1415
{
1516
ChildComponent: ItalicIcon,
1617
isActive: ({ selection }) => {
17-
if ($isRangeSelection(selection)) {
18+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
1819
return selection.hasFormat('italic')
1920
}
2021
return false

packages/richtext-lexical/src/features/format/strikethrough/feature.client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { $isTableSelection } from '@lexical/table'
34
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
45

56
import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough/index.js'
@@ -12,7 +13,7 @@ const toolbarGroups = [
1213
{
1314
ChildComponent: StrikethroughIcon,
1415
isActive: ({ selection }) => {
15-
if ($isRangeSelection(selection)) {
16+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
1617
return selection.hasFormat('strikethrough')
1718
}
1819
return false

packages/richtext-lexical/src/features/format/subscript/feature.client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { $isTableSelection } from '@lexical/table'
34
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
45

56
import type { ToolbarGroup } from '../../toolbars/types.js'
@@ -13,7 +14,7 @@ const toolbarGroups: ToolbarGroup[] = [
1314
{
1415
ChildComponent: SubscriptIcon,
1516
isActive: ({ selection }) => {
16-
if ($isRangeSelection(selection)) {
17+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
1718
return selection.hasFormat('subscript')
1819
}
1920
return false

packages/richtext-lexical/src/features/format/superscript/feature.client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { $isTableSelection } from '@lexical/table'
34
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
45

56
import type { ToolbarGroup } from '../../toolbars/types.js'
@@ -13,7 +14,7 @@ const toolbarGroups: ToolbarGroup[] = [
1314
{
1415
ChildComponent: SuperscriptIcon,
1516
isActive: ({ selection }) => {
16-
if ($isRangeSelection(selection)) {
17+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
1718
return selection.hasFormat('superscript')
1819
}
1920
return false

packages/richtext-lexical/src/features/format/underline/feature.client.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { $isTableSelection } from '@lexical/table'
34
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
45

56
import type { ToolbarGroup } from '../../toolbars/types.js'
@@ -13,7 +14,7 @@ const toolbarGroups: ToolbarGroup[] = [
1314
{
1415
ChildComponent: UnderlineIcon,
1516
isActive: ({ selection }) => {
16-
if ($isRangeSelection(selection)) {
17+
if ($isRangeSelection(selection) || $isTableSelection(selection)) {
1718
return selection.hasFormat('underline')
1819
}
1920
return false

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
2020
import type { MenuTextMatch } from '../useMenuTriggerMatch.js'
2121
import type { SlashMenuGroupInternal, SlashMenuItem, SlashMenuItemInternal } from './types.js'
2222

23+
import { CAN_USE_DOM } from '../../../utils/canUseDOM.js'
24+
2325
export type MenuResolution = {
2426
getRect: () => DOMRect
2527
match?: MenuTextMatch
@@ -208,7 +210,7 @@ export function LexicalMenu({
208210
resolution,
209211
shouldSplitNodeWithQuery = false,
210212
}: {
211-
anchorElementRef: RefObject<HTMLElement>
213+
anchorElementRef: RefObject<HTMLElement | null>
212214
close: () => void
213215
editor: LexicalEditor
214216
groups: Array<SlashMenuGroupInternal>
@@ -432,10 +434,15 @@ export function useMenuAnchorRef(
432434
resolution: MenuResolution | null,
433435
setResolution: (r: MenuResolution | null) => void,
434436
className?: string,
435-
): RefObject<HTMLElement> {
437+
): RefObject<HTMLElement | null> {
436438
const [editor] = useLexicalComposerContext()
437-
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'))
439+
const anchorElementRef = useRef<HTMLElement | null>(
440+
CAN_USE_DOM ? document.createElement('div') : null,
441+
)
438442
const positionMenu = useCallback(() => {
443+
if (anchorElementRef.current === null || parent === undefined) {
444+
return
445+
}
439446
const rootElement = editor.getRootElement()
440447
const containerDiv = anchorElementRef.current
441448

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export function LexicalTypeaheadMenuPlugin({
237237
}
238238
}, [editor, triggerFn, onQueryChange, resolution, closeTypeahead, openTypeahead])
239239

240-
return resolution === null || editor === null ? null : (
240+
return anchorElementRef.current === null || resolution === null || editor === null ? null : (
241241
<LexicalMenu
242242
anchorElementRef={anchorElementRef}
243243
close={closeTypeahead}

0 commit comments

Comments
 (0)