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

Commit fedf138

Browse files
author
Matt Goo
authored
feat(tab-scroller): add new component (#222)
1 parent 6e7fd40 commit fedf138

File tree

18 files changed

+752
-9
lines changed

18 files changed

+752
-9
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ matrix:
3737
- docker pull mdcreact/screenshots
3838
- docker image ls
3939
script:
40-
- docker run -it --rm --cap-add=SYS_ADMIN -e MDC_GCLOUD_SERVICE_ACCOUNT_KEY="${MDC_GCLOUD_SERVICE_ACCOUNT_KEY}" mdcreact/screenshots /bin/sh -c "git fetch; git checkout \"${CURRENT_BRANCH}\"; git pull; npm i; /home/pptruser/material-components-web-react/test/screenshot/start.sh; sleep 290s; npm run test:image-diff"
40+
- docker run -it --rm --cap-add=SYS_ADMIN -e MDC_GCLOUD_SERVICE_ACCOUNT_KEY="${MDC_GCLOUD_SERVICE_ACCOUNT_KEY}" mdcreact/screenshots /bin/sh -c "git fetch; git checkout \"${CURRENT_BRANCH}\"; git pull; npm i; /home/pptruser/material-components-web-react/test/screenshot/start.sh; sleep 300s; npm run test:image-diff"

package-lock.json

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

packages/tab-scroller/.npmignore

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

packages/tab-scroller/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# React Tab Scroller
2+
3+
A React version of an [MDC Tab Scroller](https://github.com/material-components/material-components-web/tree/master/packages/mdc-tab-scroller).
4+
5+
## Installation
6+
7+
```
8+
npm install @material/react-tab-scroller
9+
```
10+
11+
## Usage
12+
13+
### Styles
14+
15+
with Sass:
16+
```js
17+
import '@material/react-tab-scroller/index.scss';
18+
```
19+
20+
with CSS:
21+
```js
22+
import '@material/react-tab-scroller/dist/tab-scroller.css';
23+
```
24+
25+
### Javascript Instantiation
26+
27+
```js
28+
import React from 'react';
29+
import TabScroller from '@material/react-tab-scroller';
30+
31+
class MyApp extends React.Component {
32+
render() {
33+
return (
34+
<TabScroller>
35+
<div className='tab'>Tab 1</div>
36+
<div className='tab'>Tab 2</div>
37+
<div className='tab'>Tab 3</div>
38+
</TabScroller>
39+
);
40+
}
41+
}
42+
```
43+
44+
## Props
45+
46+
Prop Name | Type | Description
47+
--- | --- | ---
48+
alignStart | boolean | If true aligns the initial scroll position to the first tab.
49+
alignEnd | boolean | If true aligns the initial scroll position to the last tab.
50+
alignCenter | boolean | If true aligns the initial scroll position to the middle tab.
51+
className | string | Classes to appear on root element.
52+
onWheel | function | Callback triggered on wheel event.
53+
onTouchStart | function | Callback triggered on touchstart event.
54+
onPointerDown | function | Callback triggered on pointerdown event.
55+
onMouseDown | function | Callback triggered on mousedown event.
56+
onKeyDown | function | Callback triggered on keydown event.
57+
onTransitionEnd | function | Callback triggered on transitionend event.

packages/tab-scroller/index.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import React, {Component} from 'react';
2+
import PropTypes from 'prop-types';
3+
import classnames from 'classnames';
4+
5+
import {MDCTabScrollerFoundation, util} from '@material/tab-scroller/dist/mdc.tabScroller';
6+
7+
const convertDashToCamelCase = (propName) => propName.replace(/-(\w)/g, (_, v) => v.toUpperCase());
8+
9+
export default class TabScroller extends Component {
10+
areaElement_ = React.createRef();
11+
contentElement_ = React.createRef();
12+
state = {
13+
classList: new Set(),
14+
areaClassList: new Set(),
15+
scrollAreaStyleProperty: {},
16+
scrollContentStyleProperty: {},
17+
};
18+
19+
componentDidMount() {
20+
this.foundation_ = new MDCTabScrollerFoundation(this.adapter);
21+
this.foundation_.init();
22+
}
23+
24+
componentWillUnmount() {
25+
this.foundation_.destroy();
26+
}
27+
28+
get classes() {
29+
const {alignStart, alignEnd, alignCenter, className} = this.props;
30+
const {classList} = this.state;
31+
return classnames('mdc-tab-scroller', Array.from(classList), className, {
32+
'mdc-tab-scroller--align-start': alignStart,
33+
'mdc-tab-scroller--align-end': alignEnd,
34+
'mdc-tab-scroller--align-center': alignCenter,
35+
});
36+
}
37+
38+
setStyleToElement = (prop, value, elementStyleProperty) => {
39+
const styleName = convertDashToCamelCase(prop);
40+
const updateElementStyleProperty = Object.assign(
41+
this.state[elementStyleProperty],
42+
{[styleName]: value}
43+
);
44+
this.setState({[elementStyleProperty]: updateElementStyleProperty});
45+
}
46+
47+
get adapter() {
48+
return {
49+
eventTargetMatchesSelector: (evtTarget, selector) => {
50+
const MATCHES = util.getMatchesProperty(HTMLElement.prototype);
51+
return evtTarget[MATCHES](selector);
52+
},
53+
addClass: (className) => {
54+
const classList = new Set(this.state.classList);
55+
classList.add(className);
56+
this.setState({classList});
57+
},
58+
removeClass: (className) => {
59+
const classList = new Set(this.state.classList);
60+
classList.delete(className);
61+
this.setState({classList});
62+
},
63+
addScrollAreaClass: (className) => {
64+
const areaClassList = new Set(this.state.areaClassList);
65+
areaClassList.add(className);
66+
this.setState({areaClassList});
67+
},
68+
setScrollAreaStyleProperty: (prop, value) => this.setStyleToElement(prop, value, 'scrollAreaStyleProperty'),
69+
setScrollContentStyleProperty: (prop, value) => this.setStyleToElement(prop, value, 'scrollContentStyleProperty'),
70+
getScrollContentStyleValue: (propName) => this.contentElement_.current
71+
&& window.getComputedStyle(this.contentElement_.current).getPropertyValue(propName),
72+
setScrollAreaScrollLeft: (scrollX) => {
73+
if (!this.areaElement_.current) return;
74+
this.areaElement_.current.scrollLeft = scrollX;
75+
},
76+
getScrollAreaScrollLeft: () => this.areaElement_.current && this.areaElement_.current.scrollLeft,
77+
getScrollContentOffsetWidth: this.getScrollContentWidth,
78+
getScrollAreaOffsetWidth: () => this.areaElement_.current && this.areaElement_.current.offsetWidth,
79+
computeScrollAreaClientRect: () => this.areaElement_.current && this.areaElement_.current.getBoundingClientRect(),
80+
computeScrollContentClientRect: () => this.contentElement_.current
81+
&& this.contentElement_.current.getBoundingClientRect(),
82+
computeHorizontalScrollbarHeight: () => util.computeHorizontalScrollbarHeight(document),
83+
};
84+
}
85+
86+
getScrollPosition = () => {
87+
return this.foundation_.getScrollPosition();
88+
}
89+
90+
// needs to be public class method for react tab-bar
91+
getScrollContentWidth = () => {
92+
return this.contentElement_.current && this.contentElement_.current.offsetWidth;
93+
}
94+
95+
incrementScroll = (scrollXIncrement) => {
96+
this.foundation_.incrementScroll(scrollXIncrement);
97+
}
98+
99+
scrollTo = (scrollX) => {
100+
this.foundation_.scrollTo(scrollX);
101+
}
102+
103+
handleWheel_ = (evt) => {
104+
this.props.onWheel(evt);
105+
this.foundation_.handleInteraction(evt);
106+
}
107+
108+
handleTouchStart_ = (evt) => {
109+
this.props.onTouchStart(evt);
110+
this.foundation_.handleInteraction(evt);
111+
}
112+
113+
handlePointerDown_ = (evt) => {
114+
this.props.onPointerDown(evt);
115+
this.foundation_.handleInteraction(evt);
116+
}
117+
118+
handleMouseDown_ = (evt) => {
119+
this.props.onMouseDown(evt);
120+
this.foundation_.handleInteraction(evt);
121+
}
122+
123+
handleKeyDown_ = (evt) => {
124+
this.props.onKeyDown(evt);
125+
this.foundation_.handleInteraction(evt);
126+
}
127+
128+
handleTransitionEnd_ = (evt) => {
129+
this.props.onTransitionEnd(evt);
130+
this.foundation_.handleTransitionEnd(evt);
131+
}
132+
133+
render() {
134+
const {areaClassList} = this.state;
135+
const {
136+
children,
137+
/* eslint-disable */
138+
alignStart,
139+
alignEnd,
140+
alignCenter,
141+
className,
142+
onWheel,
143+
onTouchStart,
144+
onPointerDown,
145+
onMouseDown,
146+
onKeyDown,
147+
onTransitionEnd,
148+
/* eslint-enable */
149+
...otherProps
150+
} = this.props;
151+
const areaClasses = classnames('mdc-tab-scroller__scroll-area', Array.from(areaClassList));
152+
153+
return (
154+
<div
155+
className={this.classes}
156+
onWheel={this.handleWheel_}
157+
onTouchStart={this.handleTouchStart_}
158+
onPointerDown={this.handlePointerDown_}
159+
onMouseDown={this.handleMouseDown_}
160+
onKeyDown={this.handleKeyDown_}
161+
onTransitionEnd={this.handleTransitionEnd_}
162+
{...otherProps}
163+
>
164+
<div
165+
className={areaClasses}
166+
ref={this.areaElement_}
167+
>
168+
<div
169+
className='mdc-tab-scroller__scroll-content'
170+
ref={this.contentElement_}
171+
>
172+
{children}
173+
</div>
174+
</div>
175+
</div>
176+
);
177+
}
178+
}
179+
180+
TabScroller.propTypes = {
181+
alignStart: PropTypes.bool,
182+
alignEnd: PropTypes.bool,
183+
alignCenter: PropTypes.bool,
184+
children: PropTypes.node,
185+
className: PropTypes.string,
186+
onWheel: PropTypes.func,
187+
onTouchStart: PropTypes.func,
188+
onPointerDown: PropTypes.func,
189+
onMouseDown: PropTypes.func,
190+
onKeyDown: PropTypes.func,
191+
onTransitionEnd: PropTypes.func,
192+
};
193+
194+
TabScroller.defaultProps = {
195+
alignStart: false,
196+
alignEnd: false,
197+
alignCenter: false,
198+
className: '',
199+
children: null,
200+
onWheel: () => {},
201+
onTouchStart: () => {},
202+
onPointerDown: () => {},
203+
onMouseDown: () => {},
204+
onKeyDown: () => {},
205+
onTransitionEnd: () => {},
206+
};

packages/tab-scroller/index.scss

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

packages/tab-scroller/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@material/react-tab-scroller",
3+
"version": "0.0.0",
4+
"description": "Material Components React Tab Scroller",
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 scroller",
12+
"tabscroller",
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/tab-scroller": "^0.38.1",
21+
"classnames": "^2.2.5",
22+
"prop-types": "^15.6.1",
23+
"react": "^16.4.2"
24+
},
25+
"publishConfig": {
26+
"access": "public"
27+
}
28+
}

