import type {SchemaNode, SchemaUnionNode} from '@sanity/presentation-comlink' import type {SchemaType} from '@sanity/types' import {AddIcon} from '@sanity/icons' import {Button, Flex} from '@sanity/ui/_visual-editing' import { useCallback, useRef, useState, type FunctionComponent, type HTMLProps, type MouseEvent, } from 'react' import {styled} from 'styled-components' import type {ElementNode, OverlayComponent} from '../../types' import {useDocuments} from '../../react/useDocuments' import {useTelemetry} from '../../ui/telemetry/useTelemetry' import {getArrayInsertPatches} from '../../util/mutations' import {InsertMenuPopover} from './InsertMenu' const AddButton = styled(Button)` position: relative; transform: var(--add-button-position); --add-button-position: translateY(0); [data-position='top'] & { --add-button-position: translateY(-50%); } [data-position='right'] & { --add-button-position: translateX(50%); } [data-position='bottom'] & { --add-button-position: translateY(50%); } [data-position='left'] & { --add-button-position: translateX(-50%); } ` const HoverAreaRoot = styled(Flex)` pointer-events: all; height: var(--hover-area-height); width: var(--hover-area-width); --hover-area-height: 100%; --hover-area-width: 100%; &[data-position='top'], &[data-position='bottom'] { --hover-area-height: 48px; } &[data-position='right'], &[data-position='left'] { --hover-area-width: 48px; } ` const HoverArea: FunctionComponent<{ element: ElementNode hoverAreaExtent: HTMLProps['height' | 'width'] node: SchemaUnionNode onAddUnion: (insertPosition: 'before' | 'after', name: string) => void position: 'top' | 'right' | 'bottom' | 'left' }> = (props) => { const {element, hoverAreaExtent, node, onAddUnion, position} = props const [showButton, setShowButton] = useState(false) const onEnter = useCallback(() => { setShowButton(true) }, []) const onLeave = useCallback(() => { setShowButton(false) }, []) const ref = useRef(null) const sendTelemetry = useTelemetry() // This function clones and dispatches MouseEvents so that they can be handled // by the underlying element. This is useful because we want to handle hover // events on the overlay element to display the add button, but let the // underlying element handle click events, drag and drop, etc. const relayEventToElement = useCallback( (event: MouseEvent) => { if (event.target === ref.current) { const newEvent = new MouseEvent(event.type, { ...event.nativeEvent, bubbles: true, cancelable: true, }) element.dispatchEvent(newEvent) } }, [element], ) // The element that the popover containing the InsertMenu will be positioned // relative to (in this case, the AddButton). const [popoverReferenceElement, setPopoverReferenceElement] = useState(null) const [menuVisible, setMenuVisible] = useState(false) const dismissPortal = useCallback(() => { setMenuVisible(false) setShowButton(false) }, []) const onSelect = useCallback( (schemaType: SchemaType) => { setMenuVisible(false) const insertPosition = position === 'top' || position === 'left' ? 'before' : 'after' onAddUnion(insertPosition, schemaType.name) sendTelemetry('Visual Editing Insert Menu Item Inserted', null) }, [onAddUnion, position, sendTelemetry], ) const align = position === 'top' ? 'flex-start' : position === 'bottom' ? 'flex-end' : 'center' const justify = position === 'left' ? 'flex-start' : position === 'right' ? 'flex-end' : 'center' const blockDirection = position === 'top' || position === 'bottom' ? 'height' : 'width' return ( {(showButton || menuVisible) && ( setMenuVisible((visible) => !visible)} radius={'full'} selected={menuVisible} /> )} {menuVisible && popoverReferenceElement && ( )} ) } export const UnionInsertMenuOverlay: OverlayComponent< { direction?: 'horizontal' | 'vertical' hoverAreaExtent?: HTMLProps['height' | 'width'] }, SchemaUnionNode > = (props) => { const {direction = 'vertical', element, hoverAreaExtent, node, parent} = props const {getDocument} = useDocuments() const onAddUnion = useCallback( (insertPosition: 'before' | 'after', name: string) => { const doc = getDocument(node.id) const patches = getArrayInsertPatches(node, name, insertPosition) doc.patch(patches) }, [getDocument, node], ) if (!parent) return null return ( ) } export default UnionInsertMenuOverlay