feat(richtext-lexical): more powerful custom Block RSCs, improved selection handling (#9422)

Now, custom Lexical block & inline block components are re-rendered if
the fields drawer is saved. This ensures that RSCs receive the updated
values, without having to resort to a client component that utilizes the
`useForm` hook.

Additionally, this PRs fixes the lexical selection jumping around after
opening a Block or InlineBlock drawer and clicking inside of it.
This commit is contained in:
Alessio Gravili
2024-11-21 19:34:29 -07:00
committed by GitHub
parent b9cc4d4083
commit 9e31e17e31
5 changed files with 190 additions and 16 deletions

View File

@@ -67,7 +67,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
slug: `lexical-blocks-create-${uuidFromContext}-${formData.id}`,
depth: editDepth,
})
const { toggleDrawer } = useLexicalDrawer(drawerSlug, true)
const { toggleDrawer } = useLexicalDrawer(drawerSlug)
// Used for saving collapsed to preferences (and gettin' it from there again)
// Remember, these preferences are scoped to the whole document, not just this form. This
@@ -92,6 +92,16 @@ export const BlockComponent: React.FC<Props> = (props) => {
: false,
)
const [CustomLabel, setCustomLabel] = React.useState<React.ReactNode | undefined>(
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
initialState?.['_components']?.customComponents?.BlockLabel,
)
const [CustomBlock, setCustomBlock] = React.useState<React.ReactNode | undefined>(
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
initialState?.['_components']?.customComponents?.Block,
)
// Initial state for newly created blocks
useEffect(() => {
const abortController = new AbortController()
@@ -124,6 +134,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
}
setInitialState(state)
setCustomLabel(state._components?.customComponents?.BlockLabel)
setCustomBlock(state._components?.customComponents?.Block)
}
}
@@ -178,6 +190,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
formState: prevFormState,
globalSlug,
operation: 'update',
renderAllFields: submit ? true : false,
schemaPath: schemaFieldsPath,
signal: controller.signal,
})
@@ -209,6 +222,9 @@ export const BlockComponent: React.FC<Props> = (props) => {
}, 0)
if (submit) {
setCustomLabel(newFormState._components?.customComponents?.BlockLabel)
setCustomBlock(newFormState._components?.customComponents?.Block)
let rowErrorCount = 0
for (const formField of Object.values(newFormState)) {
if (formField?.valid === false) {
@@ -246,11 +262,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
})
}, [editor, nodeKey])
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
const CustomLabel = initialState?.['_components']?.customComponents?.BlockLabel
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
const CustomBlock = initialState?.['_components']?.customComponents?.Block
const blockDisplayName = clientBlock?.labels?.singular
? getTranslation(clientBlock.labels.singular, i18n)
: clientBlock?.slug
@@ -291,10 +302,18 @@ export const BlockComponent: React.FC<Props> = (props) => {
buttonStyle="icon-label"
className={`${baseClass}__editButton`}
disabled={readOnly}
el="div"
el="button"
icon="edit"
onClick={() => {
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toggleDrawer()
return false
}}
onMouseDown={(e) => {
// Needed to preserve lexical selection for toggleDrawer lexical selection restore.
// I believe this is needed due to this button (usually) being inside of a collapsible.
e.preventDefault()
}}
round
size="small"
@@ -453,6 +472,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
<Form
beforeSubmit={[
async ({ formState }) => {
// This is only called when form is submitted from drawer - usually only the case if the block has a custom Block component
return await onChange({ formState, submit: true })
},
]}
@@ -460,7 +480,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
initialState={initialState}
onChange={[onChange]}
onSubmit={(formState) => {
// THis is only called when form is submitted from drawer - usually only the case if the block has a custom Block component
// This is only called when form is submitted from drawer - usually only the case if the block has a custom Block component
const newData: any = reduceFieldsToValues(formState)
newData.blockType = formData.blockType
editor.update(() => {

View File

@@ -86,6 +86,16 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
initialLexicalFormState?.[formData.id]?.formState,
)
const [CustomLabel, setCustomLabel] = React.useState<React.ReactNode | undefined>(
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
initialState?.['_components']?.customComponents?.BlockLabel,
)
const [CustomBlock, setCustomBlock] = React.useState<React.ReactNode | undefined>(
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
initialState?.['_components']?.customComponents?.Block,
)
const drawerSlug = formatDrawerSlug({
slug: `lexical-inlineBlocks-create-` + uuidFromContext,
depth: editDepth,
@@ -194,6 +204,8 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
if (state) {
setInitialState(state)
setCustomLabel(state['_components']?.customComponents?.BlockLabel)
setCustomBlock(state['_components']?.customComponents?.Block)
}
}
@@ -219,7 +231,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
* HANDLE ONCHANGE
*/
const onChange = useCallback(
async ({ formState: prevFormState }: { formState: FormState }) => {
async ({ formState: prevFormState, submit }: { formState: FormState; submit?: boolean }) => {
abortAndIgnore(onChangeAbortControllerRef.current)
const controller = new AbortController()
@@ -235,6 +247,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
formState: prevFormState,
globalSlug,
operation: 'update',
renderAllFields: submit ? true : false,
schemaPath: schemaFieldsPath,
signal: controller.signal,
})
@@ -243,6 +256,11 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
return prevFormState
}
if (submit) {
setCustomLabel(state['_components']?.customComponents?.BlockLabel)
setCustomBlock(state['_components']?.customComponents?.Block)
}
return state
},
[getFormState, id, collectionSlug, getDocPreferences, globalSlug, schemaFieldsPath],
@@ -270,10 +288,6 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
},
[editor, nodeKey, formData],
)
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
const CustomLabel = initialState?.['_components']?.customComponents?.BlockLabel
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
const CustomBlock = initialState?.['_components']?.customComponents?.Block
const RemoveButton = useMemo(
() => () => (
@@ -300,7 +314,7 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
buttonStyle="icon-label"
className={`${baseClass}__editButton`}
disabled={readOnly}
el="div"
el="button"
icon="edit"
onClick={() => {
toggleDrawer()
@@ -342,7 +356,12 @@ export const InlineBlockComponent: React.FC<Props> = (props) => {
return (
<Form
beforeSubmit={[onChange]}
beforeSubmit={[
async ({ formState }) => {
// This is only called when form is submitted from drawer
return await onChange({ formState, submit: true })
},
]}
disableValidationOnSubmit
fields={clientBlock.fields}
initialState={initialState || {}}