From 6a51eb17ab0493a9a3804a576535c3cbd3ed9a73 Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Mon, 20 Nov 2023 15:55:43 +0100 Subject: [PATCH 1/6] feat(form/inputs): support rendering PTE Editable through PortableTextInput --- .../src/editor/Editable.tsx | 1 + .../portable-text-editor/src/types/editor.ts | 4 ++ .../form/inputs/PortableText/Compositor.tsx | 10 ++- .../core/form/inputs/PortableText/Editor.tsx | 67 +++++++++++-------- .../inputs/PortableText/PortableTextInput.tsx | 8 +++ .../sanity/src/core/form/types/inputProps.ts | 6 ++ 6 files changed, 66 insertions(+), 30 deletions(-) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index 3f2e91ac18c..d0d1cf54700 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx @@ -92,6 +92,7 @@ export type PortableTextEditableProps = Omit< onBeforeInput?: (event: InputEvent) => void onPaste?: OnPasteFn onCopy?: OnCopyFn + ref: React.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 +108,7 @@ export function Editor(props: EditorProps): ReactNode { renderAnnotation, renderBlock, renderChild, + renderEditable, scrollElement, setPortalElement, setScrollElement, @@ -143,44 +147,49 @@ export function Editor(props: EditorProps): ReactNode { 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': spellcheck, + 'style': noOutlineStyle, + } satisfies PortableTextEditableProps + const defaultRender = (defProps: 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/types/inputProps.ts b/packages/sanity/src/core/form/types/inputProps.ts index 8acaa94fef6..7e06df7924d 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,11 @@ 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. + */ + renderEditable?: (props: RenderPortableTextInputEditableProps) => JSX.Element /** * Array of {@link RangeDecoration} that can be used to decorate the content. */ From 8fb1e8eba51c327179fde85b0e3748383ef82472 Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Tue, 21 Nov 2023 19:15:27 +0100 Subject: [PATCH 2/6] fix(form/inputs): render with props, but without defaultRender defaultRender is not part of the PortableTextEditor's Editable props --- packages/sanity/src/core/form/inputs/PortableText/Editor.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx index e410566c096..907f689585d 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx @@ -17,6 +17,7 @@ import {type Path} from '@sanity/types' import {BoundaryElementProvider, useBoundaryElement, useGlobalKeyDown, useLayer} from '@sanity/ui' // eslint-disable-next-line camelcase import {getTheme_v2} from '@sanity/ui/theme' +import {omit} from 'lodash' import {type ReactNode, useCallback, useMemo} from 'react' import {css, styled} from 'styled-components' @@ -167,8 +168,8 @@ export function Editor(props: EditorProps): ReactNode { 'spellCheck': spellcheck, 'style': noOutlineStyle, } satisfies PortableTextEditableProps - const defaultRender = (defProps: PortableTextEditableProps) => ( - + const defaultRender = (defaultRenderProps: PortableTextEditableProps) => ( + ) if (renderEditable) { return renderEditable({...editableProps, renderDefault: defaultRender}) From 00cce433c49f4cf4c646f83b877ba245bf97b396 Mon Sep 17 00:00:00 2001 From: Robin Pyon Date: Sun, 12 May 2024 11:23:10 +0100 Subject: [PATCH 3/6] refactor: rename useSpellcheck hook for consistency --- .../sanity/src/core/form/inputs/PortableText/Editor.tsx | 8 ++++---- .../core/form/inputs/PortableText/hooks/useSpellCheck.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx index 907f689585d..e3dc459d379 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx @@ -34,7 +34,7 @@ import { ToolbarCard, } from './Editor.styles' import {useScrollSelectionIntoView} from './hooks/useScrollSelectionIntoView' -import {useSpellcheck} from './hooks/useSpellCheck' +import {useSpellCheck} from './hooks/useSpellCheck' import {Decorator} from './text' import {ListItem} from './text/ListItem' import {Style} from './text/Style' @@ -144,7 +144,7 @@ export function Editor(props: EditorProps): ReactNode { ), [t], ) - const spellcheck = useSpellcheck() + const spellCheck = useSpellCheck() const scrollSelectionIntoView = useScrollSelectionIntoView(scrollElement) @@ -165,7 +165,7 @@ export function Editor(props: EditorProps): ReactNode { renderStyle, scrollSelectionIntoView, 'selection': initialSelection, - 'spellCheck': spellcheck, + spellCheck, 'style': noOutlineStyle, } satisfies PortableTextEditableProps const defaultRender = (defaultRenderProps: PortableTextEditableProps) => ( @@ -189,7 +189,7 @@ export function Editor(props: EditorProps): ReactNode { renderEditable, renderPlaceholder, scrollSelectionIntoView, - spellcheck, + spellCheck, ]) const handleToolBarOnMemberOpen = useCallback( 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 From fd1d18315ceb92f974dddcb85723d0f4ad15e40b Mon Sep 17 00:00:00 2001 From: Robin Pyon Date: Sun, 12 May 2024 11:42:08 +0100 Subject: [PATCH 4/6] refactor: prefer importing react MutableRefObject type directly --- packages/@sanity/portable-text-editor/src/editor/Editable.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index d0d1cf54700..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,7 +93,7 @@ export type PortableTextEditableProps = Omit< onBeforeInput?: (event: InputEvent) => void onPaste?: OnPasteFn onCopy?: OnCopyFn - ref: React.MutableRefObject + ref: MutableRefObject rangeDecorations?: RangeDecoration[] renderAnnotation?: RenderAnnotationFunction renderBlock?: RenderBlockFunction From 847d5283b5022cf6b79277c90ad2075d4cd90f77 Mon Sep 17 00:00:00 2001 From: Robin Pyon Date: Sun, 12 May 2024 16:14:09 +0100 Subject: [PATCH 5/6] chore(test-studio): update custom block editor schema --- .../portableText/customBlockEditors.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) 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'}], + }, ], }) From caaef1076af3e40765439a5896c82f8a5619bac6 Mon Sep 17 00:00:00 2001 From: Robin Pyon Date: Mon, 13 May 2024 09:41:24 +0100 Subject: [PATCH 6/6] chore: mark renderEditable as hidden and in beta --- packages/sanity/src/core/form/types/inputProps.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sanity/src/core/form/types/inputProps.ts b/packages/sanity/src/core/form/types/inputProps.ts index 7e06df7924d..2eb3fe83bbf 100644 --- a/packages/sanity/src/core/form/types/inputProps.ts +++ b/packages/sanity/src/core/form/types/inputProps.ts @@ -559,6 +559,8 @@ export interface PortableTextInputProps /** * 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 /**