@beydesign/storybook 0.2.12 → 0.2.13
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/stories/UI/TabMenu.d.ts +3 -0
- package/dist/stories/UI/TabMenu.js +85 -0
- package/dist/stories/UI/TabMenu.stories.d.ts +9 -0
- package/dist/stories/UI/TabMenu.stories.js +110 -0
- package/dist/stories/UI/index.d.ts +1 -0
- package/dist/stories/UI/index.js +1 -0
- package/dist/stories/UI/types.d.ts +39 -0
- package/dist/stories/UI/types.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Box } from '@mui/material';
|
|
4
|
+
import { Typography, TypographySize, TypographyWeight } from '../Typography';
|
|
5
|
+
import { TabMenuSize } from './types';
|
|
6
|
+
const SIZE_TOKENS = {
|
|
7
|
+
[TabMenuSize.md]: {
|
|
8
|
+
containerGap: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)',
|
|
9
|
+
containerPadding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xs)',
|
|
10
|
+
tabPaddingY: 'var(--spacing-tokens-size-in-px-spacing-spacing-md)',
|
|
11
|
+
tabRadius: 'var(--spacing-tokens-size-in-px-radius-radius-xs)',
|
|
12
|
+
tabHeight: '40px',
|
|
13
|
+
dividerHeight: '24px',
|
|
14
|
+
textSize: TypographySize.TextM,
|
|
15
|
+
textWeight: TypographyWeight.SemiBold,
|
|
16
|
+
},
|
|
17
|
+
[TabMenuSize.sm]: {
|
|
18
|
+
containerGap: 'var(--spacing-tokens-size-in-px-spacing-spacing-xxs)',
|
|
19
|
+
containerPadding: 'var(--spacing-tokens-size-in-px-spacing-spacing-xxs)',
|
|
20
|
+
tabPaddingY: '5px',
|
|
21
|
+
tabRadius: 'var(--spacing-tokens-size-in-px-radius-radius-sm)',
|
|
22
|
+
tabHeight: 'auto',
|
|
23
|
+
dividerHeight: '20px',
|
|
24
|
+
textSize: TypographySize.TextS,
|
|
25
|
+
textWeight: TypographyWeight.Medium,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const normalizeTab = (tab) => typeof tab === 'string' ? { label: tab } : tab;
|
|
29
|
+
export const TabMenu = ({ tabs, activeTab = 0, onTabChange, size = TabMenuSize.md, showDividers = true, animated = true, style, tabStyle, testId, }) => {
|
|
30
|
+
const tokens = SIZE_TOKENS[size];
|
|
31
|
+
const containerRef = useRef(null);
|
|
32
|
+
const tabRefs = useRef([]);
|
|
33
|
+
const [indicator, setIndicator] = useState(null);
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
const measure = () => {
|
|
36
|
+
const container = containerRef.current;
|
|
37
|
+
const tab = tabRefs.current[activeTab];
|
|
38
|
+
if (!container || !tab)
|
|
39
|
+
return;
|
|
40
|
+
const containerRect = container.getBoundingClientRect();
|
|
41
|
+
const tabRect = tab.getBoundingClientRect();
|
|
42
|
+
setIndicator({
|
|
43
|
+
left: tabRect.left - containerRect.left,
|
|
44
|
+
top: tabRect.top - containerRect.top,
|
|
45
|
+
width: tabRect.width,
|
|
46
|
+
height: tabRect.height,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
measure();
|
|
50
|
+
window.addEventListener('resize', measure);
|
|
51
|
+
return () => window.removeEventListener('resize', measure);
|
|
52
|
+
}, [activeTab, size, showDividers, tabs]);
|
|
53
|
+
return (_jsxs(Box, { ref: containerRef, "data-testid": testId, position: 'relative', display: 'inline-flex', flexDirection: 'row', alignItems: 'center', gap: tokens.containerGap, padding: tokens.containerPadding, bgcolor: 'var(--color-background-bg-component)', border: '1px solid var(--color-border-border-component)', borderRadius: 'var(--spacing-tokens-size-in-px-radius-radius-md)', sx: style, children: [indicator && (_jsx(Box, { "aria-hidden": true, position: 'absolute', bgcolor: 'var(--color-background-bg-brand-transparent)', borderRadius: tokens.tabRadius, sx: {
|
|
54
|
+
left: 0,
|
|
55
|
+
top: 0,
|
|
56
|
+
width: indicator.width,
|
|
57
|
+
height: indicator.height,
|
|
58
|
+
transform: `translate(${indicator.left}px, ${indicator.top}px)`,
|
|
59
|
+
transition: animated
|
|
60
|
+
? 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)'
|
|
61
|
+
: 'none',
|
|
62
|
+
pointerEvents: 'none',
|
|
63
|
+
zIndex: 0,
|
|
64
|
+
} })), tabs.map((tab, index) => {
|
|
65
|
+
const { label } = normalizeTab(tab);
|
|
66
|
+
const isActive = index === activeTab;
|
|
67
|
+
return (_jsxs(React.Fragment, { children: [showDividers && index > 0 && (_jsx(Box, { width: '1px', height: tokens.dividerHeight, bgcolor: 'var(--color-border-border-divider)', flexShrink: 0, sx: { zIndex: 1 } })), _jsx(Box, { ref: (el) => {
|
|
68
|
+
tabRefs.current[index] = el;
|
|
69
|
+
}, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', position: 'relative', flexShrink: 0, height: tokens.tabHeight, paddingX: 'var(--spacing-tokens-size-in-px-spacing-spacing-4xl)', paddingY: tokens.tabPaddingY, borderRadius: tokens.tabRadius, onClick: onTabChange ? () => onTabChange(index) : undefined, sx: {
|
|
70
|
+
zIndex: 1,
|
|
71
|
+
cursor: onTabChange ? 'pointer' : 'default',
|
|
72
|
+
'&:hover': onTabChange && !isActive
|
|
73
|
+
? {
|
|
74
|
+
bgcolor: 'var(--color-background-bg-brand-transparent-hover)',
|
|
75
|
+
transition: 'background-color 0.15s ease',
|
|
76
|
+
}
|
|
77
|
+
: undefined,
|
|
78
|
+
...tabStyle,
|
|
79
|
+
}, children: _jsx(Typography, { size: tokens.textSize, weight: tokens.textWeight, color: isActive
|
|
80
|
+
? 'var(--color-text-text-clicable)'
|
|
81
|
+
: 'var(--color-text-text-subtle)', textStyle: {
|
|
82
|
+
transition: animated ? 'color 0.25s ease' : 'none',
|
|
83
|
+
}, children: label }) })] }, index));
|
|
84
|
+
})] }));
|
|
85
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { TabMenu } from './TabMenu';
|
|
3
|
+
declare const meta: Meta<typeof TabMenu>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof TabMenu>;
|
|
6
|
+
export declare const Medium: Story;
|
|
7
|
+
export declare const Small: Story;
|
|
8
|
+
export declare const MediumStatic: Story;
|
|
9
|
+
export declare const SmallStatic: Story;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { TabMenu } from './TabMenu';
|
|
4
|
+
import { TabMenuSize } from './types';
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Design System/Components/UI/TabMenu',
|
|
7
|
+
component: TabMenu,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
design: {
|
|
11
|
+
type: 'figspec',
|
|
12
|
+
url: 'https://www.figma.com/design/mIhjz2yJjcpLlIqw6oUivt/Beyond-Presence?node-id=6516-41431',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
argTypes: {
|
|
17
|
+
tabs: {
|
|
18
|
+
control: 'object',
|
|
19
|
+
description: 'Tabs to render (labels or { label, value } objects)',
|
|
20
|
+
table: {
|
|
21
|
+
type: { summary: 'string[] | TabMenuTab[]' },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
activeTab: {
|
|
25
|
+
control: 'number',
|
|
26
|
+
description: 'Index of the active tab',
|
|
27
|
+
table: {
|
|
28
|
+
type: { summary: 'number' },
|
|
29
|
+
defaultValue: { summary: '0' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
onTabChange: {
|
|
33
|
+
description: 'Called with the index of the clicked tab',
|
|
34
|
+
table: {
|
|
35
|
+
type: { summary: 'function' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
size: {
|
|
39
|
+
control: 'select',
|
|
40
|
+
options: Object.values(TabMenuSize),
|
|
41
|
+
description: 'Size variant of the tab menu',
|
|
42
|
+
table: {
|
|
43
|
+
type: { summary: 'string' },
|
|
44
|
+
defaultValue: { summary: 'md' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
showDividers: {
|
|
48
|
+
control: 'boolean',
|
|
49
|
+
description: 'Show the vertical dividers between tabs',
|
|
50
|
+
table: {
|
|
51
|
+
type: { summary: 'boolean' },
|
|
52
|
+
defaultValue: { summary: 'true' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
animated: {
|
|
56
|
+
control: 'boolean',
|
|
57
|
+
description: 'Animate the active highlight when switching tabs',
|
|
58
|
+
table: {
|
|
59
|
+
type: { summary: 'boolean' },
|
|
60
|
+
defaultValue: { summary: 'true' },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
testId: {
|
|
64
|
+
control: 'text',
|
|
65
|
+
description: 'Test ID of the tab menu',
|
|
66
|
+
table: {
|
|
67
|
+
type: { summary: 'string' },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export default meta;
|
|
73
|
+
const TABS = ['Tab name', 'Tab name', 'Tab name', 'Tab name', 'Tab name'];
|
|
74
|
+
const InteractiveTabMenu = (args) => {
|
|
75
|
+
const [activeTab, setActiveTab] = useState(args.activeTab ?? 0);
|
|
76
|
+
return _jsx(TabMenu, { ...args, activeTab: activeTab, onTabChange: setActiveTab });
|
|
77
|
+
};
|
|
78
|
+
export const Medium = {
|
|
79
|
+
render: (args) => _jsx(InteractiveTabMenu, { ...args }),
|
|
80
|
+
args: {
|
|
81
|
+
tabs: TABS,
|
|
82
|
+
activeTab: 0,
|
|
83
|
+
size: TabMenuSize.md,
|
|
84
|
+
showDividers: true,
|
|
85
|
+
animated: true,
|
|
86
|
+
testId: 'tab-menu',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
export const Small = {
|
|
90
|
+
render: (args) => _jsx(InteractiveTabMenu, { ...args }),
|
|
91
|
+
args: {
|
|
92
|
+
...Medium.args,
|
|
93
|
+
size: TabMenuSize.sm,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
export const MediumStatic = {
|
|
97
|
+
render: (args) => _jsx(InteractiveTabMenu, { ...args }),
|
|
98
|
+
args: {
|
|
99
|
+
...Medium.args,
|
|
100
|
+
animated: false,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
export const SmallStatic = {
|
|
104
|
+
render: (args) => _jsx(InteractiveTabMenu, { ...args }),
|
|
105
|
+
args: {
|
|
106
|
+
...Medium.args,
|
|
107
|
+
size: TabMenuSize.sm,
|
|
108
|
+
animated: false,
|
|
109
|
+
},
|
|
110
|
+
};
|
package/dist/stories/UI/index.js
CHANGED
|
@@ -444,3 +444,42 @@ export interface StepperProps {
|
|
|
444
444
|
/** Connector width of the stepper */
|
|
445
445
|
connectorWidth?: string | number;
|
|
446
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* TabMenu size variants
|
|
449
|
+
*/
|
|
450
|
+
export declare enum TabMenuSize {
|
|
451
|
+
md = "md",
|
|
452
|
+
sm = "sm"
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* A single tab within a {@link TabMenu}.
|
|
456
|
+
*/
|
|
457
|
+
export interface TabMenuTab {
|
|
458
|
+
/** Label shown on the tab */
|
|
459
|
+
label: string;
|
|
460
|
+
/** Optional value identifying the tab. Defaults to the label. */
|
|
461
|
+
value?: string;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* TabMenu component props — a bordered segmented control of selectable tabs.
|
|
465
|
+
*/
|
|
466
|
+
export interface TabMenuProps {
|
|
467
|
+
/** Tabs to render. Accepts labels or `{ label, value }` objects. */
|
|
468
|
+
tabs: (string | TabMenuTab)[];
|
|
469
|
+
/** Index of the active tab. Defaults to `0`. */
|
|
470
|
+
activeTab?: number;
|
|
471
|
+
/** Called with the index of the tab that was clicked. */
|
|
472
|
+
onTabChange?: (index: number) => void;
|
|
473
|
+
/** Size variant. Defaults to `Md`. */
|
|
474
|
+
size?: TabMenuSize;
|
|
475
|
+
/** Show the vertical dividers between tabs. Defaults to `true`. */
|
|
476
|
+
showDividers?: boolean;
|
|
477
|
+
/** Animate the active highlight when switching tabs. Defaults to `true`. */
|
|
478
|
+
animated?: boolean;
|
|
479
|
+
/** Custom style of the container. */
|
|
480
|
+
style?: SxProps<Theme>;
|
|
481
|
+
/** Custom style applied to each tab. */
|
|
482
|
+
tabStyle?: SxProps<Theme>;
|
|
483
|
+
/** Test ID of the tab menu. */
|
|
484
|
+
testId?: string;
|
|
485
|
+
}
|
package/dist/stories/UI/types.js
CHANGED
|
@@ -119,3 +119,11 @@ export var FeatureBannerMedia;
|
|
|
119
119
|
FeatureBannerMedia["Image"] = "Image";
|
|
120
120
|
FeatureBannerMedia["None"] = "None";
|
|
121
121
|
})(FeatureBannerMedia || (FeatureBannerMedia = {}));
|
|
122
|
+
/**
|
|
123
|
+
* TabMenu size variants
|
|
124
|
+
*/
|
|
125
|
+
export var TabMenuSize;
|
|
126
|
+
(function (TabMenuSize) {
|
|
127
|
+
TabMenuSize["md"] = "md";
|
|
128
|
+
TabMenuSize["sm"] = "sm";
|
|
129
|
+
})(TabMenuSize || (TabMenuSize = {}));
|