@dxos/react-ui-menu 0.8.3-staging.0fa589b → 0.8.4-main.1068cf700f
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 +364 -339
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +364 -339
- 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 +14 -0
- package/dist/types/src/builder.d.ts.map +1 -0
- 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 +1 -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 +3 -4
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +13 -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 -28
- package/src/builder.ts +62 -0
- package/src/components/ActionLabel.tsx +6 -7
- package/src/components/DropdownMenu.tsx +16 -6
- package/src/components/MenuContext.tsx +18 -4
- package/src/components/ToolbarMenu.stories.tsx +81 -0
- package/src/components/ToolbarMenu.tsx +70 -35
- package/src/hooks/useMenuActions.ts +23 -45
- package/src/index.ts +1 -2
- package/src/testing/generator.ts +63 -58
- package/src/translations.ts +5 -3
- package/src/types.ts +14 -26
- package/src/util.ts +32 -14
- package/dist/lib/node/index.cjs +0 -506
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- 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 -67
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-menu",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4-main.1068cf700f",
|
|
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,38 +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/react-ui-attention": "0.8.
|
|
33
|
-
"@dxos/util": "0.8.
|
|
34
|
-
"@dxos/
|
|
34
|
+
"@dxos/keyboard": "0.8.4-main.1068cf700f",
|
|
35
|
+
"@dxos/react-ui-attention": "0.8.4-main.1068cf700f",
|
|
36
|
+
"@dxos/util": "0.8.4-main.1068cf700f",
|
|
37
|
+
"@dxos/invariant": "0.8.4-main.1068cf700f",
|
|
38
|
+
"@dxos/effect": "0.8.4-main.1068cf700f",
|
|
39
|
+
"@dxos/app-graph": "0.8.4-main.1068cf700f",
|
|
40
|
+
"@dxos/ui-types": "0.8.4-main.1068cf700f"
|
|
35
41
|
},
|
|
36
42
|
"devDependencies": {
|
|
37
|
-
"@effect-
|
|
38
|
-
"@effect/platform": "0.
|
|
39
|
-
"@types/react": "~
|
|
40
|
-
"@types/react-dom": "~
|
|
41
|
-
"effect": "3.
|
|
42
|
-
"react": "~
|
|
43
|
-
"react-dom": "~
|
|
44
|
-
"vite": "
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/react-ui": "0.8.
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/random": "0.8.3-staging.0fa589b"
|
|
43
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
44
|
+
"@effect/platform": "0.94.4",
|
|
45
|
+
"@types/react": "~19.2.7",
|
|
46
|
+
"@types/react-dom": "~19.2.3",
|
|
47
|
+
"effect": "3.19.16",
|
|
48
|
+
"react": "~19.2.3",
|
|
49
|
+
"react-dom": "~19.2.3",
|
|
50
|
+
"vite": "7.1.9",
|
|
51
|
+
"@dxos/random": "0.8.4-main.1068cf700f",
|
|
52
|
+
"@dxos/react-ui": "0.8.4-main.1068cf700f",
|
|
53
|
+
"@dxos/ui-theme": "0.8.4-main.1068cf700f",
|
|
54
|
+
"@dxos/storybook-utils": "0.8.4-main.1068cf700f"
|
|
50
55
|
},
|
|
51
56
|
"peerDependencies": {
|
|
52
|
-
"@effect-
|
|
53
|
-
"@effect/platform": "0.
|
|
54
|
-
"effect": "
|
|
55
|
-
"react": "~
|
|
56
|
-
"react-dom": "~
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/react-ui": "0.8.
|
|
57
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
58
|
+
"@effect/platform": "0.94.4",
|
|
59
|
+
"effect": "3.19.16",
|
|
60
|
+
"react": "~19.2.3",
|
|
61
|
+
"react-dom": "~19.2.3",
|
|
62
|
+
"@dxos/ui-theme": "0.8.4-main.1068cf700f",
|
|
63
|
+
"@dxos/react-ui": "0.8.4-main.1068cf700f"
|
|
59
64
|
},
|
|
60
65
|
"publishConfig": {
|
|
61
66
|
"access": "public"
|
package/src/builder.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { invariant } from '@dxos/invariant';
|
|
6
|
+
import type { MenuActionProperties, MenuItemGroupProperties } from '@dxos/ui-types';
|
|
7
|
+
|
|
8
|
+
import type { ActionGraphProps } from './hooks';
|
|
9
|
+
import { createMenuAction, createMenuItemGroup } from './util';
|
|
10
|
+
|
|
11
|
+
export interface ActionGroupBuilder {
|
|
12
|
+
action<P extends {} = {}>(id: string, props: P & MenuActionProperties, invoke: () => void): this;
|
|
13
|
+
group<P extends {} = {}>(
|
|
14
|
+
id: string,
|
|
15
|
+
props: P & MenuItemGroupProperties,
|
|
16
|
+
cb: (builder: ActionGroupBuilder) => void,
|
|
17
|
+
): this;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MenuBuilder extends ActionGroupBuilder {
|
|
21
|
+
root(props: MenuItemGroupProperties): this;
|
|
22
|
+
build(): ActionGraphProps;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const MenuBuilder = Object.freeze({
|
|
26
|
+
make: (): MenuBuilder => new MenuBuilderImpl({ nodes: [], edges: [] }, 'root'),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
class MenuBuilderImpl implements MenuBuilder {
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly _data: ActionGraphProps,
|
|
32
|
+
private readonly _rootId: string,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
root(props: MenuItemGroupProperties): this {
|
|
36
|
+
invariant(this._rootId === 'root', 'Root group can only be at the top level');
|
|
37
|
+
invariant(this._data.nodes.find((node) => node.id === 'root') === undefined, 'Root group can only be created once');
|
|
38
|
+
this._data.nodes.push(createMenuItemGroup('root', props));
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
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
|
+
group<P extends {} = {}>(
|
|
49
|
+
id: string,
|
|
50
|
+
props: P & MenuItemGroupProperties,
|
|
51
|
+
cb: (builder: ActionGroupBuilder) => void,
|
|
52
|
+
): this {
|
|
53
|
+
this._data.nodes.push(createMenuItemGroup(id, props));
|
|
54
|
+
this._data.edges.push({ source: this._rootId, target: id });
|
|
55
|
+
cb(new MenuBuilderImpl(this._data, id));
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
build(): ActionGraphProps {
|
|
60
|
+
return this._data;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -4,21 +4,20 @@
|
|
|
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
|
-
const shortcut = getShortcut(action);
|
|
20
19
|
const { t } = useTranslation(translationKey);
|
|
21
|
-
|
|
20
|
+
const shortcut = getShortcut(action);
|
|
22
21
|
return (
|
|
23
22
|
<>
|
|
24
23
|
<span className='grow truncate'>{toLocalizedString(action.properties!.label, t)}</span>
|
|
@@ -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,31 @@ 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
|
+
alwaysActive,
|
|
34
|
+
onAction,
|
|
35
|
+
}: MenuProviderProps) => {
|
|
31
36
|
const { scope } = useMenuScope(undefined);
|
|
32
37
|
return (
|
|
33
|
-
<MenuContextProvider
|
|
38
|
+
<MenuContextProvider
|
|
39
|
+
useGroupItems={useGroupItems}
|
|
40
|
+
iconSize={iconSize}
|
|
41
|
+
attendableId={attendableId}
|
|
42
|
+
alwaysActive={alwaysActive}
|
|
43
|
+
onAction={onAction}
|
|
44
|
+
scope={scope}
|
|
45
|
+
>
|
|
34
46
|
{children}
|
|
35
47
|
</MenuContextProvider>
|
|
36
48
|
);
|
|
@@ -48,3 +60,5 @@ export const useMenuItems = (
|
|
|
48
60
|
};
|
|
49
61
|
|
|
50
62
|
export { useMenu, createMenuScope, MenuProvider };
|
|
63
|
+
|
|
64
|
+
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, 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,50 @@ 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
|
-
const { attendableId } = useMenu('ToolbarMenu', __menuScope);
|
|
190
|
+
const { attendableId, alwaysActive } = 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, classNames]}
|
|
201
|
+
disabled={!alwaysActive && !hasAttention}
|
|
202
|
+
layoutManaged={textBlockWidthProp}
|
|
172
203
|
>
|
|
173
204
|
<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
|
-
)}
|
|
205
|
+
{items?.map((item: MenuItem, i: number) => (
|
|
206
|
+
<ToolbarMenuItem key={item.id ?? i} item={item} />
|
|
207
|
+
))}
|
|
190
208
|
</InnerRoot>
|
|
191
209
|
</NaturalToolbar.Root>
|
|
192
210
|
);
|
|
193
211
|
};
|
|
212
|
+
|
|
213
|
+
const ToolbarMenuItem = ({ item }: { item: MenuItem }) => {
|
|
214
|
+
if (isSeparator(item)) {
|
|
215
|
+
return <NaturalToolbar.Separator variant={item.properties.variant} />;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
219
|
+
if (isMenuGroup(item)) {
|
|
220
|
+
if (item.properties.variant === 'dropdownMenu') {
|
|
221
|
+
return <DropdownMenuToolbarItem group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />;
|
|
222
|
+
} else {
|
|
223
|
+
return <ToggleGroupToolbarItem group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return <ActionToolbarItem action={item as MenuAction} />;
|
|
228
|
+
};
|
|
@@ -2,64 +2,42 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { useCallback, useContext,
|
|
5
|
+
import { type Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
acc[source] = { inbound: [], outbound: [] };
|
|
18
|
-
}
|
|
19
|
-
if (!acc[target]) {
|
|
20
|
-
acc[target] = { inbound: [], outbound: [] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const sourceEdges = acc[source];
|
|
24
|
-
if (!sourceEdges.outbound.includes(target)) {
|
|
25
|
-
sourceEdges.outbound.push(target);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const targetEdges = acc[target];
|
|
29
|
-
if (!targetEdges.inbound.includes(source)) {
|
|
30
|
-
targetEdges.inbound.push(source);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return acc;
|
|
34
|
-
}, {}),
|
|
35
|
-
),
|
|
36
|
-
);
|
|
12
|
+
export type ActionGraphNodes = Node.NodeArg<any>[];
|
|
13
|
+
export type ActionGraphEdges = Graph.Edge[];
|
|
14
|
+
export type ActionGraphProps = {
|
|
15
|
+
nodes: ActionGraphNodes;
|
|
16
|
+
edges: ActionGraphEdges;
|
|
37
17
|
};
|
|
38
18
|
|
|
39
|
-
export type
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
export type MenuActions = {
|
|
20
|
+
useGroupItems: (sourceNode?: MenuItemGroup) => MenuItem[];
|
|
21
|
+
};
|
|
42
22
|
|
|
43
|
-
export const useMenuActions = (props:
|
|
23
|
+
export const useMenuActions = (props: Atom.Atom<ActionGraphProps>): MenuActions => {
|
|
44
24
|
const registry = useContext(RegistryContext);
|
|
45
|
-
const menuGraphProps =
|
|
25
|
+
const menuGraphProps = useAtomValue(props);
|
|
46
26
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
27
|
+
// Create a new graph whenever props change to preserve correct order.
|
|
28
|
+
// (Graph.addEdges appends rather than replaces, which breaks ordering on updates.)
|
|
29
|
+
const graphRef = useRef<{ graph: Graph.WritableGraph; props: ActionGraphProps } | null>(null);
|
|
30
|
+
if (!graphRef.current || graphRef.current.props !== menuGraphProps) {
|
|
31
|
+
const newGraph = Graph.make({ registry });
|
|
32
|
+
newGraph.pipe(Graph.addNodes(menuGraphProps.nodes as Node.NodeArg<any>[]), Graph.addEdges(menuGraphProps.edges));
|
|
33
|
+
graphRef.current = { graph: newGraph, props: menuGraphProps };
|
|
34
|
+
}
|
|
54
35
|
|
|
55
|
-
|
|
56
|
-
graph.addNodes(menuGraphProps.nodes);
|
|
57
|
-
graph.addEdges(menuGraphProps.edges);
|
|
58
|
-
}, [menuGraphProps]);
|
|
36
|
+
const graph = graphRef.current.graph;
|
|
59
37
|
|
|
60
38
|
const useGroupItems = useCallback(
|
|
61
39
|
(sourceNode?: MenuItemGroup) => {
|
|
62
|
-
const items =
|
|
40
|
+
const items = useAtomValue(graph.connections(sourceNode?.id || Node.RootId)) as MenuItem[];
|
|
63
41
|
return items;
|
|
64
42
|
},
|
|
65
43
|
[graph],
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export { rxFromSignal } from '@dxos/app-graph';
|
|
6
|
-
|
|
7
5
|
export * from './components';
|
|
8
6
|
export * from './hooks';
|
|
9
7
|
export * from './types';
|
|
10
8
|
export { createMenuAction, createMenuItemGroup, createLineSeparator, createGapSeparator } from './util';
|
|
9
|
+
export { MenuBuilder } from './builder';
|