@dxos/react-ui-menu 0.9.0 → 0.9.1-staging.ee54ba693a
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 +156 -43
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +3 -1
- package/dist/lib/browser/translations.mjs.map +2 -2
- package/dist/lib/node-esm/index.mjs +156 -43
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +3 -1
- package/dist/lib/node-esm/translations.mjs.map +2 -2
- package/dist/types/src/builder.d.ts +15 -3
- package/dist/types/src/builder.d.ts.map +1 -1
- package/dist/types/src/components/ActionLabel.d.ts +2 -2
- package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
- package/dist/types/src/components/Menu.d.ts +4 -4
- package/dist/types/src/components/Menu.d.ts.map +1 -1
- package/dist/types/src/components/Menu.stories.d.ts +3 -1
- package/dist/types/src/components/Menu.stories.d.ts.map +1 -1
- package/dist/types/src/components/ToolbarMenu.d.ts +14 -12
- package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
- package/dist/types/src/components/ToolbarMenu.stories.d.ts +7 -1
- package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useGraphMenuActions.d.ts +31 -0
- package/dist/types/src/hooks/useGraphMenuActions.d.ts.map +1 -0
- package/dist/types/src/hooks/useGraphMenuActions.test.d.ts +2 -0
- package/dist/types/src/hooks/useGraphMenuActions.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +3 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +7 -11
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +8 -3
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +25 -25
- package/src/builder.test.ts +50 -11
- package/src/builder.ts +36 -5
- package/src/components/ActionLabel.tsx +2 -2
- package/src/components/Menu.tsx +7 -12
- package/src/components/ToolbarMenu.stories.tsx +31 -2
- package/src/components/ToolbarMenu.tsx +125 -66
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useGraphMenuActions.test.ts +93 -0
- package/src/hooks/useGraphMenuActions.ts +96 -0
- package/src/index.ts +1 -1
- package/src/translations.ts +3 -1
- package/src/types.ts +9 -6
- package/src/util.ts +1 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-menu",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
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",
|
|
@@ -37,39 +37,39 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@radix-ui/react-context": "1.1.1",
|
|
39
39
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
40
|
-
"@dxos/app-graph": "0.9.
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
40
|
+
"@dxos/app-graph": "0.9.1-staging.ee54ba693a",
|
|
41
|
+
"@dxos/invariant": "0.9.1-staging.ee54ba693a",
|
|
42
|
+
"@dxos/keyboard": "0.9.1-staging.ee54ba693a",
|
|
43
|
+
"@dxos/effect": "0.9.1-staging.ee54ba693a",
|
|
44
|
+
"@dxos/react-ui-attention": "0.9.1-staging.ee54ba693a",
|
|
45
|
+
"@dxos/util": "0.9.1-staging.ee54ba693a",
|
|
46
|
+
"@dxos/log": "0.9.1-staging.ee54ba693a",
|
|
47
|
+
"@dxos/ui-types": "0.9.1-staging.ee54ba693a"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@effect-atom/atom-react": "^0.5.0",
|
|
51
|
-
"@effect/platform": "0.96.
|
|
51
|
+
"@effect/platform": "0.96.2",
|
|
52
52
|
"@testing-library/jest-dom": "^6.9.1",
|
|
53
|
-
"@testing-library/react": "^16.3.
|
|
54
|
-
"@types/react": "~19.2.
|
|
53
|
+
"@testing-library/react": "^16.3.2",
|
|
54
|
+
"@types/react": "~19.2.17",
|
|
55
55
|
"@types/react-dom": "~19.2.3",
|
|
56
|
-
"effect": "3.21.
|
|
57
|
-
"react": "~19.2.
|
|
58
|
-
"react-dom": "~19.2.
|
|
56
|
+
"effect": "3.21.4",
|
|
57
|
+
"react": "~19.2.7",
|
|
58
|
+
"react-dom": "~19.2.7",
|
|
59
59
|
"vite": "^8.0.16",
|
|
60
|
-
"@dxos/random": "0.9.
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/
|
|
63
|
-
"@dxos/ui-theme": "0.9.
|
|
60
|
+
"@dxos/random": "0.9.1-staging.ee54ba693a",
|
|
61
|
+
"@dxos/storybook-utils": "0.9.1-staging.ee54ba693a",
|
|
62
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a",
|
|
63
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@effect-atom/atom-react": "^0.5.0",
|
|
67
|
-
"@effect/platform": "0.96.
|
|
68
|
-
"effect": "3.21.
|
|
69
|
-
"react": "~19.2.
|
|
70
|
-
"react-dom": "~19.2.
|
|
71
|
-
"@dxos/
|
|
72
|
-
"@dxos/ui
|
|
67
|
+
"@effect/platform": "0.96.2",
|
|
68
|
+
"effect": "3.21.4",
|
|
69
|
+
"react": "~19.2.7",
|
|
70
|
+
"react-dom": "~19.2.7",
|
|
71
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
|
|
72
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a"
|
|
73
73
|
},
|
|
74
74
|
"publishConfig": {
|
|
75
75
|
"access": "public"
|
package/src/builder.test.ts
CHANGED
|
@@ -74,7 +74,7 @@ describe('MenuBuilder', () => {
|
|
|
74
74
|
test('merges pre-built subgraph', ({ expect }) => {
|
|
75
75
|
const headings = MenuBuilder.make()
|
|
76
76
|
.root({ label: 'headings' })
|
|
77
|
-
.group('headings', { label: 'Headings', variant: 'dropdownMenu' }, (group) => {
|
|
77
|
+
.group('headings', { label: 'Headings', variant: 'dropdownMenu', icon: 'ph--text-h-one--regular' }, (group) => {
|
|
78
78
|
group
|
|
79
79
|
.action('h1', { label: 'H1', icon: 'ph--text-h-one--regular' }, () => {})
|
|
80
80
|
.action('h2', { label: 'H2', icon: 'ph--text-h-two--regular' }, () => {});
|
|
@@ -115,11 +115,15 @@ describe('MenuBuilder', () => {
|
|
|
115
115
|
.root({ label: 'toolbar' })
|
|
116
116
|
.subgraph((sub) => {
|
|
117
117
|
sub
|
|
118
|
-
.group(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
.group(
|
|
119
|
+
'headings',
|
|
120
|
+
{ label: 'Headings', variant: 'dropdownMenu', icon: 'ph--text-h-one--regular' },
|
|
121
|
+
(group) => {
|
|
122
|
+
group
|
|
123
|
+
.action('h1', { label: 'H1', icon: 'ph--text-h-one--regular' }, () => {})
|
|
124
|
+
.action('h2', { label: 'H2', icon: 'ph--text-h-two--regular' }, () => {});
|
|
125
|
+
},
|
|
126
|
+
)
|
|
123
127
|
.group('formatting', { label: 'Formatting' }, (group) => {
|
|
124
128
|
group.action('bold', { label: 'Bold', icon: 'ph--text-b--regular' }, () => {});
|
|
125
129
|
});
|
|
@@ -160,11 +164,15 @@ describe('MenuBuilder', () => {
|
|
|
160
164
|
test('composable curried subgraph functions', ({ expect }) => {
|
|
161
165
|
// Simulates the curried pattern: addX(args) returns (builder) => void.
|
|
162
166
|
const addHeadingsSection = () => (builder: import('./builder').ActionGroupBuilder) => {
|
|
163
|
-
builder.group(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
builder.group(
|
|
168
|
+
'headings',
|
|
169
|
+
{ label: 'Headings', variant: 'dropdownMenu', icon: 'ph--text-h-one--regular' },
|
|
170
|
+
(group) => {
|
|
171
|
+
group
|
|
172
|
+
.action('h1', { label: 'H1', icon: 'ph--text-h-one--regular' }, () => {})
|
|
173
|
+
.action('h2', { label: 'H2', icon: 'ph--text-h-two--regular' }, () => {});
|
|
174
|
+
},
|
|
175
|
+
);
|
|
168
176
|
};
|
|
169
177
|
|
|
170
178
|
const addFormattingSection = () => (builder: import('./builder').ActionGroupBuilder) => {
|
|
@@ -206,6 +214,37 @@ describe('MenuBuilder', () => {
|
|
|
206
214
|
expect(graph.edges).toHaveLength(1);
|
|
207
215
|
});
|
|
208
216
|
|
|
217
|
+
test('menu creates overflow dropdown with built-in trigger', ({ expect }) => {
|
|
218
|
+
const graph = MenuBuilder.make()
|
|
219
|
+
.root({ label: 'toolbar' })
|
|
220
|
+
.menu('more', (group) => {
|
|
221
|
+
group.action('save', { label: 'Save', icon: 'ph--cloud-arrow-up--regular' }, () => {});
|
|
222
|
+
})
|
|
223
|
+
.build();
|
|
224
|
+
|
|
225
|
+
const moreGroup = graph.nodes.find((node) => node.id === 'more');
|
|
226
|
+
expect(moreGroup).toBeDefined();
|
|
227
|
+
expect(moreGroup!.properties).toEqual({
|
|
228
|
+
label: ['toolbar-overflow.menu', { ns: '@dxos/react-ui-menu' }],
|
|
229
|
+
icon: 'ph--dots-three-vertical--regular',
|
|
230
|
+
iconOnly: true,
|
|
231
|
+
variant: 'dropdownMenu',
|
|
232
|
+
caretDown: false,
|
|
233
|
+
});
|
|
234
|
+
expect(graph.edges).toContainEqual({ source: 'root', target: 'more', relation: 'child' });
|
|
235
|
+
expect(graph.edges).toContainEqual({ source: 'more', target: 'save', relation: 'child' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('menu accepts custom group id', ({ expect }) => {
|
|
239
|
+
const graph = MenuBuilder.make()
|
|
240
|
+
.root({ label: 'toolbar' })
|
|
241
|
+
.menu('overflow', () => {})
|
|
242
|
+
.build();
|
|
243
|
+
|
|
244
|
+
expect(graph.nodes.find((node) => node.id === 'overflow')).toBeDefined();
|
|
245
|
+
expect(graph.nodes.find((node) => node.id === 'more')).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
|
|
209
248
|
test('nodes have correct types', ({ expect }) => {
|
|
210
249
|
const graph = MenuBuilder.make()
|
|
211
250
|
.root({ label: 'toolbar' })
|
package/src/builder.ts
CHANGED
|
@@ -3,12 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { invariant } from '@dxos/invariant';
|
|
6
|
-
import type { MenuActionProperties, MenuItemGroupProperties } from '@dxos/ui-types';
|
|
6
|
+
import type { DropdownMenuItemGroupProperties, MenuActionProperties, MenuItemGroupProperties } from '@dxos/ui-types';
|
|
7
|
+
|
|
8
|
+
import { translationKey } from '#translations';
|
|
7
9
|
|
|
8
10
|
import type { ActionGraphProps } from './hooks';
|
|
9
11
|
import { MenuSeparatorType } from './types';
|
|
10
12
|
import { createMenuAction, createMenuItemGroup } from './util';
|
|
11
13
|
|
|
14
|
+
/** Default trigger for toolbar overflow (⋮) dropdown menus. */
|
|
15
|
+
const overflowMenuProperties = {
|
|
16
|
+
label: ['toolbar-overflow.menu', { ns: translationKey }],
|
|
17
|
+
icon: 'ph--dots-three-vertical--regular',
|
|
18
|
+
iconOnly: true,
|
|
19
|
+
caretDown: false,
|
|
20
|
+
variant: 'dropdownMenu',
|
|
21
|
+
} as const satisfies DropdownMenuItemGroupProperties;
|
|
22
|
+
|
|
12
23
|
/** Callback that populates an action group builder. */
|
|
13
24
|
export type ActionGroupBuilderFn = (builder: ActionGroupBuilder) => void;
|
|
14
25
|
|
|
@@ -30,8 +41,11 @@ export interface ActionGroupBuilder {
|
|
|
30
41
|
/** Add an action node as a child of the current group. */
|
|
31
42
|
action<P extends {} = {}>(id: string, props: P & MenuActionProperties, invoke: () => void): this;
|
|
32
43
|
|
|
44
|
+
/** Add a switch action rendered as a labeled Input.Switch. */
|
|
45
|
+
switch(id: string, props: Omit<MenuActionProperties, 'variant'> & { checked: boolean }, invoke: () => void): this;
|
|
46
|
+
|
|
33
47
|
/** Add a nested action group. */
|
|
34
|
-
group
|
|
48
|
+
group(id: string, props: MenuItemGroupProperties, cb: ActionGroupBuilderFn): this;
|
|
35
49
|
|
|
36
50
|
/** Merge pre-built nodes and edges, or build a subgraph via callback scoped to the current group. Falsy values are ignored. */
|
|
37
51
|
subgraph(subgraphOrCb: ActionGraphProps | ActionGroupBuilderFn | false | null | undefined): this;
|
|
@@ -42,10 +56,19 @@ export interface ActionGroupBuilder {
|
|
|
42
56
|
|
|
43
57
|
/** Top-level builder that creates the root group and produces the final action graph. */
|
|
44
58
|
export interface MenuBuilder extends ActionGroupBuilder {
|
|
45
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Set properties on the root menu group. May only be called once.
|
|
61
|
+
*/
|
|
46
62
|
root(props: MenuItemGroupProperties): this;
|
|
47
63
|
|
|
48
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Add a root-level toolbar overflow dropdown (⋮) with built-in trigger styling.
|
|
66
|
+
*/
|
|
67
|
+
menu(id: string, cb: ActionGroupBuilderFn): this;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Return the assembled action graph.
|
|
71
|
+
*/
|
|
49
72
|
build(): ActionGraphProps;
|
|
50
73
|
}
|
|
51
74
|
|
|
@@ -74,13 +97,21 @@ class MenuBuilderImpl implements MenuBuilder {
|
|
|
74
97
|
return this;
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
|
|
100
|
+
switch(id: string, props: Omit<MenuActionProperties, 'variant'> & { checked: boolean }, invoke: () => void): this {
|
|
101
|
+
return this.action(id, { ...props, variant: 'switch' }, invoke);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
group(id: string, props: MenuItemGroupProperties, cb: ActionGroupBuilderFn): this {
|
|
78
105
|
this._data.nodes.push(createMenuItemGroup(id, props));
|
|
79
106
|
this._data.edges.push({ source: this._rootId, target: id, relation: 'child' });
|
|
80
107
|
cb(new MenuBuilderImpl(this._data, id));
|
|
81
108
|
return this;
|
|
82
109
|
}
|
|
83
110
|
|
|
111
|
+
menu(id: string, cb: ActionGroupBuilderFn): this {
|
|
112
|
+
return this.group(id, overflowMenuProperties, cb);
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
subgraph(subgraphOrCb: ActionGraphProps | ActionGroupBuilderFn | false | null | undefined): this {
|
|
85
116
|
if (!subgraphOrCb) {
|
|
86
117
|
return this;
|
|
@@ -8,13 +8,13 @@ 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
10
|
import { mx } from '@dxos/ui-theme';
|
|
11
|
-
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
11
|
+
import { type MenuActionProperties, type MenuItemChrome } from '@dxos/ui-types';
|
|
12
12
|
|
|
13
13
|
import { translationKey } from '#translations';
|
|
14
14
|
|
|
15
15
|
import { getShortcut } from '../util';
|
|
16
16
|
|
|
17
|
-
type Action = Node.Action<MenuActionProperties> | Node.ActionGroup<
|
|
17
|
+
type Action = Node.Action<MenuActionProperties> | Node.ActionGroup<MenuItemChrome>;
|
|
18
18
|
|
|
19
19
|
export const ActionLabel = ({ action }: { action: Action }) => {
|
|
20
20
|
const { t } = useTranslation(translationKey);
|
package/src/components/Menu.tsx
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
6
|
import { type Scope, createContextScope } from '@radix-ui/react-context';
|
|
7
7
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
8
|
-
import React, {
|
|
8
|
+
import React, { type MouseEvent, type PropsWithChildren, createContext, useCallback, useContext, useMemo } from 'react';
|
|
9
9
|
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
11
|
import { type DropdownMenuRootProps, Icon, DropdownMenu as NaturalDropdownMenu } from '@dxos/react-ui';
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
type AddMenuItemsProps,
|
|
15
15
|
type MenuAction,
|
|
16
16
|
type MenuContextValue,
|
|
17
|
+
type MenuGroupContext,
|
|
17
18
|
type MenuItem,
|
|
18
|
-
type MenuItemGroup,
|
|
19
19
|
type MenuItems,
|
|
20
20
|
type MenuItemsAccessor,
|
|
21
21
|
type MenuItemsMap,
|
|
@@ -70,12 +70,7 @@ const MenuDropdownContext = createContext<MenuDropdownContextValue>({
|
|
|
70
70
|
const DEFAULT_PRIORITY = 100;
|
|
71
71
|
|
|
72
72
|
const sortMenuItems = (items: MenuItems[]) =>
|
|
73
|
-
[...items].sort((a, b) =>
|
|
74
|
-
if (a.priority !== b.priority) {
|
|
75
|
-
return a.priority - b.priority;
|
|
76
|
-
}
|
|
77
|
-
return a.id.localeCompare(b.id);
|
|
78
|
-
});
|
|
73
|
+
[...items].sort((a, b) => (a.priority !== b.priority ? a.priority - b.priority : a.id.localeCompare(b.id)));
|
|
79
74
|
|
|
80
75
|
type MenuProviderProps = PropsWithChildren<Partial<MenuContextValue>>;
|
|
81
76
|
|
|
@@ -135,7 +130,7 @@ const MenuProvider = ({
|
|
|
135
130
|
|
|
136
131
|
const resolveItems = (
|
|
137
132
|
baseItems: MenuItem[] | null,
|
|
138
|
-
group:
|
|
133
|
+
group: MenuGroupContext | undefined,
|
|
139
134
|
entries: ReadonlyMap<string, MenuItems>,
|
|
140
135
|
): MenuItem[] | null => {
|
|
141
136
|
const applicable = [...entries.values()].filter((entry) => !entry.groupFilter || entry.groupFilter(group));
|
|
@@ -171,7 +166,7 @@ const resolveItems = (
|
|
|
171
166
|
//
|
|
172
167
|
|
|
173
168
|
const useMenuItems = (
|
|
174
|
-
group?:
|
|
169
|
+
group?: MenuGroupContext,
|
|
175
170
|
propsItems?: MenuItem[],
|
|
176
171
|
consumerName: string = 'useMenuItemConsumer',
|
|
177
172
|
__menuScope?: Scope,
|
|
@@ -238,7 +233,7 @@ const MenuRoot = ({ children, open, defaultOpen, onOpenChange, caller, ...props
|
|
|
238
233
|
//
|
|
239
234
|
|
|
240
235
|
type MenuContentProps = {
|
|
241
|
-
group?:
|
|
236
|
+
group?: MenuGroupContext;
|
|
242
237
|
items?: MenuItem[];
|
|
243
238
|
caller?: string;
|
|
244
239
|
};
|
|
@@ -339,10 +334,10 @@ export { Menu, menuContextDefaults, useMenu, useMenuItems, useMenuScoped };
|
|
|
339
334
|
export type { MenuContentProps, MenuRootProps, MenuScopedProps };
|
|
340
335
|
|
|
341
336
|
export type {
|
|
342
|
-
ToolbarMenuProps,
|
|
343
337
|
ToolbarMenuActionGroupProperties,
|
|
344
338
|
ToolbarMenuActionGroupProps,
|
|
345
339
|
ToolbarMenuActionProps,
|
|
346
340
|
ToolbarMenuDropdownMenuActionGroup,
|
|
341
|
+
ToolbarMenuProps,
|
|
347
342
|
ToolbarMenuToggleGroupActionGroup,
|
|
348
343
|
} from './ToolbarMenu';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { Atom, RegistryContext } from '@effect-atom/atom-react';
|
|
6
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
|
-
import React, { useContext, useMemo } from 'react';
|
|
7
|
+
import React, { useContext, useMemo, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { random } from '@dxos/random';
|
|
10
10
|
import { IconButton } from '@dxos/react-ui';
|
|
@@ -13,8 +13,9 @@ import { withRegistry } from '@dxos/storybook-utils';
|
|
|
13
13
|
|
|
14
14
|
import { translations } from '#translations';
|
|
15
15
|
|
|
16
|
+
import { MenuBuilder } from '../builder';
|
|
16
17
|
import { Menu } from '../components';
|
|
17
|
-
import { type ActionGraphProps, useMenuActions } from '../hooks';
|
|
18
|
+
import { type ActionGraphProps, useMenuActions, useMenuBuilder } from '../hooks';
|
|
18
19
|
import { createActions, createNestedActions, createNestedActionsResolver, useMutateActions } from '../testing';
|
|
19
20
|
|
|
20
21
|
random.seed(1234);
|
|
@@ -93,3 +94,31 @@ export const UseMenuActionsToolbar: Story = {
|
|
|
93
94
|
);
|
|
94
95
|
},
|
|
95
96
|
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Toolbar with both labeled and tooltip-only (iconOnly) Input.Switch items.
|
|
100
|
+
*/
|
|
101
|
+
export const SwitchToolbar: Story = {
|
|
102
|
+
render: () => {
|
|
103
|
+
const [wordWrap, setWordWrap] = useState(false);
|
|
104
|
+
const [lineNumbers, setLineNumbers] = useState(true);
|
|
105
|
+
|
|
106
|
+
const menuActions = useMenuBuilder(
|
|
107
|
+
() =>
|
|
108
|
+
MenuBuilder.make()
|
|
109
|
+
.root({ label: 'Editor settings' })
|
|
110
|
+
.switch('word-wrap', { label: 'Word wrap', checked: wordWrap }, () => setWordWrap((v) => !v))
|
|
111
|
+
.switch('line-numbers', { label: 'Line numbers', iconOnly: true, checked: lineNumbers }, () =>
|
|
112
|
+
setLineNumbers((v) => !v),
|
|
113
|
+
)
|
|
114
|
+
.build(),
|
|
115
|
+
[wordWrap, lineNumbers],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Menu.Root {...menuActions} alwaysActive>
|
|
120
|
+
<Menu.Toolbar />
|
|
121
|
+
</Menu.Root>
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -4,42 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
Input,
|
|
9
|
+
Toolbar as NaturalToolbar,
|
|
10
|
+
type ToolbarRootProps,
|
|
11
|
+
Tooltip,
|
|
12
|
+
toLocalizedString,
|
|
13
|
+
useTranslation,
|
|
14
|
+
} from '@dxos/react-ui';
|
|
8
15
|
import { composable, composableProps } from '@dxos/react-ui';
|
|
9
16
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
10
|
-
import { type
|
|
17
|
+
import { type DropdownMenuItemGroupProperties, type ToggleGroupMenuItemGroupProperties } from '@dxos/ui-types';
|
|
11
18
|
|
|
12
19
|
import { translationKey } from '#translations';
|
|
13
20
|
|
|
14
|
-
import {
|
|
15
|
-
type MenuAction,
|
|
16
|
-
type MenuItem,
|
|
17
|
-
type MenuItemGroup,
|
|
18
|
-
type MenuMultipleSelectActionGroup,
|
|
19
|
-
type MenuSingleSelectActionGroup,
|
|
20
|
-
isMenuGroup,
|
|
21
|
-
isSeparator,
|
|
22
|
-
} from '../types';
|
|
21
|
+
import { type MenuAction, type MenuItem, type MenuItemGroup, isMenuGroup, isSeparator } from '../types';
|
|
23
22
|
import { executeMenuAction } from '../util';
|
|
24
23
|
import { ActionLabel, actionLabel } from './ActionLabel';
|
|
25
24
|
import { DropdownMenu } from './DropdownMenu';
|
|
26
25
|
import { type MenuScopedProps, useMenuItems, useMenuScoped } from './Menu';
|
|
27
26
|
|
|
28
|
-
export type ToolbarMenuDropdownMenuActionGroup =
|
|
29
|
-
variant: 'dropdownMenu';
|
|
30
|
-
icon: string;
|
|
31
|
-
applyActive?: boolean;
|
|
32
|
-
};
|
|
27
|
+
export type ToolbarMenuDropdownMenuActionGroup = DropdownMenuItemGroupProperties;
|
|
33
28
|
|
|
34
|
-
export type ToolbarMenuToggleGroupActionGroup =
|
|
35
|
-
variant: 'toggleGroup';
|
|
36
|
-
};
|
|
29
|
+
export type ToolbarMenuToggleGroupActionGroup = ToggleGroupMenuItemGroupProperties;
|
|
37
30
|
|
|
38
|
-
export type ToolbarMenuActionGroupProperties =
|
|
39
|
-
| ToolbarMenuDropdownMenuActionGroup
|
|
40
|
-
| ToolbarMenuToggleGroupActionGroup
|
|
41
|
-
) &
|
|
42
|
-
(MenuSingleSelectActionGroup | MenuMultipleSelectActionGroup);
|
|
31
|
+
export type ToolbarMenuActionGroupProperties = DropdownMenuItemGroupProperties | ToggleGroupMenuItemGroupProperties;
|
|
43
32
|
|
|
44
33
|
export type ToolbarMenuProps = ToolbarRootProps;
|
|
45
34
|
|
|
@@ -48,8 +37,18 @@ export type ToolbarMenuActionGroupProps = {
|
|
|
48
37
|
items?: MenuItem[];
|
|
49
38
|
};
|
|
50
39
|
|
|
40
|
+
export type ToolbarMenuDropdownGroupProps = {
|
|
41
|
+
group: MenuItemGroup<DropdownMenuItemGroupProperties>;
|
|
42
|
+
items?: MenuItem[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ToolbarMenuToggleGroupProps = {
|
|
46
|
+
group: MenuItemGroup<ToggleGroupMenuItemGroupProperties>;
|
|
47
|
+
items?: MenuItem[];
|
|
48
|
+
};
|
|
49
|
+
|
|
51
50
|
export type ToolbarMenuActionProps = {
|
|
52
|
-
group: MenuItemGroup<
|
|
51
|
+
group: MenuItemGroup<ToggleGroupMenuItemGroupProperties>;
|
|
53
52
|
action: MenuAction;
|
|
54
53
|
};
|
|
55
54
|
|
|
@@ -98,50 +97,100 @@ const ActionToolbarItem = ({ __menuScope, action }: MenuScopedProps<{ action: Me
|
|
|
98
97
|
);
|
|
99
98
|
};
|
|
100
99
|
|
|
100
|
+
const SwitchToolbarItem = ({ __menuScope, action }: MenuScopedProps<{ action: MenuAction }>) => {
|
|
101
|
+
const { onAction } = useMenuScoped('SwitchToolbarItem', __menuScope);
|
|
102
|
+
const { t } = useTranslation(translationKey);
|
|
103
|
+
const { label, iconOnly, disabled, testId, hidden, checked } = action.properties;
|
|
104
|
+
const labelStr = toLocalizedString(label, t);
|
|
105
|
+
|
|
106
|
+
const handleCheckedChange = useCallback(() => {
|
|
107
|
+
if (onAction) {
|
|
108
|
+
onAction(action, {});
|
|
109
|
+
} else {
|
|
110
|
+
void executeMenuAction(action);
|
|
111
|
+
}
|
|
112
|
+
}, [action, onAction]);
|
|
113
|
+
|
|
114
|
+
if (hidden) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const switchInput = (
|
|
119
|
+
<Input.Switch
|
|
120
|
+
checked={checked}
|
|
121
|
+
disabled={disabled}
|
|
122
|
+
aria-label={iconOnly ? labelStr : undefined}
|
|
123
|
+
onCheckedChange={handleCheckedChange}
|
|
124
|
+
{...(testId && { 'data-testid': testId })}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Input.Root>
|
|
130
|
+
{iconOnly ? (
|
|
131
|
+
<Tooltip.Trigger asChild content={labelStr}>
|
|
132
|
+
<Input.Block>{switchInput}</Input.Block>
|
|
133
|
+
</Tooltip.Trigger>
|
|
134
|
+
) : (
|
|
135
|
+
<Input.Block>{switchInput}</Input.Block>
|
|
136
|
+
)}
|
|
137
|
+
{!iconOnly && <Input.Label>{labelStr}</Input.Label>}
|
|
138
|
+
</Input.Root>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
101
142
|
const DropdownMenuToolbarItem = ({
|
|
102
143
|
__menuScope,
|
|
103
144
|
group,
|
|
104
145
|
items: propsItems,
|
|
105
|
-
}: MenuScopedProps<
|
|
106
|
-
const { iconOnly, disabled, testId } = group.properties;
|
|
146
|
+
}: MenuScopedProps<ToolbarMenuDropdownGroupProps>) => {
|
|
107
147
|
const { t } = useTranslation(translationKey);
|
|
108
148
|
const { iconSize } = useMenuScoped('DropdownMenuToolbarItem', __menuScope);
|
|
109
149
|
const items = useMenuItems(group, propsItems, 'DropdownMenuToolbarItem', __menuScope);
|
|
110
|
-
const
|
|
150
|
+
const {
|
|
151
|
+
iconOnly,
|
|
152
|
+
disabled,
|
|
153
|
+
testId,
|
|
154
|
+
applyActive,
|
|
155
|
+
caretDown = true,
|
|
156
|
+
icon: groupIcon,
|
|
157
|
+
iconClassNames: groupIconClassNames,
|
|
158
|
+
} = group.properties;
|
|
159
|
+
const activeItem = items?.find((item) => !!(item as MenuAction).properties.checked) as MenuAction | undefined;
|
|
111
160
|
const icon =
|
|
112
|
-
(
|
|
161
|
+
(applyActive &&
|
|
113
162
|
// TODO(thure): Handle other menu item types.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Follow the same `applyActive` rule for `iconClassNames` so a per-item
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
((group.properties as any).applyActive && (activeItem as MenuAction)?.properties.iconClassNames) ||
|
|
120
|
-
(group.properties as { iconClassNames?: any }).iconClassNames;
|
|
121
|
-
const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
|
|
122
|
-
const labelAction = (group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group;
|
|
123
|
-
const rootProps = icon
|
|
124
|
-
? {
|
|
125
|
-
icon,
|
|
126
|
-
size: iconSize,
|
|
127
|
-
iconOnly,
|
|
128
|
-
iconClassNames,
|
|
129
|
-
label: actionLabel(labelAction, t),
|
|
130
|
-
caretDown: true,
|
|
131
|
-
}
|
|
132
|
-
: {
|
|
133
|
-
children: (
|
|
134
|
-
<>
|
|
135
|
-
<ActionLabel action={labelAction} />
|
|
136
|
-
<Icon size={3} icon='ph--caret-down--bold' classNames='ms-1' />
|
|
137
|
-
</>
|
|
138
|
-
),
|
|
139
|
-
};
|
|
163
|
+
activeItem?.properties.icon) ||
|
|
164
|
+
groupIcon;
|
|
165
|
+
// Follow the same `applyActive` rule for `iconClassNames` so a per-item accent (e.g. tag colour) tracks the displayed icon.
|
|
166
|
+
const iconClassNames = (applyActive && activeItem?.properties.iconClassNames) || groupIconClassNames;
|
|
167
|
+
const labelAction = applyActive && activeItem ? activeItem : group;
|
|
140
168
|
|
|
141
169
|
return (
|
|
142
170
|
<DropdownMenu.Root group={group} items={items}>
|
|
143
171
|
<DropdownMenu.Trigger asChild>
|
|
144
|
-
|
|
172
|
+
{icon ? (
|
|
173
|
+
<NaturalToolbar.IconButton
|
|
174
|
+
variant='ghost'
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
icon={icon}
|
|
177
|
+
size={iconSize}
|
|
178
|
+
iconOnly={iconOnly}
|
|
179
|
+
iconClassNames={iconClassNames}
|
|
180
|
+
label={actionLabel(labelAction, t)}
|
|
181
|
+
caretDown={caretDown}
|
|
182
|
+
{...(testId && { 'data-testid': testId })}
|
|
183
|
+
/>
|
|
184
|
+
) : (
|
|
185
|
+
<NaturalToolbar.Button
|
|
186
|
+
variant='ghost'
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
caretDown={caretDown}
|
|
189
|
+
{...(testId && { 'data-testid': testId })}
|
|
190
|
+
>
|
|
191
|
+
<ActionLabel action={labelAction} />
|
|
192
|
+
</NaturalToolbar.Button>
|
|
193
|
+
)}
|
|
145
194
|
</DropdownMenu.Trigger>
|
|
146
195
|
</DropdownMenu.Root>
|
|
147
196
|
);
|
|
@@ -189,7 +238,7 @@ const ToggleGroupToolbarItem = ({
|
|
|
189
238
|
__menuScope,
|
|
190
239
|
group,
|
|
191
240
|
items: itemsProp,
|
|
192
|
-
}: MenuScopedProps<
|
|
241
|
+
}: MenuScopedProps<ToolbarMenuToggleGroupProps>) => {
|
|
193
242
|
const items = useMenuItems(group, itemsProp, 'ToggleGroupToolbarItem', __menuScope);
|
|
194
243
|
const { selectCardinality } = group.properties;
|
|
195
244
|
|
|
@@ -244,18 +293,28 @@ const ToolbarMenuItem = ({ __menuScope, item }: MenuScopedProps<{ item: MenuItem
|
|
|
244
293
|
return (
|
|
245
294
|
<DropdownMenuToolbarItem
|
|
246
295
|
__menuScope={__menuScope}
|
|
247
|
-
group={item as MenuItemGroup<
|
|
248
|
-
/>
|
|
249
|
-
);
|
|
250
|
-
} else {
|
|
251
|
-
return (
|
|
252
|
-
<ToggleGroupToolbarItem
|
|
253
|
-
__menuScope={__menuScope}
|
|
254
|
-
group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>}
|
|
296
|
+
group={item as MenuItemGroup<DropdownMenuItemGroupProperties>}
|
|
255
297
|
/>
|
|
256
298
|
);
|
|
257
299
|
}
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<ToggleGroupToolbarItem
|
|
303
|
+
__menuScope={__menuScope}
|
|
304
|
+
group={item as MenuItemGroup<ToggleGroupMenuItemGroupProperties>}
|
|
305
|
+
/>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const action = item as MenuAction;
|
|
310
|
+
if (action.properties?.variant === 'switch') {
|
|
311
|
+
return <SwitchToolbarItem __menuScope={__menuScope} action={action} />;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// The contributor owns the rendered element (interactions the action model cannot express).
|
|
315
|
+
if (action.properties?.variant === 'custom' && action.properties.render) {
|
|
316
|
+
return <>{action.properties.render()}</>;
|
|
258
317
|
}
|
|
259
318
|
|
|
260
|
-
return <ActionToolbarItem __menuScope={__menuScope} action={
|
|
319
|
+
return <ActionToolbarItem __menuScope={__menuScope} action={action} />;
|
|
261
320
|
};
|