diff --git a/src/core/client/ui/components/Tabs/Tab.css b/src/core/client/ui/components/Tabs/Tab.css new file mode 100644 index 000000000..ead948a36 --- /dev/null +++ b/src/core/client/ui/components/Tabs/Tab.css @@ -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); + } +} diff --git a/src/core/client/ui/components/Tabs/Tab.spec.tsx b/src/core/client/ui/components/Tabs/Tab.spec.tsx new file mode 100644 index 000000000..3af6a904a --- /dev/null +++ b/src/core/client/ui/components/Tabs/Tab.spec.tsx @@ -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(Three); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/Tabs/Tab.tsx b/src/core/client/ui/components/Tabs/Tab.tsx new file mode 100644 index 000000000..fd7e99e0f --- /dev/null +++ b/src/core/client/ui/components/Tabs/Tab.tsx @@ -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 { + 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 ( + + ); + } +} + +const enhanced = withStyles(styles)(Tab); +export default enhanced; diff --git a/src/core/client/ui/components/Tabs/TabBar.css b/src/core/client/ui/components/Tabs/TabBar.css new file mode 100644 index 000000000..0ba3a25f2 --- /dev/null +++ b/src/core/client/ui/components/Tabs/TabBar.css @@ -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); +} diff --git a/src/core/client/ui/components/Tabs/TabBar.spec.tsx b/src/core/client/ui/components/Tabs/TabBar.spec.tsx new file mode 100644 index 000000000..ffe91d4fd --- /dev/null +++ b/src/core/client/ui/components/Tabs/TabBar.spec.tsx @@ -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( + + One + Two + Three + + ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("sets initial tab as active", () => { + const renderer = TestRenderer.create( + + One + Two + Three + + ); + + 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); +}); diff --git a/src/core/client/ui/components/Tabs/TabBar.tsx b/src/core/client/ui/components/Tabs/TabBar.tsx new file mode 100644 index 000000000..29f993bf1 --- /dev/null +++ b/src/core/client/ui/components/Tabs/TabBar.tsx @@ -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 = 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, 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 ( +
    + {tabs} +
+ ); +}; + +TabBar.defaultProps = { + variant: "primary", +}; + +const enhanced = withStyles(styles)(TabBar); +export default enhanced; diff --git a/src/core/client/ui/components/Tabs/TabContent.spec.tsx b/src/core/client/ui/components/Tabs/TabContent.spec.tsx new file mode 100644 index 000000000..f411b53f5 --- /dev/null +++ b/src/core/client/ui/components/Tabs/TabContent.spec.tsx @@ -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( + + Hola One + Hola Two + Hola Three + + ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("sets initial tab as active, renders only one", () => { + const renderer = TestRenderer.create( + + Hola One + Hola Two + Hola Three + + ); + + const testInstance = renderer.root; + expect(testInstance.findByType(TabContent).props.activeTab).toBe("one"); + const panes = testInstance.findAllByType(TabPane); + expect(panes.length).toBe(1); +}); diff --git a/src/core/client/ui/components/Tabs/TabContent.tsx b/src/core/client/ui/components/Tabs/TabContent.tsx new file mode 100644 index 000000000..da68f05a9 --- /dev/null +++ b/src/core/client/ui/components/Tabs/TabContent.tsx @@ -0,0 +1,27 @@ +import React, { StatelessComponent } from "react"; + +export interface TabContentProps { + /** + * Active tab id/name + */ + activeTab?: string; +} + +const TabContent: StatelessComponent = props => { + const { children, activeTab } = props; + return ( + <> + {React.Children.toArray(children) + .filter( + (child: React.ReactElement) => child.props.tabId === activeTab + ) + .map((child: React.ReactElement, i) => + React.cloneElement(child, { + tabId: child.props.tabId ? child.props.tabId : i, + }) + )} + + ); +}; + +export default TabContent; diff --git a/src/core/client/ui/components/Tabs/TabPane.spec.tsx b/src/core/client/ui/components/Tabs/TabPane.spec.tsx new file mode 100644 index 000000000..595e054de --- /dev/null +++ b/src/core/client/ui/components/Tabs/TabPane.spec.tsx @@ -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(Three); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/Tabs/TabPane.tsx b/src/core/client/ui/components/Tabs/TabPane.tsx new file mode 100644 index 000000000..a3c84480c --- /dev/null +++ b/src/core/client/ui/components/Tabs/TabPane.tsx @@ -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 = props => { + const { className, children, tabId } = props; + return ( +
+ {children} +
+ ); +}; + +export default TabPane; diff --git a/src/core/client/ui/components/Tabs/__snapshots__/Tab.spec.tsx.snap b/src/core/client/ui/components/Tabs/__snapshots__/Tab.spec.tsx.snap new file mode 100644 index 000000000..a30d114a8 --- /dev/null +++ b/src/core/client/ui/components/Tabs/__snapshots__/Tab.spec.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + +`; diff --git a/src/core/client/ui/components/Tabs/__snapshots__/TabBar.spec.tsx.snap b/src/core/client/ui/components/Tabs/__snapshots__/TabBar.spec.tsx.snap new file mode 100644 index 000000000..fd9f2ae4a --- /dev/null +++ b/src/core/client/ui/components/Tabs/__snapshots__/TabBar.spec.tsx.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
    + + + +
+`; diff --git a/src/core/client/ui/components/Tabs/__snapshots__/TabContent.spec.tsx.snap b/src/core/client/ui/components/Tabs/__snapshots__/TabContent.spec.tsx.snap new file mode 100644 index 000000000..cdaf48d7d --- /dev/null +++ b/src/core/client/ui/components/Tabs/__snapshots__/TabContent.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ Hola One +
+`; diff --git a/src/core/client/ui/components/Tabs/__snapshots__/TabPane.spec.tsx.snap b/src/core/client/ui/components/Tabs/__snapshots__/TabPane.spec.tsx.snap new file mode 100644 index 000000000..ad884add2 --- /dev/null +++ b/src/core/client/ui/components/Tabs/__snapshots__/TabPane.spec.tsx.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ Three +
+`; diff --git a/src/core/client/ui/components/Tabs/index.ts b/src/core/client/ui/components/Tabs/index.ts new file mode 100644 index 000000000..f8818e9ea --- /dev/null +++ b/src/core/client/ui/components/Tabs/index.ts @@ -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"; diff --git a/src/core/client/ui/components/Tabs/tabs.mdx b/src/core/client/ui/components/Tabs/tabs.mdx new file mode 100644 index 000000000..f465209ae --- /dev/null +++ b/src/core/client/ui/components/Tabs/tabs.mdx @@ -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 + + ( +
+ props.setActiveId(id)}> + One + Two + Three + + + Hola One + Hola Two + Hola Three + +
+ )}/> +
+ +## Secondary Tabs + + ( +
+ props.setActiveId(id)}> + One + Two + Three + + + Hola One + Hola Two + Hola Three + +
+ )}/> +
diff --git a/src/core/client/ui/components/index.ts b/src/core/client/ui/components/index.ts index a6d027393..6477af50e 100644 --- a/src/core/client/ui/components/index.ts +++ b/src/core/client/ui/components/index.ts @@ -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";