From d6d8aa7f4c836680b7fcf69c7a2e719c1f9d9616 Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Tue, 2 Aug 2022 12:17:42 +0200 Subject: [PATCH] refactor(inputs): improve fullscreen and activation re-rendering --- .../inputs/PortableText/Compositor.styles.tsx | 3 + .../form/inputs/PortableText/Compositor.tsx | 61 +++++++++---------- .../src/form/inputs/PortableText/Editor.tsx | 25 ++++---- .../inputs/PortableText/PortableTextInput.tsx | 37 ++++++++++- 4 files changed, 80 insertions(+), 46 deletions(-) diff --git a/packages/sanity/src/form/inputs/PortableText/Compositor.styles.tsx b/packages/sanity/src/form/inputs/PortableText/Compositor.styles.tsx index 22e71581c25..97cd8d3e136 100644 --- a/packages/sanity/src/form/inputs/PortableText/Compositor.styles.tsx +++ b/packages/sanity/src/form/inputs/PortableText/Compositor.styles.tsx @@ -52,4 +52,7 @@ export const ExpandedLayer = styled(Layer)` left: 0; right: 0; bottom: 0; + &:not([data-fullscreen]) { + position: relative; + } ` diff --git a/packages/sanity/src/form/inputs/PortableText/Compositor.tsx b/packages/sanity/src/form/inputs/PortableText/Compositor.tsx index f0f0e61ffb8..65eef4668af 100644 --- a/packages/sanity/src/form/inputs/PortableText/Compositor.tsx +++ b/packages/sanity/src/form/inputs/PortableText/Compositor.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState, useMemo, useCallback} from 'react' +import React, {useState, useMemo, useCallback} from 'react' import { EditorSelection, OnCopyFn, @@ -38,7 +38,9 @@ import {_isBlockType} from './_helpers' interface InputProps extends ArrayOfObjectsInputProps { hasFocus: boolean hotkeys?: HotkeyOptions + isActive: boolean isFullscreen: boolean + onActivate: () => void onCopy?: OnCopyFn onPaste?: OnPasteFn onToggleFullscreen: () => void @@ -49,7 +51,16 @@ interface InputProps extends ArrayOfObjectsInputProps { export type PortableTextEditorElement = HTMLDivElement | HTMLSpanElement | null -const ACTIVATE_ON_FOCUS_MESSAGE = Click to activate +function isTouchDevice() { + return ( + (typeof window !== 'undefined' && 'ontouchstart' in window) || + (typeof navigator !== 'undefined' && navigator.maxTouchPoints > 0) + ) +} + +const activateVerb = isTouchDevice() ? 'Tap' : 'Click' + +const ACTIVATE_ON_FOCUS_MESSAGE = {activateVerb} to activate export function Compositor(props: InputProps) { const { @@ -58,12 +69,13 @@ export function Compositor(props: InputProps) { focused, hasFocus, hotkeys, + isActive, isFullscreen, onChange, onCopy, + onActivate, onOpenItem, onCloseItem, - onFocusPath, onPaste, onToggleFullscreen, path, @@ -72,12 +84,10 @@ export function Compositor(props: InputProps) { value, readOnly, renderPreview, - // ...restProps } = props const editor = usePortableTextEditor() - const [isActive, setIsActive] = useState(false) const [wrapperElement, setWrapperElement] = useState(null) const [scrollElement, setScrollElement] = useState(null) const portableTextMemberItems = usePortableTextMemberItems() @@ -92,13 +102,6 @@ export function Compositor(props: InputProps) { onCloseItem, }) - // Set as active whenever we have focus inside the editor. - useEffect(() => { - if (hasFocus) { - setIsActive(true) - } - }, [hasFocus]) - const handleToggleFullscreen = useCallback(() => { PortableTextEditor.blur(editor) onToggleFullscreen() @@ -119,17 +122,6 @@ export function Compositor(props: InputProps) { const editorHotkeys = useHotkeys(hotkeysWithFullscreenToggle) - const focus = useCallback((): void => { - PortableTextEditor.focus(editor) - }, [editor]) - - const handleActivate = useCallback((): void => { - if (!isActive) { - setIsActive(true) - focus() - } - }, [focus, isActive]) - const ptFeatures = useMemo(() => PortableTextEditor.getPortableTextFeatures(editor), [editor]) const hasContent = !!value @@ -260,12 +252,11 @@ export function Compositor(props: InputProps) { initialSelection={initialSelection} isFullscreen={isFullscreen} onOpenItem={onOpenItem} - onFocusPath={onFocusPath} onCopy={onCopy} onPaste={onPaste} onToggleFullscreen={handleToggleFullscreen} path={path} - readOnly={isActive === false || readOnly} + readOnly={readOnly} renderAnnotation={renderAnnotation} renderBlock={renderBlock} renderChild={renderChild} @@ -280,11 +271,9 @@ export function Compositor(props: InputProps) { editorHotkeys, handleToggleFullscreen, initialSelection, - isActive, isFullscreen, onCopy, onOpenItem, - onFocusPath, onPaste, path, readOnly, @@ -333,25 +322,31 @@ export function Compositor(props: InputProps) { [portal.element, portalElement, wrapperElement] ) + + const editorLayer = useMemo( + () => ( + + {children} + + ), + [children, isFullscreen] + ) return (
- - {/* TODO: Can we get rid of this DOM-rerender? */} - {isFullscreen ? {children} : children} - + {editorLayer}
diff --git a/packages/sanity/src/form/inputs/PortableText/Editor.tsx b/packages/sanity/src/form/inputs/PortableText/Editor.tsx index b84e8d6fc36..554db96956a 100644 --- a/packages/sanity/src/form/inputs/PortableText/Editor.tsx +++ b/packages/sanity/src/form/inputs/PortableText/Editor.tsx @@ -24,13 +24,13 @@ import { } from './Editor.styles' import {useSpellcheck} from './hooks/useSpellCheck' import {useScrollSelectionIntoView} from './hooks/useScrollSelectionIntoView' + interface EditorProps { initialSelection?: EditorSelection isFullscreen: boolean hotkeys: HotkeyOptions onCopy?: OnCopyFn onOpenItem: (path: Path) => void - onFocusPath: (nextPath: Path) => void onPaste?: OnPasteFn onToggleFullscreen: () => void path: Path @@ -53,7 +53,6 @@ export function Editor(props: EditorProps) { initialSelection, isFullscreen, onCopy, - onFocusPath, onOpenItem, onPaste, onToggleFullscreen, @@ -106,6 +105,7 @@ export function Editor(props: EditorProps) { onCopy={onCopy} onPaste={onPaste} ref={editableRef} + readOnly={readOnly} renderAnnotation={renderAnnotation} renderBlock={renderBlock} renderChild={renderChild} @@ -121,6 +121,7 @@ export function Editor(props: EditorProps) { initialSelection, onCopy, onPaste, + readOnly, renderAnnotation, renderBlock, renderChild, @@ -139,15 +140,17 @@ export function Editor(props: EditorProps) { return ( - - - + {!readOnly && ( + + + + )} diff --git a/packages/sanity/src/form/inputs/PortableText/PortableTextInput.tsx b/packages/sanity/src/form/inputs/PortableText/PortableTextInput.tsx index 81983e7a912..81c77622ed8 100644 --- a/packages/sanity/src/form/inputs/PortableText/PortableTextInput.tsx +++ b/packages/sanity/src/form/inputs/PortableText/PortableTextInput.tsx @@ -91,7 +91,7 @@ export function PortableTextInput(props: PortableTextInputProps) { onInsert, onPaste, path, - readOnly, + readOnly: readOnlyFromProps, renderBlockActions, renderCustomMarkers, schemaType: type, @@ -113,6 +113,12 @@ export function PortableTextInput(props: PortableTextInputProps) { const [ignoreValidationError, setIgnoreValidationError] = useState(false) const [invalidValue, setInvalidValue] = useState(null) const [isFullscreen, setIsFullscreen] = useState(false) + const [isActive, setIsActive] = useState(false) + + const readOnly = useMemo(() => { + return isActive ? Boolean(readOnlyFromProps) : true + }, [isActive, readOnlyFromProps]) + const toast = useToast() const portableTextMemberItemsRef: React.MutableRefObject = useRef([]) @@ -126,7 +132,18 @@ export function PortableTextInput(props: PortableTextInputProps) { const innerElementRef = useRef(null) const handleToggleFullscreen = useCallback(() => { - setIsFullscreen((v) => !v) + if (editorRef.current) { + const prevSel = PortableTextEditor.getSelection(editorRef.current) + setIsFullscreen((v) => !v) + setTimeout(() => { + if (editorRef.current) { + PortableTextEditor.focus(editorRef.current) + if (prevSel) { + PortableTextEditor.select(editorRef.current, {...prevSel}) + } + } + }) + } }, []) // Reset invalidValue if new value is coming in from props @@ -311,6 +328,19 @@ export function PortableTextInput(props: PortableTextInputProps) { } }, [focusPath]) + const focus = useCallback((): void => { + if (editorRef.current) { + PortableTextEditor.focus(editorRef.current) + } + }, [editorRef]) + + const handleActivate = useCallback((): void => { + if (!isActive) { + setIsActive(true) + setTimeout(() => focus()) // Setting active will trigger a re-render of the DOM entry, so call editor focus in the next tick. + } + }, [focus, isActive]) + return ( {!readOnly && ( @@ -338,12 +368,15 @@ export function PortableTextInput(props: PortableTextInputProps) { focusPath={focusPath} hasFocus={hasFocus} hotkeys={hotkeys} + isActive={isActive} isFullscreen={isFullscreen} + onActivate={handleActivate} onChange={onChange} onCopy={onCopy} onInsert={onInsert} onPaste={onPaste} onToggleFullscreen={handleToggleFullscreen} + readOnly={readOnly} renderBlockActions={renderBlockActions} renderCustomMarkers={renderCustomMarkers} value={value}