├── .babelrc.js
├── .eslintrc
├── .github
├── renovate.json
└── workflows
│ ├── ci.yml
│ ├── shipjs-manual-prepare.yml
│ └── shipjs-trigger.yml
├── .gitignore
├── changelog.md
├── example
├── package.json
├── src
│ ├── App.js
│ ├── accordion.css
│ ├── accordion.js
│ ├── animated-tabs.js
│ ├── app.css
│ ├── autoplayed-tabs.js
│ ├── height-tabs.js
│ ├── index.html
│ ├── main.js
│ ├── simple-tabs.js
│ ├── tabs.css
│ └── useHover.js
└── yarn.lock
├── package-lock.json
├── package.json
├── readme.md
├── rollup.config.js
├── ship.config.js
├── src
└── index.js
└── test
└── index.test.js
/.babelrc.js:
--------------------------------------------------------------------------------
1 | const isTest = process.env.NODE_ENV === 'test'
2 |
3 | module.exports = {
4 | presets: [isTest && '@babel/preset-env', '@babel/preset-react'].filter(
5 | Boolean
6 | )
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard", "eslint-config-standard-jsx"],
3 | "plugins": [
4 | "react-hooks"
5 | ],
6 | "rules": {
7 | "react-hooks/rules-of-hooks": "error",
8 | "react-hooks/exhaustive-deps": "warn"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:js-lib", "schedule:weekly", ":automergeMinor"],
3 | "reviewers": ["jeetiss"],
4 | "timezone": "Europe/Moscow",
5 | "packageRules": [
6 | {
7 | "depTypeList": ["devDependencies"],
8 | "updateTypes": ["patch", "minor"],
9 | "groupName": "dev dependencies (non-major)"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - "*"
10 |
11 | jobs:
12 | build:
13 | name: build and test
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v1
18 | - run: npm ci
19 | - run: npm run build
20 | - run: npm test
21 | - run: npm run size
22 |
--------------------------------------------------------------------------------
/.github/workflows/shipjs-manual-prepare.yml:
--------------------------------------------------------------------------------
1 | name: Ship js Manual Prepare
2 | on:
3 | issue_comment:
4 | types: [created]
5 | jobs:
6 | manual_prepare:
7 | if: |
8 | github.event_name == 'issue_comment' &&
9 | (github.event.comment.author_association == 'member' || github.event.comment.author_association == 'owner') &&
10 | startsWith(github.event.comment.body, '@shipjs prepare')
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | with:
15 | fetch-depth: 0
16 | ref: master
17 | - uses: actions/setup-node@v1
18 | - run: |
19 | if [ -f "yarn.lock" ]; then
20 | yarn install
21 | else
22 | npm install
23 | fi
24 | - run: |
25 | git config --global user.email "jeetiss@yandex.ru"
26 | git config --global user.name "jeetiss"
27 | - run: npm run release:prepare -- --yes --no-browse
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 | SLACK_INCOMING_HOOK: ${{ secrets.SLACK_INCOMING_HOOK }}
31 |
32 | create_done_comment:
33 | if: success()
34 | needs: manual_prepare
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: actions/github@master
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 | with:
41 | args: comment "@${{ github.actor }} `shipjs prepare` done"
42 |
43 | create_fail_comment:
44 | if: cancelled() || failure()
45 | needs: manual_prepare
46 | runs-on: ubuntu-latest
47 | steps:
48 | - uses: actions/github@master
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | with:
52 | args: comment "@${{ github.actor }} `shipjs prepare` fail"
53 |
--------------------------------------------------------------------------------
/.github/workflows/shipjs-trigger.yml:
--------------------------------------------------------------------------------
1 | name: Ship js trigger
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | build:
8 | name: Release
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | with:
13 | ref: master
14 | - uses: actions/setup-node@v1
15 | with:
16 | registry-url: "https://registry.npmjs.org"
17 | - run: |
18 | if [ -f "yarn.lock" ]; then
19 | yarn install
20 | else
21 | npm install
22 | fi
23 | - run: npm test
24 | - run: npm run release:trigger
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
28 | SLACK_INCOMING_HOOK: ${{ secrets.SLACK_INCOMING_HOOK }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
4 | .DS_Store
5 | yarn.lock
6 | coverage
7 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | # [0.2.0](https://github.com/jeetiss/tabs/compare/0.1.3...0.2.0) (2019-11-16)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * security vulnerability in example ([#9](https://github.com/jeetiss/tabs/issues/9)) ([93635ba](https://github.com/jeetiss/tabs/commit/93635ba8374401d16b60ff573d5c309afc0af18c))
7 |
8 |
9 | ### Features
10 |
11 | * add umd bundle ([#11](https://github.com/jeetiss/tabs/issues/11)) ([2b560d4](https://github.com/jeetiss/tabs/commit/2b560d44e4d8745bd9cc16251260c1b06b6d7dcb))
12 |
13 | ## [0.1.3](https://github.com/jeetiss/tabs/compare/v0.1.2...v0.1.3) (2019-08-03)
14 |
15 |
16 | ### Bug Fixes
17 |
18 | * provide cjs module for jest ([#7](https://github.com/jeetiss/tabs/issues/7)) ([a35d491](https://github.com/jeetiss/tabs/commit/a35d491))
19 | * travis config ([#8](https://github.com/jeetiss/tabs/issues/8)) ([f092b23](https://github.com/jeetiss/tabs/commit/f092b23))
20 |
21 |
22 |
23 | ## [0.1.2](https://github.com/jeetiss/tabs/compare/v0.1.1...v0.1.2) (2019-07-29)
24 |
25 |
26 |
27 | ## [0.1.1](https://github.com/jeetiss/tabs/compare/0.1.0...v0.1.1) (2019-07-19)
28 |
29 |
30 |
31 | # 0.1.0 (2019-07-19)
32 |
33 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "parcel-react-example",
3 | "description": "Simple React example",
4 | "version": "1.0.0",
5 | "main": "src/index.html",
6 | "license": "MIT",
7 | "scripts": {
8 | "start": "parcel src/index.html --open",
9 | "build": "parcel build src/index.html",
10 | "predeploy": "parcel build src/index.html --public-url /tabs",
11 | "deploy": "gh-pages -d dist"
12 | },
13 | "dependencies": {
14 | "@bumaga/tabs": "0.2.0",
15 | "@restart/hooks": "^0.5.0",
16 | "framer-motion": "^1.2.3",
17 | "react": "^16.8.6",
18 | "react-dom": "^16.8.6"
19 | },
20 | "devDependencies": {
21 | "@babel/core": "7.22.5",
22 | "@babel/preset-react": "7.22.5",
23 | "gh-pages": "2.2.0",
24 | "parcel-bundler": "1.12.5"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/example/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './app.css'
3 | import './tabs.css'
4 |
5 | import SimpleTabs from './simple-tabs'
6 | import AnimatedTabs from './animated-tabs'
7 | import AutoplayedTabs from './autoplayed-tabs'
8 | import HeightTabs from './height-tabs'
9 | import Accordion from './accordion'
10 |
11 | export default () => {
12 | return (
13 | <>
14 |
15 |
16 |
17 |
18 |
19 | >
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/example/src/accordion.css:
--------------------------------------------------------------------------------
1 | .accordion {
2 | position: relative;
3 | display: flex;
4 | flex-direction: column;
5 | width: 450px;
6 |
7 | border: 1px solid #f1f1f1;
8 | background-color: #fff;
9 | box-shadow: 0px 2px 24px 0px rgba(0, 0, 0, 0.1);
10 |
11 | margin: 80px 0;
12 | }
13 |
14 | .accordion-tab {
15 | outline: none;
16 | cursor: pointer;
17 | border: none;
18 | font-size: 16px;
19 | line-height: 24px;
20 | padding: 8px 16px;
21 | color: #492796;
22 | background-color: #fff;
23 | border: 1px solid #f1f1f1;
24 | box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.1);
25 |
26 | transition: color 0.16s ease-in-out, background-color 0.16s ease-in-out,
27 | border-color 0.16s ease-in-out;
28 | }
29 |
30 | .accordion-tab:focus,
31 | .accordion-tab:focus-visible {
32 | box-shadow: 0 0 0 4px rgba(73, 39, 150, 0.5);
33 | }
34 |
35 | .accordion-tab:focus:not(:focus-visible) {
36 | box-shadow: none;
37 | }
38 |
39 | .accordion-tab.active {
40 | background-color: #492796;
41 | border-color: #492796;
42 | color: white;
43 | cursor: default;
44 | }
45 |
46 | .accordion-panel {
47 | box-sizing: border-box;
48 | overflow: hidden;
49 | }
50 |
51 | .accordion-panel > p {
52 | padding: 24px 32px;
53 | }
54 |
--------------------------------------------------------------------------------
/example/src/accordion.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Tabs, useTabState, usePanelState } from '@bumaga/tabs'
3 | import { motion } from 'framer-motion'
4 |
5 | import './accordion.css'
6 |
7 | const cn = (...args) => args.filter(Boolean).join(' ')
8 |
9 | const Tab = ({ children }) => {
10 | const { isActive, onClick } = useTabState()
11 |
12 | return (
13 |
17 | {children}
18 |
19 | )
20 | }
21 |
22 | const panel = {
23 | hidden: { height: 0 },
24 | visible: { height: 'auto' }
25 | }
26 |
27 | const Panel = ({ children }) => {
28 | const isActive = usePanelState()
29 |
30 | return (
31 |
36 | {children}
37 |
38 | )
39 | }
40 |
41 | export default () => {
42 | return (
43 |
44 |
45 |
Tab 1
46 |
47 |
48 | Creates a MotionValue that, when set, will use a spring animation to
49 | animate to its new state.
50 |
51 |
52 |
53 |
Tab 2
54 |
55 |
56 | In sociology, anthropology, and linguistics, structuralism is the
57 | methodology that implies elements of human culture must be
58 | understood by way of their relationship to a broader, overarching
59 | system or structure. It works to uncover the structures that
60 | underlie all the things that humans do, think, perceive, and feel.
61 | Alternatively, as summarized by philosopher Simon Blackburn,
62 | structuralism is "the belief that phenomena of human life are not
63 | intelligible except through their interrelations. These relations
64 | constitute a structure, and behind local variations in the surface
65 | phenomena there are constant laws of abstract structure".
66 |
67 |
68 |
69 |
Tab 3
70 |
71 |
72 | The input range must be a linear series of numbers. The output range
73 | can be any value type supported by Framer Motion: numbers, colors,
74 | shadows, etc.
75 |
76 |
77 |
78 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/example/src/animated-tabs.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect, cloneElement } from 'react'
2 | import { Tabs, useTabState, Panel } from '@bumaga/tabs'
3 | import { motion, AnimatePresence } from 'framer-motion'
4 |
5 | const cn = (...args) => args.filter(Boolean).join(' ')
6 |
7 | const Tab = ({ children }) => {
8 | const { isActive, onClick } = useTabState()
9 |
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
17 | const PanelList = ({ state, children }) => {
18 | const panelRef = useRef()
19 | const [height, set] = useState(0)
20 | const [activeIndex] = state
21 |
22 | useEffect(() => {
23 | panelRef.current && set(panelRef.current.offsetHeight)
24 | }, [activeIndex, set])
25 |
26 | return (
27 |
28 |
29 |
37 | {cloneElement(children[activeIndex], { active: true })}
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default () => {
45 | const state = useState(0)
46 |
47 | return (
48 |
49 |
50 |
51 | Tab 1
52 |
53 | Tab 2
54 |
55 | Tab 3
56 |
57 |
58 |
59 |
60 |
61 |
62 | animations with framer/motion
63 |
64 |
65 |
66 | is pure
67 |
68 |
69 |
70 | ❤️
71 |
72 |
73 |
74 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/example/src/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 80px 0;
3 | margin: 0;
4 | }
5 |
6 | body {
7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
8 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
9 | background-image: linear-gradient(135deg, #aeafe8 0%, #e68ca7 100%);
10 | background-repeat: no-repeat;
11 | }
12 |
13 | div#app {
14 | display: flex;
15 | flex-direction: column;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 |
20 | p {
21 | margin: 0;
22 | padding: 16px 0;
23 | }
--------------------------------------------------------------------------------
/example/src/autoplayed-tabs.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react'
2 | import { Tabs, useTabState, Panel } from '@bumaga/tabs'
3 | import { useInterval } from '@restart/hooks'
4 | import { motion } from 'framer-motion'
5 |
6 | import { useHover } from './useHover'
7 |
8 | const cn = (...args) => args.filter(Boolean).join(' ')
9 |
10 | const Tab = ({ children }) => {
11 | const { isActive, onClick } = useTabState()
12 |
13 | return (
14 |
15 | {children}
16 |
17 | )
18 | }
19 |
20 | const duration = 2
21 | const variants = {
22 | pd: { scaleX: 1 },
23 | pl: { scaleX: 0 }
24 | }
25 |
26 | export default () => {
27 | const tabsRef = useRef()
28 | const [index, setIndex] = useState(0)
29 | const [paused, stop] = useState(false)
30 |
31 | useHover(tabsRef, () => stop(true), () => stop(false))
32 | useInterval(() => setIndex(index => (index + 1) % 3), duration * 1000, paused)
33 |
34 | return (
35 |
36 |
37 |
38 | Tab 1
39 |
40 | Tab 2
41 |
42 | Tab 3
43 |
44 |
45 |
57 |
58 |
59 | Hello World 📦 🚀
60 |
61 |
62 |
63 | Tabs with hooks 🎣
64 |
65 |
66 |
67 | So nice 🚨
68 |
69 |
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/example/src/height-tabs.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect, cloneElement } from 'react'
2 | import { Tabs, useTabState, Panel } from '@bumaga/tabs'
3 | import { motion } from 'framer-motion'
4 |
5 | const cn = (...args) => args.filter(Boolean).join(' ')
6 |
7 | const Tab = ({ children }) => {
8 | const { isActive, onClick } = useTabState()
9 |
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
17 | const PanelList = ({ state, children }) => {
18 | const panelRef = useRef()
19 | const [height, set] = useState(0)
20 | const [activeIndex] = state
21 |
22 | useEffect(() => {
23 | set(panelRef.current.offsetHeight)
24 | }, [activeIndex, set])
25 |
26 | return (
27 |
28 |
29 | {cloneElement(children[activeIndex], { active: true })}
30 |
31 |
32 | )
33 | }
34 |
35 | export default () => {
36 | const state = useState(0)
37 |
38 | return (
39 |
40 |
41 |
42 | Tab 1
43 |
44 | Tab 2
45 |
46 | Tab 3
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | In sociology, anthropology, and linguistics, structuralism is the
55 | methodology that implies elements of human culture must be
56 | understood by way of their relationship to a broader, overarching
57 | system or structure. It works to uncover the structures that
58 | underlie all the things that humans do, think, perceive, and feel.
59 | Alternatively, as summarized by philosopher Simon Blackburn,
60 | structuralism is "the belief that phenomena of human life are not
61 | intelligible except through their interrelations. These relations
62 | constitute a structure, and behind local variations in the surface
63 | phenomena there are constant laws of abstract structure".
64 |
65 |
66 |
67 |
68 |
69 | The input range must be a linear series of numbers. The output
70 | range can be any value type supported by Framer Motion: numbers,
71 | colors, shadows, etc.
72 |
73 |
74 |
75 |
76 |
77 | Creates a MotionValue that, when set, will use a spring animation
78 | to animate to its new state.
79 |
80 |
81 |
82 |
83 |
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/example/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tabs
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/src/main.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import App from './App'
4 |
5 | render( , document.getElementById('app'))
6 |
--------------------------------------------------------------------------------
/example/src/simple-tabs.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Tabs, useTabState, Panel } from '@bumaga/tabs'
3 |
4 | const cn = (...args) => args.filter(Boolean).join(' ')
5 |
6 | const Tab = ({ children }) => {
7 | const { isActive, onClick } = useTabState()
8 |
9 | return (
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
16 | export default () => (
17 |
18 |
19 |
20 | Tab 1
21 |
22 | Tab 2
23 |
24 | Tab 3
25 |
26 |
27 |
28 |
29 |
30 |
31 | In sociology, anthropology, and linguistics, structuralism is the
32 | methodology that implies elements of human culture must be understood
33 | by way of their relationship to a broader, overarching system or
34 | structure. It works to uncover the structures that underlie all the
35 | things that humans do, think, perceive, and feel. Alternatively, as
36 | summarized by philosopher Simon Blackburn, structuralism is "the
37 | belief that phenomena of human life are not intelligible except
38 | through their interrelations. These relations constitute a structure,
39 | and behind local variations in the surface phenomena there are
40 | constant laws of abstract structure".
41 |
42 |
43 |
44 |
45 |
46 | The input range must be a linear series of numbers. The output range
47 | can be any value type supported by Framer Motion: numbers, colors,
48 | shadows, etc.
49 |
50 |
51 |
52 |
53 |
54 | Creates a MotionValue that, when set, will use a spring animation to
55 | animate to its new state.
56 |
57 |
58 |
59 |
60 | )
61 |
--------------------------------------------------------------------------------
/example/src/tabs.css:
--------------------------------------------------------------------------------
1 | .tabs {
2 | box-sizing: border-box;
3 | position: relative;
4 | display: flex;
5 | flex-direction: column;
6 | width: 450px;
7 |
8 | padding: 24px 32px;
9 | border: 1px solid #f1f1f1;
10 | background-color: #fff;
11 | box-shadow: 0px 2px 24px 0px rgba(0, 0, 0, 0.1);
12 |
13 | margin: 80px 0;
14 | }
15 |
16 | .tab-list {
17 | display: flex;
18 |
19 | padding-bottom: 24px;
20 | }
21 |
22 | .tab-progress {
23 | position: absolute;
24 | left: -1px;
25 | right: -1px;
26 | top: 66px;
27 | height: 4px;
28 | z-index: 0;
29 |
30 | background-color: #492796;
31 | transform-origin: 0%;
32 | }
33 |
34 | .tab {
35 | outline: none;
36 | cursor: pointer;
37 | border: none;
38 | font-size: 16px;
39 | line-height: 24px;
40 | padding: 8px 16px;
41 | color: #492796;
42 | background-color: #fff;
43 | border: 1px solid #f1f1f1;
44 | box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.1);
45 | margin-right: 24px;
46 |
47 | transition: color 0.16s ease-in-out, background-color 0.16s ease-in-out,
48 | border-color 0.16s ease-in-out;
49 | }
50 |
51 | .tab:focus,
52 | .tab:focus-visible {
53 | box-shadow: 0 0 0 4px rgba(73, 39, 150, 0.5);
54 | }
55 |
56 | .tab:focus:not(:focus-visible) {
57 | box-shadow: none;
58 | }
59 |
60 | .tab.active {
61 | background-color: #492796;
62 | border-color: #492796;
63 | color: white;
64 | cursor: default;
65 | }
66 |
67 | .panel-list {
68 | list-style: none;
69 | margin: 0;
70 | padding: 0;
71 |
72 | position: relative;
73 | }
74 |
75 | .panel {
76 | position: absolute;
77 | }
78 |
--------------------------------------------------------------------------------
/example/src/useHover.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useEventCallback } from '@restart/hooks'
3 |
4 | export const useHover = (ref, onHoverStart, onHoverEnd) => {
5 | const start = useEventCallback(onHoverStart)
6 | const end = useEventCallback(onHoverEnd)
7 | useEffect(() => {
8 | const element = ref.current
9 | if (!element) return
10 | element.addEventListener('mouseenter', start)
11 | element.addEventListener('mouseleave', end)
12 | return () => {
13 | element.removeEventListener('mouseenter', start)
14 | element.removeEventListener('mouseleave', end)
15 | }
16 | }, [end, ref, start])
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bumaga/tabs",
3 | "version": "0.2.0",
4 | "main": "dist/index.cjs.js",
5 | "module": "dist/index.js",
6 | "files": [
7 | "dist"
8 | ],
9 | "repository": "jeetiss/tabs",
10 | "scripts": {
11 | "build": "rollup -c",
12 | "size": "size-limit",
13 | "test": "NODE_ENV=test jest --verbose --coverage",
14 | "release:prepare": "shipjs prepare",
15 | "release:trigger": "shipjs trigger"
16 | },
17 | "dependencies": {
18 | "use-constant": "^1.0.0"
19 | },
20 | "peerDependencies": {
21 | "react": "^16.8.0",
22 | "react-dom": "^16.8.0"
23 | },
24 | "devDependencies": {
25 | "@babel/cli": "7.22.5",
26 | "@babel/core": "7.22.5",
27 | "@babel/preset-env": "7.22.5",
28 | "@babel/preset-react": "7.22.5",
29 | "@rollup/plugin-node-resolve": "7.1.3",
30 | "@size-limit/preset-small-lib": "4.12.0",
31 | "@testing-library/jest-dom": "5.16.5",
32 | "@testing-library/react": "9.5.0",
33 | "eslint": "6.8.0",
34 | "eslint-config-standard": "14.1.1",
35 | "eslint-config-standard-jsx": "8.1.0",
36 | "eslint-plugin-import": "2.27.5",
37 | "eslint-plugin-node": "11.1.0",
38 | "eslint-plugin-promise": "4.3.1",
39 | "eslint-plugin-react": "7.32.2",
40 | "eslint-plugin-react-hooks": "2.5.1",
41 | "eslint-plugin-standard": "4.1.0",
42 | "jest": "25.5.4",
43 | "react": "16.14.0",
44 | "react-dom": "16.14.0",
45 | "rollup": "2.79.1",
46 | "rollup-plugin-babel": "4.4.0",
47 | "rollup-plugin-terser": "5.3.1",
48 | "shipjs": "0.26.3",
49 | "size-limit": "4.12.0"
50 | },
51 | "size-limit": [
52 | {
53 | "path": "dist/index.js",
54 | "limit": "400 B"
55 | }
56 | ],
57 | "author": "Ivakhnenko Dmitry ",
58 | "license": "ISC"
59 | }
60 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # @bumaga/tabs
2 |
3 | Headless tabs component for React
4 |
5 | ## Features
6 |
7 | - 📦 super small, 381 B vs 3.5kB [react-tabs](https://github.com/reactjs/react-tabs)
8 | - 🚫 no styles, just logic. Style what you want, as you want
9 | - 🎣 components and hooks API
10 |
11 | ## Install
12 |
13 | ```
14 | npm install @bumaga/tabs
15 | ```
16 |
17 | ```
18 | yarn add @bumaga/tabs
19 | ```
20 |
21 | ## Usage
22 |
23 | ### With components
24 |
25 | ```jsx
26 | import React from 'react'
27 | import { Tabs, Tab, Panel } from '@bumaga/tabs'
28 |
29 | export default () => (
30 |
31 |
32 | Tab 1
33 | Tab 2
34 | Tab 3
35 |
36 |
37 | Panel 1
38 | Panel 2
39 | panel 3
40 |
41 | );
42 | ```
43 |
44 | ### With hooks
45 |
46 | ```jsx
47 | import React from "react";
48 | import { Tabs, useTabState, usePanelState } from "@bumaga/tabs";
49 |
50 | const Tab = ({ children }) => {
51 | const { onClick } = useTabState();
52 |
53 | return {children} ;
54 | };
55 |
56 | const Panel = ({ children }) => {
57 | const isActive = usePanelState();
58 |
59 | return isActive ? {children}
: null;
60 | };
61 |
62 | export default () => (
63 |
64 |
65 | Tab 1
66 | Tab 2
67 |
68 |
69 | Panel 1
70 | Panel 2
71 |
72 | );
73 | ```
74 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel'
2 | import resolve from '@rollup/plugin-node-resolve'
3 | import { terser } from 'rollup-plugin-terser'
4 |
5 | export default [
6 | {
7 | input: 'src/index.js',
8 | external: ['react', 'use-constant'],
9 | output: {
10 | format: 'esm',
11 | file: 'dist/index.js',
12 | sourcemap: false
13 | },
14 | plugins: [babel()]
15 | },
16 | {
17 | input: 'src/index.js',
18 | external: ['react', 'use-constant'],
19 | output: {
20 | format: 'cjs',
21 | file: 'dist/index.cjs.js',
22 | sourcemap: false
23 | },
24 | plugins: [babel()]
25 | },
26 | {
27 | input: 'src/index.js',
28 | external: ['react'],
29 | output: {
30 | format: 'umd',
31 | name: 'bumaga',
32 | file: 'dist/index.umd.js',
33 | globals: { react: 'React' },
34 | sourcemap: false
35 | },
36 | plugins: [babel(), resolve()]
37 | },
38 | {
39 | input: 'src/index.js',
40 | external: ['react'],
41 | output: {
42 | format: 'umd',
43 | name: 'bumaga',
44 | file: 'dist/index.umd.min.js',
45 | globals: { react: 'React' },
46 | sourcemap: false
47 | },
48 | plugins: [babel(), resolve(), terser()]
49 | }
50 | ]
51 |
--------------------------------------------------------------------------------
/ship.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | publishCommand: ({ defaultCommand }) => `${defaultCommand} --access public`,
3 | mergeStrategy: { toSameBranch: ['master'] }
4 | }
5 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useState,
4 | useMemo,
5 | useContext,
6 | cloneElement,
7 | isValidElement
8 | } from 'react'
9 |
10 | import useConstant from 'use-constant'
11 |
12 | const TabsState = createContext()
13 | const Elements = createContext()
14 |
15 | export const Tabs = ({ state: outerState, children }) => {
16 | const innerState = useState(0)
17 | const elements = useConstant(() => ({ tabs: 0, panels: 0 }))
18 | const state = outerState || innerState
19 |
20 | return (
21 |
22 | {children}
23 |
24 | )
25 | }
26 |
27 | export const useTabState = () => {
28 | const [activeIndex, setActive] = useContext(TabsState)
29 | const elements = useContext(Elements)
30 |
31 | const tabIndex = useConstant(() => {
32 | const currentIndex = elements.tabs
33 | elements.tabs += 1
34 |
35 | return currentIndex
36 | })
37 |
38 | const onClick = useConstant(() => () => setActive(tabIndex))
39 |
40 | const state = useMemo(
41 | () => ({
42 | isActive: activeIndex === tabIndex,
43 | onClick
44 | }),
45 | [activeIndex, onClick, tabIndex]
46 | )
47 |
48 | return state
49 | }
50 |
51 | export const usePanelState = () => {
52 | const [activeIndex] = useContext(TabsState)
53 | const elements = useContext(Elements)
54 |
55 | const panelIndex = useConstant(() => {
56 | const currentIndex = elements.panels
57 | elements.panels += 1
58 |
59 | return currentIndex
60 | })
61 |
62 | return panelIndex === activeIndex
63 | }
64 |
65 | export const Tab = ({ children }) => {
66 | const state = useTabState()
67 |
68 | if (typeof children === 'function') {
69 | return children(state)
70 | }
71 |
72 | return isValidElement(children) ? cloneElement(children, state) : children
73 | }
74 |
75 | export const Panel = ({ active, children }) => {
76 | const isActive = usePanelState()
77 |
78 | return isActive || active ? children : null
79 | }
80 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import '@testing-library/jest-dom/extend-expect'
4 |
5 | import React from 'react'
6 | import { render, fireEvent } from '@testing-library/react'
7 | import { Panel, Tab, Tabs } from '../src'
8 |
9 | const Button = ({ isActive, onClick, children }) => (
10 |
11 | {children}
12 |
13 | )
14 |
15 | const Testing = () => (
16 |
17 |
18 |
19 | tab 1
20 |
21 |
22 | tab 2
23 |
24 |
25 |
26 | content 1
27 | content 2
28 |
29 | )
30 |
31 | test('renders and change tabs', () => {
32 | const { container, queryByText } = render( )
33 |
34 | expect(container).toHaveTextContent('content 1')
35 | expect(container).not.toHaveTextContent('content 2')
36 |
37 | fireEvent.click(queryByText('tab 2'))
38 |
39 | expect(container).not.toHaveTextContent('content 1')
40 | expect(container).toHaveTextContent('content 2')
41 | })
42 |
--------------------------------------------------------------------------------