Skip to content

Commit 67d49d0

Browse files
committed
chore(tabs): improve stories
1 parent e2c94a7 commit 67d49d0

File tree

4 files changed

+204
-84
lines changed

4 files changed

+204
-84
lines changed
Lines changed: 147 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,170 @@
1-
import { Button, Dialog, Tab, TabList, TabPanel, TabProvider } from '@lumx/react';
1+
/* eslint-disable react-hooks/rules-of-hooks */
2+
3+
import { Alignment, Button, Dialog, Tab, TabList, TabListLayout, TabPanel, TabProvider } from '@lumx/react';
4+
import { iconArgType } from '@lumx/react/stories/controls/icons';
5+
import { getSelectArgType } from '@lumx/react/stories/controls/selectArgType';
6+
import { withNestedProps } from '@lumx/react/stories/decorators/withNestedProps';
7+
import { toFlattenProps } from '@lumx/react/stories/utils/toFlattenProps';
8+
import { withCategory } from '@lumx/react/stories/utils/withCategory';
29
import get from 'lodash/get';
310
import times from 'lodash/times';
411
import React, { useState } from 'react';
512

6-
export default { title: 'LumX components/tabs' };
13+
export default {
14+
title: 'LumX components/tabs',
15+
decorators: [withNestedProps()],
16+
parameters: { controls: { sort: 'alpha' } },
17+
};
18+
19+
/** Default tab behavior with some controllable args */
20+
export const Default = {
21+
render: ({ theme, tabProviderProps, tabListProps, tabProps }: any) => (
22+
<TabProvider {...tabProviderProps}>
23+
<TabList theme={theme} aria-label="Tab list" {...tabListProps}>
24+
<Tab {...tabProps[0]} />
25+
<Tab {...tabProps[1]} />
26+
<Tab {...tabProps[2]} />
27+
</TabList>
28+
<TabPanel className="lumx-spacing-padding-huge">{tabProps[0].label} content</TabPanel>
29+
<TabPanel className="lumx-spacing-padding-huge">{tabProps[1].label} content</TabPanel>
30+
<TabPanel className="lumx-spacing-padding-huge">{tabProps[2].label} content</TabPanel>
31+
</TabProvider>
32+
),
33+
args: toFlattenProps({
34+
tabProps: [
35+
{ label: 'Tab 1' },
36+
{
37+
label: 'Tab 2',
38+
isDisabled: true,
39+
},
40+
{ label: 'Tab 3' },
41+
],
42+
}),
43+
argTypes: toFlattenProps({
44+
tabProviderProps: withCategory('Tab Provider', {
45+
isLazy: { control: 'boolean' },
46+
shouldActivateOnFocus: { control: 'boolean' },
47+
}),
48+
tabListProps: withCategory('Tab List', {
49+
layout: getSelectArgType(TabListLayout),
50+
position: getSelectArgType([Alignment.left, Alignment.center, Alignment.right]),
51+
}),
52+
tabProps: times(3, (index) =>
53+
withCategory(`Tab ${index + 1}`, {
54+
label: { control: 'text' },
55+
icon: iconArgType,
56+
isDisabled: { control: 'boolean' },
57+
}),
58+
),
59+
}),
60+
};
761

