@xh/hoist 74.0.0-SNAPSHOT.1749232318711 → 74.0.0-SNAPSHOT.1749665625714

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import {RuleLike} from '@xh/hoist/data';
9
- import {MouseEvent, ReactElement, ReactNode} from 'react';
9
+ import {MouseEvent, ReactElement, ReactNode, isValidElement} from 'react';
10
+ import {isString} from 'lodash';
10
11
  import {LoadSpec} from '../load';
11
12
  import {Intent, PlainObject, Thunkable} from './Types';
12
13
 
@@ -265,12 +266,41 @@ export interface TrackOptions {
265
266
  omit?: Thunkable<boolean>;
266
267
  }
267
268
 
269
+ /**
270
+ * The base `MenuToken` type. '-' is interpreted as the standard textless divider.
271
+ * Components will likely extend this type to support other strings like 'copyToClipboard',
272
+ * 'print', etc. which the component then converts into a {@link MenuItem}.
273
+ */
274
+ export type MenuToken = '-';
275
+
276
+ /**
277
+ * `MenuContext` is the set of contextual arguments passed to a {@link MenuItem}'s
278
+ * `actionFn` and `prepareFn`. `contextMenuEvent` is the right click event that opened the
279
+ * context menu. It is optional because the `contextMenu` component can also be used on
280
+ * popover buttons, where there is no `contextMenuEvent`.
281
+ *
282
+ * Components offering a built-in {@link contextMenu} can extend `MenuContext` to add values
283
+ * relevant to the component. See for example {@link ChartMenuContext}.
284
+ */
285
+ export interface MenuContext {
286
+ contextMenuEvent?: MouseEvent | PointerEvent;
287
+ }
288
+
289
+ /**
290
+ * A context menu is specified as an array of items, a function to generate one from a click, or
291
+ * a full element representing a contextMenu Component.
292
+ */
293
+ export type ContextMenuSpec<T = MenuToken, C = MenuContext> =
294
+ | MenuItemLike<T, C>[]
295
+ | ((e: MouseEvent | PointerEvent, context: C) => MenuItemLike<T, C>[])
296
+ | boolean;
297
+
268
298
  /**
269
299
  * Basic interface for a MenuItem to appear in a menu.
270
300
  *
271
301
  * MenuItems can be displayed within a context menu, or shown when clicking on a button.
272
302
  */
