@dxos/react-ui-menu 0.8.2-main.12df754 → 0.8.2-main.30e4dbb

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.
Files changed (50) hide show
  1. package/dist/lib/browser/index.mjs +38 -38
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +35 -35
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +38 -38
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/ActionLabel.d.ts +8 -4
  11. package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
  12. package/dist/types/src/components/DropdownMenu.d.ts +3 -2
  13. package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
  14. package/dist/types/src/components/MenuContext.d.ts +4 -13
  15. package/dist/types/src/components/MenuContext.d.ts.map +1 -1
  16. package/dist/types/src/{testing → components}/Menus.stories.d.ts +1 -1
  17. package/dist/types/src/components/Menus.stories.d.ts.map +1 -0
  18. package/dist/types/src/components/ToolbarMenu.d.ts +5 -1
  19. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  20. package/dist/types/src/hooks/index.d.ts +2 -0
  21. package/dist/types/src/hooks/index.d.ts.map +1 -0
  22. package/dist/types/src/{hooks.d.ts → hooks/useMenuActions.d.ts} +2 -2
  23. package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -0
  24. package/dist/types/src/index.d.ts +1 -1
  25. package/dist/types/src/index.d.ts.map +1 -1
  26. package/dist/types/src/testing/generator.d.ts +26 -0
  27. package/dist/types/src/testing/generator.d.ts.map +1 -0
  28. package/dist/types/src/testing/index.d.ts +1 -25
  29. package/dist/types/src/testing/index.d.ts.map +1 -1
  30. package/dist/types/src/{defs.d.ts → types.d.ts} +2 -4
  31. package/dist/types/src/types.d.ts.map +1 -0
  32. package/dist/types/src/util.d.ts +2 -2
  33. package/dist/types/src/util.d.ts.map +1 -1
  34. package/dist/types/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +12 -12
  36. package/src/components/ActionLabel.tsx +11 -8
  37. package/src/components/DropdownMenu.tsx +5 -5
  38. package/src/components/MenuContext.tsx +5 -16
  39. package/src/{testing → components}/Menus.stories.tsx +14 -14
  40. package/src/components/ToolbarMenu.tsx +26 -28
  41. package/src/hooks/index.ts +5 -0
  42. package/src/{hooks.ts → hooks/useMenuActions.ts} +1 -1
  43. package/src/index.ts +1 -1
  44. package/src/testing/generator.ts +111 -0
  45. package/src/testing/index.ts +1 -107
  46. package/src/{defs.ts → types.ts} +1 -4
  47. package/src/util.ts +7 -7
  48. package/dist/types/src/defs.d.ts.map +0 -1
  49. package/dist/types/src/hooks.d.ts.map +0 -1
  50. package/dist/types/src/testing/Menus.stories.d.ts.map +0 -1
@@ -4,20 +4,18 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type Action, type ActionGroup } from '@dxos/app-graph';
7
+ import { type Action as GraphAction, type ActionGroup } from '@dxos/app-graph';
8
8
  import { keySymbols } from '@dxos/keyboard';
9
- import { toLocalizedString, useTranslation } from '@dxos/react-ui';
9
+ import { toLocalizedString, useTranslation, type TFunction } from '@dxos/react-ui';
10
10
  import { mx, descriptionText } from '@dxos/react-ui-theme';
11
11
 
12
- import { type MenuActionProperties } from '../defs';
13
12
  import { translationKey } from '../translations';
13
+ import { type MenuActionProperties } from '../types';
14
14
  import { getShortcut } from '../util';
15
15
 
