fix: incorrect key property in Tabs field component (#4311)
Fixes #4282
This commit is contained in:
@@ -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<RichTextField> = (props) => {
|
||||
const RichText: React.FC<RichTextField> = (fieldprops) => {
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const editor: RichTextAdapter = props.editor
|
||||
return <editor.FieldComponent {...props} />
|
||||
const editor: RichTextAdapter = fieldprops.editor
|
||||
const { FieldComponent } = editor
|
||||
|
||||
const FieldComponentImpl: React.FC<any> = useMemo(() => {
|
||||
return FieldComponent()
|
||||
}, [FieldComponent])
|
||||
|
||||
return <FieldComponentImpl {...fieldprops} />
|
||||
}
|
||||
|
||||
export default RichText
|
||||
|
||||
@@ -18,10 +18,10 @@ export type RichTextAdapter<
|
||||
AdapterProps = any,
|
||||
ExtraFieldProperties = {},
|
||||
> = {
|
||||
CellComponent: React.FC<
|
||||
CellComponent: () => React.FC<
|
||||
CellComponentProps<RichTextField<Value, AdapterProps, ExtraFieldProperties>>
|
||||
>
|
||||
FieldComponent: React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
|
||||
FieldComponent: () => React.FC<RichTextFieldProps<Value, AdapterProps, ExtraFieldProperties>>
|
||||
afterReadPromise?: ({
|
||||
field,
|
||||
incomingEditorState,
|
||||
|
||||
@@ -95,7 +95,7 @@ const TabsField: React.FC<Props> = (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> = (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]
|
||||
|
||||
@@ -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<CellComponentProps<RichTextField>> = (props) => {
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const editor: RichTextAdapter = props.field.editor
|
||||
const { CellComponent } = editor
|
||||
|
||||
return <editor.CellComponent {...props} />
|
||||
const CellComponentImpl: React.FC<any> = useMemo(() => {
|
||||
return CellComponent()
|
||||
}, [CellComponent])
|
||||
|
||||
return <CellComponentImpl {...props} />
|
||||
}
|
||||
|
||||
export default RichTextCell
|
||||
|
||||
@@ -64,6 +64,7 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
key={path}
|
||||
style={{
|
||||
...style,
|
||||
width,
|
||||
@@ -88,6 +89,7 @@ const RichText: React.FC<FieldProps> = (props) => {
|
||||
|
||||
setValue(serializedEditorState)
|
||||
}}
|
||||
path={path}
|
||||
readOnly={readOnly}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
@@ -16,11 +16,12 @@ export type LexicalProviderProps = {
|
||||
editorConfig: SanitizedEditorConfig
|
||||
fieldProps: FieldProps
|
||||
onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
|
||||
path: string
|
||||
readOnly: boolean
|
||||
value: SerializedEditorState
|
||||
}
|
||||
export const LexicalProvider: React.FC<LexicalProviderProps> = (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<LexicalProviderProps> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig}>
|
||||
<LexicalComposer initialConfig={initialConfig} key={path}>
|
||||
<EditorConfigProvider editorConfig={editorConfig} fieldProps={fieldProps}>
|
||||
<div className="editor-shell">
|
||||
<LexicalEditorComponent editorConfig={editorConfig} onChange={onChange} />
|
||||
|
||||
@@ -57,11 +57,13 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
|
||||
}
|
||||
|
||||
return {
|
||||
CellComponent: withMergedProps({
|
||||
CellComponent: () =>
|
||||
withMergedProps({
|
||||
Component: RichTextCell,
|
||||
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
|
||||
}),
|
||||
FieldComponent: withMergedProps({
|
||||
FieldComponent: () =>
|
||||
withMergedProps({
|
||||
Component: RichTextField,
|
||||
toMergeIntoProps: { editorConfig: finalSanitizedEditorConfig },
|
||||
}),
|
||||
|
||||
@@ -13,11 +13,13 @@ export function slateEditor(
|
||||
args: AdapterArguments,
|
||||
): RichTextAdapter<any[], AdapterArguments, AdapterArguments> {
|
||||
return {
|
||||
CellComponent: withMergedProps({
|
||||
CellComponent: () =>
|
||||
withMergedProps({
|
||||
Component: RichTextCell,
|
||||
toMergeIntoProps: args,
|
||||
}),
|
||||
FieldComponent: withMergedProps({
|
||||
FieldComponent: () =>
|
||||
withMergedProps({
|
||||
Component: RichTextField,
|
||||
toMergeIntoProps: args,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
53
test/fields/globals/TabsWithRichText.ts
Normal file
53
test/fields/globals/TabsWithRichText.ts
Normal file
@@ -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
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user