@dxos/react-ui-tabs 0.8.4-main.dedc0f3 → 0.8.4-main.e00bdcdb52

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.dedc0f3",
3
+ "version": "0.8.4-main.e00bdcdb52",
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
  ".": {
@@ -17,41 +21,37 @@
17
21
  }
18
22
  },
19
23
  "types": "dist/types/src/index.d.ts",
20
- "typesVersions": {
21
- "*": {}
22
- },
23
24
  "files": [
24
25
  "dist",
25
26
  "src"
26
27
  ],
27
28
  "dependencies": {
28
- "@fluentui/react-tabster": "^9.24.2",
29
- "@preact-signals/safe-react": "^0.9.0",
29
+ "@fluentui/react-tabster": "9.26.11",
30
30
  "@radix-ui/primitive": "1.1.1",
31
31
  "@radix-ui/react-context": "1.1.1",
32
32
  "@radix-ui/react-primitive": "2.0.2",
33
33
  "@radix-ui/react-slot": "1.1.2",
34
34
  "@radix-ui/react-tabs": "1.1.3",
35
35
  "@radix-ui/react-use-controllable-state": "1.1.0",
36
- "@dxos/react-ui-attention": "0.8.4-main.dedc0f3",
37
- "@dxos/util": "0.8.4-main.dedc0f3"
36
+ "@dxos/util": "0.8.4-main.e00bdcdb52",
37
+ "@dxos/react-ui-attention": "0.8.4-main.e00bdcdb52"
38
38
  },
39
39
  "devDependencies": {
40
- "@types/react": "~18.2.0",
41
- "@types/react-dom": "~18.2.0",
42
- "react": "~18.2.0",
43
- "react-dom": "~18.2.0",
44
- "vite": "7.1.1",
45
- "@dxos/random": "0.8.4-main.dedc0f3",
46
- "@dxos/react-ui": "0.8.4-main.dedc0f3",
47
- "@dxos/storybook-utils": "0.8.4-main.dedc0f3",
48
- "@dxos/react-ui-theme": "0.8.4-main.dedc0f3"
40
+ "@types/react": "~19.2.7",
41
+ "@types/react-dom": "~19.2.3",
42
+ "react": "~19.2.3",
43
+ "react-dom": "~19.2.3",
44
+ "vite": "^8.0.10",
45
+ "@dxos/random": "0.8.4-main.e00bdcdb52",
46
+ "@dxos/react-ui": "0.8.4-main.e00bdcdb52",
47
+ "@dxos/storybook-utils": "0.8.4-main.e00bdcdb52",
48
+ "@dxos/ui-theme": "0.8.4-main.e00bdcdb52"
49
49
  },
50
50
  "peerDependencies": {
51
- "react": "~18.2.0",
52
- "react-dom": "~18.2.0",
53
- "@dxos/react-ui": "0.8.4-main.dedc0f3",
54
- "@dxos/react-ui-theme": "0.8.4-main.dedc0f3"
51
+ "react": "~19.2.3",
52
+ "react-dom": "~19.2.3",
53
+ "@dxos/react-ui": "0.8.4-main.e00bdcdb52",
54
+ "@dxos/ui-theme": "0.8.4-main.e00bdcdb52"
55
55
  },
