@dxos/react-ui-tabs 0.8.4-main.70d3990 → 0.8.4-main.74a063c4e0

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/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-tabs",
3
- "version": "0.8.4-main.70d3990",
3
+ "version": "0.8.4-main.74a063c4e0",
4
4
  "description": "Components for facilitating a Tabs pattern.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
- "sideEffects": true,
13
+ "sideEffects": false,
10
14
  "type": "module",
11
15
  "exports": {
12
16
  ".": {
@@ -25,33 +29,32 @@
25
29
  "src"
26
30
  ],
27
31
  "dependencies": {
28
- "@fluentui/react-tabster": "^9.24.2",
29
- "@preact-signals/safe-react": "^0.9.0",
32
+ "@fluentui/react-tabster": "9.26.11",
30
33
  "@radix-ui/primitive": "1.1.1",
31
34
  "@radix-ui/react-context": "1.1.1",
32
35
  "@radix-ui/react-primitive": "2.0.2",
33
36
  "@radix-ui/react-slot": "1.1.2",
34
37
  "@radix-ui/react-tabs": "1.1.3",
35
38
  "@radix-ui/react-use-controllable-state": "1.1.0",
36
- "@dxos/react-ui-attention": "0.8.4-main.70d3990",
37
- "@dxos/util": "0.8.4-main.70d3990"
39
+ "@dxos/react-ui-attention": "0.8.4-main.74a063c4e0",
40
+ "@dxos/util": "0.8.4-main.74a063c4e0"
38
41
  },
39
42
  "devDependencies": {
40
- "@types/react": "~19.2.2",
41
- "@types/react-dom": "~19.2.2",
42
- "react": "~19.2.0",
43
- "react-dom": "~19.2.0",
44
- "vite": "7.1.9",
45
- "@dxos/random": "0.8.4-main.70d3990",
46
- "@dxos/react-ui": "0.8.4-main.70d3990",
47
- "@dxos/react-ui-theme": "0.8.4-main.70d3990",
48
- "@dxos/storybook-utils": "0.8.4-main.70d3990"
43
+ "@types/react": "~19.2.7",
44
+ "@types/react-dom": "~19.2.3",
45
+ "react": "~19.2.3",
46
+ "react-dom": "~19.2.3",
47
+ "vite": "^7.1.11",
48
+ "@dxos/react-ui": "0.8.4-main.74a063c4e0",
49
+ "@dxos/random": "0.8.4-main.74a063c4e0",
50
+ "@dxos/storybook-utils": "0.8.4-main.74a063c4e0",
51
+ "@dxos/ui-theme": "0.8.4-main.74a063c4e0"
49
52
  },
50
53
  "peerDependencies": {
51
- "react": "^19.0.0",
52
- "react-dom": "^19.0.0",
53
- "@dxos/react-ui": "0.8.4-main.70d3990",
54
- "@dxos/react-ui-theme": "0.8.4-main.70d3990"
54
+ "react": "~19.2.3",
55
+ "react-dom": "~19.2.3",
56
+ "@dxos/react-ui": "0.8.4-main.74a063c4e0",
57
+ "@dxos/ui-theme": "0.8.4-main.74a063c4e0"
55
58
  },
56
59
  "publishConfig": {
57
60
  "access": "public"
@@ -5,66 +5,73 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
- import { faker } from '@dxos/random';
9
- import { Dialog, Icon } from '@dxos/react-ui';
10
- import { withTheme } from '@dxos/react-ui/testing';
8
+ import { random } from '@dxos/random';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+ import { mx } from '@dxos/ui-theme';
11
11
 
12
- import { Tabs as NaturalTabs } from './Tabs';
12
+ import { Tabs, TabsRootProps } from './Tabs';
13
13
 
14
- faker.seed(1234);
14
+ random.seed(1234);
15
15
 
16
- export const DefaultStory = () => {
16
+ const DefaultStory = ({ orientation }: TabsRootProps) => {
17
17
  return (
18
- <Dialog.Root open>
19
- <Dialog.Overlay blockAlign='start'>
20
- <Dialog.Content classNames='is-[calc(100dvw-4rem)] !max-is-full'>
21
- <NaturalTabs.Root orientation='vertical' defaultValue={Object.keys(content)[3]} defaultActivePart='list'>
22
- <NaturalTabs.Viewport>
23
- <NaturalTabs.Tablist>
24
- {Object.entries(content).map(([id, { title }]) => {
25
- return (
26
- <NaturalTabs.Tab key={id} value={id}>
27
- {title}
28
- </NaturalTabs.Tab>
29
- );
30
- })}
31
- </NaturalTabs.Tablist>
32
- {Object.entries(content).map(([id, { panel }]) => {
33
- return (
34
- <NaturalTabs.Tabpanel key={id} value={id} classNames='m-1'>
35
- <NaturalTabs.BackButton density='fine'>
36
- <Icon icon='ph--arrow-left--bold' size={4} />
37
- <span>Back to tab list</span>
38
- </NaturalTabs.BackButton>
39
- <p className='pli-1'>{panel}</p>
40
- </NaturalTabs.Tabpanel>
41
- );
42
- })}
43
- </NaturalTabs.Viewport>
44
- </NaturalTabs.Root>
45
- </Dialog.Content>
46
- </Dialog.Overlay>
47
- </Dialog.Root>
18
+ <Tabs.Root orientation={orientation} defaultValue={Object.keys(content)[3]} defaultActivePart='list'>
19
+ <Tabs.Viewport
20
+ classNames={mx(
21
+ 'w-full overflow-hidden grid',
22
+ orientation === 'vertical' && 'grid-cols-[minmax(min-content,1fr)_3fr]',
23
+ )}
24
+ >
25
+ <Tabs.Tablist>
26
+ {Object.entries(content).map(([id, { title }]) => (
27
+ <Tabs.Tab key={id} value={id}>
28
+ {title}
29
+ </Tabs.Tab>
30
+ ))}
31
+ </Tabs.Tablist>
32
+ <div className='dx-container'>
33
+ {Object.entries(content).map(([id, { panel }]) => (
34
+ <Tabs.Panel key={id} value={id}>
35
+ <p className='px-1'>{panel}</p>
36
+ </Tabs.Panel>
37
+ ))}
38
+ </div>
39
+ </Tabs.Viewport>
40
+ </Tabs.Root>
48
41
  );
49
42
  };
50
43
 
51
44
  const content = [...Array(24)].reduce((acc: { [key: string]: { title: string; panel: string } }, _, index) => {
52
45
  acc[`t${index}`] = {
53
- title: faker.commerce.productName(),
54
- panel: faker.lorem.paragraphs(5),
46
+ title: random.commerce.productName(),
47
+ panel: random.lorem.paragraphs(5),
55
48
  };
56
49
  return acc;
57
50
  }, {});
58
51
 
59
52
  const meta = {
60
53
  title: 'ui/react-ui-tabs/Tabs',
61
- component: NaturalTabs.Root,
54
+ component: Tabs.Root,
62
55
  render: DefaultStory,
63
- decorators: [withTheme],
56
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
57
+ parameters: {
58
+ layout: 'fullscreen',
59
+ },
64
60
  } satisfies Meta<typeof DefaultStory>;
65
61
 
66
62
  export default meta;
67
63
 
68
64
  type Story = StoryObj<typeof meta>;
69
65
 
70
- export const Default: Story = {};
66
+ // TODO(burdon): Scrolling.
67
+ export const Horizontal: Story = {
68
+ args: {
69
+ orientation: 'horizontal',
70
+ },
71
+ };
72
+
73
+ export const Vertical: Story = {
74
+ args: {
75
+ orientation: 'vertical',
76
+ },
77
+ };
package/src/Tabs.tsx CHANGED
@@ -10,14 +10,21 @@ import React, {
10
10
  Activity,
11
11
  type ComponentPropsWithoutRef,
12
12
  type MouseEvent,
13
+ forwardRef,
13
14
  useCallback,
14
15
  useLayoutEffect,
15
- useRef,
16
16
  } from 'react';
17
17
 
18
- import { Button, type ButtonProps, IconButton, type IconButtonProps, type ThemedClassName } from '@dxos/react-ui';
18
+ import {
19
+ Button,
20
+ type ButtonProps,
21
+ IconButton,
22
+ type IconButtonProps,
23
+ type ThemedClassName,
24
+ useForwardedRef,
25
+ } from '@dxos/react-ui';
19
26
  import { useAttention } from '@dxos/react-ui-attention';
20
- import { ghostSelectedContainerMd, mx } from '@dxos/react-ui-theme';
27
+ import { mx } from '@dxos/ui-theme';
21
28
 
22
29
  type TabsActivePart = 'list' | 'panel';
23
30
 
@@ -27,118 +34,105 @@ type TabsContextValue = {
27
34
  activePart: TabsActivePart;
28
35
  setActivePart: (nextActivePart: TabsActivePart) => void;
29
36
  attendableId?: string;
30
- verticalVariant?: 'stateful' | 'stateless';
31
37
  } & Pick<TabsPrimitive.TabsProps, 'orientation' | 'value'>;
32
38
 
33
39
  const [TabsContextProvider, useTabsContext] = createContext<TabsContextValue>(TABS_NAME, {
40
+ orientation: 'vertical',
34
41
  activePart: 'list',
35
42
  setActivePart: () => {},
36
- orientation: 'vertical',
37
43
  });
38
44
 
39
45
  type TabsRootProps = ThemedClassName<TabsPrimitive.TabsProps> &
40
- Partial<Pick<TabsContextValue, 'activePart' | 'verticalVariant' | 'attendableId'>> &
41
- Partial<{
42
- onActivePartChange: (nextActivePart: TabsActivePart) => void;
43
- defaultActivePart: TabsActivePart;
44
- }>;
45
-
46
- const TabsRoot = ({
47
- children,
48
- classNames,
49
- activePart: propsActivePart,
50
- onActivePartChange,
51
- defaultActivePart,
52
- value: propsValue,
53
- onValueChange,
54
- defaultValue,
55
- orientation = 'vertical',
56
- activationMode = 'manual',
57
- verticalVariant = 'stateful',
58
- attendableId,
59
- ...props
60
- }: TabsRootProps) => {
61
- // TODO(thure): Without these, we get Groupper/Mover `API used before initialization`, but why?
62
- const _1 = useArrowNavigationGroup();
63
- const _2 = useFocusableGroup();
64
- const [activePart = 'list', setActivePart] = useControllableState({
65
- prop: propsActivePart,
66
- onChange: onActivePartChange,
67
- defaultProp: defaultActivePart,
68
- });
69
-
70
- const [value, setValue] = useControllableState({
71
- prop: propsValue,
72
- onChange: onValueChange,
73
- defaultProp: defaultValue,
74
- });
75
-
76
- const handleValueChange = useCallback(
77
- (nextValue: string) => {
78
- setActivePart('panel');
79
- setValue(nextValue);
80
- },
81
- [value],
82
- );
83
-
84
- const { findFirstFocusable } = useFocusFinders();
85
- const tabsRoot = useRef<HTMLDivElement | null>(null);
86
-
87
- useLayoutEffect(() => {
88
- if (tabsRoot.current) {
89
- findFirstFocusable(tabsRoot.current)?.focus();
46
+ Partial<
47
+ Pick<TabsContextValue, 'activePart' | 'attendableId'> & {
48
+ onActivePartChange: (nextActivePart: TabsActivePart) => void;
49
+ defaultActivePart: TabsActivePart;
90
50
  }
91
- }, [activePart]);
92
-
93
- return (
94
- <TabsContextProvider
95
- orientation={orientation}
96
- activePart={activePart}
97
- setActivePart={setActivePart}
98
- value={value}
99
- attendableId={attendableId}
100
- verticalVariant={verticalVariant}
101
- >
102
- <TabsPrimitive.Root
103
- activationMode={activationMode}
104
- data-active={activePart}
51
+ >;
52
+
53
+ const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
54
+ (
55
+ {
56
+ children,
57
+ classNames,
58
+ activePart: propsActivePart,
59
+ onActivePartChange,
60
+ defaultActivePart,
61
+ value: propsValue,
62
+ onValueChange,
63
+ defaultValue,
64
+ orientation = 'vertical',
65
+ activationMode = 'manual',
66
+ attendableId,
67
+ ...props
68
+ },
69
+ forwardedRef,
70
+ ) => {
71
+ // const tabsRoot = useRef<HTMLDivElement | null>(null);
72
+ const tabsRoot = useForwardedRef(forwardedRef);
73
+
74
+ // TODO(thure): Without these, we get Groupper/Mover `API used before initialization`, but why?
75
+ const _1 = useArrowNavigationGroup();
76
+ const _2 = useFocusableGroup();
77
+ const [activePart = 'list', setActivePart] = useControllableState({
78
+ prop: propsActivePart,
79
+ onChange: onActivePartChange,
80
+ defaultProp: defaultActivePart,
81
+ });
82
+
83
+ const [value, setValue] = useControllableState({
84
+ prop: propsValue,
85
+ onChange: onValueChange,
86
+ defaultProp: defaultValue,
87
+ });
88
+
89
+ const handleValueChange = useCallback(
90
+ (nextValue: string) => {
91
+ setActivePart('panel');
92
+ setValue(nextValue);
93
+ },
94
+ [value],
95
+ );
96
+
97
+ const { findFirstFocusable } = useFocusFinders();
98
+
99
+ useLayoutEffect(() => {
100
+ if (tabsRoot.current) {
101
+ findFirstFocusable(tabsRoot.current)?.focus();
102
+ }
103
+ }, [activePart]);
104
+
105
+ return (
106
+ <TabsContextProvider
105
107
  orientation={orientation}
106
- {...props}
108
+ activePart={activePart}
109
+ setActivePart={setActivePart}
107
110
  value={value}
108
- onValueChange={handleValueChange}
109
- className={mx(
110
- 'overflow-hidden',
111
- orientation === 'vertical' &&
112
- verticalVariant === 'stateful' &&
113
- '[&[data-active=list]_[role=tabpanel]]:invisible @md:[&[data-active=list]_[role=tabpanel]]:visible',
114
- classNames,
115
- )}
116
- ref={tabsRoot}
111
+ attendableId={attendableId}
117
112
  >
118
- {children}
119
- </TabsPrimitive.Root>
120
- </TabsContextProvider>
121
- );
122
- };
113
+ <TabsPrimitive.Root
114
+ {...props}
115
+ className={mx('overflow-hidden', classNames)}
116
+ orientation={orientation}
117
+ activationMode={activationMode}
118
+ data-active={activePart}
119
+ value={value}
120
+ onValueChange={handleValueChange}
121
+ ref={tabsRoot}
122
+ >
123
+ {children}
124
+ </TabsPrimitive.Root>
125
+ </TabsContextProvider>
126
+ );
127
+ },
128
+ );
123
129
 
124
130
  type TabsViewportProps = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
125
131
 
126
132
  const TabsViewport = ({ classNames, children, ...props }: TabsViewportProps) => {
127
- const { orientation, activePart, verticalVariant } = useTabsContext('TabsViewport');
133
+ const { activePart } = useTabsContext('TabsViewport');
128
134
  return (
129
- <div
130
- role='none'
131
- {...props}
132
- data-active={activePart}
133
- className={mx(
134
- orientation === 'vertical' &&
135
- verticalVariant === 'stateful' && [
136
- 'grid is-[200%] grid-cols-2 data-[active=panel]:mis-[-100%]',
137
- '@md:is-auto @md:data-[active=panel]:mis-0 @md:grid-cols-[minmax(min-content,1fr)_3fr] @md:gap-1',
138
- ],
139
- classNames,
140
- )}
141
- >
135
+ <div role='none' {...props} data-active={activePart} className={mx(classNames)}>
142
136
  {children}
143
137
  </div>
144
138
  );
@@ -147,16 +141,15 @@ const TabsViewport = ({ classNames, children, ...props }: TabsViewportProps) =>
147
141
  type TabsTablistProps = ThemedClassName<TabsPrimitive.TabsListProps>;
148
142
 
149
143
  const TabsTablist = ({ children, classNames, ...props }: TabsTablistProps) => {
150
- const { orientation, verticalVariant } = useTabsContext('TabsTablist');
144
+ const { orientation } = useTabsContext('TabsTablist');
151
145
  return (
152
146
  <TabsPrimitive.List
153
147
  {...props}
154
148
  data-arrow-keys={orientation === 'vertical' ? 'up down' : 'left right'}
155
149
  className={mx(
156
- 'max-bs-full is-full',
157
- // NOTE: Padding should be common to Toolbar.
158
- orientation === 'vertical' ? 'overflow-y-auto' : 'flex items-stretch justify-start overflow-x-auto p-1 gap-1',
159
- orientation === 'vertical' && verticalVariant === 'stateful' && 'place-self-start p-1',
150
+ 'max-h-full w-full',
151
+ // TODO(burdon): Should be embeddable inside Toolbar (if horizontal).
152
+ orientation === 'vertical' ? 'overflow-y-auto' : 'flex p-1 gap-1 items-stretch justify-start overflow-x-auto',
160
153
  classNames,
161
154
  )}
162
155
  >
@@ -172,17 +165,17 @@ const TabsBackButton = ({ onClick, classNames, ...props }: ButtonProps) => {
172
165
  setActivePart('list');
173
166
  return onClick?.(event);
174
167
  },
175
- [onClick, setActivePart],
168
+ [setActivePart, onClick],
176
169
  );
177
170
 
178
- return <Button {...props} classNames={['is-full text-start @md:hidden mbe-2', classNames]} onClick={handleClick} />;
171
+ return <Button {...props} classNames={['@md:hidden text-start', classNames]} onClick={handleClick} />;
179
172
  };
180
173
 
181
174
  type TabsTabGroupHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'h2'>>;
182
175
 
183
176
  const TabsTabGroupHeading = ({ children, classNames, ...props }: ThemedClassName<TabsTabGroupHeadingProps>) => {
184
177
  return (
185
- <h2 {...props} className={mx('mlb-1 pli-2 text-sm text-unAccent', classNames)}>
178
+ <h2 {...props} className={mx('my-1 px-2 text-sm text-un-accent', classNames)}>
186
179
  {children}
187
180
  </h2>
188
181
  );
@@ -206,17 +199,16 @@ const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProp
206
199
  return (
207
200
  <TabsPrimitive.Trigger value={value} asChild>
208
201
  <Button
209
- density='fine'
202
+ {...props}
210
203
  variant={
211
204
  orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
212
205
  }
213
- {...props}
214
- onClick={handleClick}
215
206
  classNames={[
216
- orientation === 'vertical' && 'block justify-start text-start is-full',
217
- orientation === 'vertical' && ghostSelectedContainerMd,
207
+ orientation === 'vertical' && 'block justify-start text-start w-full',
208
+ orientation === 'vertical' && 'dx-selected',
218
209
  classNames,
219
210
  ]}
211
+ onClick={handleClick}
220
212
  >
221
213
  {children}
222
214
  </Button>
@@ -242,25 +234,25 @@ const TabsIconTab = ({ value, classNames, onClick, ...props }: TabsIconTabProps)
242
234
  return (
243
235
  <TabsPrimitive.Trigger value={value} asChild>
244
236
  <IconButton
245
- density='fine'
237
+ {...props}
246
238
  variant={
247
239
  orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
248
240
  }
249
- {...props}
250
- onClick={handleClick}
251
241
  classNames={[
252
- orientation === 'vertical' && 'justify-start text-start is-full',
253
- orientation === 'vertical' && ghostSelectedContainerMd,
242
+ orientation === 'vertical' && 'justify-start text-start w-full',
243
+ orientation === 'vertical' && 'dx-selected',
254
244
  classNames,
255
245
  ]}
246
+ onClick={handleClick}
256
247
  />
257
248
  </TabsPrimitive.Trigger>
258
249
  );
259
250
  };
260
251
 
261
- type TabsTabpanelProps = ThemedClassName<TabsPrimitive.TabsContentProps>;
252
+ type TabsPanelProps = ThemedClassName<TabsPrimitive.TabsContentProps>;
262
253
 
263
- const TabsTabpanel = ({ classNames, children, ...props }: TabsTabpanelProps) => {
254
+ // TODO(burdon): Make slottable.
255
+ const TabsPanel = ({ classNames, children, ...props }: TabsPanelProps) => {
264
256
  const { value: contextValue } = useTabsContext('TabsTab');
265
257
  return (
266
258
  <Activity mode={contextValue === props.value ? 'visible' : 'hidden'}>
@@ -280,7 +272,7 @@ export const Tabs = {
280
272
  IconTab: TabsIconTab,
281
273
  TabPrimitive: TabsPrimitive.Trigger,
282
274
  TabGroupHeading: TabsTabGroupHeading,
283
- Tabpanel: TabsTabpanel,
275
+ Panel: TabsPanel,
284
276
  BackButton: TabsBackButton,
285
277
  Viewport: TabsViewport,
286
278
  };
@@ -292,6 +284,6 @@ export type {
292
284
  TabsTabProps,
293
285
  TabsTabPrimitiveProps,
294
286
  TabsTabGroupHeadingProps,
295
- TabsTabpanelProps,
287
+ TabsPanelProps,
296
288
  TabsViewportProps,
297
289
  };