├── .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 | [![license](https://img.shields.io/github/license/onmotion/react-tabtab-next)](https://www.npmjs.com/package/@react-tabtab-next/tabtab) 4 | [![npm](https://img.shields.io/npm/v/@react-tabtab-next/tabtab.svg)](https://www.npmjs.com/package/@react-tabtab-next/tabtab) 5 | [![npm](https://img.shields.io/npm/dm/@react-tabtab-next/tabtab.svg?maxAge=43200&downloads)](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 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 304 | 305 | 306 | 307 | 308 | 309 | 314 | 315 | 316 | 317 | 318 | 319 | 322 | 323 | 324 | 325 | 326 | 327 | 331 | 332 | 333 | 334 | 335 | 336 | 341 | 342 | 343 | 344 | 345 | 346 | 349 | 350 | 351 | 352 | 361 | 362 | 365 | 366 | 367 |
propstypedefault
activeIndexnumbernullcontrol current activeIndex.
You need to pass new activeIndex value if you want to show different tab.
defaultIndexnumber0default selected index if active index is not provided
showModalButtonboolean
number
4 298 |
    299 |
  • true: always show button
  • 300 |
  • false: always hide button
  • 301 |
  • [number]: when number of tab >= [number], show button
  • 302 |
303 |
showArrowButtonauto
boolean
auto 310 |
  • auto: detect tab width, if they exceed container, show button
  • 311 |
  • true: always show button
  • 312 |
  • false: always hide button
  • 313 |
    ExtraButtonReact Nodenull 320 | customize extra button content, example: `+` button 321 |
    onTabChange(tabIndex) => {}null 328 | return tabIndex is clicked
    329 | You can use this api with activeIndex. When user click tab, update activeIndex. 330 |
    onTabSequenceChange(oldIndex, newIndex) => {}null 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 |
    onTabClose(index) => {}null 347 | When user click close button , this api will return the clicked close button index. 348 |
    customStyle 353 |
    354 | {
    355 |   TabList: React.Element,
    356 |   Tab: React.Element,
    357 |   Panel: React.Element,
    358 |   ActionButton: React.Element
    359 | }
    360 |
    theme 363 | customized tab style component 364 |
    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 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 |
    propstypedefault
    closablebooleanfalsewhether to show close button
    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 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 433 | 434 | 435 | 436 | 437 | 438 | 441 | 442 | 443 | 444 | 445 | 446 | 449 | 450 | 451 | 452 | 453 | 454 | 457 | 458 | 459 | 460 | 461 |
    propstypedefault
    loadContent * 430 | (cb) => cb(error, data) or
    431 | (cb) => Promise 432 |
    nullwhen loadContent finish, call the callback or you can return promise
    render * 439 | (data) => Component 440 | nullwhen finish loading data, render this component
    renderLoading * 447 | () => Component 448 | nullwhen it is loading data, render this component
    cache 455 | boolean 456 | trueshould cache the data
    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 |
    11 |

    react-tabtab-next

    12 | 19 | Star 20 | 21 |
    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 | 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 | 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 | 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 | 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`] = `"
    • DragTab1
    • DragTab2
    Content1
    "`; 4 | 5 | exports[`render Tabs normal drag with ExtraButton 1`] = `"
    • DragTab1
    • DragTab2
    Content1
    "`; 6 | 7 | exports[`render Tabs normal tab with ExtraButton 1`] = `"
    • Tab1
    • Tab2
    Content1
    "`; 8 | 9 | exports[`render Tabs normal tabs 1`] = `"
    • Tab1
    • Tab2
    Content1
    "`; 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 | --------------------------------------------------------------------------------