diff --git a/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx b/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx index ae2ab1663..6b1ccbf43 100644 --- a/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/RichText/index.tsx @@ -1,12 +1,17 @@ -import React from 'react' +import React, { useMemo } from 'react' import type { RichTextField } from '../../../../../fields/config/types' import type { RichTextAdapter } from './types' - -const RichText: React.FC = (props) => { +const RichText: React.FC = (fieldprops) => { // eslint-disable-next-line react/destructuring-assignment - const editor: RichTextAdapter = props.editor - return + const editor: RichTextAdapter = fieldprops.editor + const { FieldComponent } = editor + + const FieldComponentImpl: React.FC = useMemo(() => { + return FieldComponent() + }, [FieldComponent]) + + return } export default RichText diff --git a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts index c031739aa..9c81a0fc5 100644 --- a/packages/payload/src/admin/components/forms/field-types/RichText/types.ts +++ b/packages/payload/src/admin/components/forms/field-types/RichText/types.ts @@ -18,10 +18,10 @@ export type RichTextAdapter< AdapterProps = any, ExtraFieldProperties = {}, > = { - CellComponent: React.FC< + CellComponent: () => React.FC< CellComponentProps> > - FieldComponent: React.FC> + FieldComponent: () => React.FC> afterReadPromise?: ({ field, incomingEditorState, diff --git a/packages/payload/src/admin/components/forms/field-types/Tabs/index.tsx b/packages/payload/src/admin/components/forms/field-types/Tabs/index.tsx index 65ac8c2ac..8e4845a60 100644 --- a/packages/payload/src/admin/components/forms/field-types/Tabs/index.tsx +++ b/packages/payload/src/admin/components/forms/field-types/Tabs/index.tsx @@ -95,7 +95,7 @@ const TabsField: React.FC = (props) => { : existingPreferences?.fields?.[tabsPrefKey]?.tabIndex setActiveTabIndex(initialIndex || 0) } - getInitialPref() + void getInitialPref() }, [path, indexPath, getPreference, preferencesKey, tabsPrefKey]) const handleTabChange = useCallback( @@ -193,7 +193,11 @@ const TabsField: React.FC = (props) => { fieldTypes={fieldTypes} forceRender={forceRender} indexPath={indexPath} - key={String(activeTabConfig.label)} + key={ + activeTabConfig.label + ? getTranslation(activeTabConfig.label, i18n) + : activeTabConfig['name'] + } margins="small" permissions={ tabHasName(activeTabConfig) && permissions?.[activeTabConfig.name] diff --git a/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx b/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx index d2d7e47bb..55cfefd7d 100644 --- a/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx +++ b/packages/payload/src/admin/components/views/collections/List/Cell/field-types/Richtext/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import type { RichTextField } from '../../../../../../../../fields/config/types' import type { RichTextAdapter } from '../../../../../../forms/field-types/RichText/types' @@ -7,8 +7,13 @@ import type { CellComponentProps } from '../../types' const RichTextCell: React.FC> = (props) => { // eslint-disable-next-line react/destructuring-assignment const editor: RichTextAdapter = props.field.editor + const { CellComponent } = editor - return + const CellComponentImpl: React.FC = useMemo(() => { + return CellComponent() + }, [CellComponent]) + + return } export default RichTextCell diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx index 369ed03a5..4d5f96bdc 100644 --- a/packages/richtext-lexical/src/field/Field.tsx +++ b/packages/richtext-lexical/src/field/Field.tsx @@ -64,6 +64,7 @@ const RichText: React.FC = (props) => { return (
= (props) => { setValue(serializedEditorState) }} + path={path} readOnly={readOnly} value={value} /> diff --git a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx index bfa09e4c9..cba79e796 100644 --- a/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx +++ b/packages/richtext-lexical/src/field/lexical/LexicalProvider.tsx @@ -16,11 +16,12 @@ export type LexicalProviderProps = { editorConfig: SanitizedEditorConfig fieldProps: FieldProps onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set) => void + path: string readOnly: boolean value: SerializedEditorState } export const LexicalProvider: React.FC = (props) => { - const { editorConfig, fieldProps, onChange, readOnly } = props + const { editorConfig, fieldProps, onChange, path, readOnly } = props let { value } = props if (editorConfig?.features?.hooks?.load?.length) { @@ -60,7 +61,7 @@ export const LexicalProvider: React.FC = (props) => { } return ( - +
diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 5289c5c2a..3a9e48c1a 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -57,14 +57,16 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte } return { - CellComponent: withMergedProps({ - Component: RichTextCell, - toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, - }), - FieldComponent: withMergedProps({ - Component: RichTextField, - toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, - }), + CellComponent: () => + withMergedProps({ + Component: RichTextCell, + toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, + }), + FieldComponent: () => + withMergedProps({ + Component: RichTextField, + toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig }, + }), afterReadPromise: ({ field, incomingEditorState, siblingDoc }) => { return new Promise((resolve, reject) => { const promises: Promise[] = [] diff --git a/packages/richtext-slate/src/index.ts b/packages/richtext-slate/src/index.ts index d20b799d3..190faeae6 100644 --- a/packages/richtext-slate/src/index.ts +++ b/packages/richtext-slate/src/index.ts @@ -13,14 +13,16 @@ export function slateEditor( args: AdapterArguments, ): RichTextAdapter { return { - CellComponent: withMergedProps({ - Component: RichTextCell, - toMergeIntoProps: args, - }), - FieldComponent: withMergedProps({ - Component: RichTextField, - toMergeIntoProps: args, - }), + CellComponent: () => + withMergedProps({ + Component: RichTextCell, + toMergeIntoProps: args, + }), + FieldComponent: () => + withMergedProps({ + Component: RichTextField, + toMergeIntoProps: args, + }), outputSchema: ({ isRequired }) => { return { items: { diff --git a/test/fields/config.ts b/test/fields/config.ts index bd5946d48..e69305f97 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -28,6 +28,7 @@ import TextFields from './collections/Text' import Uploads from './collections/Upload' import Uploads2 from './collections/Upload2' import Uploads3 from './collections/Uploads3' +import TabsWithRichText from './globals/TabsWithRichText' import { clearAndSeedEverything } from './seed' export const collectionSlugs: CollectionConfig[] = [ @@ -85,6 +86,7 @@ export default buildConfigWithDefaults({ }), }, collections: collectionSlugs, + globals: [TabsWithRichText], localization: { defaultLocale: 'en', fallback: true, diff --git a/test/fields/globals/TabsWithRichText.ts b/test/fields/globals/TabsWithRichText.ts new file mode 100644 index 000000000..7516f7148 --- /dev/null +++ b/test/fields/globals/TabsWithRichText.ts @@ -0,0 +1,53 @@ +/** + * IMPORTANT: Do not change this style. This specific configuration is needed to reproduce this issue before it was fixed (https://github.com/payloadcms/payload/issues/4282): + * - lexicalEditor initialized on the outside and then shared between two richText fields + * - tabs field with two tabs, each with a richText field + * - each tab has a different label in each language. Needs to be a LOCALIZED label, not a single label for all languages. Only then can it be reproduced + */ + +import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' + +import { lexicalEditor } from '../../../packages/richtext-lexical/src' + +const initializedEditor = lexicalEditor() + +const TabsWithRichText: GlobalConfig = { + slug: 'tabsWithRichText', + fields: [ + { + type: 'tabs', + tabs: [ + { + name: 'tab1', + label: { + en: 'en tab1', + es: 'es tab1', + }, + fields: [ + { + name: 'rt1', + type: 'richText', + editor: initializedEditor, + }, + ], + }, + { + name: 'tab2', + label: { + en: 'en tab2', + es: 'es tab2', + }, + fields: [ + { + name: 'rt2', + type: 'richText', + editor: initializedEditor, + }, + ], + }, + ], + }, + ], +} + +export default TabsWithRichText diff --git a/test/fields/lexical.e2e.spec.ts b/test/fields/lexical.e2e.spec.ts index 353288a6b..b41b3ab31 100644 --- a/test/fields/lexical.e2e.spec.ts +++ b/test/fields/lexical.e2e.spec.ts @@ -184,6 +184,27 @@ describe('lexical', () => { expect(textNode2.format).toBe(0) }) + test('Make sure highly specific issue does not occur when two richText fields share the same editor prop', async () => { + // Reproduces https://github.com/payloadcms/payload/issues/4282 + const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'tabsWithRichText') + await page.goto(url.global('tabsWithRichText')) + const richTextField = page.locator('.rich-text-lexical').first() + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + await richTextField.click() // Use click, because focus does not work + await page.keyboard.type('some text') + + await page.locator('.tabs-field__tabs').first().getByText('en tab2').first().click() + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + + const contentEditable = richTextField.locator('.ContentEditable__root').first() + const textContent = await contentEditable.textContent() + + expect(textContent).not.toBe('some text') + expect(textContent).toBe('') + }) + describe('nested lexical editor in block', () => { test('should type and save typed text', async () => { await navigateToLexicalFields()