diff --git a/docs/src/app/components/pages/components/tabs.jsx b/docs/src/app/components/pages/components/tabs.jsx index d3ca5d686edf85..83f77b63169089 100644 --- a/docs/src/app/components/pages/components/tabs.jsx +++ b/docs/src/app/components/pages/components/tabs.jsx @@ -1,76 +1,69 @@ let React = require('react'); let CodeExample = require('../../code-example/code-example'); -let { Slider, Styles, Tab, Tabs } = require('material-ui'); +let {IconButton, Slider, Styles, Tab, Tabs } = require('material-ui'); let ComponentDoc = require('../../component-doc'); - -let { Typography } = Styles; +let { Colors, Typography } = Styles; class TabsPage extends React.Component { constructor() { super(); - this._onActive = this._onActive.bind(this); - } - - getStyles() { - return { - headline: { - fontSize: '24px', - lineHeight: '32px', - paddingTop: '16px', - marginBottom: '12px', - letterSpacing: '0', - fontWeight: Typography.fontWeightNormal, - color: Typography.textDarkBlack - } - } + this._handleTabActive = this._handleTabActive.bind(this); + this.state = {tabsValue: 'a'}; } render(){ - let code = ' \n' + - ' \n' + - '
\n' + - '

Tab One Template Example

\n' + - '

\n' + - ' This is an example of a tab template! \n' + - '

\n' + - '

\n' + - ' You can put any sort of HTML or react component in here. \n' + - '

\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '

Tab Two Template Example

\n' + - '

\n' + - ' This is another example of a tab template! \n' + - '

\n' + - '

\n' + - ' Fair warning - the next tab routes to home! \n' + - '

\n' + - '
\n' + - '
\n' + - ' \n' + - '
\n' + - '\n' + - '_onActive(tab){ \n' + - ' this.context.router.transitionTo(tab.props.route); \n' + - '}'; - - let desc = 'Refs cannot be set on individual Tab components as cloneWithProps is being ' + - 'used to extend the individual tab components under the hood. However, ' + - 'refs can be passed to the Tabs container and to any element or component within the template. ' + - 'If you need to access a tab directly - you can do so with the first argument of onActive or ' + - 'by accessing the props.children array by passing refs to the Tabs container.'; + let code = ` + //Uncontrolled Tabs + + + (Tab content...) + + + (Tab content...) + + + + + + home + + + //Controlled Tabs using valueLink (passing value and onChange props works too!) + + + (Tab content...) + + + (Tab content...) + + + + `; + + let desc = 'Tabs can now operate in two different modes: controlled and uncontrolled. ' + + 'The uncontrolled mode takes over automatically if no value prop is passed to your' + + 'Tabs and Tab components. If you want controllable tabs, passing a value to both the' + + ' Tabs and Tab elements will let you programmatically adjust which one is selected. ' + + 'ValueLink is now supported by Tabs.'; let componentInfo = [ { name: 'Tabs Props', infoArray: [ + { + name: 'contentContainerStyle', + type: 'object', + header: 'optional', + desc: 'Override the inline-styles of the content\'s container.' + }, { name: 'initialSelectedIndex', type: 'number', @@ -96,17 +89,11 @@ class TabsPage extends React.Component { desc: 'Override the inline-styles of the tab-labels container.' }, { - name: 'contentContainerStyle', - type: 'object', + name: 'value', + type: 'string or number', header: 'optional', - desc: 'Override the inline-styles of the content\'s container.' + desc: 'Makes Tabs controllable and selects the tab whose value prop matches this prop.' }, - { - name: 'tabWidth', - type: 'number', - header: 'optional', - desc: 'Specifiy tabWidth to set each tab to a set number of pixels. Tab Width is set by default to an even distribution of the parent Tabs container. If tabWidth is set but the total width of all tabs is greater than the container, tabWidth will revert back to default' - } ] }, { @@ -114,9 +101,9 @@ class TabsPage extends React.Component { infoArray: [ { name: 'onChange', - type: 'function(tabIndex, tab)', + type: 'function(e, tab)', header: 'optional', - desc: 'Fired on touch or tap of a tab.' + desc: 'Fired on touch or tap of a tab. Passes the index and the tab element.' } ] }, @@ -130,10 +117,11 @@ class TabsPage extends React.Component { desc: 'Sets the text value of the tab item to the string specified.' }, { - name: 'route', + name: 'value', type: 'string', header: 'optional', - desc: 'Specifies a router RouteName if included.' + desc: 'If value prop passed to Tabs component, this value prop is also required. It assigns a value ' + + 'to the tab so that it can be selected by the Tabs.' } ] }, @@ -150,18 +138,57 @@ class TabsPage extends React.Component { } ]; + let padding = 400; + + let styles = { + contentContainerStyle: { + marginLeft: -padding, + }, + div: { + position: 'absolute', + left: 48, + backgroundColor: Colors.cyan500, + width: padding, + height: 48, + }, + headline: { + fontSize: 24, + lineHeight: '32px', + paddingTop: 16, + marginBottom: 12, + letterSpacing: 0, + fontWeight: Typography.fontWeightNormal, + color: Typography.textDarkBlack, + }, + iconButton: { + position: 'absolute', + left: 0, + backgroundColor: Colors.cyan500, + color: 'white', + marginRight: padding, + }, + iconStyle: { + color: Colors.white, + }, + tabs: { + position: 'relative', + }, + tabsContainer: { + position: 'relative', + paddingLeft: padding, + }, + }; + return ( - -
- - + +
-

Tab One Template Example

+

Tab One Template Example

This is an example of a tab template!

@@ -171,9 +198,9 @@ class TabsPage extends React.Component {
- +
-

Tab Two Template Example

+

Tab Two Template Example

This is another example of a tab template!

@@ -183,19 +210,65 @@ class TabsPage extends React.Component {
+ label="Item Three" + route="home" + onActive={this._handleTabActive} />
-
+
+ + home + +
+ + +
+

Controllable Tab Examples

+

+ Tabs are also controllable if you want to programmatically pass them their values. + This allows for more functionality in Tabs such as not + having any Tab selected or assigning them different values. +

+

(The home Icon Button will unselect all the tabs and hide their content.)

+
+
+ +
+

Controllable Tab B

+

+ This is another example of a controllable tab. Remember, if you + use controllable Tabs, you need to give all of your tabs values or else + you wont be able to select them. +

+

+ To see one use for controlled Tabs, press the home button on the right. +

+
+
+
+
); } - _onActive(tab){ + _handleButtonClick() { + this.setState({tabsValue: 'c'}); + } + + _handleTabActive(tab){ this.context.router.transitionTo(tab.props.route); } + + _handleTabsChange(e, tab){ + this.setState({tabsValue: tab.props.value}); + } } TabsPage.contextTypes = { diff --git a/src/tabs/tab.jsx b/src/tabs/tab.jsx index 4185492c3efe18..bc5b9c13ca61c6 100644 --- a/src/tabs/tab.jsx +++ b/src/tabs/tab.jsx @@ -13,15 +13,27 @@ let Tab = React.createClass({ propTypes: { handleTouchTap: React.PropTypes.func, + onActive: React.PropTypes.func, selected: React.PropTypes.bool, width: React.PropTypes.string, + value: React.PropTypes.string, }, - handleTouchTap() { - this.props.handleTouchTap(this.props.tabIndex, this); + getDefaultProps(){ + return { + onActive: () => {}, + }; }, render() { + let { + label, + selected, + style, + value, + width, + ...other, + } = this.props; let styles = this.mergeAndPrefix({ display: 'table-cell', cursor: 'pointer', @@ -29,24 +41,30 @@ let Tab = React.createClass({ verticalAlign: 'middle', height: 48, color: Colors.white, - opacity: 0.6, + opacity: selected ? 1 : 0.6, + outline: 'none', fontSize: 14, - fontWeight: '500', + fontWeight: 500, whiteSpace: 'initial', fontFamily: this.context.muiTheme.contentFontFamily, boxSizing: 'border-box', - width: this.props.width, - }, this.props.style); - - if (this.props.selected) styles.opacity = '1'; + width: width, + }, style); return ( -
- {this.props.label} +
+ {label}
); }, + _handleTouchTap(e) { + this.props.handleTouchTap(e, this); + }, + }); module.exports = Tab; diff --git a/src/tabs/tabs.jsx b/src/tabs/tabs.jsx index 399de9549caebc..74bc11a6ea71c8 100644 --- a/src/tabs/tabs.jsx +++ b/src/tabs/tabs.jsx @@ -2,33 +2,40 @@ let React = require('react/addons'); let TabTemplate = require('./tabTemplate'); let InkBar = require('../ink-bar'); let StylePropable = require('../mixins/style-propable'); -let Events = require('../utils/events'); +let Controllable = require('../mixins/controllable'); let Tabs = React.createClass({ - mixins: [StylePropable], + mixins: [StylePropable, Controllable], contextTypes: { muiTheme: React.PropTypes.object, }, propTypes: { - initialSelectedIndex: React.PropTypes.number, - onActive: React.PropTypes.func, - tabWidth: React.PropTypes.number, - tabItemContainerStyle: React.PropTypes.object, contentContainerStyle: React.PropTypes.object, + initialSelectedIndex: React.PropTypes.number, inkBarStyle: React.PropTypes.object, + tabItemContainerStyle: React.PropTypes.object, + }, + + getDefaultProps() { + return { + initialSelectedIndex : 0, + }; }, getInitialState(){ - let selectedIndex = 0; - if (this.props.initialSelectedIndex && this.props.initialSelectedIndex < this.getTabCount()) { - selectedIndex = this.props.initialSelectedIndex; - } + let valueLink = this.getValueLink(this.props); + let initialIndex = this.props.initialSelectedIndex; + return { - selectedIndex: selectedIndex, + selectedIndex: valueLink.value ? + this._getSelectedIndex(this.props) : + initialIndex < this.getTabCount() ? + initialIndex : + 0, }; }, @@ -44,116 +51,136 @@ let Tabs = React.createClass({ return React.Children.count(this.props.children); }, - componentDidMount() { - this._updateTabWidth(); - Events.on(window, 'resize', this._updateTabWidth); - }, - - componentWillUnmount() { - Events.off(window, 'resize', this._updateTabWidth); - }, - componentWillReceiveProps(newProps) { - if (newProps.hasOwnProperty('style')) this._updateTabWidth(); - }, + let valueLink = this.getValueLink(newProps); - handleTouchTap(tabIndex, tab){ - if (this.props.onChange && this.state.selectedIndex !== tabIndex) { - this.props.onChange(tabIndex, tab); + if (valueLink.value){ + this.setState({selectedIndex: this._getSelectedIndex(newProps)}); } - - this.setState({selectedIndex: tabIndex}); - //default CB is _onActive. Can be updated in tab.jsx - if (tab.props.onActive) tab.props.onActive(tab); }, - getStyles() { - let themeVariables = this.context.muiTheme.component.tabs; + render() { + let { + children, + contentContainerStyle, + initialSelectedIndex, + inkBarStyle, + style, + tabWidth, + tabItemContainerStyle, + ...other, + } = this.props; - return { + let themeVariables = this.context.muiTheme.component.tabs; + let styles = { tabItemContainer: { - margin: '0', - padding: '0', + margin: 0, + padding: 0, width: '100%', - height: '48px', + height: 48, backgroundColor: themeVariables.backgroundColor, whiteSpace: 'nowrap', display: 'table', }, }; - }, - - render() { - let styles = this.getStyles(); + let valueLink = this.getValueLink(this.props); + let tabValue = valueLink.value; let tabContent = []; - let width = this.state.fixedWidth ? - 100 / this.getTabCount() +'%' : - this.props.tabWidth + 'px'; + + let width = 100 / this.getTabCount() +'%'; let left = 'calc(' + width + '*' + this.state.selectedIndex + ')'; - let tabs = React.Children.map(this.props.children, (tab, index) => { + let tabs = React.Children.map(children, (tab, index) => { if (tab.type.displayName === "Tab") { - if (tab.props.children) { - tabContent.push(React.createElement(TabTemplate, { - key: index, - selected: this.state.selectedIndex === index, - }, tab.props.children)); - } - else { - tabContent.push(undefined); + if (!tab.props.value && tabValue && process.env.NODE_ENV !== 'production') { + console.error('Tabs value prop has been passed, but Tab ' + index + + ' does not have a value prop. Needs value if Tabs is going' + + ' to be a controlled component.'); } - return React.addons.cloneWithProps(tab, { + tabContent.push(tab.props.children ? + React.createElement(TabTemplate, { + key: index, + selected: this._getSelected(tab, index), + }, tab.props.children) : undefined); + + return React.cloneElement(tab, { key: index, - selected: this.state.selectedIndex === index, + selected: this._getSelected(tab, index), tabIndex: index, width: width, - handleTouchTap: this.handleTouchTap, + handleTouchTap: this._handleTouchTap, }); } else { let type = tab.type.displayName || tab.type; - throw 'Tabs only accepts Tab Components as children. Found ' + - type + ' as child number ' + (index + 1) + ' of Tabs'; + console.error('Tabs only accepts Tab Components as children. Found ' + + type + ' as child number ' + (index + 1) + ' of Tabs'); } }, this); + let inkBar = this.state.selectedIndex !== -1 ? ( + + ) : null; + + let inkBarContainerWidth = tabItemContainerStyle ? + tabItemContainerStyle.width : '100%'; + return ( -
-
+
+
{tabs}
- -
+
+ {inkBar} +
+
{tabContent}
); }, - _tabWidthPropIsValid() { - return this.props.tabWidth && - (this.props.tabWidth * this.getTabCount() <= this.getEvenWidth()); + _getSelectedIndex(props) { + let valueLink = this.getValueLink(props); + let selectedIndex = -1; + + React.Children.forEach(props.children, (tab, index) => { + if (valueLink.value === tab.props.value) { + selectedIndex = index; + } + }); + + return selectedIndex; }, - // Validates that the tabWidth can fit all tabs on the tab bar. If not, the - // tabWidth is recalculated and fixed. - _updateTabWidth() { - if (this._tabWidthPropIsValid()) { - this.setState({ - fixedWidth: false, - }); - } - else { - this.setState({ - fixedWidth: true, - }); + _handleTouchTap(e, tab){ + let valueLink = this.getValueLink(this.props); + let tabIndex = tab.props.tabIndex; + let value = tab.props.value; + + if ((valueLink.value && valueLink.value !== value) || + this.state.selectedIndex !== tabIndex) { + valueLink.requestChange(e, tab); } + + this.setState({selectedIndex: tabIndex}); + tab.props.onActive(tab); + }, + + _getSelected(tab, index) { + let valueLink = this.getValueLink(this.props); + return valueLink.value ? valueLink.value === tab.props.value : + this.state.selectedIndex === index; }, }); module.exports = Tabs; -