Skip to content

Commit 36bf149

Browse files
authored
feat: EmbeddedTabs (#253)
1 parent 91085e4 commit 36bf149

File tree

6 files changed

+210
-0
lines changed

6 files changed

+210
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { FC, useState } from 'react';
2+
import { Box, BoxProps } from 'rebass';
3+
import * as S from './styles';
4+
import Tab, { Props as EmbeddedTabItem } from './Tab';
5+
6+
export interface Props extends Omit<BoxProps, 'css'> {
7+
tabs: EmbeddedTabItem[];
8+
initialTab?: number;
9+
onTabChange?: (tabIndex: number) => void;
10+
}
11+
12+
const EmbeddedTabs: FC<Props> = ({
13+
tabs,
14+
onTabChange,
15+
initialTab = 0,
16+
sx = {},
17+
...boxProps
18+
}: Props) => {
19+
const [activeIndex, setActiveIndex] = useState(initialTab);
20+
21+
const handleTabClick = (newIndex: number) => {
22+
setActiveIndex(newIndex);
23+
24+
if (onTabChange) {
25+
onTabChange(newIndex);
26+
}
27+
};
28+
29+
return (
30+
<Box sx={{ ...S.tabsList, ...sx }} {...boxProps}>
31+
{tabs.map(({ title, onClick: onTabClick, ...tabProps }, index) => (
32+
<Tab
33+
key={title}
34+
title={title}
35+
active={index === activeIndex}
36+
onClick={() => {
37+
if (onTabClick) {
38+
onTabClick();
39+
}
40+
41+
handleTabClick(index);
42+
}}
43+
{...tabProps}
44+
/>
45+
))}
46+
{/* This box is needed to draw the horizontal line up to the full width, it's a stylistic decision */}
47+
<Box tabIndex={-1} sx={S.tabsLineEnding} />
48+
</Box>
49+
);
50+
};
51+
52+
export default EmbeddedTabs;

src/components/embedded-tabs/Tab.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as R from 'ramda';
2+
import React, { FC } from 'react';
3+
import { Box, SxStyleProp } from 'rebass';
4+
import Label from '../label';
5+
import * as S from './styles';
6+
7+
export interface Props {
8+
title: string;
9+
active?: boolean;
10+
onClick?: () => any;
11+
disabled?: boolean;
12+
}
13+
14+
const getStyles = ({ active, disabled }: Props) =>
15+
({
16+
...S.tab,
17+
...(active ? S.activeTab : {}),
18+
...(disabled ? S.disabledTab : {}),
19+
} as SxStyleProp);
20+
21+
const Tab: FC<Props> = (props) => {
22+
const propagatedProps = R.pick(['disabled', 'onClick'], props);
23+
24+
return (
25+
<Box sx={getStyles(props)} {...propagatedProps}>
26+
<Label pb="1px">{props.title}</Label>
27+
</Box>
28+
);
29+
};
30+
31+
export default Tab;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Meta, Story } from '@storybook/react/types-6-0';
2+
import React, { useState } from 'react';
3+
import { Box } from 'rebass';
4+
import Labeling from '../typography/labeling';
5+
import EmbeddedTabs, { Props } from './EmbeddedTabs';
6+
7+
export default {
8+
title: 'Quartz/EmbeddedTabs',
9+
component: EmbeddedTabs,
10+
} as Meta;
11+
12+
const argTypes = {
13+
tabs: {
14+
required: true,
15+
description: 'A list of tab items.',
16+
},
17+
initialTab: {
18+
required: false,
19+
description: 'The initial tab to be selected.',
20+
default: 0,
21+
},
22+
onTabChange: {
23+
description: 'Callback to be called when a tab is selected.',
24+
required: false
25+
},
26+
};
27+
28+
const tabs = [
29+
{
30+
title: 'Validation Reports',
31+
},
32+
{
33+
title: 'Results',
34+
},
35+
{
36+
title: 'Statistics',
37+
},
38+
{
39+
title: 'And a disabled tab',
40+
disabled: true,
41+
},
42+
];
43+
44+
const Template: Story<Props> = (props) => {
45+
const [activeTab, setActiveTab] = useState(props.initialTab ?? 0);
46+
47+
return (
48+
<Box width="700px">
49+
<EmbeddedTabs {...props} onTabChange={setActiveTab} />
50+
<Box mt={3}>
51+
<Labeling bold>Active tab: {tabs[activeTab].title}</Labeling>
52+
</Box>
53+
</Box>
54+
);
55+
};
56+
57+
export const Default = Template.bind({});
58+
59+
Default.args = {
60+
tabs,
61+
};
62+
63+
Default.argTypes = argTypes;

src/components/embedded-tabs/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from './EmbeddedTabs';
2+
export type { Props as EmbeddedTabsProps } from './EmbeddedTabs';
3+
export type { Props as EmbeddedTabsItem } from './Tab';
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { SxStyleProp } from 'rebass';
2+
import * as R from 'ramda';
3+
4+
export const tabsList = {
5+
/* It is `inline-table` to have the ability to collapse borders */
6+
display: 'inline-table',
7+
verticalAlign: 'middle',
8+
borderCollapse: 'collapse',
9+
10+
'> div:not(:last-child)': {
11+
marginRight: '20px'
12+
}
13+
} as SxStyleProp;
14+
15+
/* this one is needed to have the line that spans across the rest of the width. Couldn't find any other solution. */
16+
export const tabsLineEnding = {
17+
display: 'table-cell',
18+
width: '100%',
19+
borderBottom: '1px solid',
20+
borderBottomColor: 'gray',
21+
pointerEvents: 'none',
22+
};
23+
24+
export const activeTab = {
25+
backgroundColor: 'white',
26+
borderBottomColor: 'grayShade3',
27+
};
28+
29+
export const disabledTab = {
30+
backgroundColor: 'grayShade1',
31+
pointerEvents: 'none',
32+
};
33+
34+
export const tab = {
35+
display: 'table-cell',
36+
37+
p: 10,
38+
39+
backgroundColor: 'grayShade3',
40+
border: '1px solid',
41+
borderColor: ' gray',
42+
43+
whiteSpace: 'nowrap',
44+
userSelect: 'none',
45+
cursor: 'pointer',
46+
'*': {
47+
cursor: 'pointer',
48+
},
49+
50+
transition: ({ transitions }) => transitions.button,
51+
52+
':hover': {
53+
...R.dissoc('borderBottomColor', activeTab),
54+
},
55+
} as SxStyleProp;

src/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ export {
252252
constants,
253253
};
254254

255+
export { default as EmbeddedTabs } from './components/embedded-tabs';
256+
255257
export type { ITheme, IThemeColors, IThemeIconSizes } from './theme/types';
256258
export type TooltipProps = import('./components/tooltip').TooltipProps;
257259
export type { BadgeProps, BlinkProps } from './components/badges';
@@ -289,6 +291,10 @@ export type {
289291
RadioGroupProps,
290292
RadioGroupOption,
291293
} from './components/radio/radio-group';
294+
export type {
295+
EmbeddedTabsItem,
296+
EmbeddedTabsProps,
297+
} from './components/embedded-tabs';
292298

293299
// Rebass components
294300
export { Flex, Box } from 'rebass';

0 commit comments

Comments
 (0)