@dxos/react-ui-menu 0.8.4-main.66e292d → 0.8.4-main.69d29f4
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.
- package/dist/lib/browser/index.mjs +289 -308
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +289 -308
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/builder.d.ts +1 -1
- package/dist/types/src/builder.d.ts.map +1 -1
- package/dist/types/src/components/ActionLabel.d.ts +3 -3
- package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
- package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
- package/dist/types/src/components/MenuContext.d.ts +1 -1
- package/dist/types/src/components/MenuContext.d.ts.map +1 -1
- package/dist/types/src/components/ToolbarMenu.d.ts +3 -2
- package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
- package/dist/types/src/components/ToolbarMenu.stories.d.ts +2 -2
- package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useMenuActions.d.ts +3 -3
- package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/generator.d.ts +12 -10
- package/dist/types/src/testing/generator.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +11 -24
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +12 -5
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +31 -28
- package/src/builder.ts +1 -1
- package/src/components/ActionLabel.tsx +4 -4
- package/src/components/DropdownMenu.tsx +10 -3
- package/src/components/MenuContext.tsx +9 -1
- package/src/components/ToolbarMenu.stories.tsx +16 -8
- package/src/components/ToolbarMenu.tsx +30 -11
- package/src/hooks/useMenuActions.ts +21 -15
- package/src/index.ts +0 -2
- package/src/testing/generator.ts +63 -58
- package/src/translations.ts +1 -1
- package/src/types.ts +12 -26
- package/src/util.ts +26 -13
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-menu",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.69d29f4",
|
|
4
4
|
"description": "Components for facilitating reactive and consistent menus.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
13
|
+
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
11
15
|
"exports": {
|
|
12
16
|
".": {
|
|
@@ -25,39 +29,38 @@
|
|
|
25
29
|
"src"
|
|
26
30
|
],
|
|
27
31
|
"dependencies": {
|
|
28
|
-
"@preact-signals/safe-react": "^0.9.0",
|
|
29
|
-
"@preact/signals-react": "^3.3.0",
|
|
30
32
|
"@radix-ui/react-context": "1.1.1",
|
|
31
33
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/keyboard": "0.8.4-main.
|
|
36
|
-
"@dxos/
|
|
34
|
+
"@dxos/app-graph": "0.8.4-main.69d29f4",
|
|
35
|
+
"@dxos/effect": "0.8.4-main.69d29f4",
|
|
36
|
+
"@dxos/invariant": "0.8.4-main.69d29f4",
|
|
37
|
+
"@dxos/keyboard": "0.8.4-main.69d29f4",
|
|
38
|
+
"@dxos/ui-types": "0.8.4-main.69d29f4",
|
|
39
|
+
"@dxos/react-ui-attention": "0.8.4-main.69d29f4",
|
|
40
|
+
"@dxos/util": "0.8.4-main.69d29f4"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
|
-
"@effect-atom/atom-react": "^0.
|
|
40
|
-
"@effect/platform": "0.
|
|
41
|
-
"@types/react": "~19.2.
|
|
42
|
-
"@types/react-dom": "~19.2.
|
|
43
|
-
"effect": "3.
|
|
44
|
-
"react": "~19.2.
|
|
45
|
-
"react-dom": "~19.2.
|
|
43
|
+
"@effect-atom/atom-react": "^0.4.6",
|
|
44
|
+
"@effect/platform": "0.93.6",
|
|
45
|
+
"@types/react": "~19.2.7",
|
|
46
|
+
"@types/react-dom": "~19.2.3",
|
|
47
|
+
"effect": "3.19.11",
|
|
48
|
+
"react": "~19.2.3",
|
|
49
|
+
"react-dom": "~19.2.3",
|
|
46
50
|
"vite": "7.1.9",
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/storybook-utils": "0.8.4-main.66e292d"
|
|
51
|
+
"@dxos/random": "0.8.4-main.69d29f4",
|
|
52
|
+
"@dxos/react-ui": "0.8.4-main.69d29f4",
|
|
53
|
+
"@dxos/ui-theme": "0.8.4-main.69d29f4",
|
|
54
|
+
"@dxos/storybook-utils": "0.8.4-main.69d29f4"
|
|
52
55
|
},
|
|
53
56
|
"peerDependencies": {
|
|
54
|
-
"@effect-atom/atom-react": "^0.
|
|
55
|
-
"@effect/platform": "
|
|
56
|
-
"effect": "
|
|
57
|
-
"react": "
|
|
58
|
-
"react-dom": "
|
|
59
|
-
"@dxos/
|
|
60
|
-
"@dxos/react-ui
|
|
57
|
+
"@effect-atom/atom-react": "^0.4.6",
|
|
58
|
+
"@effect/platform": "0.93.6",
|
|
59
|
+
"effect": "3.19.11",
|
|
60
|
+
"react": "~19.2.3",
|
|
61
|
+
"react-dom": "~19.2.3",
|
|
62
|
+
"@dxos/ui-theme": "0.8.4-main.69d29f4",
|
|
63
|
+
"@dxos/react-ui": "0.8.4-main.69d29f4"
|
|
61
64
|
},
|
|
62
65
|
"publishConfig": {
|
|
63
66
|
"access": "public"
|
package/src/builder.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { invariant } from '@dxos/invariant';
|
|
6
|
+
import type { MenuActionProperties, MenuItemGroupProperties } from '@dxos/ui-types';
|
|
6
7
|
|
|
7
8
|
import type { ActionGraphProps } from './hooks';
|
|
8
|
-
import type { MenuActionProperties, MenuItemGroupProperties } from './types';
|
|
9
9
|
import { createMenuAction, createMenuItemGroup } from './util';
|
|
10
10
|
|
|
11
11
|
export interface ActionGroupBuilder {
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
7
|
+
import { type Node } from '@dxos/app-graph';
|
|
8
8
|
import { keySymbols } from '@dxos/keyboard';
|
|
9
9
|
import { type TFunction, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
10
|
-
import { descriptionText, mx } from '@dxos/
|
|
10
|
+
import { descriptionText, mx } from '@dxos/ui-theme';
|
|
11
|
+
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
11
12
|
|
|
12
13
|
import { translationKey } from '../translations';
|
|
13
|
-
import { type MenuActionProperties } from '../types';
|
|
14
14
|
import { getShortcut } from '../util';
|
|
15
15
|
|
|
16
|
-
type Action =
|
|
16
|
+
type Action = Node.Action<MenuActionProperties> | Node.ActionGroup<Omit<MenuActionProperties, 'variant'>>;
|
|
17
17
|
|
|
18
18
|
export const ActionLabel = ({ action }: { action: Action }) => {
|
|
19
19
|
const { t } = useTranslation(translationKey);
|
|
@@ -8,6 +8,7 @@ import React, { type MouseEvent, useCallback } from 'react';
|
|
|
8
8
|
import { type DropdownMenuRootProps, Icon, DropdownMenu as NaturalDropdownMenu } from '@dxos/react-ui';
|
|
9
9
|
|
|
10
10
|
import { type MenuAction, type MenuItem, type MenuItemGroup } from '../types';
|
|
11
|
+
import { executeMenuAction } from '../util';
|
|
11
12
|
|
|
12
13
|
import { ActionLabel } from './ActionLabel';
|
|
13
14
|
import { type MenuScopedProps, useMenu, useMenuItems } from './MenuContext';
|
|
@@ -60,17 +61,23 @@ const DropdownMenuRoot = ({
|
|
|
60
61
|
onChange: onOpenChange,
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
const { onAction } = useMenu('DropdownMenuRoot', __menuScope);
|
|
65
|
+
|
|
63
66
|
const handleActionClick = useCallback(
|
|
64
67
|
(action: MenuAction, event: MouseEvent) => {
|
|
65
68
|
if (action.properties?.disabled) {
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
event.stopPropagation();
|
|
69
|
-
// TODO(thure): Why does Dialog
|
|
72
|
+
// TODO(thure): Why does Dialog's modal-ness cause issues if we don't explicitly close the menu here?
|
|
70
73
|
setOptionsMenuOpen(false);
|
|
71
|
-
|
|
74
|
+
if (onAction) {
|
|
75
|
+
onAction(action, { parent: group, caller });
|
|
76
|
+
} else {
|
|
77
|
+
void executeMenuAction(action, { parent: group, caller });
|
|
78
|
+
}
|
|
72
79
|
},
|
|
73
|
-
[group, caller],
|
|
80
|
+
[group, caller, onAction],
|
|
74
81
|
);
|
|
75
82
|
|
|
76
83
|
const items = useMenuItems(group, propsItems);
|
|
@@ -18,6 +18,7 @@ const [MenuContextProvider, useMenu] = createMenuContext<MenuContextValue>(MENU_
|
|
|
18
18
|
export const menuContextDefaults: MenuContextValue = {
|
|
19
19
|
iconSize: 5,
|
|
20
20
|
useGroupItems: () => null,
|
|
21
|
+
onAction: undefined,
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
const useMenuScope = createMenuScope();
|
|
@@ -29,10 +30,17 @@ const MenuProvider = ({
|
|
|
29
30
|
useGroupItems = menuContextDefaults.useGroupItems,
|
|
30
31
|
iconSize = menuContextDefaults.iconSize,
|
|
31
32
|
attendableId,
|
|
33
|
+
onAction,
|
|
32
34
|
}: MenuProviderProps) => {
|
|
33
35
|
const { scope } = useMenuScope(undefined);
|
|
34
36
|
return (
|
|
35
|
-
<MenuContextProvider
|
|
37
|
+
<MenuContextProvider
|
|
38
|
+
useGroupItems={useGroupItems}
|
|
39
|
+
iconSize={iconSize}
|
|
40
|
+
attendableId={attendableId}
|
|
41
|
+
onAction={onAction}
|
|
42
|
+
scope={scope}
|
|
43
|
+
>
|
|
36
44
|
{children}
|
|
37
45
|
</MenuContextProvider>
|
|
38
46
|
);
|
|
@@ -2,25 +2,26 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Atom } from '@effect-atom/atom-react';
|
|
5
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React, {
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
7
8
|
|
|
8
9
|
import { faker } from '@dxos/random';
|
|
9
10
|
import { IconButton } from '@dxos/react-ui';
|
|
10
11
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
12
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
11
13
|
|
|
12
14
|
import { MenuProvider, DropdownMenu as NaturalDropdownMenu, ToolbarMenu } from '../components';
|
|
13
|
-
import { useMenuActions } from '../hooks';
|
|
15
|
+
import { type ActionGraphProps, useMenuActions } from '../hooks';
|
|
14
16
|
import { createActions, createNestedActions, createNestedActionsResolver, useMutateActions } from '../testing';
|
|
15
17
|
import { translations } from '../translations';
|
|
16
|
-
import { type MenuAction } from '../types';
|
|
17
18
|
|
|
18
19
|
faker.seed(1234);
|
|
19
20
|
|
|
20
21
|
const meta = {
|
|
21
22
|
title: 'ui/react-ui-menu/ToolbarMenu',
|
|
22
23
|
component: ToolbarMenu,
|
|
23
|
-
decorators: [withTheme],
|
|
24
|
+
decorators: [withTheme, withRegistry],
|
|
24
25
|
parameters: {
|
|
25
26
|
translations,
|
|
26
27
|
},
|
|
@@ -32,12 +33,18 @@ type Story = StoryObj<typeof meta>;
|
|
|
32
33
|
|
|
33
34
|
export const DropdownMenu: Story = {
|
|
34
35
|
render: () => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const actionsAtom = useMemo(() => {
|
|
37
|
+
const actions = createActions();
|
|
38
|
+
return Atom.make<ActionGraphProps>({
|
|
39
|
+
nodes: actions,
|
|
40
|
+
edges: actions.map((action) => ({ source: 'root', target: action.id })),
|
|
41
|
+
}).pipe(Atom.keepAlive);
|
|
42
|
+
}, []);
|
|
43
|
+
useMutateActions(actionsAtom);
|
|
44
|
+
const menu = useMenuActions(actionsAtom);
|
|
38
45
|
|
|
39
46
|
return (
|
|
40
|
-
<MenuProvider
|
|
47
|
+
<MenuProvider {...menu}>
|
|
41
48
|
<NaturalDropdownMenu.Root>
|
|
42
49
|
<NaturalDropdownMenu.Trigger asChild>
|
|
43
50
|
<IconButton icon='ph--list-checks--regular' label='Options' />
|
|
@@ -62,6 +69,7 @@ export const Toolbar: Story = {
|
|
|
62
69
|
|
|
63
70
|
export const UseMenuActionsToolbar: Story = {
|
|
64
71
|
render: () => {
|
|
72
|
+
useMutateActions(createNestedActions);
|
|
65
73
|
const menu = useMenuActions(createNestedActions);
|
|
66
74
|
|
|
67
75
|
return (
|
|
@@ -6,12 +6,12 @@ import React, { Fragment, useCallback } from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { Icon, Toolbar as NaturalToolbar, type ToolbarRootProps, useTranslation } from '@dxos/react-ui';
|
|
8
8
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
9
|
-
import { mx, textBlockWidth, toolbarInactive, toolbarLayout } from '@dxos/
|
|
9
|
+
import { mx, textBlockWidth, toolbarInactive, toolbarLayout } from '@dxos/ui-theme';
|
|
10
|
+
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
10
11
|
|
|
11
12
|
import { translationKey } from '../translations';
|
|
12
13
|
import {
|
|
13
14
|
type MenuAction,
|
|
14
|
-
type MenuActionProperties,
|
|
15
15
|
type MenuItem,
|
|
16
16
|
type MenuItemGroup,
|
|
17
17
|
type MenuMultipleSelectActionGroup,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
isMenuGroup,
|
|
20
20
|
isSeparator,
|
|
21
21
|
} from '../types';
|
|
22
|
+
import { executeMenuAction } from '../util';
|
|
22
23
|
|
|
23
24
|
import { ActionLabel, actionLabel } from './ActionLabel';
|
|
24
25
|
import { DropdownMenu } from './DropdownMenu';
|
|
@@ -53,7 +54,7 @@ export type ToolbarMenuActionProps = {
|
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
const ActionToolbarItem = ({ action, __menuScope }: MenuScopedProps<{ action: MenuAction }>) => {
|
|
56
|
-
const { iconSize } = useMenu('ActionToolbarItem', __menuScope);
|
|
57
|
+
const { iconSize, onAction } = useMenu('ActionToolbarItem', __menuScope);
|
|
57
58
|
const { t } = useTranslation(translationKey);
|
|
58
59
|
|
|
59
60
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
@@ -62,7 +63,13 @@ const ActionToolbarItem = ({ action, __menuScope }: MenuScopedProps<{ action: Me
|
|
|
62
63
|
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
63
64
|
: { children: <ActionLabel action={action} /> };
|
|
64
65
|
|
|
65
|
-
const handleClick = useCallback(() =>
|
|
66
|
+
const handleClick = useCallback(() => {
|
|
67
|
+
if (onAction) {
|
|
68
|
+
onAction(action, {});
|
|
69
|
+
} else {
|
|
70
|
+
void executeMenuAction(action);
|
|
71
|
+
}
|
|
72
|
+
}, [action, onAction]);
|
|
66
73
|
|
|
67
74
|
return hidden ? null : (
|
|
68
75
|
<Root
|
|
@@ -95,7 +102,13 @@ const DropdownMenuToolbarItem = ({
|
|
|
95
102
|
const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
|
|
96
103
|
const labelAction = (group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group;
|
|
97
104
|
const rootProps = icon
|
|
98
|
-
? {
|
|
105
|
+
? {
|
|
106
|
+
icon,
|
|
107
|
+
size: iconSize,
|
|
108
|
+
iconOnly,
|
|
109
|
+
label: actionLabel(labelAction, t),
|
|
110
|
+
caretDown: true,
|
|
111
|
+
}
|
|
99
112
|
: {
|
|
100
113
|
children: (
|
|
101
114
|
<>
|
|
@@ -115,10 +128,16 @@ const DropdownMenuToolbarItem = ({
|
|
|
115
128
|
};
|
|
116
129
|
|
|
117
130
|
const ToggleGroupItem = ({ group, action, __menuScope }: MenuScopedProps<ToolbarMenuActionProps>) => {
|
|
118
|
-
const { iconSize } = useMenu('ToggleGroupItem', __menuScope);
|
|
131
|
+
const { iconSize, onAction } = useMenu('ToggleGroupItem', __menuScope);
|
|
119
132
|
const { t } = useTranslation(translationKey);
|
|
120
133
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
121
|
-
const handleClick = useCallback(() =>
|
|
134
|
+
const handleClick = useCallback(() => {
|
|
135
|
+
if (onAction) {
|
|
136
|
+
onAction(action, { parent: group });
|
|
137
|
+
} else {
|
|
138
|
+
void executeMenuAction(action, { parent: group });
|
|
139
|
+
}
|
|
140
|
+
}, [action, group, onAction]);
|
|
122
141
|
const Root = icon ? NaturalToolbar.ToggleGroupIconItem : NaturalToolbar.ToggleGroupItem;
|
|
123
142
|
const rootProps = icon
|
|
124
143
|
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
@@ -164,14 +183,14 @@ const ToggleGroupToolbarItem = ({
|
|
|
164
183
|
export const ToolbarMenu = ({
|
|
165
184
|
__menuScope,
|
|
166
185
|
classNames,
|
|
167
|
-
textBlockWidth:
|
|
186
|
+
textBlockWidth: textBlockWidthProp,
|
|
168
187
|
...props
|
|
169
188
|
}: MenuScopedProps<ToolbarMenuProps>) => {
|
|
170
189
|
const items = useMenuItems(undefined, undefined, 'ToolbarMenu', __menuScope);
|
|
171
190
|
const { attendableId } = useMenu('ToolbarMenu', __menuScope);
|
|
172
191
|
const { hasAttention } = useAttention(attendableId);
|
|
173
|
-
const InnerRoot =
|
|
174
|
-
const innerRootProps =
|
|
192
|
+
const InnerRoot = textBlockWidthProp ? 'div' : Fragment;
|
|
193
|
+
const innerRootProps = textBlockWidthProp
|
|
175
194
|
? { role: 'none', className: mx(textBlockWidth, toolbarLayout, 'bs-full') }
|
|
176
195
|
: {};
|
|
177
196
|
|
|
@@ -179,7 +198,7 @@ export const ToolbarMenu = ({
|
|
|
179
198
|
<NaturalToolbar.Root
|
|
180
199
|
{...props}
|
|
181
200
|
classNames={[attendableId && !hasAttention && toolbarInactive, classNames]}
|
|
182
|
-
layoutManaged={
|
|
201
|
+
layoutManaged={textBlockWidthProp}
|
|
183
202
|
>
|
|
184
203
|
<InnerRoot {...innerRootProps}>
|
|
185
204
|
{items?.map((item: MenuItem, i: number) => (
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
-
import { useCallback, useContext,
|
|
6
|
+
import { useCallback, useContext, useRef } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Graph, Node } from '@dxos/app-graph';
|
|
9
9
|
|
|
10
10
|
import { type MenuItem, type MenuItemGroup } from '../types';
|
|
11
11
|
|
|
12
|
-
const edgesArrayToRecord = (edges: Edge[]): Record<string, Edges> => {
|
|
12
|
+
const edgesArrayToRecord = (edges: Graph.Edge[]): Record<string, Graph.Edges> => {
|
|
13
13
|
return Object.fromEntries(
|
|
14
14
|
Object.entries(
|
|
15
15
|
edges.reduce((acc: Record<string, { inbound: string[]; outbound: string[] }>, { source, target }) => {
|
|
@@ -36,8 +36,8 @@ const edgesArrayToRecord = (edges: Edge[]): Record<string, Edges> => {
|
|
|
36
36
|
);
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export type ActionGraphNodes = NodeArg<any>[];
|
|
40
|
-
export type ActionGraphEdges = Edge[];
|
|
39
|
+
export type ActionGraphNodes = Node.NodeArg<any>[];
|
|
40
|
+
export type ActionGraphEdges = Graph.Edge[];
|
|
41
41
|
export type ActionGraphProps = {
|
|
42
42
|
nodes: ActionGraphNodes;
|
|
43
43
|
edges: ActionGraphEdges;
|
|
@@ -51,22 +51,28 @@ export const useMenuActions = (props: Atom.Atom<ActionGraphProps>): MenuActions
|
|
|
51
51
|
const registry = useContext(RegistryContext);
|
|
52
52
|
const menuGraphProps = useAtomValue(props);
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// Create graph once, then update it when props change.
|
|
55
|
+
const graphRef = useRef<Graph.WritableGraph | null>(null);
|
|
56
|
+
if (!graphRef.current) {
|
|
57
|
+
graphRef.current = Graph.make({
|
|
56
58
|
registry,
|
|
57
|
-
nodes: menuGraphProps.nodes as Node[],
|
|
59
|
+
nodes: menuGraphProps.nodes as Node.Node[],
|
|
58
60
|
edges: edgesArrayToRecord(menuGraphProps.edges),
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const graph = graphRef.current;
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
// Update graph nodes synchronously when props change (before render completes).
|
|
66
|
+
// Using a ref to track the previous value to detect changes.
|
|
67
|
+
const prevPropsRef = useRef(menuGraphProps);
|
|
68
|
+
if (prevPropsRef.current !== menuGraphProps) {
|
|
69
|
+
prevPropsRef.current = menuGraphProps;
|
|
70
|
+
graph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
|
|
71
|
+
}
|
|
66
72
|
|
|
67
73
|
const useGroupItems = useCallback(
|
|
68
74
|
(sourceNode?: MenuItemGroup) => {
|
|
69
|
-
const items = useAtomValue(graph.connections(sourceNode?.id ||
|
|
75
|
+
const items = useAtomValue(graph.connections(sourceNode?.id || Node.RootId)) as MenuItem[];
|
|
70
76
|
return items;
|
|
71
77
|
},
|
|
72
78
|
[graph],
|
package/src/index.ts
CHANGED
package/src/testing/generator.ts
CHANGED
|
@@ -2,27 +2,17 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Atom } from '@effect-atom/atom-react';
|
|
6
|
-
import { useEffect } from 'react';
|
|
5
|
+
import { Atom, RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import { useContext, useEffect } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
ACTION_GROUP_TYPE,
|
|
10
|
-
ACTION_TYPE,
|
|
11
|
-
type Action,
|
|
12
|
-
Graph,
|
|
13
|
-
type NodeArg,
|
|
14
|
-
ROOT_ID,
|
|
15
|
-
actionGroupSymbol,
|
|
16
|
-
} from '@dxos/app-graph';
|
|
17
|
-
import { live } from '@dxos/live-object';
|
|
8
|
+
import { Graph, Node } from '@dxos/app-graph';
|
|
18
9
|
import { faker } from '@dxos/random';
|
|
19
|
-
import { type DeepWriteable } from '@dxos/util';
|
|
20
10
|
|
|
21
11
|
import { type ActionGraphProps } from '../hooks/useMenuActions';
|
|
22
|
-
import { type
|
|
12
|
+
import { type MenuItem, type MenuItemGroup } from '../types';
|
|
23
13
|
|
|
24
|
-
export type
|
|
25
|
-
type?: typeof
|
|
14
|
+
export type CreateActionsProps = Partial<{
|
|
15
|
+
type?: typeof Node.ActionType | typeof Node.ActionGroupType;
|
|
26
16
|
callback: () => void;
|
|
27
17
|
count: number;
|
|
28
18
|
}>;
|
|
@@ -46,29 +36,27 @@ const icons = {
|
|
|
46
36
|
],
|
|
47
37
|
};
|
|
48
38
|
|
|
49
|
-
export const createActions = (params?:
|
|
50
|
-
|
|
51
|
-
const { callback = () => console.log('invoke'), count = 12, type = ACTION_TYPE } = params ?? {};
|
|
39
|
+
export const createActions = (params?: CreateActionsProps) => {
|
|
40
|
+
const { callback = () => console.log('invoke'), count = 12, type = Node.ActionType } = params ?? {};
|
|
52
41
|
return faker.helpers.multiple(
|
|
53
|
-
() =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}),
|
|
42
|
+
() => ({
|
|
43
|
+
id: faker.string.uuid(),
|
|
44
|
+
type,
|
|
45
|
+
data: type === Node.ActionGroupType ? Node.actionGroupSymbol : callback,
|
|
46
|
+
properties: {
|
|
47
|
+
label: faker.lorem.words(2),
|
|
48
|
+
icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
|
|
49
|
+
disabled: faker.helpers.arrayElement([true, false]),
|
|
50
|
+
...(type === Node.ActionGroupType && { variant: 'dropdownMenu' }),
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
65
53
|
{ count },
|
|
66
54
|
);
|
|
67
55
|
};
|
|
68
56
|
|
|
69
|
-
|
|
57
|
+
const buildNestedActions = (): ActionGraphProps => {
|
|
70
58
|
const result: ActionGraphProps = { edges: [], nodes: [] };
|
|
71
|
-
const actionGroups = createActions({ type:
|
|
59
|
+
const actionGroups = createActions({ type: Node.ActionGroupType });
|
|
72
60
|
actionGroups.forEach((group) => {
|
|
73
61
|
const actions = createActions();
|
|
74
62
|
result.nodes.push(group, ...actions);
|
|
@@ -78,39 +66,56 @@ export const createNestedActions = Atom.make(() => {
|
|
|
78
66
|
);
|
|
79
67
|
});
|
|
80
68
|
return result;
|
|
81
|
-
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const createNestedActions = Atom.make(buildNestedActions()).pipe(Atom.keepAlive);
|
|
82
72
|
|
|
83
|
-
export const createNestedActionsResolver = (groupParams?:
|
|
84
|
-
const graph =
|
|
85
|
-
const actionGroups = createActions({ type:
|
|
73
|
+
export const createNestedActionsResolver = (groupParams?: CreateActionsProps, params?: CreateActionsProps) => {
|
|
74
|
+
const graph = Graph.make();
|
|
75
|
+
const actionGroups = createActions({ type: Node.ActionGroupType, ...groupParams });
|
|
86
76
|
actionGroups.forEach((group) => {
|
|
87
77
|
const actions = createActions(params);
|
|
88
|
-
graph.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
78
|
+
graph.pipe(
|
|
79
|
+
Graph.addNodes([group as Node.NodeArg<any>, ...(actions as Node.NodeArg<any>[])]),
|
|
80
|
+
Graph.addEdges([
|
|
81
|
+
{ source: 'root', target: group.id },
|
|
82
|
+
...actions.map((action) => ({ source: group.id, target: action.id })),
|
|
83
|
+
]),
|
|
84
|
+
Graph.expand(group.id),
|
|
85
|
+
);
|
|
94
86
|
});
|
|
95
87
|
const resolveGroupItems = (groupNode?: MenuItemGroup) =>
|
|
96
|
-
(
|
|
88
|
+
(Graph.getActions(graph, groupNode?.id ?? Node.RootId) || null) as MenuItem[] | null;
|
|
97
89
|
return { resolveGroupItems };
|
|
98
90
|
};
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
action.properties.icon.endsWith('regular') ? icons.fill : icons.regular,
|
|
106
|
-
);
|
|
107
|
-
action.properties.disabled = !action.properties.disabled;
|
|
108
|
-
cursor = (cursor + actions.length + 1) % actions.length;
|
|
109
|
-
}, 1_000);
|
|
110
|
-
};
|
|
92
|
+
/**
|
|
93
|
+
* Hook to mutate actions in an atom on an interval for testing reactivity.
|
|
94
|
+
*/
|
|
95
|
+
export const useMutateActions = (actionsAtom: Atom.Writable<ActionGraphProps>) => {
|
|
96
|
+
const registry = useContext(RegistryContext);
|
|
111
97
|
|
|
112
|
-
export const useMutateActions = (actions: MenuAction[]) =>
|
|
113
98
|
useEffect(() => {
|
|
114
|
-
|
|
99
|
+
let cursor = 0;
|
|
100
|
+
const interval = setInterval(() => {
|
|
101
|
+
const current = registry.get(actionsAtom);
|
|
102
|
+
const nodes = current.nodes.map((node, index) => {
|
|
103
|
+
if (index !== cursor) {
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...node,
|
|
108
|
+
properties: {
|
|
109
|
+
...node.properties,
|
|
110
|
+
icon: faker.helpers.arrayElement(node.properties?.icon?.endsWith('regular') ? icons.fill : icons.regular),
|
|
111
|
+
disabled: !node.properties?.disabled,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
registry.set(actionsAtom, { ...current, nodes });
|
|
116
|
+
cursor = (cursor + 1) % current.nodes.length;
|
|
117
|
+
}, 1_000);
|
|
118
|
+
|
|
115
119
|
return () => clearInterval(interval);
|
|
116
|
-
}, []);
|
|
120
|
+
}, [actionsAtom, registry]);
|
|
121
|
+
};
|
package/src/translations.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -2,51 +2,37 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { type
|
|
7
|
-
|
|
8
|
-
export type MenuActionProperties = {
|
|
9
|
-
label: Label;
|
|
10
|
-
icon?: string;
|
|
11
|
-
value?: string;
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
hidden?: boolean;
|
|
14
|
-
iconOnly?: boolean;
|
|
15
|
-
testId?: string;
|
|
16
|
-
variant?: 'action' | 'toggle';
|
|
17
|
-
checked?: boolean;
|
|
18
|
-
classNames?: ClassNameValue;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type MenuItemGroupProperties = {
|
|
22
|
-
label: Label;
|
|
23
|
-
icon?: string;
|
|
24
|
-
iconOnly?: boolean;
|
|
25
|
-
};
|
|
5
|
+
import { Node } from '@dxos/app-graph';
|
|
6
|
+
import { type IconButtonProps, type ToolbarSeparatorProps } from '@dxos/react-ui';
|
|
7
|
+
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
26
8
|
|
|
27
|
-
export type MenuAction<P extends {} = {}> = Action<P & MenuActionProperties>;
|
|
9
|
+
export type MenuAction<P extends {} = {}> = Node.Action<P & MenuActionProperties>;
|
|
28
10
|
|
|
29
11
|
export const MenuSeparatorType = '@dxos/react-ui-toolbar/separator' as const;
|
|
30
12
|
|
|
31
|
-
export type MenuSeparator = Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
|
|
13
|
+
export type MenuSeparator = Node.Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
|
|
32
14
|
type: typeof MenuSeparatorType;
|
|
33
15
|
};
|
|
34
16
|
|
|
35
|
-
export const isSeparator = (node: Node): node is MenuSeparator => node.type === MenuSeparatorType;
|
|
17
|
+
export const isSeparator = (node: Node.Node): node is MenuSeparator => node.type === MenuSeparatorType;
|
|
36
18
|
|
|
37
19
|
export type MenuSingleSelectActionGroup = { selectCardinality: 'single'; value: string };
|
|
38
20
|
export type MenuMultipleSelectActionGroup = { selectCardinality: 'multiple'; value: string[] };
|
|
39
21
|
|
|
40
|
-
export type MenuItemGroup<P extends Record<string, any> = Record<string, any>> = ActionGroup<P>;
|
|
22
|
+
export type MenuItemGroup<P extends Record<string, any> = Record<string, any>> = Node.ActionGroup<P>;
|
|
41
23
|
|
|
42
|
-
export const isMenuGroup = (node: Node): node is MenuItemGroup => node.type ===
|
|
24
|
+
export const isMenuGroup = (node: Node.Node): node is MenuItemGroup => node.type === Node.ActionGroupType;
|
|
43
25
|
|
|
44
26
|
export type MenuItem = MenuSeparator | MenuAction | MenuItemGroup;
|
|
45
27
|
|
|
46
28
|
export type MenuItemsResolver = (group?: MenuItemGroup) => MenuItem[] | null;
|
|
47
29
|
|
|
30
|
+
export type ActionExecutor = (action: MenuAction, params: Node.InvokeProps) => void;
|
|
31
|
+
|
|
48
32
|
export type MenuContextValue = {
|
|
49
33
|
useGroupItems: MenuItemsResolver;
|
|
50
34
|
iconSize: IconButtonProps['size'];
|
|
51
35
|
attendableId?: string;
|
|
36
|
+
/** Optional action executor. If provided, will be used instead of default execution. */
|
|
37
|
+
onAction?: ActionExecutor;
|
|
52
38
|
};
|