@dxos/plugin-navtree 0.8.4-main.2e9d522 → 0.8.4-main.3c1ae3b

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 (113) hide show
  1. package/dist/lib/browser/{app-graph-builder-5J4LRWDX.mjs → app-graph-builder-OHSCN3GX.mjs} +13 -12
  2. package/dist/lib/browser/app-graph-builder-OHSCN3GX.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-WEMCSQI7.mjs → chunk-5DWDYPGQ.mjs} +212 -189
  4. package/dist/lib/browser/chunk-5DWDYPGQ.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-WCARAZ7J.mjs → chunk-5VLWX4PK.mjs} +4 -4
  6. package/dist/lib/browser/chunk-5VLWX4PK.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-E6MZBCU5.mjs +19 -0
  8. package/dist/lib/browser/chunk-E6MZBCU5.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +6 -10
  10. package/dist/lib/browser/index.mjs.map +3 -3
  11. package/dist/lib/browser/{intent-resolver-3XYN2CBU.mjs → intent-resolver-YUVXVPHG.mjs} +7 -7
  12. package/dist/lib/browser/intent-resolver-YUVXVPHG.mjs.map +7 -0
  13. package/dist/lib/browser/{keyboard-QRS6WOQH.mjs → keyboard-YJEK2HTH.mjs} +8 -10
  14. package/dist/lib/browser/{keyboard-QRS6WOQH.mjs.map → keyboard-YJEK2HTH.mjs.map} +2 -2
  15. package/dist/lib/browser/meta.json +1 -1
  16. package/dist/lib/browser/{react-surface-JSJ5SQX5.mjs → react-surface-RPXGIV45.mjs} +22 -16
  17. package/dist/lib/browser/react-surface-RPXGIV45.mjs.map +7 -0
  18. package/dist/lib/browser/{state-54DY3DOY.mjs → state-BFD5KJCV.mjs} +7 -7
  19. package/dist/lib/browser/state-BFD5KJCV.mjs.map +7 -0
  20. package/dist/types/src/NavTreePlugin.d.ts +1 -1
  21. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  22. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -1
  23. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  24. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  25. package/dist/types/src/capabilities/index.d.ts +18 -15
  26. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  27. package/dist/types/src/capabilities/intent-resolver.d.ts +2 -2
  28. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  29. package/dist/types/src/capabilities/keyboard.d.ts +1 -1
  30. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  32. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  33. package/dist/types/src/capabilities/state.d.ts +3 -3
  34. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  35. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts +3 -2
  36. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +1 -1
  37. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +1 -1
  38. package/dist/types/src/components/NavTree/NavTree.d.ts +30 -1
  39. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -1
  40. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +29 -4
  41. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/NavTreeContainer.d.ts +8 -1
  43. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  44. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  45. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +32 -4
  46. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +1 -1
  48. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  49. package/dist/types/src/components/Sidebar/L0Menu.d.ts +2 -0
  50. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  51. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  52. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  53. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  54. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts +8 -5
  55. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/index.d.ts.map +1 -1
  57. package/dist/types/src/components/types.d.ts +3 -3
  58. package/dist/types/src/components/types.d.ts.map +1 -1
  59. package/dist/types/src/events.d.ts.map +1 -1
  60. package/dist/types/src/experimental/Tree.d.ts +1 -2
  61. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  62. package/dist/types/src/experimental/Tree.stories.d.ts +7 -4
  63. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  64. package/dist/types/src/meta.d.ts +2 -3
  65. package/dist/types/src/meta.d.ts.map +1 -1
  66. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  67. package/dist/types/src/testing/generator.d.ts +2 -2
  68. package/dist/types/src/testing/generator.d.ts.map +1 -1
  69. package/dist/types/src/types.d.ts +4 -3
  70. package/dist/types/src/types.d.ts.map +1 -1
  71. package/dist/types/src/util.d.ts +1 -2
  72. package/dist/types/src/util.d.ts.map +1 -1
  73. package/dist/types/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +49 -48
  75. package/src/NavTreePlugin.tsx +75 -76
  76. package/src/capabilities/app-graph-builder.ts +10 -9
  77. package/src/capabilities/capabilities.ts +2 -2
  78. package/src/capabilities/intent-resolver.ts +2 -2
  79. package/src/capabilities/keyboard.ts +3 -5
  80. package/src/capabilities/react-surface.tsx +16 -13
  81. package/src/capabilities/state.ts +5 -5
  82. package/src/components/CommandsDialog/CommandsDialog.tsx +102 -90
  83. package/src/components/CommandsDialog/CommandsTrigger.tsx +4 -3
  84. package/src/components/NavTree/NavTree.stories.tsx +67 -53
  85. package/src/components/NavTree/NavTree.tsx +8 -4
  86. package/src/components/NavTreeContainer.tsx +313 -244
  87. package/src/components/NavTreeDocumentTitle.tsx +2 -2
  88. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +4 -6
  89. package/src/components/NavTreeItem/NavTreeItemAction.tsx +7 -6
  90. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +31 -24
  91. package/src/components/Sidebar/L0Menu.tsx +56 -55
  92. package/src/components/Sidebar/L1Panel.tsx +23 -28
  93. package/src/components/Sidebar/L1Tabs.tsx +1 -0
  94. package/src/components/UserAccountAvatar/UserAccountAvatar.stories.tsx +9 -8
  95. package/src/components/UserAccountAvatar/UserAccountAvatar.tsx +20 -12
  96. package/src/components/index.ts +1 -0
  97. package/src/components/types.ts +4 -3
  98. package/src/events.ts +2 -2
  99. package/src/experimental/Tree.stories.tsx +28 -30
  100. package/src/experimental/Tree.tsx +6 -23
  101. package/src/meta.ts +10 -7
  102. package/src/testing/app-graph-builder.ts +20 -19
  103. package/src/testing/generator.ts +10 -8
  104. package/src/types.ts +4 -3
  105. package/src/util.ts +2 -20
  106. package/dist/lib/browser/app-graph-builder-5J4LRWDX.mjs.map +0 -7
  107. package/dist/lib/browser/chunk-ERDGQTH5.mjs +0 -16
  108. package/dist/lib/browser/chunk-ERDGQTH5.mjs.map +0 -7
  109. package/dist/lib/browser/chunk-WCARAZ7J.mjs.map +0 -7
  110. package/dist/lib/browser/chunk-WEMCSQI7.mjs.map +0 -7
  111. package/dist/lib/browser/intent-resolver-3XYN2CBU.mjs.map +0 -7
  112. package/dist/lib/browser/react-surface-JSJ5SQX5.mjs.map +0 -7
  113. package/dist/lib/browser/state-54DY3DOY.mjs.map +0 -7
