Skip to content
This repository was archived by the owner on Jul 29, 2025. It is now read-only.

Commit 731e980

Browse files
authored
feat(tab-bar): Add component (#229)
1 parent 3eb8a4b commit 731e980

File tree

17 files changed

+770
-109
lines changed

17 files changed

+770
-109
lines changed

package-lock.json

Lines changed: 84 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@
5858
"@material/select": "^0.39.0",
5959
"@material/switch": "^0.39.0",
6060
"@material/tab": "^0.39.0",
61+
"@material/tab-bar": "^0.39.0",
6162
"@material/tab-indicator": "^0.39.0",
63+
"@material/tab-scroller": "^0.39.0",
6264
"@material/textfield": "^0.39.0",
6365
"@material/top-app-bar": "^0.39.0",
6466
"@material/typography": "^0.39.0",

packages/tab-bar/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
index.js

packages/tab-bar/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# React Tab Bar
2+
3+
A React version of an [MDC Tab Bar](https://github.com/material-components/material-components-web/tree/master/packages/mdc-tab-bar).
4+
5+
## Installation
6+
7+
```
8+
npm install @material/react-tab-bar
9+
```
10+
11+
## Usage
12+
13+
### Styles
14+
15+
with Sass:
16+
```scss
17+
import '@material/react-tab-bar/index.scss';
18+
```
19+
20+
with CSS:
21+
```css
22+
import '@material/react-tab-bar/dist/tab-bar.css';
23+
```
24+
25+
### Javascript Instantiation
26+
27+
```js
28+
import React from 'react';
29+
import Tab from '@material/react-tab';
30+
import TabBar from '@material/react-tab-bar';
31+
32+
class MyApp extends React.Component {
33+
state = {activeIndex: 0};
34+
35+
render() {
36+
return (
37+
<div>
38+
<TabBar
39+
activeIndex={this.state.activeIndex}
40+
handleActiveIndexUpdate={(activeIndex) => this.setState({activeIndex})}
41+
>
42+
<Tab>
43+
<span className='mdc-tab__text-label'>One</span>
44+
</Tab>
45+
...
46+
</TabBar>
47+
</div>
48+
);
49+
}
50+
}
51+
```
52+
53+
> _NOTE_: You can also use a custom tab component with the `TabBar`, but it must implement the methods `activate`, `deactivate`, `focus`, `computeIndicatorClientRect`, and `computeDimensions`. See [`MDCTab` documentation](https://github.com/material-components/material-components-web/blob/master/packages/mdc-tab/README.md#mdctab-properties-and-methods) for more details.
54+
55+
## Props
56+
57+
Prop Name | Type | Description
58+
--- | --- | ---
59+
activeIndex | number | Index of the active tab.
60+
indexInView | number | Index of the tab to be scrolled into view.
61+
handleActiveIndexUpdate | Function(activeIndex: number) => void | Callback after the active index is updated.
62+
className | string | Classes to appear on className attribute of root element.
63+
isRtl | Boolean | Whether the direction of the tab bar is RTL.
64+
65+
## Sass Mixins
66+
67+
Sass mixins may be available to customize various aspects of the components. Please refer to the
68+
MDC Web repository for more information on what mixins are available, and how to use them.
69+
70+
[Advanced Sass Mixins](https://github.com/material-components/material-components-web/blob/master/packages/mdc-tab-bar/README.md#sass-mixins)

packages/tab-bar/index.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React, {Component} from 'react';
2+
import PropTypes from 'prop-types';
3+
import classnames from 'classnames';
4+
5+
import TabScroller from '@material/react-tab-scroller';
6+
import {MDCTabBarFoundation} from '@material/tab-bar/dist/mdc.tabBar';
7+
8+
export default class TabBar extends Component {
9+
tabBarElement_ = React.createRef();
10+
tabScroller_ = React.createRef();
11+
tabList_ = [];
12+
13+
foundation_ = null;
14+
state = {
15+
previousActiveIndex: -1,
16+
}
17+
18+
componentDidMount() {
19+
this.foundation_ = new MDCTabBarFoundation(this.adapter);
20+
this.foundation_.init();
21+
22+
const {
23+
activeIndex,
24+
indexInView,
25+
} = this.props;
26+
if (this.tabList_[activeIndex]) {
27+
this.tabList_[activeIndex].activate({} /* previousIndicatorClientRect */);
28+
}
29+
this.foundation_.scrollIntoView(indexInView);
30+
}
31+
32+
componentDidUpdate(prevProps) {
33+
if (this.props.activeIndex !== prevProps.activeIndex) {
34+
this.foundation_.activateTab(this.props.activeIndex);
35+
}
36+
if (this.props.indexInView !== prevProps.indexInView) {
37+
this.foundation_.scrollIntoView(this.props.indexInView);
38+
}
39+
}
40+
41+
componentWillUnmount() {
42+
this.foundation_.destroy();
43+
}
44+
45+
get classes() {
46+
return classnames('mdc-tab-bar', this.props.className);
47+
}
48+
49+
get adapter() {
50+
return {
51+
scrollTo: (scrollX) => this.tabScroller_.current.scrollTo(scrollX),
52+
incrementScroll: (scrollXIncrement) => this.tabScroller_.current.incrementScroll(scrollXIncrement),
53+
getScrollPosition: () => this.tabScroller_.current.getScrollPosition(),
54+
getScrollContentWidth: () => this.tabScroller_.current.getScrollContentWidth(),
55+
getOffsetWidth: () => this.tabBarElement_.current.offsetWidth,
56+
isRTL: () => !!this.props.isRtl,
57+
setActiveTab: (index) => this.props.handleActiveIndexUpdate(index),
58+
activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect),
59+
deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(),
60+
focusTabAtIndex: (index) => this.tabList_[index].focus(),
61+
getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(),
62+
getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(),
63+
getPreviousActiveTabIndex: () => this.state.previousActiveIndex,
64+
getFocusedTabIndex: () => {
65+
const activeElement = document.activeElement;
66+
this.tabList_.forEach((tabList, index) => {
67+
if (tabList.tabElement_.current === activeElement) {
68+
return index;
69+
}
70+
});
71+
},
72+
getIndexOfTab: (tabToFind) => this.tabList_.indexOf(tabToFind),
73+
getTabListLength: () => this.tabList_.length,
74+
};
75+
}
76+
77+
pushToTabList = (el) => {
78+
this.tabList_.push(el);
79+
}
80+
81+
onKeyDown = (e) => {
82+
// Persist the synthetic event to access its `key`.
83+
e.persist();
84+
this.setState(
85+
{previousActiveIndex: this.props.activeIndex},
86+
() => this.foundation_.handleKeyDown(e));
87+
this.props.onKeyDown(e);
88+
}
89+
90+
render() {
91+
const {
92+
/* eslint-disable no-unused-vars */
93+
className,
94+
indexInView,
95+
activeIndex,
96+
handleActiveIndexUpdate,
97+
onKeyDown,
98+
/* eslint-enable no-unused-vars */
99+
isRtl,
100+
children,
101+
...otherProps
102+
} = this.props;
103+
104+
return (
105+
<div
106+
dir={isRtl ? 'rtl' : 'ltr'}
107+
className={this.classes}
108+
role='tablist'
109+
onKeyDown={this.onKeyDown}
110+
ref={this.tabBarElement_}
111+
{...otherProps}
112+
>
113+
<TabScroller ref={this.tabScroller_}>
114+
{React.Children.map(children, this.renderTab)}
115+
</TabScroller>
116+
</div>
117+
);
118+
}
119+
120+
renderTab = (tab, index) => {
121+
const {
122+
children,
123+
onClick, // eslint-disable-line no-unused-vars
124+
...otherProps
125+
} = tab.props;
126+
127+
const props = {
128+
onClick: (e) => {
129+
this.setState(
130+
{previousActiveIndex: this.props.activeIndex},
131+
() => this.adapter.setActiveTab(index));
132+
this.props.onClick(e);
133+
},
134+
ref: this.pushToTabList,
135+
...otherProps,
136+
};
137+
138+
return React.cloneElement(tab, props, children);
139+
}
140+
}
141+
142+
TabBar.propTypes = {
143+
indexInView: PropTypes.number,
144+
activeIndex: PropTypes.number,
145+
handleActiveIndexUpdate: PropTypes.func,
146+
className: PropTypes.string,
147+
children: PropTypes.oneOfType([
148+
PropTypes.arrayOf(PropTypes.element),
149+
PropTypes.element,
150+
]),
151+
onClick: PropTypes.func,
152+
onKeyDown: PropTypes.func,
153+
isRtl: PropTypes.bool,
154+
};
155+
156+
TabBar.defaultProps = {
157+
indexInView: 0,
158+
activeIndex: 0,
159+
handleActiveIndexUpdate: () => {},
160+
className: '',
161+
children: [],
162+
onClick: () => {},
163+
onKeyDown: () => {},
164+
isRtl: false,
165+
};

packages/tab-bar/index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "@material/tab-bar/mdc-tab-bar";

packages/tab-bar/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@material/react-tab-bar",
3+
"version": "0.0.0",
4+
"description": "Material Components React Tab Bar",
5+
"license": "Apache-2.0",
6+
"main": "dist/index.js",
7+
"keywords": [
8+
"mdc web react",
9+
"material components react",
10+
"material design",
11+
"tab bar",
12+
"tabbar",
13+
"tabs"
14+
],
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/material-components/material-components-web-react.git"
18+
},
19+
"dependencies": {
20+
"@material/react-tab-scroller": "^0.0.0",
21+
"@material/tab-bar": "^0.39.0",
22+
"classnames": "^2.2.5",
23+
"prop-types": "^15.6.1",
24+
"react": "^16.3.2"
25+
},
26+
"publishConfig": {
27+
"access": "public"
28+
}
29+
}

0 commit comments

Comments
 (0)