Skip to content

Commit

Permalink
refactor(inputs): improve fullscreen and activation re-rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
skogsmaskin committed Aug 11, 2022
1 parent e7f36a5 commit d6d8aa7
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 46 deletions.
Expand Up @@ -52,4 +52,7 @@ export const ExpandedLayer = styled(Layer)`
left: 0;
right: 0;
bottom: 0;
&:not([data-fullscreen]) {
position: relative;
}
`
61 changes: 28 additions & 33 deletions 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,
Expand Down Expand Up @@ -38,7 +38,9 @@ import {_isBlockType} from './_helpers'
interface InputProps extends ArrayOfObjectsInputProps<PortableTextBlock> {
hasFocus: boolean
hotkeys?: HotkeyOptions
isActive: boolean
isFullscreen: boolean
onActivate: () => void
onCopy?: OnCopyFn
onPaste?: OnPasteFn
onToggleFullscreen: () => void
Expand All @@ -49,7 +51,16 @@ interface InputProps extends ArrayOfObjectsInputProps<PortableTextBlock> {

export type PortableTextEditorElement = HTMLDivElement | HTMLSpanElement | null

const ACTIVATE_ON_FOCUS_MESSAGE = <Text weight="semibold">Click to activate</Text>
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 = <Text weight="semibold">{activateVerb} to activate</Text>

export function Compositor(props: InputProps) {
const {
Expand All @@ -58,12 +69,13 @@ export function Compositor(props: InputProps) {
focused,
hasFocus,
hotkeys,
isActive,
isFullscreen,
onChange,
onCopy,
onActivate,
onOpenItem,
onCloseItem,
onFocusPath,
onPaste,
onToggleFullscreen,
path,
Expand All @@ -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<HTMLDivElement | null>(null)
const [scrollElement, setScrollElement] = useState<HTMLElement | null>(null)
const portableTextMemberItems = usePortableTextMemberItems()
Expand All @@ -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()
Expand All @@ -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

Expand Down Expand Up @@ -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}
Expand All @@ -280,11 +271,9 @@ export function Compositor(props: InputProps) {
editorHotkeys,
handleToggleFullscreen,
initialSelection,
isActive,
isFullscreen,
onCopy,
onOpenItem,
onFocusPath,
onPaste,
path,
readOnly,
Expand Down Expand Up @@ -333,25 +322,31 @@ export function Compositor(props: InputProps) {

[portal.element, portalElement, wrapperElement]
)

const editorLayer = useMemo(
() => (
<Portal __unstable_name={isFullscreen ? 'expanded' : 'collapsed'}>
<ExpandedLayer data-fullscreen={isFullscreen ? '' : undefined}>{children}</ExpandedLayer>
</Portal>
),
[children, isFullscreen]
)
return (
<PortalProvider __unstable_elements={portalElements}>
<ActivateOnFocus
message={ACTIVATE_ON_FOCUS_MESSAGE}
onActivate={handleActivate}
onActivate={onActivate}
isOverlayActive={!isActive}
>
<ChangeIndicator
disabled={isFullscreen}
hasFocus={!!focused}
hasFocus={Boolean(focused)}
isChanged={changed}
path={path}
>
<Root data-focused={hasFocus ? '' : undefined} data-read-only={readOnly ? '' : undefined}>
<div data-wrapper="" ref={setWrapperElement}>
<Portal __unstable_name={isFullscreen ? 'expanded' : 'collapsed'}>
{/* TODO: Can we get rid of this DOM-rerender? */}
{isFullscreen ? <ExpandedLayer>{children}</ExpandedLayer> : children}
</Portal>
{editorLayer}
</div>
<div data-border="" />
</Root>
Expand Down
25 changes: 14 additions & 11 deletions packages/sanity/src/form/inputs/PortableText/Editor.tsx
Expand Up @@ -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
Expand All @@ -53,7 +53,6 @@ export function Editor(props: EditorProps) {
initialSelection,
isFullscreen,
onCopy,
onFocusPath,
onOpenItem,
onPaste,
onToggleFullscreen,
Expand Down Expand Up @@ -106,6 +105,7 @@ export function Editor(props: EditorProps) {
onCopy={onCopy}
onPaste={onPaste}
ref={editableRef}
readOnly={readOnly}
renderAnnotation={renderAnnotation}
renderBlock={renderBlock}
renderChild={renderChild}
Expand All @@ -121,6 +121,7 @@ export function Editor(props: EditorProps) {
initialSelection,
onCopy,
onPaste,
readOnly,
renderAnnotation,
renderBlock,
renderChild,
Expand All @@ -139,15 +140,17 @@ export function Editor(props: EditorProps) {

return (
<Root $fullscreen={isFullscreen} data-testid="pt-editor" onMouseDown={handleMouseDown}>
<ToolbarCard data-testid="pt-editor__toolbar-card" shadow={1}>
<Toolbar
isFullscreen={isFullscreen}
hotkeys={hotkeys}
onExpand={handleToolBarOnExpand}
readOnly={readOnly}
onToggleFullscreen={onToggleFullscreen}
/>
</ToolbarCard>
{!readOnly && (
<ToolbarCard data-testid="pt-editor__toolbar-card" shadow={1}>
<Toolbar
isFullscreen={isFullscreen}
hotkeys={hotkeys}
onExpand={handleToolBarOnExpand}
readOnly={readOnly}
onToggleFullscreen={onToggleFullscreen}
/>
</ToolbarCard>
)}

<EditableCard flex={1}>
<Scroller ref={setScrollElement}>
Expand Down
37 changes: 35 additions & 2 deletions packages/sanity/src/form/inputs/PortableText/PortableTextInput.tsx
Expand Up @@ -91,7 +91,7 @@ export function PortableTextInput(props: PortableTextInputProps) {
onInsert,
onPaste,
path,
readOnly,
readOnly: readOnlyFromProps,
renderBlockActions,
renderCustomMarkers,
schemaType: type,
Expand All @@ -113,6 +113,12 @@ export function PortableTextInput(props: PortableTextInputProps) {
const [ignoreValidationError, setIgnoreValidationError] = useState(false)
const [invalidValue, setInvalidValue] = useState<InvalidValue | null>(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<PortableTextMemberItem[]> = useRef([])

Expand All @@ -126,7 +132,18 @@ export function PortableTextInput(props: PortableTextInputProps) {
const innerElementRef = useRef<HTMLDivElement | null>(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
Expand Down Expand Up @@ -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 (
<Box ref={innerElementRef}>
{!readOnly && (
Expand Down Expand Up @@ -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}
Expand Down

0 comments on commit d6d8aa7

Please sign in to comment.