Skip to content

Commit f3bec93

Browse files
authored
fix(richtext-lexical): richtext fields in drawers aren't editable, inline toolbar artifacts are shown for readOnly editors (#8774)
Fixes this: https://github.com/user-attachments/assets/cf78082d-9054-4324-90cd-c81219a4f26d
1 parent fa49215 commit f3bec93

File tree

8 files changed

+235
-44
lines changed

8 files changed

+235
-44
lines changed

packages/richtext-lexical/src/features/toolbars/inline/client/Toolbar/index.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -295,22 +295,18 @@ function InlineToolbar({
295295
return (
296296
<div className="inline-toolbar-popup" ref={floatingToolbarRef}>
297297
<div className="caret" ref={caretRef} />
298-
{editor.isEditable() && (
299-
<React.Fragment>
300-
{editorConfig?.features &&
301-
editorConfig.features?.toolbarInline?.groups.map((group, i) => {
302-
return (
303-
<ToolbarGroupComponent
304-
anchorElem={anchorElem}
305-
editor={editor}
306-
group={group}
307-
index={i}
308-
key={group.key}
309-
/>
310-
)
311-
})}
312-
</React.Fragment>
313-
)}
298+
{editorConfig?.features &&
299+
editorConfig.features?.toolbarInline?.groups.map((group, i) => {
300+
return (
301+
<ToolbarGroupComponent
302+
anchorElem={anchorElem}
303+
editor={editor}
304+
group={group}
305+
index={i}
306+
key={group.key}
307+
/>
308+
)
309+
})}
314310
</div>
315311
)
316312
}
@@ -392,7 +388,7 @@ function useInlineToolbar(
392388
)
393389
}, [editor, updatePopup])
394390

