diff --git a/packages/richtext-lexical/src/features/toolbars/inline/client/Toolbar/index.tsx b/packages/richtext-lexical/src/features/toolbars/inline/client/Toolbar/index.tsx
index ee2bba68f5f..29864d71321 100644
--- a/packages/richtext-lexical/src/features/toolbars/inline/client/Toolbar/index.tsx
+++ b/packages/richtext-lexical/src/features/toolbars/inline/client/Toolbar/index.tsx
@@ -295,22 +295,18 @@ function InlineToolbar({
return (
- {editor.isEditable() && (
-
- {editorConfig?.features &&
- editorConfig.features?.toolbarInline?.groups.map((group, i) => {
- return (
-
- )
- })}
-
- )}
+ {editorConfig?.features &&
+ editorConfig.features?.toolbarInline?.groups.map((group, i) => {
+ return (
+
+ )
+ })}
)
}
@@ -392,7 +388,7 @@ function useInlineToolbar(
)
}, [editor, updatePopup])
- if (!isText) {
+ if (!isText || !editor.isEditable()) {
return null
}
diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx
index 46bfa5185b6..390eeb6a59a 100644
--- a/packages/richtext-lexical/src/field/Field.tsx
+++ b/packages/richtext-lexical/src/field/Field.tsx
@@ -5,6 +5,7 @@ import {
FieldDescription,
FieldError,
FieldLabel,
+ useEditDepth,
useField,
useFieldProps,
withCondition,
@@ -47,6 +48,8 @@ const RichTextComponent: React.FC<
const Label = components?.Label
const readOnlyFromProps = readOnlyFromTopLevelProps || readOnlyFromAdmin
+ const editDepth = useEditDepth()
+
const memoizedValidate = useCallback(
(value, validationOptions) => {
if (typeof validate === 'function') {
@@ -82,10 +85,12 @@ const RichTextComponent: React.FC<
.filter(Boolean)
.join(' ')
+ const pathWithEditDepth = `${path}.${editDepth}`
+
return (
{}}>
diff --git a/packages/richtext-lexical/src/lexical/LexicalProvider.tsx b/packages/richtext-lexical/src/lexical/LexicalProvider.tsx
index 177464696a2..ef1b08ae798 100644
--- a/packages/richtext-lexical/src/lexical/LexicalProvider.tsx
+++ b/packages/richtext-lexical/src/lexical/LexicalProvider.tsx
@@ -17,10 +17,10 @@ import { LexicalEditor as LexicalEditorComponent } from './LexicalEditor.js'
import { getEnabledNodes } from './nodes/index.js'
export type LexicalProviderProps = {
+ composerKey: string
editorConfig: SanitizedClientEditorConfig
field: LexicalRichTextFieldProps['field']
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set) => void
- path: string
readOnly: boolean
value: SerializedEditorState
}
@@ -41,7 +41,7 @@ const NestProviders = ({ children, providers }) => {
}
export const LexicalProvider: React.FC = (props) => {
- const { editorConfig, field, onChange, path, readOnly, value } = props
+ const { composerKey, editorConfig, field, onChange, readOnly, value } = props
const parentContext = useEditorConfigContext()
@@ -82,7 +82,7 @@ export const LexicalProvider: React.FC = (props) => {
editable: readOnly !== true,
editorState: processedValue != null ? JSON.stringify(processedValue) : undefined,
namespace: editorConfig.lexical.namespace,
- nodes: [...getEnabledNodes({ editorConfig })],
+ nodes: getEnabledNodes({ editorConfig }),
onError: (error: Error) => {
throw error
},
@@ -94,8 +94,10 @@ export const LexicalProvider: React.FC = (props) => {
return Loading...
}
+ // We need to add initialConfig.editable to the key to force a re-render when the readOnly prop changes.
+ // 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.
return (
-
+
{
describe('nested lexical editor in block', () => {
test('should type and save typed text', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -157,7 +157,7 @@ describe('lexicalBlocks', () => {
test('should be able to bold text using floating select toolbar', async () => {
// Reproduces https://github.com/payloadcms/payload/issues/4025
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -239,7 +239,7 @@ describe('lexicalBlocks', () => {
test('should be able to select text, make it an external link and receive the updated link value', async () => {
// Reproduces https://github.com/payloadcms/payload/issues/4025
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -323,7 +323,7 @@ describe('lexicalBlocks', () => {
test('ensure slash menu is not hidden behind other blocks', async () => {
// This test makes sure there are no z-index issues here
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -396,7 +396,7 @@ describe('lexicalBlocks', () => {
})
test('should allow adding new blocks to a sub-blocks field, part of a parent lexical blocks field', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -471,7 +471,7 @@ describe('lexicalBlocks', () => {
// 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
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 () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -690,7 +690,7 @@ describe('lexicalBlocks', () => {
// This test ensures that https://github.com/payloadcms/payload/issues/3911 does not happen again
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -762,7 +762,7 @@ describe('lexicalBlocks', () => {
// 3. In the issue, after writing one character, the cursor focuses back into the parent editor
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -802,7 +802,7 @@ describe('lexicalBlocks', () => {
})
const shouldRespectRowRemovalTest = async () => {
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -859,7 +859,7 @@ describe('lexicalBlocks', () => {
await navigateToLexicalFields()
// Wait for lexical to be loaded up fully
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -882,7 +882,7 @@ describe('lexicalBlocks', () => {
test('ensure pre-seeded uploads node is visible', async () => {
// 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
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -897,7 +897,7 @@ describe('lexicalBlocks', () => {
test('should respect required error state in deeply nested text field', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -946,7 +946,7 @@ describe('lexicalBlocks', () => {
// Reproduces https://github.com/payloadcms/payload/issues/6631
test('ensure tabs field within lexical block correctly loads and saves data', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
diff --git a/test/fields/collections/Lexical/e2e/main/e2e.spec.ts b/test/fields/collections/Lexical/e2e/main/e2e.spec.ts
index 8fb68e2a166..5dda1650686 100644
--- a/test/fields/collections/Lexical/e2e/main/e2e.spec.ts
+++ b/test/fields/collections/Lexical/e2e/main/e2e.spec.ts
@@ -103,7 +103,7 @@ describe('lexicalMain', () => {
await navigateToLexicalFields()
await expect(
- page.locator('.rich-text-lexical').nth(1).locator('.lexical-block').first(),
+ page.locator('.rich-text-lexical').nth(2).locator('.lexical-block').first(),
).toBeVisible()
// Navigate to some different page, away from the current document
@@ -116,7 +116,7 @@ describe('lexicalMain', () => {
test('should not warn about unsaved changes when navigating to lexical editor with blocks node and then leaving the page after making a change and saving', async () => {
// Relevant issue: https://github.com/payloadcms/payload/issues/4115
await navigateToLexicalFields()
- const thirdBlock = page.locator('.rich-text-lexical').nth(1).locator('.lexical-block').nth(2)
+ const thirdBlock = page.locator('.rich-text-lexical').nth(2).locator('.lexical-block').nth(2)
await thirdBlock.scrollIntoViewIfNeeded()
await expect(thirdBlock).toBeVisible()
@@ -150,7 +150,7 @@ describe('lexicalMain', () => {
test('should type and save typed text', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -194,7 +194,7 @@ describe('lexicalMain', () => {
})
test('should be able to bold text using floating select toolbar', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -299,7 +299,7 @@ describe('lexicalMain', () => {
// This test makes sure there are no z-index issues here
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').first()
+ const richTextField = page.locator('.rich-text-lexical').nth(1)
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -360,7 +360,7 @@ describe('lexicalMain', () => {
// This reproduces an issue where if you create an upload node, the document drawer opens, you select a collection other than the default one, create a NEW upload document and save, it throws a lexical error
test('ensure creation of new upload document within upload node works', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
@@ -439,10 +439,13 @@ describe('lexicalMain', () => {
// This reproduces https://github.com/payloadcms/payload/issues/7128
test('ensure newly created upload node has fields, saves them, and loads them correctly', async () => {
await navigateToLexicalFields()
- const richTextField = page.locator('.rich-text-lexical').nth(1) // second
+ const richTextField = page.locator('.rich-text-lexical').nth(2) // second
await richTextField.scrollIntoViewIfNeeded()
await expect(richTextField).toBeVisible()
+ // Wait until there at least 10 blocks visible in that richtext field - thus wait for it to be fully loaded
+ await expect(richTextField.locator('.lexical-block')).toHaveCount(10)
+
const lastParagraph = richTextField.locator('p').last()
await lastParagraph.scrollIntoViewIfNeeded()
await expect(lastParagraph).toBeVisible()
@@ -503,7 +506,7 @@ describe('lexicalMain', () => {
await wait(300)
const reloadedUploadNode = page
.locator('.rich-text-lexical')
- .nth(1)
+ .nth(2)
.locator('.lexical-upload')
.nth(1)
await reloadedUploadNode.scrollIntoViewIfNeeded()
@@ -561,6 +564,126 @@ describe('lexicalMain', () => {
await expect(page.locator('.rich-text-lexical').nth(1)).toBeVisible()
})
+ /**
+ * There was a bug where the inline toolbar inside a lexical editor in a drawer was not shown
+ */
+ test('ensure lexical editor within drawer within relationship within lexical field has fully-functioning inline toolbar', async () => {
+ await navigateToLexicalFields()
+ const richTextField = page.locator('.rich-text-lexical').first()
+ await richTextField.scrollIntoViewIfNeeded()
+ await expect(richTextField).toBeVisible()
+
+ const paragraph = richTextField.locator('.LexicalEditorTheme__paragraph').first()
+ await paragraph.scrollIntoViewIfNeeded()
+ await expect(paragraph).toBeVisible()
+
+ /**
+ * Create new relationship node
+ */
+ // type / to open the slash menu
+ await paragraph.click()
+ await page.keyboard.press('/')
+ await page.keyboard.type('Relationship')
+
+ // Create Relationship node
+ const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup')
+ await expect(slashMenuPopover).toBeVisible()
+
+ const relationshipSelectButton = slashMenuPopover.locator('button').first()
+ await expect(relationshipSelectButton).toBeVisible()
+ await expect(relationshipSelectButton).toHaveText('Relationship')
+ await relationshipSelectButton.click()
+ await expect(slashMenuPopover).toBeHidden()
+
+ await wait(500) // wait for drawer form state to initialize (it's a flake)
+ const relationshipListDrawer = page.locator('dialog[id^=list-drawer_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
+ await expect(relationshipListDrawer).toBeVisible()
+ await wait(500)
+
+ await expect(relationshipListDrawer.locator('.rs__single-value')).toHaveText('Lexical Field')
+
+ await relationshipListDrawer.locator('button').getByText('Rich Text').first().click()
+ await expect(relationshipListDrawer).toBeHidden()
+
+ const newRelationshipNode = richTextField.locator('.lexical-relationship').first()
+ await newRelationshipNode.scrollIntoViewIfNeeded()
+ await expect(newRelationshipNode).toBeVisible()
+
+ await newRelationshipNode.locator('.doc-drawer__toggler').first().click()
+ await wait(500) // wait for drawer form state to initialize (it's a flake)
+
+ /**
+ * Now we are inside the doc drawer containing the richtext field.
+ * Let's test if its inline toolbar works
+ */
+ const docDrawer = page.locator('dialog[id^=doc-drawer_lexical-fields_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore)
+ await expect(docDrawer).toBeVisible()
+ await wait(500)
+
+ const docRichTextField = docDrawer.locator('.rich-text-lexical').first()
+ await docRichTextField.scrollIntoViewIfNeeded()
+ await expect(docRichTextField).toBeVisible()
+
+ const docParagraph = docRichTextField.locator('.LexicalEditorTheme__paragraph').first()
+ await docParagraph.scrollIntoViewIfNeeded()
+ await expect(docParagraph).toBeVisible()
+ await docParagraph.click()
+ await page.keyboard.type('Some text')
+ // Select "text" by pressing shift + arrow left
+ for (let i = 0; i < 4; i++) {
+ await page.keyboard.press('Shift+ArrowLeft')
+ }
+ // Ensure inline toolbar appeared
+ const inlineToolbar = docRichTextField.locator('.inline-toolbar-popup')
+ await expect(inlineToolbar).toBeVisible()
+
+ const boldButton = inlineToolbar.locator('.toolbar-popup__button-bold')
+ await expect(boldButton).toBeVisible()
+
+ // make text bold
+ await boldButton.click()
+
+ // Save drawer
+ await docDrawer.locator('button').getByText('Save').first().click()
+ await expect(docDrawer).toBeHidden()
+ await wait(1500) // Ensure doc is saved in the database
+
+ // Do not save the main page, as it will still have the stale, previous data. // TODO: This should eventually be fixed. It's a separate issue than what this test is about though.
+
+ // Check if the text is bold. It's a self-relationship, so no need to follow relationship
+ await expect(async () => {
+ const lexicalDoc: LexicalField = (
+ await payload.find({
+ collection: lexicalFieldsSlug,
+ depth: 0,
+ overrideAccess: true,
+ where: {
+ title: {
+ equals: lexicalDocData.title,
+ },
+ },
+ })
+ ).docs[0] as never
+
+ const lexicalField: SerializedEditorState = lexicalDoc.lexicalRootEditor
+ const firstParagraph: SerializedParagraphNode = lexicalField.root
+ .children[0] as SerializedParagraphNode
+
+ expect(firstParagraph.children).toHaveLength(2)
+
+ const textNode: SerializedTextNode = firstParagraph.children[0] as SerializedTextNode
+ const boldNode: SerializedTextNode = firstParagraph.children[1] as SerializedTextNode
+
+ expect(textNode.text).toBe('Some ')
+ expect(textNode.format).toBe(0)
+
+ expect(boldNode.text).toBe('text')
+ expect(boldNode.format).toBe(1)
+ }).toPass({
+ timeout: POLL_TOPASS_TIMEOUT,
+ })
+ })
+
describe('localization', () => {
test.skip('ensure simple localized lexical field works', async () => {
await navigateToLexicalFields(true, true)
diff --git a/test/fields/collections/Lexical/index.ts b/test/fields/collections/Lexical/index.ts
index d8fbfc2d1be..fb691070bfc 100644
--- a/test/fields/collections/Lexical/index.ts
+++ b/test/fields/collections/Lexical/index.ts
@@ -123,6 +123,10 @@ export const LexicalFields: CollectionConfig = {
type: 'text',
required: true,
},
+ {
+ name: 'lexicalRootEditor',
+ type: 'richText',
+ },
{
name: 'lexicalSimple',
type: 'richText',
diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts
index 07a4b25d5eb..d3daae2085e 100644
--- a/test/fields/payload-types.ts
+++ b/test/fields/payload-types.ts
@@ -31,6 +31,7 @@ export interface Config {
'lexical-fields': LexicalField;
'lexical-migrate-fields': LexicalMigrateField;
'lexical-localized-fields': LexicalLocalizedField;
+ lexicalObjectReferenceBug: LexicalObjectReferenceBug;
users: User;
'array-fields': ArrayField;
'block-fields': BlockField;
@@ -102,6 +103,21 @@ export interface UserAuthOperations {
export interface LexicalField {
id: string;
title: string;
+ lexicalRootEditor?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
lexicalSimple?: {
root: {
type: string;
@@ -271,6 +287,45 @@ export interface LexicalLocalizedField {
updatedAt: string;
createdAt: string;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "lexicalObjectReferenceBug".
+ */
+export interface LexicalObjectReferenceBug {
+ id: string;
+ lexicalDefault?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
+ lexicalEditor?: {
+ root: {
+ type: string;
+ children: {
+ type: string;
+ version: number;
+ [k: string]: unknown;
+ }[];
+ direction: ('ltr' | 'rtl') | null;
+ format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
+ indent: number;
+ version: number;
+ };
+ [k: string]: unknown;
+ } | null;
+ updatedAt: string;
+ createdAt: string;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -1678,6 +1733,10 @@ export interface PayloadLockedDocument {
relationTo: 'lexical-localized-fields';
value: string | LexicalLocalizedField;
} | null)
+ | ({
+ relationTo: 'lexicalObjectReferenceBug';
+ value: string | LexicalObjectReferenceBug;
+ } | null)
| ({
relationTo: 'users';
value: string | User;