56
56
  "publishConfig": {
57
57
  "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/storybook-utils';
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
@@ -6,11 +6,25 @@ import { useArrowNavigationGroup, useFocusFinders, useFocusableGroup } from '@fl
6
6
  import { createContext } from '@radix-ui/react-context';
7
7
  import * as TabsPrimitive from '@radix-ui/react-tabs';
8
8
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
9
- import React, { type ComponentPropsWithoutRef, type MouseEvent, useCallback, useLayoutEffect, useRef } from 'react';
10
-
11
- import { Button, type ButtonProps, IconButton, type IconButtonProps, type ThemedClassName } from '@dxos/react-ui';
9
+ import React, {
10
+ Activity,
11
+ type ComponentPropsWithoutRef,
12
+ type MouseEvent,
13
+ forwardRef,
14
+ useCallback,
15
+ useLayoutEffect,
16
+ } from 'react';
17
+
18
+ import {
19
+ Button,
20
+ type ButtonProps,
21
+ IconButton,
22
+ type IconButtonProps,
23
+ type ThemedClassName,
24
+ useForwardedRef,
25
+ } from '@dxos/react-ui';
12
26
  import { useAttention } from '@dxos/react-ui-attention';
13
- import { ghostSelectedContainerMd, mx } from '@dxos/react-ui-theme';
27
+ import { mx } from '@dxos/ui-theme';
14
28
 
15
29
  type TabsActivePart = 'list' | 'panel';
16
30
 
@@ -20,118 +34,105 @@ type TabsContextValue = {
20
34
  activePart: TabsActivePart;
21
35
  setActivePart: (nextActivePart: TabsActivePart) => void;
22
36
  attendableId?: string;
23
- verticalVariant?: 'stateful' | 'stateless';
24
37
  } & Pick<TabsPrimitive.TabsProps, 'orientation' | 'value'>;
25
38
 
26
39
  const [TabsContextProvider, useTabsContext] = createContext<TabsContextValue>(TABS_NAME, {
40
+ orientation: 'vertical',
27
41
  activePart: 'list',
28
42
  setActivePart: () => {},
29
- orientation: 'vertical',
30
43
  });
31
44
 
32
45
  type TabsRootProps = ThemedClassName<TabsPrimitive.TabsProps> &
33
- Partial<Pick<TabsContextValue, 'activePart' | 'verticalVariant' | 'attendableId'>> &
34
- Partial<{
35
- onActivePartChange: (nextActivePart: TabsActivePart) => void;
36
- defaultActivePart: TabsActivePart;
37
- }>;
38
-
39
- const TabsRoot = ({
40
- children,
41
- classNames,
42
- activePart: propsActivePart,
43
- onActivePartChange,
44
- defaultActivePart,
45
- value: propsValue,
46
- onValueChange,
47
- defaultValue,
48
- orientation = 'vertical',
49
- activationMode = 'manual',
50
- verticalVariant = 'stateful',
51
- attendableId,
52
- ...props
53
- }: TabsRootProps) => {
54
- // TODO(thure): Without these, we get Groupper/Mover `API used before initialization`, but why?
55
- const _1 = useArrowNavigationGroup();
56
- const _2 = useFocusableGroup();
57
- const [activePart = 'list', setActivePart] = useControllableState({
58
- prop: propsActivePart,
59
- onChange: onActivePartChange,
60
- defaultProp: defaultActivePart,
61
- });
62
-
63
- const [value, setValue] = useControllableState({
64
- prop: propsValue,
65
- onChange: onValueChange,
66
- defaultProp: defaultValue,
67
- });
68
-
69
- const handleValueChange = useCallback(
70
- (nextValue: string) => {
71
- setActivePart('panel');
72
- setValue(nextValue);
73
- },
74
- [value],
75
- );
76
-
77
- const { findFirstFocusable } = useFocusFinders();
78
- const tabsRoot = useRef<HTMLDivElement | null>(null);
79
-
80
- useLayoutEffect(() => {
81
- if (tabsRoot.current) {
82
- findFirstFocusable(tabsRoot.current)?.focus();
46
+ Partial<
47
+ Pick<TabsContextValue, 'activePart' | 'attendableId'> & {
48
+ onActivePartChange: (nextActivePart: TabsActivePart) => void;
49
+ defaultActivePart: TabsActivePart;
83
50
  }
84
- }, [activePart]);
85
-
86
- return (
87
- <TabsContextProvider
88
- orientation={orientation}
89
- activePart={activePart}
90
- setActivePart={setActivePart}
91
- value={value}
92
- attendableId={attendableId}
93
- verticalVariant={verticalVariant}
94
- >
95
- <TabsPrimitive.Root
96
- activationMode={activationMode}
97
- 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
98
107
  orientation={orientation}
99
- {...props}
108
+ activePart={activePart}
109
+ setActivePart={setActivePart}
100
110
  value={value}
101
- onValueChange={handleValueChange}
102
- className={mx(
103
- 'overflow-hidden',
104
- orientation === 'vertical' &&
105
- verticalVariant === 'stateful' &&
106
- '[&[data-active=list]_[role=tabpanel]]:invisible @md:[&[data-active=list]_[role=tabpanel]]:visible',
107
- classNames,
108
- )}
109
- ref={tabsRoot}
111
+ attendableId={attendableId}
110
112
  >
111
- {children}
112
- </TabsPrimitive.Root>
113
- </TabsContextProvider>
114
- );
115
- };
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
+ );
116
129
 
117
130
  type TabsViewportProps = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
118
131
 
119
132
  const TabsViewport = ({ classNames, children, ...props }: TabsViewportProps) => {
120
- const { orientation, activePart, verticalVariant } = useTabsContext('TabsViewport');
133
+ const { activePart } = useTabsContext('TabsViewport');
121
134
  return (
122
- <div
123
- role='none'
124
- {...props}
125
- data-active={activePart}
126
- className={mx(
127
- orientation === 'vertical' &&
128
- verticalVariant === 'stateful' && [
129
- 'grid is-[200%] grid-cols-2 data-[active=panel]:mis-[-100%]',
130
- '@md:is-auto @md:data-[active=panel]:mis-0 @md:grid-cols-[minmax(min-content,1fr)_3fr] @md:gap-1',
131
- ],
132
- classNames,
133
- )}
134
- >
135
+ <div role='none' {...props} data-active={activePart} className={mx(classNames)}>
135
136
  {children}
136
137
  </div>
137
138
  );
@@ -140,16 +141,15 @@ const TabsViewport = ({ classNames, children, ...props }: TabsViewportProps) =>
140
141
  type TabsTablistProps = ThemedClassName<TabsPrimitive.TabsListProps>;
141
142
 
142
143
  const TabsTablist = ({ children, classNames, ...props }: TabsTablistProps) => {
143
- const { orientation, verticalVariant } = useTabsContext('TabsTablist');
144
+ const { orientation } = useTabsContext('TabsTablist');
144
145
  return (
145
146
  <TabsPrimitive.List
146
147
  {...props}
147
148
  data-arrow-keys={orientation === 'vertical' ? 'up down' : 'left right'}
148
149
  className={mx(
149
- 'max-bs-full is-full',
150
- // NOTE: Padding should be common to Toolbar.
151
- orientation === 'vertical' ? 'overflow-y-auto' : 'flex items-stretch justify-start overflow-x-auto p-1 gap-1',
152
- 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',
153
153
  classNames,
154
154
  )}
155
155
  >
@@ -165,17 +165,17 @@ const TabsBackButton = ({ onClick, classNames, ...props }: ButtonProps) => {
165
165
  setActivePart('list');
166
166
  return onClick?.(event);
167
167
  },
168
- [onClick, setActivePart],
168
+ [setActivePart, onClick],
169
169
  );
170
170
 
171
- 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} />;
172
172
  };
173
173
 
174
174
  type TabsTabGroupHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'h2'>>;
175
175
 
176
176
  const TabsTabGroupHeading = ({ children, classNames, ...props }: ThemedClassName<TabsTabGroupHeadingProps>) => {
177
177
  return (
178
- <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)}>
179
179
  {children}
180
180
  </h2>
181
181
  );
