diff --git a/dev/test-studio/schema/standard/portableText/customBlockEditors.tsx b/dev/test-studio/schema/standard/portableText/customBlockEditors.tsx index 73e41323ddc..22f07611887 100644 --- a/dev/test-studio/schema/standard/portableText/customBlockEditors.tsx +++ b/dev/test-studio/schema/standard/portableText/customBlockEditors.tsx @@ -1,3 +1,5 @@ +/* eslint-disable react/jsx-no-bind */ +import {Card} from '@sanity/ui' import {BlockEditor, defineType, type PortableTextInputProps} from 'sanity' export const ptCustomBlockEditors = defineType({ @@ -19,7 +21,6 @@ export const ptCustomBlockEditors = defineType({ { name: 'hiddenToolbar', title: 'Hidden toolbar', - description: 'hideToolbar=true', type: 'array', components: { input: (props: PortableTextInputProps) => , @@ -29,12 +30,39 @@ export const ptCustomBlockEditors = defineType({ { name: 'readOnly', title: 'Read only', - description: 'readOnly=true', type: 'array', components: { input: (props: PortableTextInputProps) => , }, of: [{type: 'block'}], }, + { + name: 'renderEditable', + title: 'Custom renderEditable', + description: 'Wrapped in card components with a custom placeholder', + type: 'array', + components: { + input: (props: PortableTextInputProps) => ( + { + return ( + + + {editableProps.renderDefault({ + ...editableProps, + renderPlaceholder: () => ( + Nothing to see here + ), + })} + + + ) + }} + /> + ), + }, + of: [{type: 'block'}], + }, ], }) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index 3f2e91ac18c..88e575ce338 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx @@ -8,6 +8,7 @@ import { forwardRef, type HTMLProps, type KeyboardEvent, + type MutableRefObject, type ReactNode, type TextareaHTMLAttributes, useCallback, @@ -92,6 +93,7 @@ export type PortableTextEditableProps = Omit< onBeforeInput?: (event: InputEvent) => void onPaste?: OnPasteFn onCopy?: OnCopyFn + ref: MutableRefObject rangeDecorations?: RangeDecoration[] renderAnnotation?: RenderAnnotationFunction renderBlock?: RenderBlockFunction diff --git a/packages/@sanity/portable-text-editor/src/types/editor.ts b/packages/@sanity/portable-text-editor/src/types/editor.ts index 0ae1dc2e9d9..ae443808ccd 100644 --- a/packages/@sanity/portable-text-editor/src/types/editor.ts +++ b/packages/@sanity/portable-text-editor/src/types/editor.ts @@ -28,6 +28,7 @@ import {type Descendant, type Node as SlateNode, type Operation as SlateOperatio import {type ReactEditor} from 'slate-react' import {type DOMNode} from 'slate-react/dist/utils/dom' +import {type PortableTextEditableProps} from '../editor/Editable' import {type PortableTextEditor} from '../editor/PortableTextEditor' import {type Patch} from '../types/patch' @@ -484,6 +485,9 @@ export type RenderBlockFunction = (props: BlockRenderProps) => JSX.Element /** @beta */ export type RenderChildFunction = (props: BlockChildRenderProps) => JSX.Element +/** @beta */ +export type RenderEditableFunction = (props: PortableTextEditableProps) => JSX.Element + /** @beta */ export type RenderAnnotationFunction = (props: BlockAnnotationRenderProps) => JSX.Element diff --git a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx index 348269cf25d..32dd1aa98bc 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx @@ -16,7 +16,11 @@ import {type ReactNode, useCallback, useMemo, useState} from 'react' import {ChangeIndicator} from '../../../changeIndicators' import {EMPTY_ARRAY} from '../../../util' import {ActivateOnFocus} from '../../components/ActivateOnFocus/ActivateOnFocus' -import {type ArrayOfObjectsInputProps, type RenderCustomMarkers} from '../../types' +import { + type ArrayOfObjectsInputProps, + type PortableTextInputProps, + type RenderCustomMarkers, +} from '../../types' import {type RenderBlockActionsCallback} from '../../types/_transitional' import {ExpandedLayer, Root} from './Compositor.styles' import {Editor} from './Editor' @@ -42,6 +46,7 @@ interface InputProps extends ArrayOfObjectsInputProps { rangeDecorations?: RangeDecoration[] renderBlockActions?: RenderBlockActionsCallback renderCustomMarkers?: RenderCustomMarkers + renderEditable?: PortableTextInputProps['renderEditable'] } /** @internal */ @@ -74,6 +79,7 @@ export function Compositor(props: Omit void setScrollElement: (scrollElement: HTMLElement | null) => void @@ -105,6 +109,7 @@ export function Editor(props: EditorProps): ReactNode { renderAnnotation, renderBlock, renderChild, + renderEditable, scrollElement, setPortalElement, setScrollElement, @@ -139,48 +144,53 @@ export function Editor(props: EditorProps): ReactNode { ), [t], ) - const spellcheck = useSpellcheck() + const spellCheck = useSpellCheck() const scrollSelectionIntoView = useScrollSelectionIntoView(scrollElement) - const editable = useMemo( - () => ( - - ), - [ - ariaDescribedBy, - elementRef, + const editable = useMemo(() => { + const editableProps = { + 'aria-describedby': ariaDescribedBy, hotkeys, - initialSelection, onCopy, onPaste, rangeDecorations, + 'ref': elementRef, renderAnnotation, renderBlock, renderChild, + renderDecorator, + renderListItem, renderPlaceholder, + renderStyle, scrollSelectionIntoView, - spellcheck, - ], - ) + 'selection': initialSelection, + spellCheck, + 'style': noOutlineStyle, + } satisfies PortableTextEditableProps + const defaultRender = (defaultRenderProps: PortableTextEditableProps) => ( + + ) + if (renderEditable) { + return renderEditable({...editableProps, renderDefault: defaultRender}) + } + return defaultRender(editableProps) + }, [ + ariaDescribedBy, + elementRef, + hotkeys, + initialSelection, + onCopy, + onPaste, + rangeDecorations, + renderAnnotation, + renderBlock, + renderChild, + renderEditable, + renderPlaceholder, + scrollSelectionIntoView, + spellCheck, + ]) const handleToolBarOnMemberOpen = useCallback( (relativePath: Path) => { diff --git a/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx b/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx index 8feda1f9014..fc6fa51c063 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/PortableTextInput.tsx @@ -5,8 +5,10 @@ import { type OnPasteFn, type Patch as EditorPatch, type Patch, + type PortableTextEditableProps, PortableTextEditor, type RangeDecoration, + type RenderEditableFunction, } from '@sanity/portable-text-editor' import {useTelemetry} from '@sanity/telemetry/react' import {isKeySegment, type PortableTextBlock} from '@sanity/types' @@ -63,6 +65,10 @@ export interface PortableTextMemberItem { elementRef?: MutableRefObject input?: ReactNode } +/** @public */ +export interface RenderPortableTextInputEditableProps extends PortableTextEditableProps { + renderDefault: RenderEditableFunction +} /** * Input component for editing block content @@ -96,6 +102,7 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode { rangeDecorations: rangeDecorationsProp, renderBlockActions, renderCustomMarkers, + renderEditable, schemaType, value, resolveUploader, @@ -397,6 +404,7 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode { rangeDecorations={rangeDecorations} renderBlockActions={renderBlockActions} renderCustomMarkers={renderCustomMarkers} + renderEditable={renderEditable} /> diff --git a/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx b/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx index 10896c46e2a..886fb96d8ab 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/hooks/useSpellCheck.tsx @@ -1,7 +1,7 @@ import {usePortableTextEditor} from '@sanity/portable-text-editor' import {useMemo} from 'react' -export function useSpellcheck(): boolean { +export function useSpellCheck(): boolean { const editor = usePortableTextEditor() return useMemo(() => { // Chrome 96. has serious perf. issues with spellchecking diff --git a/packages/sanity/src/core/form/types/inputProps.ts b/packages/sanity/src/core/form/types/inputProps.ts index 8acaa94fef6..2eb3fe83bbf 100644 --- a/packages/sanity/src/core/form/types/inputProps.ts +++ b/packages/sanity/src/core/form/types/inputProps.ts @@ -31,6 +31,7 @@ import { type ReactElement, } from 'react' +import {type RenderPortableTextInputEditableProps} from '../inputs' import {type FormPatch, type PatchEvent} from '../patch' import {type FormFieldGroup} from '../store' import { @@ -555,6 +556,13 @@ export interface PortableTextInputProps * Use the `renderBlock` interface instead. */ renderCustomMarkers?: RenderCustomMarkers + /** + * Function to render the PortableTextInput's editable component. + * This is the actual contentEditable element that users type into. + * @hidden + * @beta + */ + renderEditable?: (props: RenderPortableTextInputEditableProps) => JSX.Element /** * Array of {@link RangeDecoration} that can be used to decorate the content. */