@dxos/react-ui-tabs 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8
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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +152 -167
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +152 -167
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/Tabs.d.ts +67 -12
- package/dist/types/src/Tabs.d.ts.map +1 -1
- package/dist/types/src/Tabs.stories.d.ts +21 -6
- package/dist/types/src/Tabs.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -23
- package/src/Tabs.stories.tsx +57 -45
- package/src/Tabs.tsx +209 -124
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-tabs",
|
|
3
|
-
"version": "0.8.4-
|
|
3
|
+
"version": "0.8.4-staging.60fe92afc8",
|
|
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
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
|
-
"sideEffects":
|
|
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": "
|
|
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-
|
|
37
|
-
"@dxos/util": "0.8.4-
|
|
36
|
+
"@dxos/react-ui-attention": "0.8.4-staging.60fe92afc8",
|
|
37
|
+
"@dxos/util": "0.8.4-staging.60fe92afc8"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/react": "~
|
|
41
|
-
"@types/react-dom": "~
|
|
42
|
-
"react": "~
|
|
43
|
-
"react-dom": "~
|
|
44
|
-
"vite": "
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
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.16",
|
|
45
|
+
"@dxos/react-ui": "0.8.4-staging.60fe92afc8",
|
|
46
|
+
"@dxos/storybook-utils": "0.8.4-staging.60fe92afc8",
|
|
47
|
+
"@dxos/ui-theme": "0.8.4-staging.60fe92afc8",
|
|
48
|
+
"@dxos/random": "0.8.4-staging.60fe92afc8"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"react": "~
|
|
52
|
-
"react-dom": "~
|
|
53
|
-
"@dxos/react-ui": "0.8.4-
|
|
54
|
-
"@dxos/
|
|
51
|
+
"react": "~19.2.3",
|
|
52
|
+
"react-dom": "~19.2.3",
|
|
53
|
+
"@dxos/react-ui": "0.8.4-staging.60fe92afc8",
|
|
54
|
+
"@dxos/ui-theme": "0.8.4-staging.60fe92afc8"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
package/src/Tabs.stories.tsx
CHANGED
|
@@ -2,64 +2,76 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
5
6
|
import React from 'react';
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { random } from '@dxos/random';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
10
11
|
|
|
11
|
-
import { Tabs
|
|
12
|
+
import { Tabs, TabsRootProps } from './Tabs';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
random.seed(1234);
|
|
15
|
+
|
|
16
|
+
const DefaultStory = ({ orientation }: TabsRootProps) => {
|
|
17
|
+
return (
|
|
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>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
14
43
|
|
|
15
44
|
const content = [...Array(24)].reduce((acc: { [key: string]: { title: string; panel: string } }, _, index) => {
|
|
16
45
|
acc[`t${index}`] = {
|
|
17
|
-
title:
|
|
18
|
-
panel:
|
|
46
|
+
title: random.commerce.productName(),
|
|
47
|
+
panel: random.lorem.paragraphs(5),
|
|
19
48
|
};
|
|
20
49
|
return acc;
|
|
21
50
|
}, {});
|
|
22
51
|
|
|
23
|
-
|
|
52
|
+
const meta = {
|
|
24
53
|
title: 'ui/react-ui-tabs/Tabs',
|
|
25
|
-
component:
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
component: Tabs.Root,
|
|
55
|
+
render: DefaultStory,
|
|
56
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
57
|
+
parameters: {
|
|
58
|
+
layout: 'fullscreen',
|
|
59
|
+
},
|
|
60
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
61
|
+
|
|
62
|
+
export default meta;
|
|
63
|
+
|
|
64
|
+
type Story = StoryObj<typeof meta>;
|
|
65
|
+
|
|
66
|
+
// TODO(burdon): Scrolling.
|
|
67
|
+
export const Horizontal: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
orientation: 'horizontal',
|
|
70
|
+
},
|
|
28
71
|
};
|
|
29
72
|
|
|
30
|
-
export const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<Dialog.Root open>
|
|
34
|
-
<Dialog.Overlay blockAlign='start'>
|
|
35
|
-
<Dialog.Content classNames='is-[calc(100dvw-4rem)] !max-is-full'>
|
|
36
|
-
<NaturalTabs.Root orientation='vertical' defaultValue={Object.keys(content)[3]} defaultActivePart='list'>
|
|
37
|
-
<NaturalTabs.Viewport>
|
|
38
|
-
<NaturalTabs.Tablist>
|
|
39
|
-
{Object.entries(content).map(([id, { title }]) => {
|
|
40
|
-
return (
|
|
41
|
-
<NaturalTabs.Tab key={id} value={id}>
|
|
42
|
-
{title}
|
|
43
|
-
</NaturalTabs.Tab>
|
|
44
|
-
);
|
|
45
|
-
})}
|
|
46
|
-
</NaturalTabs.Tablist>
|
|
47
|
-
{Object.entries(content).map(([id, { panel }]) => {
|
|
48
|
-
return (
|
|
49
|
-
<NaturalTabs.Tabpanel key={id} value={id} classNames='m-1'>
|
|
50
|
-
<NaturalTabs.BackButton density='fine'>
|
|
51
|
-
<Icon icon='ph--arrow-left--bold' size={4} />
|
|
52
|
-
<span>Back to tab list</span>
|
|
53
|
-
</NaturalTabs.BackButton>
|
|
54
|
-
<p className='pli-1'>{panel}</p>
|
|
55
|
-
</NaturalTabs.Tabpanel>
|
|
56
|
-
);
|
|
57
|
-
})}
|
|
58
|
-
</NaturalTabs.Viewport>
|
|
59
|
-
</NaturalTabs.Root>
|
|
60
|
-
</Dialog.Content>
|
|
61
|
-
</Dialog.Overlay>
|
|
62
|
-
</Dialog.Root>
|
|
63
|
-
);
|
|
73
|
+
export const Vertical: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
orientation: 'vertical',
|
|
64
76
|
},
|
|
65
77
|
};
|
package/src/Tabs.tsx
CHANGED
|
@@ -6,149 +6,164 @@ 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
|
|
9
|
+
import React, { type ComponentPropsWithoutRef, type MouseEvent, forwardRef, useCallback, useLayoutEffect } from 'react';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Button,
|
|
13
|
+
type ButtonProps,
|
|
14
|
+
IconButton,
|
|
15
|
+
type IconButtonProps,
|
|
16
|
+
type ThemedClassName,
|
|
17
|
+
useForwardedRef,
|
|
18
|
+
} from '@dxos/react-ui';
|
|
12
19
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
13
|
-
import {
|
|
20
|
+
import { mx } from '@dxos/ui-theme';
|
|
21
|
+
|
|
22
|
+
// TODO(burdon): Move to @dxos/react-ui.
|
|
14
23
|
|
|
15
24
|
type TabsActivePart = 'list' | 'panel';
|
|
16
25
|
|
|
17
26
|
const TABS_NAME = 'Tabs';
|
|
18
27
|
|
|
28
|
+
//
|
|
29
|
+
// Context
|
|
30
|
+
//
|
|
31
|
+
|
|
19
32
|
type TabsContextValue = {
|
|
20
33
|
activePart: TabsActivePart;
|
|
21
34
|
setActivePart: (nextActivePart: TabsActivePart) => void;
|
|
22
35
|
attendableId?: string;
|
|
23
|
-
verticalVariant?: 'stateful' | 'stateless';
|
|
24
36
|
} & Pick<TabsPrimitive.TabsProps, 'orientation' | 'value'>;
|
|
25
37
|
|
|
26
38
|
const [TabsContextProvider, useTabsContext] = createContext<TabsContextValue>(TABS_NAME, {
|
|
39
|
+
orientation: 'vertical',
|
|
27
40
|
activePart: 'list',
|
|
28
41
|
setActivePart: () => {},
|
|
29
|
-
orientation: 'vertical',
|
|
30
42
|
});
|
|
31
43
|
|
|
44
|
+
//
|
|
45
|
+
// Root
|
|
46
|
+
//
|
|
47
|
+
|
|
32
48
|
type TabsRootProps = ThemedClassName<TabsPrimitive.TabsProps> &
|
|
33
|
-
Partial<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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);
|
|
49
|
+
Partial<
|
|
50
|
+
Pick<TabsContextValue, 'activePart' | 'attendableId'> & {
|
|
51
|
+
onActivePartChange: (nextActivePart: TabsActivePart) => void;
|
|
52
|
+
defaultActivePart: TabsActivePart;
|
|
53
|
+
}
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
|
|
57
|
+
(
|
|
58
|
+
{
|
|
59
|
+
children,
|
|
60
|
+
classNames,
|
|
61
|
+
activePart: propsActivePart,
|
|
62
|
+
onActivePartChange,
|
|
63
|
+
defaultActivePart,
|
|
64
|
+
value: propsValue,
|
|
65
|
+
onValueChange,
|
|
66
|
+
defaultValue,
|
|
67
|
+
orientation = 'vertical',
|
|
68
|
+
activationMode = 'manual',
|
|
69
|
+
attendableId,
|
|
70
|
+
...props
|
|
73
71
|
},
|
|
74
|
-
|
|
75
|
-
)
|
|
72
|
+
forwardedRef,
|
|
73
|
+
) => {
|
|
74
|
+
const tabsRoot = useForwardedRef(forwardedRef);
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// TODO(thure): Without these, we get Groupper/Mover `API used before initialization`, but why?
|
|
77
|
+
const _1 = useArrowNavigationGroup();
|
|
78
|
+
const _2 = useFocusableGroup();
|
|
79
|
+
const [activePart = 'list', setActivePart] = useControllableState({
|
|
80
|
+
prop: propsActivePart,
|
|
81
|
+
onChange: onActivePartChange,
|
|
82
|
+
defaultProp: defaultActivePart,
|
|
83
|
+
});
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
const [value, setValue] = useControllableState({
|
|
86
|
+
prop: propsValue,
|
|
87
|
+
onChange: onValueChange,
|
|
88
|
+
defaultProp: defaultValue,
|
|
89
|
+
});
|
|
85
90
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
value
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
const handleValueChange = useCallback(
|
|
92
|
+
(nextValue: string) => {
|
|
93
|
+
setActivePart('panel');
|
|
94
|
+
setValue(nextValue);
|
|
95
|
+
},
|
|
96
|
+
[value],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const { findFirstFocusable } = useFocusFinders();
|
|
100
|
+
|
|
101
|
+
useLayoutEffect(() => {
|
|
102
|
+
if (tabsRoot.current) {
|
|
103
|
+
findFirstFocusable(tabsRoot.current)?.focus();
|
|
104
|
+
}
|
|
105
|
+
}, [activePart]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<TabsContextProvider
|
|
98
109
|
orientation={orientation}
|
|
99
|
-
{
|
|
110
|
+
activePart={activePart}
|
|
111
|
+
setActivePart={setActivePart}
|
|
100
112
|
value={value}
|
|
101
|
-
|
|
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}
|
|
113
|
+
attendableId={attendableId}
|
|
110
114
|
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
115
|
+
<TabsPrimitive.Root
|
|
116
|
+
{...props}
|
|
117
|
+
className={mx('overflow-hidden', classNames)}
|
|
118
|
+
orientation={orientation}
|
|
119
|
+
activationMode={activationMode}
|
|
120
|
+
data-active={activePart}
|
|
121
|
+
value={value}
|
|
122
|
+
onValueChange={handleValueChange}
|
|
123
|
+
ref={tabsRoot}
|
|
124
|
+
>
|
|
125
|
+
{children}
|
|
126
|
+
</TabsPrimitive.Root>
|
|
127
|
+
</TabsContextProvider>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
TabsRoot.displayName = 'Tabs.Root';
|
|
133
|
+
|
|
134
|
+
//
|
|
135
|
+
// Viewport
|
|
136
|
+
//
|
|
116
137
|
|
|
117
138
|
type TabsViewportProps = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
|
|
118
139
|
|
|
119
140
|
const TabsViewport = ({ classNames, children, ...props }: TabsViewportProps) => {
|
|
120
|
-
const {
|
|
141
|
+
const { activePart } = useTabsContext('TabsViewport');
|
|
121
142
|
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
|
-
>
|
|
143
|
+
<div {...props} data-active={activePart} className={mx(classNames)}>
|
|
135
144
|
{children}
|
|
136
145
|
</div>
|
|
137
146
|
);
|
|
138
147
|
};
|
|
139
148
|
|
|
149
|
+
TabsViewport.displayName = 'Tabs.Viewport';
|
|
150
|
+
|
|
151
|
+
//
|
|
152
|
+
// Tablist
|
|
153
|
+
//
|
|
154
|
+
|
|
140
155
|
type TabsTablistProps = ThemedClassName<TabsPrimitive.TabsListProps>;
|
|
141
156
|
|
|
142
157
|
const TabsTablist = ({ children, classNames, ...props }: TabsTablistProps) => {
|
|
143
|
-
const { orientation
|
|
158
|
+
const { orientation } = useTabsContext('TabsTablist');
|
|
144
159
|
return (
|
|
145
160
|
<TabsPrimitive.List
|
|
146
161
|
{...props}
|
|
162
|
+
data-arrow-keys={orientation === 'vertical' ? 'up down' : 'left right'}
|
|
147
163
|
className={mx(
|
|
148
|
-
'max-
|
|
149
|
-
//
|
|
150
|
-
orientation === 'vertical' ? 'overflow-y-auto' : 'flex items-stretch justify-start overflow-x-auto
|
|
151
|
-
orientation === 'vertical' && verticalVariant === 'stateful' && 'place-self-start p-1',
|
|
164
|
+
'max-h-full w-full',
|
|
165
|
+
// TODO(burdon): Should be embeddable inside Toolbar (if horizontal).
|
|
166
|
+
orientation === 'vertical' ? 'overflow-y-auto' : 'flex p-1 gap-1 items-stretch justify-start overflow-x-auto',
|
|
152
167
|
classNames,
|
|
153
168
|
)}
|
|
154
169
|
>
|
|
@@ -157,6 +172,12 @@ const TabsTablist = ({ children, classNames, ...props }: TabsTablistProps) => {
|
|
|
157
172
|
);
|
|
158
173
|
};
|
|
159
174
|
|
|
175
|
+
TabsTablist.displayName = 'Tabs.Tablist';
|
|
176
|
+
|
|
177
|
+
//
|
|
178
|
+
// BackButton
|
|
179
|
+
//
|
|
180
|
+
|
|
160
181
|
const TabsBackButton = ({ onClick, classNames, ...props }: ButtonProps) => {
|
|
161
182
|
const { setActivePart } = useTabsContext('TabsBackButton');
|
|
162
183
|
const handleClick = useCallback(
|
|
@@ -164,27 +185,38 @@ const TabsBackButton = ({ onClick, classNames, ...props }: ButtonProps) => {
|
|
|
164
185
|
setActivePart('list');
|
|
165
186
|
return onClick?.(event);
|
|
166
187
|
},
|
|
167
|
-
[
|
|
188
|
+
[setActivePart, onClick],
|
|
168
189
|
);
|
|
169
190
|
|
|
170
|
-
return <Button {...props} classNames={['
|
|
191
|
+
return <Button {...props} classNames={['@md:hidden text-start', classNames]} onClick={handleClick} />;
|
|
171
192
|
};
|
|
172
193
|
|
|
194
|
+
TabsBackButton.displayName = 'Tabs.BackButton';
|
|
195
|
+
|
|
196
|
+
//
|
|
197
|
+
// TabGroupHeading
|
|
198
|
+
//
|
|
199
|
+
|
|
173
200
|
type TabsTabGroupHeadingProps = ThemedClassName<ComponentPropsWithoutRef<'h2'>>;
|
|
174
201
|
|
|
175
|
-
const TabsTabGroupHeading = ({ children, classNames, ...props }:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
const TabsTabGroupHeading = ({ children, classNames, ...props }: TabsTabGroupHeadingProps) => (
|
|
203
|
+
<h2 {...props} className={mx('my-1 px-2 text-sm text-un-accent', classNames)}>
|
|
204
|
+
{children}
|
|
205
|
+
</h2>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
TabsTabGroupHeading.displayName = 'Tabs.TabGroupHeading';
|
|
209
|
+
|
|
210
|
+
//
|
|
211
|
+
// Tab
|
|
212
|
+
//
|
|
182
213
|
|
|
183
214
|
type TabsTabProps = ButtonProps & Pick<TabsPrimitive.TabsTriggerProps, 'value'>;
|
|
184
215
|
|
|
185
216
|
const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProps) => {
|
|
186
217
|
const { setActivePart, orientation, value: contextValue, attendableId } = useTabsContext('TabsTab');
|
|
187
218
|
const { hasAttention } = useAttention(attendableId);
|
|
219
|
+
|
|
188
220
|
const handleClick = useCallback(
|
|
189
221
|
// NOTE: This handler is only called if the tab is *already active*.
|
|
190
222
|
(event: MouseEvent<HTMLButtonElement>) => {
|
|
@@ -197,19 +229,16 @@ const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProp
|
|
|
197
229
|
return (
|
|
198
230
|
<TabsPrimitive.Trigger value={value} asChild>
|
|
199
231
|
<Button
|
|
200
|
-
|
|
232
|
+
{...props}
|
|
201
233
|
variant={
|
|
202
234
|
orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
|
|
203
235
|
}
|
|
204
|
-
{...props}
|
|
205
|
-
onClick={handleClick}
|
|
206
236
|
classNames={[
|
|
207
|
-
'
|
|
208
|
-
orientation === 'vertical' && '
|
|
209
|
-
orientation === 'vertical' && ghostSelectedContainerMd,
|
|
210
|
-
ghostHover,
|
|
237
|
+
orientation === 'vertical' && 'block justify-start text-start w-full',
|
|
238
|
+
orientation === 'vertical' && 'dx-selected',
|
|
211
239
|
classNames,
|
|
212
240
|
]}
|
|
241
|
+
onClick={handleClick}
|
|
213
242
|
>
|
|
214
243
|
{children}
|
|
215
244
|
</Button>
|
|
@@ -217,27 +246,83 @@ const TabsTab = ({ value, classNames, children, onClick, ...props }: TabsTabProp
|
|
|
217
246
|
);
|
|
218
247
|
};
|
|
219
248
|
|
|
220
|
-
|
|
249
|
+
TabsTab.displayName = 'Tabs.Tab';
|
|
250
|
+
|
|
251
|
+
//
|
|
252
|
+
// IconTab
|
|
253
|
+
//
|
|
254
|
+
|
|
255
|
+
type TabsIconTabProps = IconButtonProps & Pick<TabsPrimitive.TabsTriggerProps, 'value'>;
|
|
256
|
+
|
|
257
|
+
const TabsIconTab = ({ value, classNames, onClick, ...props }: TabsIconTabProps) => {
|
|
258
|
+
const { setActivePart, orientation, value: contextValue, attendableId } = useTabsContext('TabsTab');
|
|
259
|
+
const { hasAttention } = useAttention(attendableId);
|
|
260
|
+
|
|
261
|
+
// NOTE: This handler is only called if the tab is *already active*.
|
|
262
|
+
const handleClick = useCallback(
|
|
263
|
+
(event: MouseEvent<HTMLButtonElement>) => {
|
|
264
|
+
setActivePart('panel');
|
|
265
|
+
onClick?.(event);
|
|
266
|
+
},
|
|
267
|
+
[setActivePart, onClick],
|
|
268
|
+
);
|
|
221
269
|
|
|
222
|
-
const TabsTabpanel = ({ classNames, children, ...props }: TabsTabpanelProps) => {
|
|
223
270
|
return (
|
|
224
|
-
<TabsPrimitive.
|
|
225
|
-
|
|
226
|
-
|
|
271
|
+
<TabsPrimitive.Trigger value={value} asChild>
|
|
272
|
+
<IconButton
|
|
273
|
+
{...props}
|
|
274
|
+
variant={
|
|
275
|
+
orientation === 'horizontal' && contextValue === value ? (hasAttention ? 'primary' : 'default') : 'ghost'
|
|
276
|
+
}
|
|
277
|
+
classNames={[
|
|
278
|
+
orientation === 'vertical' && 'justify-start text-start w-full',
|
|
279
|
+
orientation === 'vertical' && 'dx-selected',
|
|
280
|
+
classNames,
|
|
281
|
+
]}
|
|
282
|
+
onClick={handleClick}
|
|
283
|
+
/>
|
|
284
|
+
</TabsPrimitive.Trigger>
|
|
227
285
|
);
|
|
228
286
|
};
|
|
229
287
|
|
|
288
|
+
TabsIconTab.displayName = 'Tabs.IconTab';
|
|
289
|
+
|
|
290
|
+
//
|
|
291
|
+
// Panel
|
|
292
|
+
//
|
|
293
|
+
// Do NOT wrap TabsPanel children in React.Activity.
|
|
294
|
+
// Radix TabsPrimitive.Content already unmounts inactive panels (no forceMount) — inactive tab
|
|
295
|
+
// content is not in the DOM and effects do not run, which is the desired behaviour.
|
|
296
|
+
// React.Activity (experimental in React 19) is a reconciler-level symbol that deactivates its
|
|
297
|
+
// subtree when mode='hidden'. It was redundant here and prevented initial render of active panels.
|
|
298
|
+
//
|
|
299
|
+
|
|
300
|
+
type TabsPanelProps = ThemedClassName<TabsPrimitive.TabsContentProps>;
|
|
301
|
+
|
|
302
|
+
const TabsPanel = ({ classNames, children, ...props }: TabsPanelProps) => (
|
|
303
|
+
<TabsPrimitive.Content {...props} className={mx('p-0! dx-focus-ring-inset-over-all', classNames)}>
|
|
304
|
+
{children}
|
|
305
|
+
</TabsPrimitive.Content>
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
TabsPanel.displayName = 'Tabs.Panel';
|
|
309
|
+
|
|
230
310
|
type TabsTabPrimitiveProps = TabsPrimitive.TabsTriggerProps;
|
|
231
311
|
|
|
312
|
+
//
|
|
313
|
+
// Tabs
|
|
314
|
+
//
|
|
315
|
+
|
|
232
316
|
export const Tabs = {
|
|
233
317
|
Root: TabsRoot,
|
|
234
318
|
Tablist: TabsTablist,
|
|
235
319
|
Tab: TabsTab,
|
|
320
|
+
IconTab: TabsIconTab,
|
|
236
321
|
TabPrimitive: TabsPrimitive.Trigger,
|
|
237
322
|
TabGroupHeading: TabsTabGroupHeading,
|
|
238
|
-
Tabpanel: TabsTabpanel,
|
|
239
|
-
BackButton: TabsBackButton,
|
|
240
323
|
Viewport: TabsViewport,
|
|
324
|
+
Panel: TabsPanel,
|
|
325
|
+
BackButton: TabsBackButton,
|
|
241
326
|
};
|
|
242
327
|
|
|
243
328
|
export type {
|
|
@@ -247,6 +332,6 @@ export type {
|
|
|
247
332
|
TabsTabProps,
|
|
248
333
|
TabsTabPrimitiveProps,
|
|
249
334
|
TabsTabGroupHeadingProps,
|
|
250
|
-
TabsTabpanelProps,
|
|
251
335
|
TabsViewportProps,
|
|
336
|
+
TabsPanelProps,
|
|
252
337
|
};
|