Skip to content

Commit

Permalink
feat(core): support insert menu options in array item context menus (#…
Browse files Browse the repository at this point in the history
…6921)

* chore(core): introduce useInsertMenuMenuItems

* chore(core): allow ContextMenuButton.selected

* feat(core): support insert menu options in preview item context menu

* feat(core): support insert menu options in grid item context menu

* feat(core): support insert menu options in reference item context menu

* chore(core): add array item context menu insert menu test cases
  • Loading branch information
christianhg committed Jun 21, 2024
1 parent 6b9f910 commit 784cfd3
Show file tree
Hide file tree
Showing 6 changed files with 432 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {type ForwardedRef, forwardRef, type HTMLProps} from 'react'
import {Button, type ButtonProps} from '../../../ui-components'
import {useTranslation} from '../..'

type ContextMenuButtonProps = Pick<ButtonProps, 'mode' | 'size' | 'tone' | 'tooltipProps'>
type ContextMenuButtonProps = Pick<
ButtonProps,
'mode' | 'selected' | 'size' | 'tone' | 'tooltipProps'
>

/**
* Simple context menu button (with horizontal ellipsis icon) with shared localization.
Expand Down
113 changes: 68 additions & 45 deletions packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
useCallback,
useMemo,
useRef,
useState,
} from 'react'
import {IntentLink} from 'sanity/router'

Expand All @@ -30,7 +31,7 @@ import {set, unset} from '../../patch'
import {type ObjectItem, type ObjectItemProps} from '../../types'
import {randomKey} from '../../utils/randomKey'
import {createProtoArrayValue} from '../arrays/ArrayOfObjectsInput/createProtoArrayValue'
import {InsertMenuGroups} from '../arrays/ArrayOfObjectsInput/InsertMenuGroups'
import {useInsertMenuMenuItems} from '../arrays/ArrayOfObjectsInput/InsertMenuMenuItems'
import {RowLayout} from '../arrays/layouts/RowLayout'
import {PreviewReferenceValue} from './PreviewReferenceValue'
import {ReferenceFinalizeAlertStrip} from './ReferenceFinalizeAlertStrip'
Expand Down Expand Up @@ -185,62 +186,84 @@ export function ReferenceItem<Item extends ReferenceItemValue = ReferenceItemVal
onPathFocus(['_ref'])
}
}, [hasRef, isEditing, onPathFocus])
const [contextMenuButtonElement, setContextMenuButtonElement] =
useState<HTMLButtonElement | null>(null)
const {insertBefore, insertAfter} = useInsertMenuMenuItems({
schemaTypes: insertableTypes,
insertMenuOptions: parentSchemaType.options?.insertMenu,
onInsert: handleInsert,
referenceElement: contextMenuButtonElement,
})

const menu = useMemo(
() =>
readOnly ? null : (
<MenuButton
button={<ContextMenuButton />}
id={`${inputId}-menuButton`}
menu={
<Menu ref={menuRef}>
{!readOnly && (
<>
<MenuItem
text={t('inputs.reference.action.remove')}
tone="critical"
icon={TrashIcon}
onClick={onRemove}
/>
<MenuItem
text={t(
hasRef && isEditing
? 'inputs.reference.action.replace-cancel'
: 'inputs.reference.action.replace',
)}
icon={hasRef && isEditing ? CloseIcon : ReplaceIcon}
onClick={handleReplace}
/>
<>
<MenuButton
ref={setContextMenuButtonElement}
onOpen={() => {
insertBefore.send({type: 'close'})
insertAfter.send({type: 'close'})
}}
button={
<ContextMenuButton
selected={insertBefore.state.open || insertAfter.state.open ? true : undefined}
/>
}
id={`${inputId}-menuButton`}
menu={
<Menu ref={menuRef}>
{!readOnly && (
<>
<MenuItem
text={t('inputs.reference.action.remove')}
tone="critical"
icon={TrashIcon}
onClick={onRemove}
/>
<MenuItem
text={t(
hasRef && isEditing
? 'inputs.reference.action.replace-cancel'
: 'inputs.reference.action.replace',
)}
icon={hasRef && isEditing ? CloseIcon : ReplaceIcon}
onClick={handleReplace}
/>
<MenuItem
text={t('inputs.reference.action.duplicate')}
icon={DuplicateIcon}
onClick={handleDuplicate}
/>
{insertBefore.menuItem}
{insertAfter.menuItem}
</>
)}

{!readOnly && !isEditing && hasRef && <MenuDivider />}
{!isEditing && hasRef && (
<MenuItem
text={t('inputs.reference.action.duplicate')}
icon={DuplicateIcon}
onClick={handleDuplicate}
as={OpenLink}
data-as="a"
text={t('inputs.reference.action.open-in-new-tab')}
icon={OpenInNewTabIcon}
/>
<InsertMenuGroups onInsert={handleInsert} types={insertableTypes} />
</>
)}

{!readOnly && !isEditing && hasRef && <MenuDivider />}
{!isEditing && hasRef && (
<MenuItem
as={OpenLink}
data-as="a"
text={t('inputs.reference.action.open-in-new-tab')}
icon={OpenInNewTabIcon}
/>
)}
</Menu>
}
popover={MENU_POPOVER_PROPS}
/>
)}
</Menu>
}
popover={MENU_POPOVER_PROPS}
/>
{insertBefore.popover}
{insertAfter.popover}
</>
),
[
handleDuplicate,
handleInsert,
handleReplace,
hasRef,
inputId,
insertableTypes,
insertBefore,
insertAfter,
isEditing,
onRemove,
OpenLink,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {CopyIcon as DuplicateIcon, TrashIcon} from '@sanity/icons'
import {type SchemaType} from '@sanity/types'
import {Box, Card, type CardTone, Menu} from '@sanity/ui'
import {useCallback, useMemo, useRef} from 'react'
import {useCallback, useMemo, useRef, useState} from 'react'
import {styled} from 'styled-components'

import {MenuButton, MenuItem} from '../../../../../../ui-components'
Expand All @@ -22,7 +22,7 @@ import {type ObjectItem, type ObjectItemProps} from '../../../../types'
import {randomKey} from '../../../../utils/randomKey'
import {CellLayout} from '../../layouts/CellLayout'
import {createProtoArrayValue} from '../createProtoArrayValue'
import {InsertMenuGroups} from '../InsertMenuGroups'
import {useInsertMenuMenuItems} from '../InsertMenuMenuItems'

type GridItemProps<Item extends ObjectItem> = Omit<ObjectItemProps<Item>, 'renderDefault'>

Expand Down Expand Up @@ -134,33 +134,55 @@ export function GridItem<Item extends ObjectItem = ObjectItem>(props: GridItemPr

const hasErrors = childValidation.some((v) => v.level === 'error')
const hasWarnings = childValidation.some((v) => v.level === 'warning')
const [contextMenuButtonElement, setContextMenuButtonElement] =
useState<HTMLButtonElement | null>(null)
const {insertBefore, insertAfter} = useInsertMenuMenuItems({
schemaTypes: insertableTypes,
insertMenuOptions: parentSchemaType.options?.insertMenu,
onInsert: handleInsert,
referenceElement: contextMenuButtonElement,
})

const menu = useMemo(
() =>
readOnly ? null : (
<MenuButton
button={<ContextMenuButton />}
id={`${props.inputId}-menuButton`}
menu={
<Menu>
<MenuItem
text={t('inputs.array.action.remove')}
tone="critical"
icon={TrashIcon}
onClick={onRemove}
/>
<MenuItem
text={t('inputs.array.action.duplicate')}
icon={DuplicateIcon}
onClick={handleDuplicate}
<>
<MenuButton
ref={setContextMenuButtonElement}
onOpen={() => {
insertBefore.send({type: 'close'})
insertAfter.send({type: 'close'})
}}
button={
<ContextMenuButton
selected={insertBefore.state.open || insertAfter.state.open ? true : undefined}
/>
<InsertMenuGroups types={insertableTypes} onInsert={handleInsert} />
</Menu>
}
popover={MENU_POPOVER_PROPS}
/>
}
id={`${props.inputId}-menuButton`}
menu={
<Menu>
<MenuItem
text={t('inputs.array.action.remove')}
tone="critical"
icon={TrashIcon}
onClick={onRemove}
/>
<MenuItem
text={t('inputs.array.action.duplicate')}
icon={DuplicateIcon}
onClick={handleDuplicate}
/>
{insertBefore.menuItem}
{insertAfter.menuItem}
</Menu>
}
popover={MENU_POPOVER_PROPS}
/>
{insertBefore.popover}
{insertAfter.popover}
</>
),
[handleDuplicate, handleInsert, onRemove, insertableTypes, props.inputId, readOnly, t],
[insertBefore, insertAfter, handleDuplicate, onRemove, props.inputId, readOnly, t],
)

const tone = getTone({readOnly, hasErrors, hasWarnings})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {InsertAboveIcon, InsertBelowIcon} from '@sanity/icons'
import {type InsertMenuOptions} from '@sanity/insert-menu'
import {type SchemaType} from '@sanity/types'
import {useCallback, useMemo} from 'react'
import {useTranslation} from 'sanity'

import {MenuItem} from '../../../../../ui-components'
import {useInsertMenuPopover} from './InsertMenuPopover'

/**
* @internal
*/
type InsertMenuItemsProps = {
insertMenuOptions?: InsertMenuOptions
onInsert: (pos: 'before' | 'after', type: SchemaType) => void
referenceElement: HTMLElement | null
schemaTypes?: SchemaType[]
}