862
/* Control active tab externally (with activate tab on focus). */
9-
export const Controlled = ({ theme }: any) => {
10-
const [activeTab, setActiveTab] = useState(1);
11-
const changeActiveTabIndex = (evt: any) => setActiveTab(parseInt(get(evt, 'target.value', '0'), 10));
63+
export const Controlled = {
64+
render({ theme }: any) {
65+
const [activeTab, setActiveTab] = useState(1);
66+
const changeActiveTabIndex = (evt: any) => setActiveTab(parseInt(get(evt, 'target.value', '0'), 10));
1267

13-
const [isLazy, setIsLazy] = useState(true);
14-
const changeIsLazy = (evt: any) => setIsLazy(get(evt, 'target.checked'));
68+
const [isLazy, setIsLazy] = useState(true);
69+
const changeIsLazy = (evt: any) => setIsLazy(get(evt, 'target.checked'));
1570

16-
const [shouldActivateOnFocus, setShouldActivateOnFocus] = useState(true);
17-
const changeShouldActivateOnFocus = (evt: any) => setShouldActivateOnFocus(get(evt, 'target.checked'));
71+
const [shouldActivateOnFocus, setShouldActivateOnFocus] = useState(true);
72+
const changeShouldActivateOnFocus = (evt: any) => setShouldActivateOnFocus(get(evt, 'target.checked'));
1873

19-
return (
20-
<>
21-
<div>
22-
Active tab index:
23-
<input type="number" min={0} max={2} value={activeTab} onChange={changeActiveTabIndex} />
24-
</div>
74+
return (
75+
<>
76+
<div>
77+
Active tab index:
78+
<input type="number" min={0} max={2} value={activeTab} onChange={changeActiveTabIndex} />
79+
</div>
2580

26-
<div>
27-
Lazy render tab panel content:
28-
<input type="checkbox" checked={isLazy} onChange={changeIsLazy} />
29-
</div>
81+
<div>
82+
Lazy render tab panel content:
83+
<input type="checkbox" checked={isLazy} onChange={changeIsLazy} />
84+
</div>
3085

31-
<div>
32-
Activate tab on focus:
33-
<input type="checkbox" checked={shouldActivateOnFocus} onChange={changeShouldActivateOnFocus} />
34-
</div>
35-
<TabProvider
36-
activeTabIndex={activeTab}
37-
onChange={setActiveTab}
38-
isLazy={isLazy}
39-
shouldActivateOnFocus={shouldActivateOnFocus}
40-
>
41-
<TabList theme={theme} aria-label="Tab list">
42-
<Tab label="Tab a" />
43-
<Tab label="Tab b" />
44-
<Tab label="Tab c" />
45-
</TabList>
86+
<div>
87+
Activate tab on focus:
88+
<input type="checkbox" checked={shouldActivateOnFocus} onChange={changeShouldActivateOnFocus} />
89+
</div>
90+
<TabProvider
91+
activeTabIndex={activeTab}
92+
onChange={setActiveTab}
93+
isLazy={isLazy}
94+
shouldActivateOnFocus={shouldActivateOnFocus}
95+
>
96+
<TabList theme={theme} aria-label="Tab list">
97+
<Tab label="Tab a" />
98+
<Tab label="Tab b" />
99+
<Tab label="Tab c" />
100+
</TabList>
46101

47-
<TabPanel className="lumx-spacing-padding-huge">Tab a content</TabPanel>
48-
<TabPanel className="lumx-spacing-padding-huge">Tab b content</TabPanel>
49-
<TabPanel className="lumx-spacing-padding-huge">Tab c content</TabPanel>
50-
</TabProvider>
51-
</>
52-
);
102+
<TabPanel className="lumx-spacing-padding-huge">Tab a content</TabPanel>
103+
<TabPanel className="lumx-spacing-padding-huge">Tab b content</TabPanel>
104+
<TabPanel className="lumx-spacing-padding-huge">Tab c content</TabPanel>
105+
</TabProvider>
106+
</>
107+
);
108+
},
109+
chromatic: { disable: true },
53110
};
54111

55112
/* Display tabs far from their tab panels. */
56-
export const SplitTabListAndTabPanels = ({ theme }: any) => {
57-
const [isOpen, setOpen] = useState(true);
58-
const [activeTabIndex, onChange] = useState(1);
113+
export const SplitTabListAndTabPanels = {
114+
render({ theme }: any) {
115+
const [isOpen, setOpen] = useState(true);
116+
const [activeTabIndex, onChange] = useState(1);
59117

60-
return (
61-
<TabProvider activeTabIndex={activeTabIndex} onChange={onChange} isLazy={false}>
62-
<Button
63-
onClick={() => {
64-
setOpen(!isOpen);
65-
onChange(1);
66-
}}
67-
>
68-
Open dialog with tabs in footer
69-
</Button>
70-
<Dialog isOpen={isOpen} forceFooterDivider onClose={() => setOpen(false)}>
71-
<TabPanel className="lumx-spacing-padding-huge">Tab 1 content</TabPanel>
72-
<TabPanel className="lumx-spacing-padding-huge">Tab 2 content</TabPanel>
73-
<TabPanel className="lumx-spacing-padding-huge">Tab 3 content</TabPanel>
118+
return (
119+
<TabProvider activeTabIndex={activeTabIndex} onChange={onChange} isLazy={false}>
120+
<Button
121+
onClick={() => {
122+
setOpen(!isOpen);
123+
onChange(1);
124+
}}
125+
>
126+
Open dialog with tabs in footer
127+
</Button>
128+
<Dialog isOpen={isOpen} forceFooterDivider onClose={() => setOpen(false)}>
129+
<TabPanel className="lumx-spacing-padding-huge">Tab 1 content</TabPanel>
130+
<TabPanel className="lumx-spacing-padding-huge">Tab 2 content</TabPanel>
131+
<TabPanel className="lumx-spacing-padding-huge">Tab 3 content</TabPanel>
74132

75-
<footer>
76-
<TabList theme={theme} aria-label="Tab list">
77-
<Tab label="Tab 1" />
78-
<Tab label="Tab 2" />
79-
<Tab label="Tab 3" />
80-
</TabList>
81-
</footer>
82-
</Dialog>
83-
</TabProvider>
84-
);
133+
<footer>
134+
<TabList theme={theme} aria-label="Tab list">
135+
<Tab label="Tab 1" />
136+
<Tab label="Tab 2" />
137+
<Tab label="Tab 3" />
138+
</TabList>
139+
</footer>
140+
</Dialog>
141+
</TabProvider>
142+
);
143+
},
144+
chromatic: { disable: true },
85145
};
86146

87147
/* Dynamically generate tabs. */
88-
export const DynamicTabs = ({ theme, tabCount }: any) => {
89-
return (
90-
<TabProvider>
91-
<TabList theme={theme} aria-label="Tab list">
148+
export const DynamicTabs = {
149+
render({ theme, tabCount }: any) {
150+
return (
151+
<TabProvider>
152+
<TabList theme={theme} aria-label="Tab list">
153+
{times(tabCount, (tabNumber) => (
154+
<Tab key={tabNumber} label={`Tab ${tabNumber}`} />
155+
))}
156+
</TabList>
157+
92158
{times(tabCount, (tabNumber) => (
93-
<Tab key={tabNumber} label={`Tab ${tabNumber}`} />
159+
<TabPanel key={tabNumber} className="lumx-spacing-padding-huge">
160+
Tab {tabNumber} content
161+
</TabPanel>
94162
))}
95-
</TabList>
96-
97-
{times(tabCount, (tabNumber) => (
98-
<TabPanel key={tabNumber} className="lumx-spacing-padding-huge">
99-
Tab {tabNumber} content
100-
</TabPanel>
101-
))}
102-
</TabProvider>
103-
);
104-
};
105-
DynamicTabs.args = {
106-
tabCount: 3,
163+
</TabProvider>
164+
);
165+
},
166+
args: {
167+
tabCount: 3,
168+
},
169+
chromatic: { disable: true },
107170
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export type PathPart = string | number;
2+
export type Path = PathPart | Array<PathPart>;
3+
4+
/**
5+
* Concat flatten object path
6+
*
7+
* @example concatPath('foo', 'bar') // => 'foo.bar'
8+
* @example concatPath(['foo', 0]) // => 'foo[0]'
9+
* @example concatPath('foo', 0, ['bar']) // => 'foo[0].bar'
10+
*/
11+
export const concatPath = (...prefix: Path[]) => {
12+
const [first, ...rest] = prefix.flat();
13+
return rest.reduce<string>((acc, part) => {
14+
if (typeof part === 'number') return `${acc}[${part}]`;
15+
return `${acc}.${part}`;
16+
}, String(first));
17+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import castArray from 'lodash/castArray';
2+
import { concatPath } from './concatPath';
3+
4+
type Props = Record<string, any>;
5+
type OneOrMoreProps = Props | Array<Props>;
6+
7+
/**
8+
* Build a flat props object from the given nested props
9+
*
10+
* @example toFlattenProps({ foo: { bar: 4 } }) // => { 'foo.bar': 4 }
11+
* @example toFlattenProps({ foo: [{ bar: 4 }, { bar: 5 }] }) // => { 'foo[0].bar': 4, 'foo[0].bar': 5 }
12+
*/
13+
export function toFlattenProps(props: { [prefix: string]: OneOrMoreProps }): Props {
14+
const out: Props = {};
15+
for (const [prefix, oneOrMoreProps] of Object.entries(props)) {
16+
const propsArray = castArray(oneOrMoreProps);
17+
18+
for (let i = 0; i < propsArray.length; i += 1) {
19+
const path = Array.isArray(oneOrMoreProps) ? concatPath(prefix, i) : prefix;
20+
const subProps = propsArray[i];
21+
22+
for (const [key, value] of Object.entries(subProps)) {
23+
out[concatPath(path, key)] = value;
24+
}
25+
}
26+
}
27+
return out;
28+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import setWith from 'lodash/setWith';
2+
3+
/**
4+
* Add table.category to the provided argTypes
5+
*/
6+
export function withCategory(category: string, argTypes: Record<string, any>) {
7+
const out: typeof argTypes = {};
8+
for (const [key, value] of Object.entries(argTypes)) {
9+
out[key] = setWith({ ...value }, 'table.category', category);
10+
}
11+
return out;
12+
}

0 commit comments

Comments
 (0)