@@ -199,17 +199,16 @@ const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProp
199
199
  return (
200
200
  <TabsPrimitive.Trigger value={value} asChild>
201
201
  <Button
202
- density='fine'
202
+ {...props}
203
203
  variant={
204
204
  orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
205
205
  }
206
- {...props}
207
- onClick={handleClick}
208
206
  classNames={[
209
- orientation === 'vertical' && 'block justify-start text-start is-full',
210
- orientation === 'vertical' && ghostSelectedContainerMd,
207
+ orientation === 'vertical' && 'block justify-start text-start w-full',
208
+ orientation === 'vertical' && 'dx-selected',
211
209
  classNames,
212
210
  ]}
211
+ onClick={handleClick}
213
212
  >
214
213
  {children}
215
214
  </Button>
@@ -235,29 +234,31 @@ const TabsIconTab = ({ value, classNames, onClick, ...props }: TabsIconTabProps)
235
234
  return (
236
235
  <TabsPrimitive.Trigger value={value} asChild>
237
236
  <IconButton
238
- density='fine'
237
+ {...props}
239
238
  variant={
240
239
  orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
241
240
  }
242
- {...props}
243
- onClick={handleClick}
244
241
  classNames={[
245
- orientation === 'vertical' && 'justify-start text-start is-full',
246
- orientation === 'vertical' && ghostSelectedContainerMd,
242
+ orientation === 'vertical' && 'justify-start text-start w-full',
243
+ orientation === 'vertical' && 'dx-selected',
247
244
  classNames,
248
245
  ]}
246
+ onClick={handleClick}
249
247
  />
250
248
  </TabsPrimitive.Trigger>
251
249
  );
252
250
  };
253
251
 
254
- type TabsTabpanelProps = ThemedClassName<TabsPrimitive.TabsContentProps>;
252
+ type TabsPanelProps = ThemedClassName<TabsPrimitive.TabsContentProps>;
255
253
 
256
- const TabsTabpanel = ({ classNames, children, ...props }: TabsTabpanelProps) => {
254
+ const TabsPanel = ({ classNames, children, ...props }: TabsPanelProps) => {
255
+ const { value: contextValue } = useTabsContext('TabsTab');
257
256
  return (
258
- <TabsPrimitive.Content {...props} className={mx('dx-focus-ring-inset-over-all', classNames)}>
259
- {children}
260
- </TabsPrimitive.Content>
257
+ <Activity mode={contextValue === props.value ? 'visible' : 'hidden'}>
258
+ <TabsPrimitive.Content {...props} className={mx('p-0! dx-focus-ring-inset-over-all', classNames)}>
259
+ {children}
260
+ </TabsPrimitive.Content>
261
+ </Activity>
261
262
  );
262
263
  };
263
264
 
@@ -270,7 +271,7 @@ export const Tabs = {
270
271
  IconTab: TabsIconTab,
271
272
  TabPrimitive: TabsPrimitive.Trigger,
272
273
  TabGroupHeading: TabsTabGroupHeading,
273
- Tabpanel: TabsTabpanel,
274
+ Panel: TabsPanel,
274
275
  BackButton: TabsBackButton,
275
276
  Viewport: TabsViewport,
276
277
  };
@@ -282,6 +283,6 @@ export type {
282
283
  TabsTabProps,
283
284
  TabsTabPrimitiveProps,
284
285
  TabsTabGroupHeadingProps,
285
- TabsTabpanelProps,
286
+ TabsPanelProps,
286
287
  TabsViewportProps,
287
288
  };