Skip to content

Commit

Permalink
fix(TabBarScroller): Support for dynamic tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
James Friedman committed Jan 14, 2018
1 parent efdc130 commit 1ed982e
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 37 deletions.
23 changes: 20 additions & 3 deletions src/Base/withMDC.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ type WithMDCOpts = {
getMdcConstructorOrInstance?: (props: Object, context: Object) => ?Function,
/** MDC events mapped from eventName => handler */
mdcEvents?: Object,
/** Decides wether or not to pass down an elementRef */
/** Decides wether or not to pass down an elementRef prop */
mdcElementRef?: boolean,
/** Decides wether or not to pass down an mdcComponentReinit prop */
mdcComponentReinit?: boolean,
/** Decides wether or not to pass down an apiRef */
mdcApiRef?: boolean,
/** component on mount */
Expand Down Expand Up @@ -54,6 +56,7 @@ export const withMDC = ({
mdcEvents = {},
mdcElementRef = false,
mdcApiRef = false,
mdcComponentReinit = false,
defaultProps = {},
onMount = noop,
onUpdate = noop,
Expand All @@ -77,6 +80,8 @@ export const withMDC = ({
mdcElementRef: this.mdcSetRootElement
} :
{};

this.mdcComponentReinit = this.mdcComponentReinit.bind(this);
}

componentDidMount(): void {
Expand Down Expand Up @@ -114,7 +119,7 @@ export const withMDC = ({
if (isInstance) {
this.mdcApi = MDCConstructorToUse;
this.props.apiRef(this.mdcApi);
} else if (MDCConstructorToUse && !this.props.cssOnly) {
} else if (MDCConstructorToUse && (this.props && !this.props.cssOnly)) {
const el = this.mdcGetRootElement();
try {
this.mdcApi = new MDCConstructorToUse(el);
Expand Down Expand Up @@ -186,7 +191,19 @@ export const withMDC = ({
mdcApiRef: this.mdcApi
} :
{};
return <Component {...apiRefProps} {...this.elementRefProps} {...rest} />;

const mdcComponentReinitProp = mdcComponentReinit ?
{ mdcComponentReinit: this.mdcComponentReinit } :
{};

return (
<Component
{...mdcComponentReinitProp}
{...apiRefProps}
{...this.elementRefProps}
{...rest}
/>
);
}
};
};
65 changes: 50 additions & 15 deletions src/Tabs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ export const TabBar: React.ComponentType<TabBarPropsT> = withMDC({
props.onChange(evt);
}
},
contextTypes: { isTabScroller: PropTypes.bool, tabBarApi: PropTypes.any },
contextTypes: {
isTabScroller: PropTypes.bool,
tabBarApi: PropTypes.any,
reinitTabBarScroller: PropTypes.func
},
defaultProps: {
onChange: noop,
activeTabIndex: 0
Expand All @@ -125,15 +129,17 @@ export const TabBar: React.ComponentType<TabBarPropsT> = withMDC({
}
},
didUpdate: (props, nextProps, api, inst) => {
if (!api) {
if (inst.context.tabBarApi && api !== inst.context.tabBarApi) {
// if we dont have an api, it might be being held hostage by the TabBar scroller
// Grab it if its available from the context and re-init the component
if (inst.context.tabBarApi) {
inst.mdcComponentReinit();
}
inst.mdcComponentDestroy();
inst.mdcComponentReinit();
return;
}

// exit if we have no api
if (!api) return;

const childrenDidChange =
props &&
props.children &&
Expand All @@ -142,17 +148,35 @@ export const TabBar: React.ComponentType<TabBarPropsT> = withMDC({
JSON.stringify(props.children.map(({ key }) => key)) !==
JSON.stringify(nextProps.children.map(({ key }) => key));

// destroy the foundation for all tabs manually to remove all listeners
if (childrenDidChange && api.tabs) {
api.tabs.forEach(mdcTab => {
mdcTab.foundation_ && mdcTab.foundation_.destroy();
});
const tabsLengthMismatch =
React.Children.toArray(nextProps.children).filter(
c => c.type.displayName === 'Tab'
).length !== api.tabs.length;

inst.mdcComponentReinit();
// Children changing is a pain...
// We have to perform a lot of cleanup and sometimes we have to reinit
// A parent tab bar scroller
if (childrenDidChange || tabsLengthMismatch) {
// destroy the foundation for all tabs manually to remove all listeners
if (api.tabs) {
api.tabs.forEach(mdcTab => {
mdcTab.foundation_ && mdcTab.foundation_.destroy();
});
}
// when tab scroller is wrapping the component
if (inst.context.isTabScroller) {
// destroy the foundation
inst.mdcComponentDestroy();
// trigger reinit on the scroller container
inst.context.reinitTabBarScroller();
} else {
// reinit
inst.mdcComponentReinit();
}
}
}
})(
class extends React.PureComponent<TabBarPropsT> {
class extends React.Component<TabBarPropsT> {
static displayName = 'TabBar';

static contextTypes = {
Expand Down Expand Up @@ -185,7 +209,8 @@ type TabBarScrollerStateT = {
/** The TabBar Scroll container */
export const TabBarScroller = withMDC({
mdcConstructor: MDCTabBarScroller,
mdcApiRef: true
mdcApiRef: true,
mdcComponentReinit: true
})(
class extends React.Component<TabBarScrollerPropsT, TabBarScrollerStateT> {
static displayName = 'TabBarScroller';
Expand Down Expand Up @@ -220,7 +245,11 @@ export const TabBarScroller = withMDC({
* We have to jump through some context hoops to get the tabBar api instance back to the TabBar
*/
getChildContext() {
return { isTabScroller: true, tabBarApi: this.state.tabBarApi };
return {
isTabScroller: true,
tabBarApi: this.state.tabBarApi,
reinitTabBarScroller: () => this.reinitTabBarScroller()
};
}

componentWillReceiveProps(props) {
Expand All @@ -240,12 +269,18 @@ export const TabBarScroller = withMDC({

static childContextTypes = {
isTabScroller: PropTypes.bool,
tabBarApi: PropTypes.any
tabBarApi: PropTypes.any,
reinitTabBarScroller: PropTypes.func
};

reinitTabBarScroller() {
this.props.mdcComponentReinit();
}

render() {
const {
children,
mdcComponentReinit,
indicatorForward,
indicatorBack,
mdcApiRef,
Expand Down
67 changes: 48 additions & 19 deletions src/Tabs/tabs.story.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,73 @@ import React from 'react';

import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { number, text } from '@storybook/addon-knobs';
import { number } from '@storybook/addon-knobs';
import { Tab, TabBar, TabBarScroller } from './';
import { Button } from '../Button';
import { Checkbox } from '../Checkbox';

class TabBarStory extends React.Component {
state = {
withScroller: false,
count: 0,
activeTabIndex: 0,
tabs: ['Cookies', 'Pizza', 'Icecream']
};

onToggleWithScroller(evt) {
this.setState({ withScroller: !this.state.withScroller });
action('withScroller: ' + !this.state.withScroller)();
}

onChange(evt) {
this.setState({ activeTabIndex: evt.target.value });
action('activeTabIndex: ' + evt.target.value)();
}
onChangeTabNames(evt) {
this.setState({
const state = {
tabs: this.state.tabs.map((label, index) => index),
count: this.state.count + 1
});
};
this.setState(state);
action('onChangeTabNames: ' + JSON.stringify(state))();
}
onAddTab(evt) {
this.setState({
const state = {
tabs: [...this.state.tabs, `Dynamic Tab #${this.state.tabs.length + 1}`]
});
};

this.setState(state);

action('onAddTab: ' + JSON.stringify(state))();
}
onRemoveLastTab(evt) {
const init = this.state.tabs.slice(0, -1);
this.setState({
const state = {
activeTabIndex: 0,
tabs: init
});
};

this.setState(state);
action('onRemoveLastTab: ' + JSON.stringify(state))();
}

render() {
const tabBar = (
<TabBar
count={this.state.count}
activeTabIndex={number('activeTabIndex', this.state.activeTabIndex)}
onChange={evt => this.onChange(evt)}
>
{this.state.tabs.map(label => <Tab key={label}>{label}</Tab>)}
</TabBar>
);

const finalComponent = this.state.withScroller ? (
<TabBarScroller>{tabBar}</TabBarScroller>
) : (
tabBar
);

return (
<div>
<div>
Expand All @@ -52,15 +84,15 @@ class TabBarStory extends React.Component {
onClick={evt => this.onRemoveLastTab(evt)}
>
Remove Last Tab
</Button>
</Button>{' '}
<Checkbox
checked={this.state.withScroller}
onChange={evt => this.onToggleWithScroller(evt)}
>
with TabBarScroller
</Checkbox>
</div>
<TabBar
count={this.state.count}
activeTabIndex={number('activeTabIndex', this.state.activeTabIndex)}
onChange={evt => this.onChange(evt)}
>
{this.state.tabs.map(label => <Tab key={label}>{label}</Tab>)}
</TabBar>
{finalComponent}
</div>
);
}
Expand All @@ -78,10 +110,7 @@ class TabBarScrollerStory extends React.Component {

render() {
return (
<TabBarScroller
indicatorForward={text('indicatorForward', undefined)}
indicatorBack={text('indicatorBack', undefined)}
>
<TabBarScroller>
<TabBar
activeTabIndex={number('activeTabIndex', this.state.activeTabIndex)}
onChange={evt => this.onChange(evt)}
Expand Down

0 comments on commit 1ed982e

Please sign in to comment.