/**
* @internal
*/
export function useInsertMenuMenuItems(props: InsertMenuItemsProps) {
const {t} = useTranslation()
const {onInsert, schemaTypes: types} = props
const insertBefore = useInsertMenuPopover({
insertMenuProps: {
...props.insertMenuOptions,
schemaTypes: props.schemaTypes ?? [],
onSelect: (insertType) => {
props.onInsert('before', insertType)
},
},
popoverProps: {
referenceElement: props.referenceElement,
placement: 'top-end',
fallbackPlacements: ['bottom-end'],
},
})
const insertAfter = useInsertMenuPopover({
insertMenuProps: {
...props.insertMenuOptions,
schemaTypes: props.schemaTypes ?? [],
onSelect: (insertType) => {
props.onInsert('after', insertType)
},
},
popoverProps: {
referenceElement: props.referenceElement,
placement: 'bottom-end',
fallbackPlacements: ['top-end'],
},
})
const handleToggleInsertBefore = useCallback(() => {
if (!types) {
return
}

if (types.length === 1) {
onInsert('before', types[0])
} else {
insertBefore.send({type: 'toggle'})
}
}, [insertBefore, onInsert, types])
const handleToggleInsertAfter = useCallback(() => {
if (!types) {
return
}

if (types.length === 1) {
onInsert('after', types[0])
} else {
insertAfter.send({type: 'toggle'})
}
}, [insertAfter, onInsert, types])

const insertBeforeMenuItem = useMemo(
() =>
types ? (
<MenuItem
text={
types.length === 1
? t('inputs.array.action.add-before')
: `${t('inputs.array.action.add-before')}...`
}
icon={InsertAboveIcon}
onClick={handleToggleInsertBefore}
/>
) : null,
[handleToggleInsertBefore, t, types],
)
const insertAfterMenuItem = useMemo(
() =>
types ? (
<MenuItem
text={
types.length === 1
? t('inputs.array.action.add-after')
: `${t('inputs.array.action.add-after')}...`
}
icon={InsertBelowIcon}
onClick={handleToggleInsertAfter}
/>
) : null,
[handleToggleInsertAfter, t, types],
)

return {
insertBefore: {
...insertBefore,
menuItem: insertBeforeMenuItem,
},
insertAfter: {
...insertAfter,
menuItem: insertAfterMenuItem,
},
}
}
Loading

0 comments on commit 784cfd3

Please sign in to comment.