@dxos/plugin-navtree 0.7.4 → 0.7.5-main.937ce75

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.
Files changed (37) hide show
  1. package/dist/lib/browser/{chunk-PSVCPL4T.mjs → chunk-VAXG4OAG.mjs} +3 -1
  2. package/dist/lib/browser/chunk-VAXG4OAG.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +165 -352
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/meta.mjs +3 -1
  7. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  8. package/dist/types/src/components/CommandsTrigger.d.ts.map +1 -1
  9. package/dist/types/src/components/NavTree.d.ts +2 -2
  10. package/dist/types/src/components/NavTree.d.ts.map +1 -1
  11. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  12. package/dist/types/src/components/NavTreeFooter.d.ts.map +1 -1
  13. package/dist/types/src/components/NavTreeItemAction.d.ts +4 -8
  14. package/dist/types/src/components/NavTreeItemAction.d.ts.map +1 -1
  15. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +0 -5
  16. package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +1 -1
  17. package/dist/types/src/meta.d.ts +1 -0
  18. package/dist/types/src/meta.d.ts.map +1 -1
  19. package/dist/types/src/translations.d.ts +0 -1
  20. package/dist/types/src/translations.d.ts.map +1 -1
  21. package/dist/types/src/types.d.ts +1 -1
  22. package/dist/types/src/types.d.ts.map +1 -1
  23. package/dist/types/tsconfig.tsbuildinfo +1 -0
  24. package/package.json +30 -29
  25. package/src/NavTreePlugin.tsx +67 -62
  26. package/src/components/CommandsDialog.tsx +3 -3
  27. package/src/components/CommandsTrigger.tsx +10 -7
  28. package/src/components/NavTree.tsx +6 -11
  29. package/src/components/NavTreeContainer.tsx +32 -54
  30. package/src/components/NavTreeFooter.tsx +3 -2
  31. package/src/components/NavTreeItemAction.stories.tsx +0 -2
  32. package/src/components/NavTreeItemAction.tsx +53 -316
  33. package/src/components/NotchStart.tsx +4 -4
  34. package/src/meta.ts +2 -0
  35. package/src/translations.ts +0 -1
  36. package/src/types.ts +1 -1
  37. package/dist/lib/browser/chunk-PSVCPL4T.mjs.map +0 -7
@@ -5,24 +5,27 @@
5
5
  import { MagnifyingGlass } from '@phosphor-icons/react';
6
6
  import React from 'react';
7
7
 
8
- import { LayoutAction, useIntent } from '@dxos/app-framework';
8
+ import { createIntent, LayoutAction, useIntentDispatcher } from '@dxos/app-framework';
9
9
  import { Button, useTranslation } from '@dxos/react-ui';
10
10
  import { getSize } from '@dxos/react-ui-theme';
11
11
 
12
- import { NAVTREE_PLUGIN } from '../meta';
12
+ import { COMMANDS_DIALOG, NAVTREE_PLUGIN } from '../meta';
13
13
 
14
14
  // TODO(thure): Refactor to be handled by a more appropriate plugin.
