diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index e2b8b247d..e2aa429b9 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -6,6 +6,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import ErrorBoundary from './common/components/error-boundary/ErrorBoundary'; import { AppContextProvider } from './common/context/AppContext'; +import { ContextMenuProvider } from './common/context/ContextMenuContext'; import useElectronEvent from './common/hooks/useElectronEvent'; import { ontimeQueryClient } from './common/queryClient'; import { connectSocket } from './common/utils/socket'; @@ -49,16 +50,18 @@ function App() { - -
- - - - - - -
-
+ + +
+ + + + + + +
+
+
diff --git a/apps/client/src/common/context/ContextMenuContext.module.scss b/apps/client/src/common/context/ContextMenuContext.module.scss new file mode 100644 index 000000000..e9cca7c35 --- /dev/null +++ b/apps/client/src/common/context/ContextMenuContext.module.scss @@ -0,0 +1,9 @@ +.contextMenuButton { + position: absolute; + cursor: pointer; +} + +.contextMenuBackdrop { + position: fixed; + inset: 0; +} diff --git a/apps/client/src/common/context/ContextMenuContext.tsx b/apps/client/src/common/context/ContextMenuContext.tsx new file mode 100644 index 000000000..2504c332e --- /dev/null +++ b/apps/client/src/common/context/ContextMenuContext.tsx @@ -0,0 +1,76 @@ +// logic (with some modifications) culled from: +// https://github.com/lukasbach/chakra-ui-contextmenu/blob/main/src/ContextMenu.tsx + +import { createContext, ReactNode, useState } from 'react'; +import { Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react'; +import { IconType } from '@react-icons/all-files'; + +import style from './ContextMenuContext.module.scss'; + +type ContextMenuCoords = { + x: number; + y: number; +}; + +type ContextMenuContextType = { + createContextMenu: (options: Option[], menuCoordinates: ContextMenuCoords) => void; +}; + +export const ContextMenuContext = createContext(null); + +export type Option = { + label: string; + icon: IconType; + onClick: () => void; +}; + +interface ContextMenuProviderProps { + children: ReactNode; +} + +export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { + const [isOpen, setIsOpen] = useState(false); + + const [coords, setCoords] = useState({ x: 0, y: 0 }); + const [options, setOptions] = useState([]); + + const onClose = () => { + return setIsOpen(false); + }; + + const createContextMenu = (options: Option[], menuCoords: ContextMenuCoords) => { + setCoords(menuCoords); + setOptions(options); + setIsOpen(true); + }; + + return ( + + {children} + {isOpen && ( + <> +
+ + + + {options.map(({ label, icon: Icon, onClick }, i) => ( + } onClick={onClick}> + {label} + + ))} + + + + )} + + ); +}; diff --git a/apps/client/src/common/hooks/useContextMenu.tsx b/apps/client/src/common/hooks/useContextMenu.tsx new file mode 100644 index 000000000..d21f574f6 --- /dev/null +++ b/apps/client/src/common/hooks/useContextMenu.tsx @@ -0,0 +1,23 @@ +import { MouseEvent, useContext } from 'react'; + +import { ContextMenuContext, Option } from '../context/ContextMenuContext'; + +export const useContextMenu = (options: Option[]) => { + const contextMenuContext = useContext(ContextMenuContext); + + if (contextMenuContext === null) { + throw new Error('useContextMenu should be wrapped by ContextMenuProvider'); + } + + const { createContextMenu } = contextMenuContext; + + const localCreateContextMenu = (contextMenuEvent: MouseEvent) => { + // prevent browser default context menu from showing up + contextMenuEvent.preventDefault(); + + const { pageX, pageY } = contextMenuEvent; + return createContextMenu(options, { x: pageX, y: pageY }); + }; + + return [localCreateContextMenu]; +}; diff --git a/apps/client/src/features/rundown/event-block/EventBlock.tsx b/apps/client/src/features/rundown/event-block/EventBlock.tsx index ab8675585..dc803ddcd 100644 --- a/apps/client/src/features/rundown/event-block/EventBlock.tsx +++ b/apps/client/src/features/rundown/event-block/EventBlock.tsx @@ -1,9 +1,11 @@ import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; +import { IoAdd } from '@react-icons/all-files/io5/IoAdd'; import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo'; import { EndAction, OntimeEvent, Playback, TimerType } from 'ontime-types'; +import { useContextMenu } from '../../../common/hooks/useContextMenu'; import { useAppMode } from '../../../common/stores/appModeStore'; import { cx, getAccessibleColour } from '../../../common/utils/styleUtils'; import type { EventItemActions } from '../RundownEntry'; @@ -75,6 +77,9 @@ export default function EventBlock(props: EventBlockProps) { const handleRef = useRef(null); const [isVisible, setIsVisible] = useState(false); const openId = useAppMode((state) => state.editId); + const [onContextMenu] = useContextMenu([ + { label: eventId, icon: IoAdd, onClick: () => console.log(eventId) }, + ]); const { isDragging, @@ -142,7 +147,13 @@ export default function EventBlock(props: EventBlockProps) { }; return ( -
+