├── .babelrc ├── .eslintignore ├── .eslintrc.cjs ├── .gitbook.yaml ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── demo ├── clear-screen.js ├── index.jsx ├── ink-tab-demo-column.json └── ink-tab-demo-row.json ├── docs ├── .gitbook │ └── assets │ │ ├── demo-column.svg │ │ └── demo.svg ├── README.md ├── SUMMARY.md ├── api │ ├── tab.md │ └── tabs.md ├── hacking-ink-tab.md ├── usage.md └── who-is-using-ink-tab.md ├── media ├── demo-column.svg ├── demo.svg └── demoNoIndex.svg ├── package.json ├── src └── index.tsx ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "module", 3 | "presets": [ 4 | "@babel/preset-typescript", 5 | "@babel/preset-react", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | "modules": false 10 | } 11 | ] 12 | ], 13 | "plugins": [ 14 | "@babel/proposal-class-properties", 15 | "@babel/proposal-object-rest-spread" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | 4 | # don't lint build output (make sure it's set to your correct build folder name) 5 | dist 6 | 7 | *.d.ts 8 | 9 | lib/index.js 10 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: ['plugin:react/recommended', 'standard-with-typescript', 'prettier'], 7 | overrides: [], 8 | parserOptions: { 9 | project: './tsconfig.json', 10 | ecmaVersion: 'latest', 11 | sourceType: 'module', 12 | }, 13 | plugins: ['react'], 14 | rules: {}, 15 | settings: { 16 | react: { 17 | version: 'detect', 18 | }, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jdeniau 2 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | versions: 12 | - node: '14.x' 13 | ink: '^4.0.0' 14 | - node: '16.x' 15 | ink: '^4.0.0' 16 | - node: '18.x' 17 | - node: '19.x' 18 | - nome: '20.x' 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Use Node.js ${{ matrix.versions.node }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.versions.node }} 27 | 28 | - name: force ink 4 for older node versions 29 | if: ${{ matrix.versions.ink }} 30 | run: yarn add ink@${{ matrix.versions.ink }} 31 | 32 | - name: npm install, build, and test 33 | run: | 34 | yarn install 35 | yarn run lint 36 | yarn check-types 37 | env: 38 | CI: true 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | demo/index.compiled.js 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry="https://registry.npmjs.org" 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | tsconfig.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | 4 | node_js: 5 | - 'node' 6 | - 'lts/*' 7 | 8 | script: yarn lint && yarn check-types 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 5.1.0 4 | 5 | - Accept ink 5. The only breaking change is that [ink requires node > 18](https://github.com/vadimdemedes/ink/releases/tag/v5.0.0). ink-tab still accept older node version with ink 4 though. 6 | 7 | ## 5.0.0 8 | 9 | - [BREAKING] require ink 4. See [ink 4.0.0 release](https://github.com/vadimdemedes/ink/releases/tag/v4.0.0) to upgrade. `ink-tab` did not change aside from that. 10 | 11 | ## 4.3.1 12 | 13 | Fix TS types 14 | 15 | ## 4.3.0 16 | 17 | Possiblility to override active tab colors 18 | 19 | ## 4.2.4 20 | 21 | Fix react peerDep 22 | 23 | ## 4.2.3 24 | 25 | Remove `babel-plugin-typescript-to-proptypes` 26 | 27 | ## 4.2.2 28 | 29 | Remove `prop-types` package from dependencies 30 | 31 | ## 4.2.1 32 | 33 | Change types to handle "children" due to @types/react change 34 | 35 | ## 4.2.0 36 | 37 | ### Added 38 | 39 | Add the `showIndex` on the `Tabs` component. It allows you to disable showing tab indexes. 40 | 41 | ## 4.1.0 42 | 43 | ### Added 44 | 45 | Add the `defaultValue` on the `Tabs` component. It allows you to set initial opened tab to any tab you want instead of the first one. Thanks to @zarezadeh (#29) 46 | 47 | ## 4.0.0 48 | 49 | ### Changed 50 | 51 | - Upgrade dependencies to ink `^3.0.0` 52 | - `hasFocus` has been renamed to `isFocused` to match ink API. 53 | - Drop support for node < 10 (ink does not supports them anyway). 54 | 55 | ## 3.0.2 56 | 57 | ### Changed 58 | 59 | - Smaller build 60 | 61 | ## 3.0.1 62 | 63 | ### Fixed 64 | 65 | - Fix issue with typescript and Component. See #23 66 | 67 | ## 3.0.0 68 | 69 | ### API Compatibility 70 | 71 | Version 3.0.0 maintains API compatibility with 2.x but due to major internal changes and potential behavior differences across nearly all API surfaces, semver dictates a major version bump. 72 | 73 | ### Changed 74 | 75 | - Moved to typescript 😃 76 | 77 | ### Added 78 | 79 | - Added the `hasFocus` props. 80 | 81 | ## 2.2.1 82 | 83 | ### Fixed 84 | 85 | Fixed type module node ( Thanks to @aequasi ) 86 | 87 | ## 2.2.0 88 | 89 | ### Added 90 | 91 | Add typescript definition files #17. (Thanks to @sw-yx ) 92 | 93 | ## 2.1.4 94 | 95 | ### Fixed 96 | 97 | Fix issue with ink >= 2.4.0 preventing keypress events to be triggered. See 98 | 99 | ## 2.1.3 100 | 101 | ### Changed 102 | 103 | Fix issue from 2.1.2 where the tabs did appear in column instead of in row. 104 | 105 | ## 2.1.2 106 | 107 | ### Changed 108 | 109 | Add better proptypes of TabsWithStdin to avoid issue with StdinContext & nodemon. Fixes #9 110 | 111 | ## 2.1.1 112 | 113 | ### Changed 114 | 115 | - Fix small issue with proptypes and default width parameters 116 | 117 | ## 2.1.0 118 | 119 | ### Changed 120 | 121 | - [Minor BC Break] Need ink ^2.1.0 (use automatic keypress event, added in 2.0.4) 122 | - Fix issue with CTRL-C and multiple instances 123 | - Use the `width` parameter when flexDirection is set to `column(-reverse)` to set the separator width 124 | - Expose a `keyMap` object to override default keyMap 125 | 126 | ## 2.0.1 127 | 128 | ### Changed 129 | 130 | Remove unmaintained keypress in favor of node "readline" 131 | 132 | ## 2.0.0 133 | 134 | - [Breaking] Use `ink` v2 (and thus React + react-reconcilier) 135 | - [Breaking] drop support for Node < 8. (ink 2 is dropping support anyway) 136 | 137 | ## 1.3.0 138 | 139 | ### Changed 140 | 141 | - Use Babel 7. 142 | 143 | ## 1.2.1 144 | 145 | ### Changed 146 | 147 | - Upgrade peerDependency to ink ^0.5.0 See [#4](https://github.com/jdeniau/ink-tab/pull/4) 148 | 149 | ## 1.2.0 150 | 151 | ### Changed 152 | 153 | - Upgrade dependency to ink ^0.5.0 See [#4](https://github.com/jdeniau/ink-tab/pull/4) 154 | - Tab index now starts at 1 ("1. 2. 3." instead of "0. 1. 2.") 155 | 156 | ## 1.1.0 157 | 158 | ### Added 159 | 160 | - Cycle through tabs when hitting bounds 161 | - Navigation through tabs with TAB (move to next tab) and SHIFT+TAB (move to previous tab) 162 | - Navigation with "META" + Tab number 163 | 164 | ### Fixed 165 | 166 | Fixed a bug when hitting bounds of tabs 167 | 168 | ## 1.0.1 169 | 170 | ### Changed 171 | 172 | Allow version 0.4.x of ink 173 | 174 | ## 1.0.0 175 | 176 | Initial version, working version of ink-tab 177 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Julien Deniau 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: demo clean install 2 | 3 | demo: clean install media/demo.svg media/demo-column.svg 4 | 5 | install: node_modules 6 | 7 | node_modules: yarn.lock 8 | yarn install 9 | 10 | clean: 11 | rm demo/ink-tab-demo-row.json || true 12 | rm demo/ink-tab-demo-column.json || true 13 | 14 | media/demo.svg: demo/ink-tab-demo-row.json 15 | cat demo/ink-tab-demo-row.json | yarn svg-term --window --no-cursor --width 70 --height 15 --out media/demo.svg 16 | 17 | media/demo-column.svg: demo/ink-tab-demo-column.json 18 | cat demo/ink-tab-demo-column.json | yarn svg-term --window --no-cursor --width 70 --height 15 --out media/demo-column.svg 19 | 20 | demo/ink-tab-demo-row.json: clean 21 | asciinema rec demo/ink-tab-demo-row.json -c 'yarn demo' 22 | 23 | demo/ink-tab-demo-column.json: clean 24 | asciinema rec demo/ink-tab-demo-column.json -c 'yarn demo --column' 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ink-tab 2 | 3 | > Tab component for [Ink](https://github.com/vadimdemedes/ink). 4 | 5 | ## ink version dependency 6 | 7 | | ink version | ink-tab version | 8 | | ----------- | --------------- | 9 | | 4.x, 5.x | 5.x | 10 | | 3.x | 4.x | 11 | 12 | ## Demo 13 | 14 | ### With Index 15 | 16 | ![Demo](media/demo.svg) 17 | 18 | ### Without Index 19 | 20 | ![Demo Without Index](media/demoNoIndex.svg) 21 | 22 | [Read the documentation](https://jdeniau.gitbook.io/ink-tab/) 23 | -------------------------------------------------------------------------------- /demo/clear-screen.js: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/tawseefnabi/console-clear/blob/master/index.js 2 | 3 | process.stdout.write( 4 | process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H' 5 | ); 6 | -------------------------------------------------------------------------------- /demo/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { render, Box, Text, useFocus } from 'ink'; 4 | import { Tabs, Tab } from '../lib/index.js'; 5 | 6 | function FocusableTabs(props) { 7 | const { isFocused } = useFocus({ autoFocus: true }); 8 | 9 | return ; 10 | } 11 | 12 | function Focusable({ children }) { 13 | const { isFocused } = useFocus(); 14 | 15 | return ( 16 | 17 | {children} 18 | {isFocused && with focus} 19 | 20 | ); 21 | } 22 | 23 | const MainContent = ({ activeTab }) => ( 24 | 25 | {activeTab === 'foo' && 'Selected tab is "foo"'} 26 | {activeTab === 'bar' && 'Selected tab is "bar"'} 27 | {activeTab === 'baz' && 'Selected tab is "baz"'} 28 | 29 | ); 30 | 31 | MainContent.propTypes = { 32 | activeTab: PropTypes.string.isRequired, 33 | }; 34 | 35 | class TabExample extends Component { 36 | static propTypes = { 37 | direction: PropTypes.oneOf(['row', 'column']).isRequired, 38 | isFocusManagedByInk: PropTypes.bool.isRequired, 39 | defaultTab: PropTypes.string, 40 | }; 41 | 42 | constructor(props) { 43 | super(props); 44 | 45 | this.state = { 46 | activeTab: null, 47 | }; 48 | 49 | this.handleTabChange = this.handleTabChange.bind(this); 50 | } 51 | 52 | handleTabChange(name /* , child */) { 53 | this.setState({ 54 | activeTab: name, 55 | }); 56 | } 57 | 58 | render() { 59 | const { direction, isFocusManagedByInk, defaultTab } = this.props; 60 | const { activeTab } = this.state; 61 | 62 | const TabElement = isFocusManagedByInk ? FocusableTabs : Tabs; 63 | 64 | return ( 65 | 69 | {activeTab && } 70 | 71 | {direction === 'column' && ( 72 | 73 | 74 | 75 | )} 76 | 77 | {isFocusManagedByInk && ( 78 | 79 | focus switcher 80 | 81 | )} 82 | 83 | 91 | Foo 92 | Bar 93 | Baz 94 | 95 | 96 | ); 97 | } 98 | } 99 | 100 | render( 101 | , 106 | { debug: false } 107 | ); 108 | -------------------------------------------------------------------------------- /demo/ink-tab-demo-column.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "width": 177, 4 | "height": 139, 5 | "timestamp": 1551698378, 6 | "env": { "SHELL": "/bin/zsh", "TERM": "xterm-256color" } 7 | }[(0.237157, "o", "\u001b[2K")][ 8 | (0.238725, "o", "\u001b[1G\u001b[1myarn run v1.13.0\u001b[22m\r\n") 9 | ][(0.303485, "o", "\u001b[2K")][ 10 | (0.30357, 11 | "o", 12 | "\u001b[1G\u001b[2m$ clear && babel-node demo/index.js --column\u001b[22m\r\n") 13 | ][(0.32783, "o", "\u001b[H\u001b[2J\u001b[3J")][(1.246181, "o", "\u001b[?25l")][ 14 | (1.295048, "o", "\u001b[?25l") 15 | ][ 16 | (1.297107, 17 | "o", 18 | "\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mF\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m Selected tab is \"foo\"\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 19 | ][(2.199611, "o", "\u001b[?25l")][ 20 | (2.20087, 21 | "o", 22 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mFoo Selected tab is \"bar\"\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mB\u001b[49m\u001b[39m\u001b[30m\u001b[42ma\u001b[49m\u001b[39m\u001b[30m\u001b[42mr\u001b[49m\u001b[39m\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 23 | ][(2.990738, "o", "\u001b[?25l")][ 24 | (2.991528, 25 | "o", 26 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mFoo Selected tab is \"baz\"\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mB\u001b[49m\u001b[39m\u001b[30m\u001b[42ma\u001b[49m\u001b[39m\u001b[30m\u001b[42mz\u001b[49m\u001b[39m\r\n\r\n") 27 | ][(4.093104, "o", "\u001b[?25l")][ 28 | (4.094156, 29 | "o", 30 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mF\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m Selected tab is \"foo\"\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 31 | ][(4.925664, "o", "\u001b[?25l")][ 32 | (4.926672, 33 | "o", 34 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mFoo Selected tab is \"bar\"\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mB\u001b[49m\u001b[39m\u001b[30m\u001b[42ma\u001b[49m\u001b[39m\u001b[30m\u001b[42mr\u001b[49m\u001b[39m\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 35 | ][(5.727899, "o", "\u001b[?25l")][ 36 | (5.729039, 37 | "o", 38 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mF\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m Selected tab is \"foo\"\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\r\n\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\u001b[2m─\u001b[22m\r\n\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 39 | ][(6.319722, "o", "\u001b[?25h\u001b[?25h")][ 40 | (6.331316, "o", "\u001b[2K\u001b[1G") 41 | ][(6.331546, "o", "Done in 6.10s.\r\n")] 42 | -------------------------------------------------------------------------------- /demo/ink-tab-demo-row.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "width": 177, 4 | "height": 139, 5 | "timestamp": 1551698369, 6 | "env": { "SHELL": "/bin/zsh", "TERM": "xterm-256color" } 7 | }[(0.235981, "o", "\u001b[2K")][ 8 | (0.237114, "o", "\u001b[1G\u001b[1myarn run v1.13.0\u001b[22m\r\n") 9 | ][(0.301144, "o", "\u001b[2K")][ 10 | (0.301451, 11 | "o", 12 | "\u001b[1G\u001b[2m$ clear && babel-node demo/index.js\u001b[22m\r\n") 13 | ][(0.346071, "o", "\u001b[H\u001b[2J\u001b[3J")][ 14 | (1.246222, "o", "\u001b[?25l") 15 | ][(1.294293, "o", "\u001b[?25l")][ 16 | (1.296223, 17 | "o", 18 | "Selected tab is \"foo\"\r\n\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mF\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 19 | ][(2.341421, "o", "\u001b[?25l")][ 20 | (2.342269, 21 | "o", 22 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[GSelected tab is \"bar\"\r\n\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mFoo\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mB\u001b[49m\u001b[39m\u001b[30m\u001b[42ma\u001b[49m\u001b[39m\u001b[30m\u001b[42mr\u001b[49m\u001b[39m\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 23 | ][(3.497947, "o", "\u001b[?25l")][ 24 | (3.498805, 25 | "o", 26 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[GSelected tab is \"baz\"\r\n\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mFoo\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mB\u001b[49m\u001b[39m\u001b[30m\u001b[42ma\u001b[49m\u001b[39m\u001b[30m\u001b[42mz\u001b[49m\u001b[39m\r\n\r\n") 27 | ][(4.403149, "o", "\u001b[?25l")][ 28 | (4.404255, 29 | "o", 30 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[GSelected tab is \"foo\"\r\n\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mF\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 31 | ][(5.491362, "o", "\u001b[?25l")][ 32 | (5.49228, 33 | "o", 34 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[GSelected tab is \"bar\"\r\n\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mFoo\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mB\u001b[49m\u001b[39m\u001b[30m\u001b[42ma\u001b[49m\u001b[39m\u001b[30m\u001b[42mr\u001b[49m\u001b[39m\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 35 | ][(6.231968, "o", "\u001b[?25l")][ 36 | (6.232794, 37 | "o", 38 | "\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[GSelected tab is \"foo\"\r\n\u001b[38;2;128;128;128m1\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39m\u001b[30m\u001b[42mF\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[30m\u001b[42mo\u001b[49m\u001b[39m\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m2\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBar\u001b[2m \u001b[22m\u001b[2m|\u001b[22m\u001b[2m \u001b[22m\u001b[38;2;128;128;128m3\u001b[39m\u001b[38;2;128;128;128m.\u001b[39m\u001b[38;2;128;128;128m \u001b[39mBaz\r\n\r\n") 39 | ][(6.904149, "o", "\u001b[?25h")][(6.906088, "o", "\u001b[?25h")][ 40 | (6.914694, "o", "\u001b[2K\u001b[1GDone in 6.68s.\r\n") 41 | ] 42 | -------------------------------------------------------------------------------- /docs/.gitbook/assets/demo-column.svg: -------------------------------------------------------------------------------- 1 | yarnrunv1.13.01.FooSelectedtabis"foo"──────2.Bar3.Baz1.FooSelectedtabis"bar"2.Bar1.FooSelectedtabis"baz"3.Baz$clear&&babel-nodedemo/index.js--columnDonein6.10s. -------------------------------------------------------------------------------- /docs/.gitbook/assets/demo.svg: -------------------------------------------------------------------------------- 1 | yarnrunv1.13.0Selectedtabis"foo"1.Foo|2.Bar|3.BazSelectedtabis"bar"1.Foo|2.Bar|3.BazSelectedtabis"baz"1.Foo|2.Bar|3.Baz$clear&&babel-nodedemo/index.jsDonein6.68s. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ink-tab 2 | 3 | Tab component for [Ink](https://github.com/vadimdemedes/ink). 4 | 5 | ### Demo 6 | 7 | ![Demo](https://github.com/jdeniau/ink-tab/raw/main/media/demo.svg?sanitize=true) 8 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [ink-tab](README.md) 4 | * [Usage](usage.md) 5 | 6 | ## API 7 | 8 | * [Tabs](api/tabs.md) 9 | * [Tab](api/tab.md) 10 | * [Who is using ink-tab ?](who-is-using-ink-tab.md) 11 | * [Hacking ink-tab](hacking-ink-tab.md) 12 | 13 | -------------------------------------------------------------------------------- /docs/api/tab.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Tab component 3 | --- 4 | 5 | # Tab 6 | 7 | ### **name** 8 | 9 | Type: `string` the name that will be returned in the `onChange` function. 10 | 11 | It is the name you should pass to `Tabs.defaultValue` if you want to change the default opened tab. 12 | -------------------------------------------------------------------------------- /docs/api/tabs.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Tabs component 3 | --- 4 | 5 | # Tabs 6 | 7 | ### **children** 8 | 9 | `Tabs` must only contain `Tab` children 10 | 11 | ### **onChange** 12 | 13 | Type: `Function` 14 | 15 | Parameters: 16 | 17 | - `name`: the name specified in the `name` prop 18 | - `activeTab`: the current active tab component 19 | 20 | `onChange` function is called on component start and on every changes in tabs 21 | 22 | ### **colors** 23 | 24 | You can override the default color of the active tab: 25 | 26 | ```tsx 27 | 30 | ``` 31 | 32 | ### **keyMap** 33 | 34 | The default keyMap is the following: 35 | 36 | - use left / right or up / down to move to previous / next tab \(depending if you use column or row direction\), 37 | - use shift+tab / tab to move to previous / next tab (disabled if the focus is managed externally, see [#Focus management](Focus management)), 38 | - use meta \(alt\) + 1-9 number to go to selected tab. 39 | 40 | You can override it this way: 41 | 42 | ```javascript 43 | 49 | ``` 50 | 51 | ### **flexDirection** 52 | 53 | The `` component pass every props given to the containing `` of the tabs. This way you can easily build a vertical tabs component by using ``. 54 | 55 | ![Demo column](https://github.com/jdeniau/ink-tab/raw/main/media/demo-column.svg?sanitize=true) 56 | 57 | ### **width** 58 | 59 | If you specify a `width` to ` 86 | Some tab 87 | 88 | ); 89 | } 90 | ``` 91 | 92 | If ink focus management is used, then the "TAB" key won't work for tab switching. 93 | -------------------------------------------------------------------------------- /docs/hacking-ink-tab.md: -------------------------------------------------------------------------------- 1 | # Hacking ink-tab 2 | 3 | Issues and pull requests are welcome 🙏. 4 | 5 | You can run the demo script by running `yarn demo` or `npm run demo` 6 | 7 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ### Installation 4 | 5 | {% tabs %} 6 | {% tab title="npm" %} 7 | 8 | ```bash 9 | npm install ink-tab 10 | ``` 11 | 12 | {% endtab %} 13 | 14 | {% tab title="yarn" %} 15 | 16 | ```bash 17 | yarn add ink-tab 18 | ``` 19 | 20 | {% endtab %} 21 | {% endtabs %} 22 | 23 | ### Usage 24 | 25 | {% tabs %} 26 | 27 | {% tab title="Javascript : Functional component" %} 28 | 29 | ```jsx 30 | import React, { useState } from 'react'; 31 | import { render, Box, Color } from 'ink'; 32 | import { Tabs, Tab } from 'ink-tab'; 33 | 34 | function TabExample(props) { 35 | const [activeTabName, setActiveTabName] = useState(null); 36 | 37 | // the handleTabChange method get two arguments: 38 | // - the tab name 39 | // - the React tab element 40 | function handleTabChange(name, activeTab) { 41 | // set the active tab name to do what you want with the content 42 | setActiveTabName(name); 43 | } 44 | 45 | return ( 46 | 47 | 48 | 49 | {activeTabName === 'foo' && 'Selected tab is "foo"'} 50 | {activeTabName === 'bar' && 'Selected tab is "bar"'} 51 | {activeTabName === 'baz' && 'Selected tab is "baz"'} 52 | 53 | 54 | 55 | 56 | Foo 57 | Bar 58 | Baz 59 | 60 | 61 | ); 62 | } 63 | 64 | render(); 65 | ``` 66 | 67 | {% endtab %} 68 | 69 | {% tab title="JavaScript : Class component" %} 70 | 71 | ```jsx 72 | import React, { Component } from 'react'; 73 | import { render, Box, Color } from 'ink'; 74 | import { Tabs, Tab } from 'ink-tab'; 75 | 76 | class TabExample extends Component { 77 | constructor(props) { 78 | super(props); 79 | 80 | this.state = { 81 | activeTabName: null, 82 | }; 83 | 84 | this.handleTabChange = this.handleTabChange.bind(this); 85 | } 86 | 87 | // the handleTabChange method get two arguments: 88 | // - the tab name 89 | // - the React tab element 90 | handleTabChange(name, activeTab) { 91 | // set the active tab name to do what you want with the content 92 | this.setState({ 93 | activeTabName: name, 94 | }); 95 | } 96 | 97 | render() { 98 | return ( 99 | 100 | 101 | 102 | {this.state.activeTabName === 'foo' && 'Selected tab is "foo"'} 103 | {this.state.activeTabName === 'bar' && 'Selected tab is "bar"'} 104 | {this.state.activeTabName === 'baz' && 'Selected tab is "baz"'} 105 | 106 | 107 | 108 | 109 | Foo 110 | Bar 111 | Baz 112 | 113 | 114 | ); 115 | } 116 | } 117 | 118 | render(); 119 | ``` 120 | 121 | {% endtab %} 122 | {% endtabs %} 123 | -------------------------------------------------------------------------------- /docs/who-is-using-ink-tab.md: -------------------------------------------------------------------------------- 1 | # Who is using ink-tab ? 2 | 3 | I created `ink-tab` at first to use in [changelog-view](https://github.com/jdeniau/changelog-view), a small cli to help you read what did change in your dependencies since your current version, reading CHANGELOG.md, HISTORY.md, github releases, etc. 4 | 5 | If you use it in a component, feel free to tell me, I will be pleased ! 6 | 7 | -------------------------------------------------------------------------------- /media/demo-column.svg: -------------------------------------------------------------------------------- 1 | yarnrunv1.13.01.FooSelectedtabis"foo"──────2.Bar3.Baz1.FooSelectedtabis"bar"2.Bar1.FooSelectedtabis"baz"3.Baz$clear&&babel-nodedemo/index.js--columnDonein6.10s. -------------------------------------------------------------------------------- /media/demo.svg: -------------------------------------------------------------------------------- 1 | yarnrunv1.13.0Selectedtabis"foo"1.Foo|2.Bar|3.BazSelectedtabis"bar"1.Foo|2.Bar|3.BazSelectedtabis"baz"1.Foo|2.Bar|3.Baz$clear&&babel-nodedemo/index.jsDonein6.68s. -------------------------------------------------------------------------------- /media/demoNoIndex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 88 | 89 | 90 | yarnrunv1.13.0 92 | Selectedtabis"foo" 94 | 95 | Foo|Bar|Baz 98 | 99 | Selectedtabis"bar" 101 | Foo| 102 | Bar|Baz 104 | 105 | Selectedtabis"baz" 107 | Foo|Bar| 109 | Baz 110 | 111 | 112 | 113 | 114 | 115 | 116 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | $clear&&babel-nodedemo/index.js 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | Donein6.68s. 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ink-tab", 3 | "type": "module", 4 | "version": "5.1.0", 5 | "description": "Tab component for Ink", 6 | "main": "lib/index.js", 7 | "exports": "./lib/index.js", 8 | "typings": "lib/index.d.ts", 9 | "engines": { 10 | "node": ">=14.16" 11 | }, 12 | "scripts": { 13 | "check-types": "tsc --noEmit", 14 | "type-check:watch": "npm run type-check -- --watch", 15 | "build": "yarn rimraf lib && yarn build:types && yarn build:js", 16 | "build:types": "tsc --emitDeclarationOnly", 17 | "build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline", 18 | "lint": "eslint src/ --ext .js,.jsx,.ts,.tsx", 19 | "test": "echo \"Error: no test specified\" && exit 1", 20 | "demo": "yarn build && node demo/clear-screen.js && babel demo/index.jsx > demo/index.compiled.js && node demo/index.compiled.js", 21 | "prepublishOnly": "npm run build" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/jdeniau/ink-tab.git" 26 | }, 27 | "keywords": [ 28 | "Ink", 29 | "tab" 30 | ], 31 | "author": "Julien Deniau", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/jdeniau/ink-tab/issues" 35 | }, 36 | "homepage": "https://github.com/jdeniau/ink-tab#readme", 37 | "peerDependencies": { 38 | "@types/react": "^18.0.0", 39 | "ink": "^4.0.0 || ^5.0.0", 40 | "react": "^18.0.0" 41 | }, 42 | "peerDependenciesMeta": { 43 | "@types/react": { 44 | "optional": true 45 | } 46 | }, 47 | "devDependencies": { 48 | "@babel/cli": "^7.21.0", 49 | "@babel/core": "^7.21.4", 50 | "@babel/node": "^7.20.7", 51 | "@babel/plugin-proposal-class-properties": "^7.18.6", 52 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7", 53 | "@babel/preset-env": "^7.21.4", 54 | "@babel/preset-react": "^7.18.6", 55 | "@babel/preset-typescript": "^7.21.4", 56 | "@types/react": "^18.0.0", 57 | "@typescript-eslint/eslint-plugin": "^5.43.0", 58 | "eslint": "^8.0.1", 59 | "eslint-config-prettier": "^8.8.0", 60 | "eslint-config-standard-with-typescript": "^34.0.1", 61 | "eslint-plugin-import": "^2.25.2", 62 | "eslint-plugin-n": "^15.0.0", 63 | "eslint-plugin-promise": "^6.0.0", 64 | "eslint-plugin-react": "^7.32.2", 65 | "husky": "^3.1.0", 66 | "ink": "^5.0.0", 67 | "lint-staged": "^9.5.0", 68 | "prettier": "^2.8.8", 69 | "prop-types": "^15.7.2", 70 | "react": "^18.0.0", 71 | "rimraf": "^3.0.0", 72 | "svg-term-cli": "^2.1.1", 73 | "typescript": "*" 74 | }, 75 | "husky": { 76 | "hooks": { 77 | "pre-commit": "lint-staged" 78 | } 79 | }, 80 | "lint-staged": { 81 | "*.(js|jsx|ts|tsx|json|md)": [ 82 | "prettier --write", 83 | "git add" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import readline from 'readline'; 3 | import { Box, type StdinProps, type BoxProps, Text, useStdin } from 'ink'; 4 | 5 | type ExtractFCProps = T extends React.FunctionComponent ? P : never; 6 | 7 | /** 8 | * Represent props of a 9 | */ 10 | export interface TabProps { 11 | name: string; 12 | children: React.ReactNode; 13 | } 14 | 15 | /** 16 | * A component 17 | */ 18 | // eslint-disable-next-line react/prop-types 19 | export const Tab: React.FunctionComponent = ({ children }) => ( 20 | <>{children} 21 | ); 22 | 23 | /** 24 | * Declare how does the keyboard interacts with ink-tab here 25 | */ 26 | interface KeyMapProps { 27 | useNumbers?: boolean; 28 | useTab?: boolean; 29 | previous?: string[]; 30 | next?: string[]; 31 | } 32 | 33 | interface RequiredKeyMapProps { 34 | useNumbers: boolean; 35 | useTab: boolean; 36 | previous: string[]; 37 | next: string[]; 38 | } 39 | 40 | /** 41 | * Props for the component 42 | */ 43 | export interface TabsProps { 44 | /** 45 | * A function called whenever a tab is changing. 46 | * @param {string} name the name of the tab passed in the `name` prop 47 | * @param {React.Component} activeTab the current active tab component 48 | */ 49 | onChange: (name: string, activeTab: React.ReactElement) => void; 50 | children: Array>; 51 | flexDirection?: BoxProps['flexDirection']; 52 | width?: BoxProps['width']; 53 | keyMap?: KeyMapProps; 54 | isFocused?: boolean; 55 | defaultValue?: string; 56 | showIndex?: boolean; 57 | colors?: { 58 | activeTab?: { 59 | color?: ExtractFCProps['color']; 60 | backgroundColor?: ExtractFCProps['backgroundColor']; 61 | }; 62 | }; 63 | } 64 | interface TabsWithStdinProps extends TabsProps { 65 | isRawModeSupported: boolean; 66 | setRawMode: StdinProps['setRawMode']; 67 | stdin: StdinProps['stdin']; 68 | } 69 | 70 | interface TabsWithStdinState { 71 | activeTab: number; 72 | } 73 | 74 | class TabsWithStdin extends React.Component< 75 | TabsWithStdinProps, 76 | TabsWithStdinState 77 | > { 78 | // eslint-disable-next-line react/sort-comp 79 | private readonly defaultKeyMap: RequiredKeyMapProps; 80 | 81 | public static defaultProps = { 82 | flexDirection: 'row', 83 | keyMap: null, 84 | isFocused: null, // isFocused is null mean that the focus not handle by ink 85 | defaultValue: null, 86 | showIndex: true, 87 | }; 88 | 89 | constructor(props: TabsWithStdinProps) { 90 | super(props); 91 | 92 | this.handleTabChange = this.handleTabChange.bind(this); 93 | this.handleKeyPress = this.handleKeyPress.bind(this); 94 | this.moveToNextTab = this.moveToNextTab.bind(this); 95 | this.moveToPreviousTab = this.moveToPreviousTab.bind(this); 96 | 97 | this.state = { 98 | activeTab: 0, 99 | }; 100 | 101 | this.defaultKeyMap = { 102 | useNumbers: true, 103 | useTab: true, 104 | previous: [this.isColumn() ? 'up' : 'left'], 105 | next: [this.isColumn() ? 'down' : 'right'], 106 | }; 107 | } 108 | 109 | componentDidMount(): void { 110 | const { stdin, setRawMode, isRawModeSupported, children, defaultValue } = 111 | this.props; 112 | 113 | if (isRawModeSupported) { 114 | // use ink / node `setRawMode` to read key-by-key 115 | setRawMode(true); 116 | 117 | readline.emitKeypressEvents(stdin); 118 | stdin.on('keypress', this.handleKeyPress); 119 | } 120 | 121 | // select defaultValue if it's valid otherwise select the first tab on component mount 122 | let initialTabIndex = 0; 123 | 124 | if (typeof defaultValue !== 'undefined') { 125 | const foundIndex = children.findIndex( 126 | (child) => child.props.name === defaultValue 127 | ); 128 | 129 | if (foundIndex > 0) { 130 | initialTabIndex = foundIndex; 131 | } 132 | } 133 | 134 | this.handleTabChange(initialTabIndex); 135 | } 136 | 137 | componentWillUnmount(): void { 138 | const { stdin, setRawMode, isRawModeSupported } = this.props; 139 | 140 | if (isRawModeSupported) { 141 | setRawMode(false); // remove set raw mode, as it might interfere with CTRL-C 142 | stdin.removeListener('keypress', this.handleKeyPress); 143 | } 144 | } 145 | 146 | handleTabChange(tabId: number): void { 147 | const { children, onChange } = this.props; 148 | 149 | const tab = children[tabId]; 150 | 151 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -- handle possible runtime errors 152 | if (!tab) { 153 | return; 154 | } 155 | 156 | this.setState({ 157 | activeTab: tabId, 158 | }); 159 | 160 | onChange(tab.props.name, tab); 161 | } 162 | 163 | handleKeyPress( 164 | ch: string, 165 | key: null | { name: string; shift: boolean; meta: boolean } 166 | ): void { 167 | const { keyMap, isFocused } = this.props; 168 | 169 | if (key == null || isFocused === false) { 170 | return; 171 | } 172 | 173 | const currentKeyMap = { ...this.defaultKeyMap, ...keyMap }; 174 | const { useNumbers, useTab, previous, next } = currentKeyMap; 175 | 176 | if (previous.some((keyName) => keyName === key.name)) { 177 | this.moveToPreviousTab(); 178 | } 179 | 180 | if (next.some((keyName) => keyName === key.name)) { 181 | this.moveToNextTab(); 182 | } 183 | 184 | switch (key.name) { 185 | case 'tab': { 186 | if (!useTab || isFocused !== null) { 187 | // if isFocused != null, then the focus is managed by ink and thus we can not use this key 188 | return; 189 | } 190 | 191 | if (key.shift) { 192 | this.moveToPreviousTab(); 193 | } else { 194 | this.moveToNextTab(); 195 | } 196 | 197 | break; 198 | } 199 | 200 | case '0': 201 | case '1': 202 | case '2': 203 | case '3': 204 | case '4': 205 | case '5': 206 | case '6': 207 | case '7': 208 | case '8': 209 | case '9': { 210 | if (!useNumbers) { 211 | return; 212 | } 213 | if (key.meta) { 214 | const tabId = key.name === '0' ? 9 : parseInt(key.name, 10) - 1; 215 | 216 | this.handleTabChange(tabId); 217 | } 218 | 219 | break; 220 | } 221 | 222 | default: 223 | break; 224 | } 225 | } 226 | 227 | isColumn(): boolean { 228 | const { flexDirection } = this.props; 229 | 230 | return flexDirection === 'column' || flexDirection === 'column-reverse'; 231 | } 232 | 233 | moveToNextTab(): void { 234 | const { children } = this.props; 235 | const { activeTab } = this.state; 236 | 237 | let nextTabId = activeTab + 1; 238 | if (nextTabId >= children.length) { 239 | nextTabId = 0; 240 | } 241 | 242 | this.handleTabChange(nextTabId); 243 | } 244 | 245 | moveToPreviousTab(): void { 246 | const { children } = this.props; 247 | const { activeTab } = this.state; 248 | 249 | let nextTabId = activeTab - 1; 250 | if (nextTabId < 0) { 251 | nextTabId = children.length - 1; 252 | } 253 | 254 | this.handleTabChange(nextTabId); 255 | } 256 | 257 | render(): React.ReactNode { 258 | const { 259 | children, 260 | flexDirection, 261 | width, 262 | isFocused, 263 | showIndex, 264 | colors: colorsProp, 265 | ...rest 266 | } = this.props; 267 | const { activeTab } = this.state; 268 | 269 | const separatorWidth = width ?? 6; 270 | 271 | const separator = this.isColumn() 272 | ? new Array(separatorWidth).fill('─').join('') 273 | : ' | '; 274 | 275 | return ( 276 | 277 | {children.map((child, key) => { 278 | const { name } = child.props; 279 | let colors = {}; 280 | if (isFocused !== false) { 281 | colors = { 282 | backgroundColor: 283 | activeTab === key 284 | ? colorsProp?.activeTab?.color ?? 'green' 285 | : undefined, 286 | color: 287 | activeTab === key 288 | ? colorsProp?.activeTab?.backgroundColor ?? 'black' 289 | : undefined, 290 | }; 291 | } else { 292 | colors = { 293 | backgroundColor: activeTab === key ? 'gray' : undefined, 294 | color: activeTab === key ? 'black' : undefined, 295 | }; 296 | } 297 | 298 | return ( 299 | 300 | {key !== 0 && {separator}} 301 | 302 | {showIndex === true && {key + 1}. } 303 | {child} 304 | 305 | 306 | ); 307 | })} 308 | 309 | ); 310 | } 311 | } 312 | 313 | /** 314 | * The component 315 | */ 316 | export const Tabs: React.FunctionComponent = (props) => { 317 | const { isRawModeSupported, stdin, setRawMode } = useStdin(); 318 | 319 | return ( 320 | 326 | ); 327 | }; 328 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 7 | "module": "node16", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "lib", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true, /* Enable all strict type-checking options. */ 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | "moduleResolution": "node16", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | 54 | /* Source Map Options */ 55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 59 | 60 | /* Experimental Options */ 61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 63 | 64 | /* Advanced Options */ 65 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | } 68 | } 69 | --------------------------------------------------------------------------------