diff --git a/README.md b/README.md
index 5649606f86..490ff0ea8c 100644
--- a/README.md
+++ b/README.md
@@ -129,7 +129,7 @@ Once you change the flag, you need to refresh your browser to see the changes in
| ✓ Loader | | | Shape | |
| ✓ Rail | | | ✓ Sidebar | |
| ✓ Reveal | | | Sticky | |
-| ✓ Segment | | | Tab | |
+| ✓ Segment | | | ✓ Tab | |
| ✓ Step | | | Transition | |
## Our Principles
diff --git a/docs/app/Examples/modules/Tab/MenuVariations/TabExampleAttachedBottom.js b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleAttachedBottom.js
new file mode 100644
index 0000000000..899d820382
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleAttachedBottom.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleAttachedBottom = () => (
+
+)
+
+export default TabExampleAttachedBottom
diff --git a/docs/app/Examples/modules/Tab/MenuVariations/TabExampleAttachedFalse.js b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleAttachedFalse.js
new file mode 100644
index 0000000000..be54f7c941
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleAttachedFalse.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleAttachedFalse = () => (
+
+)
+
+export default TabExampleAttachedFalse
diff --git a/docs/app/Examples/modules/Tab/MenuVariations/TabExampleBorderless.js b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleBorderless.js
new file mode 100644
index 0000000000..face808e91
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleBorderless.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleBorderless = () => (
+
+)
+
+export default TabExampleBorderless
diff --git a/docs/app/Examples/modules/Tab/MenuVariations/TabExampleColored.js b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleColored.js
new file mode 100644
index 0000000000..36970d345d
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleColored.js
@@ -0,0 +1,41 @@
+import _ from 'lodash'
+import React, { Component } from 'react'
+import { Divider, Tab } from 'semantic-ui-react'
+
+const colors = [
+ 'red', 'orange', 'yellow', 'olive', 'green', 'teal',
+ 'blue', 'violet', 'purple', 'pink', 'brown', 'grey',
+]
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+class TabExampleColored extends Component {
+ state = { color: colors[0] }
+
+ handleColorChange = e => this.setState({ color: e.target.value })
+
+ render() {
+ const { color } = this.state
+
+ return (
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default TabExampleColored
diff --git a/docs/app/Examples/modules/Tab/MenuVariations/TabExampleColoredInverted.js b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleColoredInverted.js
new file mode 100644
index 0000000000..27181dcf83
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleColoredInverted.js
@@ -0,0 +1,41 @@
+import _ from 'lodash'
+import React, { Component } from 'react'
+import { Divider, Tab } from 'semantic-ui-react'
+
+const colors = [
+ 'red', 'orange', 'yellow', 'olive', 'green', 'teal',
+ 'blue', 'violet', 'purple', 'pink', 'brown', 'grey',
+]
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+class TabExampleColoredInverted extends Component {
+ state = { color: colors[0] }
+
+ handleColorChange = e => this.setState({ color: e.target.value })
+
+ render() {
+ const { color } = this.state
+
+ return (
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default TabExampleColoredInverted
diff --git a/docs/app/Examples/modules/Tab/MenuVariations/TabExampleTabularFalse.js b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleTabularFalse.js
new file mode 100644
index 0000000000..dd4ca5e113
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/MenuVariations/TabExampleTabularFalse.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleTabularFalse = () => (
+
+)
+
+export default TabExampleTabularFalse
diff --git a/docs/app/Examples/modules/Tab/MenuVariations/index.js b/docs/app/Examples/modules/Tab/MenuVariations/index.js
new file mode 100644
index 0000000000..b078bf3912
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/MenuVariations/index.js
@@ -0,0 +1,45 @@
+import React from 'react'
+import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'
+
+import { Message } from 'semantic-ui-react'
+
+const TabMenuVariationsExamples = () => (
+
+
+
+
+
+
+
+
+
+
+)
+
+export default TabMenuVariationsExamples
diff --git a/docs/app/Examples/modules/Tab/States/TabExampleLoading.js b/docs/app/Examples/modules/Tab/States/TabExampleLoading.js
new file mode 100644
index 0000000000..0e86026a12
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/States/TabExampleLoading.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleLoading = () => (
+
+)
+
+export default TabExampleLoading
diff --git a/docs/app/Examples/modules/Tab/States/index.js b/docs/app/Examples/modules/Tab/States/index.js
new file mode 100644
index 0000000000..75ab7651db
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/States/index.js
@@ -0,0 +1,15 @@
+import React from 'react'
+import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'
+
+const TabStatesExamples = () => (
+
+
+
+)
+
+export default TabStatesExamples
diff --git a/docs/app/Examples/modules/Tab/Types/TabExampleBasic.js b/docs/app/Examples/modules/Tab/Types/TabExampleBasic.js
new file mode 100644
index 0000000000..9b666b3ff3
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Types/TabExampleBasic.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleBasic = () => (
+
+)
+
+export default TabExampleBasic
diff --git a/docs/app/Examples/modules/Tab/Types/TabExamplePointing.js b/docs/app/Examples/modules/Tab/Types/TabExamplePointing.js
new file mode 100644
index 0000000000..7d5deba9b6
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Types/TabExamplePointing.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExamplePointing = () => (
+
+)
+
+export default TabExamplePointing
diff --git a/docs/app/Examples/modules/Tab/Types/TabExampleSecondary.js b/docs/app/Examples/modules/Tab/Types/TabExampleSecondary.js
new file mode 100644
index 0000000000..30a6e08539
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Types/TabExampleSecondary.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleSecondary = () => (
+
+)
+
+export default TabExampleSecondary
diff --git a/docs/app/Examples/modules/Tab/Types/TabExampleSecondaryPointing.js b/docs/app/Examples/modules/Tab/Types/TabExampleSecondaryPointing.js
new file mode 100644
index 0000000000..dbf75c79da
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Types/TabExampleSecondaryPointing.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleSecondaryPointing = () => (
+
+)
+
+export default TabExampleSecondaryPointing
diff --git a/docs/app/Examples/modules/Tab/Types/TabExampleText.js b/docs/app/Examples/modules/Tab/Types/TabExampleText.js
new file mode 100644
index 0000000000..efe6cebc68
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Types/TabExampleText.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleText = () => (
+
+)
+
+export default TabExampleText
diff --git a/docs/app/Examples/modules/Tab/Types/index.js b/docs/app/Examples/modules/Tab/Types/index.js
new file mode 100644
index 0000000000..bd831232cd
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Types/index.js
@@ -0,0 +1,33 @@
+import React from 'react'
+import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'
+
+const TabTypesExamples = () => (
+
+
+
+
+
+
+
+)
+
+export default TabTypesExamples
diff --git a/docs/app/Examples/modules/Tab/Usage/TabExampleActiveIndex.js b/docs/app/Examples/modules/Tab/Usage/TabExampleActiveIndex.js
new file mode 100644
index 0000000000..473955b32b
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Usage/TabExampleActiveIndex.js
@@ -0,0 +1,28 @@
+import React, { Component } from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+class TabExampleActiveIndex extends Component {
+ state = { activeIndex: 1 }
+
+ handleChange = e => this.setState({ activeIndex: e.target.value })
+
+ render() {
+ const { activeIndex } = this.state
+
+ return (
+
+
activeIndex: {activeIndex}
+
+
+
+ )
+ }
+}
+
+export default TabExampleActiveIndex
diff --git a/docs/app/Examples/modules/Tab/Usage/TabExampleDefaultActiveIndex.js b/docs/app/Examples/modules/Tab/Usage/TabExampleDefaultActiveIndex.js
new file mode 100644
index 0000000000..d09b70ecb4
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Usage/TabExampleDefaultActiveIndex.js
@@ -0,0 +1,14 @@
+import React from 'react'
+import { Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+const TabExampleDefaultActiveIndex = () => (
+
+)
+
+export default TabExampleDefaultActiveIndex
diff --git a/docs/app/Examples/modules/Tab/Usage/TabExampleOnTabChange.js b/docs/app/Examples/modules/Tab/Usage/TabExampleOnTabChange.js
new file mode 100644
index 0000000000..c5b0b50330
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Usage/TabExampleOnTabChange.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import { Segment, Tab } from 'semantic-ui-react'
+
+const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+]
+
+class TabExampleOnTabChange extends Component {
+ state = {}
+
+ handleChange = (e, data) => this.setState(data)
+
+ render() {
+ return (
+
+
+
+ {JSON.stringify(this.state, null, 2)}
+
+
+ )
+ }
+}
+
+export default TabExampleOnTabChange
diff --git a/docs/app/Examples/modules/Tab/Usage/index.js b/docs/app/Examples/modules/Tab/Usage/index.js
new file mode 100644
index 0000000000..50857b69c8
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/Usage/index.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'
+
+const TabUsageExamples = () => (
+
+
+
+
+
+)
+
+export default TabUsageExamples
diff --git a/docs/app/Examples/modules/Tab/index.js b/docs/app/Examples/modules/Tab/index.js
new file mode 100644
index 0000000000..ef0c133cc9
--- /dev/null
+++ b/docs/app/Examples/modules/Tab/index.js
@@ -0,0 +1,17 @@
+import React from 'react'
+
+import Types from './Types'
+import States from './States'
+import MenuVariations from './MenuVariations'
+import Usage from './Usage'
+
+const TabExamples = () => (
+
+
+
+
+
+
+)
+
+export default TabExamples
diff --git a/src/collections/Menu/Menu.d.ts b/src/collections/Menu/Menu.d.ts
index bf0ac7fcca..8d7e8b499e 100644
--- a/src/collections/Menu/Menu.d.ts
+++ b/src/collections/Menu/Menu.d.ts
@@ -12,7 +12,7 @@ export interface MenuProps {
as?: any;
/** Index of the currently active item. */
- activeIndex?: number;
+ activeIndex?: number | string;
/** A menu may be attached to other content segments. */
attached?: boolean | 'bottom' | 'top';
@@ -33,7 +33,7 @@ export interface MenuProps {
compact?: boolean;
/** Initial activeIndex value. */
- defaultActiveIndex?: number;
+ defaultActiveIndex?: number | string;
/** A menu can be fixed to a side of its context. */
fixed?: 'left'| 'right'| 'bottom'| 'top';
diff --git a/src/collections/Menu/Menu.js b/src/collections/Menu/Menu.js
index 3d41262130..b5d2912853 100644
--- a/src/collections/Menu/Menu.js
+++ b/src/collections/Menu/Menu.js
@@ -6,6 +6,7 @@ import React from 'react'
import {
AutoControlledComponent as Component,
customPropTypes,
+ createShorthandFactory,
getElementType,
getUnhandledProps,
META,
@@ -29,7 +30,10 @@ class Menu extends Component {
as: customPropTypes.as,
/** Index of the currently active item. */
- activeIndex: PropTypes.number,
+ activeIndex: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ ]),
/** A menu may be attached to other content segments. */
attached: PropTypes.oneOfType([
@@ -53,7 +57,10 @@ class Menu extends Component {
compact: PropTypes.bool,
/** Initial activeIndex value. */
- defaultActiveIndex: PropTypes.number,
+ defaultActiveIndex: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ ]),
/** A menu can be fixed to a side of its context. */
fixed: PropTypes.oneOf(['left', 'right', 'bottom', 'top']),
@@ -151,7 +158,7 @@ class Menu extends Component {
return _.map(items, (item, index) => MenuItem.create(item, {
defaultProps: {
- active: activeIndex === index,
+ active: parseInt(activeIndex, 10) === index,
index,
},
overrideProps: this.handleItemOverrides,
@@ -215,4 +222,6 @@ class Menu extends Component {
}
}
+Menu.create = createShorthandFactory(Menu, items => ({ items }))
+
export default Menu
diff --git a/src/collections/Message/Message.d.ts b/src/collections/Message/Message.d.ts
index c25d6198aa..4020a2c5ed 100644
--- a/src/collections/Message/Message.d.ts
+++ b/src/collections/Message/Message.d.ts
@@ -9,7 +9,7 @@ import { default as MessageList } from './MessageList';
export interface MessageProps {
[key: string]: any;
- /** An element type to render as (string or function). */
+ /** An element type to render as (string or function). */
as?: any;
/** A message can be formatted to attach itself to other content. */
diff --git a/src/index.js b/src/index.js
index 2b0c61b58c..a21723caaf 100644
--- a/src/index.js
+++ b/src/index.js
@@ -141,6 +141,9 @@ export { default as Sidebar } from './modules/Sidebar'
export { default as SidebarPushable } from './modules/Sidebar/SidebarPushable'
export { default as SidebarPusher } from './modules/Sidebar/SidebarPusher'
+export { default as Tab } from './modules/Tab'
+export { default as TabPane } from './modules/Tab/TabPane'
+
// Views
export { default as Advertisement } from './views/Advertisement'
diff --git a/src/lib/customPropTypes.js b/src/lib/customPropTypes.js
index 587ac7723c..a4736b1cec 100644
--- a/src/lib/customPropTypes.js
+++ b/src/lib/customPropTypes.js
@@ -282,7 +282,6 @@ export const contentShorthand = (...args) => every([
export const itemShorthand = (...args) => every([
disallow(['children']),
PropTypes.oneOfType([
- PropTypes.array,
PropTypes.node,
PropTypes.object,
]),
diff --git a/src/modules/Tab/Tab.d.ts b/src/modules/Tab/Tab.d.ts
new file mode 100644
index 0000000000..e3364e7c26
--- /dev/null
+++ b/src/modules/Tab/Tab.d.ts
@@ -0,0 +1,39 @@
+import * as React from 'react';
+import { default as TabPane } from './TabPane';
+
+export interface TabProps {
+ [key: string]: any;
+
+ /** An element type to render as (string or function). */
+ as?: any;
+
+ /** The initial activeIndex. */
+ defaultActiveIndex?: number | string;
+
+ /** Index of the currently active tab. */
+ activeIndex?: number | string;
+
+ /** Shorthand props for the Menu. */
+ menu?: any;
+
+ /**
+ * Called on tab change.
+ *
+ * @param {SyntheticEvent} event - React's original SyntheticEvent.
+ * @param {object} data - The proposed new Tab.Pane.
+ * @param {object} data.activeIndex - The new proposed activeIndex.
+ * @param {object} data.panes - Props of the new proposed active pane.
+ */
+ onTabChange?: (event: React.MouseEvent, data: TabProps) => void;
+
+ /** Shorthand props for the Menu. */
+ panes?: any;
+}
+
+interface TabComponent extends React.ComponentClass {
+ Pane: typeof TabPane;
+}
+
+declare const Tab: TabComponent;
+
+export default Tab;
diff --git a/src/modules/Tab/Tab.js b/src/modules/Tab/Tab.js
new file mode 100644
index 0000000000..92e1775230
--- /dev/null
+++ b/src/modules/Tab/Tab.js
@@ -0,0 +1,117 @@
+import _ from 'lodash'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import {
+ AutoControlledComponent as Component,
+ customPropTypes,
+ getElementType,
+ getUnhandledProps,
+ META,
+} from '../../lib'
+import Menu from '../../collections/Menu/Menu'
+import TabPane from './TabPane'
+
+/**
+ * A Tab is a hidden section of content activated by a Menu.
+ * @see Menu
+ * @see Segment
+ */
+class Tab extends Component {
+ static propTypes = {
+ /** An element type to render as (string or function). */
+ as: customPropTypes.as,
+
+ /** The initial activeIndex. */
+ defaultActiveIndex: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ ]),
+
+ /** Index of the currently active tab. */
+ activeIndex: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ ]),
+
+ /** Shorthand props for the Menu. */
+ menu: PropTypes.object,
+
+ /**
+ * Called on tab change.
+ *
+ * @param {SyntheticEvent} event - React's original SyntheticEvent.
+ * @param {object} data - All props and proposed new activeIndex.
+ * @param {object} data.activeIndex - The new proposed activeIndex.
+ */
+ onTabChange: PropTypes.func,
+
+ /**
+ * Array of objects describing each Menu.Item and Tab.Pane:
+ * {
+ * menuItem: 'Home',
+ * render: () => Welcome!
+ * }
+ */
+ panes: PropTypes.arrayOf(PropTypes.shape({
+ menuItem: PropTypes.string.isRequired,
+ render: PropTypes.func.isRequired,
+ })),
+ }
+
+ static autoControlledProps = [
+ 'activeIndex',
+ ]
+
+ static defaultProps = {
+ menu: { attached: true, tabular: true },
+ }
+
+ static _meta = {
+ name: 'Tab',
+ type: META.TYPES.MODULE,
+ }
+
+ static Pane = TabPane
+
+ state = {
+ activeIndex: 0,
+ }
+
+ handleItemClick = (e, { index }) => {
+ _.invoke(this.props, 'onTabChange', e, { activeIndex: index, ...this.props })
+ this.trySetState({ activeIndex: index })
+ }
+
+ renderMenu() {
+ const { menu, panes } = this.props
+ const { activeIndex } = this.state
+
+ return Menu.create(menu, {
+ overrideProps: {
+ items: _.map(panes, 'menuItem'),
+ onItemClick: this.handleItemClick,
+ activeIndex,
+ },
+ })
+ }
+
+ render() {
+ const { panes } = this.props
+ const { activeIndex } = this.state
+
+ const menu = this.renderMenu()
+ const rest = getUnhandledProps(Tab, this.props)
+ const ElementType = getElementType(Tab, this.props)
+
+ return (
+
+ {menu.props.attached !== 'bottom' && menu}
+ {_.invoke(_.get(panes, `[${activeIndex}]`), 'render', this.props)}
+ {menu.props.attached === 'bottom' && menu}
+
+ )
+ }
+}
+
+export default Tab
diff --git a/src/modules/Tab/TabPane.d.ts b/src/modules/Tab/TabPane.d.ts
new file mode 100644
index 0000000000..7a2df32a15
--- /dev/null
+++ b/src/modules/Tab/TabPane.d.ts
@@ -0,0 +1,21 @@
+import * as React from 'react';
+
+export interface TabPaneProps {
+ [key: string]: any;
+
+ /** An element type to render as (string or function). */
+ as?: any;
+
+ /** Primary content. */
+ children?: React.ReactNode;
+
+ /** Additional classes. */
+ className?: string;
+
+ /** A Tab.Pane can display a loading indicator. */
+ loading?: boolean;
+}
+
+declare const TabPane: React.StatelessComponent;
+
+export default TabPane;
diff --git a/src/modules/Tab/TabPane.js b/src/modules/Tab/TabPane.js
new file mode 100644
index 0000000000..a5851289ed
--- /dev/null
+++ b/src/modules/Tab/TabPane.js
@@ -0,0 +1,67 @@
+import cx from 'classnames'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import {
+ createShorthandFactory,
+ customPropTypes,
+ getElementType,
+ getUnhandledProps,
+ META,
+ useKeyOnly,
+} from '../../lib'
+import Segment from '../../elements/Segment/Segment'
+
+/**
+ * A tab pane holds the content of a tab.
+ */
+function TabPane(props) {
+ const { children, className, loading } = props
+
+ const classes = cx(
+ useKeyOnly(loading, 'loading'),
+ 'active tab',
+ className
+ )
+ const rest = getUnhandledProps(TabPane, props)
+ const ElementType = getElementType(TabPane, props)
+
+ const calculatedDefaultProps = {}
+ if (ElementType === Segment) {
+ calculatedDefaultProps.attached = 'bottom'
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+TabPane._meta = {
+ name: 'TabPane',
+ parent: 'Tab',
+ type: META.TYPES.MODULE,
+}
+
+TabPane.defaultProps = {
+ as: Segment,
+}
+
+TabPane.propTypes = {
+ /** An element type to render as (string or function). */
+ as: customPropTypes.as,
+
+ /** Primary content. */
+ children: PropTypes.string,
+
+ /** Additional classes. */
+ className: PropTypes.string,
+
+ /** A Tab.Pane can display a loading indicator. */
+ loading: PropTypes.bool,
+}
+
+TabPane.create = createShorthandFactory(TabPane, children => ({ children }))
+
+export default TabPane
diff --git a/src/modules/Tab/index.d.ts b/src/modules/Tab/index.d.ts
new file mode 100644
index 0000000000..da5f1734f3
--- /dev/null
+++ b/src/modules/Tab/index.d.ts
@@ -0,0 +1 @@
+export { default, TabProps } from './Tab';
diff --git a/src/modules/Tab/index.js b/src/modules/Tab/index.js
new file mode 100644
index 0000000000..18adc78f41
--- /dev/null
+++ b/src/modules/Tab/index.js
@@ -0,0 +1 @@
+export default from './Tab'
diff --git a/test/specs/collections/Menu/Menu-test.js b/test/specs/collections/Menu/Menu-test.js
index 45cfaa7de2..f4c329e0b1 100644
--- a/test/specs/collections/Menu/Menu-test.js
+++ b/test/specs/collections/Menu/Menu-test.js
@@ -71,6 +71,13 @@ describe('Menu', () => {
.at(1)
.should.have.prop('active', true)
})
+
+ it('works as a string', () => {
+ mount()
+ .find('MenuItem')
+ .at(1)
+ .should.have.prop('active', true)
+ })
})
describe('items', () => {
diff --git a/test/specs/modules/Tab/Tab-test.js b/test/specs/modules/Tab/Tab-test.js
new file mode 100644
index 0000000000..cd9b6031b6
--- /dev/null
+++ b/test/specs/modules/Tab/Tab-test.js
@@ -0,0 +1,127 @@
+import React from 'react'
+
+import Tab from 'src/modules/Tab/Tab'
+import TabPane from 'src/modules/Tab/TabPane'
+import * as common from 'test/specs/commonTests'
+import { sandbox } from 'test/utils'
+
+describe('Tab', () => {
+ common.isConformant(Tab)
+ common.hasSubComponents(Tab, [TabPane])
+
+ const panes = [
+ { menuItem: 'Tab 1', render: () => Tab 1 Content },
+ { menuItem: 'Tab 2', render: () => Tab 2 Content },
+ { menuItem: 'Tab 3', render: () => Tab 3 Content },
+ ]
+
+ describe('menu', () => {
+ it('defaults to an attached tabular menu', () => {
+ Tab.defaultProps
+ .should.have.property('menu')
+ .which.deep.equals({ attached: true, tabular: true })
+ })
+
+ it('passes the props to the Menu', () => {
+ shallow()
+ .find('Menu')
+ .should.have.props({ 'data-foo': 'bar' })
+ })
+
+ it('has an item for every menuItem in panes', () => {
+ const items = shallow()
+ .find('Menu')
+ .shallow()
+ .find('MenuItem')
+
+ items.should.have.lengthOf(3)
+ items.at(0).shallow().should.contain.text('Tab 1')
+ items.at(1).shallow().should.contain.text('Tab 2')
+ items.at(2).shallow().should.contain.text('Tab 3')
+ })
+
+ it('renders above the pane by default', () => {
+ const wrapper = shallow()
+
+ wrapper.childAt(0).should.match('Menu')
+ wrapper.childAt(1).shallow().should.match('Segment')
+ })
+
+ it("renders below the pane when attached='bottom'", () => {
+ const wrapper = shallow()
+
+ wrapper.childAt(0).shallow().should.match('Segment')
+ wrapper.childAt(1).should.match('Menu')
+ })
+ })
+
+ describe('activeIndex', () => {
+ it('is passed to the Menu', () => {
+ const wrapper = mount()
+
+ wrapper
+ .find('Menu')
+ .should.have.prop('activeIndex', 123)
+ })
+
+ it('is set when clicking an item', () => {
+ const wrapper = mount()
+
+ wrapper
+ .find('.active.tab')
+ .should.contain.text('Tab 1 Content')
+
+ wrapper
+ .find('MenuItem')
+ .at(1)
+ .simulate('click')
+
+ wrapper
+ .find('.active.tab')
+ .should.contain.text('Tab 2 Content')
+ })
+
+ it('can be set via props', () => {
+ const wrapper = mount()
+
+ wrapper
+ .find('.active.tab')
+ .should.contain.text('Tab 2 Content')
+
+ wrapper
+ .setProps({ activeIndex: 2 })
+ .find('.active.tab')
+ .should.contain.text('Tab 3 Content')
+ })
+
+ it('determines which pane render method is called', () => {
+ const activeIndex = 1
+ const props = { activeIndex, panes }
+ sandbox.spy(panes[activeIndex], 'render')
+
+ shallow()
+
+ panes[activeIndex].render.should.have.been.calledOnce()
+ panes[activeIndex].render.should.have.been.calledWithMatch(props)
+ })
+ })
+
+ describe('onTabChange', () => {
+ it('is called with (e, { activeIndex, ...props }) a menu item is clicked', () => {
+ const activeIndex = 1
+ const spy = sandbox.spy()
+ const event = { fake: 'event' }
+ const props = { onTabChange: spy, panes }
+
+ mount()
+ .find('MenuItem')
+ .at(activeIndex)
+ .simulate('click', event)
+
+ // Since React will have generated a key the returned tab won't match
+ // exactly so match on the props instead.
+ spy.should.have.been.calledOnce()
+ spy.should.have.been.calledWithMatch(event, { activeIndex, ...props })
+ })
+ })
+})
diff --git a/test/specs/modules/Tab/TabPane-test.js b/test/specs/modules/Tab/TabPane-test.js
new file mode 100644
index 0000000000..dc78b0b54f
--- /dev/null
+++ b/test/specs/modules/Tab/TabPane-test.js
@@ -0,0 +1,12 @@
+import React from 'react'
+import TabPane from 'src/modules/Tab/TabPane'
+import * as common from 'test/specs/commonTests'
+
+describe('TabPane', () => {
+ common.isConformant(TabPane)
+ common.propKeyOnlyToClassName(TabPane, 'loading')
+
+ it('renders a Segment by default', () => {
+ shallow().should.match('Segment')
+ })
+})