Skip to content

Commit

Permalink
feat(pte): initial support for renderEditable in portable text inpu…
Browse files Browse the repository at this point in the history
…ts (#6627)

* feat(form/inputs): support rendering PTE Editable through PortableTextInput

* fix(form/inputs): render with props, but without defaultRender

defaultRender is not part of the PortableTextEditor's Editable props

* refactor: rename useSpellcheck hook for consistency

* refactor: prefer importing react MutableRefObject type directly

* chore(test-studio): update custom block editor schema

* chore: mark renderEditable as hidden and in beta

---------

Co-authored-by: Per-Kristian Nordnes <per.kristian.nordnes@gmail.com>
  • Loading branch information
robinpyon and skogsmaskin committed May 13, 2024
1 parent cc0027b commit cb90da3
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -19,7 +21,6 @@ export const ptCustomBlockEditors = defineType({
{
name: 'hiddenToolbar',
title: 'Hidden toolbar',
description: 'hideToolbar=true',
type: 'array',
components: {
input: (props: PortableTextInputProps) => <BlockEditor {...props} hideToolbar />,
Expand All @@ -29,12 +30,39 @@ export const ptCustomBlockEditors = defineType({
{
name: 'readOnly',
title: 'Read only',
description: 'readOnly=true',
type: 'array',
components: {
input: (props: PortableTextInputProps) => <BlockEditor {...props} readOnly />,
},
of: [{type: 'block'}],
},
{
name: 'renderEditable',
title: 'Custom renderEditable',
description: 'Wrapped in card components with a custom placeholder',
type: 'array',
components: {
input: (props: PortableTextInputProps) => (
<BlockEditor
{...props}
renderEditable={(editableProps) => {
return (
<Card border padding={2} tone="critical">
<Card border padding={2} tone="critical">
{editableProps.renderDefault({
...editableProps,
renderPlaceholder: () => (
<span style={{opacity: 0.25}}>Nothing to see here</span>
),
})}
</Card>
</Card>
)
}}
/>
),
},
of: [{type: 'block'}],
},
],
})
2 changes: 2 additions & 0 deletions packages/@sanity/portable-text-editor/src/editor/Editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
forwardRef,
type HTMLProps,
type KeyboardEvent,
type MutableRefObject,
type ReactNode,
type TextareaHTMLAttributes,
useCallback,
Expand Down Expand Up @@ -92,6 +93,7 @@ export type PortableTextEditableProps = Omit<
onBeforeInput?: (event: InputEvent) => void
onPaste?: OnPasteFn
onCopy?: OnCopyFn
ref: MutableRefObject<HTMLDivElement | null>
rangeDecorations?: RangeDecoration[]
renderAnnotation?: RenderAnnotationFunction
renderBlock?: RenderBlockFunction
Expand Down
4 changes: 4 additions & 0 deletions packages/@sanity/portable-text-editor/src/types/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -42,6 +46,7 @@ interface InputProps extends ArrayOfObjectsInputProps<PortableTextBlock> {
rangeDecorations?: RangeDecoration[]
renderBlockActions?: RenderBlockActionsCallback
renderCustomMarkers?: RenderCustomMarkers
renderEditable?: PortableTextInputProps['renderEditable']
}

/** @internal */
Expand Down Expand Up @@ -74,6 +79,7 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
renderBlock,
renderBlockActions,
renderCustomMarkers,
renderEditable,
renderField,
renderInlineBlock,
renderInput,
Expand Down Expand Up @@ -408,6 +414,7 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
renderAnnotation={editorRenderAnnotation}
renderBlock={editorRenderBlock}
renderChild={editorRenderChild}
renderEditable={renderEditable}
setPortalElement={setPortalElement}
scrollElement={scrollElement}
setScrollElement={setScrollElement}
Expand All @@ -433,6 +440,7 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
path,
rangeDecorations,
readOnly,
renderEditable,
scrollElement,
],
)
Expand Down
72 changes: 41 additions & 31 deletions packages/sanity/src/core/form/inputs/PortableText/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type OnCopyFn,
type OnPasteFn,
PortableTextEditable,
type PortableTextEditableProps,
type RangeDecoration,
type RenderAnnotationFunction,
type RenderBlockFunction,
Expand All @@ -16,11 +17,13 @@ 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'

import {TooltipDelayGroupProvider} from '../../../../ui-components'
import {useTranslation} from '../../../i18n'
import {type PortableTextInputProps} from '../../types/inputProps'
import {useFormBuilder} from '../../useFormBuilder'
import {
EditableCard,
Expand All @@ -31,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'
Expand Down Expand Up @@ -67,6 +70,7 @@ interface EditorProps {
renderAnnotation: RenderAnnotationFunction
renderBlock: RenderBlockFunction
renderChild: RenderChildFunction
renderEditable?: PortableTextInputProps['renderEditable']
scrollElement: HTMLElement | null
setPortalElement?: (portalElement: HTMLDivElement | null) => void
setScrollElement: (scrollElement: HTMLElement | null) => void
Expand Down Expand Up @@ -105,6 +109,7 @@ export function Editor(props: EditorProps): ReactNode {
renderAnnotation,
renderBlock,
renderChild,
renderEditable,
scrollElement,
setPortalElement,
setScrollElement,
Expand Down Expand Up @@ -139,48 +144,53 @@ export function Editor(props: EditorProps): ReactNode {
),
[t],
)
const spellcheck = useSpellcheck()
const spellCheck = useSpellCheck()

const scrollSelectionIntoView = useScrollSelectionIntoView(scrollElement)

const editable = useMemo(
() => (
<PortableTextEditable
aria-describedby={ariaDescribedBy}
hotkeys={hotkeys}
onCopy={onCopy}
onPaste={onPaste}
ref={elementRef}
rangeDecorations={rangeDecorations}
renderAnnotation={renderAnnotation}
renderBlock={renderBlock}
renderChild={renderChild}
renderDecorator={renderDecorator}
renderListItem={renderListItem}
renderPlaceholder={renderPlaceholder}
renderStyle={renderStyle}
scrollSelectionIntoView={scrollSelectionIntoView}
selection={initialSelection}
spellCheck={spellcheck}
style={noOutlineStyle}
/>
),
[
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) => (
<PortableTextEditable {...editableProps} {...omit(defaultRenderProps, ['renderDefault'])} />
)
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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -63,6 +65,10 @@ export interface PortableTextMemberItem {
elementRef?: MutableRefObject<PortableTextEditorElement | null>
input?: ReactNode
}
/** @public */
export interface RenderPortableTextInputEditableProps extends PortableTextEditableProps {
renderDefault: RenderEditableFunction
}

/**
* Input component for editing block content
Expand Down Expand Up @@ -96,6 +102,7 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
rangeDecorations: rangeDecorationsProp,
renderBlockActions,
renderCustomMarkers,
renderEditable,
schemaType,
value,
resolveUploader,
Expand Down Expand Up @@ -397,6 +404,7 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
rangeDecorations={rangeDecorations}
renderBlockActions={renderBlockActions}
renderCustomMarkers={renderCustomMarkers}
renderEditable={renderEditable}
/>
</PortableTextEditor>
</PortableTextMemberItemsProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/sanity/src/core/form/types/inputProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
*/
Expand Down

0 comments on commit cb90da3

Please sign in to comment.