├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── examples
├── .env
├── .eslintcache
├── .gitignore
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.js
│ ├── App.test.js
│ ├── Basic
│ │ ├── index.js
│ │ └── styles.css
│ ├── TabsRemoval
│ │ ├── index.js
│ │ └── styles.css
│ ├── dummyData.js
│ ├── index.css
│ └── index.js
└── yarn.lock
├── package-lock.json
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── components
│ ├── InkBar.js
│ ├── ShowMore.js
│ ├── Tab.js
│ └── TabPanel.js
└── index.js
├── styles.css
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/react"],
3 | "plugins": [
4 | "@babel/plugin-proposal-class-properties",
5 | [
6 | "transform-imports",
7 | {
8 | "lodash": {
9 | "transform": "lodash/${member}",
10 | "preventFullImport": true
11 | }
12 | }
13 | ],
14 | [
15 | "module-resolver",
16 | {
17 | "root": ["./", "./src"]
18 | }
19 | ]
20 | ],
21 | "env": {
22 | "esm": {
23 | "presets": [
24 | [
25 | "@babel/env",
26 | {
27 | "modules": false
28 | }
29 | ],
30 | "@babel/react"
31 | ],
32 | "plugins": [
33 | [
34 | "transform-imports",
35 | {
36 | "lodash": {
37 | "transform": "lodash-es/${member}",
38 | "preventFullImport": true
39 | }
40 | }
41 | ]
42 | ]
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/dist/**
2 | **/lib/**
3 | **/node_modules/**
4 | webpack.config.js
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "es6": true
8 | },
9 | "rules": {
10 | "no-underscore-dangle": 0,
11 | "react/jsx-filename-extension": 0,
12 | "import/no-named-default": 0,
13 | "jsx-a11y/no-static-element-interactions": 0,
14 | "no-return-assign": 0,
15 | "comma-dangle": 0,
16 | "import/no-named-as-default": 0,
17 | "object-curly-newline": 0,
18 | "jsx-a11y/click-events-have-key-events": 0,
19 | "react/jsx-props-no-spreading": 0,
20 | "max-len": ["error", 120],
21 | "operator-linebreak": ["error", "after"],
22 | "arrow-parens": [
23 | "error",
24 | "as-needed",
25 | {
26 | "requireForBlockBody": false
27 | }
28 | ]
29 | },
30 | "settings": {
31 | "import/resolver": {
32 | "node": {
33 | "paths": [".", "src", "node_modules"],
34 | "extensions": [".js", ".jsx"]
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | */**/dist
3 | *.log
4 | .DS_Store
5 | build
6 | yarn.lock
7 | lib
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | node_modules
4 | src
5 | examples
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "prettier.singleQuote": true,
3 | "prettier.arrowParens": "avoid",
4 | "prettier.printWidth": 120
5 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Denis Rul
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React responsive tabs
2 |
3 | #### [Live demo](http://maslianok.github.io/react-responsive-tabs/)
4 |
5 | ### Your feedback is highly appreciated!
6 |
7 | Please, file an issue if something went wrong or let me know via Twitter @maslianok
8 |
9 | #### Responsive
10 |
11 | - Hide tabs under the 'Show more' option when they don't fit into the screen
12 | - Transform tabs into the accordion when the wrapper width reaches the `transformWidth` value
13 |
14 | 
15 |
16 | #### Accessible
17 |
18 | The component outputs HTML code that follows accessibility principles (aka [WAI-ARIA](https://en.wikipedia.org/wiki/WAI-ARIA)) and uses ARIA attributes such as `role`, `aria-selected`, `aria-controls`, `aria-labeledby` etc.
19 |
20 | 
21 |
22 | #### Fast
23 |
24 | We are using [`react-resize-detector`](https://github.com/maslianok/react-resize-detector). No timers. Just pure event-based element resize detection.
25 |
26 | ## Installation
27 |
28 | `npm install react-responsive-tabs`
29 |
30 | ## Demo
31 |
32 | #### [Live demo](http://maslianok.github.io/react-responsive-tabs/)
33 |
34 | Local demo
35 |
36 | ```sh
37 | # 1. clone the repository
38 | git clone https://github.com/maslianok/react-responsive-tabs.git
39 |
40 | # 2. Install react-responsive-tabs dependencies. You must do it because we use raw library code in the example
41 | cd react-responsive-tabs
42 | npm install
43 |
44 | # 3. Install dependencies to run the example
45 | cd examples
46 | npm install
47 |
48 | # 4. Finally run the example
49 | npm start
50 | ```
51 |
52 | ## Example
53 |
54 | ```javascript
55 | import React, { Component } from 'react';
56 | import { render } from 'react-dom';
57 | import Tabs from 'react-responsive-tabs';
58 |
59 | // IMPORTANT you need to include the default styles
60 | import 'react-responsive-tabs/styles.css';
61 |
62 | const presidents = [
63 | { name: 'George Washington', biography: '...' },
64 | { name: 'Theodore Roosevelt', biography: '...' },
65 | ];
66 |
67 | function getTabs() {
68 | return presidents.map((president, index) => ({
69 | title: president.name,
70 | getContent: () => president.biography,
71 | /* Optional parameters */
72 | key: index,
73 | tabClassName: 'tab',
74 | panelClassName: 'panel',
75 | }));
76 | }
77 |
78 | const App = () => ;
79 |
80 | render(, document.getElementById('root'));
81 | ```
82 |
83 | ## API
84 |
85 | All entities listed below should be used as props to the `Tabs` component.
86 |
87 | | Prop | Type | Description | Default |
88 | | ---------------- | ------------- | ---------------------------------------------------------------------------- | --------------- |
89 | | items | Array | Tabs data | [Item](#Item)[] |
90 | | beforeChange | Function | Fires right before a tab changes. Return `false` to prevent the tab change | undefined |
91 | | onChange | Function | onChange callback | undefined |
92 | | selectedTabKey | Number/String | Selected tab | undefined |
93 | | showMore | Bool | Whether to show `Show more` or not | `true` |
94 | | showMoreLabel | String/Node | `Show more` tab name | `...` |
95 | | transform | Bool | Transform to accordion when the wrapper width is less than `transformWidth`. | `true` |
96 | | transformWidth | Number | Transform width. | 800 |
97 | | unmountOnExit | Bool | Whether to unmount inactive tabs from DOM tree or not | `true` |
98 | | tabsWrapperClass | String | Wrapper class | undefined |
99 | | tabClassName | String | Tab class | undefined |
100 | | panelClassName | String | Tab panel class | undefined |
101 | | allowRemove | Bool | Allows tabs removal. | `false` |
102 | | removeActiveOnly | Bool | Only active tab has removal option | `false` |
103 | | showInkBar | Bool | Add MaterialUI InkBar effect | `false` |
104 | | uid | any | An optional external id. The component rerenders when it changes | undefined |
105 |
106 | #### Item
107 |
108 | | Prop | Type | Description |
109 | | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
110 | | title | String | Tab title |
111 | | getContent | Function | A function that returns data that will be rendered when tab become active |
112 | | content | String | Use this prop insted of getContent. This is a sync version of `getContent`. The data will be always rendered in a hidden div. Sometimes it may be useful for SEO |
113 | | key | Number | A uniq tab id |
114 | | tabClassName | String | Tab class name |
115 | | panelClassName | String | Panel class name |
116 |
117 | ### License
118 |
119 | MIT
120 |
--------------------------------------------------------------------------------
/examples/.env:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
--------------------------------------------------------------------------------
/examples/.eslintcache:
--------------------------------------------------------------------------------
1 | [{"/Users/maslianok/www/react-responsive-tabs/examples/src/index.js":"1","/Users/maslianok/www/react-responsive-tabs/examples/src/App.js":"2","/Users/maslianok/www/react-responsive-tabs/examples/src/TabsRemoval/index.js":"3","/Users/maslianok/www/react-responsive-tabs/examples/src/Basic/index.js":"4","/Users/maslianok/www/react-responsive-tabs/examples/src/dummyData.js":"5"},{"size":147,"mtime":1591348102709,"results":"6","hashOfConfig":"7"},{"size":2547,"mtime":1612256266175,"results":"8","hashOfConfig":"7"},{"size":1749,"mtime":1612256415867,"results":"9","hashOfConfig":"7"},{"size":2509,"mtime":1612256325201,"results":"10","hashOfConfig":"7"},{"size":1195,"mtime":1591348102708,"results":"11","hashOfConfig":"7"},{"filePath":"12","messages":"13","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"v91sf1",{"filePath":"14","messages":"15","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"16","messages":"17","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"18","messages":"19","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"20","messages":"21","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/maslianok/www/react-responsive-tabs/examples/src/index.js",[],"/Users/maslianok/www/react-responsive-tabs/examples/src/App.js",[],"/Users/maslianok/www/react-responsive-tabs/examples/src/TabsRemoval/index.js",["22"],"/Users/maslianok/www/react-responsive-tabs/examples/src/Basic/index.js",[],"/Users/maslianok/www/react-responsive-tabs/examples/src/dummyData.js",[],{"ruleId":"23","severity":1,"message":"24","line":36,"column":7,"nodeType":"25","messageId":"26","endLine":36,"endColumn":48},"no-alert","Unexpected alert.","CallExpression","unexpected"]
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/examples/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "examples",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "classnames": "^2.3.1",
7 | "linklocal": "^2.8.2",
8 | "react": "^17.0.2",
9 | "react-dom": "^17.0.2",
10 | "react-ga": "^3.3.0",
11 | "react-helmet": "^6.1.0",
12 | "react-responsive-tabs": "file:../",
13 | "react-scripts": "4.0.3"
14 | },
15 | "scripts": {
16 | "start": "linklocal && react-scripts start",
17 | "build": "react-scripts build",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/examples/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maslianok/react-responsive-tabs/ca3a9fa2adc48b7fbf0fa0a16a4795f2b10d82ed/examples/public/favicon.ico
--------------------------------------------------------------------------------
/examples/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | React responsive tabs
9 |
14 |
18 |
19 |
28 | React App
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/examples/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/examples/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import ReactGA from 'react-ga';
3 | import { Helmet } from 'react-helmet';
4 | import cs from 'classnames';
5 |
6 | import 'react-responsive-tabs/styles.css';
7 | import './index.css';
8 |
9 | import BasicExample from './Basic';
10 | import TabsRemovalExample from './TabsRemoval';
11 |
12 | ReactGA.initialize('UA-94085609-1');
13 | ReactGA.set({ page: window.location.pathname });
14 | ReactGA.pageview(window.location.pathname);
15 |
16 | class App extends PureComponent {
17 | constructor(props) {
18 | super(props);
19 |
20 | this.state = {
21 | active: 'basic',
22 | };
23 | }
24 |
25 | onChangeExample = type => () => this.setState({ active: type });
26 |
27 | render() {
28 | const { active } = this.state;
29 | return (
30 |
31 |
48 |
49 |
react-responsive-tabs
50 |
64 |
65 |
66 |
72 | basic usage
73 |
74 |
80 | tabs removal
81 |
82 |
83 |
84 | {active === 'basic' &&
}
85 | {active === 'removal' &&
}
86 |
87 | );
88 | }
89 | }
90 |
91 | export default App;
92 |
--------------------------------------------------------------------------------
/examples/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/examples/src/Basic/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React, { PureComponent } from 'react';
3 | import Tabs from 'react-responsive-tabs';
4 |
5 | import dummyData from '../dummyData';
6 |
7 | import './styles.css';
8 |
9 | export class Basic extends PureComponent {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | showMore: true,
15 | transform: true,
16 | showInkBar: false,
17 | items: this.getSimpleTabs(),
18 | selectedTabKey: 0,
19 | unmountOnExit: true,
20 | };
21 | }
22 |
23 | onChangeProp = propsName => evt => {
24 | this.setState({ [propsName]: evt.target.type === 'checkbox' ? evt.target.checked : +evt.target.value });
25 | };
26 |
27 | getSimpleTabs = () =>
28 | dummyData.map(({ name, biography }, index) => ({
29 | key: index,
30 | title: name,
31 | getContent: () => biography,
32 | }));
33 |
34 | render() {
35 | const { showMore, transform, showInkBar, selectedTabKey, unmountOnExit } = this.state;
36 |
37 | return (
38 |
81 | );
82 | }
83 | }
84 |
85 | export default Basic;
86 |
--------------------------------------------------------------------------------
/examples/src/Basic/styles.css:
--------------------------------------------------------------------------------
1 | /* basic example */
2 | .basic__wrapper {
3 | display: flex;
4 | flex-grow: 1;
5 | }
6 |
7 | .basic__props {
8 | width: 260px;
9 | flex-shrink: 0;
10 | padding: 10px;
11 | border-right: 1px solid #f0f0f0;
12 | }
13 |
14 | .basic__prop {
15 | margin: 10px 0;
16 | }
17 |
18 | .basic__input {
19 | width: 40px;
20 | }
21 |
22 | .basic__tabs {
23 | padding: 10px;
24 | flex-grow: 1;
25 | }
26 |
--------------------------------------------------------------------------------
/examples/src/TabsRemoval/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Tabs from 'react-responsive-tabs';
3 |
4 | import dummyData from '../dummyData';
5 |
6 | import './styles.css';
7 |
8 | export class TabsRemoval extends PureComponent {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {
13 | items: this.getTabs(),
14 | selectedTabKey: 0,
15 | };
16 | }
17 |
18 | onChangeProp = propsName => evt => {
19 | this.setState({ [propsName]: evt.target.type === 'checkbox' ? evt.target.checked : +evt.target.value });
20 | };
21 |
22 | onRemoveTab = (key, evt) => {
23 | evt.stopPropagation();
24 |
25 | // current tabs
26 | const { items } = this.state;
27 |
28 | // find index to remove
29 | const indexToRemove = items.findIndex(tab => tab.key === key);
30 |
31 | // create a new array without [indexToRemove] item
32 | const newTabs = [...items.slice(0, indexToRemove), ...items.slice(indexToRemove + 1)];
33 |
34 | const nextSelectedIndex = newTabs[indexToRemove] ? indexToRemove : indexToRemove - 1;
35 | if (!newTabs[nextSelectedIndex]) {
36 | alert('You can not delete the last tab!');
37 | return;
38 | }
39 |
40 | this.setState({ items: newTabs, selectedTabKey: newTabs[nextSelectedIndex].key });
41 | };
42 |
43 | getTabs = () =>
44 | // eslint-disable-next-line
45 | dummyData.map(({ name, biography }, i) => ({
46 | key: i,
47 | title: (
48 |
51 | ),
52 | getContent: () => biography,
53 | tabClassName: 'tab-wrapper',
54 | }));
55 |
56 | render() {
57 | const { items, selectedTabKey } = this.state;
58 | return (
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | export default TabsRemoval;
67 |
--------------------------------------------------------------------------------
/examples/src/TabsRemoval/styles.css:
--------------------------------------------------------------------------------
1 | .itemRemoval__wrapper {
2 | padding: 10px;
3 | }
4 |
5 | .tab-wrapper {
6 | display: flex;
7 | align-items: center;
8 | padding: 0;
9 | }
10 |
11 | .tab-container {
12 | position: relative;
13 | }
14 |
15 | .tab-name {
16 | margin-right: 10px;
17 | padding: 10px;
18 | }
19 |
20 | .tab-cross-icon {
21 | font-size: 12px;
22 | position: absolute;
23 | top: 0;
24 | bottom: 0;
25 | right: 0;
26 | width: 20px;
27 | display: flex;
28 | align-items: center;
29 | justify-content: center;
30 | font-weight: bold;
31 | }
--------------------------------------------------------------------------------
/examples/src/dummyData.js:
--------------------------------------------------------------------------------
1 | /* eslint max-len: 0 */
2 |
3 | export const presidents = [
4 | {
5 | name: 'George Washington',
6 | biography: 'George Washington (February 22, 1732 – December 14, 1799) was the first President of the United States...',
7 | },
8 | {
9 | name: 'Thomas Jefferson',
10 | biography: 'Thomas Jefferson (April 13 1743 – July 4, 1826) was an American lawyer',
11 | },
12 | {
13 | name: 'Abraham Lincoln',
14 | biography: 'Abraham Lincoln (February 12, 1809 – April 15, 1865) was the 16th President of the United States',
15 | },
16 | {
17 | name: 'Benjamin Harrison',
18 | biography: 'Benjamin Harrison (August 20, 1833 – March 13, 1901) was the 23rd President of the United States',
19 | },
20 | {
21 | name: 'William McKinley',
22 | biography: 'William McKinley (January 29, 1843 – September 14, 1901) was the 25th President of the United States',
23 | },
24 | {
25 | name: 'Franklin D. Roosevelt',
26 | biography: 'Franklin Delano Roosevelt (January 30, 1882 – April 12, 1945), commonly known by his initials FDR, was an American statesman',
27 | },
28 | {
29 | name: 'Theodore Roosevelt',
30 | biography: 'Theodore Roosevelt (October 27, 1858 – January 6, 1919), often referred to as Teddy or TR...',
31 | },
32 | ];
33 |
34 | export default presidents;
35 |
--------------------------------------------------------------------------------
/examples/src/index.css:
--------------------------------------------------------------------------------
1 | html, body, #root {
2 | min-height: 100%;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | font-family: sans-serif;
9 | }
10 |
11 | .container {
12 | display: flex;
13 | flex-direction: column;
14 | height: 100%;
15 | }
16 |
17 | .jumbotron {
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | flex-direction: column;
22 | height: 300px;
23 | background-color: #6f5499;
24 | color: #cdbfe3;
25 |
26 | font-size: 32px;
27 | }
28 |
29 | .github-link {
30 | margin-top: 20px;
31 | }
32 |
33 | .menu {
34 | display: flex;
35 | align-items: stretch;
36 | justify-content: center;
37 | color: #563d7c;
38 | border-bottom: 1px solid #f0f0f0;
39 | }
40 |
41 | .menu-item {
42 | font-size: 18px;
43 | font-variant: small-caps;
44 | padding: 18px;
45 | cursor: pointer;
46 | }
47 |
48 | .menu-item:hover,
49 | .menu-item--active {
50 | background-color: #f9f9f9;
51 | text-decoration: underline;
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/examples/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import App from './App';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Vitalii Maslianok (https://github.com/maslianok)",
3 | "version": "4.4.3",
4 | "name": "react-responsive-tabs",
5 | "description": "React responsive tabs",
6 | "license": "MIT",
7 | "main": "lib/index.js",
8 | "bugs": {
9 | "url": "https://github.com/maslianok/react-responsive-tabs/issues"
10 | },
11 | "directories": {
12 | "example": "examples"
13 | },
14 | "homepage": "https://github.com/maslianok/react-responsive-tabs",
15 | "keywords": [
16 | "react",
17 | "responsive",
18 | "tabs",
19 | "tab"
20 | ],
21 | "maintainers": [
22 | "maslianok "
23 | ],
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/maslianok/react-responsive-tabs.git"
27 | },
28 | "peerDependencies": {
29 | "react": "^15.1.0 || ^16.0.0 || ^17.0.0",
30 | "react-dom": "^15.1.0 || ^16.0.0 || ^17.0.0"
31 | },
32 | "dependencies": {
33 | "classnames": "^2.3.1",
34 | "lodash.throttle": "^4.1.1",
35 | "prop-types": "^15.7.2",
36 | "react-resize-detector": "^6.7.5"
37 | },
38 | "devDependencies": {
39 | "@babel/cli": "^7.14.8",
40 | "@babel/core": "^7.15.0",
41 | "@babel/plugin-proposal-class-properties": "^7.14.5",
42 | "@babel/preset-env": "^7.15.0",
43 | "@babel/preset-react": "^7.14.5",
44 | "babel-eslint": "^10.1.0",
45 | "babel-plugin-module-resolver": "^4.1.0",
46 | "babel-plugin-transform-imports": "^2.0.0",
47 | "cross-env": "^7.0.3",
48 | "eslint": "^7.32.0",
49 | "eslint-config-airbnb": "^18.2.1",
50 | "eslint-plugin-import": "^2.24.0",
51 | "eslint-plugin-jsx-a11y": "^6.4.1",
52 | "eslint-plugin-react": "^7.24.0",
53 | "react": "^17.0.2",
54 | "react-dom": "^17.0.2",
55 | "rimraf": "^3.0.2"
56 | },
57 | "scripts": {
58 | "build": "npm run build:cjs && npm run build:esm",
59 | "build:cjs": "babel src --out-dir lib",
60 | "build:esm": "cross-env BABEL_ENV=esm babel src --out-dir lib/esm",
61 | "clean": "rimraf lib",
62 | "lint": "eslint -c .eslintrc src",
63 | "test": "npm run lint && npm run clean && npm run build"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maslianok/react-responsive-tabs/ca3a9fa2adc48b7fbf0fa0a16a4795f2b10d82ed/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | React App
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/InkBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export const InkBar = ({ left, width }) => (
5 |
8 | );
9 |
10 | export default InkBar;
11 |
12 | InkBar.propTypes = {
13 | left: PropTypes.number,
14 | width: PropTypes.number
15 | };
16 |
17 | InkBar.defaultProps = {
18 | left: 0,
19 | width: 0
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/ShowMore.js:
--------------------------------------------------------------------------------
1 | /* eslint jsx-a11y/no-noninteractive-element-interactions: 0, jsx-a11y/no-noninteractive-tabindex: 0 */
2 |
3 | import React, { Component } from 'react';
4 | import classNames from 'classnames';
5 | import PropTypes from 'prop-types';
6 |
7 | export default class ShowMore extends Component {
8 | constructor() {
9 | super();
10 |
11 | this.state = {
12 | isFocused: false,
13 | isHidden: true
14 | };
15 | }
16 |
17 | componentDidMount() {
18 | if (typeof window !== 'undefined') {
19 | window.addEventListener('click', this.close);
20 | window.addEventListener('keydown', this.onKeyDown);
21 | }
22 | }
23 |
24 | shouldComponentUpdate(nextProps, nextState) {
25 | const { children, isShown, hasChildSelected } = this.props;
26 | return (
27 | children.length !== nextProps.children.length ||
28 | isShown !== nextProps.isShown ||
29 | hasChildSelected !== nextProps.hasChildSelected ||
30 | this.state !== nextState
31 | );
32 | }
33 |
34 | componentWillUnmount() {
35 | if (typeof window !== 'undefined') {
36 | window.removeEventListener('click', this.close);
37 | window.removeEventListener('keydown', this.onKeyDown);
38 | }
39 | }
40 |
41 | onFocus = () => this.setState({ isFocused: true });
42 |
43 | onBlur = () => this.setState({ isFocused: false });
44 |
45 | onKeyDown = event => {
46 | const { isFocused, isHidden } = this.state;
47 | if (event.keyCode === 13) {
48 | if (isFocused) {
49 | this.setState({ isHidden: !isHidden });
50 | } else if (!isHidden) {
51 | this.setState({ isHidden: true });
52 | }
53 | }
54 | };
55 |
56 | close = () => {
57 | const { isHidden } = this.state;
58 | if (!isHidden) {
59 | this.setState({ isHidden: true });
60 | }
61 | };
62 |
63 | toggleVisibility = event => {
64 | const { isHidden } = this.state;
65 | event.stopPropagation();
66 | this.setState({ isHidden: !isHidden });
67 | };
68 |
69 | render() {
70 | const { isShown, children, onShowMoreChanged, hasChildSelected, label } = this.props;
71 | const { isHidden } = this.state;
72 | if (!isShown || !children || !children.length) {
73 | return null;
74 | }
75 |
76 | const isListHidden = isHidden;
77 | const showMoreStyles = classNames({
78 | RRT__showmore: true,
79 | 'RRT__showmore--selected': hasChildSelected
80 | });
81 |
82 | const listStyles = classNames({
83 | 'RRT__showmore-list': true,
84 | 'RRT__showmore-list--opened': !isListHidden
85 | });
86 |
87 | const showMoreLabelStyles = classNames({
88 | 'RRT__showmore-label': true,
89 | 'RRT__showmore-label--selected': !isListHidden
90 | });
91 |
92 | return (
93 |
102 |
{label}
103 |
104 | {children}
105 |
106 |
107 | );
108 | }
109 | }
110 |
111 | ShowMore.propTypes = {
112 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.string]),
113 | hasChildSelected: PropTypes.bool,
114 | isShown: PropTypes.bool.isRequired,
115 | onShowMoreChanged: PropTypes.func,
116 | label: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
117 | };
118 |
119 | ShowMore.defaultProps = {
120 | children: undefined,
121 | hasChildSelected: false,
122 | label: '...',
123 | onShowMoreChanged: () => null
124 | };
125 |
--------------------------------------------------------------------------------
/src/components/Tab.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class Tab extends Component {
5 | shouldComponentUpdate(nextProps) {
6 | const { children, selected, classNames } = this.props;
7 | return children !== nextProps.children || selected !== nextProps.selected || classNames !== nextProps.classNames;
8 | }
9 |
10 | onTabClick = evt => {
11 | const { onClick, originalKey } = this.props;
12 | onClick(originalKey, evt);
13 | };
14 |
15 | renderRemovableTab = () => {
16 | const { children, onRemove } = this.props;
17 | return (
18 |
19 |
{children}
20 |
21 | x
22 |
23 |
24 | );
25 | };
26 |
27 | renderTab = () => {
28 | const { children, allowRemove } = this.props;
29 |
30 | if (allowRemove) {
31 | return this.renderRemovableTab();
32 | }
33 |
34 | return children;
35 | };
36 |
37 | render() {
38 | const { id, classNames, selected, disabled, panelId, onFocus, onBlur, originalKey } = this.props;
39 |
40 | return (
41 | (this.tab = e)}
43 | role="tab"
44 | className={classNames}
45 | id={id}
46 | aria-selected={selected ? 'true' : 'false'}
47 | aria-expanded={selected ? 'true' : 'false'}
48 | aria-disabled={disabled ? 'true' : 'false'}
49 | aria-controls={panelId}
50 | tabIndex="0"
51 | onClick={this.onTabClick}
52 | onFocus={onFocus(originalKey)}
53 | onBlur={onBlur}
54 | >
55 | {this.renderTab()}
56 |
57 | );
58 | }
59 | }
60 |
61 | Tab.propTypes = {
62 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.string]),
63 | disabled: PropTypes.bool,
64 |
65 | // generic props
66 | panelId: PropTypes.string.isRequired,
67 | selected: PropTypes.bool.isRequired,
68 | onClick: PropTypes.func.isRequired,
69 | onRemove: PropTypes.func,
70 | onFocus: PropTypes.func.isRequired,
71 | onBlur: PropTypes.func.isRequired,
72 | allowRemove: PropTypes.bool,
73 | id: PropTypes.string.isRequired,
74 | originalKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
75 | classNames: PropTypes.string.isRequired,
76 | };
77 |
78 | Tab.defaultProps = {
79 | children: undefined,
80 | onRemove: () => {},
81 | allowRemove: false,
82 | disabled: false,
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/TabPanel.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default class TabPanel extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | renderedAtLeastOnce: !props.isHidden,
9 | };
10 | }
11 |
12 | static getDerivedStateFromProps(props, state) {
13 | return {
14 | renderedAtLeastOnce: state.renderedAtLeastOnce || !props.isHidden,
15 | };
16 | }
17 |
18 | shouldComponentUpdate(nextProps) {
19 | const { children, getContent, classNames, isHidden } = this.props;
20 | return (
21 | getContent !== nextProps.getContent ||
22 | children !== nextProps.children ||
23 | classNames !== nextProps.classNames ||
24 | isHidden !== nextProps.isHidden
25 | );
26 | }
27 |
28 | render() {
29 | const { classNames, id, tabId, children, getContent, isHidden } = this.props;
30 | const { renderedAtLeastOnce } = this.state;
31 |
32 | return (
33 |
34 | {getContent && renderedAtLeastOnce && getContent()}
35 | {!getContent && children}
36 |
37 | );
38 | }
39 | }
40 |
41 | TabPanel.propTypes = {
42 | getContent: PropTypes.func,
43 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.string]),
44 | id: PropTypes.string.isRequired,
45 | isHidden: PropTypes.bool,
46 |
47 | // generic props
48 | classNames: PropTypes.string.isRequired,
49 | tabId: PropTypes.string.isRequired,
50 | };
51 |
52 | TabPanel.defaultProps = {
53 | getContent: undefined,
54 | children: undefined,
55 | isHidden: false,
56 | };
57 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, createRef } from 'react';
2 | import ResizeDetector from 'react-resize-detector/build/withPolyfill';
3 | import cs from 'classnames';
4 | import throttle from 'lodash.throttle';
5 | import PropTypes from 'prop-types';
6 | import ShowMore from './components/ShowMore';
7 | import Tab from './components/Tab';
8 | import TabPanel from './components/TabPanel';
9 | import InkBar from './components/InkBar';
10 |
11 | const tabPrefix = 'tab-';
12 | const panelPrefix = 'panel-';
13 |
14 | export default class Tabs extends Component {
15 | constructor(props) {
16 | super(props);
17 |
18 | this.tabRefs = {};
19 | this.tabsWrapper = createRef();
20 | this.selectedTabKeyProp = props.selectedTabKey;
21 |
22 | this.state = {
23 | tabDimensions: {},
24 | blockWidth: 0,
25 | tabsTotalWidth: 0,
26 | showMoreWidth: 40,
27 | selectedTabKey: props.selectedTabKey,
28 | focusedTabKey: null,
29 | };
30 |
31 | this.onResizeThrottled = throttle(this.onResize, props.resizeThrottle, { trailing: true });
32 | }
33 |
34 | componentDidMount() {
35 | this.setTabsDimensions();
36 | }
37 |
38 | shouldComponentUpdate(nextProps, nextState) {
39 | const { selectedTabKey, tabsTotalWidth, blockWidth, showMoreWidth } = this.state;
40 | const { items, transform, showMore, showInkBar, allowRemove, removeActiveOnly, uid } = this.props;
41 |
42 | return (
43 | items !== nextProps.items ||
44 | nextProps.uid !== uid ||
45 | nextProps.transform !== transform ||
46 | nextProps.showMore !== showMore ||
47 | nextProps.showInkBar !== showInkBar ||
48 | nextProps.allowRemove !== allowRemove ||
49 | nextProps.removeActiveOnly !== removeActiveOnly ||
50 | nextState.tabsTotalWidth !== tabsTotalWidth ||
51 | nextState.blockWidth !== blockWidth ||
52 | nextState.showMoreWidth !== showMoreWidth ||
53 | nextProps.selectedTabKey !== this.selectedTabKeyProp ||
54 | nextState.selectedTabKey !== selectedTabKey
55 | );
56 | }
57 |
58 | componentDidUpdate(prevProps) {
59 | const { uid, items, selectedTabKey } = this.props;
60 |
61 | if (this.selectedTabKeyProp !== selectedTabKey) {
62 | // eslint-disable-next-line react/no-did-update-set-state
63 | this.setState({ selectedTabKey });
64 | }
65 |
66 | if (
67 | uid !== prevProps.uid ||
68 | items.length !== prevProps.items.length ||
69 | items.every((item, i) => item.title !== prevProps.items[i].title)
70 | ) {
71 | this.setTabsDimensions();
72 | }
73 |
74 | this.selectedTabKeyProp = selectedTabKey;
75 | }
76 |
77 | onResize = () => {
78 | if (this.tabsWrapper.current) {
79 | const currentIsCollapsed = this.getIsCollapsed();
80 | this.setState({ blockWidth: this.tabsWrapper.current.offsetWidth }, () => {
81 | const { items } = this.props;
82 | const { selectedTabKey } = this.state;
83 | const nextIsCollapsed = this.getIsCollapsed();
84 | if (currentIsCollapsed && !nextIsCollapsed && selectedTabKey === -1 && items && items.length) {
85 | const firstTabKey = items[0].key || 0;
86 | this.setState({ selectedTabKey: firstTabKey });
87 | }
88 | });
89 | }
90 | };
91 |
92 | onChangeTab = (nextTabKey, evt) => {
93 | const { beforeChange, onChange } = this.props;
94 | const { selectedTabKey } = this.state;
95 |
96 | if (typeof beforeChange === 'function') {
97 | const beforeChangeRes = beforeChange({ selectedTabKey, nextTabKey });
98 | if (beforeChangeRes === false) {
99 | evt.preventDefault();
100 | return;
101 | }
102 | }
103 |
104 | const isCollapsed = this.getIsCollapsed();
105 | if (isCollapsed && selectedTabKey === nextTabKey) {
106 | // hide on mobile
107 | this.setState({ selectedTabKey: -1 });
108 | } else {
109 | // change active tab
110 | this.setState({ selectedTabKey: nextTabKey });
111 | }
112 |
113 | if (onChange) {
114 | onChange(nextTabKey);
115 | }
116 | };
117 |
118 | onFocusTab = focusedTabKey => () => this.setState({ focusedTabKey });
119 |
120 | onBlurTab = () => this.setState({ focusedTabKey: null });
121 |
122 | onKeyDown = event => {
123 | const { focusedTabKey } = this.state;
124 | if (event.keyCode === 13 && focusedTabKey !== null) {
125 | this.setState({ selectedTabKey: focusedTabKey });
126 | }
127 | };
128 |
129 | setTabsDimensions = () => {
130 | if (!this.tabsWrapper.current) {
131 | // it shouldn't happen ever. Just a paranoic check
132 | return;
133 | }
134 |
135 | const { tabDimensions } = this.state;
136 |
137 | // initial wrapper width calculation
138 | const blockWidth = this.tabsWrapper.current.offsetWidth;
139 |
140 | // calculate width and offset for each tab
141 | let tabsTotalWidth = 0;
142 | const tabDimensionsNext = {};
143 | Object.keys(this.tabRefs).forEach(key => {
144 | if (this.tabRefs[key]) {
145 | const tabKey = key.replace(tabPrefix, '');
146 | const width = this.tabRefs[key].tab.offsetWidth;
147 | if (width) {
148 | tabDimensionsNext[tabKey] = { width, offset: tabsTotalWidth };
149 | } else {
150 | tabDimensionsNext[tabKey] = tabDimensions[tabKey];
151 | }
152 |
153 | tabsTotalWidth += tabDimensionsNext[tabKey].width;
154 | }
155 | });
156 |
157 | this.setState({ tabDimensions: tabDimensionsNext, tabsTotalWidth, blockWidth });
158 | };
159 |
160 | getTabs = () => {
161 | const { showMore, transform, transformWidth, items, allowRemove, removeActiveOnly, onRemove } = this.props;
162 | const { blockWidth, tabsTotalWidth, tabDimensions, showMoreWidth } = this.state;
163 | const selectedTabKey = this.getSelectedTabKey();
164 | const collapsed = blockWidth && transform && blockWidth < transformWidth;
165 |
166 | let tabIndex = 0;
167 | let availableWidth = blockWidth - (tabsTotalWidth > blockWidth ? showMoreWidth : 0);
168 |
169 | return items.reduce(
170 | (result, item, index) => {
171 | const { key = index, title, content, getContent, disabled, tabClassName, panelClassName } = item;
172 |
173 | const selected = selectedTabKey === key;
174 | const payload = { tabIndex, collapsed, selected, disabled, key };
175 | const tabPayload = {
176 | ...payload,
177 | title,
178 | onRemove: evt => {
179 | if (typeof onRemove === 'function') {
180 | onRemove(key, evt);
181 | }
182 | },
183 | allowRemove: allowRemove && (!removeActiveOnly || selected),
184 | className: tabClassName,
185 | };
186 |
187 | const panelPayload = {
188 | ...payload,
189 | content,
190 | getContent,
191 | className: panelClassName,
192 | };
193 |
194 | const tabWidth = tabDimensions[key] ? tabDimensions[key].width : 0;
195 |
196 | tabIndex += 1;
197 |
198 | /* eslint-disable no-param-reassign */
199 | if (
200 | // don't need to `Show more` button
201 | !showMore ||
202 | // initial call
203 | !blockWidth ||
204 | // collapsed mode
205 | collapsed ||
206 | // all tabs are fit into the block
207 | blockWidth > tabsTotalWidth ||
208 | // current tab fit into the block
209 | availableWidth - tabWidth > 0
210 | ) {
211 | result.tabsVisible.push(tabPayload);
212 | } else {
213 | result.tabsHidden.push(tabPayload);
214 | if (selected) result.isSelectedTabHidden = true;
215 | }
216 | /* eslint-enable no-param-reassign */
217 |
218 | result.panels[key] = panelPayload; // eslint-disable-line no-param-reassign
219 | availableWidth -= tabWidth;
220 |
221 | return result;
222 | },
223 | { tabsVisible: [], tabsHidden: [], panels: {}, isSelectedTabHidden: false },
224 | );
225 | };
226 |
227 | getTabProps = ({ title, key, selected, collapsed, tabIndex, disabled, className, onRemove, allowRemove }) => ({
228 | selected,
229 | allowRemove,
230 | children: title,
231 | key: tabPrefix + key,
232 | id: tabPrefix + key,
233 | ref: e => (this.tabRefs[tabPrefix + key] = e),
234 | originalKey: key,
235 | onClick: this.onChangeTab,
236 | onFocus: this.onFocusTab,
237 | onBlur: this.onBlurTab,
238 | onRemove,
239 | panelId: panelPrefix + key,
240 | classNames: this.getClassNamesFor('tab', {
241 | selected,
242 | collapsed,
243 | tabIndex,
244 | disabled,
245 | className,
246 | }),
247 | });
248 |
249 | getPanelProps = ({ key, content, getContent, className }, isHidden) => ({
250 | getContent,
251 | children: content,
252 | key: panelPrefix + key,
253 | id: panelPrefix + key,
254 | tabId: tabPrefix + key,
255 | classNames: this.getClassNamesFor('panel', { className, isHidden }),
256 | isHidden,
257 | });
258 |
259 | getShowMoreProps = (isShown, isSelectedTabHidden, showMoreLabel) => ({
260 | onShowMoreChanged: this.showMoreChanged,
261 | isShown,
262 | label: showMoreLabel,
263 | hasChildSelected: isSelectedTabHidden,
264 | });
265 |
266 | getClassNamesFor = (type, { selected, collapsed, tabIndex, disabled, className = '', isHidden }) => {
267 | const { tabClass, panelClass } = this.props;
268 | switch (type) {
269 | case 'tab':
270 | return cs('RRT__tab', className, tabClass, {
271 | 'RRT__tab--first': !tabIndex,
272 | 'RRT__tab--selected': selected,
273 | 'RRT__tab--disabled': disabled,
274 | 'RRT__tab--collapsed': collapsed,
275 | });
276 | case 'panel':
277 | return cs('RRT__panel', className, panelClass, { 'RRT__panel--hidden': isHidden });
278 | default:
279 | return '';
280 | }
281 | };
282 |
283 | getSelectedTabKey = () => {
284 | const { items } = this.props;
285 | const { selectedTabKey } = this.state;
286 |
287 | if (typeof selectedTabKey === 'undefined') {
288 | if (!items[0]) {
289 | return undefined;
290 | }
291 |
292 | return items[0].key || 0;
293 | }
294 |
295 | return selectedTabKey;
296 | };
297 |
298 | getIsCollapsed = () => {
299 | const { transform, transformWidth } = this.props;
300 | const { blockWidth } = this.state;
301 | return blockWidth && transform && blockWidth < transformWidth;
302 | };
303 |
304 | showMoreChanged = element => {
305 | if (!element) {
306 | return;
307 | }
308 |
309 | const { showMoreWidth } = this.state;
310 | const { offsetWidth } = element;
311 | if (showMoreWidth === offsetWidth) {
312 | return;
313 | }
314 |
315 | this.setState({
316 | showMoreWidth: offsetWidth,
317 | });
318 | };
319 |
320 | getExpandedTabs = (panels, selectedTabKey, isCollapsed) => {
321 | const { unmountOnExit } = this.props;
322 | if (isCollapsed) {
323 | return undefined;
324 | }
325 |
326 | if (!unmountOnExit) {
327 | // render all tabs if unmountOnExit === false (inactive are hidden)
328 | return Object.keys(panels).map(key => (
329 |
330 | ));
331 | }
332 |
333 | if (panels[selectedTabKey]) {
334 | // render only active tab if unmountOnExit === true
335 | return ;
336 | }
337 |
338 | return undefined;
339 | };
340 |
341 | render() {
342 | const { showInkBar, containerClass, tabsWrapperClass, showMore, transform, showMoreLabel, unmountOnExit } =
343 | this.props;
344 | const { tabDimensions } = this.state;
345 | const { tabsVisible, tabsHidden, panels, isSelectedTabHidden } = this.getTabs();
346 | const isCollapsed = this.getIsCollapsed();
347 | const selectedTabKey = this.getSelectedTabKey();
348 | const selectedTabDimensions = tabDimensions[selectedTabKey] || {};
349 |
350 | const containerClasses = cs('RRT__container', containerClass);
351 | const tabsClasses = cs('RRT__tabs', tabsWrapperClass, { RRT__accordion: isCollapsed });
352 |
353 | const handleResize = showMore || transform;
354 |
355 | return (
356 |
362 | {() => (
363 |
364 |
365 | {tabsVisible.reduce((result, tab) => {
366 | result.push();
367 |
368 | if (isCollapsed && (!unmountOnExit || selectedTabKey === tab.key)) {
369 | result.push();
370 | }
371 | return result;
372 | }, [])}
373 |
374 | {!isCollapsed && (
375 |
376 | {tabsHidden.map(tab => (
377 |
378 | ))}
379 |
380 | )}
381 |
382 |
383 | {showInkBar && !isCollapsed && !isSelectedTabHidden && (
384 |
385 | )}
386 |
387 | {this.getExpandedTabs(panels, selectedTabKey, isCollapsed)}
388 |
389 | )}
390 |
391 | );
392 | }
393 | }
394 |
395 | Tabs.propTypes = {
396 | /* eslint-disable react/no-unused-prop-types */
397 | // list of tabs
398 | items: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
399 | /* eslint-enable react/no-unused-prop-types */
400 | // selected tab key
401 | selectedTabKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
402 | // show 'X' and remove tab
403 | allowRemove: PropTypes.bool,
404 | // show 'X' closing element only for active tab
405 | removeActiveOnly: PropTypes.bool,
406 | // move tabs to the special `Show more` tab if they don't fit into a screen
407 | showMore: PropTypes.bool,
408 | // materialUI-like rail under the selected tab
409 | showInkBar: PropTypes.bool,
410 | // transform to the accordion on small screens
411 | transform: PropTypes.bool,
412 | // tabs will be transformed to accodrion for screen sizes below `transformWidth`px
413 | transformWidth: PropTypes.number,
414 | // beforeChange callback: return false to prevent tab change
415 | beforeChange: PropTypes.func,
416 | // onChange active tab callback
417 | onChange: PropTypes.func,
418 | // onRemove callback
419 | onRemove: PropTypes.func,
420 | // frequency of onResize recalculation fires
421 | resizeThrottle: PropTypes.number,
422 | // unmounts the tab when it gets inactive (unselected)
423 | unmountOnExit: PropTypes.bool,
424 | // classnames
425 | containerClass: PropTypes.string,
426 | tabsWrapperClass: PropTypes.string,
427 | tabClass: PropTypes.string,
428 | panelClass: PropTypes.string,
429 | // optional external id. Force rerender when it changes
430 | // eslint-disable-next-line react/forbid-prop-types
431 | uid: PropTypes.any,
432 | // labels
433 | showMoreLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
434 | };
435 |
436 | Tabs.defaultProps = {
437 | items: [],
438 | uid: undefined,
439 | selectedTabKey: undefined,
440 | showMore: true,
441 | showInkBar: false,
442 | allowRemove: false,
443 | removeActiveOnly: false,
444 | transform: true,
445 | transformWidth: 800,
446 | resizeThrottle: 100,
447 | containerClass: undefined,
448 | tabsWrapperClass: undefined,
449 | tabClass: undefined,
450 | panelClass: undefined,
451 | showMoreLabel: '...',
452 | unmountOnExit: true,
453 | beforeChange: undefined,
454 | onChange: () => null,
455 | onRemove: () => null,
456 | };
457 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .RRT__container {
2 | position: relative;
3 | }
4 |
5 | /****************************/
6 | /******** tab styles ********/
7 | /****************************/
8 | .RRT__tabs {
9 | display: flex;
10 | flex-wrap: wrap;
11 | }
12 |
13 | .RRT__accordion {
14 | flex-direction: column;
15 | }
16 |
17 | .RRT__tab {
18 | background: #eee;
19 | border-style: solid;
20 | border-color: #ddd;
21 | border-width: 1px 1px 1px 0;
22 | cursor: pointer;
23 | z-index: 1;
24 | white-space: nowrap;
25 | padding: 0.7em 1em;
26 | }
27 |
28 | .RRT__tab:focus {
29 | outline: 0;
30 | background-color: #e6e6e6;
31 | }
32 |
33 | .RRT__accordion .RRT__tab {
34 | border-left-width: 1px;
35 | }
36 |
37 | .RRT__tab--first {
38 | border-left-width: 1px;
39 | }
40 |
41 | .RRT__tab--selected {
42 | background: #fff;
43 | border-color: #ddd #ddd #fff;
44 | }
45 |
46 | .RRT__tab--selected:focus {
47 | background-color: #fff;
48 | }
49 |
50 | .RRT__tab--disabled {
51 | opacity: 0.5;
52 | cursor: not-allowed;
53 | }
54 |
55 | .RRT__tab:focus {
56 | z-index: 2;
57 | }
58 |
59 | .RRT__tab--selected .RRT__removable {
60 | position: relative;
61 | }
62 |
63 | .RRT__tab--selected .RRT__removable-text {
64 | margin-right: 10px;
65 | }
66 |
67 | .RRT__tab--selected .RRT__removable-icon {
68 | position: absolute;
69 | font-size: 18px;
70 | right: 0.5em;
71 | top: 0.2em;
72 | }
73 |
74 | /****************************/
75 | /********* panel styles *****/
76 | /****************************/
77 | .RRT__panel {
78 | margin-top: -1px;
79 | padding: 1em;
80 | border: 1px solid #ddd;
81 | }
82 |
83 | .RRT__panel--hidden {
84 | display: none;
85 | }
86 |
87 | .RRT__accordion .RRT__panel {
88 | margin-top: 0;
89 | }
90 |
91 | /****************************/
92 | /******* showmore control ***/
93 | /****************************/
94 | .RRT__showmore {
95 | background: #eee;
96 | border: 1px solid #ddd;
97 | cursor: pointer;
98 | z-index: 1;
99 | white-space: nowrap;
100 | margin-left: -1px;
101 | position: relative;
102 | }
103 |
104 | .RRT__showmore--selected {
105 | background: white;
106 | border-bottom: none;
107 | }
108 |
109 | .RRT__showmore-label {
110 | padding: 0.7em 1em;
111 | position: relative;
112 | bottom: -1px;
113 | z-index: 1;
114 | }
115 |
116 | .RRT__showmore-label--selected {
117 | background-color: #eee;
118 | }
119 |
120 | .RRT__showmore-list {
121 | position: absolute;
122 | right: -1px;
123 | top: 100%;
124 | display: none;
125 | }
126 |
127 | .RRT__showmore-list--opened {
128 | display: block;
129 | }
130 |
131 | /****************************/
132 | /********** inkbar **********/
133 | /****************************/
134 | .RRT__inkbar-wrapper {
135 | width: 100%;
136 | }
137 |
138 | .RRT__inkbar {
139 | position: relative;
140 | bottom: 0;
141 | height: 2px;
142 | margin-top: -2px;
143 | background-color: deepskyblue;
144 | transition: left 800ms cubic-bezier(0.23, 1, 0.32, 1);
145 | z-index: 2;
146 | }
147 |
--------------------------------------------------------------------------------