diff --git a/package.json b/package.json
index 0dd5c5b65..2907d98eb 100644
--- a/package.json
+++ b/package.json
@@ -26,11 +26,14 @@
},
"devDependencies": {
"@babel/cli": "^7.1.5",
+ "@types/enzyme": "^3.1.15",
+ "@types/enzyme-adapter-react-16": "^1.0.3",
"@types/jest": "^23.3.10",
"@types/node": "^10.12.12",
"@types/react": "^16.7.13",
"@types/react-dom": "^16.0.11",
"@types/react-router-dom": "^4.3.1",
+ "@types/react-test-renderer": "^16.0.3",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"fiori-fundamentals": "^1.3.3",
diff --git a/src/Tabs/Tab.tsx b/src/Tabs/Tab.tsx
new file mode 100644
index 000000000..ee33f24b6
--- /dev/null
+++ b/src/Tabs/Tab.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { ReactNode } from 'react';
+import { TabKey } from "./Tabs";
+import { StatelessComponent } from 'enzyme';
+
+export interface TabProps {
+ /**
+ * A unique (per Tabs component) identifier for the tab.
+ */
+ key: TabKey;
+
+ /**
+ * The content to render in the tab title
+ */
+ title: ReactNode;
+
+ /**
+ * If the tab is disabled. If disabled, it cannot be activated.
+ */
+ disabled?: boolean;
+
+ /**
+ * The tab content
+ */
+ children?: ReactNode;
+}
+
+/**
+ * A Tab to render within the Tabs component.
+ */
+export const Tab: StatelessComponent = (props: TabProps) => (
+
+ {props.children || null}
+
+);
\ No newline at end of file
diff --git a/src/Tabs/TabHeader.tsx b/src/Tabs/TabHeader.tsx
new file mode 100644
index 000000000..95a2fccd5
--- /dev/null
+++ b/src/Tabs/TabHeader.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import { ReactNode } from 'react';
+import { TabProps } from './Tab';
+import { TabKey } from './Tabs';
+
+export interface TabHeaderProps {
+ tabs: React.ReactElement[];
+ selectedTabKey?: string;
+ onSelectTab(key: TabKey): any;
+}
+
+export interface TabHeaderItemProps {
+ tabKey: string;
+ title: ReactNode;
+ active: boolean;
+ disabled: boolean;
+ onSelectTab(key: TabKey): any;
+}
+
+export class TabHeaderItem extends React.Component {
+ render() {
+ const { disabled, title, active } = this.props;
+ const classes = ['fd-tabs__link'];
+ if (active) {
+ classes.push('is-selected');
+ }
+ return (
+
+ `${a} ${b}`, '')}
+ aria-disabled={disabled}
+ onClick={this.onClicked}
+ >
+ {title}
+
+
+ )
+ }
+
+ private onClicked = () => {
+ const { disabled, tabKey, onSelectTab } = this.props;
+ if (!disabled) {
+ onSelectTab(tabKey);
+ }
+ }
+}
+
+export const TabHeader = (props: TabHeaderProps) => {
+ return (
+
+ {
+ props.tabs.map(tab => (
+
+ ))
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/src/Tabs/Tabs.Component.js b/src/Tabs/Tabs.Component.js
deleted file mode 100644
index 8bf237141..000000000
--- a/src/Tabs/Tabs.Component.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React from 'react';
-import { Tabs, TabComponent } from '../';
-import { DocsTile, DocsText, Separator, Header, Description, Import, Properties, Playground } from '../';
-
-export const TabsComponent = () => {
- const tabscomponentCode = `
-
-
-
- `;
-
- return (
-
-
-
- Tabs are based on a folder metaphor and used to separate content into different sections. Tabs should be
- ordered to create a visual hierarchy based on priority.
-
-
-
-
-
-
-
-
-
{tabscomponentCode}
-
-
Playground
-
-
-
-
-
-
- );
-};
diff --git a/src/Tabs/Tabs.Component.tsx b/src/Tabs/Tabs.Component.tsx
new file mode 100644
index 000000000..86ff8e8d8
--- /dev/null
+++ b/src/Tabs/Tabs.Component.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { Tabs, Tab } from '.';
+import { DocsTile, DocsText, Separator, Header, Description, Import, Properties, Playground } from '..';
+
+export const TabsComponent = () => {
+ const tabscomponentCode = `
+
+
+
+
+ `;
+
+ return (
+
+
+
+ Tabs are based on a folder metaphor and used to separate content into different sections. Tabs should be
+ ordered to create a visual hierarchy based on priority.
+
+
+
+
+
+ Hello World
+ Hello World 2
+ Hello World 3
+
+
+
{tabscomponentCode}
+
+
+ );
+};
diff --git a/src/Tabs/Tabs.js b/src/Tabs/Tabs.js
deleted file mode 100644
index 854992a1f..000000000
--- a/src/Tabs/Tabs.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { BrowserRouter, Link } from 'react-router-dom';
-
-export const Tabs = props => {
- const { children } = props;
- return ;
-};
-
-export class TabComponent extends Component {
- constructor(props) {
- super(props);
- let initialStates = [];
-
- initialStates = props.ids.forEach(ids => {
- let obj = {};
- let id = ids.id;
- obj[id] = false;
- return obj;
- });
- this.state = {
- selectedTab: '1',
- tabStates: initialStates
- };
-
- this.handleTabSelection = this.handleTabSelection.bind(this);
- }
-
- handleTabSelection(e, id) {
- let iStates = Object.assign({}, this.state.tabStates);
- iStates[id.id] = !iStates[id.id];
- this.setState({ tabStates: iStates });
- this.setState({ selectedTab: id.id });
- }
-
- render() {
- const { ids } = this.props;
- return (
-
-
- {ids.map(id => {
- return (
-
- {
- !id.disabled && this.handleTabSelection(e, id, id.disabled);
- }}
- >
- {id.name}
-
- {this.state.selectedTab === id.id ? (
- {id.content}
- ) : null}
-
- );
- })}
-
-
- );
- }
-}
-
-TabComponent.propTypes = {
- ids: PropTypes.array.isRequired
-};
diff --git a/src/Tabs/Tabs.test.js b/src/Tabs/Tabs.test.js
deleted file mode 100644
index 483b89e18..000000000
--- a/src/Tabs/Tabs.test.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react';
-import renderer from 'react-test-renderer';
-import Enzyme, { mount } from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-import { Tabs, TabComponent } from './Tabs';
-
-Enzyme.configure({ adapter: new Adapter() });
-
-describe(' ', () => {
- const tabComponent = (
-
- );
-
- const defaultTabs = {tabComponent} ;
-
- test('create tabs component', () => {
- let component = renderer.create(defaultTabs);
- let tree = component.toJSON();
- expect(tree).toMatchSnapshot();
-
- component = renderer.create(tabComponent);
- tree = component.toJSON();
- expect(tree).toMatchSnapshot();
- });
-
- test('tab selection', () => {
- const wrapper = mount(tabComponent);
-
- // check selected tab
- expect(wrapper.state(['selectedTab'])).toEqual('1');
-
- wrapper
- .find('ul.fd-tabs li.fd-tabs__item a.fd-tabs__link')
- .at(1)
- .simulate('click');
-
- // check selected tab changed
- expect(wrapper.state(['selectedTab'])).toEqual('2');
- });
-});
diff --git a/src/Tabs/Tabs.test.tsx b/src/Tabs/Tabs.test.tsx
new file mode 100644
index 000000000..d59574c21
--- /dev/null
+++ b/src/Tabs/Tabs.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Enzyme, { mount } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import { Tabs, Tab } from './index';
+
+import { writeFile } from 'fs';
+import { JsxEmit } from 'typescript';
+
+Enzyme.configure({ adapter: new Adapter() });
+
+describe(' ', () => {
+ function getTabs(additionalProps = {}) {
+ return (
+
+
+ Hello World
+
+
+ Hello World 2
+
+
+ Hello World
+
+
+ );
+ }
+
+ test('create tabs component', () => {
+ const tabs = getTabs();
+ let component = renderer.create(tabs);
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+
+ component = renderer.create(tabs);
+ tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+
+ test('tab selection', () => {
+ const onChangeFn = jest.fn();
+ const wrapper = mount(getTabs({ onChange: onChangeFn }));
+
+ expect((wrapper.state() as any)['activeKey']).toEqual('1');
+
+ wrapper
+ .find('ul.fd-tabs li.fd-tabs__item a.fd-tabs__link')
+ .at(1)
+ .simulate('click');
+
+ // check selected tab changed
+ expect((wrapper.state() as any).activeKey).toEqual('2');
+ expect(onChangeFn).toHaveBeenCalledWith('2', '1');
+ });
+});
diff --git a/src/Tabs/Tabs.tsx b/src/Tabs/Tabs.tsx
new file mode 100644
index 000000000..ae359b535
--- /dev/null
+++ b/src/Tabs/Tabs.tsx
@@ -0,0 +1,98 @@
+import * as React from 'react';
+import { ReactElement } from 'react';
+import { TabProps } from './Tab';
+import { TabHeader } from './TabHeader';
+
+export type TabKey = string;
+
+export interface TabsProps {
+ /**
+ * Set the active tab.
+ * If the active tab isn't set externally the component
+ * will behave as an uncontrolled component.
+ */
+ activeKey?: TabKey;
+ /**
+ * The content of the tabs component.
+ * Only Tab elements are allowed as children.
+ */
+ children?: ReactElement[] | ReactElement
+
+ /**
+ * Callback that is invoked whenever the active tab changes.
+ * @param selectedKey The newly selected key.
+ */
+ onChange?(selectedKey: TabKey, previousSelectedKey?: TabKey): any;
+}
+
+interface TabsState {
+ activeKey?: TabKey;
+}
+
+/**
+ * Component that renders react-fundamental tabs.
+ * It can either be used as a controlled component (by passing the active key as props and handling the callback)
+ * or as an uncontrolled component (in which case the state is managed internal to the component).
+ *
+ * Not implemented yet:
+ * * Overflow display (if the number of tabs exceeds the size of the tab container)
+ */
+export class Tabs extends React.PureComponent {
+ constructor(props: TabsProps) {
+ super(props);
+ // If no active key is passed in props, set the key of the first child as active key.
+ this.state = {
+ activeKey: props.activeKey || (this.childrenAsArray(props.children)[0] || {})['key'] as any
+ }
+ }
+
+ render() {
+ return (
+
+
+
+ {this.activeTabContent()}
+
+
+
+ );
+ }
+
+ private childrenAsArray = (
+ children: React.ReactElement[] | React.ReactElement | undefined = this.props.children
+ ): ReactElement[] => {
+
+ if (Array.isArray(children)) {
+ return children;
+ }
+ if (children) {
+ return [children];
+ }
+ return [];
+ };
+
+ private selectedTab: () => (TabKey | undefined) = () => {
+ if (this.props.activeKey !== undefined) {
+ return this.props.activeKey;
+ }
+ return this.state.activeKey;
+ };
+ private activeTabContent = () => {
+ return this.childrenAsArray()
+ .filter(child => (child as any).key === this.selectedTab()) || null;
+ }
+
+ private onTabSelected = (key: TabKey) => {
+ if (this.props.onChange) {
+ this.props.onChange(key, this.selectedTab());
+ }
+ this.setState({
+ activeKey: key
+ });
+ }
+}
+
diff --git a/src/Tabs/__snapshots__/Tabs.test.js.snap b/src/Tabs/__snapshots__/Tabs.test.js.snap
index 39abeb6ca..cce5f80c3 100644
--- a/src/Tabs/__snapshots__/Tabs.test.js.snap
+++ b/src/Tabs/__snapshots__/Tabs.test.js.snap
@@ -1,101 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[` create tabs component 1`] = `
-
+ ,
+
+ Hello World
+
,
+]
`;
exports[` create tabs component 2`] = `
-
-
-
-
+
+
- Tab 2
-
-
-
+
+ ,
+ ,
+]
`;
diff --git a/src/Tabs/index.ts b/src/Tabs/index.ts
new file mode 100644
index 000000000..b9b880ac6
--- /dev/null
+++ b/src/Tabs/index.ts
@@ -0,0 +1,2 @@
+export { Tab } from './Tab';
+export { Tabs } from './Tabs';
\ No newline at end of file
diff --git a/src/documentation/Playground/Playground.js b/src/documentation/Playground/Playground.js
index 33b3d8b7a..c279ebb94 100644
--- a/src/documentation/Playground/Playground.js
+++ b/src/documentation/Playground/Playground.js
@@ -6,7 +6,7 @@ import { Dropdown } from '../../';
import { Icon } from '../../';
import { Identifier } from '../../';
import { Image } from '../../';
-import { Tabs, TabComponent } from '../../';
+import { Tabs } from '../../';
import { FormGroup, FormLabel, FormItem, InputGroup } from '../../';
import { ListGroup, ListGroupItem, ListGroupItemActions } from '../../';
import { Tile, TileContent, TileMedia, TileActions, ProductTile, ProductTileContent, ProductTileMedia } from '../../';
@@ -393,7 +393,7 @@ export class Playground extends Component {
case 'tabs':
componentToGenerate = (
-
+ {this.state.childs.children}
);
break;
diff --git a/src/index.tsx b/src/index.tsx
index 7c3c6df39..3fb76e8f2 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -76,7 +76,7 @@ import {
SideNavGroup
} from '../src/SideNavigation/SideNavigation';
import { Table } from '../src/Table/Table';
-import { Tabs, TabComponent } from '../src/Tabs/Tabs';
+import { Tabs, Tab } from '../src/Tabs';
import { Token } from './Token/Token';
import {
Tile,
@@ -176,7 +176,7 @@ export {
SideNavGroup,
Table,
Tabs,
- TabComponent,
+ Tab,
Token,
Tile,
TileContent,