├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .prettierignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── appveyor.yml
├── demo
├── .babelrc
├── public
│ └── index.html
├── src
│ ├── App.tsx
│ ├── Components
│ │ ├── CustomThemeExample.tsx
│ │ └── tabsTheme.ts
│ ├── data.tsx
│ ├── index.css
│ └── index.jsx
├── tsconfig.json
└── webpack.config.js
├── docs
├── demo.gif
├── js.svg
└── ts.svg
├── package-lock.json
├── package.json
├── packages
├── tabtab
│ ├── README.md
│ ├── dist
│ │ ├── AsyncPanel.d.ts
│ │ ├── CloseButton.d.ts
│ │ ├── DragTab.d.ts
│ │ ├── DragTabList.d.ts
│ │ ├── ExtraButton.d.ts
│ │ ├── IconSvg.d.ts
│ │ ├── Panel.d.ts
│ │ ├── PanelList.d.ts
│ │ ├── SortMethod.d.ts
│ │ ├── Tab.d.ts
│ │ ├── TabList.d.ts
│ │ ├── TabListElement.d.ts
│ │ ├── TabListModal.d.ts
│ │ ├── Tabs.d.ts
│ │ ├── helpers
│ │ │ ├── delete.d.ts
│ │ │ └── move.d.ts
│ │ ├── index.d.ts
│ │ ├── index.js
│ │ ├── styledElements.d.ts
│ │ └── utils
│ │ │ ├── countTab.d.ts
│ │ │ └── isType.d.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── AsyncPanel.tsx
│ │ ├── CloseButton.tsx
│ │ ├── DragTab.tsx
│ │ ├── DragTabList.tsx
│ │ ├── ExtraButton.tsx
│ │ ├── IconSvg.tsx
│ │ ├── Panel.tsx
│ │ ├── PanelList.tsx
│ │ ├── SortMethod.tsx
│ │ ├── Tab.tsx
│ │ ├── TabList.tsx
│ │ ├── TabListElement.tsx
│ │ ├── TabListModal.tsx
│ │ ├── Tabs.tsx
│ │ ├── helpers
│ │ │ ├── delete.ts
│ │ │ └── move.ts
│ │ ├── index.ts
│ │ ├── styledElements.tsx
│ │ ├── styles
│ │ │ └── modal.css
│ │ └── utils
│ │ │ ├── countTab.tsx
│ │ │ └── isType.ts
│ └── tsconfig.json
└── themes
│ ├── dist
│ ├── bootstrap
│ │ └── index.d.ts
│ ├── bulma
│ │ └── index.d.ts
│ ├── index.d.ts
│ ├── index.js
│ └── material-design
│ │ └── index.d.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── bootstrap
│ │ └── index.ts
│ ├── bulma
│ │ └── index.ts
│ ├── index.ts
│ └── material-design
│ │ └── index.ts
│ └── tsconfig.json
├── prettier.config.js
├── rollup.config.js
├── test
├── AsyncPanel.test.js
├── DragTabList.test.js
├── Panel.test.js
├── PanelList.test.js
├── SortMethod.test.js
├── Tab.test.js
├── TabList.test.js
├── TabModal.test.js
├── Tabs.test.js
├── __snapshots__
│ ├── DragTabList.test.js.snap
│ ├── Panel.test.js.snap
│ ├── PanelList.test.js.snap
│ ├── Tab.test.js.snap
│ ├── TabList.test.js.snap
│ ├── TabModal.test.js.snap
│ └── Tabs.test.js.snap
├── enzyme-setup.js
├── helpers
│ └── delete.test.js
├── index.test.js
├── shim.js
├── tabListTest.js
└── utils
│ ├── countTab.test.js
│ └── isType.test.js
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": ["@typescript-eslint", "react", "unused-imports"],
5 | "env": {
6 | "browser": true,
7 | "node": true,
8 | "es6": true
9 | },
10 | "extends": [
11 | "plugin:react/recommended",
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/eslint-recommended",
14 | "plugin:@typescript-eslint/recommended"
15 | ],
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "unused-imports/no-unused-imports": "warn",
19 | "react/prop-types": "off",
20 | "@typescript-eslint/no-empty-function": "off",
21 | "react/display-name": "off",
22 | "@typescript-eslint/no-var-requires": "off",
23 | "@typescript-eslint/no-unused-vars": "off",
24 | "@typescript-eslint/no-explicit-any": "off"
25 | },
26 | "settings": {
27 | "react": {
28 | "version": "detect"
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | _gh-pages
3 | lib
4 | yarn.lock
5 | .DS_Store
6 | bundle-stats.html
7 | coverage
8 | .vscode
9 | demo/__build__
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/*.map
3 | example
4 | src
5 | scss
6 | test.js
7 | server.js
8 | webpack*
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 8
4 | cache:
5 | yarn: true
6 | before_install:
7 | yarn add codecov
8 | script:
9 | - npm run flow
10 | - npm test -- --coverage
11 | after_success:
12 | codecov
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 3.2.0
4 |
5 | - Refactored drag-n-drop sort based on **@dnd-kit** instead of deprecated **react-sortable-hoc**
6 |
7 | ## 3.1.0
8 |
9 | - Implemented modal based on `react-modal`
10 |
11 | ## 3.0.0
12 |
13 | - Forked original repo and refactored to TypeScript
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 onmotion
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-tabtab-next
2 |
3 | [](https://www.npmjs.com/package/@react-tabtab-next/tabtab)
4 | [](https://www.npmjs.com/package/@react-tabtab-next/tabtab)
5 | [](https://www.npmjs.com/package/@react-tabtab-next/tabtab)
6 |
7 | ## A mobile support, draggable, editable and api based Tab for ReactJS
8 |
9 | **(!) This lib based on [react-tabtab](https://github.com/ctxhou/react-tabtab) but refactored using Typescript and replacing some deprecated libs**
10 |
11 |
12 |
13 | ### [Demo Page](https://onmotion.github.io/react-tabtab-next/)
14 |
15 | ### [Demo Playground](https://codesandbox.io/s/react-tabtab-next-yk4moo)
16 |
17 |
18 |
19 |
20 |
21 | for build a local playground run
22 |
23 | ```bash
24 | npm run demo
25 | ```
26 |
27 | also here Codesandbox [playground](https://codesandbox.io/s/react-tabtab-next-yk4moo)
28 |
29 | ## Features
30 |
31 | - **Mobile supported** — Touch support. Easy to use on mobile device
32 | - **Draggable tab** — Support drag and drop tab
33 | - **Add & Delete** — Tab can be added and deleted
34 | - **Async content** — Lazy load panel content
35 | - **Customizable style** — Based on `styled-components`, super easy to customize tab style
36 | - **API based** — All actions are controllable
37 | - **ARIA accessible**
38 |
39 | ## Table of Contents
40 |
41 | - [Installation](#installation)
42 | - [Usage](#usage)
43 | - [Minimal setup](#minimal-setup)
44 | - [Draggable tab](#draggable-tab)
45 | - [Async Panel](#async-panel)
46 | - [Another Examples](#another-examples)
47 | - [Components / Api](#components--api)
48 | - [Customize style](#customize-style)
49 | - [Development](#development)
50 | - [License](#license)
51 |
52 |
53 |
54 | ## Installation
55 |
56 | Install it with npm or yarn
57 |
58 | ```sh
59 | npm install @react-tabtab-next/tabtab --save
60 | ```
61 |
62 | Then, import the module by module bundler like `webpack`, `browserify`
63 |
64 | ```js
65 | // es6
66 | import { Tabs, DragTabList, PanelList, Panel, ExtraButton } from '@react-tabtab-next/tabtab';
67 |
68 | // not using es6
69 | var Tabtab = require('react-tabtab');
70 | var Tabs = Tabtab.Tabs;
71 | ```
72 |
73 | ## Usage
74 |
75 | React-tabtab is a tab component with highly customization. You can create a tab in simply setting. You also can create a tab system full with `draggable`, `async loading`, `close and create button`.
76 | All the actions are api based. It means there is `no state` in the component. Developers have full control.
77 |
78 | ### Minimal setup
79 |
80 | ```js
81 | import React from 'react';
82 | import { Tabs, Panel, Tab, TabList, PanelList } from '@react-tabtab-next/tabtab';
83 |
84 | export const Example = () => {
85 | return (
86 |
87 |
88 | Tab1
89 | Tab2
90 |
91 |
92 | Content1
93 | Content2
94 |
95 |
96 | );
97 | };
98 | ```
99 |
100 | It's simple to use. Zero configuration!
101 |
102 | ### Draggable tab
103 |
104 | ```js
105 | import React, { Component } from 'react';
106 | import { Tabs, DragTabList, PanelList, Panel, Tab, helpers } from '@react-tabtab-next/tabtab';
107 |
108 | const makeData = (number, titlePrefix = 'Tab') => {
109 | const data = [];
110 | for (let i = 0; i < number; i++) {
111 | data.push({
112 | title: `${titlePrefix} ${i}`,
113 | content: Content {i}
,
114 | });
115 | }
116 | return data;
117 | };
118 |
119 | export default class Drag extends Component {
120 | constructor(props) {
121 | super(props);
122 | this.handleTabChange = this.handleTabChange.bind(this);
123 | this.handleTabSequenceChange = this.handleTabSequenceChange.bind(this);
124 | const tabs = makeData(10, 'Some Tab');
125 | this.state = {
126 | activeIndex: 0,
127 | tabs,
128 | };
129 | }
130 |
131 | handleTabChange(index) {
132 | this.setState({ activeIndex: index });
133 | }
134 |
135 | handleTabSequenceChange({ oldIndex, newIndex }) {
136 | const { tabs } = this.state;
137 | const updateTabs = helpers.simpleSwitch(tabs, oldIndex, newIndex);
138 | this.setState({ tabs: updateTabs, activeIndex: newIndex });
139 | }
140 |
141 | render() {
142 | const { tabs, activeIndex } = this.state;
143 | const tabsTemplate = [];
144 | const panelTemplate = [];
145 | tabs.forEach((tab, index) => {
146 | tabsTemplate.push({tab.title} );
147 | panelTemplate.push({tab.content} );
148 | });
149 | return (
150 |
156 | {tabsTemplate}
157 | {panelTemplate}
158 |
159 | );
160 | }
161 | }
162 |
163 | ReactDOM.render( , document.getElementById('root'));
164 | ```
165 |
166 | Based on above example, the different to implement `normal tab` or `drag tab` is using different wrapper and child.
167 |
168 | And all the actions are controllable. You can customize your switch action. But if you don't want to write customized switch logic, you can directly use `import {simpleSwitch} from 'react-tabtab/lib/helpers/move'` this built-in function.
169 |
170 | ### normal tab
171 |
172 | ```js
173 |
174 |
175 | Tab1
176 | Tab2
177 |
178 |
179 | Content1
180 | Content2
181 |
182 |
183 | ```
184 |
185 | ### Sortable tabs (+ ExtraButton)
186 |
187 | ```js
188 | {
196 | console.log(e);
197 | }}
198 | >
199 | +
200 |
201 | }
202 | >
203 |
204 | Tab1
205 | Tab2
206 |
207 |
208 | Content1
209 | Content2
210 |
211 |
212 | ```
213 |
214 | ### Async Panel
215 |
216 | In some case, if the data is large or we want to save the bandwidth, lazy loading the content is possible solution. You can use `AsyncPanel` to laze load panel content.
217 | Moreover, you can mix lazy load panel with normal panel!
218 |
219 | ```js
220 | import React from 'react';
221 | import { Tabs, Panel, Tab, TabList, PanelList, AsyncPanel } from '@react-tabtab-next/tabtab';
222 |
223 | const AsyncTabsExmple = () => {
224 | const loadContentFunc = (callback) => {
225 | setTimeout(() => {
226 | callback(null, 'some content');
227 | }, 1000);
228 | };
229 | return (
230 |
231 |
232 | Tab1
233 | Tab2
234 |
235 |
236 | Content1
237 | {JSON.stringify(data)}
}
240 | renderLoading={() => Loading...
}
241 | cache={true}
242 | />
243 |
244 |
245 | );
246 | };
247 |
248 | export default AsyncTabsExmple;
249 | ```
250 |
251 | To implement lazy loading, use `AsyncPanel` to wrap your panel content. Remember to provide `loadContent`, `render`, `renderLoading` these 3 props.
252 |
253 | In `loadContent` props, both `callback` and `promise` type are supported.
254 |
255 | If you use `callback`, remember to call `callback` when finish async loading.
256 |
257 | If you use `promise`, need to return promise action.
258 |
259 | When data is loading, the panel content will show `renderLoading` component.
260 |
261 | After finishing loading data, the panel content will show `render` component and react-tabtab will pass the `loadContent` result as first parameter. So you can customize the component of panel content.
262 |
263 | ### Another Examples
264 |
265 | More code examples are avalable [here](https://github.com/onmotion/react-tabtab-next/blob/master/demo/src/App.tsx).
266 |
267 | ## Components / Api
268 |
269 | ### <Tabs />
270 |
271 | ` ` is the main component of `react-tabtab`. Most of the api is passed from it.
272 |
273 |
274 |
275 |
276 | props
277 | type
278 | default
279 |
280 |
281 |
282 | activeIndex
283 | number
284 | null
285 | control current activeIndex. You need to pass new activeIndex value if you want to show different tab.
286 |
287 |
288 | defaultIndex
289 | number
290 | 0
291 | default selected index if active index is not provided
292 |
293 |
294 | showModalButton
295 | boolean
number
296 | 4
297 |
298 |
299 | true : always show button
300 | false : always hide button
301 | [number] : when number of tab >= [number]
, show button
302 |
303 |
304 |
305 |
306 | showArrowButton
307 | auto
boolean
308 | auto
309 |
310 | auto : detect tab width, if they exceed container, show button
311 | true : always show button
312 | false : always hide button
313 |
314 |
315 |
316 | ExtraButton
317 | React Node
318 | null
319 |
320 | customize extra button content, example: `+` button
321 |
322 |
323 |
324 | onTabChange
325 | (tabIndex) => {}
326 | null
327 |
328 | return tabIndex is clicked
329 | You can use this api with activeIndex
. When user click tab, update activeIndex
.
330 |
331 |
332 |
333 | onTabSequenceChange
334 | (oldIndex, newIndex) => {}
335 | null
336 |
337 | return changed oldIndex and newIndex value
338 | With this api, you can do switch tab very easily.
339 | Note: This api is only called by <DragTabList/>
340 |
341 |
342 |
343 | onTabClose
344 | (index) => {}
345 | null
346 |
347 | When user click close button , this api will return the clicked close button index.
348 |
349 |
350 |
351 | customStyle
352 |
353 |
354 | {
355 | TabList: React.Element,
356 | Tab: React.Element,
357 | Panel: React.Element,
358 | ActionButton: React.Element
359 | }
360 |
361 | theme
362 |
363 | customized tab style component
364 |
365 |
366 |
367 |
368 |
369 | ### <TabList />
370 |
371 | Use to wrap ` `.
372 |
373 | ### <DragTabList />
374 |
375 | Use to wrap ` `.
376 |
377 | ### <Tab />
378 |
379 | Normal Tab. Show the children component on tab.
380 |
381 |
382 |
383 |
384 | props
385 | type
386 | default
387 |
388 |
389 |
390 | closable
391 | boolean
392 | false
393 | whether to show close button
394 |
395 |
396 |
397 |
398 | **Example**
399 |
400 | ```js
401 |
402 |
403 | map tab
404 |
405 | ```
406 |
407 | ### <PanelList/ >
408 |
409 | Use to wrap ` `
410 |
411 | ### <Panel />
412 |
413 | Tab content.
414 |
415 | ### <AsyncPanel />
416 |
417 | Lazy loading panel content.
418 |
419 |
420 |
421 |
422 | props
423 | type
424 | default
425 |
426 |
427 |
428 | loadContent *
429 |
430 | (cb) => cb(error, data)
or
431 | (cb) => Promise
432 |
433 | null
434 | when loadContent finish, call the callback or you can return promise
435 |
436 |
437 | render *
438 |
439 | (data) => Component
440 |
441 | null
442 | when finish loading data, render this component
443 |
444 |
445 | renderLoading *
446 |
447 | () => Component
448 |
449 | null
450 | when it is loading data, render this component
451 |
452 |
453 | cache
454 |
455 | boolean
456 |
457 | true
458 | should cache the data
459 |
460 |
461 |
462 |
463 | ## Customize style
464 |
465 | `react-tabtab-next` is based on `styled-components`. Therefore, it's super easy to customize the tab style.
466 |
467 | Just extend the default component style and pass it to `customStyle` props.
468 |
469 | ### Use current style
470 |
471 | Install tabtab themes
472 |
473 | ```sh
474 | npm install @react-tabtab-next/themes --save
475 | ```
476 |
477 | Available themes: `md`, `bootstrap`, `bulma`
478 |
479 | For example, if you want to use `material-design`, import the style and pass to `customStyle` props.
480 |
481 | **Example:**
482 |
483 | ```js
484 | import React, { Component } from 'react';
485 | import { Tabs, TabList, Tab, PanelList, Panel } from '@react-tabtab-next/tabtab';
486 | import { md } from '@react-tabtab-next/themes';
487 |
488 | export default class Customized extends Component {
489 | render() {
490 | return (
491 |
492 |
493 | Tab1
494 | Tab2
495 |
496 |
497 | Content1
498 | Content2
499 |
500 |
501 | );
502 | }
503 | }
504 | ```
505 |
506 | And now your tab is material design style!
507 |
508 | ### Make your own style
509 |
510 | If current theme doesn't meet your demand, follow this three steps and create a new one.
511 |
512 | - First step: import current style
513 |
514 | ```js
515 | import styled from 'styled-components';
516 | import { styled as styledTabTab } from '@react-tabtab-next/tabtab';
517 |
518 | let { TabListStyle, ActionButtonStyle, TabStyle, PanelStyle } = styledTabTab;
519 | ```
520 |
521 | - Second: extend style and export it
522 |
523 | ```js
524 | import styled from 'styled-components';
525 | import { styled as themeStyled } from '@react-tabtab-next/tabtab';
526 |
527 | let { TabList, ActionButton, Tab, Panel } = themeStyled;
528 |
529 | TabList = styled(TabList)`
530 | background-color: transparent;
531 | line-height: 1.2;
532 | border: 0;
533 | `;
534 |
535 | Tab = styled(Tab)`
536 | padding: 1px 10px;
537 | position: relative;
538 | font-size: 12px;
539 | text-transform: uppercase;
540 | border: 0;
541 | background: transparent;
542 | ${(props) => {
543 | return props.active && !props.vertical
544 | ? `
545 | border-bottom: 2px solid #ce93d8;
546 | `
547 | : null;
548 | }}
549 | &:hover .tab-label_close-button {
550 | opacity: 1;
551 | }
552 | &:hover {
553 | color: unset;
554 | background: #89898920;
555 | }
556 | `;
557 |
558 | ActionButton = styled(ActionButton)`
559 | background-color: transparent;
560 | border-radius: 0;
561 | border: none;
562 | opacity: 0.3;
563 | transition: opacity 0.2s;
564 | & svg {
565 | font-size: 21px;
566 | padding: 0;
567 | }
568 | &:hover {
569 | opacity: 1;
570 | }
571 | `;
572 |
573 | Panel = styled(Panel)``;
574 |
575 | export { TabList, ActionButton, Tab, Panel };
576 | ```
577 |
578 | - Last: import your style and use it!
579 |
580 | When you finish the new `@react-tabtab-next/theme` style, feel free to add it to `theme/` folder and send PR!
581 |
582 | ## Development
583 |
584 | ```bash
585 | npm i
586 | npm run demo
587 | ```
588 |
589 | or
590 |
591 | ```bash
592 | yarn install
593 | yarn demo
594 | ```
595 |
596 | Build the bundle
597 |
598 | ```bash
599 | npm i
600 | ```
601 |
602 | ## License
603 |
604 | MIT
605 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: "{build}"
2 |
3 | environment:
4 | nodejs_version: "6"
5 |
6 | install:
7 | - ps: Install-Product node $env:nodejs_version x64
8 | - yarn
9 |
10 | cache:
11 | - '%LOCALAPPDATA%/Yarn'
12 |
13 | test_script:
14 | - node --version
15 | - npm --version
16 | - npm run flow
17 | - npm test
18 |
19 | # Don't actually build.
20 | build: off
21 |
--------------------------------------------------------------------------------
/demo/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-typescript", ["@babel/preset-react", { "runtime": "automatic" }]]
3 | }
4 |
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-tabtab-next
8 |
9 |
10 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/demo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, useState } from 'react';
2 | import { makeData } from './data';
3 | import {
4 | Tabs,
5 | Panel,
6 | DragTabList,
7 | PanelList,
8 | helpers,
9 | Tab,
10 | TabList,
11 | ExtraButton,
12 | AsyncPanel,
13 | } from '../../packages/tabtab/src';
14 | import { md, bootstrap, bulma } from '../../packages/themes/src';
15 | import { CustomThemeExample } from './Components/CustomThemeExample';
16 |
17 | export default function App() {
18 | const [activeTab, setActiveTab] = useState(0);
19 | const [tabs, setTabs] = useState(makeData(15, 'Some Tab'));
20 |
21 | const closableTabItems = useMemo(() => {
22 | return tabs.map((tab, index) => {
23 | return (
24 |
25 | {tab.title}
26 |
27 | );
28 | });
29 | }, [tabs]);
30 |
31 | const tabItems = useMemo(() => {
32 | return tabs.map((tab, index) => {
33 | return {tab.title} ;
34 | });
35 | }, [tabs]);
36 |
37 | const shortTabItems = useMemo(() => {
38 | return makeData(3, 'Tab').map((tab, index) => {
39 | return {tab.title} ;
40 | });
41 | }, []);
42 |
43 | const panelItems = useMemo(() => {
44 | return tabs.map((tab, index) => {
45 | return {tab.content} ;
46 | });
47 | }, [tabs]);
48 |
49 | const handleOnTabSequenceChange = useCallback(({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
50 | console.log({ oldIndex, newIndex });
51 | setTabs((tabs) => helpers.simpleSwitch(tabs, oldIndex, newIndex));
52 | setActiveTab(newIndex);
53 | }, []);
54 |
55 | const handleOnTabChange = useCallback((i: number) => {
56 | console.log('select tab', i);
57 | setActiveTab(i);
58 | }, []);
59 |
60 | function loadContentFunc(callback: (err: any, data: any) => void) {
61 | setTimeout(() => {
62 | callback(null, 'some async content');
63 | }, 1000);
64 | }
65 |
66 | return (
67 |
68 |
69 |
70 |
Material draggable
71 |
{
79 | setTabs((prev) => {
80 | const newTabs = [...prev];
81 | const newItem = makeData(1, 'New Tab ' + (newTabs.length + 1), false)[0];
82 | newTabs.push(newItem);
83 | return newTabs;
84 | });
85 | setActiveTab(tabs.length);
86 | }}
87 | >
88 | +
89 |
90 | }
91 | >
92 | {tabItems}
93 | {panelItems}
94 |
95 |
96 |
97 |
Bootstrap closable
98 |
{
100 | console.log('close', i);
101 | setTabs((prev) => prev.filter((_, idx) => idx !== i));
102 | }}
103 | showModalButton={false}
104 | customStyle={bootstrap}
105 | activeIndex={activeTab}
106 | onTabChange={handleOnTabChange}
107 | onTabSequenceChange={handleOnTabSequenceChange}
108 | >
109 | {closableTabItems}
110 | {panelItems}
111 |
112 |
113 |
114 |
Async data loading
115 |
116 |
117 | Static Tab
118 | Async Tab
119 | Async Tab Cache
120 |
121 |
122 | Static content
123 | {JSON.stringify(data)}
}
126 | renderLoading={() => Loading...
}
127 | cache={false}
128 | />
129 | {JSON.stringify(data)}
}
132 | renderLoading={() => Loading...
}
133 | cache={true}
134 | />
135 |
136 |
137 |
138 |
139 |
Custom theme example
140 |
tabItems.length - 1 ? tabItems.length - 1 : activeTab}
142 | onTabChange={handleOnTabChange}
143 | panelItems={panelItems}
144 | tabItems={tabItems}
145 | onTabSequenceChange={handleOnTabSequenceChange}
146 | >
147 |
148 |
149 |
Bulma minimal
150 |
shortTabItems.length - 1 ? shortTabItems.length - 1 : activeTab}
153 | onTabChange={handleOnTabChange}
154 | >
155 | {shortTabItems}
156 | {panelItems}
157 |
158 |
159 |
160 |
161 | );
162 | }
163 |
--------------------------------------------------------------------------------
/demo/src/Components/CustomThemeExample.tsx:
--------------------------------------------------------------------------------
1 | import { TabsProps } from '../../../packages/tabtab/src/Tabs';
2 | import React, { FC, memo } from 'react';
3 | import { Tabs, DragTabList, PanelList, Tab, Panel } from '../../../packages/tabtab/src';
4 | import * as customStyle from './tabsTheme';
5 |
6 | interface IProps extends Partial {
7 | tabItems: React.ReactElement[];
8 | panelItems: React.ReactElement[];
9 | }
10 |
11 | export const CustomThemeExample: FC = memo(({ tabItems, panelItems, ...rest }) => {
12 | return (
13 | <>
14 |
15 | {tabItems}
16 | {panelItems}
17 |
18 | >
19 | );
20 | });
21 |
--------------------------------------------------------------------------------
/demo/src/Components/tabsTheme.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import { styled as themeStyled } from '@react-tabtab-next/tabtab';
4 |
5 | let { TabList, ActionButton, Tab, Panel } = themeStyled;
6 |
7 | TabList = styled(TabList)`
8 | background-color: transparent;
9 | line-height: 1.2;
10 | border: 0;
11 | `;
12 |
13 | Tab = styled(Tab)`
14 | padding: 1px 10px;
15 | position: relative;
16 | font-size: 12px;
17 | text-transform: uppercase;
18 | border: 0;
19 | background: transparent;
20 | transition: background 0.15s, border-bottom 0.15s;
21 | border-bottom: 2px solid transparent;
22 | color: unset;
23 | ${(props) => {
24 | return props.active
25 | ? `
26 | border-bottom: 2px solid #ce93d8;
27 | `
28 | : null;
29 | }}
30 | &:hover .tab-label_close-button {
31 | opacity: 1;
32 | }
33 | &:hover {
34 | color: unset;
35 | background: #89898920;
36 | }
37 | `;
38 |
39 | ActionButton = styled(ActionButton)`
40 | background-color: transparent;
41 | border-radius: 0;
42 | border: none;
43 | opacity: 0.3;
44 | transition: opacity 0.2s;
45 | & svg {
46 | font-size: 21px;
47 | padding: 0;
48 | }
49 | &:hover {
50 | opacity: 1;
51 | }
52 | `;
53 |
54 | Panel = styled(Panel)``;
55 |
56 | export { TabList, ActionButton, Tab, Panel };
57 |
--------------------------------------------------------------------------------
/demo/src/data.tsx:
--------------------------------------------------------------------------------
1 | import { LoremIpsum } from 'lorem-ipsum';
2 | import * as React from 'react';
3 |
4 | const lorem = new LoremIpsum({
5 | sentencesPerParagraph: {
6 | max: 8,
7 | min: 4,
8 | },
9 | wordsPerSentence: {
10 | max: 16,
11 | min: 4,
12 | },
13 | });
14 |
15 | export const makeData = (number: number, titlePrefix = 'Tab', useTitleCounter = true) => {
16 | const data = [];
17 | for (let i = 0; i < number; i++) {
18 | data.push({
19 | title: useTitleCounter ? `${titlePrefix} ${i + 1}` : titlePrefix,
20 | content: (
21 |
22 |
Content {i + 1}
23 |
{lorem.generateWords(15)}
24 |
25 | ),
26 | });
27 | }
28 | return data;
29 | };
30 |
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
3 | color: #34475a;
4 |
5 | background: linear-gradient(90deg, #fff 21px, transparent 1%) center,
6 | linear-gradient(#fff 21px, transparent 1%) center,
7 | #2a3340;
8 | background-size: 22px 22px;
9 | }
10 | .container {
11 | max-width: 800px;
12 | margin: auto;
13 | }
14 | .header {
15 | display: flex;
16 | justify-content: space-between;
17 | align-items: center;
18 | }
19 |
20 | h2 {
21 | margin: 10px 0;
22 | }
23 | .title {
24 | text-align: center;
25 | font-size: 1.1em;
26 | font-weight: bold;
27 |
28 | margin-top: 25px;
29 | }
30 |
31 | .example {
32 | padding: 10px 25px;
33 | box-shadow: 0 3px 35px #0000001c;
34 | border-radius: 7px;
35 | margin: 25px 0;
36 | background: #fff;
37 | }
38 |
--------------------------------------------------------------------------------
/demo/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "jsx": "react",
7 | "module": "es6",
8 | "moduleResolution": "node",
9 | "noImplicitAny": true,
10 | "preserveConstEnums": true,
11 | "target": "es5",
12 | "allowSyntheticDefaultImports": true,
13 | "noEmitOnError": true
14 | },
15 | "exclude": ["node_modules"]
16 | }
17 |
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | module.exports = {
6 | entry: path.resolve(__dirname, './src/index.jsx'),
7 | devServer: {
8 | // static: './demo/__build__',
9 | compress: !!process.env.PRODUCTION,
10 | port: 9000,
11 | open: true,
12 | },
13 | mode: 'development',
14 | output: {
15 | filename: '[name].js',
16 | path: path.resolve(__dirname, './__build__'),
17 | publicPath: process.env.PRODUCTION ? './' : '/',
18 | },
19 | plugins: [
20 | new HtmlWebpackPlugin({
21 | template: './demo/public/index.html',
22 | }),
23 | ],
24 | module: {
25 | rules: [
26 | {
27 | test: /\.jsx?$/,
28 | exclude: /node_modules/,
29 | // include: [path.resolve(__dirname, './src')],
30 | use: { loader: 'babel-loader' },
31 | },
32 | {
33 | test: /\.tsx?$/,
34 | exclude: /node_modules/,
35 | use: { loader: 'ts-loader' },
36 | },
37 | {
38 | test: /\.css$/i,
39 | use: ['style-loader', 'css-loader'],
40 | },
41 | ],
42 | },
43 |
44 | resolve: {
45 | extensions: ['.js', '.json', '.jsx', '.ts', '.tsx', '.css'],
46 | // alias: {
47 | // 'react-tabtab-next-themes': path.resolve(__dirname, '../../themes/dist/index'),
48 | // },
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onmotion/react-tabtab-next/16f369f20d6528d93a8c6dfc219e4682b07d38a8/docs/demo.gif
--------------------------------------------------------------------------------
/docs/js.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/ts.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
9 |
10 |
19 |
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-tabtab-next",
3 | "version": "3.3.0",
4 | "description": "A mobile support, draggable, editable and api based Tab for ReactJS",
5 | "workspaces": [
6 | "./packages/*"
7 | ],
8 | "scripts": {
9 | "start": "node devServer.js",
10 | "build": "npm run build --prefix packages/tabtab && npm run build --prefix packages/themes",
11 | "lint": "eslint packages/**/src/*.ts packages/**/src/*.tsx packages/**/src/*.ts packages/**/src/*.tsx",
12 | "test": "jest",
13 | "test:watch": "NODE_ENV=test npm test -- --watch",
14 | "prepublish": "npm run build",
15 | "validate": "npm ls",
16 | "demo": "npx webpack serve --config ./demo/webpack.config.js",
17 | "demo:build": "PRODUCTION=true npx webpack build --config ./demo/webpack.config.js ",
18 | "gh-pages": "rimraf demo/__build__ && npm run demo:build && npm run gh-pages:publish",
19 | "gh-pages:publish": "git-directory-deploy --directory demo/__build__ --branch gh-pages"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/onmotion/react-tabtab-next.git"
24 | },
25 | "keywords": [
26 | "react",
27 | "tabs",
28 | "drag",
29 | "react-tabtab",
30 | "react-component",
31 | "tab",
32 | "tabtab",
33 | "tabtab-next",
34 | "react-tabtab-next",
35 | "typescript",
36 | "react draggable tabs"
37 | ],
38 | "private": false,
39 | "author": {
40 | "name": "Alexandr Kozhevnikov",
41 | "email": "onmotion1@gmail.com"
42 | },
43 | "license": "MIT",
44 | "bugs": {
45 | "url": "https://github.com/onmotion/react-tabtab-next/issues"
46 | },
47 | "homepage": "https://github.com/onmotion/react-tabtab-next#readme",
48 | "dependencies": {
49 | "react": "^16.8.0 || ^17 || ^18",
50 | "react-dom": "^16.8.0 || ^17 || ^18",
51 | "styled-components": "5.3.3"
52 | },
53 | "devDependencies": {
54 | "@babel/preset-env": "^7.16.11",
55 | "@babel/preset-react": "^7.16.7",
56 | "@babel/preset-typescript": "^7.16.7",
57 | "@rollup/plugin-babel": "^5.3.1",
58 | "@rollup/plugin-commonjs": "^21.0.2",
59 | "@rollup/plugin-json": "^4.1.0",
60 | "@rollup/plugin-node-resolve": "^13.1.3",
61 | "@rollup/plugin-replace": "^4.0.0",
62 | "@rollup/plugin-typescript": "^8.3.1",
63 | "@types/estree": "^0.0.51",
64 | "@types/invariant": "^2.2.35",
65 | "@types/react": ">=16.8.0",
66 | "@types/react-modal": "^3.13.1",
67 | "@types/styled-components": "^5.1.24",
68 | "@typescript-eslint/eslint-plugin": "^5.17.0",
69 | "babel-eslint": "^10.1.0",
70 | "babel-loader": "^8.2.4",
71 | "css-loader": "^6.7.1",
72 | "enzyme-to-json": "^3.6.2",
73 | "eslint-config-google": "^0.14.0",
74 | "eslint-config-prettier": "^8.5.0",
75 | "eslint-plugin-prettier": "^4.0.0",
76 | "eslint-plugin-react": "^7.29.4",
77 | "eslint-plugin-unused-imports": "^2.0.0",
78 | "html-webpack-plugin": "^5.5.0",
79 | "jest": "^27.5.1",
80 | "lorem-ipsum": "^2.0.4",
81 | "prettier": "^2.6.1",
82 | "rimraf": "^2.6.2",
83 | "rollup": "^2.70.1",
84 | "rollup-plugin-styles": "^4.0.0",
85 | "rollup-plugin-terser": "^7.0.2",
86 | "style-loader": "^3.3.1",
87 | "ts-loader": "^9.2.8",
88 | "tslib": "^2.3.1",
89 | "typescript": "^4.6.3",
90 | "webpack": "^5.70.0",
91 | "webpack-cli": "^4.9.2",
92 | "webpack-dev-server": "^4.8.1"
93 | },
94 | "peerDependencies": {},
95 | "jest": {
96 | "setupFiles": [
97 | "./test/shim",
98 | "./test/enzyme-setup"
99 | ],
100 | "roots": [
101 | "/test/"
102 | ],
103 | "unmockedModulePathPatterns": [
104 | "node_modules/react/",
105 | "node_modules/enzyme/"
106 | ],
107 | "snapshotSerializers": [
108 | "enzyme-to-json/serializer"
109 | ]
110 | },
111 | "pre-commit": [
112 | "lint",
113 | "test"
114 | ]
115 | }
--------------------------------------------------------------------------------
/packages/tabtab/README.md:
--------------------------------------------------------------------------------
1 | # @react-tabtab-next/tabtab
2 |
3 | Main component of [react-tabtab-next](https://github.com/onmotion/react-tabtab-next)
4 |
5 | Documentation: https://github.com/onmotion/react-tabtab-next
6 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/AsyncPanel.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { PanelProps } from './Panel';
3 | declare type Props = {
4 | loadContent: (cb: (err: any, data?: any) => void) => any;
5 | render: (data: any) => React.ReactNode;
6 | renderLoading: () => React.ReactNode;
7 | CustomPanelStyle?: React.FC>;
8 | active?: boolean;
9 | index?: number;
10 | cache?: boolean;
11 | };
12 | declare type State = {
13 | isLoading: boolean;
14 | data: any;
15 | };
16 | export default class AsyncPanelComponent extends React.PureComponent {
17 | static defaultProps: {
18 | cache: boolean;
19 | };
20 | cacheData: any;
21 | constructor(props: Props);
22 | componentDidMount(): void;
23 | componentDidUpdate(prevProps: Props): void;
24 | loadPanel(): void;
25 | render(): JSX.Element;
26 | }
27 | export {};
28 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/CloseButton.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | declare type Props = {
3 | handleTabClose: (event: any) => void;
4 | };
5 | export default class CloseButton extends React.PureComponent {
6 | render(): JSX.Element;
7 | }
8 | export {};
9 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/DragTab.d.ts:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | interface Props {
3 | id: string;
4 | activeIndex?: number;
5 | index?: number;
6 | children: React.ReactNode;
7 | }
8 | declare const DragTab: FC;
9 | export default DragTab;
10 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/DragTabList.d.ts:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { TabProps } from './Tab';
3 | import { TabsProps } from './Tabs';
4 | interface IDragTabListProps {
5 | onTabSequenceChange?: TabsProps['onTabSequenceChange'];
6 | children: React.ReactNode;
7 | }
8 | declare const DragTabList: FC>;
9 | export default DragTabList;
10 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/ExtraButton.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | declare type Props = {
3 | onClick: (event: any) => void;
4 | disabled: boolean;
5 | children: React.ReactNode;
6 | };
7 | export default class ExtraButton extends React.PureComponent {
8 | static defaultProps: {
9 | disabled: boolean;
10 | };
11 | render(): JSX.Element;
12 | }
13 | export {};
14 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/IconSvg.d.ts:
--------------------------------------------------------------------------------
1 | declare const CloseIcon: () => JSX.Element;
2 | declare const LeftIcon: () => JSX.Element;
3 | declare const RightIcon: () => JSX.Element;
4 | declare const BulletIcon: () => JSX.Element;
5 | export { CloseIcon, LeftIcon, RightIcon, BulletIcon };
6 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/Panel.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | declare const PanelStyle: import("styled-components").StyledComponent<"div", any, {
3 | active: boolean;
4 | }, never>;
5 | export declare type PanelProps = {
6 | CustomPanelStyle?: React.FC>;
7 | active?: boolean;
8 | index?: number;
9 | };
10 | export default class PanelComponent extends React.PureComponent> {
11 | render(): JSX.Element;
12 | }
13 | export { PanelStyle };
14 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/PanelList.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | declare type Props = {
3 | activeIndex?: number;
4 | customStyle?: {
5 | Panel: () => void;
6 | };
7 | };
8 | export default class PanelList extends React.PureComponent> {
9 | render(): JSX.Element;
10 | }
11 | export {};
12 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/SortMethod.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { TabListProps } from './TabList';
3 | export declare type SortMathodComponentProps = {
4 | handleTabChange?: (event: any) => void;
5 | handleTabSequence?: (event: any) => void;
6 | activeIndex?: number;
7 | };
8 | export default class SortMethod extends React.PureComponent {
9 | constructor(props: SortMathodComponentProps & TabListProps);
10 | onSortEnd({ oldIndex, newIndex }: {
11 | oldIndex: number;
12 | newIndex: number;
13 | }): void;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/Tab.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | export declare type TabElementProps = React.ComponentPropsWithoutRef<'li'> & TabProps;
3 | export declare const TabElement: React.MemoExoticComponent, HTMLLIElement>, "key" | keyof React.LiHTMLAttributes> & TabProps & React.RefAttributes>>;
4 | declare const TabStyle: import("styled-components").StyledComponent, HTMLLIElement>, "key" | keyof React.LiHTMLAttributes> & TabProps & React.RefAttributes>>, any, {}, never>;
5 | export declare type TabProps = {
6 | CustomTabStyle?: React.FC>;
7 | handleTabChange?: (event: any) => void;
8 | handleTabClose?: (event: any) => void;
9 | index?: number;
10 | active?: boolean;
11 | closable?: boolean;
12 | vertical?: boolean;
13 | tabIndex?: string;
14 | };
15 | export default class Tab extends React.PureComponent> {
16 | __INTERNAL_NODE: React.ElementRef;
17 | constructor(props: TabProps);
18 | clickTab(e: React.MouseEvent): void;
19 | clickDelete(event: React.SyntheticEvent): void;
20 | render(): JSX.Element;
21 | }
22 | export { TabStyle };
23 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/TabList.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { TabListElementProps } from './TabListElement';
3 | import { TabElementProps } from './Tab';
4 | import { PanelProps } from './Panel';
5 | import { SortableContextProps } from '@dnd-kit/sortable';
6 | import { DndContextProps } from '@dnd-kit/core';
7 | export declare type TabListProps = {
8 | customStyle?: {
9 | TabList?: React.ElementType;
10 | Tab?: React.ElementType;
11 | Panel?: React.ElementType;
12 | ActionButton?: React.ElementType;
13 | };
14 | showArrowButton?: 'auto' | boolean;
15 | showModalButton?: number | boolean;
16 | handleTabChange?: (event: any) => void;
17 | handleTabSequence?: (event: any) => void;
18 | handleTabClose?: (index: number) => void;
19 | ExtraButton?: JSX.Element;
20 | activeIndex?: number;
21 | children: React.ReactNode[];
22 | sortableContextProps?: Omit;
23 | dndContextProps?: DndContextProps;
24 | };
25 | declare type State = {
26 | modalIsOpen: boolean;
27 | showArrowButton: 'auto' | boolean;
28 | showModalButton: boolean | number;
29 | };
30 | export default class TabListComponent extends React.PureComponent {
31 | listContainer: React.ElementRef<'div'>;
32 | rightArrowNode: React.ReactElement;
33 | leftArrowNode: React.ReactElement;
34 | listScroll: React.ElementRef<'ul'>;
35 | foldNode: React.ReactElement;
36 | tabRefs: React.ElementRef<'div'>[];
37 | scrollPosition: number;
38 | FoldButton: React.ElementType;
39 | ScrollButton: React.ElementType;
40 | TabList: React.ElementType;
41 | constructor(props: TabListProps);
42 | chackActiveIndexRange(): boolean;
43 | componentDidMount(): void;
44 | componentDidUpdate(prevProps: TabListProps, prevState: State): void;
45 | getTabNode(tab: HTMLDivElement & {
46 | __INTERNAL_NODE?: any;
47 | }): React.ElementRef<'div'>;
48 | unifyScrollMax(width: number): number;
49 | handleScroll(direction: 'right' | 'left'): void;
50 | scrollToIndex(index: number, rectSide: 'left' | 'right'): void;
51 | scrollToZero(): void;
52 | toggleModal(): void;
53 | isShowModalButton(): void;
54 | isShowArrowButton(): void;
55 | renderTabs(options?: any, isModal?: boolean): React.ReactElement>[];
56 | renderArrowButtons(ScrollButton: React.ElementType): JSX.Element;
57 | render(): JSX.Element;
58 | }
59 | export {};
60 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/TabListElement.d.ts:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | export interface TabListElementProps {
3 | showArrowButton?: 'auto' | boolean;
4 | showModalButton?: number | boolean;
5 | }
6 | export declare const TabListElement: FC;
7 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/TabListModal.d.ts:
--------------------------------------------------------------------------------
1 | import { DndContextProps } from '@dnd-kit/core';
2 | import { SortableContextProps } from '@dnd-kit/sortable';
3 | import { FC } from 'react';
4 | import ReactModal from 'react-modal';
5 | import './styles/modal.css';
6 | interface ITabListModalProps extends ReactModal.Props {
7 | dndContextProps?: DndContextProps;
8 | sortableContextProps?: Omit;
9 | isOpen: boolean;
10 | onRequestClose: ReactModal.Props['onRequestClose'];
11 | }
12 | export declare const TabListModal: FC;
13 | export {};
14 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/Tabs.d.ts:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from 'react';
2 | import { PanelProps } from './Panel';
3 | import { TabElementProps } from './Tab';
4 | import { TabListElementProps } from './TabListElement';
5 | export declare type TabsProps = {
6 | defaultIndex?: number;
7 | activeIndex?: number | null;
8 | showModalButton?: number | boolean;
9 | showArrowButton?: 'auto' | boolean;
10 | ExtraButton?: React.ReactNode;
11 | onTabChange?: (index: number) => void;
12 | onTabSequenceChange?: (e: {
13 | oldIndex: number;
14 | newIndex: number;
15 | }) => void;
16 | onTabClose?: (index: number) => void;
17 | customStyle?: {
18 | TabList?: React.ElementType;
19 | Tab?: React.ElementType;
20 | Panel?: React.ElementType;
21 | ActionButton?: React.ElementType;
22 | };
23 | };
24 | declare type State = {
25 | activeIndex: number;
26 | };
27 | export default class Tabs extends React.PureComponent, State> {
28 | constructor(props: TabsProps);
29 | static defaultProps: Partial;
30 | getActiveIndex(props: TabsProps): number;
31 | componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void;
32 | handleTabChange(index: number): void;
33 | handleTabSequence({ oldIndex, newIndex }: {
34 | oldIndex: number;
35 | newIndex: number;
36 | }): void;
37 | handleTabClose(index: number): void;
38 | render(): JSX.Element;
39 | }
40 | export {};
41 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/helpers/delete.d.ts:
--------------------------------------------------------------------------------
1 | declare function deleteHelper(sequence: [], deleteIndex: number): never[];
2 | export default deleteHelper;
3 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/helpers/move.d.ts:
--------------------------------------------------------------------------------
1 | import { arrayMove } from '@dnd-kit/sortable';
2 | export default arrayMove;
3 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | import Tabs from './Tabs';
2 | import TabList from './TabList';
3 | import Tab from './Tab';
4 | import DragTabList from './DragTabList';
5 | import DragTab from './DragTab';
6 | import PanelList from './PanelList';
7 | import Panel from './Panel';
8 | import AsyncPanel from './AsyncPanel';
9 | import ExtraButton from './ExtraButton';
10 | import simpleSwitch from './helpers/move';
11 | import deleteHelper from './helpers/delete';
12 | export { Tabs, TabList, Tab, DragTabList, DragTab, PanelList, Panel, AsyncPanel, ExtraButton };
13 | export declare const styled: {
14 | TabList: import("styled-components").StyledComponent, any, {}, never>;
15 | ActionButton: import("styled-components").StyledComponent<"div", any, {}, never>;
16 | Tab: import("styled-components").StyledComponent, HTMLLIElement>, "key" | keyof import("react").LiHTMLAttributes> & import("./Tab").TabProps & import("react").RefAttributes>>, any, {}, never>;
17 | Panel: import("styled-components").StyledComponent<"div", any, {
18 | active: boolean;
19 | }, never>;
20 | };
21 | export declare const helpers: {
22 | simpleSwitch: typeof simpleSwitch;
23 | deleteHelper: typeof deleteHelper;
24 | };
25 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/styledElements.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { TabListElementProps } from './TabListElement';
3 | export declare const buttonWidth = 35;
4 | export declare const TabListStyle: import("styled-components").StyledComponent, any, {}, never>;
5 | export declare const ListInner: import("styled-components").StyledComponent<"div", any, {}, never>;
6 | export declare const ListScroll: import("styled-components").StyledComponent<"ul", any, {}, never>;
7 | export declare const ActionButtonStyle: import("styled-components").StyledComponent<"div", any, {}, never>;
8 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/utils/countTab.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export default function countTab(children: React.ReactElement[]): number;
3 |
--------------------------------------------------------------------------------
/packages/tabtab/dist/utils/isType.d.ts:
--------------------------------------------------------------------------------
1 | export declare function isTabList(element: any): boolean;
2 | export declare function isTab(element: any): boolean;
3 | export declare function isNumber(number: any): boolean;
4 |
--------------------------------------------------------------------------------
/packages/tabtab/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-tabtab-next/tabtab",
3 | "version": "3.3.0",
4 | "description": "[TypeScript] A mobile support, draggable, editable and api based Tab for ReactJS",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "rimraf dist && rollup -c ../../rollup.config.js",
9 | "validate": "npm ls"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/onmotion/react-tabtab-next.git",
14 | "directory": "packages/tabtab"
15 | },
16 | "keywords": [
17 | "react",
18 | "tabs",
19 | "drag",
20 | "react-tabtab",
21 | "react-component",
22 | "tab",
23 | "tabtab",
24 | "tabtab-next",
25 | "react-tabtab-next",
26 | "typescript",
27 | "react draggable tabs"
28 | ],
29 | "files": [
30 | "dist"
31 | ],
32 | "publishConfig": {
33 | "access": "public"
34 | },
35 | "author": {
36 | "name": "Alexandr Kozhevnikov",
37 | "email": "onmotion1@gmail.com"
38 | },
39 | "license": "MIT",
40 | "bugs": {
41 | "url": "https://github.com/onmotion/react-tabtab-next/issues"
42 | },
43 | "homepage": "https://github.com/onmotion/react-tabtab-next#readme",
44 | "dependencies": {
45 | "@dnd-kit/core": "^5.0.3",
46 | "@dnd-kit/sortable": "^6.0.1",
47 | "@dnd-kit/utilities": "^3.1.0",
48 | "classnames": "^2.2.5",
49 | "invariant": "^2.2.2",
50 | "react-modal": "^3.14.4",
51 | "react-transition-group": "^4.4.2"
52 | },
53 | "devDependencies": {
54 | "@types/estree": "^0.0.51",
55 | "@types/invariant": "^2.2.35",
56 | "@types/react-modal": "^3.13.1",
57 | "@types/styled-components": "^5.1.24",
58 | "rimraf": "^2.6.2",
59 | "tslib": "^2.3.1"
60 | },
61 | "peerDependencies": {
62 | "@types/react": "^16.8.0 || ^17 || ^18",
63 | "react": "^16.8.0 || ^17 || ^18",
64 | "react-dom": "^16.8.0 || ^17 || ^18",
65 | "styled-components": "^5.3.3"
66 | },
67 | "peerDependenciesMeta": {
68 | "@types/react": {
69 | "optional": true
70 | }
71 | },
72 | "jest": {
73 | "setupFiles": [
74 | "./test/shim",
75 | "./test/enzyme-setup"
76 | ],
77 | "roots": [
78 | "/test/"
79 | ],
80 | "unmockedModulePathPatterns": [
81 | "node_modules/react/",
82 | "node_modules/enzyme/"
83 | ],
84 | "snapshotSerializers": [
85 | "enzyme-to-json/serializer"
86 | ]
87 | },
88 | "pre-commit": [
89 | "lint",
90 | "test"
91 | ]
92 | }
--------------------------------------------------------------------------------
/packages/tabtab/src/AsyncPanel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Panel, { PanelProps } from './Panel';
3 |
4 | type Props = {
5 | loadContent: (cb: (err: any, data?: any) => void) => any;
6 | render: (data: any) => React.ReactNode;
7 | renderLoading: () => React.ReactNode;
8 | CustomPanelStyle?: React.FC>;
9 | active?: boolean;
10 | index?: number;
11 | cache?: boolean;
12 | };
13 |
14 | type State = {
15 | isLoading: boolean;
16 | data: any;
17 | };
18 |
19 | export default class AsyncPanelComponent extends React.PureComponent {
20 | static defaultProps = {
21 | cache: true,
22 | };
23 |
24 | cacheData: any;
25 |
26 | constructor(props: Props) {
27 | super(props);
28 | this.loadPanel = this.loadPanel.bind(this);
29 | this.cacheData = undefined;
30 | this.state = {
31 | isLoading: false,
32 | data: undefined,
33 | };
34 | }
35 |
36 | componentDidMount() {
37 | if (this.props.active) this.loadPanel();
38 | }
39 | componentDidUpdate(prevProps: Props) {
40 | this.props.active && !prevProps.active && this.loadPanel();
41 | }
42 |
43 | loadPanel() {
44 | const { loadContent, cache } = this.props;
45 | if (cache && this.cacheData) {
46 | this.setState({
47 | isLoading: false,
48 | data: this.cacheData,
49 | });
50 | return;
51 | }
52 | const callback = (err: any, data?: any) => {
53 | if (err) {
54 | console.error('React-Tabtab async panel error:', err);
55 | }
56 | if (cache) {
57 | this.cacheData = data;
58 | }
59 | this.setState({
60 | isLoading: false,
61 | data,
62 | });
63 | };
64 | const promise = loadContent(callback);
65 | if (promise) {
66 | promise.then(
67 | (data: any) => callback(null, data),
68 | (err: any) => callback(err)
69 | );
70 | }
71 | if (!this.state.isLoading) {
72 | this.setState({ isLoading: true });
73 | }
74 | }
75 |
76 | render() {
77 | const { render, renderLoading, CustomPanelStyle, active, index } = this.props;
78 | const { isLoading, data } = this.state;
79 | let content;
80 | if (isLoading) {
81 | content = renderLoading();
82 | } else {
83 | content = render(data);
84 | }
85 | return {content} ;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/packages/tabtab/src/CloseButton.tsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import { CloseIcon } from './IconSvg';
4 | import styled from 'styled-components';
5 |
6 | const CloseWrapper = styled.button`
7 | display: inline-block;
8 | color: #777;
9 | line-height: 0;
10 | margin-left: 5px;
11 | padding: 0;
12 | vertical-align: middle;
13 | background-color: transparent;
14 | border: 0;
15 | padding: 2px;
16 | outline: 0;
17 | &:hover {
18 | color: black;
19 | background-color: #eee;
20 | cursor: pointer;
21 | border-radius: 50%;
22 | }
23 | > svg {
24 | vertical-align: middle;
25 | }
26 | `;
27 |
28 | type Props = {
29 | handleTabClose: (event: any) => void;
30 | };
31 |
32 | export default class CloseButton extends React.PureComponent {
33 | render() {
34 | return (
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/packages/tabtab/src/DragTab.tsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { FC, forwardRef, memo } from 'react';
3 | import { useSortable } from '@dnd-kit/sortable';
4 | import { CSS } from '@dnd-kit/utilities';
5 |
6 | interface Props {
7 | id: string;
8 | activeIndex?: number;
9 | index?: number;
10 | children: React.ReactNode;
11 | }
12 | const DragTab: FC = memo(
13 | forwardRef(({ children, id, index, activeIndex, ...rest }, ref) => {
14 | const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: id });
15 |
16 | const style = {
17 | transform: CSS.Transform.toString(transform),
18 | transition,
19 | // cursor: 'default',
20 | };
21 |
22 | return (
23 |
24 | {React.cloneElement(children as React.ReactElement, {
25 | ...rest,
26 | key: id,
27 | active: index === activeIndex,
28 | index,
29 | tabIndex: id,
30 | ref: ref,
31 | })}
32 |
33 | );
34 | })
35 | );
36 |
37 | export default DragTab;
38 |
--------------------------------------------------------------------------------
/packages/tabtab/src/DragTabList.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, memo, useCallback, useEffect, useState } from 'react';
2 | import { DndContext, useSensor, useSensors, MouseSensor, DndContextProps, TouchSensor } from '@dnd-kit/core';
3 | import { SortableContext } from '@dnd-kit/sortable';
4 | import DragTab from './DragTab';
5 | import TabList from './TabList';
6 | import { TabProps } from './Tab';
7 | import { TabsProps } from './Tabs';
8 |
9 | interface IDragTabListProps {
10 | onTabSequenceChange?: TabsProps['onTabSequenceChange'];
11 | children: React.ReactNode;
12 | }
13 |
14 | const DragTabList: FC> = memo(({ children, ...props }) => {
15 | const [items, setItems] = useState([]);
16 |
17 | useEffect(() => {
18 | setItems(React.Children.map(children, (_, i) => i.toString()));
19 | }, [children]);
20 |
21 | const mouseSensor = useSensor(MouseSensor, {
22 | // Require the mouse to move by 10 pixels before activating
23 | activationConstraint: {
24 | distance: 10,
25 | },
26 | });
27 | const touchSensor = useSensor(TouchSensor, {
28 | activationConstraint: {
29 | delay: 200,
30 | tolerance: 0,
31 | },
32 | });
33 |
34 | const sensors = useSensors(mouseSensor, touchSensor);
35 |
36 | const handleOnDragEnd: DndContextProps['onDragEnd'] = useCallback(
37 | (event) => {
38 | const { active, over } = event;
39 | if (!props.onTabSequenceChange || !over?.id) {
40 | return;
41 | }
42 |
43 | if (active.id !== over.id) {
44 | props.onTabSequenceChange({ newIndex: Number(over.id), oldIndex: Number(active.id) });
45 | }
46 | },
47 | [props.onTabSequenceChange]
48 | );
49 |
50 | return (
51 | <>
52 |
53 |
54 |
59 | {React.Children.map(children, (child, i) => (
60 |
61 | {child}
62 |
63 | ))}
64 |
65 |
66 |
67 | >
68 | );
69 | });
70 |
71 | export default DragTabList;
72 |
--------------------------------------------------------------------------------
/packages/tabtab/src/ExtraButton.tsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import styled from 'styled-components';
4 |
5 | const Wrapper = styled.button`
6 | flex-shrink: 0;
7 | align-self: center;
8 | height: 100%;
9 | width: 30px;
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | font-size: 1.2em;
14 | background: transparent;
15 | border: none;
16 | margin-top: 0;
17 | padding: 3px;
18 | margin-left: 2px;
19 | display: inline-block;
20 | color: #777;
21 | vertical-align: middle;
22 | user-select: none;
23 | ${(props) =>
24 | props.disabled
25 | ? `
26 | pointer-events: none;
27 | color: #AAA;
28 | background: #F5F5F5;
29 | `
30 | : null}
31 | &:hover {
32 | color: black;
33 | cursor: pointer;
34 | }
35 | &:disabled,
36 | &[disabled] {
37 | border: 1px solid grey;
38 | background-color: #e7e7e7;
39 | cursor: not-allowed;
40 | }
41 | `;
42 |
43 | type Props = {
44 | onClick: (event: any) => void;
45 | disabled: boolean;
46 | children: React.ReactNode;
47 | };
48 |
49 | export default class ExtraButton extends React.PureComponent {
50 | static defaultProps = {
51 | disabled: false,
52 | };
53 |
54 | render() {
55 | const { disabled, onClick } = this.props;
56 | return (
57 |
58 | {this.props.children}
59 |
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/tabtab/src/IconSvg.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | // The svg path is from react-icons: https://github.com/gorangajic/react-icons/
3 | const Svg = ({ d }: { d: string }) => (
4 |
11 |
12 |
13 |
14 |
15 | )
16 |
17 | const CloseIcon = () => (
18 |
19 | )
20 |
21 | const LeftIcon = () => (
22 |
23 | )
24 |
25 | const RightIcon = () => (
26 |
27 | )
28 |
29 | const BulletIcon = () => (
30 |
31 | )
32 |
33 | export { CloseIcon, LeftIcon, RightIcon, BulletIcon }
34 |
--------------------------------------------------------------------------------
/packages/tabtab/src/Panel.tsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import styled from 'styled-components';
4 |
5 | const PanelStyle = styled.div<{ active: boolean }>`
6 | text-align: left;
7 | padding: 20px 15px;
8 | ${(props) => (!props.active ? `display: none;` : null)}
9 | `;
10 |
11 | export type PanelProps = {
12 | CustomPanelStyle?: React.FC>;
13 | active?: boolean;
14 | index?: number;
15 | };
16 |
17 | export default class PanelComponent extends React.PureComponent> {
18 | render() {
19 | const { active, index } = this.props;
20 | const Panel = this.props.CustomPanelStyle || PanelStyle;
21 | return (
22 |
29 | {active ? this.props.children : null}
30 |
31 | );
32 | }
33 | }
34 |
35 | export { PanelStyle };
36 |
--------------------------------------------------------------------------------
/packages/tabtab/src/PanelList.tsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import { ReactElement } from 'react';
4 |
5 | type Props = {
6 | activeIndex?: number;
7 | customStyle?: {
8 | Panel: () => void;
9 | };
10 | };
11 |
12 | export default class PanelList extends React.PureComponent> {
13 | render() {
14 | const { children, activeIndex, customStyle } = this.props;
15 | if (!children || activeIndex === undefined) {
16 | return null;
17 | }
18 |
19 | let props = {};
20 | if (customStyle && customStyle.Panel) {
21 | props = { ...props, CustomPanelStyle: customStyle.Panel };
22 | }
23 |
24 | // to prevent the type of one children is object type
25 | const result = React.Children.toArray(children).map((child, index) =>
26 | React.cloneElement(child as ReactElement, {
27 | key: index,
28 | active: index === activeIndex,
29 | index,
30 | ...props,
31 | })
32 | );
33 | return {result}
;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/tabtab/src/SortMethod.tsx:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from 'react';
3 | import { TabListProps } from './TabList';
4 |
5 | export type SortMathodComponentProps = {
6 | handleTabChange?: (event: any) => void;
7 | handleTabSequence?: (event: any) => void;
8 | activeIndex?: number;
9 | };
10 |
11 | export default class SortMethod extends React.PureComponent {
12 | constructor(props: SortMathodComponentProps & TabListProps) {
13 | super(props);
14 | this.onSortEnd = this.onSortEnd.bind(this);
15 | }
16 |
17 | onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
18 | const { activeIndex, handleTabChange, handleTabSequence } = this.props;
19 | if (activeIndex === undefined) {
20 | return;
21 | }
22 | if (oldIndex === newIndex) {
23 | if (activeIndex !== oldIndex) {
24 | handleTabChange && handleTabChange(oldIndex);
25 | }
26 | } else {
27 | handleTabSequence && handleTabSequence({ oldIndex, newIndex });
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/tabtab/src/Tab.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 | import CloseButton from './CloseButton';
4 |
5 | export type TabElementProps = React.ComponentPropsWithoutRef<'li'> & TabProps;
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
8 | export const TabElement = React.memo(
9 | React.forwardRef(({ active, closable, vertical, ...props }, ref) => (
10 |
11 | {props.children}
12 |
13 | ))
14 | );
15 |
16 | const TabStyle = styled(TabElement)`
17 | display: ${(props) => (props.vertical ? 'flex' : 'inline-block')};
18 | justify-content: space-between;
19 | touch-action: auto;
20 | color: #000000bb;
21 | border-bottom: 2px solid transparent;
22 | white-space: nowrap;
23 | ${(props) =>
24 | props.vertical
25 | ? `
26 | background-color: white;
27 | color: black;
28 | padding: 10px 10px;
29 | z-index: 1;
30 | `
31 | : (props) => (props.closable ? 'padding: 10px 10px 8px 15px;' : 'padding: 10px 15px 8px 15px;')}
32 |
33 | user-select: none;
34 | &:hover,
35 | &:active {
36 | cursor: pointer;
37 | color: black;
38 | }
39 | ${(props) =>
40 | props.active
41 | ? `
42 | color: black;
43 | border-bottom: 2px solid;
44 | `
45 | : null}
46 | `;
47 |
48 | const TabText = styled.span`
49 | vertical-align: middle;
50 | `;
51 |
52 | export type TabProps = {
53 | CustomTabStyle?: React.FC>;
54 | handleTabChange?: (event: any) => void;
55 | handleTabClose?: (event: any) => void;
56 | index?: number;
57 | active?: boolean;
58 | closable?: boolean;
59 | vertical?: boolean;
60 | tabIndex?: string;
61 | };
62 |
63 | export default class Tab extends React.PureComponent> {
64 | __INTERNAL_NODE: React.ElementRef;
65 |
66 | constructor(props: TabProps) {
67 | super(props);
68 | this.clickTab = this.clickTab.bind(this);
69 | this.clickDelete = this.clickDelete.bind(this);
70 | }
71 |
72 | clickTab(e: React.MouseEvent) {
73 | const { handleTabChange, index } = this.props;
74 | handleTabChange(index);
75 | }
76 |
77 | clickDelete(event: React.SyntheticEvent) {
78 | event.stopPropagation(); // prevent trigger clickTab event.
79 | const { handleTabClose, index } = this.props;
80 | handleTabClose(index);
81 | }
82 |
83 | render() {
84 | const { CustomTabStyle, active, closable, vertical, index } = this.props;
85 | const TabComponent = CustomTabStyle || TabStyle;
86 |
87 | return (
88 | (this.__INTERNAL_NODE = node)}
90 | style={{ touchAction: 'auto' }}
91 | onClick={this.clickTab}
92 | active={active}
93 | vertical={vertical}
94 | closable={closable}
95 | role="tab"
96 | id={`react-tabtab-tab-${index}`}
97 | aria-controls={`react-tabtab-panel-${index}`}
98 | aria-selected={active}
99 | >
100 | {this.props.children}
101 | {closable ? : null}
102 |
103 | );
104 | }
105 | }
106 |
107 | export { TabStyle };
108 |
--------------------------------------------------------------------------------
/packages/tabtab/src/TabList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 | import invariant from 'invariant';
4 | import { LeftIcon, RightIcon, BulletIcon } from './IconSvg';
5 | import { isNumber } from './utils/isType';
6 | import { ActionButtonStyle, buttonWidth, ListInner, ListScroll, TabListStyle } from './styledElements';
7 | import { TabListElementProps } from './TabListElement';
8 | import { TabElementProps } from './Tab';
9 | import { PanelProps } from './Panel';
10 | import { SortableContextProps } from '@dnd-kit/sortable';
11 | import { DndContextProps } from '@dnd-kit/core';
12 | import { TabListModal } from './TabListModal';
13 |
14 | const makeScrollButton = (ActionButton: React.ElementType) => styled(ActionButton)`
15 | display: inline-block;
16 | filter: none;
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | position: absolute;
21 | user-select: none;
22 | ${(props) => (props.left ? (props.showModalButton ? `left: ${buttonWidth + 2}px` : `left: 0`) : 'right: 0')};
23 | &:hover {
24 | cursor: pointer;
25 | }
26 | `;
27 |
28 | const makeFoldButton = (ActionButton: React.ElementType) => styled(ActionButton)`
29 | display: inline-block;
30 | filter: none;
31 | display: flex;
32 | justify-content: center;
33 | align-items: center;
34 | position: absolute;
35 | user-select: none;
36 | left: 0;
37 | &:hover {
38 | cursor: pointer;
39 | }
40 | `;
41 |
42 | export type TabListProps = {
43 | customStyle?: {
44 | TabList?: React.ElementType;
45 | Tab?: React.ElementType;
46 | Panel?: React.ElementType;
47 | ActionButton?: React.ElementType;
48 | };
49 | showArrowButton?: 'auto' | boolean;
50 | showModalButton?: number | boolean;
51 | handleTabChange?: (event: any) => void;
52 | handleTabSequence?: (event: any) => void;
53 | handleTabClose?: (index: number) => void;
54 | ExtraButton?: JSX.Element;
55 | activeIndex?: number;
56 | children: React.ReactNode[];
57 | sortableContextProps?: Omit;
58 | dndContextProps?: DndContextProps;
59 | };
60 |
61 | type State = {
62 | modalIsOpen: boolean;
63 | showArrowButton: 'auto' | boolean;
64 | showModalButton: boolean | number;
65 | };
66 |
67 | export default class TabListComponent extends React.PureComponent {
68 | listContainer: React.ElementRef<'div'>;
69 | rightArrowNode: React.ReactElement;
70 | leftArrowNode: React.ReactElement;
71 | listScroll: React.ElementRef<'ul'>;
72 | foldNode: React.ReactElement;
73 | tabRefs: React.ElementRef<'div'>[];
74 | scrollPosition: number;
75 | FoldButton: React.ElementType;
76 | ScrollButton: React.ElementType;
77 | TabList: React.ElementType;
78 |
79 | constructor(props: TabListProps) {
80 | super(props);
81 | this.handleScroll = this.handleScroll.bind(this);
82 | this.toggleModal = this.toggleModal.bind(this);
83 | this.renderTabs = this.renderTabs.bind(this);
84 | this.renderArrowButtons = this.renderArrowButtons.bind(this);
85 | this.isShowModalButton = this.isShowModalButton.bind(this);
86 | this.isShowArrowButton = this.isShowArrowButton.bind(this);
87 | this.chackActiveIndexRange = this.chackActiveIndexRange.bind(this);
88 | this.scrollPosition = 0;
89 | this.tabRefs = [];
90 | this.TabList = this.props.customStyle?.TabList || TabListStyle;
91 | this.FoldButton = makeFoldButton(this.props.customStyle?.ActionButton || ActionButtonStyle);
92 | this.ScrollButton = makeScrollButton(this.props.customStyle?.ActionButton || ActionButtonStyle);
93 |
94 | this.state = {
95 | modalIsOpen: false,
96 | showArrowButton: false,
97 | showModalButton: false,
98 | };
99 | }
100 |
101 | chackActiveIndexRange() {
102 | if (this.props.activeIndex >= this.props.children.length) {
103 | console.error('activeIndex is out of range 0-' + (this.props.children.length - 1));
104 | return false;
105 | }
106 | return true;
107 | }
108 |
109 | componentDidMount() {
110 | if (!this.chackActiveIndexRange()) return;
111 | this.isShowArrowButton();
112 | this.isShowModalButton();
113 |
114 | if (this.props.activeIndex > 0) this.scrollToIndex(this.props.activeIndex, 'left');
115 | }
116 |
117 | componentDidUpdate(prevProps: TabListProps, prevState: State) {
118 | if (prevProps.children.length !== this.props.children.length) {
119 | this.isShowArrowButton();
120 | this.isShowModalButton();
121 | }
122 |
123 | if (prevProps.activeIndex !== this.props.activeIndex) {
124 | //if we scroll to the last tab, alignment is set to the right side of the tab
125 | if (!this.chackActiveIndexRange()) return;
126 | const rectSide = this.props.activeIndex === this.props.children.length - 1 ? 'right' : 'left';
127 | this.scrollToIndex(this.props.activeIndex, rectSide);
128 | }
129 | // if prev state show arrow button, and current state doesn't show
130 | // need to reset the scroll position, or some tabs will be hided by container.
131 | if (prevState.showArrowButton && !this.state.showArrowButton) {
132 | this.scrollToZero();
133 | }
134 |
135 | if (prevProps.showModalButton !== this.props.showModalButton) {
136 | this.isShowModalButton();
137 | }
138 |
139 | if (prevProps.showArrowButton !== this.props.showArrowButton) {
140 | this.isShowArrowButton();
141 | }
142 | if (
143 | this.props.customStyle?.ActionButton &&
144 | prevProps.customStyle?.ActionButton !== this.props.customStyle?.ActionButton
145 | ) {
146 | this.FoldButton = makeFoldButton(this.props.customStyle?.ActionButton);
147 | this.ScrollButton = makeScrollButton(this.props.customStyle?.ActionButton);
148 | }
149 | }
150 |
151 | getTabNode(tab: HTMLDivElement & { __INTERNAL_NODE?: any }): React.ElementRef<'div'> {
152 | return tab.__INTERNAL_NODE;
153 | }
154 |
155 | unifyScrollMax(width: number) {
156 | return (width / 3) * 2;
157 | }
158 |
159 | handleScroll(direction: 'right' | 'left') {
160 | let leftMove = 0;
161 | const containerOffset = this.listContainer.getBoundingClientRect();
162 | const containerWidth = this.listContainer.offsetWidth;
163 |
164 | if (direction === 'right') {
165 | const tabLastOffset = this.getTabNode(this.tabRefs[this.tabRefs.length - 1]).getBoundingClientRect();
166 |
167 | leftMove = tabLastOffset.right - containerOffset.right;
168 | if (leftMove > containerWidth) {
169 | leftMove = this.unifyScrollMax(containerWidth);
170 | }
171 | } else if (direction === 'left') {
172 | const tabFirstOffset = this.getTabNode(this.tabRefs[0]).getBoundingClientRect();
173 |
174 | leftMove = tabFirstOffset.left - containerOffset.left;
175 | if (-leftMove > containerWidth) {
176 | leftMove = -this.unifyScrollMax(containerWidth);
177 | }
178 | }
179 | this.scrollPosition += leftMove;
180 | if (this.scrollPosition < 0) {
181 | this.scrollPosition = 0;
182 | }
183 |
184 | this.listScroll.style.transform = `translate3d(-${this.scrollPosition}px, 0, 0)`;
185 | }
186 |
187 | scrollToIndex(index: number, rectSide: 'left' | 'right') {
188 | if (index < 0) return
189 | const tabOffset = this.getTabNode(this.tabRefs[index]).getBoundingClientRect();
190 | const containerOffset = this.listContainer.getBoundingClientRect();
191 |
192 | // Cancel scrolling if the tab is visible
193 | if (tabOffset.right < containerOffset.right && tabOffset.left > containerOffset.left) return;
194 |
195 | const leftMove = tabOffset['right'] + (rectSide === 'left' ? tabOffset['width'] : 0) - containerOffset['right'];
196 |
197 | this.scrollPosition += leftMove;
198 | if (this.scrollPosition < 0) {
199 | this.scrollPosition = 0;
200 | }
201 |
202 | this.listScroll.style.transform = `translate3d(-${this.scrollPosition}px, 0, 0)`;
203 | }
204 |
205 | scrollToZero() {
206 | this.listScroll.style.transform = `translate3d(0, 0, 0)`;
207 | }
208 |
209 | toggleModal() {
210 | this.setState({ modalIsOpen: !this.state.modalIsOpen });
211 | }
212 |
213 | isShowModalButton() {
214 | let { showModalButton } = this.props;
215 | if (isNumber(showModalButton)) {
216 | // $FlowFixMe, weired. currently set showModalButton as number | bool, but don't know why flow only can recognize it as bool
217 | showModalButton = this.props.children.length >= showModalButton;
218 | }
219 | this.setState({ showModalButton });
220 | }
221 |
222 | isShowArrowButton() {
223 | let { showArrowButton } = this.props;
224 |
225 | if (showArrowButton === 'auto') {
226 | let tabWidth = 0;
227 | const containerWidth = this.listContainer.offsetWidth;
228 | showArrowButton = false;
229 | for (let index = 0; index < this.tabRefs.length; index++) {
230 | const tab = this.getTabNode(this.tabRefs[index]);
231 | tabWidth += tab.offsetWidth;
232 | if (tabWidth >= containerWidth) {
233 | showArrowButton = true;
234 | break;
235 | }
236 | }
237 | }
238 | this.setState({ showArrowButton });
239 | }
240 |
241 | renderTabs(options: any = {}, isModal?: boolean) {
242 | const { children, activeIndex, handleTabChange, handleTabClose, customStyle } = this.props;
243 | const props = {
244 | handleTabChange,
245 | handleTabClose,
246 | CustomTabStyle: customStyle.Tab,
247 | };
248 | if (!isModal) {
249 | this.tabRefs = [];
250 | }
251 | return React.Children.map(children, (child, index) =>
252 | React.cloneElement(child as React.ReactElement, {
253 | key: index,
254 | active: index === activeIndex,
255 | index,
256 | tabIndex: index,
257 | ref: (node: HTMLDivElement) => {
258 | if (!isModal && node) {
259 | this.tabRefs.push(node);
260 | }
261 | },
262 | ...props,
263 | ...options,
264 | })
265 | );
266 | }
267 |
268 | renderArrowButtons(ScrollButton: React.ElementType) {
269 | const { showArrowButton } = this.state;
270 | if (showArrowButton) {
271 | return (
272 |
273 | {
276 | this.handleScroll('left');
277 | }}
278 | ref={(node: React.ReactElement) => (this.leftArrowNode = node)}
279 | showModalButton={this.state.showModalButton}
280 | className={'tabtab-arrow-button_left'}
281 | >
282 |
283 |
284 | {
286 | this.handleScroll('right');
287 | }}
288 | ref={(node: React.ReactElement) => (this.rightArrowNode = node)}
289 | className={'tabtab-arrow-button_right'}
290 | >
291 |
292 |
293 |
294 | );
295 | }
296 | return null;
297 | }
298 |
299 | render() {
300 | const { ExtraButton } = this.props;
301 | const { modalIsOpen } = this.state;
302 |
303 | const TabList = this.TabList;
304 | const ScrollButton = this.ScrollButton;
305 | const FoldButton = this.FoldButton;
306 |
307 | invariant(this.props.children, 'React-tabtab Error: You MUST pass at least one tab');
308 |
309 | return (
310 |
311 |
312 | {this.state.showModalButton ? (
313 | (this.foldNode = node)}
315 | onClick={this.toggleModal}
316 | showArrowButton={this.state.showArrowButton}
317 | >
318 |
319 |
320 | ) : null}
321 | {this.renderArrowButtons(ScrollButton)}
322 | (this.listContainer = node)} className="tabtab-list-container">
323 | (this.listScroll = node)} role="tablist">
324 | {this.renderTabs()}
325 |
326 |
327 |
328 | {!!ExtraButton && ExtraButton}
329 | {this.isShowModalButton && (
330 |
336 | {this.renderTabs({ vertical: true }, true)}
337 |
338 | )}
339 |
340 | );
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/packages/tabtab/src/TabListElement.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { memo } from 'react';
3 | import { FC } from 'react';
4 |
5 | export interface TabListElementProps {
6 | showArrowButton?: 'auto' | boolean;
7 | showModalButton?: number | boolean;
8 | }
9 |
10 | export const TabListElement: FC = memo(({ showArrowButton, showModalButton, ...props }) => {
11 | return
;
12 | });
13 |
--------------------------------------------------------------------------------
/packages/tabtab/src/TabListModal.tsx:
--------------------------------------------------------------------------------
1 | import { DndContext, DndContextProps } from '@dnd-kit/core';
2 | import { SortableContext, SortableContextProps } from '@dnd-kit/sortable';
3 | import React, { FC, memo } from 'react';
4 | import ReactModal from 'react-modal';
5 | import './styles/modal.css';
6 |
7 | ReactModal.setAppElement(document.querySelector('body'));
8 |
9 | interface ITabListModalProps extends ReactModal.Props {
10 | dndContextProps?: DndContextProps;
11 | sortableContextProps?: Omit;
12 | isOpen: boolean;
13 | onRequestClose: ReactModal.Props['onRequestClose'];
14 | }
15 |
16 | export const TabListModal: FC = memo(
17 | ({ children, isOpen, dndContextProps, sortableContextProps, ...props }) => {
18 | return (
19 | <>
20 | {dndContextProps ? (
21 |
22 |
23 |
30 | {children}
31 |
32 |
33 |
34 | ) : (
35 |
42 | {children}
43 |
44 | )}
45 | >
46 | );
47 | }
48 | );
49 |
--------------------------------------------------------------------------------
/packages/tabtab/src/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from 'react';
2 | import { PanelProps } from './Panel';
3 | import { TabElementProps } from './Tab';
4 | import { TabListElementProps } from './TabListElement';
5 |
6 | export type TabsProps = {
7 | defaultIndex?: number;
8 | activeIndex?: number | null;
9 | showModalButton?: number | boolean;
10 | showArrowButton?: 'auto' | boolean;
11 | ExtraButton?: React.ReactNode;
12 | onTabChange?: (index: number) => void;
13 | onTabSequenceChange?: (e: { oldIndex: number; newIndex: number }) => void;
14 | onTabClose?: (index: number) => void;
15 | customStyle?: {
16 | TabList?: React.ElementType;
17 | Tab?: React.ElementType;
18 | Panel?: React.ElementType;
19 | ActionButton?: React.ElementType;
20 | };
21 | };
22 |
23 | type State = {
24 | activeIndex: number;
25 | };
26 |
27 | export default class Tabs extends React.PureComponent, State> {
28 | constructor(props: TabsProps) {
29 | super(props);
30 | this.handleTabChange = this.handleTabChange.bind(this);
31 | this.handleTabSequence = this.handleTabSequence.bind(this);
32 | this.handleTabClose = this.handleTabClose.bind(this);
33 | this.state = {
34 | activeIndex: this.getActiveIndex(props),
35 | };
36 | }
37 |
38 | static defaultProps: Partial = {
39 | showModalButton: 4,
40 | showArrowButton: 'auto',
41 | onTabChange: () => {},
42 | onTabSequenceChange: () => {},
43 | customStyle: {
44 | TabList: null,
45 | Tab: null,
46 | Panel: null,
47 | ActionButton: null,
48 | },
49 | };
50 |
51 | getActiveIndex(props: TabsProps) {
52 | const { defaultIndex, activeIndex } = props;
53 | if (activeIndex) return activeIndex;
54 | if (defaultIndex) return defaultIndex;
55 | return 0;
56 | }
57 | componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void {
58 | if (prevProps.activeIndex !== this.props.activeIndex) {
59 | this.setState({ activeIndex: this.getActiveIndex(this.props) });
60 | }
61 | }
62 |
63 | handleTabChange(index: number) {
64 | const { activeIndex, onTabChange } = this.props;
65 | if (activeIndex !== 0 && !activeIndex) {
66 | this.setState({ activeIndex: index });
67 | }
68 | if (onTabChange) {
69 | onTabChange(index);
70 | }
71 | }
72 |
73 | handleTabSequence({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
74 | const { onTabSequenceChange } = this.props;
75 | if (onTabSequenceChange) {
76 | onTabSequenceChange({ oldIndex, newIndex });
77 | }
78 | }
79 |
80 | handleTabClose(index: number) {
81 | const { onTabClose } = this.props;
82 | if (onTabClose) {
83 | onTabClose(index);
84 | }
85 | }
86 |
87 | render() {
88 | const { children, ...extraProps } = this.props;
89 | const { activeIndex } = this.state;
90 |
91 | const props = {
92 | handleTabChange: this.handleTabChange,
93 | handleTabSequence: this.handleTabSequence,
94 | handleTabClose: this.handleTabClose,
95 | activeIndex,
96 | ...extraProps,
97 | };
98 |
99 | return (
100 |
101 | {React.Children.map(children, (child) => {
102 | return React.cloneElement(child as React.ReactElement, props);
103 | })}
104 |
105 | );
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/packages/tabtab/src/helpers/delete.ts:
--------------------------------------------------------------------------------
1 | function deleteHelper(sequence: [], deleteIndex: number) {
2 | return sequence.filter((_, i) => i !== deleteIndex);
3 | }
4 |
5 | export default deleteHelper;
6 |
--------------------------------------------------------------------------------
/packages/tabtab/src/helpers/move.ts:
--------------------------------------------------------------------------------
1 | import { arrayMove } from '@dnd-kit/sortable';
2 |
3 | export default arrayMove;
4 |
--------------------------------------------------------------------------------
/packages/tabtab/src/index.ts:
--------------------------------------------------------------------------------
1 | import Tabs from './Tabs';
2 | import TabList from './TabList';
3 | import Tab, { TabStyle } from './Tab';
4 | import DragTabList from './DragTabList';
5 | import DragTab from './DragTab';
6 | import PanelList from './PanelList';
7 | import Panel, { PanelStyle } from './Panel';
8 | import AsyncPanel from './AsyncPanel';
9 | import ExtraButton from './ExtraButton';
10 | import { TabListStyle, ActionButtonStyle } from './styledElements';
11 | import simpleSwitch from './helpers/move';
12 | import deleteHelper from './helpers/delete';
13 |
14 | export { Tabs, TabList, Tab, DragTabList, DragTab, PanelList, Panel, AsyncPanel, ExtraButton };
15 |
16 | export const styled = { TabList: TabListStyle, ActionButton: ActionButtonStyle, Tab: TabStyle, Panel: PanelStyle };
17 |
18 | export const helpers = { simpleSwitch, deleteHelper };
19 |
--------------------------------------------------------------------------------
/packages/tabtab/src/styledElements.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { TabListElement, TabListElementProps } from './TabListElement';
3 |
4 | export const buttonWidth = 35;
5 | const getPadding = ({ showModalButton, showArrowButton }: TabListElementProps) => {
6 | let paddingLeft = 0;
7 | let paddingRight = 0;
8 | if (showModalButton) {
9 | paddingLeft += buttonWidth;
10 | }
11 | if (showArrowButton) {
12 | paddingLeft += buttonWidth;
13 | paddingRight += buttonWidth;
14 | if (showModalButton) {
15 | paddingLeft += 2;
16 | }
17 | }
18 | if (paddingLeft > 0) {
19 | paddingLeft += 3;
20 | }
21 | if (paddingRight > 0) {
22 | paddingRight += 3;
23 | }
24 | return `0 ${paddingRight}px 0 ${paddingLeft}px`;
25 | };
26 |
27 | export const TabListStyle = styled(TabListElement)`
28 | background-color: white;
29 | text-align: left;
30 | position: relative;
31 | white-space: nowrap;
32 | overflow: hidden;
33 | width: 100%;
34 | padding: ${(props) => getPadding(props)};
35 | `;
36 |
37 | export const ListInner = styled.div`
38 | overflow: hidden;
39 | `;
40 |
41 | export const ListScroll = styled.ul`
42 | padding-left: 0;
43 | position: relative;
44 | margin: 0;
45 | list-style: none;
46 | display: inline-block;
47 | transition: transform 0.3s cubic-bezier(0.42, 0, 0.58, 1);
48 | display: flex;
49 | `;
50 |
51 | export const ActionButtonStyle = styled.div`
52 | height: 100%;
53 | width: ${buttonWidth}px;
54 | text-align: center;
55 | background: #f9f9f9;
56 | color: #555;
57 | :hover {
58 | color: #000;
59 | }
60 | `;
61 |
--------------------------------------------------------------------------------
/packages/tabtab/src/styles/modal.css:
--------------------------------------------------------------------------------
1 | .ReactModal__Overlay {
2 | opacity: 0;
3 | transition: opacity .3s ease-in-out;
4 | }
5 |
6 | .ReactModal__Overlay--after-open {
7 | opacity: 1;
8 | }
9 |
10 | .ReactModal__Overlay--before-close {
11 | opacity: 0;
12 | }
13 |
14 | .ReactModal__Content {
15 | margin: auto;
16 | background-color: #fff;
17 | width: 200px;
18 | padding: 20px;
19 | margin-top: 40px;
20 | overflow: auto;
21 | max-height: calc(100vh - 120px);
22 | outline: none;
23 | border-radius: 10px;
24 | box-shadow: 0 0 35px #0000001c;
25 | }
--------------------------------------------------------------------------------
/packages/tabtab/src/utils/countTab.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { isTab, isTabList } from './isType';
3 |
4 | function loopTabList(tabList: React.ReactElement[], cb: () => void) {
5 | React.Children.forEach(tabList, (tab) => {
6 | if (isTab(tab)) {
7 | cb();
8 | }
9 | });
10 | }
11 |
12 | function deepLoop(children: React.ReactElement[], cb: () => void) {
13 | React.Children.forEach(children, (child) => {
14 | if (isTabList(child)) {
15 | if (child.props && child.props.children) {
16 | return loopTabList(child.props.children, cb);
17 | } else {
18 | throw new Error('You need to provide `Tab` children');
19 | }
20 | } else if (child.props && child.props.children) {
21 | deepLoop(child.props.children, cb);
22 | }
23 | });
24 | }
25 |
26 | export default function countTab(children: React.ReactElement[]) {
27 | let count = 0;
28 | deepLoop(children, () => count++);
29 | return count;
30 | }
31 |
--------------------------------------------------------------------------------
/packages/tabtab/src/utils/isType.ts:
--------------------------------------------------------------------------------
1 | export function isTabList(element: any) {
2 | return element.type && (element.type.displayName === 'TabList' || element.type.displayName === 'DragTabList');
3 | }
4 |
5 | export function isTab(element: any) {
6 | return element.type && (element.type.displayName === 'Tab' || element.type.displayName === 'DragTab');
7 | }
8 |
9 | export function isNumber(number: any) {
10 | return !isNaN(parseInt(number, 10));
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tabtab/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "jsx": "react",
7 | "module": "es6",
8 | "moduleResolution": "node",
9 | "noImplicitAny": true,
10 | "preserveConstEnums": true,
11 | "target": "es5",
12 | "allowSyntheticDefaultImports": true,
13 | "noEmitOnError": true,
14 | "declaration": true,
15 | "declarationDir": "dist"
16 | },
17 | "exclude": ["node_modules"],
18 | "include": ["src/**/*"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/themes/dist/bootstrap/index.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: {
2 | TabList: import("styled-components").StyledComponent, any, {}, never>;
3 | ActionButton: import("styled-components").StyledComponent<"div", any, {}, never>;
4 | Tab: import("styled-components").StyledComponent, HTMLLIElement>, "key" | keyof import("react").LiHTMLAttributes> & import("@react-tabtab-next/tabtab/dist/Tab").TabProps & import("react").RefAttributes>>, any, {}, never>;
5 | Panel: import("styled-components").StyledComponent<"div", any, {
6 | active: boolean;
7 | }, never>;
8 | };
9 | export default _default;
10 |
--------------------------------------------------------------------------------
/packages/themes/dist/bulma/index.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: {
2 | TabList: import("styled-components").StyledComponent, any, {}, never>;
3 | ActionButton: import("styled-components").StyledComponent<"div", any, {}, never>;
4 | Tab: import("styled-components").StyledComponent, HTMLLIElement>, "key" | keyof import("react").LiHTMLAttributes> & import("@react-tabtab-next/tabtab/dist/Tab").TabProps & import("react").RefAttributes>>, any, {}, never>;
5 | Panel: import("styled-components").StyledComponent<"div", any, {
6 | active: boolean;
7 | }, never>;
8 | };
9 | export default _default;
10 |
--------------------------------------------------------------------------------
/packages/themes/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | export { default as md } from "./material-design";
2 | export { default as bulma } from "./bulma";
3 | export { default as bootstrap } from "./bootstrap";
4 |
--------------------------------------------------------------------------------
/packages/themes/dist/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | Object.defineProperty(exports, '__esModule', { value: true });
4 |
5 | var tslib = require('tslib');
6 | var styled = require('styled-components');
7 | var tabtab = require('@react-tabtab-next/tabtab');
8 |
9 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
10 |
11 | var styled__default = /*#__PURE__*/_interopDefaultLegacy(styled);
12 |
13 | var TabList$2 = tabtab.styled.TabList, ActionButton$2 = tabtab.styled.ActionButton, Tab$2 = tabtab.styled.Tab, Panel$2 = tabtab.styled.Panel;
14 | var primaryColor = '#f73378';
15 | TabList$2 = styled__default["default"](TabList$2)(templateObject_1$2 || (templateObject_1$2 = tslib.__makeTemplateObject(["\n background-color: #fff;\n box-shadow: inset 0 -1px 0px 0px #00000022;\n border: 0;\n"], ["\n background-color: #fff;\n box-shadow: inset 0 -1px 0px 0px #00000022;\n border: 0;\n"])));
16 | Tab$2 = styled__default["default"](Tab$2)(templateObject_2$2 || (templateObject_2$2 = tslib.__makeTemplateObject(["\n & span {\n font-size: 0.9em;\n transition: color 0.18s;\n text-transform: uppercase;\n color: ", ";\n ", "\n }\n\n ", "\n &:hover {\n background-color: transparent;\n & span {\n color: ", ";\n }\n\n border-bottom: 2px solid ", ";\n }\n\n /* Ripple effect */\n\n background-position: center;\n transition: background 0.8s;\n\n &:hover {\n background: #fff radial-gradient(circle, transparent 1%, #fff 1%) center/15000%;\n }\n &:active {\n background-color: ", ";\n background-size: 100%;\n transition: background 0s;\n }\n"], ["\n & span {\n font-size: 0.9em;\n transition: color 0.18s;\n text-transform: uppercase;\n color: ", ";\n ", "\n }\n\n ", "\n &:hover {\n background-color: transparent;\n & span {\n color: ", ";\n }\n\n border-bottom: 2px solid ", ";\n }\n\n /* Ripple effect */\n\n background-position: center;\n transition: background 0.8s;\n\n &:hover {\n background: #fff radial-gradient(circle, transparent 1%, #fff 1%) center/15000%;\n }\n &:active {\n background-color: ", ";\n background-size: 100%;\n transition: background 0s;\n }\n"])), primaryColor, function (props) {
17 | return props.active ? "color: ".concat(primaryColor, ";") : null;
18 | }, function (props) {
19 | return props.active
20 | ? "\n border-bottom: 2px solid ".concat(primaryColor, ";\n ")
21 | : null;
22 | }, primaryColor, primaryColor, primaryColor + '22');
23 | ActionButton$2 = styled__default["default"](ActionButton$2)(templateObject_3$1 || (templateObject_3$1 = tslib.__makeTemplateObject(["\n background-color: transparent;\n border-radius: 0;\n &:hover {\n background-color: #eee;\n }\n"], ["\n background-color: transparent;\n border-radius: 0;\n &:hover {\n background-color: #eee;\n }\n"])));
24 | Panel$2 = styled__default["default"](Panel$2)(templateObject_4$1 || (templateObject_4$1 = tslib.__makeTemplateObject(["\n padding: 30px 30px;\n"], ["\n padding: 30px 30px;\n"])));
25 | var index$2 = {
26 | TabList: TabList$2,
27 | ActionButton: ActionButton$2,
28 | Tab: Tab$2,
29 | Panel: Panel$2,
30 | };
31 | var templateObject_1$2, templateObject_2$2, templateObject_3$1, templateObject_4$1;
32 |
33 | var TabList$1 = tabtab.styled.TabList, ActionButton$1 = tabtab.styled.ActionButton, Tab$1 = tabtab.styled.Tab, Panel$1 = tabtab.styled.Panel;
34 | TabList$1 = styled__default["default"](TabList$1)(templateObject_1$1 || (templateObject_1$1 = tslib.__makeTemplateObject(["\n background-color: #fff;\n border-bottom: 1px solid #dbdbdb;\n"], ["\n background-color: #fff;\n border-bottom: 1px solid #dbdbdb;\n"])));
35 | Tab$1 = styled__default["default"](Tab$1)(templateObject_2$1 || (templateObject_2$1 = tslib.__makeTemplateObject(["\n position: relative;\n color: #4a4a4a;\n border: 0;\n padding: 13px 19px;\n margin-bottom: -1px;\n &::after {\n z-index: 10;\n content: '';\n position: absolute;\n left: 0;\n bottom: 0;\n right: 0;\n height: 2px;\n background: #dbdbdb;\n }\n ", "\n &:hover::after {\n background: #3273dc;\n }\n"], ["\n position: relative;\n color: #4a4a4a;\n border: 0;\n padding: 13px 19px;\n margin-bottom: -1px;\n &::after {\n z-index: 10;\n content: '';\n position: absolute;\n left: 0;\n bottom: 0;\n right: 0;\n height: 2px;\n background: #dbdbdb;\n }\n ", "\n &:hover::after {\n background: #3273dc;\n }\n"])), function (props) {
36 | return props.active && !props.vertical
37 | ? "\n &::after {\n background: #3273dc;\n }\n "
38 | : null;
39 | });
40 | ActionButton$1 = styled__default["default"](ActionButton$1)(templateObject_3 || (templateObject_3 = tslib.__makeTemplateObject(["\n background-color: transparent;\n border-radius: 0;\n &:hover {\n background-color: #eee;\n }\n"], ["\n background-color: transparent;\n border-radius: 0;\n &:hover {\n background-color: #eee;\n }\n"])));
41 | Panel$1 = styled__default["default"](Panel$1)(templateObject_4 || (templateObject_4 = tslib.__makeTemplateObject([""], [""])));
42 | var index$1 = {
43 | TabList: TabList$1,
44 | ActionButton: ActionButton$1,
45 | Tab: Tab$1,
46 | Panel: Panel$1,
47 | };
48 | var templateObject_1$1, templateObject_2$1, templateObject_3, templateObject_4;
49 |
50 | var TabList = tabtab.styled.TabList, Tab = tabtab.styled.Tab;
51 | var ActionButton = tabtab.styled.ActionButton, Panel = tabtab.styled.Panel;
52 | TabList = styled__default["default"](TabList)(templateObject_1 || (templateObject_1 = tslib.__makeTemplateObject(["\n box-shadow: inset 0 -1px 0px 0px #00000022;\n"], ["\n box-shadow: inset 0 -1px 0px 0px #00000022;\n"])));
53 | Tab = styled__default["default"](Tab)(templateObject_2 || (templateObject_2 = tslib.__makeTemplateObject(["\n & span {\n transition: color 0.18s;\n color: ", ";\n }\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n\n border: 1px solid transparent;\n &:hover {\n & span {\n color: #000;\n }\n }\n ", "\n ", "\n ", "\n"], ["\n & span {\n transition: color 0.18s;\n color: ", ";\n }\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n\n border: 1px solid transparent;\n &:hover {\n & span {\n color: #000;\n }\n }\n ", "\n ", "\n ", "\n"])), function (props) { return (props.active ? 'black' : '#007bff'); }, function (props) {
54 | return props.vertical
55 | ? "\n border-top: 1px solid transparent;\n border-bottom: 1px solid #efefef;\n border-left: 1px solid #efefef;\n border-right: 1px solid #efefef;\n border-radius: 0;\n &:first-child {\n border-top: 1px solid #efefef; \n }\n "
56 | : "\n \n ";
57 | }, function (props) {
58 | return props.active && props.vertical
59 | ? "\n background-color: #eee;\n "
60 | : null;
61 | }, function (props) {
62 | return props.active && !props.vertical
63 | ? "\n border-color: #ddd #ddd #fff;\n "
64 | : null;
65 | });
66 | var index = {
67 | TabList: TabList,
68 | ActionButton: ActionButton,
69 | Tab: Tab,
70 | Panel: Panel,
71 | };
72 | var templateObject_1, templateObject_2;
73 |
74 | exports.bootstrap = index;
75 | exports.bulma = index$1;
76 | exports.md = index$2;
77 |
--------------------------------------------------------------------------------
/packages/themes/dist/material-design/index.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: {
2 | TabList: import("styled-components").StyledComponent, any, {}, never>;
3 | ActionButton: import("styled-components").StyledComponent<"div", any, {}, never>;
4 | Tab: import("styled-components").StyledComponent, HTMLLIElement>, "key" | keyof import("react").LiHTMLAttributes> & import("@react-tabtab-next/tabtab/dist/Tab").TabProps & import("react").RefAttributes>>, any, {}, never>;
5 | Panel: import("styled-components").StyledComponent<"div", any, {
6 | active: boolean;
7 | }, never>;
8 | };
9 | export default _default;
10 |
--------------------------------------------------------------------------------
/packages/themes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-tabtab-next/themes",
3 | "version": "3.3.0",
4 | "description": "[TypeScript] Themes for @react-tabtab-next/tabtab",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "build": "rimraf dist && rollup -c ../../rollup.config.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/onmotion/react-tabtab-next.git",
13 | "directory": "packages/themes"
14 | },
15 | "files": [
16 | "dist"
17 | ],
18 | "keywords": [
19 | "react",
20 | "tabs",
21 | "drag",
22 | "react-tabtab",
23 | "react-component",
24 | "tab",
25 | "tabtab",
26 | "tabtab-next",
27 | "react-tabtab-next"
28 | ],
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "author": {
33 | "name": "Alexandr Kozhevnikov",
34 | "email": "onmotion1@gmail.com"
35 | },
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/onmotion/react-tabtab-next/issues"
39 | },
40 | "homepage": "https://github.com/onmotion/react-tabtab-next#readme",
41 | "dependencies": {
42 | "@react-tabtab-next/tabtab": "*"
43 | },
44 | "devDependencies": {
45 | "@types/react": "^16.8.0 || ^17 || ^18",
46 | "tslib": "^2.3.1"
47 | },
48 | "peerDependencies": {
49 | "@types/react": "^16.8.0 || ^17 || ^18",
50 | "react": "^16.8.0 || ^17 || ^18",
51 | "react-dom": "^16.8.0 || ^17 || ^18",
52 | "styled-components": "^5.3.3"
53 | },
54 | "peerDependenciesMeta": {
55 | "@types/react": {
56 | "optional": true
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/packages/themes/src/bootstrap/index.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { styled as themeStyled } from '@react-tabtab-next/tabtab';
3 |
4 | let { TabList, Tab } = themeStyled;
5 | const { ActionButton, Panel } = themeStyled;
6 |
7 | TabList = styled(TabList)`
8 | box-shadow: inset 0 -1px 0px 0px #00000022;
9 | `;
10 |
11 | Tab = styled(Tab)`
12 | & span {
13 | transition: color 0.18s;
14 | color: ${(props) => (props.active ? 'black' : '#007bff')};
15 | }
16 | border-top-left-radius: 0.25rem;
17 | border-top-right-radius: 0.25rem;
18 |
19 | border: 1px solid transparent;
20 | &:hover {
21 | & span {
22 | color: #000;
23 | }
24 | }
25 | ${(props) =>
26 | props.vertical
27 | ? `
28 | border-top: 1px solid transparent;
29 | border-bottom: 1px solid #efefef;
30 | border-left: 1px solid #efefef;
31 | border-right: 1px solid #efefef;
32 | border-radius: 0;
33 | &:first-child {
34 | border-top: 1px solid #efefef;
35 | }
36 | `
37 | : `
38 |
39 | `}
40 | ${(props) =>
41 | props.active && props.vertical
42 | ? `
43 | background-color: #eee;
44 | `
45 | : null}
46 | ${(props) =>
47 | props.active && !props.vertical
48 | ? `
49 | border-color: #ddd #ddd #fff;
50 | `
51 | : null}
52 | `;
53 |
54 | export default {
55 | TabList,
56 | ActionButton: ActionButton,
57 | Tab,
58 | Panel: Panel,
59 | };
60 |
--------------------------------------------------------------------------------
/packages/themes/src/bulma/index.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { styled as themeStyled } from '@react-tabtab-next/tabtab';
3 |
4 | let { TabList, ActionButton, Tab, Panel } = themeStyled;
5 |
6 | TabList = styled(TabList)`
7 | background-color: #fff;
8 | border-bottom: 1px solid #dbdbdb;
9 | `;
10 |
11 | Tab = styled(Tab)`
12 | position: relative;
13 | color: #4a4a4a;
14 | border: 0;
15 | padding: 13px 19px;
16 | margin-bottom: -1px;
17 | &::after {
18 | z-index: 10;
19 | content: '';
20 | position: absolute;
21 | left: 0;
22 | bottom: 0;
23 | right: 0;
24 | height: 2px;
25 | background: #dbdbdb;
26 | }
27 | ${(props) =>
28 | props.active && !props.vertical
29 | ? `
30 | &::after {
31 | background: #3273dc;
32 | }
33 | `
34 | : null}
35 | &:hover::after {
36 | background: #3273dc;
37 | }
38 | `;
39 |
40 | ActionButton = styled(ActionButton)`
41 | background-color: transparent;
42 | border-radius: 0;
43 | &:hover {
44 | background-color: #eee;
45 | }
46 | `;
47 |
48 | Panel = styled(Panel)``;
49 |
50 | export default {
51 | TabList: TabList,
52 | ActionButton: ActionButton,
53 | Tab: Tab,
54 | Panel: Panel,
55 | };
56 |
--------------------------------------------------------------------------------
/packages/themes/src/index.ts:
--------------------------------------------------------------------------------
1 | export { default as md } from "./material-design"
2 | export { default as bulma } from "./bulma"
3 | export { default as bootstrap } from "./bootstrap"
4 |
--------------------------------------------------------------------------------
/packages/themes/src/material-design/index.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | import { styled as themeStyled } from '@react-tabtab-next/tabtab';
4 |
5 | let { TabList, ActionButton, Tab, Panel } = themeStyled;
6 |
7 | const primaryColor = '#f73378';
8 |
9 | TabList = styled(TabList)`
10 | background-color: #fff;
11 | box-shadow: inset 0 -1px 0px 0px #00000022;
12 | border: 0;
13 | `;
14 |
15 | Tab = styled(Tab)`
16 | & span {
17 | font-size: 0.9em;
18 | transition: color 0.18s;
19 | text-transform: uppercase;
20 | color: ${primaryColor};
21 | ${(props) => {
22 | return props.active ? `color: ${primaryColor};` : null;
23 | }}
24 | }
25 |
26 | ${(props) =>
27 | props.active
28 | ? `
29 | border-bottom: 2px solid ${primaryColor};
30 | `
31 | : null}
32 | &:hover {
33 | background-color: transparent;
34 | & span {
35 | color: ${primaryColor};
36 | }
37 |
38 | border-bottom: 2px solid ${primaryColor};
39 | }
40 |
41 | /* Ripple effect */
42 |
43 | background-position: center;
44 | transition: background 0.8s;
45 |
46 | &:hover {
47 | background: #fff radial-gradient(circle, transparent 1%, #fff 1%) center/15000%;
48 | }
49 | &:active {
50 | background-color: ${primaryColor + '22'};
51 | background-size: 100%;
52 | transition: background 0s;
53 | }
54 | `;
55 |
56 | ActionButton = styled(ActionButton)`
57 | background-color: transparent;
58 | border-radius: 0;
59 | &:hover {
60 | background-color: #eee;
61 | }
62 | `;
63 |
64 | Panel = styled(Panel)`
65 | padding: 30px 30px;
66 | `;
67 |
68 | export default {
69 | TabList: TabList,
70 | ActionButton: ActionButton,
71 | Tab: Tab,
72 | Panel: Panel,
73 | };
74 |
--------------------------------------------------------------------------------
/packages/themes/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "jsx": "react",
7 | "module": "es6",
8 | "moduleResolution": "node",
9 | "noImplicitAny": true,
10 | "preserveConstEnums": true,
11 | "target": "es5",
12 | "allowSyntheticDefaultImports": true,
13 | "noEmitOnError": true,
14 | "declaration": true,
15 | "declarationDir": "dist"
16 | },
17 | "exclude": ["node_modules"],
18 | "include": ["src/**/*"]
19 | }
20 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | singleQuote: true,
4 | tabWidth: 4
5 | }
6 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | // import { terser } from 'rollup-plugin-terser';
3 | import json from '@rollup/plugin-json';
4 | import typescript from '@rollup/plugin-typescript';
5 | import resolve from '@rollup/plugin-node-resolve';
6 | import styles from 'rollup-plugin-styles';
7 |
8 | const rootPackagePath = process.cwd();
9 | const input = path.join(rootPackagePath, 'src/index.ts');
10 |
11 | const pkg = require(path.join(rootPackagePath, 'package.json'));
12 |
13 | // const outputDir = path.join(rootPackagePath, 'dist');
14 | // const pgkName = pkg.name.split('/').pop();
15 |
16 | const external = [
17 | ...Object.keys(pkg.dependencies || {}),
18 | ...Object.keys(pkg.devDependencies || {}),
19 | ...Object.keys(pkg.peerDependencies || {}),
20 | ];
21 |
22 | const plugins = [styles(), json(), resolve(), typescript()];
23 |
24 | export default [
25 | {
26 | input,
27 | output: {
28 | dir: 'dist',
29 | exports: 'named',
30 | format: 'cjs',
31 | },
32 | external,
33 | plugins,
34 | },
35 |
36 | // Minified UMD
37 | // {
38 | // input,
39 | // output: {
40 | // name: pgkName,
41 | // exports: "named",
42 | // file: path.join(outputDir, `bundle.min.js`),
43 | // format: "umd",
44 | // sourcemap: true
45 | // },
46 | // external,
47 | // plugins: [...plugins, terser()]
48 | // }
49 | ];
50 |
--------------------------------------------------------------------------------
/test/AsyncPanel.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AsyncPanel from '../src/AsyncPanel';
3 | import {mount} from 'enzyme';
4 |
5 | function fakePromiseFetch(type, shouldRej = false) {
6 | if (type === 'cb') {
7 | return function (cb) {
8 | setTimeout(() => {
9 | if (shouldRej) {
10 | cb(true);
11 | } else {
12 | cb(null, {
13 | content: 'promise fetched'
14 | });
15 | }
16 | }, 0);
17 | }
18 | } else if (type === 'promise') {
19 | return function () {
20 | return new Promise((res, rej) => {
21 | setTimeout(() => {
22 | if (shouldRej) {
23 | rej(true);
24 | } else {
25 | res({
26 | content: 'promise fetched'
27 | });
28 | }
29 | }, 0)
30 | })
31 | }
32 |
33 | }
34 | }
35 |
36 | function makeCallback(done, body) {
37 | return (...args) => {
38 | try {
39 | body(...args);
40 | done();
41 | } catch (error) {
42 | done.fail(error);
43 | }
44 | };
45 | }
46 |
47 | function testByLoadType(type) {
48 | describe(`${type} render`, () => {
49 | test('render loading content', () => {
50 | const loadContent = fakePromiseFetch(type);
51 | const component = mount(
52 | ({JSON.stringify(data)}
)}
54 | renderLoading={() => (loading
)}
55 | active={true} />
56 | );
57 | expect(component.text()).toEqual('loading');
58 | })
59 |
60 | test('render content when loadContent finished, by promise', (done) => {
61 | const loadContent = fakePromiseFetch(type);
62 | const component = mount(
63 | ({JSON.stringify(data)}
)}
65 | renderLoading={() => (loading
)}
66 | active={true} />
67 | );
68 | setTimeout(makeCallback(done, () => {
69 | expect(component.text()).toEqual('{"content":"promise fetched"}');
70 | }), 10);
71 | })
72 |
73 | test('show error when loadContent has error, by promise', (done) => {
74 | const loadContent = fakePromiseFetch(type, true);
75 | const component = mount(
76 | ({JSON.stringify(data)}
)}
78 | renderLoading={() => (loading
)}
79 | active={true} />
80 | );
81 | setTimeout(makeCallback(done, () => {
82 | expect(component.text()).toEqual('');
83 | }), 10);
84 | })
85 |
86 | test('if the panel become active, load content', (done) => {
87 | const loadContent = fakePromiseFetch(type);
88 | const component = mount(
89 | ({JSON.stringify(data)}
)}
91 | renderLoading={() => (loading
)} />
92 | );
93 | expect(component.text()).toEqual('');
94 | component.setProps({active: true});
95 | setTimeout(makeCallback(done, () => {
96 | expect(component.text()).toEqual(JSON.stringify({content: 'promise fetched'}));
97 | }), 10);
98 | })
99 |
100 | test('if cache, should not load again', (done) => {
101 | const loadContent = fakePromiseFetch(type);
102 | const component = mount(
103 | ({JSON.stringify(data)}
)}
105 | renderLoading={() => (loading
)}
106 | active={true}/>
107 | );
108 | setTimeout(makeCallback(done, () => {
109 | component.setProps({active: false});
110 | component.setProps({active: true});
111 | expect(component.text()).toEqual(JSON.stringify({content: 'promise fetched'}));
112 | }), 10);
113 | });
114 |
115 | test('if not cache, load again', (done) => {
116 | const loadContent = fakePromiseFetch(type);
117 | const component = mount(
118 | ({JSON.stringify(data)}
)}
120 | renderLoading={() => (loading
)}
121 | active={true}
122 | cache={false}/>
123 | );
124 | setTimeout(makeCallback(done, () => {
125 | component.setProps({active: false});
126 | component.setProps({active: true});
127 | expect(component.text()).toEqual('loading');
128 | }), 10);
129 | });
130 | })
131 | }
132 |
133 | testByLoadType('promise');
134 | testByLoadType('cb');
--------------------------------------------------------------------------------
/test/DragTabList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {mount} from 'enzyme';
3 | import DragTabList from '../src/DragTabList';
4 | import DragTab from '../src/DragTab';
5 | import tabListTest from './tabListTest';
6 | import toJson from 'enzyme-to-json';
7 |
8 | describe('DragTabList', () => {
9 | tabListTest('DragTab', DragTabList, DragTab);
10 |
11 | test('DragTabList should have required props', () => {
12 | const component = mount(
13 |
14 | Tab1
15 |
16 | );
17 | expect(typeof toJson(component).node.rendered.props.pressDelay).toBe('number');
18 | })
19 | });
20 |
21 |
--------------------------------------------------------------------------------
/test/Panel.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Panel from '../src/Panel';
3 | import { shallow } from 'enzyme';
4 | import 'jest-styled-components'
5 |
6 | test('render Panel', () => {
7 | const component = shallow(
8 |
9 | panel content
10 |
11 | );
12 | expect(component).toMatchSnapshot();
13 | })
14 |
--------------------------------------------------------------------------------
/test/PanelList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PanelList from '../src/PanelList';
3 | import Panel, {PanelStyle} from '../src/Panel';
4 | import {shallow, mount} from 'enzyme';
5 | import toJson from 'enzyme-to-json';
6 | import 'jest-styled-components';
7 |
8 | describe('render', () => {
9 | test('render one Panel', () => {
10 | const component = mount(
11 |
12 |
13 | panel content
14 |
15 |
16 | );
17 | expect(toJson(component)).toMatchSnapshot();
18 | });
19 |
20 | test('render multi Panel', () => {
21 | const component = mount(
22 |
23 |
24 | panel content
25 |
26 |
27 | panel content
28 |
29 |
30 | panel content
31 |
32 |
33 | );
34 | expect(toJson(component)).toMatchSnapshot();
35 | });
36 |
37 | test('return null if no child', () => {
38 | const component = shallow(
39 |
40 |
41 | );
42 | expect(component.html()).toEqual(null);
43 | })
44 | })
45 |
46 | test('custom style', () => {
47 | const component = shallow(
48 |
52 |
53 | panel content
54 |
55 |
56 | );
57 | expect(toJson(component)).toMatchSnapshot();
58 | })
--------------------------------------------------------------------------------
/test/SortMethod.test.js:
--------------------------------------------------------------------------------
1 | import SortMethod from '../src/SortMethod';
2 |
3 | describe('SortMethod test', () => {
4 | it('should call handleTabChange & handleTabSequence', () => {
5 | const handleTabChange = jest.fn();
6 | const handleTabSequence = jest.fn();
7 | const sortMethodFn = new SortMethod({handleTabChange, handleTabSequence});
8 | sortMethodFn.onSortEnd({oldIndex: 1, newIndex: 1});
9 | sortMethodFn.onSortEnd({oldIndex: 1, newIndex: 3});
10 | expect(handleTabChange).toBeCalled();
11 | expect(handleTabChange.mock.calls[0][0]).toEqual(1);
12 | expect(handleTabSequence).toBeCalled();
13 | expect(handleTabSequence.mock.calls[0][0]).toEqual({"newIndex": 3, "oldIndex": 1});
14 | })
15 | })
--------------------------------------------------------------------------------
/test/Tab.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import {mount} from 'enzyme';
4 | import 'jest-styled-components'
5 |
6 | import Tab, {TabStyle} from '../src/Tab';
7 |
8 | describe('Tab', () => {
9 | describe('render TabList', () => {
10 | it('render pure text', () => {
11 | const component = mount(
12 |
13 | tab
14 |
15 | );
16 | expect(component.html()).toMatchSnapshot();
17 | })
18 |
19 | it('render component.', () => {
20 | const component = mount(
21 |
22 |
23 |
24 | tab
25 |
26 |
27 | );
28 | expect(component.html()).toMatchSnapshot();
29 | })
30 | })
31 |
32 | describe('event', () => {
33 | it('onClick', () => {
34 | const mockHandleClick = jest.fn();
35 | const tabKey = 1;
36 | const component = mount(
37 |
39 | text
40 |
41 | );
42 | component.find('Tab').simulate('click');
43 | expect(mockHandleClick).toBeCalled();
44 | expect(mockHandleClick.mock.calls[0][0]).toEqual(tabKey);
45 | })
46 | })
47 |
48 | test('custom ListStyle style', () => {
49 | const CustomTabStyle = styled(TabStyle)`
50 | background-color: red;
51 | `;
52 | const component = mount(
53 |
54 | tab
55 |
56 | );
57 | expect(component).toHaveStyleRule('background-color', 'red');
58 | })
59 | })
60 |
61 |
--------------------------------------------------------------------------------
/test/TabList.test.js:
--------------------------------------------------------------------------------
1 | import TabList from '../src/TabList';
2 | import Tab from '../src/Tab';
3 | import tabListTest from './tabListTest';
4 |
5 | describe('TabList', () => {
6 | tabListTest('Tab', TabList, Tab);
7 | })
8 |
--------------------------------------------------------------------------------
/test/TabModal.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TabModal from '../src/TabModal';
3 | import {shallow} from 'enzyme';
4 | import noop from 'noop3';
5 |
6 | test('render Panel', () => {
7 | const component = shallow(
8 |
11 | fake child
12 |
13 | );
14 | expect(component).toMatchSnapshot();
15 | })
16 |
--------------------------------------------------------------------------------
/test/Tabs.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Tabs, DragTabList, DragTab, TabList, Tab, PanelList, Panel, ExtraButton} from '../src';
3 | import {shallow, mount} from 'enzyme';
4 |
5 | const normalComponent = (props = {}) => (
6 |
7 |
8 | Tab1
9 | Tab2
10 |
11 |
12 | Content1
13 | Content2
14 |
15 |
16 | );
17 |
18 | const dragComponent = (props = {}) => (
19 |
20 |
21 | DragTab1
22 | DragTab2
23 |
24 |
25 | Content1
26 | Content2
27 |
28 |
29 | );
30 |
31 | describe('render Tabs', () => {
32 | const testRender = (tabsComponent) => {
33 | const component = mount(tabsComponent);
34 | expect(component.html()).toMatchSnapshot();
35 | }
36 | it('normal tabs', () => {
37 | testRender(normalComponent())
38 | });
39 | it('normal drag tabs', () => {
40 | testRender(dragComponent());
41 | });
42 | it('normal tab with ExtraButton', () => {
43 | testRender(normalComponent({
44 | ExtraButton:
45 |
46 | +
47 |
48 | }))
49 | })
50 | it('normal drag with ExtraButton', () => {
51 | testRender(dragComponent({
52 | ExtraButton:
53 |
54 | +
55 |
56 | }))
57 | })
58 | });
59 |
60 | describe('props', () => {
61 | it('defaultIndex', () => {
62 | const defaultIndex = 1;
63 | const component = shallow(normalComponent({defaultIndex}));
64 | expect(component.state().activeIndex).toEqual(defaultIndex);
65 | })
66 |
67 | it('click tab, onTabChange callback', () => {
68 | const mockTabChange = jest.fn();
69 | const component = mount(normalComponent({onTabChange: mockTabChange}));
70 | component.find('Tab').at(1).simulate('click');
71 | expect(component.state().activeIndex).toEqual(1);
72 | expect(mockTabChange).toBeCalled();
73 | expect(mockTabChange.mock.calls[0][0]).toEqual(1);
74 | })
75 |
76 | describe('activeIndex', () => {
77 | it('show active index', () => {
78 | const mountComponent = mount(normalComponent({activeIndex: 1}));
79 | expect(mountComponent.state().activeIndex).toEqual(1);
80 | });
81 |
82 | it('do nothing when click tab', () => {
83 | const mockTabChange = jest.fn();
84 | const mountComponent = mount(normalComponent({onTabChange: mockTabChange, activeIndex: 1}));
85 | mountComponent.find('Tab').at(0).simulate('click');
86 | expect(mountComponent.state().activeIndex).toEqual(1);
87 | });
88 |
89 | it('update active tab when pass new activeKey', () => {
90 | const mockTabChange = jest.fn();
91 | const mountComponent = mount(normalComponent({onTabChange: mockTabChange, activeIndex: 1}));
92 | mountComponent.setProps({activeIndex: 0})
93 | expect(mountComponent.state().activeIndex).toEqual(0);
94 | })
95 | })
96 | })
97 |
--------------------------------------------------------------------------------
/test/__snapshots__/DragTabList.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`DragTabList custom ListStyle style 1`] = `
4 | .c0 {
5 | background-color: white;
6 | text-align: left;
7 | position: relative;
8 | white-space: nowrap;
9 | overflow: hidden;
10 | width: auto;
11 | padding: 0 0px 0 0px;
12 | }
13 |
14 | .c1 {
15 | overflow: hidden;
16 | }
17 |
18 | .c2 {
19 | padding-left: 0;
20 | position: relative;
21 | margin: 0;
22 | list-style: none;
23 | display: inline-block;
24 | -webkit-transition: -webkit-transform .3s cubic-bezier(.42,0,.58,1);
25 | -webkit-transition: transform .3s cubic-bezier(.42,0,.58,1);
26 | transition: transform .3s cubic-bezier(.42,0,.58,1);
27 | }
28 |
29 | .c3 {
30 | display: inline-block;
31 | padding: 10px 15px;
32 | -webkit-user-select: none;
33 | -moz-user-select: none;
34 | -ms-user-select: none;
35 | user-select: none;
36 | }
37 |
38 | .c3:hover {
39 | cursor: pointer;
40 | color: black;
41 | }
42 |
43 | .c4 {
44 | vertical-align: middle;
45 | }
46 |
47 |
84 |
134 |
171 |
208 |
209 |
212 |
247 |
250 |
251 |
277 |
280 |
283 |
315 |
319 |
325 |
331 |
335 |
340 |
348 |
391 |
399 |
400 |
426 |
429 | Tab1
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 | `;
455 |
456 | exports[`DragTabList render DragTab List 1`] = `
457 | .c0 {
458 | background-color: white;
459 | text-align: left;
460 | position: relative;
461 | white-space: nowrap;
462 | overflow: hidden;
463 | width: auto;
464 | padding: 0 0px 0 0px;
465 | }
466 |
467 | .c1 {
468 | overflow: hidden;
469 | }
470 |
471 | .c2 {
472 | padding-left: 0;
473 | position: relative;
474 | margin: 0;
475 | list-style: none;
476 | display: inline-block;
477 | -webkit-transition: -webkit-transform .3s cubic-bezier(.42,0,.58,1);
478 | -webkit-transition: transform .3s cubic-bezier(.42,0,.58,1);
479 | transition: transform .3s cubic-bezier(.42,0,.58,1);
480 | }
481 |
482 | .c3 {
483 | display: inline-block;
484 | padding: 10px 15px;
485 | -webkit-user-select: none;
486 | -moz-user-select: none;
487 | -ms-user-select: none;
488 | user-select: none;
489 | }
490 |
491 | .c3:hover {
492 | cursor: pointer;
493 | color: black;
494 | }
495 |
496 | .c4 {
497 | vertical-align: middle;
498 | }
499 |
500 |
503 |
519 |
522 |
525 |
526 |
529 |
564 |
567 |
568 |
594 |
597 |
600 |
632 |
636 |
642 |
648 |
652 |
657 |
665 |
708 |
716 |
717 |
743 |
746 | Tab1
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 | `;
772 |
--------------------------------------------------------------------------------
/test/__snapshots__/Panel.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`render Panel 1`] = `
4 |
10 | `;
11 |
--------------------------------------------------------------------------------
/test/__snapshots__/PanelList.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`custom style 1`] = `
4 |
5 |
38 |
39 | panel content
40 |
41 |
42 |
43 | `;
44 |
45 | exports[`render render multi Panel 1`] = `
46 | .c1 {
47 | background-color: white;
48 | text-align: left;
49 | padding: 20px 15px;
50 | }
51 |
52 | .c0 {
53 | background-color: white;
54 | text-align: left;
55 | padding: 20px 15px;
56 | display: none;
57 | }
58 |
59 |
62 |
63 |
68 |
75 |
111 |
118 |
119 |
120 |
121 |
126 |
133 |
169 |
176 |
177 | panel content
178 |
179 |
180 |
181 |
182 |
183 |
188 |
195 |
231 |
238 |
239 |
240 |
241 |
242 |
243 | `;
244 |
245 | exports[`render render one Panel 1`] = `
246 | .c0 {
247 | background-color: white;
248 | text-align: left;
249 | padding: 20px 15px;
250 | }
251 |
252 |
255 |
256 |
261 |
268 |
304 |
311 |
312 | panel content
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 | `;
321 |
--------------------------------------------------------------------------------
/test/__snapshots__/Tab.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Tab render TabList render component. 1`] = `" tab "`;
4 |
5 | exports[`Tab render TabList render pure text 1`] = `"tab "`;
6 |
--------------------------------------------------------------------------------
/test/__snapshots__/TabList.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`TabList custom ListStyle style 1`] = `
4 | .c0 {
5 | background-color: white;
6 | text-align: left;
7 | position: relative;
8 | white-space: nowrap;
9 | overflow: hidden;
10 | width: auto;
11 | padding: 0 0px 0 0px;
12 | }
13 |
14 | .c1 {
15 | overflow: hidden;
16 | }
17 |
18 | .c2 {
19 | padding-left: 0;
20 | position: relative;
21 | margin: 0;
22 | list-style: none;
23 | display: inline-block;
24 | -webkit-transition: -webkit-transform .3s cubic-bezier(.42,0,.58,1);
25 | -webkit-transition: transform .3s cubic-bezier(.42,0,.58,1);
26 | transition: transform .3s cubic-bezier(.42,0,.58,1);
27 | }
28 |
29 | .c3 {
30 | display: inline-block;
31 | padding: 10px 15px;
32 | -webkit-user-select: none;
33 | -moz-user-select: none;
34 | -ms-user-select: none;
35 | user-select: none;
36 | }
37 |
38 | .c3:hover {
39 | cursor: pointer;
40 | color: black;
41 | }
42 |
43 | .c4 {
44 | vertical-align: middle;
45 | }
46 |
47 |
84 |
85 |
88 |
123 |
126 |
127 |
153 |
156 |
159 |
191 |
195 |
201 |
209 |
252 |
260 |
261 |
287 |
290 | Tab1
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 | `;
310 |
311 | exports[`TabList render Tab List 1`] = `
312 | .c0 {
313 | background-color: white;
314 | text-align: left;
315 | position: relative;
316 | white-space: nowrap;
317 | overflow: hidden;
318 | width: auto;
319 | padding: 0 0px 0 0px;
320 | }
321 |
322 | .c1 {
323 | overflow: hidden;
324 | }
325 |
326 | .c2 {
327 | padding-left: 0;
328 | position: relative;
329 | margin: 0;
330 | list-style: none;
331 | display: inline-block;
332 | -webkit-transition: -webkit-transform .3s cubic-bezier(.42,0,.58,1);
333 | -webkit-transition: transform .3s cubic-bezier(.42,0,.58,1);
334 | transition: transform .3s cubic-bezier(.42,0,.58,1);
335 | }
336 |
337 | .c3 {
338 | display: inline-block;
339 | padding: 10px 15px;
340 | -webkit-user-select: none;
341 | -moz-user-select: none;
342 | -ms-user-select: none;
343 | user-select: none;
344 | }
345 |
346 | .c3:hover {
347 | cursor: pointer;
348 | color: black;
349 | }
350 |
351 | .c4 {
352 | vertical-align: middle;
353 | }
354 |
355 |
358 |
359 |
362 |
397 |
400 |
401 |
427 |
430 |
433 |
465 |
469 |
475 |
483 |
526 |
534 |
535 |
561 |
564 | Tab1
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 | `;
584 |
--------------------------------------------------------------------------------
/test/__snapshots__/TabModal.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`render Panel 1`] = `
4 |
13 |
17 |
18 | fake child
19 |
20 |
21 |
22 | `;
23 |
--------------------------------------------------------------------------------
/test/__snapshots__/Tabs.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`render Tabs normal drag tabs 1`] = `""`;
4 |
5 | exports[`render Tabs normal drag with ExtraButton 1`] = `""`;
6 |
7 | exports[`render Tabs normal tab with ExtraButton 1`] = `""`;
8 |
9 | exports[`render Tabs normal tabs 1`] = `""`;
10 |
--------------------------------------------------------------------------------
/test/enzyme-setup.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 |
6 | global.cancelAnimationFrame = (cb) => {
7 | setTimeout(cb, 0)
8 | }
9 |
10 | // Fail tests on any warning
11 | console.error = message => {
12 | throw new Error(message);
13 | };
14 |
15 |
--------------------------------------------------------------------------------
/test/helpers/delete.test.js:
--------------------------------------------------------------------------------
1 | import deleteHelper from '../../src/helpers/delete';
2 |
3 | it('case1', () => {
4 | expect(deleteHelper([0, 1, 2], 1)).toEqual([0, 2]);
5 | })
6 |
7 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | import index from '../src';
2 |
3 | test('Tabs need to be defined', () => {
4 | expect(index.Tabs).toBeDefined();
5 | });
6 |
7 | test('Tab need to be defined', () => {
8 | expect(index.Tab).toBeDefined();
9 | });
10 |
11 | test('DragTabList need to be defined', () => {
12 | expect(index.DragTabList).toBeDefined();
13 | });
14 |
15 | test('DragTab need to be defined', () => {
16 | expect(index.DragTab).toBeDefined();
17 | });
18 |
19 | test('Panel need to be defined', () => {
20 | expect(index.Panel).toBeDefined();
21 | });
22 |
23 | test('PanelList need to be defined', () => {
24 | expect(index.PanelList).toBeDefined();
25 | });
26 |
27 | test('ExtraButton need to be defined', () => {
28 | expect(index.ExtraButton).toBeDefined();
29 | });
30 |
31 | test('TabListStyle need to be defined', () => {
32 | expect(index.styled.TabListStyle).toBeDefined();
33 | });
34 |
35 | test('ActionButtonStyle need to be defined', () => {
36 | expect(index.styled.ActionButtonStyle).toBeDefined();
37 | });
38 |
39 | test('TabStyle need to be defined', () => {
40 | expect(index.styled.TabStyle).toBeDefined();
41 | });
42 |
43 | test('PanelStyle need to be defined', () => {
44 | expect(index.styled.PanelStyle).toBeDefined();
45 | });
46 |
--------------------------------------------------------------------------------
/test/shim.js:
--------------------------------------------------------------------------------
1 | global.requestAnimationFrame = (callback) => {
2 | setTimeout(callback, 0);
3 | };
4 |
--------------------------------------------------------------------------------
/test/tabListTest.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TabListStyle } from '../src/TabList';
3 | import { mount } from 'enzyme';
4 | import toJson from 'enzyme-to-json';
5 | import 'jest-styled-components';
6 | import styled from 'styled-components';
7 | import { BulletIcon, RightIcon } from '../src/IconSvg';
8 |
9 | const tabListTest = (type, TabListComponent, TabComponent) => {
10 | const shareComponent = (props) => (
11 |
12 | Tab1
13 | Tab2
14 |
15 | );
16 |
17 | test(`render ${type} List`, () => {
18 | const component = mount(
19 |
20 | Tab1
21 |
22 | );
23 | expect(toJson(component)).toMatchSnapshot();
24 | });
25 |
26 | test('custom ListStyle style', () => {
27 | const CustomListStyle = styled(TabListStyle)`
28 | background-color: red;
29 | `;
30 | const component = mount(
31 |
32 | Tab1
33 |
34 | );
35 | expect(toJson(component)).toMatchSnapshot();
36 | });
37 |
38 | describe('showModalButton', () => {
39 | const returnMountedButton = (showModalButton) => {
40 | const component = mount(shareComponent({ showModalButton }));
41 | return component.find(BulletIcon);
42 | };
43 |
44 | it('default: true => show', () => {
45 | expect(returnMountedButton(true)).toHaveLength(1);
46 | });
47 | it('false', () => {
48 | expect(returnMountedButton(false)).toHaveLength(0);
49 | });
50 | describe('number', () => {
51 | it('2', () => {
52 | expect(returnMountedButton(2)).toHaveLength(1);
53 | });
54 | it('100', () => {
55 | expect(returnMountedButton(100)).toHaveLength(0);
56 | });
57 |
58 | it('show modal button when new tab is added', () => {
59 | const component = mount(shareComponent({ showModalButton: 4 }));
60 | expect(component.html().includes('svg')).toEqual(false);
61 | component.setProps({
62 | children: [
63 | Tab1 ,
64 | Tab2 ,
65 | Tab3 ,
66 | Tab4 ,
67 | ],
68 | });
69 | expect(component.html().includes('svg')).toEqual(true);
70 | });
71 | });
72 | });
73 |
74 | describe('showArrowButton', () => {
75 | const returnMountedButton = (showArrowButton) => {
76 | const component = mount(shareComponent({ showArrowButton }));
77 | return component.find(RightIcon);
78 | };
79 | // because in test env it's containerWidth = 0, it always show arrow
80 | it('auto', () => {
81 | expect(returnMountedButton('auto')).toHaveLength(1);
82 | });
83 |
84 | it('true', () => {
85 | expect(returnMountedButton(true)).toHaveLength(1);
86 | });
87 |
88 | it('false', () => {
89 | expect(returnMountedButton(false)).toHaveLength(0);
90 | });
91 | });
92 | // the reason to test closable button at TabList not at Tab component
93 | // is because `DragTab` need to be wrapped by `DragTabList`
94 | it('click close button', () => {
95 | const mockTabChange = jest.fn();
96 | const component = mount(
97 |
98 | Tab1
99 | Tab2
100 |
101 | );
102 | const btn1 = component.find('CloseButton').at(1);
103 | btn1.simulate('click');
104 | expect(mockTabChange).toBeCalled();
105 | expect(mockTabChange.mock.calls[0][0]).toEqual({ type: 'delete', index: 1 });
106 | });
107 | };
108 |
109 | export default tabListTest;
110 |
--------------------------------------------------------------------------------
/test/utils/countTab.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Tabs, DragTabList, DragTab, TabList, Tab, PanelList, Panel} from '../../src';
3 | import {mount} from 'enzyme';
4 | import countTab from '../../src/utils/countTab';
5 |
6 | test('count DragTab', () => {
7 | const count = countTab(mount(
8 |
9 |
10 | DragTab1
11 | DragTab2
12 | DragTab3
13 |
14 |
15 | Content1
16 | Content2
17 | Content3
18 |
19 |
20 | ));
21 | expect(count).toEqual(3);
22 | })
23 |
24 | test('count Tab', () => {
25 | const count = countTab(mount(
26 |
27 |
28 | Tab1
29 | Tab2
30 | Tab3
31 |
32 |
33 | ));
34 | expect(count).toEqual(3);
35 | })
36 |
--------------------------------------------------------------------------------
/test/utils/isType.test.js:
--------------------------------------------------------------------------------
1 | import {DragTabList, DragTab, TabList, Tab} from '../../src';
2 | import {isTabList, isTab, isNumber} from '../../src/utils/isType';
3 |
4 | describe('isTabList', () => {
5 | it('DragTabList', () => {
6 | expect(isTabList({type: DragTabList})).toEqual(true);
7 | });
8 | it('TabList', () => {
9 | expect(isTabList({type: TabList})).toEqual(true);
10 | });
11 | })
12 |
13 | describe('isTab', () => {
14 | it('DragTab', () => {
15 | expect(isTab({type: DragTab})).toEqual(true);
16 | });
17 | it('Tab', () => {
18 | expect(isTab({type: Tab})).toEqual(true);
19 | });
20 | })
21 |
22 | it('isNumber', () => {
23 | expect(isNumber(1)).toEqual(true);
24 | expect(isNumber('2')).toEqual(true);
25 | expect(isNumber('sdfsdf')).toEqual(false);
26 | })
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "jsx": "react",
7 | "module": "es6",
8 | "moduleResolution": "node",
9 | "noImplicitAny": true,
10 | "preserveConstEnums": true,
11 | "target": "es5",
12 | "allowSyntheticDefaultImports": true,
13 | "noEmitOnError": true,
14 | "declaration": true,
15 | "declarationDir": "dist"
16 | },
17 | "exclude": ["node_modules"],
18 | "include": ["src/**/*"]
19 | }
20 |
--------------------------------------------------------------------------------