273
- export interface MenuItem {
303
+ export interface MenuItem<T = MenuToken, C = MenuContext> {
274
304
  /** Label to be displayed. */
275
305
  text: ReactNode;
276
306
 
@@ -284,13 +314,13 @@ export interface MenuItem {
284
314
  className?: string;
285
315
 
286
316
  /** Executed when the user clicks the menu item. */
287
- actionFn?: (e: MouseEvent | PointerEvent) => void;
317
+ actionFn?: (e: MouseEvent | PointerEvent, context?: C) => void;
288
318
 
289
319
  /** Executed before the item is shown. Use to adjust properties dynamically. */
290
- prepareFn?: (me: MenuItem) => void;
320
+ prepareFn?: (me: MenuItem<T, C>, context?: C) => void;
291
321
 
292
322
  /** Child menu items. */
293
- items?: MenuItemLike[];
323
+ items?: MenuItemLike<T, C>[];
294
324
 
295
325
  /** True to disable this item. */
296
326
  disabled?: boolean;
@@ -304,12 +334,15 @@ export interface MenuItem {
304
334
 
305
335
  /**
306
336
  * An item that can exist in a Menu.
307
- *
308
- * Allows for a ReactNode as divider. If strings are specified, the implementations may choose
309
- * an appropriate default display, with '-' providing a standard textless divider that will also
310
- * be de-duped if appearing at the beginning, or end, or adjacent to another divider at render time.
337
+ * Components may accept token strings, in addition, '-' will be interpreted as the standard
338
+ * textless divider that will also be de-duped if appearing at the beginning, or end, or adjacent
339
+ * to another divider at render time. Also allows for a ReactNode for flexible display.
311
340
  */
312
- export type MenuItemLike = MenuItem | ReactNode;
341
+ export type MenuItemLike<T = MenuToken, C = MenuContext> = MenuItem<T, C> | T | ReactElement;
342
+
343
+ export function isMenuItem<T, C>(item: MenuItemLike<T, C>): item is MenuItem<T, C> {
344
+ return !isString(item) && !isValidElement(item);
345
+ }
313
346
 
314
347
  /**
315
348
  * An option to be passed to Select controls
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {MenuItemProps} from '@blueprintjs/core';
8
- import {hoistCmp, MenuItemLike, MenuItem, XH} from '@xh/hoist/core';
8
+ import {hoistCmp, isMenuItem, MenuItemLike, XH} from '@xh/hoist/core';
9
9
  import {ButtonProps, button} from '@xh/hoist/desktop/cmp/button';
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {Icon} from '@xh/hoist/icon';
@@ -13,8 +13,8 @@ import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
13
13
  import {wait} from '@xh/hoist/promise';
14
14
  import {filterConsecutiveMenuSeparators, isOmitted} from '@xh/hoist/utils/impl';
15
15
  import {withDefault} from '@xh/hoist/utils/js';
16
- import {clone, isEmpty, isString} from 'lodash';
17
- import {isValidElement, ReactNode} from 'react';
16
+ import {clone, isEmpty} from 'lodash';
17
+ import {ReactNode} from 'react';
18
18
 
19
19
  export interface AppMenuButtonProps extends ButtonProps {
20
20
  /**
@@ -215,7 +215,3 @@ function parseMenuItems(items: MenuItemLike[]): ReactNode[] {
215
215
  return menuItem(cfg);
216
216
  });
217
217
  }
218
-
219
- function isMenuItem(item: MenuItemLike): item is MenuItem {
220
- return !isString(item) && !isValidElement(item);
221
- }
@@ -4,22 +4,17 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {hoistCmp, HoistProps, MenuItem, MenuItemLike} from '@xh/hoist/core';
7
+ import {hoistCmp, HoistProps, isMenuItem, MenuContext, MenuItemLike} from '@xh/hoist/core';
8
8
  import '@xh/hoist/desktop/register';
9
9
  import {menu, menuDivider, menuItem} from '@xh/hoist/kit/blueprint';
10
10
  import {wait} from '@xh/hoist/promise';
11
11
  import {filterConsecutiveMenuSeparators, isOmitted} from '@xh/hoist/utils/impl';
12
- import {clone, isEmpty, isString} from 'lodash';
13
- import {isValidElement, ReactElement, ReactNode} from 'react';
14
-
15
- /**
16
- * A context menu is specified as an array of items, a function to generate one from a click, or
17
- * a full element representing a contextMenu Component.
18
- */
19
- export type ContextMenuSpec = MenuItemLike[] | ((e: MouseEvent) => MenuItemLike[]) | ReactElement;
12
+ import {clone, isEmpty} from 'lodash';
13
+ import {ReactNode} from 'react';
20
14
 
21
15
  export interface ContextMenuProps extends HoistProps {
22
16
  menuItems: MenuItemLike[];
17
+ context?: MenuContext;
23
18
  }
24
19
 
25
20
  /**
@@ -36,8 +31,8 @@ export const [ContextMenu, contextMenu] = hoistCmp.withFactory<ContextMenuProps>
36
31
  model: false,
37
32
  observer: false,
38
33
 
39
- render({menuItems}) {
40
- const items = parseItems(menuItems);
34
+ render({menuItems, context}) {
35
+ const items = parseItems(menuItems, context);
41
36
  return isEmpty(items) ? null : menu(items);
42
37
  }
43
38
  });
@@ -45,13 +40,13 @@ export const [ContextMenu, contextMenu] = hoistCmp.withFactory<ContextMenuProps>
45
40
  //---------------------------
46
41
  // Implementation
47
42
  //---------------------------
48
- function parseItems(items: MenuItemLike[]): ReactNode[] {
43
+ function parseItems(items: MenuItemLike[], context: MenuContext): ReactNode[] {
49
44
  items = items.map(item => {
50
45
  if (!isMenuItem(item)) return item;
51
46
 
52
47
  item = clone(item);
53
48
  item.items = clone(item.items);
54
- item.prepareFn?.(item);
49
+ item.prepareFn?.(item, context);
55
50
  return item;
56
51
  });
57
52
 
@@ -64,20 +59,16 @@ function parseItems(items: MenuItemLike[]): ReactNode[] {
64
59
  if (!isMenuItem(item)) return item;
65
60
 
66
61
  // Process items
67
- const items = item.items ? parseItems(item.items) : null;
62
+ const items = item.items ? parseItems(item.items, context) : null;
68
63
  return menuItem({
69
64
  text: item.text,
70
65
  icon: item.icon,
71
66
  intent: item.intent,
72
67
  className: item.className,
73
- onClick: item.actionFn ? e => wait().then(() => item.actionFn(e)) : null, // do async to allow menu to close
68
+ onClick: item.actionFn ? e => wait().then(() => item.actionFn(e, context)) : null, // do async to allow menu to close
74
69
  popoverProps: {usePortal: true},
75
70
  disabled: item.disabled,
76
71
  items
77
72
  });
78
73
  });
79
74
  }
80
-
81
- function isMenuItem(item: MenuItemLike): item is MenuItem {
82
- return !isString(item) && !isValidElement(item);
83
- }
@@ -129,7 +129,8 @@ const onContextMenu = (e, model) => {
129
129
  showContextMenu(
130
130
  dashCanvasContextMenu({
131
131
  dashCanvasModel: model,
132
- position: {x, y}
132
+ position: {x, y},
133
+ contextMenuEvent: e
133
134
  }),
134
135
  {left: clientX, top: clientY}
135
136
  );
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {hoistCmp} from '@xh/hoist/core';
8
+ import {hoistCmp, type MenuItemLike} from '@xh/hoist/core';
9
9
  import {contextMenu} from '@xh/hoist/desktop/cmp/contextmenu';
10
10
  import {createViewMenuItems} from '@xh/hoist/desktop/cmp/dash/canvas/impl/utils';
11
11
  import {Icon} from '@xh/hoist/icon';
@@ -21,16 +21,16 @@ import {isEmpty} from 'lodash';
21
21
  export const dashCanvasContextMenu = hoistCmp.factory({
22
22
  model: null,
23
23
  observer: null,
24
- render({dashCanvasModel, position}) {
24
+ render({dashCanvasModel, position, contextMenuEvent}) {
25
25
  const menuItems = createMenuItems({dashCanvasModel, position});
26
- return contextMenu({menuItems});
26
+ return contextMenu({menuItems, context: {contextMenuEvent}});
27
27
  }
28
28
  });
29
29
 
30
30
  //---------------------------
31
31
  // Implementation
32
32
  //---------------------------
33
- function createMenuItems({dashCanvasModel, position}) {
33
+ function createMenuItems({dashCanvasModel, position}): MenuItemLike[] {
34
34
  const addMenuItems = createViewMenuItems({dashCanvasModel, position}),
35
35
  {extraMenuItems, contentLocked, refreshContextModel} = dashCanvasModel;
36
36
  return [
@@ -5,6 +5,7 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
+ import {type MenuItemLike} from '@xh/hoist/core';
8
9
  import {Icon} from '@xh/hoist/icon';
9
10
  import {DashCanvasModel} from '../DashCanvasModel';
10
11
 
@@ -17,7 +18,7 @@ export function createViewMenuItems({
17
18
  position = null,
18
19
  viewId = null,
19
20
  replaceExisting = false
20
- }) {
21
+ }): MenuItemLike[] {
21
22
  if (!dashCanvasModel.ref.current) return [];
22
23
 
23
24
  const groupedItems = {},
@@ -486,7 +486,8 @@ export class DashContainerModel
486
486
  stack,
487
487
  viewModel,
488
488
  index,
489
- dashContainerModel: this
489
+ dashContainerModel: this,
490
+ contextMenuEvent: e
490
491
  });
491
492
 
492
493
  showContextMenu(menu, offset);
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {hoistCmp} from '@xh/hoist/core';
7
+ import {hoistCmp, type MenuItemLike} from '@xh/hoist/core';
8
8
  import {button} from '@xh/hoist/desktop/cmp/button';
9
9
  import {contextMenu} from '@xh/hoist/desktop/cmp/contextmenu/ContextMenu';
10
10
  import {Icon} from '@xh/hoist/icon';
@@ -23,9 +23,9 @@ import {DashContainerModel} from '../DashContainerModel';
23
23
  export const dashContainerContextMenu = hoistCmp.factory({
24
24
  model: null,
25
25
  observer: null,
26
- render(props) {
27
- const menuItems = createMenuItems(props);
28
- return contextMenu({menuItems});
26
+ render({contextMenuEvent, ...rest}) {
27
+ const menuItems = createMenuItems(rest);
28
+ return contextMenu({menuItems, context: {contextMenuEvent}});
29
29
  }
30
30
  });
31
31
 
@@ -48,7 +48,7 @@ export const dashContainerAddViewButton = hoistCmp.factory<DashContainerModel>({
48
48
  //---------------------------
49
49
  // Implementation
50
50
  //---------------------------
51
- function createMenuItems(props) {
51
+ function createMenuItems(props): MenuItemLike[] {
52
52
  const {dashContainerModel, viewModel} = props,
53
53
  {renameLocked} = dashContainerModel,
54
54
  ret = [];
@@ -15,7 +15,8 @@ import {
15
15
  Some,
16
16
  TaskObserver,
17
17
  useContextModel,
18
- uses
18
+ uses,
19
+ type ContextMenuSpec
19
20
  } from '@xh/hoist/core';
20
21
  import {loadingIndicator} from '@xh/hoist/cmp/loadingindicator';
21
22
  import {mask} from '@xh/hoist/cmp/mask';
@@ -27,7 +28,6 @@ import {logWarn} from '@xh/hoist/utils/js';
27
28
  import {splitLayoutProps} from '@xh/hoist/utils/react';
28
29
  import {castArray, omitBy} from 'lodash';
29
30
  import {Children, isValidElement, ReactElement, ReactNode, useLayoutEffect, useRef} from 'react';
30
- import {ContextMenuSpec} from '../contextmenu/ContextMenu';
31
31
  import {modalSupport} from '../modalsupport/ModalSupport';
32
32
  import {panelHeader} from './impl/PanelHeader';
33
33
  import {resizeContainer} from './impl/ResizeContainer';
@@ -4,11 +4,12 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {contextMenu, ContextMenuSpec} from '@xh/hoist/desktop/cmp/contextmenu/ContextMenu';
7
+ import type {ContextMenuSpec} from '@xh/hoist/core';
8
+ import {contextMenu} from '@xh/hoist/desktop/cmp/contextmenu/ContextMenu';
8
9
  import {showContextMenu} from '@xh/hoist/kit/blueprint';
9
10
  import {logError} from '@xh/hoist/utils/js';
10
11
  import {isArray, isEmpty, isFunction, isUndefined} from 'lodash';
11
- import {cloneElement, isValidElement, ReactElement} from 'react';
12
+ import {cloneElement, isValidElement, MouseEvent, ReactElement} from 'react';
12
13
 
13
14
  /**
14
15
  * Hook to add a right-click context menu to a component.
@@ -21,7 +22,7 @@ import {cloneElement, isValidElement, ReactElement} from 'react';
21
22
  export function useContextMenu(child?: ReactElement, spec?: ContextMenuSpec): ReactElement {
22
23
  if (!child || isUndefined(spec)) return child;
23
24
 
24
- const onContextMenu = (e: MouseEvent) => {
25
+ const onContextMenu = (e: MouseEvent | PointerEvent) => {
25
26
  let contextMenuOutput: any = spec;
26
27
 
27
28
  // 0) Skip if already consumed, otherwise consume (adapted from BP `ContextMenuTarget`).
@@ -34,7 +35,7 @@ export function useContextMenu(child?: ReactElement, spec?: ContextMenuSpec): Re
34
35
  }
35
36
  if (isArray(contextMenuOutput)) {
36
37
  contextMenuOutput = !isEmpty(contextMenuOutput)
37
- ? contextMenu({menuItems: contextMenuOutput})
38
+ ? contextMenu({menuItems: contextMenuOutput, context: {contextMenuEvent: e}})
38
39
  : null;
39
40
  }
40
41
  if (contextMenuOutput && !isValidElement(contextMenuOutput)) {
@@ -5,13 +5,13 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {div, hspacer, vbox} from '@xh/hoist/cmp/layout';
8
- import {hoistCmp, HoistModel, useLocalModel, MenuItem, MenuItemLike} from '@xh/hoist/core';
8
+ import {hoistCmp, HoistModel, useLocalModel, MenuItemLike, isMenuItem} from '@xh/hoist/core';
9
9
  import {listItem} from '@xh/hoist/kit/onsen';
10
10
  import {makeObservable, bindable} from '@xh/hoist/mobx';
11
11
  import {filterConsecutiveMenuSeparators, isOmitted} from '@xh/hoist/utils/impl';
12
12
  import classNames from 'classnames';
13
- import {clone, isEmpty, isString} from 'lodash';
14
- import {isValidElement, ReactNode, useEffect} from 'react';
13
+ import {clone, isEmpty} from 'lodash';
14
+ import {ReactNode, useEffect} from 'react';
15
15
 
16
16
  import './Menu.scss';
17
17
 
@@ -107,7 +107,3 @@ class LocalMenuModel extends HoistModel {
107
107
  });
108
108
  }
109
109
  }
110
-
111
- function isMenuItem(item: MenuItemLike): item is MenuItem {
112
- return !isString(item) && !isValidElement(item);
113
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "74.0.0-SNAPSHOT.1749232318711",
3
+ "version": "74.0.0-SNAPSHOT.1749665625714",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",