Line data Source code
1 : import * as React from 'react';
2 :
3 : /// The right click of a row that opens the action menu of the row.
4 : /// The row spreads `onContextMenu`, its action menu registers its opener with `useRowContextMenuOpener`.
5 : export interface RowContextMenu {
6 : /// The handler of the right click of the row
7 : onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
8 : /// The opener of the action menu, null while no menu is registered
9 : openerRef: React.RefObject<(() => void) | null>;
10 : }
11 :
12 : /// Create the right click of a row
13 1 : export function useRowContextMenu(): RowContextMenu {
14 1 : const openerRef = React.useRef<(() => void) | null>(null);
15 1 : const onContextMenu = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
16 1 : const open = openerRef.current;
17 1 : if (open == null || !opensRowContextMenu(event)) return;
18 1 : event.preventDefault();
19 1 : open();
20 1 : }, []);
21 1 : return React.useMemo(() => ({ onContextMenu, openerRef }), [onContextMenu]);
22 1 : }
23 :
24 : /// Register the opener of the action menu of a row
25 1 : export function useRowContextMenuOpener(menu: RowContextMenu | undefined, open: () => void): void {
26 1 : const openRef = React.useRef(open);
27 1 : openRef.current = open;
28 1 : React.useEffect(() => {
29 1 : if (menu == null) return;
30 1 : const opener = () => openRef.current();
31 1 : menu.openerRef.current = opener;
32 1 : return () => {
33 1 : if (menu.openerRef.current === opener) menu.openerRef.current = null;
34 1 : };
35 1 : }, [menu]);
36 1 : }
37 :
38 : // React bubbles the events of the portaled overlays through the row, those keep their own right click.
39 : // Text fields and editors keep the native menu with copy and paste.
40 1 : function opensRowContextMenu(event: React.MouseEvent<HTMLElement>): boolean {
41 1 : const target = event.target;
42 1 : if (!(target instanceof Element) || !event.currentTarget.contains(target)) return false;
43 1 : return target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])') == null;
44 1 : }
|