Skip to content

Commit 696fd59

Browse files
tenphicursoragent
andauthored
feat: add default type styling support for Tabs component (#1044)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 32a07b4 commit 696fd59

File tree

7 files changed

+59
-33
lines changed

7 files changed

+59
-33
lines changed

.changeset/tabs-default-type.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cube-dev/ui-kit": minor
3+
---
4+
5+
Add support for default type styling in Tabs component. TabsAction now adapts its appearance based on tabs type (outline for default type, neutral for others).

src/components/navigation/Tabs/TabDropIndicator.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export function TabDropIndicator({
3535
position,
3636
}: TabDropIndicatorProps) {
3737
const ref = useRef<HTMLDivElement>(null);
38-
const prevContentRef = useRef<string | null>(null);
3938
const [, setTick] = useState(0);
4039

4140
const { dropIndicatorProps, isHidden, isDropTarget } = useDropIndicator(
@@ -44,7 +43,7 @@ export function TabDropIndicator({
4443
ref,
4544
);
4645

47-
// Re-evaluate drop target registration when tab content changes.
46+
// Re-evaluate drop target registration when tab layout changes.
4847
// Active only while the indicator is rendered in the DOM (during drags).
4948
useEffect(() => {
5049
const element = ref.current;
@@ -53,22 +52,11 @@ export function TabDropIndicator({
5352
const tabList = element.closest('[data-element="TabList"]');
5453
if (!tabList) return;
5554

56-
prevContentRef.current = tabList.textContent;
57-
58-
const observer = new MutationObserver(() => {
59-
const currentContent = tabList.textContent;
60-
61-
if (currentContent !== prevContentRef.current) {
62-
prevContentRef.current = currentContent;
63-
setTick((n) => n + 1);
64-
}
55+
const observer = new ResizeObserver(() => {
56+
setTick((n) => n + 1);
6557
});
6658

67-
observer.observe(tabList, {
68-
subtree: true,
69-
characterData: true,
70-
childList: true,
71-
});
59+
observer.observe(tabList);
7260

7361
return () => observer.disconnect();
7462
}, [isHidden]);

src/components/navigation/Tabs/Tabs.stories.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,9 @@ export const WithPrefixAndSuffix: Story = {
338338
render: (args) => (
339339
<Tabs
340340
{...args}
341-
size="large"
342341
defaultActiveKey="tab1"
343-
prefix={<Button size="small">Menu</Button>}
344-
suffix={<Button size="small">Add New</Button>}
342+
prefix={<Tabs.Action>Menu</Tabs.Action>}
343+
suffix={<Tabs.Action>Add New</Tabs.Action>}
345344
>
346345
<Tab key="tab1" title="Items">
347346
<Paragraph>List of items here.</Paragraph>

src/components/navigation/Tabs/Tabs.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ function TabsComponent(
168168

169169
// Build TabList padding style (memoized)
170170
const tabListPaddingStyles = useMemo(
171-
() => (tabListPadding ? { padding: `0 ${tabListPadding}` } : undefined),
171+
() =>
172+
tabListPadding ? { '$tablist-padding': `${tabListPadding}` } : undefined,
172173
[tabListPadding],
173174
);
174175

src/components/navigation/Tabs/TabsAction.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ export interface CubeTabsActionProps
2929
// =============================================================================
3030

3131
const TabsActionElement = tasty(ItemButton, {
32-
shape: 'sharp',
33-
type: 'neutral',
3432
styles: {
3533
border: {
36-
'': 0,
37-
'tabs-type-file & !:first-child': 'left',
34+
'type=neutral': 0,
35+
'type=neutral & tabs-type-file & !:first-child': 'left',
36+
},
37+
radius: {
38+
'type=neutral': 0,
3839
},
3940
},
4041
});
@@ -75,7 +76,7 @@ export const TabsAction = forwardRef(function TabsAction(
7576
props: CubeTabsActionProps,
7677
ref: FocusableRef<HTMLElement>,
7778
) {
78-
const { size, mods, ...rest } = props;
79+
const { size, mods, type, ...rest } = props;
7980

8081
// Get size and type from context if available (when used inside Tabs)
8182
const tabsContext = useOptionalTabsContext();
@@ -93,6 +94,7 @@ export const TabsAction = forwardRef(function TabsAction(
9394
return (
9495
<TabsActionElement
9596
ref={ref}
97+
type={type ?? (tabsType === 'default' ? 'outline' : 'neutral')}
9698
size={effectiveSize}
9799
mods={combinedMods}
98100
{...rest}

src/components/navigation/Tabs/styled.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ export const TabsElement = tasty({
5050
placeContent: 'center',
5151
flexShrink: 0,
5252
placeSelf: 'stretch',
53+
padding: {
54+
'': 0,
55+
'type=default': '.5x 0 .5x 1x',
56+
},
57+
gap: {
58+
'': 0,
59+
'type=default': '.5x',
60+
},
5361
border: {
5462
'': 0,
5563
'type=file': 'right',
@@ -63,6 +71,14 @@ export const TabsElement = tasty({
6371
placeContent: 'center',
6472
flexShrink: 0,
6573
placeSelf: 'stretch',
74+
padding: {
75+
'': 0,
76+
'type=default': '.5x 1x .5x 0',
77+
},
78+
gap: {
79+
'': 0,
80+
'type=default': '.5x',
81+
},
6682
border: {
6783
'': 0,
6884
'type=file': 'left',
@@ -131,6 +147,7 @@ export const TabsElement = tasty({
131147
},
132148
gap: {
133149
'': 0,
150+
'type=default': '1x',
134151
'type=narrow': '2x',
135152
'type=radio': '.5x',
136153
},
@@ -140,6 +157,12 @@ export const TabsElement = tasty({
140157
'': 'max-content',
141158
'type=radio': '100%',
142159
},
160+
padding: '0 $tablist-padding',
161+
162+
'$tablist-padding': {
163+
'': '0',
164+
'type=default | type=narrow': '1x',
165+
},
143166
},
144167

145168
// Size variable for actions (if ItemAction is used instead of TabsAction)
@@ -181,17 +204,18 @@ export const TabElement = tasty(Item, {
181204
styles: {
182205
radius: {
183206
'': false,
184-
'type=radio': true,
185-
'type=default | type=narrow': 'top',
207+
'type=radio | type=default': true,
208+
'type=narrow': 'top',
186209
},
187210
color: {
188211
'': '#dark-02',
189-
'(type=default | type=narrow) & (hovered & !selected)': '#primary-text',
212+
'type=narrow & (hovered & !selected)': '#primary-text',
190213
'(type=default | type=narrow) & selected': '#primary-text',
191214
disabled: '#dark-04',
192215
},
193216
fill: {
194217
'': '#clear',
218+
'hovered & !type=narrow': '#dark.03',
195219
'type=file': '#light',
196220
'type=file & hovered': '#light.5',
197221
'type=radio & hovered': '#white.5',
@@ -252,6 +276,10 @@ export const TabContainer = tasty({
252276
styles: {
253277
position: 'relative',
254278
display: 'grid',
279+
margin: {
280+
'': 0,
281+
'type=default': '.5x 0',
282+
},
255283
border: {
256284
'': 0,
257285
'type=file': 'right',

src/utils/react/interactions.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
import { useEffect, useState } from 'react';
1+
import { useState } from 'react';
22
import { useFocus as reactAriaUseFocus, useFocusVisible } from 'react-aria';
33

44
export function useFocus(
55
{ isDisabled }: { isDisabled?: boolean },
66
onlyVisible = false,
77
) {
8-
useEffect(() => {
8+
let [isFocused, setIsFocused] = useState(false);
9+
10+
// React-aria detaches focus handlers when disabled, so blur events
11+
// aren't captured. Clear stale focus synchronously during render
12+
// to avoid a one-frame glitch (useEffect would be too late).
13+
if (isDisabled && isFocused) {
914
setIsFocused(false);
10-
}, [isDisabled]);
15+
}
1116

12-
let [isFocused, setIsFocused] = useState(false);
1317
let { isFocusVisible } = useFocusVisible({});
1418
let { focusProps } = reactAriaUseFocus({
1519
isDisabled,
16-
// @ts-ignore
1720
onFocusChange: setIsFocused,
1821
});
1922

0 commit comments

Comments
 (0)