@dxos/react-ui-tabs 0.9.0 → 0.9.1-staging.ee54ba693a
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/dist/lib/browser/index.mjs +43 -17
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +43 -17
- 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 +20 -16
- package/dist/types/src/Tabs.d.ts.map +1 -1
- package/dist/types/src/Tabs.stories.d.ts +7 -4
- package/dist/types/src/Tabs.stories.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -14
- package/src/Tabs.stories.tsx +1 -1
- package/src/Tabs.tsx +60 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-tabs",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
4
4
|
"description": "Components for facilitating a Tabs pattern.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -33,25 +33,25 @@
|
|
|
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.9.
|
|
37
|
-
"@dxos/util": "0.9.
|
|
36
|
+
"@dxos/react-ui-attention": "0.9.1-staging.ee54ba693a",
|
|
37
|
+
"@dxos/util": "0.9.1-staging.ee54ba693a"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/react": "~19.2.
|
|
40
|
+
"@types/react": "~19.2.17",
|
|
41
41
|
"@types/react-dom": "~19.2.3",
|
|
42
|
-
"react": "~19.2.
|
|
43
|
-
"react-dom": "~19.2.
|
|
42
|
+
"react": "~19.2.7",
|
|
43
|
+
"react-dom": "~19.2.7",
|
|
44
44
|
"vite": "^8.0.16",
|
|
45
|
-
"@dxos/react-ui": "0.9.
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
45
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a",
|
|
46
|
+
"@dxos/storybook-utils": "0.9.1-staging.ee54ba693a",
|
|
47
|
+
"@dxos/random": "0.9.1-staging.ee54ba693a",
|
|
48
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"react": "~19.2.
|
|
52
|
-
"react-dom": "~19.2.
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/ui
|
|
51
|
+
"react": "~19.2.7",
|
|
52
|
+
"react-dom": "~19.2.7",
|
|
53
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
|
|
54
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
package/src/Tabs.stories.tsx
CHANGED
|
@@ -15,7 +15,7 @@ random.seed(1234);
|
|
|
15
15
|
|
|
16
16
|
const DefaultStory = ({ orientation }: TabsRootProps) => {
|
|
17
17
|
return (
|
|
18
|
-
<Tabs.Root orientation={orientation} defaultValue={Object.keys(content)[3]} defaultActivePart='list'>
|
|
18
|
+
<Tabs.Root asChild orientation={orientation} defaultValue={Object.keys(content)[3]} defaultActivePart='list'>
|
|
19
19
|
<Tabs.Viewport
|
|
20
20
|
classNames={mx(
|
|
21
21
|
'w-full overflow-hidden grid',
|
package/src/Tabs.tsx
CHANGED
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useArrowNavigationGroup,
|
|
5
|
+
import { useArrowNavigationGroup, useFocusableGroup, useFocusFinders } from '@fluentui/react-tabster';
|
|
6
6
|
import { createContext } from '@radix-ui/react-context';
|
|
7
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
7
8
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
8
9
|
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
9
|
-
import React, { type ComponentPropsWithoutRef, type MouseEvent,
|
|
10
|
+
import React, { type ComponentPropsWithoutRef, type MouseEvent, useCallback, useLayoutEffect } from 'react';
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
Button,
|
|
13
14
|
type ButtonProps,
|
|
14
15
|
IconButton,
|
|
15
16
|
type IconButtonProps,
|
|
17
|
+
type SlottableProps,
|
|
16
18
|
type ThemedClassName,
|
|
19
|
+
composableProps,
|
|
20
|
+
slottable,
|
|
17
21
|
useForwardedRef,
|
|
18
22
|
} from '@dxos/react-ui';
|
|
19
23
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
20
24
|
import { mx } from '@dxos/ui-theme';
|
|
21
25
|
|
|
22
|
-
// TODO(burdon):
|
|
26
|
+
// TODO(burdon): Rewrite this; there are too many hacks/quirks.
|
|
23
27
|
|
|
24
28
|
type TabsActivePart = 'list' | 'panel';
|
|
25
29
|
|
|
@@ -45,19 +49,22 @@ const [TabsContextProvider, useTabsContext] = createContext<TabsContextValue>(TA
|
|
|
45
49
|
// Root
|
|
46
50
|
//
|
|
47
51
|
|
|
48
|
-
type
|
|
52
|
+
type TabsRootCustomProps = TabsPrimitive.TabsProps &
|
|
49
53
|
Partial<
|
|
50
54
|
Pick<TabsContextValue, 'activePart' | 'attendableId'> & {
|
|
51
55
|
onActivePartChange: (nextActivePart: TabsActivePart) => void;
|
|
52
56
|
defaultActivePart: TabsActivePart;
|
|
57
|
+
/** Skip master-detail focus moves (e.g. when a child form owns initial focus). */
|
|
58
|
+
suppressRegionFocus?: boolean;
|
|
53
59
|
}
|
|
54
60
|
>;
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
type TabsRootProps = SlottableProps<TabsRootCustomProps>;
|
|
63
|
+
|
|
64
|
+
const TabsRoot = slottable<HTMLDivElement, TabsRootCustomProps>(
|
|
57
65
|
(
|
|
58
66
|
{
|
|
59
67
|
children,
|
|
60
|
-
classNames,
|
|
61
68
|
activePart: propsActivePart,
|
|
62
69
|
onActivePartChange,
|
|
63
70
|
defaultActivePart,
|
|
@@ -67,6 +74,8 @@ const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
|
|
|
67
74
|
orientation = 'vertical',
|
|
68
75
|
activationMode = 'manual',
|
|
69
76
|
attendableId,
|
|
77
|
+
suppressRegionFocus = false,
|
|
78
|
+
asChild,
|
|
70
79
|
...props
|
|
71
80
|
},
|
|
72
81
|
forwardedRef,
|
|
@@ -74,8 +83,8 @@ const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
|
|
|
74
83
|
const tabsRoot = useForwardedRef(forwardedRef);
|
|
75
84
|
|
|
76
85
|
// TODO(thure): Without these, we get Groupper/Mover `API used before initialization`, but why?
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
useArrowNavigationGroup();
|
|
87
|
+
useFocusableGroup();
|
|
79
88
|
const [activePart = 'list', setActivePart] = useControllableState({
|
|
80
89
|
prop: propsActivePart,
|
|
81
90
|
onChange: onActivePartChange,
|
|
@@ -96,13 +105,36 @@ const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
|
|
|
96
105
|
[value],
|
|
97
106
|
);
|
|
98
107
|
|
|
99
|
-
const { findFirstFocusable } = useFocusFinders();
|
|
108
|
+
const { findFirstFocusable, findNextFocusable } = useFocusFinders();
|
|
100
109
|
|
|
101
110
|
useLayoutEffect(() => {
|
|
102
|
-
if (
|
|
103
|
-
|
|
111
|
+
if (suppressRegionFocus) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const root = tabsRoot.current;
|
|
116
|
+
if (!root) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (activePart === 'list') {
|
|
121
|
+
const tablist = root.querySelector<HTMLElement>('[role="tablist"]');
|
|
122
|
+
findFirstFocusable(tablist)?.focus();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const panel = root.querySelector<HTMLElement>('[role="tabpanel"][data-state="active"]');
|
|
127
|
+
if (!panel) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Radix marks the active panel focusable for roving tabindex; skip it so content receives focus.
|
|
132
|
+
let target = findFirstFocusable(panel);
|
|
133
|
+
if (target === panel) {
|
|
134
|
+
target = findNextFocusable(panel, { container: panel }) ?? undefined;
|
|
104
135
|
}
|
|
105
|
-
|
|
136
|
+
target?.focus();
|
|
137
|
+
}, [activePart, value, findFirstFocusable, findNextFocusable, suppressRegionFocus]);
|
|
106
138
|
|
|
107
139
|
return (
|
|
108
140
|
<TabsContextProvider
|
|
@@ -113,8 +145,8 @@ const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
|
|
|
113
145
|
attendableId={attendableId}
|
|
114
146
|
>
|
|
115
147
|
<TabsPrimitive.Root
|
|
116
|
-
{...props}
|
|
117
|
-
|
|
148
|
+
{...composableProps<HTMLDivElement>(props)}
|
|
149
|
+
asChild={asChild}
|
|
118
150
|
orientation={orientation}
|
|
119
151
|
activationMode={activationMode}
|
|
120
152
|
data-active={activePart}
|
|
@@ -135,16 +167,22 @@ TabsRoot.displayName = 'Tabs.Root';
|
|
|
135
167
|
// Viewport
|
|
136
168
|
//
|
|
137
169
|
|
|
138
|
-
type TabsViewportProps =
|
|
170
|
+
type TabsViewportProps = SlottableProps<
|
|
171
|
+
Omit<ComponentPropsWithoutRef<'div'>, 'className' | 'style' | 'children' | 'role'>
|
|
172
|
+
>;
|
|
139
173
|
|
|
140
|
-
const TabsViewport =
|
|
174
|
+
const TabsViewport = slottable<
|
|
175
|
+
HTMLDivElement,
|
|
176
|
+
Omit<ComponentPropsWithoutRef<'div'>, 'className' | 'style' | 'children' | 'role'>
|
|
177
|
+
>(({ children, asChild, ...props }, forwardedRef) => {
|
|
141
178
|
const { activePart } = useTabsContext('TabsViewport');
|
|
179
|
+
const Comp = asChild ? Slot : 'div';
|
|
142
180
|
return (
|
|
143
|
-
<
|
|
181
|
+
<Comp {...composableProps<HTMLDivElement>(props)} data-active={activePart} ref={forwardedRef}>
|
|
144
182
|
{children}
|
|
145
|
-
</
|
|
183
|
+
</Comp>
|
|
146
184
|
);
|
|
147
|
-
};
|
|
185
|
+
});
|
|
148
186
|
|
|
149
187
|
TabsViewport.displayName = 'Tabs.Viewport';
|
|
150
188
|
|
|
@@ -327,11 +365,11 @@ export const Tabs = {
|
|
|
327
365
|
|
|
328
366
|
export type {
|
|
329
367
|
TabsActivePart,
|
|
368
|
+
TabsPanelProps,
|
|
330
369
|
TabsRootProps,
|
|
370
|
+
TabsTabGroupHeadingProps,
|
|
331
371
|
TabsTablistProps,
|
|
332
|
-
TabsTabProps,
|
|
333
372
|
TabsTabPrimitiveProps,
|
|
334
|
-
|
|
373
|
+
TabsTabProps,
|
|
335
374
|
TabsViewportProps,
|
|
336
|
-
TabsPanelProps,
|
|
337
375
|
};
|