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') + }) +})