Merge pull request #1858 from coralproject/ui-tab

[next] Tabs Components - Tab, TabBar, TabPane, TabContent
This commit is contained in:
Kiwi
2018-09-18 21:08:36 +02:00
committed by GitHub
17 changed files with 541 additions and 0 deletions
@@ -0,0 +1,54 @@
.root {
display: inline-block;
list-style: none;
margin-right: -1px;
margin-bottom: -1px;
}
.button {
height: 100%;
box-sizing: border-box;
border-bottom: 0;
list-style: none;
padding: var(--spacing-unit);
font-weight: var(--font-weight-regular);
font-family: var(--font-family-serif);
&:hover {
cursor: pointer;
}
}
.root:first-child .primary {
border-top-left-radius: var(--round-corners);
}
.root:last-child .primary {
border-top-right-radius: var(--round-corners);
}
.primary {
position: relative;
background: var(--palette-grey-lightest);
color: var(--palette-grey-main);
border: 1px solid var(--palette-grey-lighter);
padding: calc(0.5 * var(--spacing-unit)) calc(var(--spacing-unit) * 2);
&.active {
background-color: var(--palette-common-white);
color: var(--palette-common-black);
border-bottom: 0;
border-top-width: calc(0.5 * var(--spacing-unit));
border-top-color: var(--palette-primary-main);
border-radius: 0;
z-index: 10;
}
}
.secondary {
padding: calc(0.5 * var(--spacing-unit)) calc(var(--spacing-unit) * 2);
&.active {
font-weight: var(--font-weight-medium);
border-bottom: 3px solid var(--palette-primary-main);
}
}
@@ -0,0 +1,9 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import Tab from "./Tab";
it("renders correctly", () => {
const renderer = TestRenderer.create(<Tab tabId="three">Three</Tab>);
expect(renderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,76 @@
import cn from "classnames";
import React from "react";
import { withStyles } from "talk-ui/hocs";
import BaseButton from "../BaseButton";
import * as styles from "./Tab.css";
export interface TabProps {
/**
* Convenient prop to override the root styling.
*/
className?: string;
/**
* Override or extend the styles applied to the component.
*/
classes: typeof styles;
/**
* The id/name of the tab
*/
tabId: string;
/**
* Active status
*/
active?: boolean;
/**
* Style variant
*/
variant?: "primary" | "secondary";
/**
* Action taken on tab click
*/
onTabClick?: (tabId: string) => void;
}
class Tab extends React.Component<TabProps> {
public handleTabClick = () => {
if (this.props.onTabClick) {
this.props.onTabClick(this.props.tabId);
}
};
public render() {
const { className, classes, children, tabId, active, variant } = this.props;
const buttonClassName = cn(
classes.button,
{
[classes.primary]: variant === "primary",
[classes.secondary]: variant === "secondary",
[classes.active]: active,
},
className
);
return (
<li
className={styles.root}
key={tabId}
id={`tab-${tabId}`}
role="presentation"
>
<BaseButton
className={buttonClassName}
aria-controls={`tabPane-${tabId}`}
role="tab"
aria-selected={active}
onClick={this.handleTabClick}
>
{children}
</BaseButton>
</li>
);
}
}
const enhanced = withStyles(styles)(Tab);
export default enhanced;
@@ -0,0 +1,13 @@
.root {
display: flex;
padding: 0;
margin: 0;
}
.primary {
border-bottom: 1px solid var(--palette-grey-lighter);
}
.secondary {
border-bottom: 1px solid var(--palette-divider);
}
@@ -0,0 +1,34 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import Tab from "./Tab";
import TabBar from "./TabBar";
it("renders correctly", () => {
const renderer = TestRenderer.create(
<TabBar activeTab="one">
<Tab tabId="one">One</Tab>
<Tab tabId="two">Two</Tab>
<Tab tabId="three">Three</Tab>
</TabBar>
);
expect(renderer.toJSON()).toMatchSnapshot();
});
it("sets initial tab as active", () => {
const renderer = TestRenderer.create(
<TabBar activeTab="one">
<Tab tabId="one">One</Tab>
<Tab tabId="two">Two</Tab>
<Tab tabId="three">Three</Tab>
</TabBar>
);
const testInstance = renderer.root;
expect(testInstance.findByType(TabBar).props.activeTab).toBe("one");
const tabs = testInstance.findAllByType(Tab);
expect(tabs.length).toBe(3);
expect(tabs[0].props.active).toBe(true);
expect(tabs[1].props.active).toBe(false);
expect(tabs[2].props.active).toBe(false);
});
@@ -0,0 +1,80 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { withStyles } from "talk-ui/hocs";
import * as styles from "./TabBar.css";
export interface TabBarProps {
/**
* Convenient prop to override the root styling.
*/
className?: string;
/**
* Override or extend the styles applied to the component.
*/
classes: typeof styles;
/**
* Style variant
*/
variant?: "primary" | "secondary";
/**
* Active tab id/name
*/
activeTab?: string;
/**
* Default active tab id/name
*/
defaultActiveTab?: string;
/**
* Action taken on tab click
*/
onTabClick?: (tabId: string) => void;
}
const TabBar: StatelessComponent<TabBarProps> = props => {
const {
className,
classes,
children,
onTabClick,
activeTab,
variant,
defaultActiveTab,
} = props;
const rootClassName = cn(
classes.root,
[
{
[classes.primary]: variant === "primary",
[classes.secondary]: variant === "secondary",
},
],
className
);
const tabs = React.Children.toArray(children).map(
(child: React.ReactElement<any>, index: number) =>
React.cloneElement(child, {
tabId: child.props.tabId ? child.props.tabId : index,
active:
defaultActiveTab && !activeTab
? child.props.tabId === defaultActiveTab
: child.props.tabId === activeTab,
variant,
onTabClick,
})
);
return (
<ul className={rootClassName} role="tablist">
{tabs}
</ul>
);
};
TabBar.defaultProps = {
variant: "primary",
};
const enhanced = withStyles(styles)(TabBar);
export default enhanced;
@@ -0,0 +1,31 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import TabContent from "./TabContent";
import TabPane from "./TabPane";
it("renders correctly", () => {
const renderer = TestRenderer.create(
<TabContent activeTab="one">
<TabPane tabId="one">Hola One</TabPane>
<TabPane tabId="two">Hola Two</TabPane>
<TabPane tabId="three">Hola Three</TabPane>
</TabContent>
);
expect(renderer.toJSON()).toMatchSnapshot();
});
it("sets initial tab as active, renders only one", () => {
const renderer = TestRenderer.create(
<TabContent activeTab="one">
<TabPane tabId="one">Hola One</TabPane>
<TabPane tabId="two">Hola Two</TabPane>
<TabPane tabId="three">Hola Three</TabPane>
</TabContent>
);
const testInstance = renderer.root;
expect(testInstance.findByType(TabContent).props.activeTab).toBe("one");
const panes = testInstance.findAllByType(TabPane);
expect(panes.length).toBe(1);
});
@@ -0,0 +1,27 @@
import React, { StatelessComponent } from "react";
export interface TabContentProps {
/**
* Active tab id/name
*/
activeTab?: string;
}
const TabContent: StatelessComponent<TabContentProps> = props => {
const { children, activeTab } = props;
return (
<>
{React.Children.toArray(children)
.filter(
(child: React.ReactElement<any>) => child.props.tabId === activeTab
)
.map((child: React.ReactElement<any>, i) =>
React.cloneElement(child, {
tabId: child.props.tabId ? child.props.tabId : i,
})
)}
</>
);
};
export default TabContent;
@@ -0,0 +1,9 @@
import React from "react";
import TestRenderer from "react-test-renderer";
import TabPane from "./TabPane";
it("renders correctly", () => {
const renderer = TestRenderer.create(<TabPane tabId="three">Three</TabPane>);
expect(renderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,29 @@
import React, { StatelessComponent } from "react";
export interface TabBarProps {
/**
* Convenient prop to override the root styling.
*/
className?: string;
/**
* Name of the tab
*/
tabId: string;
}
const TabPane: StatelessComponent<TabBarProps> = props => {
const { className, children, tabId } = props;
return (
<section
className={className}
key={tabId}
id={`tabPane-${tabId}`}
role="tabpanel"
aria-labelledby={`tab-${tabId}`}
>
{children}
</section>
);
};
export default TabPane;
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<li
className="Tab-root"
id="tab-three"
role="presentation"
>
<button
aria-controls="tabPane-three"
className="BaseButton-root Tab-button"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
Three
</button>
</li>
`;
@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<ul
className="TabBar-root TabBar-primary"
role="tablist"
>
<li
className="Tab-root"
id="tab-one"
role="presentation"
>
<button
aria-controls="tabPane-one"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
One
</button>
</li>
<li
className="Tab-root"
id="tab-two"
role="presentation"
>
<button
aria-controls="tabPane-two"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
Two
</button>
</li>
<li
className="Tab-root"
id="tab-three"
role="presentation"
>
<button
aria-controls="tabPane-three"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
Three
</button>
</li>
</ul>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<section
aria-labelledby="tab-one"
id="tabPane-one"
role="tabpanel"
>
Hola One
</section>
`;
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<section
aria-labelledby="tab-three"
id="tabPane-three"
role="tabpanel"
>
Three
</section>
`;
@@ -0,0 +1,4 @@
export { default as TabBar } from "./TabBar";
export { default as Tab } from "./Tab";
export { default as TabPane } from "./TabPane";
export { default as TabContent } from "./TabContent";
@@ -0,0 +1,52 @@
---
name: Tabs
menu: UI Kit
---
# Tabs
### Examples
import { Playground } from 'docz'
import { Flex, TabBar, Tab, TabContent, TabPane } from './index'
import Container from "react-with-state-props"
## Primary Tabs
<Playground>
<Container
state={{ activeId: "two" }}
render={props => (
<div>
<TabBar activeTab={props.activeId} onTabClick={(id)=> props.setActiveId(id)}>
<Tab tabId="one">One</Tab>
<Tab tabId="two">Two</Tab>
<Tab tabId="three">Three</Tab>
</TabBar>
<TabContent activeTab={props.activeId}>
<TabPane tabId="one">Hola One</TabPane>
<TabPane tabId="two">Hola Two</TabPane>
<TabPane tabId="three">Hola Three</TabPane>
</TabContent>
</div>
)}/>
</Playground>
## Secondary Tabs
<Playground>
<Container
state={{ activeId: "two" }}
render={props => (
<div>
<TabBar variant="secondary" activeTab={props.activeId} onTabClick={(id)=> props.setActiveId(id)}>
<Tab tabId="one">One</Tab>
<Tab tabId="two">Two</Tab>
<Tab tabId="three">Three</Tab>
</TabBar>
<TabContent activeTab={props.activeId}>
<TabPane tabId="one">Hola One</TabPane>
<TabPane tabId="two">Hola Two</TabPane>
<TabPane tabId="three">Hola Three</TabPane>
</TabContent>
</div>
)}/>
</Playground>
+1
View File
@@ -21,3 +21,4 @@ export { default as HorizontalGutter } from "./HorizontalGutter";
export { default as Icon } from "./Icon";
export { default as AriaInfo } from "./AriaInfo";
export { default as Message, MessageIcon } from "./Message";
export { Tab, TabBar, TabContent, TabPane } from "./Tabs";