Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): support insert menu options in array item context menus #6921

Merged
merged 6 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think it would be better to have this be consistent with insertAfter? I personally think setting them both to prefer 'bottom-end' feels better.

This video is the current experience.

CleanShot.2024-06-17.at.14.17.15.mp4

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decision to alter between top-end and bottom-end is primarily made by @mariuslundgard. The idea is that the placement gives a little more context in terms of where the item is inserted (above or below).

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
Loading