Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(richtext-lexical): richtext fields in drawers aren't editable, inline toolbar artifacts are shown for readOnly editors #8774

Merged
merged 6 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -295,22 +295,18 @@ function InlineToolbar({
return (
<div className="inline-toolbar-popup" ref={floatingToolbarRef}>
<div className="caret" ref={caretRef} />
{editor.isEditable() && (
<React.Fragment>
{editorConfig?.features &&
editorConfig.features?.toolbarInline?.groups.map((group, i) => {
return (
<ToolbarGroupComponent
anchorElem={anchorElem}
editor={editor}
group={group}
index={i}
key={group.key}
/>
)
})}
</React.Fragment>
)}
{editorConfig?.features &&
editorConfig.features?.toolbarInline?.groups.map((group, i) => {
return (
<ToolbarGroupComponent
anchorElem={anchorElem}
editor={editor}
group={group}
index={i}
key={group.key}
/>
)
})}
</div>
)
}
Expand Down Expand Up @@ -392,7 +388,7 @@ function useInlineToolbar(
)
}, [editor, updatePopup])

if (!isText) {
if (!isText || !editor.isEditable()) {
return null
}

Expand Down
9 changes: 7 additions & 2 deletions packages/richtext-lexical/src/field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
FieldDescription,
FieldError,
FieldLabel,
useEditDepth,
useField,
useFieldProps,
withCondition,
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -82,10 +85,12 @@ const RichTextComponent: React.FC<
.filter(Boolean)
.join(' ')

const pathWithEditDepth = `${path}.${editDepth}`

return (
<div
className={classes}
key={path}
key={pathWithEditDepth}
style={{
...style,
width,
Expand All @@ -102,6 +107,7 @@ const RichTextComponent: React.FC<
<div className={`${baseClass}__wrap`}>
<ErrorBoundary fallbackRender={fallbackRender} onReset={() => {}}>
<LexicalProvider
composerKey={pathWithEditDepth}
editorConfig={editorConfig}
field={field}
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)
Expand All @@ -117,7 +123,6 @@ const RichTextComponent: React.FC<

setValue(serializedEditorState)
}}
path={path}
readOnly={disabled}
value={value}
/>
Expand Down
10 changes: 6 additions & 4 deletions packages/richtext-lexical/src/lexical/LexicalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>) => void
path: string
readOnly: boolean
value: SerializedEditorState
}
Expand All @@ -41,7 +41,7 @@ const NestProviders = ({ children, providers }) => {
}

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

const parentContext = useEditorConfigContext()

Expand Down Expand Up @@ -82,7 +82,7 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (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
},
Expand All @@ -94,8 +94,10 @@ export const LexicalProvider: React.FC<LexicalProviderProps> = (props) => {
return <p>Loading...</p>
}

// 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 (
<LexicalComposer initialConfig={initialConfig} key={path}>
<LexicalComposer initialConfig={initialConfig} key={composerKey + initialConfig.editable}>
<EditorConfigProvider
editorConfig={editorConfig}
editorContainerRef={editorContainerRef}
Expand Down
2 changes: 2 additions & 0 deletions test/buildConfigWithDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
HeadingFeature,
IndentFeature,
InlineCodeFeature,
InlineToolbarFeature,
ItalicFeature,
lexicalEditor,
LinkFeature,
Expand Down Expand Up @@ -84,6 +85,7 @@ export async function buildConfigWithDefaults(
SubscriptFeature(),
SuperscriptFeature(),
InlineCodeFeature(),
InlineToolbarFeature(),
TreeViewFeature(),
HeadingFeature(),
IndentFeature(),
Expand Down
26 changes: 13 additions & 13 deletions test/fields/collections/Lexical/e2e/blocks/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('lexicalBlocks', () => {
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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand All @@ -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()

Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading