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;