@dxos/react-ui-tabs 0.9.0 → 0.9.1-main.c7dcc2e112

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-tabs",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-main.c7dcc2e112",
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,8 +33,8 @@
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.0",
37
- "@dxos/util": "0.9.0"
36
+ "@dxos/react-ui-attention": "0.9.1-main.c7dcc2e112",
37
+ "@dxos/util": "0.9.1-main.c7dcc2e112"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/react": "~19.2.7",
@@ -42,16 +42,16 @@
42
42
  "react": "~19.2.3",
43
43
  "react-dom": "~19.2.3",
44
44
  "vite": "^8.0.16",
45
- "@dxos/react-ui": "0.9.0",
46
- "@dxos/random": "0.9.0",
47
- "@dxos/ui-theme": "0.9.0",
48
- "@dxos/storybook-utils": "0.9.0"
45
+ "@dxos/random": "0.9.1-main.c7dcc2e112",
46
+ "@dxos/storybook-utils": "0.9.1-main.c7dcc2e112",
47
+ "@dxos/react-ui": "0.9.1-main.c7dcc2e112",
48
+ "@dxos/ui-theme": "0.9.1-main.c7dcc2e112"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "~19.2.3",
52
52
  "react-dom": "~19.2.3",
53
- "@dxos/react-ui": "0.9.0",
54
- "@dxos/ui-theme": "0.9.0"
53
+ "@dxos/react-ui": "0.9.1-main.c7dcc2e112",
54
+ "@dxos/ui-theme": "0.9.1-main.c7dcc2e112"
55
55
  },
56
56
  "publishConfig": {
57
57
  "access": "public"
@@ -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
@@ -4,22 +4,26 @@
4
4
 
5
5
  import { useArrowNavigationGroup, useFocusFinders, useFocusableGroup } 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, forwardRef, useCallback, useLayoutEffect } from 'react';
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): Move to @dxos/react-ui.
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 TabsRootProps = ThemedClassName<TabsPrimitive.TabsProps> &
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
- const TabsRoot = forwardRef<HTMLDivElement, TabsRootProps>(
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
- const _1 = useArrowNavigationGroup();
78
- const _2 = useFocusableGroup();
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 (tabsRoot.current) {
103
- findFirstFocusable(tabsRoot.current)?.focus();
111
+ if (suppressRegionFocus) {
112
+ return;
113
+ }
114
+
115
+ const root = tabsRoot.current;
116
+ if (!root) {
117
+ return;
104
118
  }
105
- }, [activePart]);
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;
135
+ }
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
- className={mx('overflow-hidden', classNames)}
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 = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
170
+ type TabsViewportProps = SlottableProps<
171
+ Omit<ComponentPropsWithoutRef<'div'>, 'className' | 'style' | 'children' | 'role'>
172
+ >;
139
173
 
140
- const TabsViewport = ({ classNames, children, ...props }: TabsViewportProps) => {
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
- <div {...props} data-active={activePart} className={mx(classNames)}>
181
+ <Comp {...composableProps<HTMLDivElement>(props)} data-active={activePart} ref={forwardedRef}>
144
182
  {children}
145
- </div>
183
+ </Comp>
146
184
  );
147
- };
185
+ });
148
186
 
149
187
  TabsViewport.displayName = 'Tabs.Viewport';
150
188