15
15
  export const CommandsTrigger = () => {
16
- const { dispatch } = useIntent();
16
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
17
17
  const { t } = useTranslation(NAVTREE_PLUGIN);
18
18
  return (
19
19
  <Button
20
20
  classNames='w-full h-full !rounded-none'
21
21
  onClick={() =>
22
- dispatch({
23
- action: LayoutAction.SET_LAYOUT,
24
- data: { element: 'dialog', component: `${NAVTREE_PLUGIN}/Commands`, dialogBlockAlign: 'start' },
25
- })
22
+ dispatch(
23
+ createIntent(LayoutAction.SetLayout, {
24
+ element: 'dialog',
25
+ component: COMMANDS_DIALOG,
26
+ dialogBlockAlign: 'start',
27
+ }),
28
+ )
26
29
  }
27
30
  >
28
31
  <span className='text-description font-normal grow text-start'>{t('command list input placeholder')}</span>
@@ -23,14 +23,13 @@ export type NavTreeProps = Omit<
23
23
 
24
24
  export const NavTree = ({ getActions, loadDescendents, renderItemEnd, popoverAnchorId, ...props }: NavTreeProps) => {
25
25
  const renderColumns = useCallback<NonNullable<TreeProps<NavTreeItemGraphNode>['renderColumns']>>(
26
- ({ item, path, menuOpen, setMenuOpen }) => {
26
+ ({ item, path, open }) => {
27
27
  return (
28
28
  <NavTreeColumns
29
29
  path={path}
30
30
  node={item}
31
31
  getActions={getActions}
32
- menuOpen={menuOpen}
33
- setMenuOpen={setMenuOpen}
32
+ open={open}
34
33
  loadDescendents={loadDescendents}
35
34
  renderItemEnd={renderItemEnd}
36
35
  popoverAnchorId={popoverAnchorId}
@@ -54,10 +53,9 @@ type NavTreeColumnsProps = {
54
53
  path: string[];
55
54
  node: Node;
56
55
  getActions: (node: Node) => FlattenedActions;
57
- menuOpen: boolean;
58
- setMenuOpen: (open: boolean) => void;
56
+ open: boolean;
59
57
  loadDescendents?: (node: Node) => MaybePromise<void>;
60
- renderItemEnd?: FC<{ node: Node }>;
58
+ renderItemEnd?: FC<{ node: Node; open: boolean }>;
61
59
  popoverAnchorId?: string;
62
60
  };
63
61
 
@@ -65,8 +63,7 @@ const NavTreeColumns = ({
65
63
  path,
66
64
  node,
67
65
  getActions,
68
- menuOpen,
69
- setMenuOpen,
66
+ open,
70
67
  loadDescendents,
71
68
  renderItemEnd: ItemEnd,
72
69
  popoverAnchorId,
@@ -123,14 +120,12 @@ const NavTreeColumns = ({
123
120
  menuActions={actions}
124
121
  menuType='dropdown'
125
122
  caller={NAV_TREE_ITEM}
126
- menuOpen={menuOpen}
127
- onChangeMenuOpen={setMenuOpen}
128
123
  />
129
124
  ) : (
130
125
  <Treegrid.Cell />
131
126
  )}
132
127
  </ActionRoot>
133
- {ItemEnd && <ItemEnd node={node} />}
128
+ {ItemEnd && <ItemEnd node={node} open={open} />}
134
129
  </>
135
130
  );
136
131
  };
@@ -7,11 +7,11 @@ import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-a
7
7
  import { untracked } from '@preact/signals-core';
8
8
  import React, { memo, useCallback, useEffect } from 'react';
9
9
 
10
- import { LayoutAction, NavigationAction, Surface, useIntentDispatcher } from '@dxos/app-framework';
10
+ import { createIntent, LayoutAction, NavigationAction, Surface, useIntentDispatcher } from '@dxos/app-framework';
11
11
  import { isAction, isActionLike, type Node } from '@dxos/app-graph';
12
12
  import { useGraph } from '@dxos/plugin-graph';
13
13
  import { isEchoObject, isSpace } from '@dxos/react-client/echo';
14
- import { ElevationProvider, useMediaQuery, useSidebars } from '@dxos/react-ui';
14
+ import { useMediaQuery, useSidebars } from '@dxos/react-ui';
15
15
  import { isTreeData, type TreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
16
16
  import { mx } from '@dxos/react-ui-theme';
17
17
  import { arrayMove } from '@dxos/util';
@@ -32,8 +32,8 @@ import {
32
32
  // TODO(thure): Is NavTree truly authoritative in this regard?
33
33
  export const NODE_TYPE = 'dxos/app-graph/node';
34
34
 
35
- const renderItemEnd = ({ node }: { node: Node }) => (
36
- <Surface role='navtree-item-end' data={{ object: node.data, id: node.id }} />
35
+ const renderItemEnd = ({ node, open }: { node: Node; open: boolean }) => (
36
+ <Surface role='navtree-item-end' data={{ id: node.id, subject: node.data, open }} limit={1} />
37
37
  );
38
38
 
39
39
  export type NavTreeContainerProps = {
@@ -43,7 +43,7 @@ export type NavTreeContainerProps = {
43
43
  export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }: NavTreeContainerProps) => {
44
44
  const { closeNavigationSidebar } = useSidebars(NAVTREE_PLUGIN);
45
45
  const [isLg] = useMediaQuery('lg', { ssr: false });
46
- const dispatch = useIntentDispatcher();
46
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
47
47
  const { graph } = useGraph();
48
48
 
49
49
  const getActions = useCallback((node: Node) => naturalGetActions(graph, node), [graph]);
@@ -124,28 +124,11 @@ export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }:
124
124
 
125
125
  const current = isCurrent(path, node);
126
126
  if (!current) {
127
- void dispatch({
128
- action: NavigationAction.OPEN,
129
- data: {
130
- activeParts: {
131
- main: [node.id],
132
- },
133
- },
134
- });
127
+ void dispatch(createIntent(NavigationAction.Open, { activeParts: { main: [node.id] } }));
135
128
  } else if (option) {
136
- void dispatch({
137
- action: NavigationAction.CLOSE,
138
- data: {
139
- activeParts: {
140
- main: [node.id],
141
- },
142
- },
143
- });
129
+ void dispatch(createIntent(NavigationAction.Close, { activeParts: { main: [node.id] } }));
144
130
  } else {
145
- void dispatch({
146
- action: LayoutAction.SCROLL_INTO_VIEW,
147
- data: { id: node.id },
148
- });
131
+ void dispatch(createIntent(LayoutAction.ScrollIntoView, { id: node.id }));
149
132
  }
150
133
 
151
134
  const defaultAction = graph.actions(node).find((action) => action.properties?.disposition === 'default');
@@ -220,35 +203,30 @@ export const NavTreeContainer = memo(({ isCurrent, popoverAnchorId, ...props }:
220
203
  }, [graph]);
221
204
 
222
205
  return (
223
- <ElevationProvider elevation='chrome'>
224
- <div
225
- role='none'
226
- className='grid grid-cols-1 grid-rows-[var(--rail-size)_1fr_min-content] bs-full overflow-hidden'
227
- >
228
- {/* TODO(wittjosiah): Factor out surfaces to layout? */}
229
- <div role='none' className='border-be border-separator'>
230
- <Surface role='search-input' limit={1} />
231
- </div>
232
-
233
- {/* TODO(thure): What gives this an inline `overflow: initial`? */}
234
- <div role='none' className='border-be border-separator !overflow-y-auto'>
235
- <NavTree
236
- id={graph.root.id}
237
- getActions={getActions}
238
- getItems={getItems}
239
- getProps={getProps}
240
- isCurrent={isCurrent}
241
- loadDescendents={loadDescendents}
242
- renderItemEnd={renderItemEnd}
243
- popoverAnchorId={popoverAnchorId}
244
- canDrop={canDrop}
245
- onSelect={handleSelect}
246
- {...props}
247
- />
248
- </div>
249
-
250
- <NavTreeFooter />
206
+ <div role='none' className='grid grid-cols-1 grid-rows-[var(--rail-size)_1fr_min-content] bs-full overflow-hidden'>
207
+ {/* TODO(wittjosiah): Factor out surfaces to layout? */}
208
+ <div role='none' className='border-be border-separator'>
209
+ <Surface role='search-input' limit={1} />
251
210
  </div>
252
- </ElevationProvider>
211
+
212
+ {/* TODO(thure): What gives this an inline `overflow: initial`? */}
213
+ <div role='none' className='border-be border-separator !overflow-y-auto'>
214
+ <NavTree
215
+ id={graph.root.id}
216
+ getActions={getActions}
217
+ getItems={getItems}
218
+ getProps={getProps}
219
+ isCurrent={isCurrent}
220
+ loadDescendents={loadDescendents}
221
+ renderItemEnd={renderItemEnd}
222
+ popoverAnchorId={popoverAnchorId}
223
+ canDrop={canDrop}
224
+ onSelect={handleSelect}
225
+ {...props}
226
+ />
227
+ </div>
228
+
229
+ <NavTreeFooter />
230
+ </div>
253
231
  );
254
232
  });
@@ -12,6 +12,7 @@ import {
12
12
  parseNavigationPlugin,
13
13
  useResolvePlugin,
14
14
  useIntentDispatcher,
15
+ createIntent,
15
16
  } from '@dxos/app-framework';
16
17
  import { useConfig } from '@dxos/react-client';
17
18
  import {
@@ -41,7 +42,7 @@ export const NavTreeFooter = (props: { layoutPart?: LayoutPart }) => {
41
42
  const config = useConfig();
42
43
  const { t } = useTranslation(NAVTREE_PLUGIN);
43
44
  const { navigationSidebarOpen } = useSidebars(NAVTREE_PLUGIN);
44
- const dispatch = useIntentDispatcher();
45
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
45
46
  const { version, timestamp, commitHash } = config.values.runtime?.app?.build ?? {};
46
47
  const navigationPlugin = useResolvePlugin(parseNavigationPlugin);
47
48
  const [_, v] = version?.match(VERSION_REGEX) ?? [];
@@ -126,7 +127,7 @@ export const NavTreeFooter = (props: { layoutPart?: LayoutPart }) => {
126
127
  data-testid='treeView.openSettings'
127
128
  data-joyride='welcome/settings'
128
129
  {...(!navigationSidebarOpen && { tabIndex: -1 })}
129
- onClick={() => dispatch({ action: SettingsAction.OPEN })}
130
+ onClick={() => dispatch(createIntent(SettingsAction.Open))}
130
131
  >
131
132
  <span className='sr-only'>{t('open settings label')}</span>
132
133
  <GearSix className={mx(getSize(5), 'rotate-90')} />
@@ -53,5 +53,3 @@ export default {
53
53
  };
54
54
 
55
55
  export const Default = {};
56
-
57
- export const SearchList = { args: { menuType: 'searchList' } };
@@ -2,24 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { useControllableState } from '@radix-ui/react-use-controllable-state';
6
- import React, { type MutableRefObject, type PropsWithChildren, useRef, useState } from 'react';
5
+ import React, { type MutableRefObject, useRef } from 'react';
7
6
 
8
- import { type Action, type ActionLike, type Node } from '@dxos/app-graph';
9
- import { keySymbols } from '@dxos/keyboard';
10
- import {
11
- Button,
12
- Dialog,
13
- DropdownMenu,
14
- ContextMenu,
15
- Tooltip,
16
- useTranslation,
17
- toLocalizedString,
18
- Icon,
19
- } from '@dxos/react-ui';
20
- import { SearchList } from '@dxos/react-ui-searchlist';
21
- import { descriptionText, hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
22
- import { getHostPlatform } from '@dxos/util';
7
+ import { type Action, type Node } from '@dxos/app-graph';
8
+ import { useTranslation, toLocalizedString, IconButton } from '@dxos/react-ui';
9
+ import { DropdownMenu, type MenuAction } from '@dxos/react-ui-menu';
10
+ import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
23
11
 
24
12
  import { NAVTREE_PLUGIN } from '../meta';
25
13
  import { type ActionProperties } from '../types';
@@ -30,338 +18,87 @@ export type NavTreeItemActionMenuProps = ActionProperties & {
30
18
  monolithic?: boolean;
31
19
  menuActions?: Action[];
32
20
  suppressNextTooltip?: MutableRefObject<boolean>;
33
- menuOpen?: boolean;
34
- defaultMenuOpen?: boolean;
35
- onChangeMenuOpen?: (nextOpen: boolean) => void;
36
21
  onAction?: (action: Action) => void;
37
22
  };
38
23
 
39
- const getShortcut = (action: ActionLike) => {
40
- return typeof action.properties?.keyBinding === 'string'
41
- ? action.properties.keyBinding
42
- : action.properties?.keyBinding?.[getHostPlatform()];
43
- };
44
-
45
24
  const fallbackIcon = 'ph--placeholder--regular';
46
25
 
26
+ const actionButtonProps = {
27
+ iconOnly: true,
28
+ size: 4 as const,
29
+ variant: 'ghost' as const,
30
+ density: 'fine' as const,
31
+ classNames: mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem),
32
+ };
33
+
47
34
  export const NavTreeItemActionDropdownMenu = ({
48
35
  label,
49
36
  icon,
50
37
  testId,
51
- defaultMenuOpen,
52
38
  menuActions,
53
- menuOpen,
54
- suppressNextTooltip,
55
- onChangeMenuOpen,
56
39
  onAction,
57
40
  }: NavTreeItemActionMenuProps) => {
58
41
  const { t } = useTranslation(NAVTREE_PLUGIN);
59
-
60
- const [optionsMenuOpen, setOptionsMenuOpen] = useControllableState({
61
- prop: menuOpen,
62
- defaultProp: defaultMenuOpen,
63
- onChange: onChangeMenuOpen,
64
- });
42
+ const suppressNextTooltip = useRef<boolean>(false);
65
43
 
66
44
  return (
67
45
  <DropdownMenu.Root
68
- {...{
69
- open: optionsMenuOpen,
70
- onOpenChange: (nextOpen: boolean) => {
71
- if (!nextOpen && suppressNextTooltip) {
72
- suppressNextTooltip.current = true;
73
- }
74
- return setOptionsMenuOpen(nextOpen);
75
- },
76
- }}
46
+ actions={menuActions as MenuAction[]}
47
+ onAction={onAction}
48
+ suppressNextTooltip={suppressNextTooltip}
77
49
  >
78
- <Tooltip.Trigger asChild>
79
- <DropdownMenu.Trigger asChild>
80
- <Button
81
- variant='ghost'
82
- density='fine'
83
- classNames={mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem)}
84
- data-testid={testId}
85
- aria-label={t('tree item actions label')}
86
- >
87
- <span className='sr-only'>{toLocalizedString(label, t)}</span>
88
- <Icon icon={icon ?? fallbackIcon} size={4} />
89
- </Button>
90
- </DropdownMenu.Trigger>
91
- </Tooltip.Trigger>
92
- <DropdownMenu.Portal>
93
- <DropdownMenu.Content classNames='z-[31]'>
94
- <DropdownMenu.Viewport>
95
- {menuActions?.map((action) => {
96
- const shortcut = getShortcut(action);
97
- return (
98
- <DropdownMenu.Item
99
- key={action.id}
100
- onClick={(event) => {
101
- if (action.properties?.disabled) {
102
- return;
103
- }
104
- event.stopPropagation();
105
- // TODO(thure): Why does Dialog’s modal-ness cause issues if we don’t explicitly close the menu here?
106
- if (suppressNextTooltip) {
107
- suppressNextTooltip.current = true;
108
- }
109
- setOptionsMenuOpen(false);
110
- onAction?.(action);
111
- }}
112
- classNames='gap-2'
113
- disabled={action.properties?.disabled}
114
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
115
- >
116
- {action.properties?.icon && <Icon icon={action.properties!.icon} size={4} />}
117
- <span className='grow truncate'>{toLocalizedString(action.properties!.label, t)}</span>
118
- {shortcut && <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>}
119
- </DropdownMenu.Item>
120
- );
121
- })}
122
- </DropdownMenu.Viewport>
123
- <DropdownMenu.Arrow />
124
- </DropdownMenu.Content>
125
- </DropdownMenu.Portal>
50
+ <DropdownMenu.Trigger asChild>
51
+ <IconButton
52
+ {...actionButtonProps}
53
+ icon={icon ?? fallbackIcon}
54
+ label={toLocalizedString(label, t)}
55
+ data-testid={testId}
56
+ suppressNextTooltip={suppressNextTooltip}
57
+ />
58
+ </DropdownMenu.Trigger>
126
59
  </DropdownMenu.Root>
127
60
  );
128
61
  };
129
62
 
130
- export const NavTreeItemActionContextMenu = (
131
- props: PropsWithChildren<Pick<NavTreeItemActionMenuProps, 'menuActions' | 'onAction'>>,
132
- ) => {
133
- return (props.menuActions?.length ?? 0) > 0 ? <NavTreeItemActionContextMenuImpl {...props} /> : <>{props.children}</>;
134
- };
135
-
136
- const NavTreeItemActionContextMenuImpl = ({
137
- menuActions,
138
- onAction,
139
- children,
140
- }: PropsWithChildren<Pick<NavTreeItemActionMenuProps, 'menuActions' | 'onAction'>>) => {
141
- const { t } = useTranslation(NAVTREE_PLUGIN);
142
-
143
- return (
144
- <ContextMenu.Root>
145
- <ContextMenu.Trigger asChild>{children}</ContextMenu.Trigger>
146
- <ContextMenu.Portal>
147
- <ContextMenu.Content classNames='z-[31]'>
148
- <ContextMenu.Viewport>
149
- {menuActions?.map((action) => {
150
- const shortcut = getShortcut(action);
151
- return (
152
- <ContextMenu.Item
153
- key={action.id}
154
- onClick={(event) => {
155
- if (action.properties?.disabled) {
156
- return;
157
- }
158
- event.stopPropagation();
159
- onAction?.(action);
160
- }}
161
- classNames='gap-2'
162
- disabled={action.properties?.disabled}
163
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
164
- >
165
- {action.properties?.icon && <Icon icon={action.properties?.icon} size={4} />}
166
- <span className='grow truncate'>{toLocalizedString(action.properties!.label, t)}</span>
167
- {shortcut && <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>}
168
- </ContextMenu.Item>
169
- );
170
- })}
171
- </ContextMenu.Viewport>
172
- <ContextMenu.Arrow />
173
- </ContextMenu.Content>
174
- </ContextMenu.Portal>
175
- </ContextMenu.Root>
176
- );
177
- };
178
-
179
- export const NavTreeItemActionSearchList = ({
180
- menuActions,
181
- icon,
182
- label,
183
- testId,
184
- suppressNextTooltip,
185
- onAction,
186
- }: Pick<
187
- NavTreeItemActionMenuProps,
188
- 'icon' | 'menuActions' | 'testId' | 'label' | 'onAction' | 'suppressNextTooltip'
189
- >) => {
190
- const { t } = useTranslation(NAVTREE_PLUGIN);
191
-
192
- const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
193
- const button = useRef<HTMLButtonElement | null>(null);
194
-
195
- // TODO(burdon): Optionally sort.
196
- const sortedActions = menuActions?.sort(({ properties: p1 }, { properties: p2 }) => {
197
- const t1 = toLocalizedString(p1!.label, t).toLowerCase();
198
- const t2 = toLocalizedString(p2!.label, t).toLowerCase();
199
- return t1.localeCompare(t2);
200
- });
201
-
202
- // TODO(thure): Use LayoutPlugin’s global Dialog.
203
- return (
204
- <Dialog.Root
205
- {...{
206
- open: optionsMenuOpen,
207
- onOpenChange: (nextOpen: boolean) => {
208
- if (!nextOpen && suppressNextTooltip) {
209
- suppressNextTooltip.current = true;
210
- }
211
- return setOptionsMenuOpen(nextOpen);
212
- },
213
- }}
214
- >
215
- <Tooltip.Trigger asChild>
216
- <Dialog.Trigger asChild>
217
- <Button
218
- variant='ghost'
219
- density='fine'
220
- classNames={mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem)}
221
- data-testid={testId}
222
- onKeyDownCapture={(event) => {
223
- // TODO(thure): Why is only this `Button` affected and not `DropdownMenu.Trigger`’s?
224
- if (button.current && button.current === event.target) {
225
- switch (event.key) {
226
- case 'Enter':
227
- case ' ':
228
- // Capture this event and stop propagation to the drag start handler.
229
- event.stopPropagation();
230
- setOptionsMenuOpen(true);
231
- break;
232
- default:
233
- }
234
- }
235
- }}
236
- ref={button}
237
- >
238
- <Icon icon={icon ?? fallbackIcon} size={4} />
239
- </Button>
240
- </Dialog.Trigger>
241
- </Tooltip.Trigger>
242
- <Dialog.Portal>
243
- <Dialog.Overlay>
244
- <Dialog.Content classNames='z-[31] is-full max-is-[24rem] pli-2 plb-1'>
245
- <SearchList.Root label={t('tree item searchlist input placeholder')}>
246
- <SearchList.Input placeholder={t('tree item searchlist input placeholder')} classNames='pli-3' />
247
- <SearchList.Content classNames='min-bs-[12rem] bs-[50dvh] max-bs-[30rem] overflow-auto'>
248
- {sortedActions?.map((action) => {
249
- const label = toLocalizedString(action.properties!.label, t);
250
- const shortcut = getShortcut(action);
251
- return (
252
- <SearchList.Item
253
- // TODO(burdon): Value should be id since label is not unique.
254
- value={label}
255
- key={action.id}
256
- onSelect={() => {
257
- if (action.properties?.disabled) {
258
- return;
259
- }
260
- if (suppressNextTooltip) {
261
- suppressNextTooltip.current = true;
262
- }
263
- setOptionsMenuOpen(false);
264
- onAction?.(action);
265
- }}
266
- classNames='flex items-center gap-2 pli-2'
267
- disabled={action.properties?.disabled}
268
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
269
- >
270
- {action.properties?.icon && <Icon icon={action.properties?.icon} size={4} />}
271
- <span className='grow truncate'>{label}</span>
272
- {shortcut && (
273
- <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>
274
- )}
275
- </SearchList.Item>
276
- );
277
- })}
278
- </SearchList.Content>
279
- <div role='none' className='flex items-center plb-2 pli-3'>
280
- <span className={descriptionText}>{toLocalizedString(label, t)}</span>
281
- </div>
282
- </SearchList.Root>
283
- </Dialog.Content>
284
- </Dialog.Overlay>
285
- </Dialog.Portal>
286
- </Dialog.Root>
287
- );
288
- };
289
-
290
63
  export const NavTreeItemMonolithicAction = ({
291
64
  parent,
292
- properties: { disabled, caller, testId, label, icon } = { label: 'never' },
65
+ properties: { disabled, caller, testId, icon } = { label: 'never' },
293
66
  data: invoke,
294
- }: Action & { parent: Node; onAction?: (action: Action) => void }) => {
295
- const { t } = useTranslation(NAVTREE_PLUGIN);
67
+ baseLabel,
68
+ }: Action & { parent: Node; onAction?: (action: Action) => void; baseLabel: string }) => {
296
69
  return (
297
- <Tooltip.Trigger asChild>
298
- <Button
299
- variant='ghost'
300
- density='fine'
301
- classNames={mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem)}
302
- disabled={disabled}
303
- onClick={(event) => {
304
- if (disabled) {
305
- return;
306
- }
307
- event.stopPropagation();
308
- void invoke?.(caller ? { node: parent, caller } : { node: parent });
309
- }}
310
- data-testid={testId}
311
- >
312
- <span className='sr-only'>{toLocalizedString(label, t)}</span>
313
- <Icon icon={icon ?? fallbackIcon} size={4} />
314
- </Button>
315
- </Tooltip.Trigger>
70
+ <IconButton
71
+ {...actionButtonProps}
72
+ icon={icon ?? fallbackIcon}
73
+ label={baseLabel}
74
+ disabled={disabled}
75
+ onClick={(event) => {
76
+ if (disabled) {
77
+ return;
78
+ }
79
+ event.stopPropagation();
80
+ void invoke?.(caller ? { node: parent, caller } : { node: parent });
81
+ }}
82
+ data-testid={testId}
83
+ />
316
84
  );
317
85
  };
318
86
 
319
87
  export const NavTreeItemAction = ({ monolithic, menuActions, parent: node, ...props }: NavTreeItemActionMenuProps) => {
320
88
  const { t } = useTranslation(NAVTREE_PLUGIN);
321
- const suppressNextTooltip = useRef<boolean>(false);
322
- const [triggerTooltipOpen, setTriggerTooltipOpen] = useState(false);
323
89
 
324
90
  const monolithicAction = menuActions?.length === 1 && menuActions[0];
325
91
  const baseLabel = toLocalizedString(monolithicAction ? monolithicAction.properties!.label : props.label, t);
326
92
 
327
- return (
328
- <Tooltip.Root
329
- open={triggerTooltipOpen}
330
- onOpenChange={(nextOpen) => {
331
- if (suppressNextTooltip.current) {
332
- setTriggerTooltipOpen(false);
333
- suppressNextTooltip.current = false;
334
- } else {
335
- setTriggerTooltipOpen(nextOpen);
336
- }
337
- }}
338
- >
339
- {baseLabel && (
340
- <Tooltip.Portal>
341
- <Tooltip.Content classNames='z-[31]' side='bottom'>
342
- {baseLabel}
343
- <Tooltip.Arrow />
344
- </Tooltip.Content>
345
- </Tooltip.Portal>
346
- )}
347
- {monolithic && menuActions?.length === 1 ? (
348
- <NavTreeItemMonolithicAction parent={node} {...menuActions[0]} />
349
- ) : props.menuType === 'searchList' ? (
350
- <NavTreeItemActionSearchList
351
- {...props}
352
- menuActions={menuActions}
353
- suppressNextTooltip={suppressNextTooltip}
354
- onAction={(action) => action.data?.(props.caller ? { node, caller: props.caller } : { node })}
355
- />
356
- ) : (
357
- <NavTreeItemActionDropdownMenu
358
- {...props}
359
- parent={node}
360
- menuActions={menuActions}
361
- suppressNextTooltip={suppressNextTooltip}
362
- onAction={(action) => action.data?.(props.caller ? { node, caller: props.caller } : { node })}
363
- />
364
- )}
365
- </Tooltip.Root>
93
+ return monolithic && menuActions?.length === 1 ? (
94
+ <NavTreeItemMonolithicAction baseLabel={baseLabel} parent={node} {...menuActions[0]} />
95
+ ) : (
96
+ <NavTreeItemActionDropdownMenu
97
+ {...props}
98
+ label={baseLabel}
99
+ parent={node}
100
+ menuActions={menuActions}
101
+ onAction={(action) => action.data?.(props.caller ? { node, caller: props.caller } : { node })}
102
+ />
366
103
  );
367
104
  };