├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── stale.yml └── workflows │ ├── nodejs-ci.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── jest.config.ts ├── jest.setup.ts ├── package-lock.json ├── package.json ├── rapid.config.js ├── renovate.json ├── src ├── SuperResponsiveTableStyle.css ├── components │ ├── Table.tsx │ ├── Tbody.tsx │ ├── Td.tsx │ ├── TdInner.tsx │ ├── Th.tsx │ ├── Thead.tsx │ ├── Tr.tsx │ └── TrInner.tsx ├── index.ts ├── types │ └── index.ts └── utils │ ├── allowed.ts │ └── tableContext.tsx ├── test └── index.test.tsx ├── tsconfig.json └── tsconfig.test.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier", 7 | "plugin:prettier/recommended" 8 | ], 9 | "plugins": ["@typescript-eslint", "prettier"], 10 | "env": { 11 | "browser": true, 12 | "es6": true, 13 | "node": true 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2020, 17 | "sourceType": "module" 18 | }, 19 | "rules": { 20 | "prettier/prettier": "error", 21 | "@typescript-eslint/no-unused-vars": [ 22 | "error", 23 | { "ignoreRestSiblings": true } 24 | ] 25 | }, 26 | "ignorePatterns": ["node_modules/", "dist/", ".next/", "out/"] 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | 8 | 9 | ## Screenshots (if appropriate): 10 | 11 | ## Types of changes 12 | 13 | 14 | 15 | - Bug fix (non-breaking change which fixes an issue) 16 | - New feature (non-breaking change which adds functionality) 17 | - Breaking change (fix or feature that would cause existing functionality to change) 18 | 19 | ## Checklist: 20 | 21 | 22 | 23 | 24 | - My code follows the code style of this project. 25 | - If my change requires a change to the documentation I have updated the documentation accordingly. 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Use Node.js lts 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 'lts/*' 18 | - run: npm ci 19 | - run: npm run lint 20 | - run: npm run test 21 | - run: npm run build 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 'lts/*' 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | run: npx semantic-release 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage tools 11 | lib-cov 12 | coverage 13 | coverage.html 14 | .cover* 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Example build directory 20 | public 21 | .publish 22 | 23 | # Editor and other tmp files 24 | *.swp 25 | *.un~ 26 | *.iml 27 | *.ipr 28 | *.iws 29 | *.sublime-* 30 | .idea/ 31 | *.DS_Store 32 | .next 33 | out 34 | dist 35 | lib 36 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Coston Perkins 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-super-responsive-table 2 | 3 | ![GitHub last commit](https://img.shields.io/github/last-commit/Coston/react-super-responsive-table) 4 | [![NPM Downloads](https://img.shields.io/npm/dm/react-super-responsive-table?style=flat-square&logo=npm) 5 | ](https://www.npmjs.com/package/react-super-responsive-table) 6 | [![GitHub Repo stars](https://img.shields.io/github/stars/coston/react-super-responsive-table) 7 | ](https://github.com/coston/react-super-responsive-table) 8 | 9 | react-super-responsive-table converts your table data to a user-friendly list in mobile view. 10 | 11 | - [Demo](#demo) 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Using Dynamic Headers](#using-dynamic-headers) 15 | - [Contributors](#Contributors) 16 | - [Contributing](#contributing) 17 | - [License](#license) 18 | 19 | ## Demo 20 | 21 | ![Demo Gif md-only](https://user-images.githubusercontent.com/7424180/55982530-baab9900-5c5e-11e9-97c0-0336c5889504.gif) 22 | 23 | View the live code demo at [https://react-super-responsive-table.coston.io](https://react-super-responsive-table.coston.io). 24 | 25 | ## Installation 26 | 27 | ``` 28 | npm install react-super-responsive-table --save 29 | ``` 30 | 31 | ## Usage 32 | 33 | 1. `import { Table, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table'` 34 | 2. Copy or import `react-super-responsive-table/dist/SuperResponsiveTableStyle.css` into your project. Customize breakpoint in the css as needed. 35 | 3. Write your html table with the imported components. 36 | 4. Resize your browser window width to pivot this super responsive table! 37 | 38 | ```jsx 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
EventDateLocationOrganizerThemeAgent
Tablescon9 April 2019East AnnexSuper FriendsData TablesCoston Perkins
Capstone Data19 May 2019205 GorgasData UndergroundData ScenceJason Phillips
Tuscaloosa D329 June 2019GithubThe Contributors ConsortiumData VizCoston Perkins
77 | ``` 78 | 79 | ## Using Dynamic Headers 80 | 81 | Headers are statefully stored on first render of the table, since the library doesn't use props for them and just checks the children of the thead to build its internal list of headers upon construction. To use dynamic headers, use a key prop to ensure the components are all internally updated when you're making this kind of change. 82 | 83 | ```jsx 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
{headers[0]}{headers[1]}
item 1item 2
98 | ``` 99 | 100 | ## Contributors 101 | 102 | Super Responsive Tables are made possible by these great community members: 103 | 104 | - [coston](https://github.com/coston) 105 | - [jasonphillips](https://github.com/jasonphillips) 106 | - [jorrit](https://github.com/jorrit) 107 | - [Droow](https://github.com/droow) 108 | - [NShahri](https://github.com/NShahri) 109 | - [PicchiKevin](https://github.com/PicchiKevin) 110 | - [alexandra-c](https://github.com/alexandra-c) 111 | - [dragos-rosca](https://github.com/dragos-rosca) 112 | - [galacemiguel](https://github.com/galacemiguel) 113 | - [themichailov](https://github.com/themichailov) 114 | - [luizeboli](https://github.com/luizeboli) 115 | - [thiagotakehana](https://github.com/thiagotakehana) 116 | - [wedvich](https://github.com/wedvich) 117 | - [wafuwafu13](https://github.com/wafuwafu13) 118 | 119 | ## Contributing 120 | 121 | Please help turn the tables on unresponsive data! Submit an issue and/or make a pull request. Check the [projects board](https://github.com/coston/react-super-responsive-table/projects) for tasks to do. 122 | 123 | ## License 124 | 125 | Licensed under the MIT license. 126 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'jsdom', 6 | setupFilesAfterEnv: ['@testing-library/jest-dom'], 7 | moduleNameMapper: { 8 | '^.+\\.(css|scss)$': 'identity-obj-proxy', 9 | }, 10 | transform: { 11 | '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.test.json' }], 12 | }, 13 | extensionsToTreatAsEsm: ['.ts', '.tsx'], 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-super-responsive-table", 3 | "version": "6.0.1", 4 | "description": "react-super-responsive-table converts your table data to a user-friendly list in mobile view.", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "./dist/cjs/index.js", 9 | "module": "./dist/esm/index.js", 10 | "types": "./dist/esm/index.d.ts", 11 | "author": "Coston Perkins (https://coston.io)", 12 | "homepage": "https://react-super-responsive-table.coston.io", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/coston/react-super-responsive-table.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/coston/react-super-responsive-table/issues" 19 | }, 20 | "engines": { 21 | "node": ">=12" 22 | }, 23 | "license": "MIT", 24 | "scripts": { 25 | "dev": "concurrently \"npm run start\" \"npm run build:watch\"", 26 | "build": "npm run build:esm && npm run build:cjs; npm run copy-css", 27 | "build:cjs": "tsc --module commonjs --outDir dist/cjs", 28 | "build:esm": "tsc", 29 | "build:watch": "tsc --watch", 30 | "build-demo": "rapid build", 31 | "clean": "rimraf dist dist", 32 | "copy-css": "cp src/SuperResponsiveTableStyle.css dist/", 33 | "coveralls": "jest --coverage --coverageReporters=text-lcov | coveralls", 34 | "lint": "eslint '**/*.{js,ts,tsx}'", 35 | "lint:fix": "eslint '**/*.{js,ts,tsx}' --fix", 36 | "postpublish": "git push && git push --tags", 37 | "preversion": "npm run test && npm run lint:fix && npm run clean && npm run build && npm run build-demo", 38 | "start": "rapid dev", 39 | "test": "jest --config jest.config.ts", 40 | "test:coverage": "jest --coverage --config jest.config.ts", 41 | "test:watch": "jest --watch --config jest.config.ts" 42 | }, 43 | "devDependencies": { 44 | "@commitlint/cli": "19.4.0", 45 | "@commitlint/config-conventional": "19.2.2", 46 | "@testing-library/jest-dom": "^6.4.8", 47 | "@testing-library/react": "^16.0.0", 48 | "@types/jest": "^29.5.12", 49 | "@typescript-eslint/eslint-plugin": "^8.0.1", 50 | "@typescript-eslint/parser": "^8.0.0", 51 | "concurrently": "^8.2.2", 52 | "coveralls": "3.1.1", 53 | "eslint": "8.57.0", 54 | "eslint-config-airbnb": "19.0.4", 55 | "eslint-config-prettier": "9.1.0", 56 | "eslint-plugin-import": "2.29.1", 57 | "eslint-plugin-jsx-a11y": "6.9.0", 58 | "eslint-plugin-prettier": "5.2.1", 59 | "eslint-plugin-react": "7.35.0", 60 | "eslint-plugin-react-hooks": "4.6.2", 61 | "husky": "9.1.4", 62 | "identity-obj-proxy": "3.0.0", 63 | "jest": "29.7.0", 64 | "jest-environment-jsdom": "29.7.0", 65 | "lint-staged": "15.2.9", 66 | "prettier": "3.3.3", 67 | "rapid-react-pkg-demo": "^0.5.1", 68 | "react": "18.3.1", 69 | "react-dom": "18.3.1", 70 | "rimraf": "6.0.1", 71 | "ts-jest": "^29.2.4", 72 | "typescript": "^5.5.4" 73 | }, 74 | "peerDependencies": { 75 | "react": ">=16.9.0", 76 | "react-dom": ">=16.9.0" 77 | }, 78 | "lint-staged": { 79 | "*.{js,jsx}": [ 80 | "eslint --fix" 81 | ] 82 | }, 83 | "husky": { 84 | "hooks": { 85 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 86 | "pre-commit": "lint-staged" 87 | } 88 | }, 89 | "commitlint": { 90 | "extends": [ 91 | "@commitlint/config-conventional" 92 | ] 93 | }, 94 | "keywords": [ 95 | "react", 96 | "react-component", 97 | "table" 98 | ], 99 | "directories": { 100 | "test": "test" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rapid.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const { Table, Thead, Tbody, Tr, Th, Td } = require('./src'); 3 | require('./src/SuperResponsiveTableStyle.css'); 4 | 5 | module.exports = { 6 | color: '#ffa3b3', 7 | packageName: 'react-super-responsive-table', 8 | description: 9 | 'react-super-responsive-table converts your table data to a user-friendly list in mobile view.', 10 | icon: '⌗', 11 | scope: { 12 | Table, 13 | Thead, 14 | Tbody, 15 | Tr, 16 | Th, 17 | Td, 18 | i: 5, 19 | headers: ['Name', 'Age'], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "schedule": ["on saturday"], 4 | "labels": ["renovate"], 5 | "stabilityDays": 7, 6 | "statusCheckVerify": true, 7 | "rangeStrategy": "auto", 8 | "prCreation": "not-pending", 9 | "masterIssue": true, 10 | "automerge": true, 11 | "requiredStatusChecks": ["Node.js CI", "Vercel"], 12 | "packageRules": [ 13 | { 14 | "packagePatterns": ["^react"], 15 | "groupName": ["React packages"] 16 | }, 17 | { 18 | "packagePatterns": ["^@babel"], 19 | "groupName": ["Babel packages"] 20 | }, 21 | { 22 | "packagePatterns": [ 23 | "^eslint", 24 | "^@commitlint", 25 | "^commitizen", 26 | "^cz", 27 | "^lint", 28 | "^prettier" 29 | ], 30 | "groupName": ["Linter packages"] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/SuperResponsiveTableStyle.css: -------------------------------------------------------------------------------- 1 | /* inspired by: https://css-tricks.com/responsive-data-tables/ */ 2 | .responsiveTable { 3 | width: 100%; 4 | } 5 | 6 | .responsiveTable td .tdBefore { 7 | display: none; 8 | } 9 | 10 | @media screen and (max-width: 40em) { 11 | /* 12 | Force table elements to not behave like tables anymore 13 | Hide table headers (but not display: none;, for accessibility) 14 | */ 15 | 16 | .responsiveTable table, 17 | .responsiveTable thead, 18 | .responsiveTable tbody, 19 | .responsiveTable th, 20 | .responsiveTable td, 21 | .responsiveTable tr { 22 | display: block; 23 | } 24 | 25 | .responsiveTable thead tr { 26 | position: absolute; 27 | top: -9999px; 28 | left: -9999px; 29 | border-bottom: 2px solid #333; 30 | } 31 | 32 | .responsiveTable tbody tr { 33 | border: 1px solid #000; 34 | padding: .25em; 35 | } 36 | 37 | .responsiveTable td.pivoted { 38 | /* Behave like a "row" */ 39 | border: none !important; 40 | position: relative; 41 | padding-left: calc(50% + 10px) !important; 42 | text-align: left !important; 43 | white-space: pre-wrap; 44 | overflow-wrap: break-word; 45 | } 46 | 47 | .responsiveTable td .tdBefore { 48 | /* Now like a table header */ 49 | position: absolute; 50 | display: block; 51 | 52 | /* Top/left values mimic padding */ 53 | left: 1rem; 54 | width: calc(50% - 20px); 55 | white-space: pre-wrap; 56 | overflow-wrap: break-word; 57 | text-align: left !important; 58 | font-weight: 600; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import { TableProps } from '../types'; 3 | import { allowed } from '../utils/allowed'; 4 | import React from 'react'; 5 | import { HeaderProvider } from '../utils/tableContext'; 6 | 7 | const Table = forwardRef( 8 | ({ className, ...props }, ref) => { 9 | const classes = `${className || ''} responsiveTable`; 10 | return ( 11 | 17 | ); 18 | } 19 | ); 20 | 21 | Table.displayName = 'Table'; 22 | 23 | function TableWithHeaderProvider(props: TableProps) { 24 | return ( 25 | 26 |
27 | 28 | ); 29 | } 30 | 31 | export default TableWithHeaderProvider; 32 | -------------------------------------------------------------------------------- /src/components/Tbody.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import allowed from '../utils/allowed'; 3 | import { TbodyProps } from '../types'; 4 | 5 | function Tbody(props: TbodyProps) { 6 | return ; 7 | } 8 | export default Tbody; 9 | -------------------------------------------------------------------------------- /src/components/Td.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import TdInner from './TdInner'; 3 | import { HeaderContext } from '../utils/tableContext'; 4 | import { TdProps } from '../types'; 5 | 6 | type CombinedTdProps = TdProps & React.TdHTMLAttributes; 7 | 8 | function Td(props: CombinedTdProps) { 9 | const context = useContext(HeaderContext); 10 | 11 | if (!context) { 12 | throw new Error('Td must be used as a child of the Table component'); 13 | } 14 | 15 | const { headers } = context; 16 | 17 | return ; 18 | } 19 | 20 | export default Td; 21 | -------------------------------------------------------------------------------- /src/components/TdInner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import allowed from '../utils/allowed'; 4 | import { TdProps } from '../types'; 5 | 6 | type TdInnerProps = TdProps & { 7 | headers: string[]; 8 | }; 9 | 10 | function TdInner(props: TdInnerProps) { 11 | const { headers, children, columnKey, className, colSpan } = props; 12 | const classes = `${className || ''} pivoted`; 13 | 14 | if (colSpan) { 15 | return 25 | ); 26 | } 27 | 28 | export default TdInner; 29 | -------------------------------------------------------------------------------- /src/components/Th.tsx: -------------------------------------------------------------------------------- 1 | import React, { TableHTMLAttributes } from 'react'; 2 | 3 | import allowed from '../utils/allowed'; 4 | 5 | function Th(props: TableHTMLAttributes) { 6 | return 11 | {React.cloneElement(children as ReactElement, { inHeader: true })} 12 | 13 | ); 14 | } 15 | 16 | export default Thead; 17 | -------------------------------------------------------------------------------- /src/components/Tr.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import TrInner from './TrInner'; 3 | import { HeaderContext } from '../utils/tableContext'; 4 | import { TrProps } from '../types'; 5 | 6 | function Tr(props: TrProps) { 7 | const context = useContext(HeaderContext); 8 | 9 | if (!context) { 10 | throw new Error('Tr must be used as a child of the Table component'); 11 | } 12 | 13 | const { headers } = context; 14 | 15 | return ; 16 | } 17 | 18 | export default Tr; 19 | -------------------------------------------------------------------------------- /src/components/TrInner.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useContext, useEffect } from 'react'; 2 | import { HeaderContext } from '../utils/tableContext'; 3 | 4 | import allowed from '../utils/allowed'; 5 | import { TrProps } from '../types'; 6 | 7 | type TrInnerProps = TrProps & { 8 | headers: string[]; 9 | }; 10 | 11 | function TrInner({ children, inHeader, ...props }: TrInnerProps) { 12 | const context = useContext(HeaderContext); 13 | 14 | if (!context) { 15 | throw new Error('TrInner must be used as a child of the Table component'); 16 | } 17 | 18 | const { setHeaders, headers } = context; 19 | 20 | useEffect(() => { 21 | if (inHeader) { 22 | const updatedHeaders = [...headers]; 23 | React.Children.forEach(children, (child, i) => { 24 | if (React.isValidElement(child)) { 25 | updatedHeaders[i] = child.props.children; 26 | } 27 | }); 28 | setHeaders(updatedHeaders); 29 | } 30 | }, [inHeader, children]); 31 | 32 | return ( 33 | 34 | {children && 35 | React.Children.map(children, (child, i) => 36 | React.isValidElement(child) 37 | ? React.cloneElement(child as ReactElement, { 38 | key: i, 39 | columnKey: i, 40 | }) 41 | : null 42 | )} 43 | 44 | ); 45 | } 46 | 47 | export default TrInner; 48 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Table from './components/Table'; 2 | import Tbody from './components/Tbody'; 3 | import Td from './components/Td'; 4 | import Th from './components/Th'; 5 | import Thead from './components/Thead'; 6 | import Tr from './components/Tr'; 7 | 8 | export { Table, Tbody, Td, Th, Thead, Tr }; 9 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dispatch, 3 | ReactNode, 4 | SetStateAction, 5 | HTMLAttributes, 6 | TableHTMLAttributes, 7 | } from 'react'; 8 | 9 | export type OmitProps = { 10 | inHeader?: boolean; 11 | columnKey?: number; 12 | headers?: string[]; 13 | forwardedRef?: React.Ref; 14 | className?: string; 15 | colSpan?: number; 16 | children?: ReactNode; 17 | }; 18 | 19 | export type HeaderContextType = { 20 | headers: string[]; 21 | setHeaders: Dispatch>; 22 | }; 23 | 24 | export type TdProps = OmitProps & { 25 | columnKey?: number; 26 | }; 27 | 28 | // Component-specific types that combine OmitProps with appropriate HTML attributes 29 | export type TrProps = OmitProps & HTMLAttributes; 30 | export type TbodyProps = OmitProps & HTMLAttributes; 31 | export type TableProps = OmitProps & TableHTMLAttributes; 32 | export type TheadProps = OmitProps & HTMLAttributes; 33 | -------------------------------------------------------------------------------- /src/utils/allowed.ts: -------------------------------------------------------------------------------- 1 | import { OmitProps } from '../types'; 2 | 3 | export const allowed = ({ 4 | inHeader, 5 | columnKey, 6 | headers, 7 | forwardedRef, 8 | ...rest 9 | }: OmitProps) => { 10 | return rest; 11 | }; 12 | 13 | export default allowed; 14 | -------------------------------------------------------------------------------- /src/utils/tableContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useState, 4 | useMemo, 5 | PropsWithChildren, 6 | } from 'react'; 7 | 8 | type HeaderContextType = { 9 | headers: string[]; 10 | setHeaders: React.Dispatch>; 11 | }; 12 | 13 | const HeaderContext = createContext(undefined); 14 | 15 | function HeaderProvider({ children }: PropsWithChildren) { 16 | const [headers, setHeaders] = useState([]); 17 | 18 | const contextValue = useMemo(() => ({ headers, setHeaders }), [headers]); 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | 27 | export { HeaderProvider, HeaderContext }; 28 | -------------------------------------------------------------------------------- /test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fireEvent, render, screen } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | 5 | import { Table, Thead, Tbody, Tr, Th, Td } from '../src'; 6 | 7 | describe('SuperResponsiveTable CommonCase', () => { 8 | const setup = () => { 9 | const [val1, val2, val3] = [0, '', false]; 10 | const { getAllByTestId, getByTestId, getAllByText } = render( 11 |
; 16 | } 17 | 18 | return ( 19 | 20 |
21 | {headers[columnKey!]} 22 |
23 | {children ??
 
} 24 |
; 7 | } 8 | 9 | export default Th; 10 | -------------------------------------------------------------------------------- /src/components/Thead.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import allowed from '../utils/allowed'; 4 | import { TheadProps } from '../types'; 5 | 6 | function Thead(props: TheadProps) { 7 | const { children } = props; 8 | 9 | return ( 10 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Header 1Header 2Header 3
Row 1Row 2Row 3
{val1}{val2}{val3}
32 | ); 33 | 34 | return { 35 | getAllByText, 36 | getTable: getByTestId('table'), 37 | getThead: getByTestId('thead'), 38 | getTr: getAllByTestId('tr'), 39 | getTh: getAllByTestId('th'), 40 | getTbody: getByTestId('tbody'), 41 | getTd: getAllByTestId('td'), 42 | getTdBefore: getAllByTestId('td-before'), 43 | }; 44 | }; 45 | 46 | it('render the table element', () => { 47 | const { getTable } = setup(); 48 | 49 | expect(getTable).toBeInTheDocument(); 50 | }); 51 | 52 | it('renders every thead', () => { 53 | const { getThead } = setup(); 54 | 55 | expect(getThead).toBeInTheDocument(); 56 | }); 57 | 58 | it('renders every th', () => { 59 | const { getTh } = setup(); 60 | 61 | expect(getTh.length).toBe(3); 62 | }); 63 | 64 | it('renders every tr', () => { 65 | const { getTr } = setup(); 66 | 67 | expect(getTr.length).toBe(3); 68 | }); 69 | 70 | it('renders every tbody', () => { 71 | const { getTbody } = setup(); 72 | 73 | expect(getTbody).toBeInTheDocument(); 74 | }); 75 | 76 | it('renders every td', () => { 77 | const { getTd } = setup(); 78 | 79 | expect(getTd.length).toBe(6); 80 | }); 81 | 82 | it('renders every td inner text', () => { 83 | const { getTdBefore } = setup(); 84 | 85 | expect(getTdBefore.length).toBe(6); 86 | }); 87 | 88 | it('renders nullish value', () => { 89 | const { getTd } = setup(); 90 | 91 | expect(getTd[3].childNodes[1].textContent).toBe('0'); 92 | }); 93 | }); 94 | 95 | describe('SuperResponsiveTable UniqueCase', () => { 96 | const setup = (components) => { 97 | const { getAllByTestId, getByTestId, getAllByText } = render(components); 98 | return { 99 | getAllByText, 100 | getTable: getByTestId('table'), 101 | getThead: getByTestId('thead'), 102 | getTr: getAllByTestId('tr'), 103 | getTh: getAllByTestId('th'), 104 | getTbody: getByTestId('tbody'), 105 | getTd: getAllByTestId('td'), 106 | getTdBefore: getAllByTestId('td-before'), 107 | }; 108 | }; 109 | 110 | it('Render table with an only one column', () => { 111 | const { getTr, getTh, getTd } = setup( 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
Annual Conference
31
124 | ); 125 | 126 | expect(getTr.length).toBe(2); 127 | expect(getTh.length).toBe(1); 128 | expect(getTd.length).toBe(1); 129 | expect(getTh[0].childNodes[0].textContent).toBe('Annual Conference'); 130 | expect(getTd[0].childNodes[1].textContent).toBe('31'); 131 | }); 132 | 133 | test('Render table with colSpan attribute', () => { 134 | const { getByText } = render( 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
MonthSavings
January$100
February$80
Sum: $180
156 | ); 157 | 158 | const spannedColumn = getByText('Sum: $180'); 159 | expect(spannedColumn).toBeInTheDocument(); 160 | expect(spannedColumn).toHaveAttribute('colSpan', '2'); 161 | }); 162 | test('Render table without any cells', () => { 163 | render( 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 |
172 | ); 173 | 174 | expect(screen.getByRole('table')).toBeInTheDocument(); 175 | 176 | expect(screen.queryByRole('cell')).not.toBeInTheDocument(); 177 | expect(screen.queryByRole('columnheader')).not.toBeInTheDocument(); 178 | }); 179 | 180 | test('Render table with only header and empty row', () => { 181 | render( 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 |
Header 1Header 2
193 | ); 194 | 195 | expect(screen.getByText('Header 1')).toBeInTheDocument(); 196 | expect(screen.getByText('Header 2')).toBeInTheDocument(); 197 | 198 | const tbody = screen.getAllByRole('rowgroup')[1]; 199 | expect(tbody).toBeInTheDocument(); 200 | 201 | expect(tbody.querySelectorAll('td').length).toBe(0); 202 | }); 203 | 204 | test('Render table with conditional column', () => { 205 | let showColumn = true; 206 | 207 | const { rerender } = render( 208 | 209 | 210 | 211 | 212 | 213 | {showColumn && } 214 | 215 | 216 | 217 | 218 | 219 | 220 | {showColumn && } 221 | 222 | 223 |
Header 1Header 2Conditional Header
Cell 1Cell 2Conditional Cell
224 | ); 225 | 226 | expect(screen.getByRole('table')).toBeInTheDocument(); 227 | 228 | expect(screen.getAllByText('Conditional Header')).toHaveLength(2); 229 | expect(screen.getAllByText('Conditional Cell')).toHaveLength(1); 230 | 231 | showColumn = false; 232 | rerender( 233 | 234 | 235 | 236 | 237 | 238 | {showColumn && } 239 | 240 | 241 | 242 | 243 | 244 | 245 | {showColumn && } 246 | 247 | 248 |
Header 1Header 2Conditional Header
Cell 1Cell 2Conditional Cell
249 | ); 250 | 251 | expect(screen.queryByText('Conditional Header')).not.toBeInTheDocument(); 252 | expect(screen.queryByText('Conditional Cell')).not.toBeInTheDocument(); 253 | }); 254 | 255 | test('Render table with conditional columns', () => { 256 | render( 257 | 258 | 259 | 260 | 261 | {false && } 262 | 263 | 264 | 265 | 266 | 267 | 268 | {false && } 269 | 270 | 271 | 272 | 273 | 274 |
C1C2C3
V1V2V3V4V6
275 | ); 276 | 277 | expect(screen.getAllByText('C1')).toHaveLength(2); 278 | expect(screen.queryAllByText('C2')).toHaveLength(0); 279 | expect(screen.getAllByText('C3')).toHaveLength(2); 280 | 281 | expect(screen.getByText('V1')).toBeInTheDocument(); 282 | expect(screen.queryByText('V2')).not.toBeInTheDocument(); 283 | expect(screen.getByText('V3')).toBeInTheDocument(); 284 | expect(screen.getByText('V4')).toBeInTheDocument(); 285 | expect(screen.getByText('V6')).toBeInTheDocument(); 286 | }); 287 | 288 | test('Render table with more columns in header', () => { 289 | render( 290 | 291 | 292 | 293 | 294 | {false && } 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | {false && } 305 | 306 | 307 | 308 |
C1C2C3C4C5C6
V1V2V3
309 | ); 310 | 311 | expect(screen.getAllByText('C1')).toHaveLength(2); 312 | expect(screen.queryByText('C2')).not.toBeInTheDocument(); 313 | expect(screen.getAllByText('C3')).toHaveLength(2); 314 | expect(screen.getAllByText('C4')).toHaveLength(1); 315 | expect(screen.getAllByText('C5')).toHaveLength(1); 316 | expect(screen.getAllByText('C6')).toHaveLength(1); 317 | 318 | expect(screen.getByText('V1')).toBeInTheDocument(); 319 | expect(screen.queryByText('V2')).not.toBeInTheDocument(); 320 | expect(screen.getByText('V3')).toBeInTheDocument(); 321 | }); 322 | 323 | test('Table updates correctly upon change of key prop', () => { 324 | const Parent = ({ headers, i }) => ( 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 |
{headers[0]}{headers[1]}
item 1item 2
339 | ); 340 | 341 | const headersA = ['alpha', 'beta']; 342 | const headersB = ['one', 'two']; 343 | 344 | const { rerender } = render(); 345 | expect(screen.getAllByText('alpha')).toHaveLength(2); 346 | expect(screen.getAllByText('beta')).toHaveLength(2); 347 | expect(screen.queryByText('one')).not.toBeInTheDocument(); 348 | expect(screen.queryByText('two')).not.toBeInTheDocument(); 349 | 350 | rerender(); 351 | expect(screen.getAllByText('one')).toHaveLength(2); 352 | expect(screen.getAllByText('two')).toHaveLength(2); 353 | expect(screen.queryByText('alpha')).not.toBeInTheDocument(); 354 | expect(screen.queryByText('beta')).not.toBeInTheDocument(); 355 | }); 356 | 357 | test('Renders rowSpan correctly', () => { 358 | render( 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 |
C1C2C3
V3V4V6
V5V6
379 | ); 380 | 381 | const cellWithRowSpan = screen.getByText('V4'); 382 | expect(cellWithRowSpan).toBeInTheDocument(); 383 | expect(cellWithRowSpan).toHaveAttribute('rowSpan', '2'); 384 | }); 385 | 386 | it('should handle events and pass React HTMLAttributes props to Tr, Th, and Td components', () => { 387 | const handleClick = jest.fn(); 388 | const handleMouseEnter = jest.fn(); 389 | const handleDoubleClick = jest.fn(); 390 | const handleKeyDown = jest.fn(); 391 | 392 | const { getByTestId } = render( 393 | 394 | 395 | 396 | 404 | 405 | 406 | 407 | 408 | 409 | 417 | 418 | 419 | 420 |
402 | Header 1 403 | Header 2
415 | Cell 1 416 | Cell 2
421 | ); 422 | 423 | const row = getByTestId('clickable-row'); 424 | const header = getByTestId('header'); 425 | const cell = getByTestId('cell'); 426 | 427 | fireEvent.click(row); 428 | expect(handleClick).toHaveBeenCalledTimes(1); 429 | 430 | fireEvent.mouseEnter(header); 431 | fireEvent.mouseEnter(cell); 432 | expect(handleMouseEnter).toHaveBeenCalledTimes(2); 433 | 434 | fireEvent.doubleClick(header); 435 | fireEvent.doubleClick(cell); 436 | expect(handleDoubleClick).toHaveBeenCalledTimes(2); 437 | 438 | fireEvent.keyDown(header, { key: 'Enter', code: 'Enter' }); 439 | fireEvent.keyDown(cell, { key: 'Enter', code: 'Enter' }); 440 | expect(handleKeyDown).toHaveBeenCalledTimes(2); 441 | }); 442 | }); 443 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/esm", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es6", "dom", "es2016", "es2017"], 7 | "declaration": true, 8 | "moduleResolution": "node", 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "noImplicitThis": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "allowSyntheticDefaultImports": true, 15 | "allowJs": true, 16 | "skipLibCheck": true, 17 | "strict": false, 18 | "forceConsistentCasingInFileNames": true, 19 | "noEmit": false, 20 | "incremental": true, 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | "jsx": "react", 24 | "esModuleInterop": true 25 | }, 26 | "ts-node": { 27 | "moduleTypes": { 28 | "jest.config.ts": "cjs" 29 | } 30 | }, 31 | "include": ["src"], 32 | "exclude": ["node_modules", "dist", "test", "docs"] 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "allowJs": true, 5 | "target": "es2015", 6 | "module": "CommonJS", 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "strict": false, 10 | "moduleResolution": "Node", 11 | "jsx": "react", 12 | "types": ["jest", "@testing-library/jest-dom"] 13 | }, 14 | "include": ["**/*.ts", "**/*.tsx", "**/*.js"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------