mirror of
https://github.com/wassname/talk.git
synced 2026-07-27 11:28:12 +08:00
Merge pull request #1858 from coralproject/ui-tab
[next] Tabs Components - Tab, TabBar, TabPane, TabContent
This commit is contained in:
@@ -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>
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user