395-
if (!isText) {
391+
if (!isText || !editor.isEditable()) {
396392
return null
397393
}
398394

packages/richtext-lexical/src/field/Field.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FieldDescription,
66
FieldError,
77
FieldLabel,
8+
useEditDepth,
89
useField,
910
useFieldProps,
1011
withCondition,
@@ -47,6 +48,8 @@ const RichTextComponent: React.FC<
4748
const Label = components?.Label
4849
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
4950

51+
const editDepth = useEditDepth()
52+
5053
const memoizedValidate = useCallback(
5154
(value, validationOptions) => {
5255
if (typeof validate === 'function') {
@@ -82,10 +85,12 @@ const RichTextComponent: React.FC<
8285
.filter(Boolean)
8386
.join(' ')
8487

88+
const pathWithEditDepth = `${path}.${editDepth}`
89+
8590
return (
8691
<div
8792
className={classes}
88-
key={path}
93+
key={pathWithEditDepth}
8994
style={{
9095
...style,
9196
width,
@@ -102,6 +107,7 @@ const RichTextComponent: React.FC<
102107
<div className={`${baseClass}__wrap`}>
103108
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
104109
<LexicalProvider
110+
composerKey={pathWithEditDepth}
105111
editorConfig={editorConfig}
106112
field={field}
107113
key={JSON.stringify({ initialValue, path })} // makes sure lexical is completely re-rendered when initialValue changes, bypassing the lexical-internal value memoization. That way, external changes to the form will update the editor. More infos in PR description (https://github.com/payloadcms/payload/pull/5010)
@@ -117,7 +123,6 @@ const RichTextComponent: React.FC<
117123

118124
setValue(serializedEditorState)
119125
}}
120-
path={path}
121126
readOnly={disabled}
122127
value={value}
123128
/>

packages/richtext-lexical/src/lexical/LexicalProvider.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import { LexicalEditor as LexicalEditorComponent } from './LexicalEditor.js'
1717
import { getEnabledNodes } from './nodes/index.js'
1818

1919
export type LexicalProviderProps = {
20+
composerKey: string
2021
editorConfig: SanitizedClientEditorConfig
2122
field: LexicalRichTextFieldProps['field']
2223
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
23-
path: string
2424
readOnly: boolean
2525
value: SerializedEditorState
2626
}
@@ -41,7 +41,7 @@ const NestProviders = ({ children, providers }) => {
4141
}
4242

4343
export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
44-
const { editorConfig, field, onChange, path, readOnly, value } = props
44+
const { composerKey, editorConfig, field, onChange, readOnly, value } = props
4545

4646
const parentContext = useEditorConfigContext()
4747

@@ -82,7 +82,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
8282
editable: readOnly !== true,
8383
editorState: processedValue != null ? JSON.stringify(processedValue) : undefined,
8484
namespace: editorConfig.lexical.namespace,
85-
nodes: [...getEnabledNodes({ editorConfig })],
85+
nodes: getEnabledNodes({ editorConfig }),
8686
onError: (error: Error) => {
8787
throw error
8888
},
@@ -94,8 +94,10 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
9494
return <p>Loading...</p>
9595
}
9696

97+
// We need to add initialConfig.editable to the key to force a re-render when the readOnly prop changes.
98+
// Without it, there were cases where lexical editors inside drawers turn readOnly initially - a few miliseconds later they turn editable, but the editor does not re-render and stays readOnly.
9799
return (
98-
<LexicalComposer initialConfig={initialConfig} key={path}>
100+
<LexicalComposer initialConfig={initialConfig} key={composerKey + initialConfig.editable}>
99101
<EditorConfigProvider
100102
editorConfig={editorConfig}
101103
editorContainerRef={editorContainerRef}

test/buildConfigWithDefaults.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
HeadingFeature,
1010
IndentFeature,
1111
InlineCodeFeature,
12+
InlineToolbarFeature,
1213
ItalicFeature,
1314
lexicalEditor,
1415
LinkFeature,
@@ -84,6 +85,7 @@ export async function buildConfigWithDefaults(
8485
SubscriptFeature(),
8586
SuperscriptFeature(),
8687
InlineCodeFeature(),
88+
InlineToolbarFeature(),
8789
TreeViewFeature(),
8890
HeadingFeature(),
8991
IndentFeature(),

test/fields/collections/Lexical/e2e/blocks/e2e.spec.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('lexicalBlocks', () => {
101101
describe('nested lexical editor in block', () => {
102102
test('should type and save typed text', async () => {
103103
await navigateToLexicalFields()
104-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
104+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
105105
await richTextField.scrollIntoViewIfNeeded()
106106
await expect(richTextField).toBeVisible()
107107

@@ -157,7 +157,7 @@ describe('lexicalBlocks', () => {
157157
test('should be able to bold text using floating select toolbar', async () => {
158158
// Reproduces https://github.com/payloadcms/payload/issues/4025
159159
await navigateToLexicalFields()
160-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
160+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
161161
await richTextField.scrollIntoViewIfNeeded()
162162
await expect(richTextField).toBeVisible()
163163

@@ -239,7 +239,7 @@ describe('lexicalBlocks', () => {
239239
test('should be able to select text, make it an external link and receive the updated link value', async () => {
240240
// Reproduces https://github.com/payloadcms/payload/issues/4025
241241
await navigateToLexicalFields()
242-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
242+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
243243
await richTextField.scrollIntoViewIfNeeded()
244244
await expect(richTextField).toBeVisible()
245245

@@ -323,7 +323,7 @@ describe('lexicalBlocks', () => {
323323
test('ensure slash menu is not hidden behind other blocks', async () => {
324324
// This test makes sure there are no z-index issues here
325325
await navigateToLexicalFields()
326-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
326+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
327327
await richTextField.scrollIntoViewIfNeeded()
328328
await expect(richTextField).toBeVisible()
329329

@@ -396,7 +396,7 @@ describe('lexicalBlocks', () => {
396396
})
397397
test('should allow adding new blocks to a sub-blocks field, part of a parent lexical blocks field', async () => {
398398
await navigateToLexicalFields()
399-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
399+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
400400
await richTextField.scrollIntoViewIfNeeded()
401401
await expect(richTextField).toBeVisible()
402402

@@ -471,7 +471,7 @@ describe('lexicalBlocks', () => {
471471
// Big test which tests a bunch of things: Creation of blocks via slash commands, creation of deeply nested sub-lexical-block fields via slash commands, properly populated deeply nested fields within those
472472
test('ensure creation of a lexical, lexical-field-block, which contains another lexical, lexical-and-upload-field-block, works and that the sub-upload field is properly populated', async () => {
473473
await navigateToLexicalFields()
474-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
474+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
475475
await richTextField.scrollIntoViewIfNeeded()
476476
await expect(richTextField).toBeVisible()
477477

@@ -690,7 +690,7 @@ describe('lexicalBlocks', () => {
690690
// This test ensures that https://github.com/payloadcms/payload/issues/3911 does not happen again
691691

692692
await navigateToLexicalFields()
693-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
693+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
694694
await richTextField.scrollIntoViewIfNeeded()
695695
await expect(richTextField).toBeVisible()
696696

@@ -762,7 +762,7 @@ describe('lexicalBlocks', () => {
762762
// 3. In the issue, after writing one character, the cursor focuses back into the parent editor
763763

764764
await navigateToLexicalFields()
765-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
765+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
766766
await richTextField.scrollIntoViewIfNeeded()
767767
await expect(richTextField).toBeVisible()
768768

@@ -802,7 +802,7 @@ describe('lexicalBlocks', () => {
802802
})
803803

804804
const shouldRespectRowRemovalTest = async () => {
805-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
805+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
806806
await richTextField.scrollIntoViewIfNeeded()
807807
await expect(richTextField).toBeVisible()
808808

@@ -859,7 +859,7 @@ describe('lexicalBlocks', () => {
859859
await navigateToLexicalFields()
860860

861861
// Wait for lexical to be loaded up fully
862-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
862+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
863863
await richTextField.scrollIntoViewIfNeeded()
864864
await expect(richTextField).toBeVisible()
865865

@@ -882,7 +882,7 @@ describe('lexicalBlocks', () => {
882882
test('ensure pre-seeded uploads node is visible', async () => {
883883
// Due to issues with the relationships condition, we had issues with that not being visible. Checking for visibility ensures there is no breakage there again
884884
await navigateToLexicalFields()
885-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
885+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
886886
await richTextField.scrollIntoViewIfNeeded()
887887
await expect(richTextField).toBeVisible()
888888

@@ -897,7 +897,7 @@ describe('lexicalBlocks', () => {
897897

898898
test('should respect required error state in deeply nested text field', async () => {
899899
await navigateToLexicalFields()
900-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
900+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
901901

902902
await richTextField.scrollIntoViewIfNeeded()
903903
await expect(richTextField).toBeVisible()
@@ -946,7 +946,7 @@ describe('lexicalBlocks', () => {
946946
// Reproduces https://github.com/payloadcms/payload/issues/6631
947947
test('ensure tabs field within lexical block correctly loads and saves data', async () => {
948948
await navigateToLexicalFields()
949-
const richTextField = page.locator('.rich-text-lexical').nth(1) // second
949+
const richTextField = page.locator('.rich-text-lexical').nth(2) // second
950950

951951
await richTextField.scrollIntoViewIfNeeded()
952952
await expect(richTextField).toBeVisible()

0 commit comments

Comments
 (0)