Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

import { type Meta, type StoryObj } from '@storybook/react'
import React from 'react'
import { ContextMenuWrapper, type ContextMenuWrapperProps, useCloseContextMenu } from './context-menu-wrapper'
import { Menu } from '../menu/menu'
import { Button } from '../button/button'

const config: Meta = {
title: 'Components/Controls/Dropdowns/ContextMenuWrapper',
component: ContextMenuWrapper,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Opens a context menu on right-click, or on Shift+F10 when the wrapped element has focus. ' +
'When opened via keyboard the first menu item is focused, arrow keys navigate the menu and Escape closes it.'
}
}
},
tags: ['autodocs']
}

export default config

const DemoMenu = (): React.JSX.Element => {
const closeMenu = useCloseContextMenu()

return (
<Menu
items={ [
{ key: 'rename', label: 'Rename', onClick: () => { closeMenu?.() } },
{ key: 'copy', label: 'Copy', onClick: () => { closeMenu?.() } },
{ key: 'delete', label: 'Delete', onClick: () => { closeMenu?.() } }
] }
/>
)
}

export const _default: StoryObj<ContextMenuWrapperProps> = {
args: {
children: <Button>Right-click or focus and press Shift+F10</Button>,
renderMenu: () => <DemoMenu />
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @license Pimcore Open Core License (POCL)
*/

import React, { type ReactElement, useState, createContext, useContext } from 'react'
import React, { type ReactElement, useState, useEffect, useRef, createContext, useContext } from 'react'
import { Dropdown } from 'antd'
import { DropdownHeightProvider } from '@Pimcore/components/dropdown/dropdown-height-provider'

Expand All @@ -35,15 +35,45 @@ export interface ContextMenuWrapperProps {

export const ContextMenuWrapper = ({ children, renderMenu, calculateAutoHeight = true }: ContextMenuWrapperProps): React.JSX.Element => {
const [open, setOpen] = useState(false)
const openedViaKeyboard = useRef(false)

const closeMenu = (): void => {
setOpen(false)
}

// When opened via keyboard, focus the first menu item so arrow keys work.
useEffect(() => {
if (open && openedViaKeyboard.current) {
openedViaKeyboard.current = false
requestAnimationFrame(() => {
const menuItem = document.querySelector<HTMLElement>('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item')
menuItem?.focus()
})
}
}, [open])

const handleContextMenu = (event: React.MouseEvent): void => {
event.stopPropagation()
}

const handleKeyDown = (event: React.KeyboardEvent): void => {
if (event.key === 'F10' && event.shiftKey) {
event.preventDefault()
event.stopPropagation()
openedViaKeyboard.current = true

// Dispatch a synthetic contextmenu event at the focused element's position
// so Ant Design anchors the dropdown correctly instead of at (0,0).
const target = event.currentTarget as HTMLElement
const rect = target.getBoundingClientRect()
target.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true,
clientX: rect.left,
clientY: rect.bottom
}))
}
}

const contextValue: ContextMenuContextType = {
closeMenu
}
Expand All @@ -55,12 +85,14 @@ export const ContextMenuWrapper = ({ children, renderMenu, calculateAutoHeight =
{renderMenu()}
</ContextMenuContext.Provider>
) }
getPopupContainer={ () => document.body }
onOpenChange={ setOpen }
open={ open }
trigger={ ['contextMenu'] }
>
<span
onContextMenu={ handleContextMenu }
onKeyDown={ handleKeyDown }
role="none"
>{children}</span>
</Dropdown>
Expand Down
Binary file not shown.
Loading