@dxos/react-ui-menu 0.8.4-main.84f28bd → 0.8.4-main.9735255
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 +309 -313
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +309 -313
- 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 +3 -3
- 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 +1 -1
- package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
- package/dist/types/src/components/MenuContext.d.ts +3 -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 +20 -0
- package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/useMenuActions.d.ts +6 -5
- 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 +13 -11
- 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 +33 -29
- package/src/builder.ts +11 -11
- package/src/components/ActionLabel.tsx +5 -5
- package/src/components/DropdownMenu.tsx +16 -6
- package/src/components/MenuContext.tsx +16 -4
- package/src/components/ToolbarMenu.stories.tsx +81 -0
- package/src/components/ToolbarMenu.tsx +68 -34
- package/src/hooks/useMenuActions.ts +32 -18
- 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 +32 -14
- package/dist/types/src/components/Menus.stories.d.ts +0 -25
- package/dist/types/src/components/Menus.stories.d.ts.map +0 -1
- package/src/components/Menus.stories.tsx +0 -69
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-menu",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.9735255",
|
|
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
|
".": {
|
|
17
|
+
"source": "./src/index.ts",
|
|
13
18
|
"types": "./dist/types/src/index.d.ts",
|
|
14
19
|
"browser": "./dist/lib/browser/index.mjs",
|
|
15
20
|
"node": "./dist/lib/node-esm/index.mjs"
|
|
@@ -24,39 +29,38 @@
|
|
|
24
29
|
"src"
|
|
25
30
|
],
|
|
26
31
|
"dependencies": {
|
|
27
|
-
"@preact-signals/safe-react": "^0.9.0",
|
|
28
|
-
"@preact/signals-react": "^3.2.0",
|
|
29
32
|
"@radix-ui/react-context": "1.1.1",
|
|
30
33
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
31
|
-
"@dxos/
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/keyboard": "0.8.4-main.
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
34
|
+
"@dxos/effect": "0.8.4-main.9735255",
|
|
35
|
+
"@dxos/app-graph": "0.8.4-main.9735255",
|
|
36
|
+
"@dxos/keyboard": "0.8.4-main.9735255",
|
|
37
|
+
"@dxos/invariant": "0.8.4-main.9735255",
|
|
38
|
+
"@dxos/react-ui-attention": "0.8.4-main.9735255",
|
|
39
|
+
"@dxos/ui-types": "0.8.4-main.9735255",
|
|
40
|
+
"@dxos/util": "0.8.4-main.9735255"
|
|
36
41
|
},
|
|
37
42
|
"devDependencies": {
|
|
38
|
-
"@effect-
|
|
39
|
-
"@effect/platform": "0.
|
|
40
|
-
"@types/react": "~
|
|
41
|
-
"@types/react-dom": "~
|
|
42
|
-
"effect": "3.
|
|
43
|
-
"react": "~
|
|
44
|
-
"react-dom": "~
|
|
45
|
-
"vite": "
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/storybook-utils": "0.8.4-main.84f28bd"
|
|
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",
|
|
50
|
+
"vite": "7.1.9",
|
|
51
|
+
"@dxos/random": "0.8.4-main.9735255",
|
|
52
|
+
"@dxos/react-ui": "0.8.4-main.9735255",
|
|
53
|
+
"@dxos/storybook-utils": "0.8.4-main.9735255",
|
|
54
|
+
"@dxos/ui-theme": "0.8.4-main.9735255"
|
|
51
55
|
},
|
|
52
56
|
"peerDependencies": {
|
|
53
|
-
"@effect-
|
|
54
|
-
"@effect/platform": "
|
|
55
|
-
"effect": "
|
|
56
|
-
"react": "~
|
|
57
|
-
"react-dom": "~
|
|
58
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
59
|
-
"@dxos/
|
|
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/react-ui": "0.8.4-main.9735255",
|
|
63
|
+
"@dxos/ui-theme": "0.8.4-main.9735255"
|
|
60
64
|
},
|
|
61
65
|
"publishConfig": {
|
|
62
66
|
"access": "public"
|
package/src/builder.ts
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
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 {
|
|
12
|
-
action<P extends {} = {}>(id: string,
|
|
12
|
+
action<P extends {} = {}>(id: string, props: P & MenuActionProperties, invoke: () => void): this;
|
|
13
13
|
group<P extends {} = {}>(
|
|
14
14
|
id: string,
|
|
15
|
-
|
|
15
|
+
props: P & MenuItemGroupProperties,
|
|
16
16
|
cb: (builder: ActionGroupBuilder) => void,
|
|
17
17
|
): this;
|
|
18
18
|
}
|
|
@@ -32,12 +32,6 @@ class MenuBuilderImpl implements MenuBuilder {
|
|
|
32
32
|
private readonly _rootId: string,
|
|
33
33
|
) {}
|
|
34
34
|
|
|
35
|
-
action<P extends {} = {}>(id: string, invoke: () => void, properties: P & MenuActionProperties): this {
|
|
36
|
-
this._data.nodes.push(createMenuAction(id, invoke, properties));
|
|
37
|
-
this._data.edges.push({ source: this._rootId, target: id });
|
|
38
|
-
return this;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
35
|
root(props: MenuItemGroupProperties): this {
|
|
42
36
|
invariant(this._rootId === 'root', 'Root group can only be at the top level');
|
|
43
37
|
invariant(this._data.nodes.find((node) => node.id === 'root') === undefined, 'Root group can only be created once');
|
|
@@ -45,12 +39,18 @@ class MenuBuilderImpl implements MenuBuilder {
|
|
|
45
39
|
return this;
|
|
46
40
|
}
|
|
47
41
|
|
|
42
|
+
action<P extends {} = {}>(id: string, props: P & MenuActionProperties, invoke: () => void): this {
|
|
43
|
+
this._data.nodes.push(createMenuAction(id, invoke, props));
|
|
44
|
+
this._data.edges.push({ source: this._rootId, target: id });
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
48
|
group<P extends {} = {}>(
|
|
49
49
|
id: string,
|
|
50
|
-
|
|
50
|
+
props: P & MenuItemGroupProperties,
|
|
51
51
|
cb: (builder: ActionGroupBuilder) => void,
|
|
52
52
|
): this {
|
|
53
|
-
this._data.nodes.push(createMenuItemGroup(id,
|
|
53
|
+
this._data.nodes.push(createMenuItemGroup(id, props));
|
|
54
54
|
this._data.edges.push({ source: this._rootId, target: id });
|
|
55
55
|
cb(new MenuBuilderImpl(this._data, id));
|
|
56
56
|
return this;
|
|
@@ -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
|
-
import { toLocalizedString, useTranslation
|
|
10
|
-
import {
|
|
9
|
+
import { type TFunction, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
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);
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
6
6
|
import React, { type MouseEvent, useCallback } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { type DropdownMenuRootProps, Icon, DropdownMenu as NaturalDropdownMenu } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { type MenuAction, type MenuItem, type MenuItemGroup } from '../types';
|
|
11
|
+
import { executeMenuAction } from '../util';
|
|
9
12
|
|
|
10
13
|
import { ActionLabel } from './ActionLabel';
|
|
11
14
|
import { type MenuScopedProps, useMenu, useMenuItems } from './MenuContext';
|
|
12
|
-
import { type MenuAction, type MenuItem, type MenuItemGroup } from '../types';
|
|
13
15
|
|
|
14
16
|
export type DropdownMenuProps = DropdownMenuRootProps & {
|
|
15
17
|
group?: MenuItemGroup;
|
|
@@ -59,17 +61,23 @@ const DropdownMenuRoot = ({
|
|
|
59
61
|
onChange: onOpenChange,
|
|
60
62
|
});
|
|
61
63
|
|
|
64
|
+
const { onAction } = useMenu('DropdownMenuRoot', __menuScope);
|
|
65
|
+
|
|
62
66
|
const handleActionClick = useCallback(
|
|
63
67
|
(action: MenuAction, event: MouseEvent) => {
|
|
64
68
|
if (action.properties?.disabled) {
|
|
65
69
|
return;
|
|
66
70
|
}
|
|
67
71
|
event.stopPropagation();
|
|
68
|
-
// 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?
|
|
69
73
|
setOptionsMenuOpen(false);
|
|
70
|
-
|
|
74
|
+
if (onAction) {
|
|
75
|
+
onAction(action, { parent: group, caller });
|
|
76
|
+
} else {
|
|
77
|
+
void executeMenuAction(action, { parent: group, caller });
|
|
78
|
+
}
|
|
71
79
|
},
|
|
72
|
-
[group, caller],
|
|
80
|
+
[group, caller, onAction],
|
|
73
81
|
);
|
|
74
82
|
|
|
75
83
|
const items = useMenuItems(group, propsItems);
|
|
@@ -80,7 +88,9 @@ const DropdownMenuRoot = ({
|
|
|
80
88
|
<NaturalDropdownMenu.Portal>
|
|
81
89
|
<NaturalDropdownMenu.Content>
|
|
82
90
|
<NaturalDropdownMenu.Viewport>
|
|
83
|
-
{items?.map((item) =>
|
|
91
|
+
{items?.map((item) => (
|
|
92
|
+
<DropdownMenuItem key={item.id} item={item} onClick={handleActionClick} />
|
|
93
|
+
))}
|
|
84
94
|
</NaturalDropdownMenu.Viewport>
|
|
85
95
|
<NaturalDropdownMenu.Arrow />
|
|
86
96
|
</NaturalDropdownMenu.Content>
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type Scope, createContextScope } from '@radix-ui/react-context';
|
|
6
6
|
import React, { type PropsWithChildren, useMemo } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type MenuContextValue, type MenuItem, type MenuItemGroup } from '../types';
|
|
9
9
|
|
|
10
10
|
export type MenuScopedProps<P> = P & { __menuScope?: Scope };
|
|
11
11
|
|
|
12
|
-
const MENU_NAME = '
|
|
12
|
+
const MENU_NAME = 'Menu';
|
|
13
13
|
|
|
14
14
|
const [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
|
|
15
15
|
|
|
@@ -18,19 +18,29 @@ 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();
|
|
24
25
|
|
|
26
|
+
type MenuProviderProps = PropsWithChildren<Partial<MenuContextValue>>;
|
|
27
|
+
|
|
25
28
|
const MenuProvider = ({
|
|
26
29
|
children,
|
|
27
30
|
useGroupItems = menuContextDefaults.useGroupItems,
|
|
28
31
|
iconSize = menuContextDefaults.iconSize,
|
|
29
32
|
attendableId,
|
|
30
|
-
|
|
33
|
+
onAction,
|
|
34
|
+
}: MenuProviderProps) => {
|
|
31
35
|
const { scope } = useMenuScope(undefined);
|
|
32
36
|
return (
|
|
33
|
-
<MenuContextProvider
|
|
37
|
+
<MenuContextProvider
|
|
38
|
+
useGroupItems={useGroupItems}
|
|
39
|
+
iconSize={iconSize}
|
|
40
|
+
attendableId={attendableId}
|
|
41
|
+
onAction={onAction}
|
|
42
|
+
scope={scope}
|
|
43
|
+
>
|
|
34
44
|
{children}
|
|
35
45
|
</MenuContextProvider>
|
|
36
46
|
);
|
|
@@ -48,3 +58,5 @@ export const useMenuItems = (
|
|
|
48
58
|
};
|
|
49
59
|
|
|
50
60
|
export { useMenu, createMenuScope, MenuProvider };
|
|
61
|
+
|
|
62
|
+
export type { MenuProviderProps };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Atom } from '@effect-atom/atom-react';
|
|
6
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import { faker } from '@dxos/random';
|
|
10
|
+
import { IconButton } from '@dxos/react-ui';
|
|
11
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
12
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
13
|
+
|
|
14
|
+
import { MenuProvider, DropdownMenu as NaturalDropdownMenu, ToolbarMenu } from '../components';
|
|
15
|
+
import { type ActionGraphProps, useMenuActions } from '../hooks';
|
|
16
|
+
import { createActions, createNestedActions, createNestedActionsResolver, useMutateActions } from '../testing';
|
|
17
|
+
import { translations } from '../translations';
|
|
18
|
+
|
|
19
|
+
faker.seed(1234);
|
|
20
|
+
|
|
21
|
+
const meta = {
|
|
22
|
+
title: 'ui/react-ui-menu/ToolbarMenu',
|
|
23
|
+
component: ToolbarMenu,
|
|
24
|
+
decorators: [withTheme, withRegistry],
|
|
25
|
+
parameters: {
|
|
26
|
+
translations,
|
|
27
|
+
},
|
|
28
|
+
} satisfies Meta<typeof ToolbarMenu>;
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
|
|
32
|
+
type Story = StoryObj<typeof meta>;
|
|
33
|
+
|
|
34
|
+
export const DropdownMenu: Story = {
|
|
35
|
+
render: () => {
|
|
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);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<MenuProvider {...menu}>
|
|
48
|
+
<NaturalDropdownMenu.Root>
|
|
49
|
+
<NaturalDropdownMenu.Trigger asChild>
|
|
50
|
+
<IconButton icon='ph--list-checks--regular' label='Options' />
|
|
51
|
+
</NaturalDropdownMenu.Trigger>
|
|
52
|
+
</NaturalDropdownMenu.Root>
|
|
53
|
+
</MenuProvider>
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const Toolbar: Story = {
|
|
59
|
+
render: () => {
|
|
60
|
+
const nestedMenuActions = useMemo(() => createNestedActionsResolver(), []);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<MenuProvider {...nestedMenuActions}>
|
|
64
|
+
<ToolbarMenu />
|
|
65
|
+
</MenuProvider>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const UseMenuActionsToolbar: Story = {
|
|
71
|
+
render: () => {
|
|
72
|
+
useMutateActions(createNestedActions);
|
|
73
|
+
const menu = useMenuActions(createNestedActions);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<MenuProvider {...menu}>
|
|
77
|
+
<ToolbarMenu />
|
|
78
|
+
</MenuProvider>
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -6,22 +6,24 @@ 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, toolbarLayout } from '@dxos/
|
|
9
|
+
import { mx, textBlockWidth, toolbarInactive, toolbarLayout } from '@dxos/ui-theme';
|
|
10
|
+
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
10
11
|
|
|
11
|
-
import { actionLabel, ActionLabel } from './ActionLabel';
|
|
12
|
-
import { DropdownMenu } from './DropdownMenu';
|
|
13
|
-
import { type MenuScopedProps, useMenu, useMenuItems } from './MenuContext';
|
|
14
12
|
import { translationKey } from '../translations';
|
|
15
13
|
import {
|
|
16
14
|
type MenuAction,
|
|
17
15
|
type MenuItem,
|
|
18
|
-
isMenuGroup,
|
|
19
|
-
isSeparator,
|
|
20
16
|
type MenuItemGroup,
|
|
21
|
-
type MenuActionProperties,
|
|
22
17
|
type MenuMultipleSelectActionGroup,
|
|
23
18
|
type MenuSingleSelectActionGroup,
|
|
19
|
+
isMenuGroup,
|
|
20
|
+
isSeparator,
|
|
24
21
|
} from '../types';
|
|
22
|
+
import { executeMenuAction } from '../util';
|
|
23
|
+
|
|
24
|
+
import { ActionLabel, actionLabel } from './ActionLabel';
|
|
25
|
+
import { DropdownMenu } from './DropdownMenu';
|
|
26
|
+
import { type MenuScopedProps, useMenu, useMenuItems } from './MenuContext';
|
|
25
27
|
|
|
26
28
|
export type ToolbarMenuDropdownMenuActionGroup = Omit<MenuActionProperties, 'variant' | 'icon'> & {
|
|
27
29
|
variant: 'dropdownMenu';
|
|
@@ -52,14 +54,23 @@ export type ToolbarMenuActionProps = {
|
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
const ActionToolbarItem = ({ action, __menuScope }: MenuScopedProps<{ action: MenuAction }>) => {
|
|
55
|
-
const { iconSize } = useMenu('ActionToolbarItem', __menuScope);
|
|
57
|
+
const { iconSize, onAction } = useMenu('ActionToolbarItem', __menuScope);
|
|
56
58
|
const { t } = useTranslation(translationKey);
|
|
57
|
-
|
|
59
|
+
|
|
58
60
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
59
61
|
const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
|
|
60
62
|
const rootProps = icon
|
|
61
63
|
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
62
64
|
: { children: <ActionLabel action={action} /> };
|
|
65
|
+
|
|
66
|
+
const handleClick = useCallback(() => {
|
|
67
|
+
if (onAction) {
|
|
68
|
+
onAction(action, {});
|
|
69
|
+
} else {
|
|
70
|
+
void executeMenuAction(action);
|
|
71
|
+
}
|
|
72
|
+
}, [action, onAction]);
|
|
73
|
+
|
|
63
74
|
return hidden ? null : (
|
|
64
75
|
<Root
|
|
65
76
|
key={action.id}
|
|
@@ -91,7 +102,13 @@ const DropdownMenuToolbarItem = ({
|
|
|
91
102
|
const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
|
|
92
103
|
const labelAction = (group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group;
|
|
93
104
|
const rootProps = icon
|
|
94
|
-
? {
|
|
105
|
+
? {
|
|
106
|
+
icon,
|
|
107
|
+
size: iconSize,
|
|
108
|
+
iconOnly,
|
|
109
|
+
label: actionLabel(labelAction, t),
|
|
110
|
+
caretDown: true,
|
|
111
|
+
}
|
|
95
112
|
: {
|
|
96
113
|
children: (
|
|
97
114
|
<>
|
|
@@ -100,6 +117,7 @@ const DropdownMenuToolbarItem = ({
|
|
|
100
117
|
</>
|
|
101
118
|
),
|
|
102
119
|
};
|
|
120
|
+
|
|
103
121
|
return (
|
|
104
122
|
<DropdownMenu.Root group={group} items={items}>
|
|
105
123
|
<DropdownMenu.Trigger asChild>
|
|
@@ -110,14 +128,21 @@ const DropdownMenuToolbarItem = ({
|
|
|
110
128
|
};
|
|
111
129
|
|
|
112
130
|
const ToggleGroupItem = ({ group, action, __menuScope }: MenuScopedProps<ToolbarMenuActionProps>) => {
|
|
113
|
-
const { iconSize } = useMenu('ToggleGroupItem', __menuScope);
|
|
131
|
+
const { iconSize, onAction } = useMenu('ToggleGroupItem', __menuScope);
|
|
114
132
|
const { t } = useTranslation(translationKey);
|
|
115
133
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
116
|
-
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]);
|
|
117
141
|
const Root = icon ? NaturalToolbar.ToggleGroupIconItem : NaturalToolbar.ToggleGroupItem;
|
|
118
142
|
const rootProps = icon
|
|
119
143
|
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
120
144
|
: { children: <ActionLabel action={action} /> };
|
|
145
|
+
|
|
121
146
|
return hidden ? null : (
|
|
122
147
|
<Root
|
|
123
148
|
key={action.id}
|
|
@@ -139,8 +164,9 @@ const ToggleGroupToolbarItem = ({
|
|
|
139
164
|
}: MenuScopedProps<ToolbarMenuActionGroupProps>) => {
|
|
140
165
|
const items = useMenuItems(group, propsItems, 'ToggleGroupToolbarItem', __menuScope);
|
|
141
166
|
const { selectCardinality, value } = group.properties;
|
|
167
|
+
|
|
142
168
|
return (
|
|
143
|
-
// TODO(thure):
|
|
169
|
+
// TODO(thure): Fix.
|
|
144
170
|
// @ts-ignore
|
|
145
171
|
<NaturalToolbar.ToggleGroup type={selectCardinality} value={value}>
|
|
146
172
|
{
|
|
@@ -153,41 +179,49 @@ const ToggleGroupToolbarItem = ({
|
|
|
153
179
|
);
|
|
154
180
|
};
|
|
155
181
|
|
|
182
|
+
// TODO(burdon): Reconcile with react-ui/Toolbar (incl. textBlockWidth)
|
|
156
183
|
export const ToolbarMenu = ({
|
|
157
184
|
__menuScope,
|
|
158
185
|
classNames,
|
|
159
|
-
textBlockWidth:
|
|
186
|
+
textBlockWidth: textBlockWidthProp,
|
|
160
187
|
...props
|
|
161
188
|
}: MenuScopedProps<ToolbarMenuProps>) => {
|
|
162
189
|
const items = useMenuItems(undefined, undefined, 'ToolbarMenu', __menuScope);
|
|
163
190
|
const { attendableId } = useMenu('ToolbarMenu', __menuScope);
|
|
164
191
|
const { hasAttention } = useAttention(attendableId);
|
|
165
|
-
const InnerRoot =
|
|
166
|
-
const innerRootProps =
|
|
192
|
+
const InnerRoot = textBlockWidthProp ? 'div' : Fragment;
|
|
193
|
+
const innerRootProps = textBlockWidthProp
|
|
194
|
+
? { role: 'none', className: mx(textBlockWidth, toolbarLayout, 'bs-full') }
|
|
195
|
+
: {};
|
|
196
|
+
|
|
167
197
|
return (
|
|
168
198
|
<NaturalToolbar.Root
|
|
169
199
|
{...props}
|
|
170
|
-
|
|
171
|
-
|
|
200
|
+
classNames={[attendableId && !hasAttention && toolbarInactive, classNames]}
|
|
201
|
+
layoutManaged={textBlockWidthProp}
|
|
172
202
|
>
|
|
173
203
|
<InnerRoot {...innerRootProps}>
|
|
174
|
-
{items?.map((item: MenuItem, i: number) =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
) : isMenuGroup(item) ? (
|
|
178
|
-
item.properties.variant === 'dropdownMenu' ? (
|
|
179
|
-
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
180
|
-
<DropdownMenuToolbarItem key={item.id} group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />
|
|
181
|
-
) : (
|
|
182
|
-
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
183
|
-
<ToggleGroupToolbarItem key={item.id} group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />
|
|
184
|
-
)
|
|
185
|
-
) : (
|
|
186
|
-
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
187
|
-
<ActionToolbarItem key={item.id} action={item as MenuAction} />
|
|
188
|
-
),
|
|
189
|
-
)}
|
|
204
|
+
{items?.map((item: MenuItem, i: number) => (
|
|
205
|
+
<ToolbarMenuItem key={item.id ?? i} item={item} />
|
|
206
|
+
))}
|
|
190
207
|
</InnerRoot>
|
|
191
208
|
</NaturalToolbar.Root>
|
|
192
209
|
);
|
|
193
210
|
};
|
|
211
|
+
|
|
212
|
+
const ToolbarMenuItem = ({ item }: { item: MenuItem }) => {
|
|
213
|
+
if (isSeparator(item)) {
|
|
214
|
+
return <NaturalToolbar.Separator variant={item.properties.variant} />;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
218
|
+
if (isMenuGroup(item)) {
|
|
219
|
+
if (item.properties.variant === 'dropdownMenu') {
|
|
220
|
+
return <DropdownMenuToolbarItem group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />;
|
|
221
|
+
} else {
|
|
222
|
+
return <ToggleGroupToolbarItem group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return <ActionToolbarItem action={item as MenuAction} />;
|
|
227
|
+
};
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { useCallback, useContext, useEffect,
|
|
5
|
+
import { type Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import { useCallback, useContext, useEffect, 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,30 +36,44 @@ const edgesArrayToRecord = (edges: Edge[]): Record<string, Edges> => {
|
|
|
36
36
|
);
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export type ActionGraphNodes = NodeArg<any>[];
|
|
40
|
-
export type ActionGraphEdges = Edge[];
|
|
41
|
-
export type ActionGraphProps = {
|
|
39
|
+
export type ActionGraphNodes = Node.NodeArg<any>[];
|
|
40
|
+
export type ActionGraphEdges = Graph.Edge[];
|
|
41
|
+
export type ActionGraphProps = {
|
|
42
|
+
nodes: ActionGraphNodes;
|
|
43
|
+
edges: ActionGraphEdges;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type MenuActions = {
|
|
47
|
+
useGroupItems: (sourceNode?: MenuItemGroup) => MenuItem[];
|
|
48
|
+
};
|
|
42
49
|
|
|
43
|
-
export const useMenuActions = (props:
|
|
50
|
+
export const useMenuActions = (props: Atom.Atom<ActionGraphProps>): MenuActions => {
|
|
44
51
|
const registry = useContext(RegistryContext);
|
|
45
|
-
const menuGraphProps =
|
|
52
|
+
const menuGraphProps = useAtomValue(props);
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
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({
|
|
49
58
|
registry,
|
|
50
|
-
nodes: menuGraphProps.nodes as Node[],
|
|
59
|
+
nodes: menuGraphProps.nodes as Node.Node[],
|
|
51
60
|
edges: edgesArrayToRecord(menuGraphProps.edges),
|
|
52
|
-
})
|
|
53
|
-
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const graph = graphRef.current;
|
|
54
64
|
|
|
65
|
+
// Update graph nodes when props change (after render completes).
|
|
66
|
+
const prevPropsRef = useRef(menuGraphProps);
|
|
55
67
|
useEffect(() => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
if (prevPropsRef.current !== menuGraphProps) {
|
|
69
|
+
prevPropsRef.current = menuGraphProps;
|
|
70
|
+
graph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
|
|
71
|
+
}
|
|
72
|
+
}, [graph, menuGraphProps]);
|
|
59
73
|
|
|
60
74
|
const useGroupItems = useCallback(
|
|
61
75
|
(sourceNode?: MenuItemGroup) => {
|
|
62
|
-
const items =
|
|
76
|
+
const items = useAtomValue(graph.connections(sourceNode?.id || Node.RootId)) as MenuItem[];
|
|
63
77
|
return items;
|
|
64
78
|
},
|
|
65
79
|
[graph],
|