refactor: improve context menu performance

- extract menu from rundown tree
- lazy create actions
This commit is contained in:
Carlos Valente
2026-01-06 17:07:01 +01:00
committed by Carlos Valente
parent 8e32314667
commit 1f8065143a
6 changed files with 24 additions and 26 deletions
@@ -1,18 +1,24 @@
import { MouseEvent } from 'react'; import { MouseEvent, useCallback } from 'react';
import { useContextMenuStore } from '../../features/rundown/rundown-context-menu/RundownContextMenu'; import { useContextMenuStore } from '../../features/rundown/rundown-context-menu/RundownContextMenu';
import { DropdownMenuOption } from '../components/dropdown-menu/DropdownMenu'; import { DropdownMenuOption } from '../components/dropdown-menu/DropdownMenu';
export const useContextMenu = <T extends HTMLElement>(options: DropdownMenuOption[]) => { type ContextMenuOptions = () => DropdownMenuOption[];
export const useContextMenu = <T extends HTMLElement>(options: ContextMenuOptions) => {
const setContextMenu = useContextMenuStore((state) => state.setContextMenu); const setContextMenu = useContextMenuStore((state) => state.setContextMenu);
const localCreateContextMenu = (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => { const localCreateContextMenu = useCallback(
// prevent browser default context menu from showing up (contextMenuEvent: MouseEvent<T, globalThis.MouseEvent>) => {
contextMenuEvent.preventDefault(); // prevent browser default context menu from showing up
contextMenuEvent.preventDefault();
const { pageX, pageY } = contextMenuEvent; const { pageX, pageY } = contextMenuEvent;
return setContextMenu({ x: pageX, y: pageY }, options); const menuOptions = options();
}; return setContextMenu({ x: pageX, y: pageY }, menuOptions);
},
[options, setContextMenu],
);
return [localCreateContextMenu]; return [localCreateContextMenu];
}; };
@@ -44,9 +44,8 @@ function RundownExport() {
<ViewNavigationMenu suppressSettings /> <ViewNavigationMenu suppressSettings />
<div className={style.rundown}> <div className={style.rundown}>
<ErrorBoundary> <ErrorBoundary>
<RundownContextMenu> <RundownWrapper isSmallDevice viewMode={viewMode} setViewMode={setViewMode} />
<RundownWrapper isSmallDevice viewMode={viewMode} setViewMode={setViewMode} /> <RundownContextMenu />
</RundownContextMenu>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
</div> </div>
@@ -65,9 +64,8 @@ function RundownExport() {
<Editor.Panel className={style.list}> <Editor.Panel className={style.list}>
<ErrorBoundary> <ErrorBoundary>
{!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />} {!isExtracted && <Editor.CornerExtract onClick={(event) => handleLinks('rundown', event)} />}
<RundownContextMenu> <RundownWrapper viewMode={viewMode} setViewMode={setViewMode} />
<RundownWrapper viewMode={viewMode} setViewMode={setViewMode} /> <RundownContextMenu />
</RundownContextMenu>
</ErrorBoundary> </ErrorBoundary>
</Editor.Panel> </Editor.Panel>
{!hideSideBar && ( {!hideSideBar && (
@@ -1,4 +1,3 @@
import type { PropsWithChildren } from 'react';
import { create } from 'zustand'; import { create } from 'zustand';
import { DropdownMenuOption, PositionedDropdownMenu } from '../../../common/components/dropdown-menu/DropdownMenu'; import { DropdownMenuOption, PositionedDropdownMenu } from '../../../common/components/dropdown-menu/DropdownMenu';
@@ -24,7 +23,7 @@ export const useContextMenuStore = create<ContextMenuStore>((set) => ({
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })), setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
})); }));
export function RundownContextMenu({ children }: PropsWithChildren) { export function RundownContextMenu() {
const { position, options, isOpen, setIsOpen } = useContextMenuStore(); const { position, options, isOpen, setIsOpen } = useContextMenuStore();
const onClose = () => { const onClose = () => {
@@ -32,13 +31,8 @@ export function RundownContextMenu({ children }: PropsWithChildren) {
}; };
if (!isOpen) { if (!isOpen) {
return children; return null;
} }
return ( return <PositionedDropdownMenu isOpen position={position} onClose={onClose} items={options} />;
<>
{children}
<PositionedDropdownMenu isOpen position={position} onClose={onClose} items={options} />
</>
);
} }
@@ -109,7 +109,7 @@ export default function RundownEvent({
const handleRef = useRef<null | HTMLSpanElement>(null); const handleRef = useRef<null | HTMLSpanElement>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [onContextMenu] = useContextMenu<HTMLDivElement>( const [onContextMenu] = useContextMenu<HTMLDivElement>(() =>
selectedEvents.size > 1 selectedEvents.size > 1
? [ ? [
{ {
@@ -41,7 +41,7 @@ export default function RundownGroup({ data, hasCursor, collapsed, onCollapse }:
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection); const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
const selectedEvents = useEventSelection((state) => state.selectedEvents); const selectedEvents = useEventSelection((state) => state.selectedEvents);
const [onContextMenu] = useContextMenu<HTMLDivElement>([ const [onContextMenu] = useContextMenu<HTMLDivElement>(() => [
{ {
type: 'item', type: 'item',
label: 'Clone Group', label: 'Clone Group',
@@ -30,7 +30,7 @@ export default function RundownMilestone({ colour, cue, entryId, hasCursor, titl
const selectedEvents = useEventSelection((state) => state.selectedEvents); const selectedEvents = useEventSelection((state) => state.selectedEvents);
const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection); const setSingleEntrySelection = useEventSelection((state) => state.setSingleEntrySelection);
const [onContextMenu] = useContextMenu<HTMLDivElement>([ const [onContextMenu] = useContextMenu<HTMLDivElement>(() => [
{ {
type: 'item', type: 'item',
label: 'Delete', label: 'Delete',