@@ -2,13 +2,14 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { useMemo, useState } from 'react';
5
+ import React, { forwardRef, useMemo, useState } from 'react';
6
6
 
7
- import { createIntent, LayoutAction, useAppGraph, useIntentDispatcher } from '@dxos/app-framework';
7
+ import { LayoutAction, createIntent } from '@dxos/app-framework';
8
+ import { useAppGraph, useIntentDispatcher } from '@dxos/app-framework/react';
8
9
  import { type ActionLike, isAction, isActionGroup } from '@dxos/app-graph';
9
10
  import { Keyboard, keySymbols } from '@dxos/keyboard';
10
11
  import { useActions } from '@dxos/plugin-graph';
11
- import { Button, Dialog, Icon, useTranslation, toLocalizedString } from '@dxos/react-ui';
12
+ import { Button, Dialog, Icon, toLocalizedString, useTranslation } from '@dxos/react-ui';
12
13
  import { SearchList } from '@dxos/react-ui-searchlist';
13
14
  import {
14
15
  cardDialogContent,
@@ -19,100 +20,111 @@ import {
19
20
  import { descriptionText, mx } from '@dxos/react-ui-theme';
20
21
  import { getHostPlatform } from '@dxos/util';
21
22
 
22
- import { KEY_BINDING, NAVTREE_PLUGIN } from '../../meta';
23
+ import { KEY_BINDING, meta } from '../../meta';
23
24
 
24
- // TODO(wittjosiah): This probably deserves its own plugin but for now it lives here w/ other navigation UI.
25
- export const CommandsDialogContent = ({ selected: initial }: { selected?: string }) => {
26
- const { t } = useTranslation(NAVTREE_PLUGIN);
27
- const [selected, setSelected] = useState<string | undefined>(initial);
28
- const { dispatchPromise: dispatch } = useIntentDispatcher();
29
- const { graph } = useAppGraph();
25
+ export type CommandsDialogContentProps = {
26
+ selected?: string;
27
+ };
30
28
 
31
- // Traverse graph.
32
- // TODO(burdon): Factor out commonality with shortcut dialog.
33
- const allActions = useMemo(() => {
34
- // TODO(burdon): Get from navtree (not keyboard).
35
- const current = Keyboard.singleton.getCurrentContext();
36
- const actionMap = new Set<string>();
37
- const actions: ActionLike[] = [];
38
- graph.traverse({
39
- visitor: (node, path) => {
40
- if (
41
- (isAction(node) || isActionGroup(node)) &&
42
- !actionMap.has(node.id) &&
43
- current.startsWith(path.slice(0, -1).join('/'))
44
- ) {
45
- actionMap.add(node.id);
46
- actions.push(node);
47
- }
48
- },
49
- });
29
+ // TODO(wittjosiah): This probably deserves its own plugin but for now it lives here w/ other navigation UI.
30
+ export const CommandsDialogContent = forwardRef<HTMLDivElement, CommandsDialogContentProps>(
31
+ ({ selected: initial }, forwardedRef) => {
32
+ const { t } = useTranslation(meta.id);
33
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
34
+ const { graph } = useAppGraph();
35
+ const [selected, setSelected] = useState<string | undefined>(initial);
50
36
 
51
- actions.sort((a, b) => {
52
- return toLocalizedString(a.properties.label, t)
53
- ?.toLowerCase()
54
- .localeCompare(toLocalizedString(b.properties.label, t)?.toLowerCase());
55
- });
37
+ // Traverse graph.
38
+ // TODO(burdon): Factor out commonality with shortcut dialog.
39
+ const allActions = useMemo(() => {
40
+ // TODO(burdon): Get from navtree (not keyboard).
41
+ const current = Keyboard.singleton.getCurrentContext();
42
+ const actionMap = new Set<string>();
43
+ const actions: ActionLike[] = [];
44
+ graph.traverse({
45
+ visitor: (node, path) => {
46
+ if (
47
+ (isAction(node) || isActionGroup(node)) &&
48
+ !actionMap.has(node.id) &&
49
+ current.startsWith(path.slice(0, -1).join('/'))
50
+ ) {
51
+ actionMap.add(node.id);
52
+ actions.push(node);
53
+ }
54
+ },
55
+ });
56
56
 
57
- return actions;
58
- }, [graph]);
57
+ actions.sort((a, b) => {
58
+ return toLocalizedString(a.properties.label, t)
59
+ ?.toLowerCase()
60
+ .localeCompare(toLocalizedString(b.properties.label, t)?.toLowerCase());
61
+ });
59
62
 
60
- const group = allActions.find(({ id }) => id === selected);
61
- const groupActions = useActions(graph, group?.id);
62
- const actions = isActionGroup(group) ? groupActions : allActions;
63
+ return actions;
64
+ }, [graph]);
63
65
 
64
- return (
65
- <Dialog.Content classNames={cardDialogContent}>
66
- <Dialog.Title classNames={cardDialogHeader}>{t('commands dialog title', { ns: NAVTREE_PLUGIN })}</Dialog.Title>
66
+ const group = allActions.find(({ id }) => id === selected);
67
+ const groupActions = useActions(graph, group?.id);
68
+ const actions = isActionGroup(group) ? groupActions : allActions;
67
69
 
68
- {/* TODO(burdon): BUG: Overscrolls container. */}
69
- <SearchList.Root label={t('command list input placeholder')} classNames={cardDialogSearchListRoot}>
70
- <SearchList.Input placeholder={t('command list input placeholder')} />
71
- <SearchList.Content classNames={cardDialogPaddedOverflow}>
72
- {actions?.map((action) => {
73
- const label = toLocalizedString(action.properties.label, t);
74
- const shortcut =
75
- typeof action.properties.keyBinding === 'string'
76
- ? action.properties.keyBinding
77
- : action.properties.keyBinding?.[getHostPlatform()];
78
- return (
79
- <SearchList.Item
80
- value={label}
81
- key={action.id}
82
- onSelect={() => {
83
- if (action.properties.disabled) {
84
- return;
85
- }
70
+ return (
71
+ <Dialog.Content classNames={cardDialogContent} ref={forwardedRef}>
72
+ <Dialog.Title classNames={cardDialogHeader}>{t('commands dialog title', { ns: meta.id })}</Dialog.Title>
86
73
 
87
- if (isActionGroup(action)) {
88
- setSelected(action.id);
89
- return;
90
- }
74
+ <SearchList.Root label={t('command list input placeholder')} classNames={cardDialogSearchListRoot}>
75
+ <SearchList.Input placeholder={t('command list input placeholder')} />
76
+ <SearchList.Content classNames={cardDialogPaddedOverflow}>
77
+ {actions?.map((action) => {
78
+ const label = toLocalizedString(action.properties.label, t);
79
+ const shortcut =
80
+ typeof action.properties.keyBinding === 'string'
81
+ ? action.properties.keyBinding
82
+ : action.properties.keyBinding?.[getHostPlatform()];
83
+ return (
84
+ <SearchList.Item
85
+ value={label}
86
+ key={action.id}
87
+ onSelect={() => {
88
+ if (action.properties.disabled) {
89
+ return;
90
+ }
91
91
 
92
- void dispatch(createIntent(LayoutAction.UpdateDialog, { part: 'dialog', options: { state: false } }));
93
- setTimeout(() => {
94
- const node = graph.getConnections(group?.id ?? action.id, 'inbound')[0];
95
- void (node && isAction(action) && action.data({ parent: node, caller: KEY_BINDING }));
96
- });
97
- }}
98
- classNames='flex items-center gap-2'
99
- disabled={action.properties.disabled}
100
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
101
- >
102
- <Icon icon={action.properties.icon} size={4} />
103
- <span className='grow truncate'>{label}</span>
104
- {shortcut && <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>}
105
- </SearchList.Item>
106
- );
107
- })}
108
- </SearchList.Content>
109
- </SearchList.Root>
92
+ if (isActionGroup(action)) {
93
+ setSelected(action.id);
94
+ return;
95
+ }
110
96
 
111
- <div role='none' className='pli-cardSpacingInline pbe-cardSpacingBlock'>
112
- <Dialog.Close asChild>
113
- <Button classNames='is-full'>{t('close label', { ns: 'os' })}</Button>
114
- </Dialog.Close>
115
- </div>
116
- </Dialog.Content>
117
- );
118
- };
97
+ void dispatch(
98
+ createIntent(LayoutAction.UpdateDialog, {
99
+ part: 'dialog',
100
+ options: { state: false },
101
+ }),
102
+ );
103
+ setTimeout(() => {
104
+ const node = graph.getConnections(group?.id ?? action.id, 'inbound')[0];
105
+ void (node && isAction(action) && action.data({ parent: node, caller: KEY_BINDING }));
106
+ });
107
+ }}
108
+ classNames='flex items-center gap-2'
109
+ disabled={action.properties.disabled}
110
+ {...(action.properties?.testId && {
111
+ 'data-testid': action.properties.testId,
112
+ })}
113
+ >
114
+ <Icon icon={action.properties.icon} size={4} />
115
+ <span className='grow truncate'>{label}</span>
116
+ {shortcut && <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>}
117
+ </SearchList.Item>
118
+ );
119
+ })}
120
+ </SearchList.Content>
121
+ </SearchList.Root>
122
+ <div role='none' className='pli-cardSpacingInline pbe-cardSpacingBlock'>
123
+ <Dialog.Close asChild>
124
+ <Button classNames='is-full'>{t('close label', { ns: 'os' })}</Button>
125
+ </Dialog.Close>
126
+ </div>
127
+ </Dialog.Content>
128
+ );
129
+ },
130
+ );
@@ -4,15 +4,16 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { createIntent, LayoutAction, useIntentDispatcher } from '@dxos/app-framework';
7
+ import { LayoutAction, createIntent } from '@dxos/app-framework';
8
+ import { useIntentDispatcher } from '@dxos/app-framework/react';
8
9
  import { Button, Icon, useTranslation } from '@dxos/react-ui';
9
10
 
10
- import { COMMANDS_DIALOG, NAVTREE_PLUGIN } from '../../meta';
11
+ import { COMMANDS_DIALOG, meta } from '../../meta';
11
12
 
12
13
  // TODO(thure): Refactor to be handled by a more appropriate plugin.
13
14
  export const CommandsTrigger = () => {
14
15
  const { dispatchPromise: dispatch } = useIntentDispatcher();
15
- const { t } = useTranslation(NAVTREE_PLUGIN);
16
+ const { t } = useTranslation(meta.id);
16
17
  return (
17
18
  <Button
18
19
  classNames='m-1 pli-1 lg:pli-2'
@@ -2,11 +2,10 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type StoryObj, type Meta } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
9
- import React from 'react';
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { type KeyboardEvent, useCallback, useRef } from 'react';
8
+ import { expect, userEvent, within } from 'storybook/test';
10
9
 
11
10
  import {
12
11
  Capabilities,
@@ -16,22 +15,20 @@ import {
16
15
  contributes,
17
16
  createResolver,
18
17
  defineCapability,
19
- useCapability,
20
18
  } from '@dxos/app-framework';
19
+ import { useCapability } from '@dxos/app-framework/react';
21
20
  import { withPluginManager } from '@dxos/app-framework/testing';
22
21
  import { live } from '@dxos/live-object';
23
22
  import { AttentionPlugin } from '@dxos/plugin-attention';
24
- import { ClientPlugin } from '@dxos/plugin-client';
25
23
  import { GraphPlugin } from '@dxos/plugin-graph';
26
- import { SpacePlugin } from '@dxos/plugin-space';
27
24
  import { StorybookLayoutPlugin } from '@dxos/plugin-storybook-layout';
28
25
  import { ThemePlugin } from '@dxos/plugin-theme';
29
26
  import { faker } from '@dxos/random';
30
27
  import { IconButton, Input, Main, Toolbar } from '@dxos/react-ui';
31
- import { useAttentionAttributes, useAttention } from '@dxos/react-ui-attention';
28
+ import { withTheme } from '@dxos/react-ui/testing';
29
+ import { useAttention, useAttentionAttributes } from '@dxos/react-ui-attention';
32
30
  import { Stack, StackItem } from '@dxos/react-ui-stack';
33
31
  import { defaultTx, mx } from '@dxos/react-ui-theme';
34
- import { withLayout } from '@dxos/storybook-utils';
35
32
 
36
33
  import { NavTreePlugin } from '../../NavTreePlugin';
37
34
  import { storybookGraphBuilders } from '../../testing';
@@ -68,6 +65,15 @@ const StoryPlankHeading = ({ attendableId }: { attendableId: string }) => {
68
65
 
69
66
  const StoryPlank = ({ attendableId }: { attendableId: string }) => {
70
67
  const attentionAttrs = useAttentionAttributes(attendableId);
68
+ const rootElement = useRef<HTMLDivElement | null>(null);
69
+
70
+ // NOTE(thure): This is the same workaround as in Plank, but that component is out of scope for this story.
71
+ // TODO(thure): Tabster’s focus group should handle moving focus to Main, but something is blocking it.
72
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
73
+ if (event.target === event.currentTarget && event.key === 'Escape') {
74
+ rootElement.current?.closest('main')?.focus();
75
+ }
76
+ }, []);
71
77
 
72
78
  return (
73
79
  <StackItem.Root
@@ -75,10 +81,12 @@ const StoryPlank = ({ attendableId }: { attendableId: string }) => {
75
81
  {...attentionAttrs}
76
82
  classNames='bg-baseSurface border-ie border-separator'
77
83
  size={30}
84
+ onKeyDown={handleKeyDown}
85
+ ref={rootElement}
78
86
  >
79
87
  <StoryPlankHeading attendableId={attendableId} />
80
88
  <StackItem.Content toolbar>
81
- <Toolbar.Root classNames='border-b border-subduedSeparator'>
89
+ <Toolbar.Root classNames='border-be border-subduedSeparator'>
82
90
  <Toolbar.Button>Test</Toolbar.Button>
83
91
  </Toolbar.Root>
84
92
 
@@ -116,20 +124,21 @@ const DefaultStory = () => {
116
124
  );
117
125
  };
118
126
 
119
- const meta: Meta<typeof NavTreeContainer> = {
127
+ const meta = {
120
128
  title: 'plugins/plugin-navtree/NavTree',
121
129
  component: NavTreeContainer,
122
130
  render: DefaultStory,
123
131
  decorators: [
132
+ withTheme,
124
133
  withPluginManager({
125
134
  plugins: [
126
135
  ThemePlugin({ tx: defaultTx }),
127
- StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
128
136
  GraphPlugin(),
129
137
  IntentPlugin(),
130
138
  SettingsPlugin(),
131
139
  AttentionPlugin(),
132
140
  NavTreePlugin(),
141
+ StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
133
142
  ],
134
143
  capabilities: (context) => [
135
144
  contributes(StoryState, live({ tab: 'space-0' })),
@@ -147,54 +156,59 @@ const meta: Meta<typeof NavTreeContainer> = {
147
156
  ]),
148
157
  ],
149
158
  }),
150
- withLayout({ fullscreen: true }),
151
159
  ],
152
160
  parameters: {
161
+ layout: 'fullscreen',
153
162
  translations,
154
163
  },
155
- };
164
+ } satisfies Meta<typeof NavTreeContainer>;
156
165
 
157
166
  export default meta;
158
167
 
159
168
  type Story = StoryObj<typeof NavTreeContainer>;
160
169
 
161
- export const Default: Story = {};
170
+ export const Default: Story = {
171
+ play: async ({ canvasElement }) => {
172
+ const canvas = within(canvasElement);
162
173
 
163
- // TODO(wittjosiah): Deduplicate plugins/capabilities with default story.
164
- export const WithClient: Story = {
165
- decorators: [
166
- withPluginManager({
167
- plugins: [
168
- ThemePlugin({ tx: defaultTx }),
169
- GraphPlugin(),
170
- IntentPlugin(),
171
- SettingsPlugin(),
172
- AttentionPlugin(),
173
- NavTreePlugin(),
174
- ClientPlugin({
175
- onClientInitialized: async (_, client) => {
176
- await client.halo.createIdentity();
177
- },
178
- }),
179
- SpacePlugin(),
180
- // Needs to be last so that the dialog and popovers have access to all contexts.
181
- StorybookLayoutPlugin({ initialState: { sidebarState: 'expanded' } }),
182
- ],
183
- capabilities: (context) => [
184
- contributes(StoryState, live({ tab: 'space-0' })),
185
- contributes(Capabilities.IntentResolver, [
186
- createResolver({
187
- intent: LayoutAction.UpdateLayout,
188
- filter: (data): data is Schema.Schema.Type<typeof LayoutAction.SwitchWorkspace.fields.input> =>
189
- Schema.is(LayoutAction.SwitchWorkspace.fields.input)(data),
190
- resolve: ({ subject }) => {
191
- const state = context.getCapability(StoryState);
192
- state.tab = subject;
193
- },
194
- }),
195
- ]),
196
- ],
197
- }),
198
- withLayout({ fullscreen: true }),
199
- ],
174
+ // Find the element with treegrid role and click on its parent
175
+ const treegridElement = await canvas.findByRole('treegrid');
176
+ const treegridParent = treegridElement.parentElement;
177
+ if (treegridParent) {
178
+ await userEvent.click(treegridParent);
179
+ }
180
+
181
+ // Press Escape
182
+ await userEvent.keyboard('{Escape}');
183
+
184
+ // Confirm that focus is on an element with attribute data-main-landmark="0"
185
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
186
+
187
+ // Press Tab
188
+ await userEvent.keyboard('{Tab}');
189
+
190
+ // Confirm that focus is now on an element with data-main-landmark="1"
191
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
192
+
193
+ // Press Tab
194
+ await userEvent.keyboard('{Tab}');
195
+
196
+ // Confirm that focus is now on an element with data-main-landmark="0"
197
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '0');
198
+
199
+ // Press Shift-Tab
200
+ await userEvent.keyboard('{Shift>}{Tab}{/Shift}');
201
+
202
+ // Press Enter
203
+ await userEvent.keyboard('{Enter}');
204
+
205
+ // Confirm that focus is now on an element with data-attendable-id="space-0:object-0"
206
+ await expect(document.activeElement).toHaveAttribute('data-attendable-id', 'space-0:object-0');
207
+
208
+ // Press Escape
209
+ await userEvent.keyboard('{Escape}');
210
+
211
+ // Confirm that focus is now on an element with data-main-landmark="1"
212
+ await expect(document.activeElement).toHaveAttribute('data-main-landmark', '1');
213
+ },
200
214
  };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useMemo } from 'react';
5
+ import React, { forwardRef, useMemo } from 'react';
6
6
 
7
7
  import { type TreeProps } from '@dxos/react-ui-list';
8
8
  import { Tabs } from '@dxos/react-ui-tabs';
@@ -16,7 +16,7 @@ export const NAV_TREE_ITEM = 'NavTreeItem';
16
16
 
17
17
  export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'> & Pick<L1TabsProps, 'open'>;
18
18
 
19
- export const NavTree = ({ id, root, ...props }: NavTreeProps) => {
19
+ export const NavTree = forwardRef<HTMLDivElement, NavTreeProps>(({ id, root, ...props }, forwardedRef) => {
20
20
  const { tab, useItems, onBack } = useNavTreeContext();
21
21
  const topLevelActions = useItems(root, { disposition: 'menu', sort: true });
22
22
  const topLevelCollections = useItems(root, { disposition: 'collection' });
@@ -47,7 +47,11 @@ export const NavTree = ({ id, root, ...props }: NavTreeProps) => {
47
47
  const path = useMemo(() => [id], [id]);
48
48
 
49
49
  return (
50
- <Tabs.Root value={tab} orientation='vertical' verticalVariant='stateless' classNames='relative'>
50
+ // TODO(thure): `Tabs.Root` forces all items that should be able to receive focus to use `Tabs.Tab(Primitive)` since
51
+ // it uses RovingFocus and doesn’t support moving focus to an item that is not a tab. Assess whether this situation
52
+ // should change including whether it should motivate a change in the design/taxonomy, or if this means this should
53
+ // not use `react-ui-tabs` at all.
54
+ <Tabs.Root value={tab} orientation='vertical' verticalVariant='stateless' classNames='relative' ref={forwardedRef}>
51
55
  <L0Menu
52
56
  menuActions={topLevelActions}
53
57
  topLevelItems={l0Items}
@@ -59,4 +63,4 @@ export const NavTree = ({ id, root, ...props }: NavTreeProps) => {
59
63
  <L1Tabs topLevelItems={topLevelItems} path={path} currentItemId={tab} onBack={onBack} {...props} />
60
64
  </Tabs.Root>
61
65
  );
62
- };
66
+ });