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.
*/