@dxos/react-ui-menu 0.8.2-staging.7ac8446 → 0.8.2
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 +298 -262
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +299 -264
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +298 -262
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ActionLabel.d.ts +8 -4
- package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
- package/dist/types/src/components/DropdownMenu.d.ts +4 -4
- package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
- package/dist/types/src/components/MenuContext.d.ts +4 -13
- package/dist/types/src/components/MenuContext.d.ts.map +1 -1
- package/dist/types/src/{testing → components}/Menus.stories.d.ts +1 -1
- package/dist/types/src/components/Menus.stories.d.ts.map +1 -0
- package/dist/types/src/components/ToolbarMenu.d.ts +5 -1
- package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useMenuActions.d.ts +13 -0
- package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/generator.d.ts +27 -0
- package/dist/types/src/testing/generator.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -25
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/{defs.d.ts → types.d.ts} +3 -5
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +2 -2
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -13
- package/src/components/ActionLabel.tsx +11 -8
- package/src/components/DropdownMenu.tsx +7 -23
- package/src/components/MenuContext.tsx +11 -26
- package/src/{testing → components}/Menus.stories.tsx +13 -13
- package/src/components/ToolbarMenu.tsx +29 -32
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useMenuActions.ts +69 -0
- package/src/index.ts +3 -1
- package/src/testing/generator.ts +116 -0
- package/src/testing/index.ts +1 -107
- package/src/{defs.ts → types.ts} +2 -5
- package/src/util.ts +7 -7
- package/dist/types/src/defs.d.ts.map +0 -1
- package/dist/types/src/hooks.d.ts +0 -17
- package/dist/types/src/hooks.d.ts.map +0 -1
- package/dist/types/src/testing/Menus.stories.d.ts.map +0 -1
- package/src/hooks.ts +0 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-menu",
|
|
3
|
-
"version": "0.8.2
|
|
3
|
+
"version": "0.8.2",
|
|
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",
|
|
@@ -24,31 +24,38 @@
|
|
|
24
24
|
"src"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@preact/
|
|
27
|
+
"@preact-signals/safe-react": "^0.9.0",
|
|
28
|
+
"@preact/signals-react": "^3.2.0",
|
|
28
29
|
"@radix-ui/react-context": "1.1.1",
|
|
29
30
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
30
|
-
"@dxos/app-graph": "0.8.2
|
|
31
|
-
"@dxos/keyboard": "0.8.2
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
31
|
+
"@dxos/app-graph": "0.8.2",
|
|
32
|
+
"@dxos/keyboard": "0.8.2",
|
|
33
|
+
"@dxos/util": "0.8.2",
|
|
34
|
+
"@dxos/react-ui-attention": "0.8.2"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
37
|
+
"@effect-rx/rx-react": "^0.34.1",
|
|
38
|
+
"@effect/platform": "0.80.12",
|
|
36
39
|
"@types/react": "~18.2.0",
|
|
37
40
|
"@types/react-dom": "~18.2.0",
|
|
41
|
+
"effect": "3.14.21",
|
|
38
42
|
"react": "~18.2.0",
|
|
39
43
|
"react-dom": "~18.2.0",
|
|
40
44
|
"vite": "5.4.7",
|
|
41
|
-
"@dxos/live-object": "0.8.2
|
|
42
|
-
"@dxos/random": "0.8.2
|
|
43
|
-
"@dxos/react-ui": "0.8.2
|
|
44
|
-
"@dxos/react-ui
|
|
45
|
-
"@dxos/storybook-utils": "0.8.2
|
|
45
|
+
"@dxos/live-object": "0.8.2",
|
|
46
|
+
"@dxos/random": "0.8.2",
|
|
47
|
+
"@dxos/react-ui-theme": "0.8.2",
|
|
48
|
+
"@dxos/react-ui": "0.8.2",
|
|
49
|
+
"@dxos/storybook-utils": "0.8.2"
|
|
46
50
|
},
|
|
47
51
|
"peerDependencies": {
|
|
52
|
+
"@effect-rx/rx-react": "^0.34.1",
|
|
53
|
+
"@effect/platform": "0.80.12",
|
|
54
|
+
"effect": "^3.13.3",
|
|
48
55
|
"react": "~18.2.0",
|
|
49
56
|
"react-dom": "~18.2.0",
|
|
50
|
-
"@dxos/react-ui": "0.8.2
|
|
51
|
-
"@dxos/react-ui-theme": "0.8.2
|
|
57
|
+
"@dxos/react-ui": "0.8.2",
|
|
58
|
+
"@dxos/react-ui-theme": "0.8.2"
|
|
52
59
|
},
|
|
53
60
|
"publishConfig": {
|
|
54
61
|
"access": "public"
|
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
+
};
|
|
@@ -3,18 +3,18 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
6
|
-
import React, { type MouseEvent,
|
|
6
|
+
import React, { type MouseEvent, useCallback } from 'react';
|
|
7
7
|
|
|
8
8
|
import { DropdownMenu as NaturalDropdownMenu, Icon, type DropdownMenuRootProps } from '@dxos/react-ui';
|
|
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 '../
|
|
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
|
+
caller?: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const DropdownMenuItem = ({
|
|
@@ -48,7 +48,7 @@ const DropdownMenuRoot = ({
|
|
|
48
48
|
open,
|
|
49
49
|
defaultOpen,
|
|
50
50
|
onOpenChange,
|
|
51
|
-
|
|
51
|
+
caller,
|
|
52
52
|
children,
|
|
53
53
|
__menuScope,
|
|
54
54
|
...naturalProps
|
|
@@ -59,8 +59,6 @@ const DropdownMenuRoot = ({
|
|
|
59
59
|
onChange: onOpenChange,
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
const { onAction } = useMenu('DropdownMenuRoot', __menuScope);
|
|
63
|
-
|
|
64
62
|
const handleActionClick = useCallback(
|
|
65
63
|
(action: MenuAction, event: MouseEvent) => {
|
|
66
64
|
if (action.properties?.disabled) {
|
|
@@ -68,30 +66,16 @@ const DropdownMenuRoot = ({
|
|
|
68
66
|
}
|
|
69
67
|
event.stopPropagation();
|
|
70
68
|
// TODO(thure): Why does Dialog’s modal-ness cause issues if we don’t explicitly close the menu here?
|
|
71
|
-
if (suppressNextTooltip) {
|
|
72
|
-
suppressNextTooltip.current = true;
|
|
73
|
-
}
|
|
74
69
|
setOptionsMenuOpen(false);
|
|
75
|
-
|
|
70
|
+
void action.data?.({ parent: group, caller });
|
|
76
71
|
},
|
|
77
|
-
[
|
|
72
|
+
[group, caller],
|
|
78
73
|
);
|
|
79
74
|
|
|
80
75
|
const items = useMenuItems(group, propsItems);
|
|
81
76
|
|
|
82
77
|
return (
|
|
83
|
-
<NaturalDropdownMenu.Root
|
|
84
|
-
{...{
|
|
85
|
-
open: optionsMenuOpen,
|
|
86
|
-
onOpenChange: (nextOpen: boolean) => {
|
|
87
|
-
if (!nextOpen && suppressNextTooltip) {
|
|
88
|
-
suppressNextTooltip.current = true;
|
|
89
|
-
}
|
|
90
|
-
return setOptionsMenuOpen(nextOpen);
|
|
91
|
-
},
|
|
92
|
-
}}
|
|
93
|
-
{...naturalProps}
|
|
94
|
-
>
|
|
78
|
+
<NaturalDropdownMenu.Root open={optionsMenuOpen} onOpenChange={setOptionsMenuOpen} {...naturalProps}>
|
|
95
79
|
{children}
|
|
96
80
|
<NaturalDropdownMenu.Portal>
|
|
97
81
|
<NaturalDropdownMenu.Content>
|
|
@@ -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
|
|
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,32 +13,24 @@ const MENU_NAME = 'GraphMenu';
|
|
|
21
13
|
|
|
22
14
|
const [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
|
|
23
15
|
|
|
24
|
-
const [MenuContextProvider, useMenu] = createMenuContext<MenuContextValue
|
|
16
|
+
const [MenuContextProvider, useMenu] = createMenuContext<MenuContextValue>(MENU_NAME);
|
|
25
17
|
|
|
26
|
-
export const menuContextDefaults: MenuContextValue
|
|
18
|
+
export const menuContextDefaults: MenuContextValue = {
|
|
27
19
|
iconSize: 5,
|
|
28
|
-
|
|
29
|
-
resolveGroupItems: () => null,
|
|
20
|
+
useGroupItems: () => null,
|
|
30
21
|
};
|
|
31
22
|
|
|
32
23
|
const useMenuScope = createMenuScope();
|
|
33
24
|
|
|
34
|
-
const MenuProvider =
|
|
25
|
+
const MenuProvider = ({
|
|
35
26
|
children,
|
|
36
|
-
|
|
37
|
-
onAction = menuContextDefaults.onAction as MenuActionHandler<A>,
|
|
27
|
+
useGroupItems = menuContextDefaults.useGroupItems,
|
|
38
28
|
iconSize = menuContextDefaults.iconSize,
|
|
39
29
|
attendableId,
|
|
40
|
-
}: PropsWithChildren<Partial<MenuContextValue
|
|
30
|
+
}: PropsWithChildren<Partial<MenuContextValue>>) => {
|
|
41
31
|
const { scope } = useMenuScope(undefined);
|
|
42
32
|
return (
|
|
43
|
-
<MenuContextProvider
|
|
44
|
-
resolveGroupItems={resolveGroupItems}
|
|
45
|
-
onAction={onAction}
|
|
46
|
-
iconSize={iconSize}
|
|
47
|
-
attendableId={attendableId}
|
|
48
|
-
scope={scope}
|
|
49
|
-
>
|
|
33
|
+
<MenuContextProvider useGroupItems={useGroupItems} iconSize={iconSize} attendableId={attendableId} scope={scope}>
|
|
50
34
|
{children}
|
|
51
35
|
</MenuContextProvider>
|
|
52
36
|
);
|
|
@@ -58,8 +42,9 @@ export const useMenuItems = (
|
|
|
58
42
|
consumerName: string = 'useMenuItemConsumer',
|
|
59
43
|
__menuScope?: Scope,
|
|
60
44
|
) => {
|
|
61
|
-
const {
|
|
62
|
-
|
|
45
|
+
const { useGroupItems } = useMenu(consumerName, __menuScope);
|
|
46
|
+
const groupItems = useGroupItems(group);
|
|
47
|
+
return useMemo(() => propsItems ?? groupItems ?? undefined, [propsItems, groupItems]);
|
|
63
48
|
};
|
|
64
49
|
|
|
65
50
|
export { useMenu, createMenuScope, MenuProvider };
|
|
@@ -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
13
|
import { useMenuActions } from '../hooks';
|
|
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(
|
|
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
|
-
|
|
32
|
+
|
|
36
33
|
return (
|
|
37
|
-
<MenuProvider
|
|
34
|
+
<MenuProvider useGroupItems={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
|
|
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
|
|
62
|
+
<MenuProvider {...menu}>
|
|
63
63
|
<ToolbarMenu />
|
|
64
64
|
</MenuProvider>
|
|
65
65
|
);
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { useCallback
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import { Icon,
|
|
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 '../
|
|
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 {
|
|
49
|
-
const
|
|
50
|
-
|
|
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:
|
|
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
|
|
69
|
+
{...(rootProps as any)}
|
|
65
70
|
classNames={classNames}
|
|
66
71
|
/>
|
|
67
72
|
);
|
|
@@ -73,7 +78,7 @@ const DropdownMenuToolbarItem = ({
|
|
|
73
78
|
__menuScope,
|
|
74
79
|
}: MenuScopedProps<ToolbarMenuActionGroupProps>) => {
|
|
75
80
|
const { iconOnly, disabled, testId } = group.properties;
|
|
76
|
-
const
|
|
81
|
+
const { t } = useTranslation(translationKey);
|
|
77
82
|
const { iconSize } = useMenu('DropdownMenuToolbarItem', __menuScope);
|
|
78
83
|
const items = useMenuItems(group, propsItems, 'DropdownMenuToolbarItem', __menuScope);
|
|
79
84
|
const activeItem = items?.find((item) => !!(item as MenuAction).properties.checked);
|
|
@@ -83,42 +88,34 @@ const DropdownMenuToolbarItem = ({
|
|
|
83
88
|
(activeItem as MenuAction)?.properties.icon) ||
|
|
84
89
|
group.properties.icon;
|
|
85
90
|
const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
|
|
86
|
-
const
|
|
87
|
-
<ActionLabel action={(group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group} />
|
|
88
|
-
);
|
|
91
|
+
const labelAction = (group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group;
|
|
89
92
|
const rootProps = icon
|
|
90
|
-
? { icon, size: iconSize, iconOnly, label, caretDown: true
|
|
93
|
+
? { icon, size: iconSize, iconOnly, label: actionLabel(labelAction, t), caretDown: true }
|
|
91
94
|
: {
|
|
92
95
|
children: (
|
|
93
96
|
<>
|
|
94
|
-
{
|
|
97
|
+
<ActionLabel action={labelAction} />
|
|
95
98
|
<Icon size={3} icon='ph--caret-down--bold' classNames='mis-1' />
|
|
96
99
|
</>
|
|
97
100
|
),
|
|
98
101
|
};
|
|
99
102
|
return (
|
|
100
|
-
<DropdownMenu.Root group={group} items={items}
|
|
103
|
+
<DropdownMenu.Root group={group} items={items}>
|
|
101
104
|
<DropdownMenu.Trigger asChild>
|
|
102
|
-
<Root
|
|
103
|
-
variant='ghost'
|
|
104
|
-
disabled={disabled}
|
|
105
|
-
{...(rootProps as IconButtonProps)}
|
|
106
|
-
{...(testId && { 'data-testid': testId })}
|
|
107
|
-
/>
|
|
105
|
+
<Root variant='ghost' disabled={disabled} {...(rootProps as any)} {...(testId && { 'data-testid': testId })} />
|
|
108
106
|
</DropdownMenu.Trigger>
|
|
109
107
|
</DropdownMenu.Root>
|
|
110
108
|
);
|
|
111
109
|
};
|
|
112
110
|
|
|
113
|
-
const ToggleGroupItem = ({ action, __menuScope }: MenuScopedProps<
|
|
114
|
-
const { iconSize
|
|
111
|
+
const ToggleGroupItem = ({ group, action, __menuScope }: MenuScopedProps<ToolbarMenuActionProps>) => {
|
|
112
|
+
const { iconSize } = useMenu('ToggleGroupItem', __menuScope);
|
|
113
|
+
const { t } = useTranslation(translationKey);
|
|
115
114
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
116
|
-
const handleClick = useCallback(() => {
|
|
117
|
-
onAction?.(action);
|
|
118
|
-
}, [action, onAction]);
|
|
115
|
+
const handleClick = useCallback(() => action.data?.({ parent: group }), [action, group]);
|
|
119
116
|
const Root = icon ? NaturalToolbar.ToggleGroupIconItem : NaturalToolbar.ToggleGroupItem;
|
|
120
117
|
const rootProps = icon
|
|
121
|
-
? { icon, size: iconSize, iconOnly, label:
|
|
118
|
+
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
122
119
|
: { children: <ActionLabel action={action} /> };
|
|
123
120
|
return hidden ? null : (
|
|
124
121
|
<Root
|
|
@@ -128,7 +125,7 @@ const ToggleGroupItem = ({ action, __menuScope }: MenuScopedProps<{ action: Menu
|
|
|
128
125
|
onClick={handleClick}
|
|
129
126
|
variant='ghost'
|
|
130
127
|
{...(testId && { 'data-testid': testId })}
|
|
131
|
-
{...(rootProps as
|
|
128
|
+
{...(rootProps as any)}
|
|
132
129
|
classNames={classNames}
|
|
133
130
|
/>
|
|
134
131
|
);
|
|
@@ -148,7 +145,7 @@ const ToggleGroupToolbarItem = ({
|
|
|
148
145
|
{
|
|
149
146
|
// TODO(thure): Handle other menu item types.
|
|
150
147
|
(items as MenuAction[]).map((action) => (
|
|
151
|
-
<ToggleGroupItem key={action.id} action={action} />
|
|
148
|
+
<ToggleGroupItem key={action.id} group={group} action={action} />
|
|
152
149
|
))
|
|
153
150
|
}
|
|
154
151
|
</NaturalToolbar.ToggleGroup>
|
|
@@ -162,7 +159,7 @@ export const ToolbarMenu = ({ __menuScope, classNames, ...props }: MenuScopedPro
|
|
|
162
159
|
return (
|
|
163
160
|
<NaturalToolbar.Root
|
|
164
161
|
{...props}
|
|
165
|
-
classNames={['p-
|
|
162
|
+
classNames={['p-1', attendableId && !hasAttention && '[&>*]:opacity-20 !bg-transparent', classNames]}
|
|
166
163
|
>
|
|
167
164
|
{items?.map((item: MenuItem, i: number) =>
|
|
168
165
|
isSeparator(item) ? (
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { RegistryContext, type Rx, useRxValue } from '@effect-rx/rx-react';
|
|
6
|
+
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type Edge, type Edges, Graph, type NodeArg, type Node, ROOT_ID } from '@dxos/app-graph';
|
|
9
|
+
|
|
10
|
+
import { type MenuItem, type MenuItemGroup } from '../types';
|
|
11
|
+
|
|
12
|
+
const edgesArrayToRecord = (edges: Edge[]): Record<string, Edges> => {
|
|
13
|
+
return Object.fromEntries(
|
|
14
|
+
Object.entries(
|
|
15
|
+
edges.reduce((acc: Record<string, { inbound: string[]; outbound: string[] }>, { source, target }) => {
|
|
16
|
+
if (!acc[source]) {
|
|
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
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ActionGraphNodes = NodeArg<any>[];
|
|
40
|
+
export type ActionGraphEdges = Edge[];
|
|
41
|
+
export type ActionGraphProps = { nodes: ActionGraphNodes; edges: ActionGraphEdges };
|
|
42
|
+
|
|
43
|
+
export const useMenuActions = (props: Rx.Rx<ActionGraphProps>) => {
|
|
44
|
+
const registry = useContext(RegistryContext);
|
|
45
|
+
const menuGraphProps = useRxValue(props);
|
|
46
|
+
|
|
47
|
+
const [graph] = useState(
|
|
48
|
+
new Graph({
|
|
49
|
+
registry,
|
|
50
|
+
nodes: menuGraphProps.nodes as Node[],
|
|
51
|
+
edges: edgesArrayToRecord(menuGraphProps.edges),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
graph.addNodes(menuGraphProps.nodes);
|
|
57
|
+
graph.addEdges(menuGraphProps.edges);
|
|
58
|
+
}, [menuGraphProps]);
|
|
59
|
+
|
|
60
|
+
const useGroupItems = useCallback(
|
|
61
|
+
(sourceNode?: MenuItemGroup) => {
|
|
62
|
+
const items = useRxValue(graph.connections(sourceNode?.id || ROOT_ID)) as MenuItem[];
|
|
63
|
+
return items;
|
|
64
|
+
},
|
|
65
|
+
[graph],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return { useGroupItems };
|
|
69
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
export { rxFromSignal } from '@dxos/app-graph';
|
|
6
|
+
|
|
5
7
|
export * from './components';
|
|
6
|
-
export * from './defs';
|
|
7
8
|
export * from './hooks';
|
|
9
|
+
export * from './types';
|
|
8
10
|
export { createMenuAction, createMenuItemGroup, createLineSeparator, createGapSeparator } from './util';
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Rx } from '@effect-rx/rx-react';
|
|
6
|
+
import { useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type Action,
|
|
10
|
+
Graph,
|
|
11
|
+
type NodeArg,
|
|
12
|
+
ACTION_TYPE,
|
|
13
|
+
ACTION_GROUP_TYPE,
|
|
14
|
+
actionGroupSymbol,
|
|
15
|
+
ROOT_ID,
|
|
16
|
+
} from '@dxos/app-graph';
|
|
17
|
+
import { live } from '@dxos/live-object';
|
|
18
|
+
import { faker } from '@dxos/random';
|
|
19
|
+
import { type DeepWriteable } from '@dxos/util';
|
|
20
|
+
|
|
21
|
+
import { type ActionGraphProps } from '../hooks/useMenuActions';
|
|
22
|
+
import { type MenuAction, type MenuItemGroup, type MenuItem } from '../types';
|
|
23
|
+
|
|
24
|
+
export type CreateActionsParams = Partial<{
|
|
25
|
+
type?: typeof ACTION_TYPE | typeof ACTION_GROUP_TYPE;
|
|
26
|
+
callback: () => void;
|
|
27
|
+
count: number;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
const icons = {
|
|
31
|
+
regular: [
|
|
32
|
+
'ph--text-b--regular',
|
|
33
|
+
'ph--text-italic--regular',
|
|
34
|
+
'ph--text-h-five--regular',
|
|
35
|
+
'ph--chat-text--regular',
|
|
36
|
+
'ph--clipboard-text--regular',
|
|
37
|
+
'ph--link-simple--regular',
|
|
38
|
+
],
|
|
39
|
+
fill: [
|
|
40
|
+
'ph--text-b--fill',
|
|
41
|
+
'ph--text-italic--fill',
|
|
42
|
+
'ph--text-h-five--fill',
|
|
43
|
+
'ph--chat-text--fill',
|
|
44
|
+
'ph--clipboard-text--fill',
|
|
45
|
+
'ph--link-simple--fill',
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const createActions = (params?: CreateActionsParams) => {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
const { callback = () => console.log('invoke'), count = 12, type = ACTION_TYPE } = params ?? {};
|
|
52
|
+
return faker.helpers.multiple(
|
|
53
|
+
() =>
|
|
54
|
+
live({
|
|
55
|
+
id: faker.string.uuid(),
|
|
56
|
+
type,
|
|
57
|
+
data: type === ACTION_GROUP_TYPE ? actionGroupSymbol : callback,
|
|
58
|
+
properties: {
|
|
59
|
+
label: faker.lorem.words(2),
|
|
60
|
+
icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
|
|
61
|
+
disabled: faker.helpers.arrayElement([true, false]),
|
|
62
|
+
...(type === ACTION_GROUP_TYPE && { variant: 'dropdownMenu' }),
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
{ count },
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const createNestedActions = Rx.make(() => {
|
|
70
|
+
const result: ActionGraphProps = { edges: [], nodes: [] };
|
|
71
|
+
const actionGroups = createActions({ type: ACTION_GROUP_TYPE });
|
|
72
|
+
actionGroups.forEach((group) => {
|
|
73
|
+
const actions = createActions();
|
|
74
|
+
result.nodes.push(group, ...actions);
|
|
75
|
+
result.edges.push(
|
|
76
|
+
{ source: 'root', target: group.id },
|
|
77
|
+
...actions.map((action) => ({ source: group.id, target: action.id })),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
return result;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const createNestedActionsResolver = (groupParams?: CreateActionsParams, params?: CreateActionsParams) => {
|
|
84
|
+
const graph = new Graph();
|
|
85
|
+
const actionGroups = createActions({ type: ACTION_GROUP_TYPE, ...groupParams });
|
|
86
|
+
actionGroups.forEach((group) => {
|
|
87
|
+
const actions = createActions(params);
|
|
88
|
+
graph.addNodes([group as NodeArg<any>, ...(actions as NodeArg<any>[])]);
|
|
89
|
+
graph.addEdges([
|
|
90
|
+
{ source: 'root', target: group.id },
|
|
91
|
+
...actions.map((action) => ({ source: group.id, target: action.id })),
|
|
92
|
+
]);
|
|
93
|
+
void graph.expand(group.id);
|
|
94
|
+
});
|
|
95
|
+
const resolveGroupItems = (groupNode?: MenuItemGroup) =>
|
|
96
|
+
(graph.getActions(groupNode?.id ?? ROOT_ID) || null) as MenuItem[] | null;
|
|
97
|
+
return { resolveGroupItems };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const mutateActionsOnInterval = (actions: Action[]) => {
|
|
101
|
+
let cursor = 0;
|
|
102
|
+
return setInterval(() => {
|
|
103
|
+
const action = actions[cursor] as DeepWriteable<Action>;
|
|
104
|
+
action.properties.icon = faker.helpers.arrayElement(
|
|
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
|
+
};
|
|
111
|
+
|
|
112
|
+
export const useMutateActions = (actions: MenuAction[]) =>
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const interval = mutateActionsOnInterval(actions);
|
|
115
|
+
return () => clearInterval(interval);
|
|
116
|
+
}, []);
|