16
- export const ActionLabel = ({
17
- action,
18
- }: {
19
- action: Action<MenuActionProperties> | ActionGroup<Omit<MenuActionProperties, 'variant'>>;
20
- }) => {
16
+ type Action = GraphAction<MenuActionProperties> | ActionGroup<Omit<MenuActionProperties, 'variant'>>;
17
+
18
+ export const ActionLabel = ({ action }: { action: Action }) => {
21
19
  const shortcut = getShortcut(action);
22
20
  const { t } = useTranslation(translationKey);
23
21
 
@@ -28,3 +26,8 @@ export const ActionLabel = ({
28
26
  </>
29
27
  );
30
28
  };
29
+
30
+ export const actionLabel = (action: Action, t: TFunction) => {
31
+ const shortcut = getShortcut(action);
32
+ return `${toLocalizedString(action.properties!.label, t)}${shortcut ? ` (${keySymbols(shortcut).join('')})` : ''}`;
33
+ };
@@ -9,12 +9,13 @@ import { DropdownMenu as NaturalDropdownMenu, Icon, type DropdownMenuRootProps }
9
9
 
10
10
  import { ActionLabel } from './ActionLabel';
11
11
  import { type MenuScopedProps, useMenu, useMenuItems } from './MenuContext';
12
- import { type MenuAction, type MenuItem, type MenuItemGroup } from '../defs';
12
+ import { type MenuAction, type MenuItem, type MenuItemGroup } from '../types';
13
13
 
14
14
  export type DropdownMenuProps = DropdownMenuRootProps & {
15
15
  group?: MenuItemGroup;
16
16
  items?: MenuItem[];
17
17
  suppressNextTooltip?: MutableRefObject<boolean>;
18
+ caller?: string;
18
19
  };
19
20
 
20
21
  const DropdownMenuItem = ({
@@ -49,6 +50,7 @@ const DropdownMenuRoot = ({
49
50
  defaultOpen,
50
51
  onOpenChange,
51
52
  suppressNextTooltip,
53
+ caller,
52
54
  children,
53
55
  __menuScope,
54
56
  ...naturalProps
@@ -59,8 +61,6 @@ const DropdownMenuRoot = ({
59
61
  onChange: onOpenChange,
60
62
  });
61
63
 
62
- const { onAction } = useMenu('DropdownMenuRoot', __menuScope);
63
-
64
64
  const handleActionClick = useCallback(
65
65
  (action: MenuAction, event: MouseEvent) => {
66
66
  if (action.properties?.disabled) {
@@ -72,9 +72,9 @@ const DropdownMenuRoot = ({
72
72
  suppressNextTooltip.current = true;
73
73
  }
74
74
  setOptionsMenuOpen(false);
75
- onAction?.(action);
75
+ void action.data?.({ parent: group, caller });
76
76
  },
77
- [onAction],
77
+ [suppressNextTooltip, group, caller],
78
78
  );
79
79
 
80
80
  const items = useMenuItems(group, propsItems);
@@ -5,15 +5,7 @@
5
5
  import { createContextScope, type Scope } from '@radix-ui/react-context';
6
6
  import React, { type PropsWithChildren, useMemo } from 'react';
7
7
 
8
- import { type Node } from '@dxos/app-graph';
9
-
10
- import {
11
- type MenuActionHandler,
12
- type MenuAction,
13
- type MenuContextValue,
14
- type MenuItem,
15
- type MenuItemGroup,
16
- } from '../defs';
8
+ import { type MenuContextValue, type MenuItem, type MenuItemGroup } from '../types';
17
9
 
18
10
  export type MenuScopedProps<P> = P & { __menuScope?: Scope };
19
11
 
@@ -21,28 +13,25 @@ const MENU_NAME = 'GraphMenu';
21
13
 
22
14
  const [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
23
15
 
24
- const [MenuContextProvider, useMenu] = createMenuContext<MenuContextValue<any>>(MENU_NAME);
16
+ const [MenuContextProvider, useMenu] = createMenuContext<MenuContextValue>(MENU_NAME);
25
17
 
26
- export const menuContextDefaults: MenuContextValue<MenuAction> = {
18
+ export const menuContextDefaults: MenuContextValue = {
27
19
  iconSize: 5,
28
- onAction: () => {},
29
20
  resolveGroupItems: () => null,
30
21
  };
31
22
 
32
23
  const useMenuScope = createMenuScope();
33
24
 
34
- const MenuProvider = <A extends Node = MenuAction>({
25
+ const MenuProvider = ({
35
26
  children,
36
27
  resolveGroupItems = menuContextDefaults.resolveGroupItems,
37
- onAction = menuContextDefaults.onAction as MenuActionHandler<A>,
38
28
  iconSize = menuContextDefaults.iconSize,
39
29
  attendableId,
40
- }: PropsWithChildren<Partial<MenuContextValue<A>>>) => {
30
+ }: PropsWithChildren<Partial<MenuContextValue>>) => {
41
31
  const { scope } = useMenuScope(undefined);
42
32
  return (
43
33
  <MenuContextProvider
44
34
  resolveGroupItems={resolveGroupItems}
45
- onAction={onAction}
46
35
  iconSize={iconSize}
47
36
  attendableId={attendableId}
48
37
  scope={scope}
@@ -3,38 +3,35 @@
3
3
  //
4
4
 
5
5
  import '@dxos-theme';
6
- import React, { useCallback } from 'react';
6
+ import React, { useCallback, useMemo } from 'react';
7
7
 
8
8
  import { faker } from '@dxos/random';
9
9
  import { IconButton } from '@dxos/react-ui';
10
10
  import { withLayout, withSignals, withTheme } from '@dxos/storybook-utils';
11
11
 
12
- import { createActions, createNestedActions, createNestedActionsResolver, useMutateActions } from './index';
13
12
  import { DropdownMenu as NaturalDropdownMenu, ToolbarMenu, MenuProvider } from '../components';
14
- import { type MenuAction, type MenuActionHandler } from '../defs';
15
- import { useMenuActions } from '../hooks';
13
+ import { useMenuActions } from '../hooks/useMenuActions';
14
+ import { createActions, createNestedActions, createNestedActionsResolver, useMutateActions } from '../testing';
16
15
  import translations from '../translations';
16
+ import { type MenuAction } from '../types';
17
17
 
18
18
  faker.seed(1234);
19
19
 
20
20
  export default {
21
21
  title: 'ui/react-ui-menu/Menus',
22
22
  component: ToolbarMenu,
23
- decorators: [withTheme, withLayout({ tooltips: true }), withSignals],
23
+ decorators: [withTheme, withLayout(), withSignals],
24
24
  parameters: { translations },
25
25
  };
26
26
 
27
- const menuActions = createActions() as MenuAction[];
28
- const nestedMenuActions = createNestedActionsResolver();
29
-
30
- const handleAction = (action: MenuAction) => console.log('[on action]', action);
31
-
32
27
  export const DropdownMenu = {
33
28
  render: () => {
29
+ const menuActions = useMemo(() => createActions() as MenuAction[], []);
30
+ const resolveGroupItems = useCallback(() => menuActions, [menuActions]);
34
31
  useMutateActions(menuActions);
35
- const resolveGroupItems = useCallback(() => menuActions, []);
32
+
36
33
  return (
37
- <MenuProvider resolveGroupItems={resolveGroupItems} onAction={handleAction as MenuActionHandler}>
34
+ <MenuProvider resolveGroupItems={resolveGroupItems}>
38
35
  <NaturalDropdownMenu.Root>
39
36
  <NaturalDropdownMenu.Trigger asChild>
40
37
  <IconButton icon='ph--list-checks--regular' size={5} label='Options' />
@@ -47,8 +44,10 @@ export const DropdownMenu = {
47
44
 
48
45
  export const Toolbar = {
49
46
  render: () => {
47
+ const nestedMenuActions = useMemo(() => createNestedActionsResolver(), []);
48
+
50
49
  return (
51
- <MenuProvider onAction={handleAction as MenuActionHandler} {...nestedMenuActions}>
50
+ <MenuProvider {...nestedMenuActions}>
52
51
  <ToolbarMenu />
53
52
  </MenuProvider>
54
53
  );
@@ -58,8 +57,9 @@ export const Toolbar = {
58
57
  export const UseMenuActionsToolbar = {
59
58
  render: () => {
60
59
  const menu = useMenuActions(createNestedActions);
60
+
61
61
  return (
62
- <MenuProvider onAction={handleAction as MenuActionHandler} {...menu}>
62
+ <MenuProvider {...menu}>
63
63
  <ToolbarMenu />
64
64
  </MenuProvider>
65
65
  );
@@ -4,12 +4,13 @@
4
4
 
5
5
  import React, { useCallback, useRef } from 'react';
6
6
 
7
- import { Icon, type IconButtonProps, Toolbar as NaturalToolbar, type ToolbarRootProps } from '@dxos/react-ui';
7
+ import { Icon, Toolbar as NaturalToolbar, type ToolbarRootProps, useTranslation } from '@dxos/react-ui';
8
8
  import { useAttention } from '@dxos/react-ui-attention';
9
9
 
10
- import { ActionLabel } from './ActionLabel';
10
+ import { actionLabel, ActionLabel } from './ActionLabel';
11
11
  import { DropdownMenu } from './DropdownMenu';
12
12
  import { type MenuScopedProps, useMenu, useMenuItems } from './MenuContext';
13
+ import { translationKey } from '../translations';
13
14
  import {
14
15
  type MenuAction,
15
16
  type MenuItem,
@@ -19,7 +20,7 @@ import {
19
20
  type MenuActionProperties,
20
21
  type MenuMultipleSelectActionGroup,
21
22
  type MenuSingleSelectActionGroup,
22
- } from '../defs';
23
+ } from '../types';
23
24
 
24
25
  export type ToolbarMenuDropdownMenuActionGroup = Omit<MenuActionProperties, 'variant' | 'icon'> & {
25
26
  variant: 'dropdownMenu';
@@ -44,15 +45,19 @@ export type ToolbarMenuActionGroupProps = {
44
45
  items?: MenuItem[];
45
46
  };
46
47
 
48
+ export type ToolbarMenuActionProps = {
49
+ group: MenuItemGroup<ToolbarMenuActionGroupProperties>;
50
+ action: MenuAction;
51
+ };
52
+
47
53
  const ActionToolbarItem = ({ action, __menuScope }: MenuScopedProps<{ action: MenuAction }>) => {
48
- const { onAction, iconSize } = useMenu('ActionToolbarItem', __menuScope);
49
- const handleClick = useCallback(() => {
50
- onAction?.(action);
51
- }, [action, onAction]);
54
+ const { iconSize } = useMenu('ActionToolbarItem', __menuScope);
55
+ const { t } = useTranslation(translationKey);
56
+ const handleClick = useCallback(() => action.data?.(), [action]);
52
57
  const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
53
58
  const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
54
59
  const rootProps = icon
55
- ? { icon, size: iconSize, iconOnly, label: <ActionLabel action={action} /> }
60
+ ? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
56
61
  : { children: <ActionLabel action={action} /> };
57
62
  return hidden ? null : (
58
63
  <Root
@@ -61,7 +66,7 @@ const ActionToolbarItem = ({ action, __menuScope }: MenuScopedProps<{ action: Me
61
66
  onClick={handleClick}
62
67
  variant='ghost'
63
68
  {...(testId && { 'data-testid': testId })}
64
- {...(rootProps as IconButtonProps)}
69
+ {...(rootProps as any)}
65
70
  classNames={classNames}
66
71
  />
67
72
  );
@@ -74,6 +79,7 @@ const DropdownMenuToolbarItem = ({
74
79
  }: MenuScopedProps<ToolbarMenuActionGroupProps>) => {
75
80
  const { iconOnly, disabled, testId } = group.properties;
76
81
  const suppressNextTooltip = useRef(false);
82
+ const { t } = useTranslation(translationKey);
77
83
  const { iconSize } = useMenu('DropdownMenuToolbarItem', __menuScope);
78
84
  const items = useMenuItems(group, propsItems, 'DropdownMenuToolbarItem', __menuScope);
79
85
  const activeItem = items?.find((item) => !!(item as MenuAction).properties.checked);
@@ -83,15 +89,13 @@ const DropdownMenuToolbarItem = ({
83
89
  (activeItem as MenuAction)?.properties.icon) ||
84
90
  group.properties.icon;
85
91
  const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
86
- const label = (
87
- <ActionLabel action={(group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group} />
88
- );
92
+ const labelAction = (group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group;
89
93
  const rootProps = icon
90
- ? { icon, size: iconSize, iconOnly, label, caretDown: true, suppressNextTooltip }
94
+ ? { icon, size: iconSize, iconOnly, label: actionLabel(labelAction, t), caretDown: true, suppressNextTooltip }
91
95
  : {
92
96
  children: (
93
97
  <>
94
- {label}
98
+ <ActionLabel action={labelAction} />
95
99
  <Icon size={3} icon='ph--caret-down--bold' classNames='mis-1' />
96
100
  </>
97
101
  ),
@@ -99,26 +103,20 @@ const DropdownMenuToolbarItem = ({
99
103
  return (
100
104
  <DropdownMenu.Root group={group} items={items} suppressNextTooltip={suppressNextTooltip}>
101
105
  <DropdownMenu.Trigger asChild>
102
- <Root
103
- variant='ghost'
104
- disabled={disabled}
105
- {...(rootProps as IconButtonProps)}
106
- {...(testId && { 'data-testid': testId })}
107
- />
106
+ <Root variant='ghost' disabled={disabled} {...(rootProps as any)} {...(testId && { 'data-testid': testId })} />
108
107
  </DropdownMenu.Trigger>
109
108
  </DropdownMenu.Root>
110
109
  );
111
110
  };
112
111
 
113
- const ToggleGroupItem = ({ action, __menuScope }: MenuScopedProps<{ action: MenuAction }>) => {
114
- const { iconSize, onAction } = useMenu('ToggleGroupItem', __menuScope);
112
+ const ToggleGroupItem = ({ group, action, __menuScope }: MenuScopedProps<ToolbarMenuActionProps>) => {
113
+ const { iconSize } = useMenu('ToggleGroupItem', __menuScope);
114
+ const { t } = useTranslation(translationKey);
115
115
  const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
116
- const handleClick = useCallback(() => {
117
- onAction?.(action);
118
- }, [action, onAction]);
116
+ const handleClick = useCallback(() => action.data?.({ parent: group }), [action, group]);
119
117
  const Root = icon ? NaturalToolbar.ToggleGroupIconItem : NaturalToolbar.ToggleGroupItem;
120
118
  const rootProps = icon
121
- ? { icon, size: iconSize, iconOnly, label: <ActionLabel action={action} /> }
119
+ ? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
122
120
  : { children: <ActionLabel action={action} /> };
123
121
  return hidden ? null : (
124
122
  <Root
@@ -128,7 +126,7 @@ const ToggleGroupItem = ({ action, __menuScope }: MenuScopedProps<{ action: Menu
128
126
  onClick={handleClick}
129
127
  variant='ghost'
130
128
  {...(testId && { 'data-testid': testId })}
131
- {...(rootProps as IconButtonProps)}
129
+ {...(rootProps as any)}
132
130
  classNames={classNames}
133
131
  />
134
132
  );
@@ -148,7 +146,7 @@ const ToggleGroupToolbarItem = ({
148
146
  {
149
147
  // TODO(thure): Handle other menu item types.
150
148
  (items as MenuAction[]).map((action) => (
151
- <ToggleGroupItem key={action.id} action={action} />
149
+ <ToggleGroupItem key={action.id} group={group} action={action} />
152
150
  ))
153
151
  }
154
152
  </NaturalToolbar.ToggleGroup>
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './useMenuActions';
@@ -7,7 +7,7 @@ import { useCallback, useState } from 'react';
7
7
 
8
8
  import { Graph, type NodeArg, type Node } from '@dxos/app-graph';
9
9
 
10
- import { type MenuItem, type MenuItemGroup } from './defs';
10
+ import { type MenuItem, type MenuItemGroup } from '../types';
11
11
 
12
12
  type Edge = { source: string; target: string };
13
13
 
package/src/index.ts CHANGED
@@ -3,6 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './components';
6
- export * from './defs';
7
6
  export * from './hooks';
7
+ export * from './types';
8
8
  export { createMenuAction, createMenuItemGroup, createLineSeparator, createGapSeparator } from './util';
@@ -0,0 +1,111 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useEffect } from 'react';
6
+
7
+ import { type Action, Graph, type NodeArg, ACTION_TYPE, ACTION_GROUP_TYPE, actionGroupSymbol } from '@dxos/app-graph';
8
+ import { live } from '@dxos/live-object';
9
+ import { faker } from '@dxos/random';
10
+ import { type DeepWriteable } from '@dxos/util';
11
+
12
+ import { type ActionGraphProps } from '../hooks/useMenuActions';
13
+ import { type MenuAction, type MenuItemGroup, type MenuItem } from '../types';
14
+
15
+ export type CreateActionsParams = Partial<{
16
+ type?: typeof ACTION_TYPE | typeof ACTION_GROUP_TYPE;
17
+ callback: () => void;
18
+ count: number;
19
+ }>;
20
+
21
+ const icons = {
22
+ regular: [
23
+ 'ph--text-b--regular',
24
+ 'ph--text-italic--regular',
25
+ 'ph--text-h-five--regular',
26
+ 'ph--chat-text--regular',
27
+ 'ph--clipboard-text--regular',
28
+ 'ph--link-simple--regular',
29
+ ],
30
+ fill: [
31
+ 'ph--text-b--fill',
32
+ 'ph--text-italic--fill',
33
+ 'ph--text-h-five--fill',
34
+ 'ph--chat-text--fill',
35
+ 'ph--clipboard-text--fill',
36
+ 'ph--link-simple--fill',
37
+ ],
38
+ };
39
+
40
+ export const createActions = (params?: CreateActionsParams) => {
41
+ // eslint-disable-next-line no-console
42
+ const { callback = () => console.log('invoke'), count = 12, type = ACTION_TYPE } = params ?? {};
43
+ return faker.helpers.multiple(
44
+ () =>
45
+ live({
46
+ id: faker.string.uuid(),
47
+ type,
48
+ data: type === ACTION_GROUP_TYPE ? actionGroupSymbol : callback,
49
+ properties: {
50
+ label: faker.lorem.words(2),
51
+ icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
52
+ disabled: faker.helpers.arrayElement([true, false]),
53
+ ...(type === ACTION_GROUP_TYPE && { variant: 'dropdownMenu' }),
54
+ },
55
+ }),
56
+ { count },
57
+ );
58
+ };
59
+
60
+ export const createNestedActions = () => {
61
+ const result: ActionGraphProps = { edges: [], nodes: [] };
62
+ const actionGroups = createActions({ type: ACTION_GROUP_TYPE });
63
+ actionGroups.forEach((group) => {
64
+ const actions = createActions();
65
+ result.nodes.push(group, ...actions);
66
+ result.edges.push(
67
+ { source: 'root', target: group.id },
68
+ ...actions.map((action) => ({ source: group.id, target: action.id })),
69
+ );
70
+ });
71
+ return result;
72
+ };
73
+
74
+ export const createNestedActionsResolver = (groupParams?: CreateActionsParams, params?: CreateActionsParams) => {
75
+ const graph = new Graph();
76
+ const actionGroups = createActions({ type: ACTION_GROUP_TYPE, ...groupParams });
77
+ actionGroups.forEach((group) => {
78
+ const actions = createActions(params);
79
+ // TODO(thure): these methods exist on `graph` but are marked as `@internal`; how should consumers properly build
80
+ // graph “islands” for the purposes of these components?
81
+ // @ts-ignore
82
+ graph._addNodes([group as NodeArg<any>, ...(actions as NodeArg<any>[])]);
83
+ // @ts-ignore
84
+ graph._addEdges([
85
+ { source: 'root', target: group.id },
86
+ ...actions.map((action) => ({ source: group.id, target: action.id })),
87
+ ]);
88
+ void graph.expand(group);
89
+ });
90
+ const resolveGroupItems = (groupNode?: MenuItemGroup) =>
91
+ (graph.actions(groupNode ?? graph.root) || null) as MenuItem[] | null;
92
+ return { resolveGroupItems };
93
+ };
94
+
95
+ export const mutateActionsOnInterval = (actions: Action[]) => {
96
+ let cursor = 0;
97
+ return setInterval(() => {
98
+ const action = actions[cursor] as DeepWriteable<Action>;
99
+ action.properties.icon = faker.helpers.arrayElement(
100
+ action.properties.icon.endsWith('regular') ? icons.fill : icons.regular,
101
+ );
102
+ action.properties.disabled = !action.properties.disabled;
103
+ cursor = (cursor + actions.length + 1) % actions.length;
104
+ }, 1_000);
105
+ };
106
+
107
+ export const useMutateActions = (actions: MenuAction[]) =>
108
+ useEffect(() => {
109
+ const interval = mutateActionsOnInterval(actions);
110
+ return () => clearInterval(interval);
111
+ }, []);
@@ -2,110 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useEffect } from 'react';
6
-
7
- import { type Action, Graph, type NodeArg, ACTION_TYPE, ACTION_GROUP_TYPE, actionGroupSymbol } from '@dxos/app-graph';
8
- import { live } from '@dxos/live-object';
9
- import { faker } from '@dxos/random';
10
- import { type DeepWriteable } from '@dxos/util';
11
-
12
- import { type MenuAction, type MenuItemGroup, type MenuItem } from '../defs';
13
- import { type ActionGraphProps } from '../hooks';
14
-
15
- export type CreateActionsParams = Partial<{
16
- type?: typeof ACTION_TYPE | typeof ACTION_GROUP_TYPE;
17
- callback: () => void;
18
- count: number;
19
- }>;
20
-
21
- const icons = {
22
- regular: [
23
- 'ph--text-b--regular',
24
- 'ph--text-italic--regular',
25
- 'ph--text-h-five--regular',
26
- 'ph--chat-text--regular',
27
- 'ph--clipboard-text--regular',
28
- 'ph--link-simple--regular',
29
- ],
30
- fill: [
31
- 'ph--text-b--fill',
32
- 'ph--text-italic--fill',
33
- 'ph--text-h-five--fill',
34
- 'ph--chat-text--fill',
35
- 'ph--clipboard-text--fill',
36
- 'ph--link-simple--fill',
37
- ],
38
- };
39
-
40
- export const createActions = (params?: CreateActionsParams) => {
41
- // eslint-disable-next-line no-console
42
- const { callback = () => console.log('invoke'), count = 12, type = ACTION_TYPE } = params ?? {};
43
- return faker.helpers.multiple(
44
- () =>
45
- live({
46
- id: faker.string.uuid(),
47
- type,
48
- data: type === ACTION_GROUP_TYPE ? actionGroupSymbol : callback,
49
- properties: {
50
- label: faker.lorem.words(2),
51
- icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
52
- disabled: faker.helpers.arrayElement([true, false]),
53
- ...(type === ACTION_GROUP_TYPE && { variant: 'dropdownMenu' }),
54
- },
55
- }),
56
- { count },
57
- );
58
- };
59
-
60
- export const createNestedActions = () => {
61
- const result: ActionGraphProps = { edges: [], nodes: [] };
62
- const actionGroups = createActions({ type: ACTION_GROUP_TYPE });
63
- actionGroups.forEach((group) => {
64
- const actions = createActions();
65
- result.nodes.push(group, ...actions);
66
- result.edges.push(
67
- { source: 'root', target: group.id },
68
- ...actions.map((action) => ({ source: group.id, target: action.id })),
69
- );
70
- });
71
- return result;
72
- };
73
-
74
- export const createNestedActionsResolver = (groupParams?: CreateActionsParams, params?: CreateActionsParams) => {
75
- const graph = new Graph();
76
- const actionGroups = createActions({ type: ACTION_GROUP_TYPE, ...groupParams });
77
- actionGroups.forEach((group) => {
78
- const actions = createActions(params);
79
- // TODO(thure): these methods exist on `graph` but are marked as `@internal`; how should consumers properly build
80
- // graph “islands” for the purposes of these components?
81
- // @ts-ignore
82
- graph._addNodes([group as NodeArg<any>, ...(actions as NodeArg<any>[])]);
83
- // @ts-ignore
84
- graph._addEdges([
85
- { source: 'root', target: group.id },
86
- ...actions.map((action) => ({ source: group.id, target: action.id })),
87
- ]);
88
- void graph.expand(group);
89
- });
90
- const resolveGroupItems = (groupNode?: MenuItemGroup) =>
91
- (graph.actions(groupNode ?? graph.root) || null) as MenuItem[] | null;
92
- return { resolveGroupItems };
93
- };
94
-
95
- export const mutateActionsOnInterval = (actions: Action[]) => {
96
- let cursor = 0;
97
- return setInterval(() => {
98
- const action = actions[cursor] as DeepWriteable<Action>;
99
- action.properties.icon = faker.helpers.arrayElement(
100
- action.properties.icon.endsWith('regular') ? icons.fill : icons.regular,
101
- );
102
- action.properties.disabled = !action.properties.disabled;
103
- cursor = (cursor + actions.length + 1) % actions.length;
104
- }, 1_000);
105
- };
106
-
107
- export const useMutateActions = (actions: MenuAction[]) =>
108
- useEffect(() => {
109
- const interval = mutateActionsOnInterval(actions);
110
- return () => clearInterval(interval);
111
- }, []);
5
+ export * from './generator';
@@ -26,8 +26,6 @@ export type MenuItemGroupProperties = {
26
26
 
27
27
  export type MenuAction<P extends {} = {}> = Action<P & MenuActionProperties>;
28
28
 
29
- export type MenuActionHandler<A extends Node = MenuAction> = (action: A) => void;
30
-
31
29
  export const MenuSeparatorType = '@dxos/react-ui-toolbar/separator' as const;
32
30
 
33
31
  export type MenuSeparator = Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
@@ -47,9 +45,8 @@ export type MenuItem = MenuSeparator | MenuAction | MenuItemGroup;
47
45
 
48
46
  export type MenuItemsResolver = (group?: MenuItemGroup) => MenuItem[] | null;
49
47
 
50
- export type MenuContextValue<A extends Node = MenuAction> = {
48
+ export type MenuContextValue = {
51
49
  resolveGroupItems: MenuItemsResolver;
52
50
  iconSize: IconButtonProps['size'];
53
- onAction: MenuActionHandler<A>;
54
51
  attendableId?: string;
55
52
  };
package/src/util.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  type MenuItemGroup,
12
12
  type MenuItemGroupProperties,
13
13
  type MenuSeparator,
14
- } from './defs';
14
+ } from './types';
15
15
 
16
16
  export const getShortcut = (action: ActionLike) => {
17
17
  return typeof action.properties?.keyBinding === 'string'
@@ -21,16 +21,16 @@ export const getShortcut = (action: ActionLike) => {
21
21
 
22
22
  export const fallbackIcon = 'ph--placeholder--regular';
23
23
 
24
- // TODO(thure,wittjosiah): This is probably a smell; consider the whether react-ui-menu should use action nodes if the
25
- // callback in `data` is never used. See https://github.com/dxos/dxos/pull/8506#discussion_r1925855118
26
- const noop = () => {};
27
-
28
- export const createMenuAction = <P extends {} = {}>(id: string, properties: P & MenuActionProperties) =>
24
+ export const createMenuAction = <P extends {} = {}>(
25
+ id: string,
26
+ invoke: () => void,
27
+ properties: P & MenuActionProperties,
28
+ ) =>
29
29
  ({
30
30
  id,
31
31
  type: ACTION_TYPE,
32
32
  properties,
33
- data: noop,
33
+ data: invoke,
34
34
  }) satisfies MenuAction;
35
35
 
36
36
  export const createMenuItemGroup = <
@@ -1 +0,0 @@
1
- {"version":3,"file":"defs.d.ts","sourceRoot":"","sources":["../../../src/defs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,KAAK,WAAW,EAAqB,MAAM,iBAAiB,CAAC;AAC9F,OAAO,EAAE,KAAK,qBAAqB,EAAE,KAAK,KAAK,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEnH,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,MAAM,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC;AAE7E,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,IAAI,GAAG,UAAU,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;AAEjF,eAAO,MAAM,iBAAiB,EAAG,kCAA2C,CAAC;AAE7E,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,CAAC,GAAG;IAChF,IAAI,EAAE,OAAO,iBAAiB,CAAC;CAChC,CAAC;AAEF,eAAO,MAAM,WAAW,SAAU,IAAI,KAAG,IAAI,IAAI,aAAgD,CAAC;AAElG,MAAM,MAAM,2BAA2B,GAAG;IAAE,iBAAiB,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACzF,MAAM,MAAM,6BAA6B,GAAG;IAAE,iBAAiB,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE/F,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;AAEhG,eAAO,MAAM,WAAW,SAAU,IAAI,KAAG,IAAI,IAAI,aAAgD,CAAC;AAElG,MAAM,MAAM,QAAQ,GAAG,aAAa,GAAG,UAAU,GAAG,aAAa,CAAC;AAElE,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,CAAC,EAAE,aAAa,KAAK,QAAQ,EAAE,GAAG,IAAI,CAAC;AAE7E,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,IAAI,GAAG,UAAU,IAAI;IAC1D,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,QAAQ,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;IAClC,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../../src/hooks.ts"],"names":[],"mappings":"AAOA,OAAO,EAAS,KAAK,OAAO,EAAa,MAAM,iBAAiB,CAAC;AAEjE,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,QAAQ,CAAC;AAE3D,KAAK,IAAI,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA6B/C,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;AAC9C,MAAM,MAAM,gBAAgB,GAAG,IAAI,EAAE,CAAC;AACtC,MAAM,MAAM,gBAAgB,GAAG;IAAE,KAAK,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAEpF,eAAO,MAAM,cAAc,kBAAmB,MAAM,gBAAgB;qCAgBnD,aAAa;CAW7B,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Menus.stories.d.ts","sourceRoot":"","sources":["../../../../src/testing/Menus.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AACrB,OAAO,KAAsB,MAAM,OAAO,CAAC;;;;;;;;;;;;;AAc3C,wBAKE;AAOF,eAAO,MAAM,YAAY;;CAcxB,CAAC;AAEF,eAAO,MAAM,OAAO;;CAQnB,CAAC;AAEF,eAAO,MAAM,qBAAqB;;CASjC,CAAC"}