packages/webpack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function getMaterialExternals() {
8585
'select',
8686
'tab',
8787
'tab-indicator',
88+
'tab-scroller',
8889
'textfield',
8990
'top-app-bar',
9091
'typography',

test/screenshot/full-suite.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default [
1010
require('./select/screenshot-suite').default,
1111
require('./tab/screenshot-suite').default,
1212
require('./tab-indicator/screenshot-suite').default,
13+
require('./tab-scroller/screenshot-suite').default,
1314
require('./text-field/screenshot-suite').default,
1415
require('./text-field/helper-text/screenshot-suite').default,
1516
require('./text-field/icon/screenshot-suite').default,

test/screenshot/golden.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
"chips/index.html": "949725ee6658e0c4edcdecd2bf057fda5fb814473f7a527da5c96e6392b84015",
2121
"select/index.html": "5ce288649fe5e78d21ab9315a35cc2a22504176b8abaf56b92579121df51f073",
2222
"tab-indicator/index.html": "19f1befc0947128b46d84e1fdee73741bffcd63950bf0cddaadd9ae283ab85f8",
23+
"tab-scroller/index.html": "726a001f1178c06e3fad4a943c6253c8c89084e3867bced469be5bb5b8529278",
2324
"tab/index.html": "2151d184cc420fdf19c3e9b6e8c553ee1cbfc7df4ebbda73b7a669dfca939bef"
2425
}

test/screenshot/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<a href="notched-outline">Notched Outline</a>
1111
<a href="select">Select</a>
1212
<a href="tab">Tab</a>
13+
<a href="tab-scroller">Tab Scroller</a>
1314
<a href="tab-indicator">Tab Indicator</a>
1415
<a href="text-field">Text Field</a>
1516
<a href="text-field/icon">Text Field Icon</a>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
5+
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
6+
<link rel="stylesheet" href="index.css">
7+
<style>
8+
body {
9+
font-family: 'Roboto', Arial, sans-serif;
10+
}
11+
</style>
12+
<script src="index.js" async></script>
13+
</head>
14+
<body>
15+
<div id='app'></div>
16+
</body>
17+
</html>

0 commit comments

Comments
 (0)