├── .codesandbox └── ci.json ├── .coveralls.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql.yml │ ├── gh-pages.yml │ ├── nodejs-ci.yml │ ├── nodejs-publish.yml │ └── size-limit.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── docs ├── App.tsx ├── components │ ├── Brand.tsx │ ├── Copyright.tsx │ ├── Frame │ │ ├── Frame.tsx │ │ ├── NavToggle.tsx │ │ ├── index.ts │ │ └── styles.less │ ├── Header │ │ ├── Header.tsx │ │ ├── index.ts │ │ └── styles.less │ └── Logo │ │ ├── Logo.tsx │ │ ├── index.tsx │ │ └── styles.less ├── data │ ├── mock.ts │ ├── usersForColSpan.ts │ └── usersForRowSpan.ts ├── examples │ ├── AffixHorizontalScrollbar.md │ ├── AffixTable.md │ ├── AutoHeightTable.md │ ├── BigTreeTable.md │ ├── ColspanTable.md │ ├── ColumnGroupTable.md │ ├── CustomCellTable.md │ ├── CustomColumnTable.md │ ├── CustomColumns.md │ ├── DraggableTable.md │ ├── DynamicTable.md │ ├── EditTable.md │ ├── EmptyDataTable.md │ ├── Expanded.md │ ├── FillHeightTable.md │ ├── FixedColumnTable.md │ ├── FluidColumnTable.md │ ├── HideTableHeader.md │ ├── InfiniteLoader.md │ ├── LoadingTable.md │ ├── ResizableColumnTable.md │ ├── RowspanTable.md │ ├── SortTable.md │ ├── TreeTable.md │ ├── UpdateData.md │ ├── Virtualized.md │ ├── WordWrapTable.md │ └── fullText.md ├── gh-pages.js ├── index.html ├── index.tsx └── less │ └── index.less ├── eslint.config.mjs ├── examples └── with-gatsby │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── LICENSE │ ├── README.md │ ├── gatsby-config.js │ ├── package.json │ ├── src │ └── pages │ │ └── index.js │ └── static │ └── favicon.ico ├── gulpfile.js ├── karma.conf.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── Cell.tsx ├── CellGroup.tsx ├── Column.tsx ├── ColumnGroup.tsx ├── ColumnResizeHandler.tsx ├── EmptyMessage.tsx ├── HeaderCell.tsx ├── Loader.tsx ├── MouseArea.tsx ├── Row.tsx ├── Scrollbar.tsx ├── Table.tsx ├── TableProvider.tsx ├── constants.ts ├── hooks │ ├── index.ts │ ├── useAffix.ts │ ├── useCellDescriptor.ts │ ├── useClassNames.ts │ ├── useControlled.ts │ ├── useIntersectionObserver.ts │ ├── useIsomorphicLayoutEffect.ts │ ├── useMount.ts │ ├── usePosition.ts │ ├── useScrollListener.ts │ ├── useTable.ts │ ├── useTableData.ts │ ├── useTableDimension.ts │ ├── useTableRows.ts │ ├── useUpdateEffect.ts │ └── useUpdateLayoutEffect.ts ├── icons │ ├── ArrowRight.tsx │ ├── Sort.tsx │ └── SortDown.tsx ├── index.ts ├── less │ ├── column-group.less │ ├── functions.less │ ├── index.less │ ├── loader.less │ ├── scrollbar.less │ ├── table.less │ ├── themes.less │ ├── treetable.less │ └── variables.less ├── types.ts └── utils │ ├── children.ts │ ├── convertToFlex.ts │ ├── defer.ts │ ├── findAllParents.ts │ ├── findRowKeys.ts │ ├── flattenData.ts │ ├── flushSync.ts │ ├── getColumnProps.ts │ ├── getTableColumns.ts │ ├── getTotalByColumns.ts │ ├── index.ts │ ├── isNumberOrTrue.ts │ ├── isRTL.ts │ ├── isSupportTouchEvent.ts │ ├── mergeCells.tsx │ ├── mergeRefs.ts │ ├── prefix.ts │ ├── react-is.ts │ ├── requestAnimationTimeout.ts │ ├── resetLeftForCells.ts │ ├── setCssPosition.ts │ ├── shouldShowRowByExpanded.ts │ ├── toggle.ts │ └── toggleClass.ts ├── test ├── CellGroupSpec.tsx ├── CellSpec.tsx ├── ColumnGroupSpec.tsx ├── ColumnGroupTableSpec.tsx ├── ColumnResizeHandlerSpec.tsx ├── ColumnSpec.tsx ├── ExpandableTableSpec.tsx ├── HeaderCellSpec.tsx ├── RowSpec.tsx ├── ScrollbarSpec.tsx ├── SortTableSpec.tsx ├── Table.test.tsx ├── TableHeightSpec.tsx ├── TableSpec.tsx ├── TreeTableSpec.tsx ├── VirtualizedTableSpec.tsx ├── build.test.js ├── findAllParentsSpec.ts ├── flattenDataSpec.ts ├── useCellDescriptorSpec.tsx └── utils │ ├── index.tsx │ └── testStandardProps.ts ├── tsconfig.json ├── tsconfig.test.json ├── webpack.build.config.js ├── webpack.config.js └── webpack.karma.js /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["new", "vanilla"] 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | repo_token: gAzr21T3BR8wMZlE5NcKKkXlAD0BTrZZi 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: rsuite 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ### Versions 10 | 11 | 15 | 16 | | package | version | 17 | | -------------- | ------- | 18 | | `react` | `X.Y.Z` | 19 | | `rsuite-table` | `X.Y.Z` | 20 | 21 | ### What is the expected behavior? 22 | 23 | ### What is the current behavior? 24 | 25 | ### What are the steps to reproduce? 26 | 27 | 35 | 36 | ### Any additional comments? (optional) 37 | 38 | 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | ### What problem does this feature solve? 10 | 11 | 24 | 25 | ### What does the proposed API look like? 26 | 27 | 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: 'CodeQL' 13 | 14 | on: 15 | push: 16 | branches: [main,] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [main] 20 | schedule: 21 | - cron: '16 1 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-20.04 14 | permissions: 15 | contents: write 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: pnpm/action-setup@v4 22 | name: Install pnpm 23 | id: pnpm-install 24 | with: 25 | version: 8 26 | 27 | - name: Setup node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: '20.x' 31 | 32 | - name: Install 33 | run: pnpm i 34 | 35 | - name: Build 36 | run: npm run build:docs 37 | 38 | - name: Deploy 39 | uses: peaceiris/actions-gh-pages@v4 40 | if: github.ref == 'refs/heads/main' 41 | with: 42 | github_token: ${{ secrets.GITHUB_TOKEN }} 43 | publish_dir: ./assets 44 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - next 11 | pull_request: 12 | branches: 13 | - main 14 | - next 15 | jobs: 16 | test: 17 | name: 'Test' 18 | 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | browser: [ChromeCi, Firefox] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: pnpm/action-setup@v4 29 | name: Install pnpm 30 | id: pnpm-install 31 | with: 32 | version: 8 33 | 34 | - name: Use Node.js LTS 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: 'lts/*' 38 | - name: Cache Node.js modules 39 | uses: actions/cache@v4 40 | with: 41 | path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS 42 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 43 | restore-keys: | 44 | ${{ runner.OS }}-node- 45 | ${{ runner.OS }}- 46 | - name: Install dependencies 47 | run: pnpm i 48 | - name: Run headless tests 49 | run: xvfb-run --auto-servernum npm test 50 | env: 51 | CI: true 52 | BROWSER: ${{ matrix.browser }} 53 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-publish.yml: -------------------------------------------------------------------------------- 1 | # see https://help.github.com/cn/actions/language-and-framework-guides/publishing-nodejs-packages 2 | 3 | name: Node.js Package 4 | 5 | on: 6 | push: 7 | tags: ['*'] 8 | 9 | jobs: 10 | publish: 11 | name: 'Publish' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: pnpm/action-setup@v4 17 | name: Install pnpm 18 | id: pnpm-install 19 | with: 20 | version: 8 21 | 22 | # Setup .npmrc file to publish to npm 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: '20.x' 26 | registry-url: 'https://registry.npmjs.org' 27 | - name: Install dependencies 28 | run: pnpm i 29 | - run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: Size Limit 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | - next 7 | 8 | jobs: 9 | size: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 'lts/*' 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Run Size Limit 24 | run: npm run size 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage directory 12 | coverage 13 | 14 | # Dependency directories 15 | node_modules 16 | jspm_packages 17 | 18 | # Build outputs 19 | dist 20 | lib 21 | es 22 | 23 | # IDE and editor files 24 | .vscode/ 25 | 26 | # OS generated files 27 | .DS_Store 28 | 29 | # Package manager files 30 | yarn.lock 31 | 32 | # Cache files 33 | .npm 34 | .eslintcache 35 | 36 | # Miscellaneous 37 | .nyc_output 38 | .grunt 39 | .lock-wscript 40 | build/Release 41 | assets -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | docs/ 3 | tools/ 4 | src/ 5 | dist/ 6 | webpack* 7 | assets 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | singleQuote: true, 5 | arrowParens: 'avoid', 6 | trailingComma: 'none' 7 | }; 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at simonguo.2009@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Suite Community 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, options) => { 2 | const { NODE_ENV } = options || process.env; 3 | const dev = NODE_ENV === 'development'; 4 | const modules = NODE_ENV === 'esm' ? false : 'commonjs'; 5 | 6 | if (api) { 7 | api.cache(() => NODE_ENV); 8 | } 9 | 10 | return { 11 | presets: [ 12 | ['@babel/preset-env', { modules, loose: true }], 13 | ['@babel/preset-react', { development: dev }], 14 | '@babel/preset-typescript' 15 | ], 16 | plugins: [ 17 | '@babel/plugin-proposal-export-default-from', 18 | ['@babel/plugin-transform-class-properties', { loose: true }], 19 | '@babel/plugin-transform-optional-chaining', 20 | '@babel/plugin-transform-export-namespace-from', 21 | ['@babel/plugin-transform-runtime', { useESModules: !modules }] 22 | ] 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /docs/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Nav } from 'rsuite'; 3 | import CodeView from 'react-code-view'; 4 | import Frame from './components/Frame'; 5 | import TableIcon from '@rsuite/icons/Table'; 6 | import GithubIcon from '@rsuite/icons/legacy/Github'; 7 | import BookIcon from '@rsuite/icons/legacy/Book'; 8 | import kebabCase from 'lodash/kebabCase'; 9 | 10 | interface ExampleType { 11 | title: string; 12 | content: React.ReactNode; 13 | } 14 | 15 | interface ExamplesProps { 16 | dependencies?: any; 17 | examples: ExampleType[]; 18 | } 19 | 20 | const getDefaultIndex = () => { 21 | const hash = document.location.hash.replace('#', ''); 22 | return hash || 'virtualized'; 23 | }; 24 | 25 | const afterCompile = (code: string) => { 26 | return code.replace(/import\ [\*\w\,\{\}\ \n]+\ from\ ?[\."'@/\w-]+;/gi, ''); 27 | }; 28 | 29 | const App = (props: ExamplesProps) => { 30 | const { examples, dependencies } = props; 31 | const [index, setIndex] = React.useState(getDefaultIndex()); 32 | 33 | const content = examples.find(item => kebabCase(item.title) === index)?.content; 34 | 35 | return ( 36 | 39 | } 45 | > 46 | {examples.map((item, i) => { 47 | const navKey = kebabCase(item.title); 48 | 49 | return ( 50 | { 55 | setIndex(navKey); 56 | }} 57 | > 58 | {item.title} 59 | 60 | ); 61 | })} 62 | 63 | } 67 | href="https://github.com/rsuite/rsuite-table#api" 68 | > 69 | APIs 70 | 71 | } 75 | href="https://github.com/rsuite/rsuite-table" 76 | > 77 | Github 78 | 79 | 80 | } 81 | > 82 | 83 | {content} 84 | 85 | 86 | ); 87 | }; 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /docs/components/Brand.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Logo from './Logo'; 4 | import { Stack } from 'rsuite'; 5 | 6 | const Brand = ({ showText, ...props }) => { 7 | return ( 8 | 9 | 10 | {showText && rsuite table} 11 | 12 | ); 13 | }; 14 | 15 | export default Brand; 16 | -------------------------------------------------------------------------------- /docs/components/Copyright.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack } from 'rsuite'; 3 | 4 | const Copyright = () => { 5 | return ( 6 | 7 |
8 |

9 | © 2022, Made with ❤️ by{' '} 10 | 11 | RSUITE 12 | 13 |

14 |
15 |
16 | ); 17 | }; 18 | 19 | export default Copyright; 20 | -------------------------------------------------------------------------------- /docs/components/Frame/Frame.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import classNames from 'classnames'; 3 | import { Container, Sidebar, Sidenav, Content, DOMHelper } from 'rsuite'; 4 | import NavToggle from './NavToggle'; 5 | import Brand from '../Brand'; 6 | import Copyright from '../Copyright'; 7 | import Header from '../Header'; 8 | 9 | const { getHeight, on } = DOMHelper; 10 | 11 | export interface NavItemData { 12 | eventKey: string; 13 | title: string; 14 | icon?: any; 15 | to?: string; 16 | target?: string; 17 | children?: NavItemData[]; 18 | } 19 | 20 | export interface FrameProps { 21 | navs: React.ReactNode; 22 | children?: React.ReactNode; 23 | } 24 | 25 | const Frame = (props: FrameProps) => { 26 | const { navs, children } = props; 27 | const [expand, setExpand] = useState(true); 28 | const [windowHeight, setWindowHeight] = useState(getHeight(window)); 29 | 30 | useEffect(() => { 31 | setWindowHeight(getHeight(window)); 32 | const resizeListenner = on(window, 'resize', () => setWindowHeight(getHeight(window))); 33 | 34 | return () => { 35 | resizeListenner.off(); 36 | }; 37 | }, []); 38 | 39 | const containerClasses = classNames('page-container', { 40 | 'container-full': !expand 41 | }); 42 | 43 | const navBodyStyle: React.CSSProperties = expand 44 | ? { height: windowHeight - 112, overflow: 'auto' } 45 | : {}; 46 | 47 | return ( 48 | 49 | 54 | 55 | 56 | 57 | 58 | {navs} 59 | 60 | setExpand(!expand)} /> 61 | 62 | 63 | 64 |
65 | {children} 66 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default Frame; 73 | -------------------------------------------------------------------------------- /docs/components/Frame/NavToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navbar, Nav } from 'rsuite'; 3 | import ArrowLeftLineIcon from '@rsuite/icons/ArrowLeftLine'; 4 | import ArrowRightLineIcon from '@rsuite/icons/ArrowRightLine'; 5 | 6 | interface NavToggleProps { 7 | expand?: boolean; 8 | onChange?: () => void; 9 | } 10 | 11 | const NavToggle = ({ expand, onChange }: NavToggleProps) => { 12 | return ( 13 | 14 | 21 | 22 | ); 23 | }; 24 | 25 | export default NavToggle; 26 | -------------------------------------------------------------------------------- /docs/components/Frame/index.ts: -------------------------------------------------------------------------------- 1 | import Frame from './Frame'; 2 | 3 | export default Frame; 4 | -------------------------------------------------------------------------------- /docs/components/Frame/styles.less: -------------------------------------------------------------------------------- 1 | .frame { 2 | height: 100vh; 3 | .rs-sidebar { 4 | background: #fff; 5 | } 6 | .rs-sidenav { 7 | flex: 1 1 auto; 8 | transition: none !important; 9 | border-top: 1px solid @B100; 10 | width: 100%; 11 | } 12 | 13 | .rs-content { 14 | padding: 0 10px; 15 | 16 | h3 { 17 | margin-top: 20px; 18 | } 19 | 20 | .rcv-markdown { 21 | margin: 0 20px; 22 | .rcv-highlight { 23 | background: #fff; 24 | border-radius: 6px; 25 | } 26 | } 27 | 28 | .rcv-container { 29 | background: #fff; 30 | padding: 20px; 31 | margin: 20px; 32 | border-radius: 6px; 33 | border: none; 34 | } 35 | } 36 | .nav-toggle { 37 | border-top: 1px solid @B100; 38 | } 39 | 40 | .rs-sidenav-item, 41 | .rs-dropdown-item { 42 | &.active { 43 | color: @H500 !important; 44 | } 45 | &-icon { 46 | height: 1.3em !important; 47 | width: 1.3em !important; 48 | left: 18px !important; 49 | } 50 | } 51 | 52 | .rs-sidenav-item-panel { 53 | padding: 15px 20px; 54 | color: #aaa; 55 | } 56 | .rs-sidenav-collapse-out { 57 | .collapse-hide { 58 | display: none; 59 | } 60 | } 61 | } 62 | 63 | .brand { 64 | padding: 10px 16px; 65 | font-size: 16px; 66 | white-space: nowrap; 67 | overflow: hidden; 68 | font-weight: bold; 69 | text-transform: uppercase; 70 | 71 | a { 72 | text-decoration: none; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack, IconButton } from 'rsuite'; 3 | import GithubIcon from '@rsuite/icons/legacy/Github'; 4 | import HeartIcon from '@rsuite/icons/legacy/HeartO'; 5 | 6 | const Header = () => { 7 | return ( 8 | 9 | 10 | npm 11 | 12 | } 14 | href="https://opencollective.com/rsuite" 15 | target="_blank" 16 | /> 17 | } 19 | href="https://github.com/rsuite/rsuite-table" 20 | target="_blank" 21 | /> 22 | 23 | ); 24 | }; 25 | 26 | export default Header; 27 | -------------------------------------------------------------------------------- /docs/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | import Header from './Header'; 2 | 3 | export default Header; 4 | -------------------------------------------------------------------------------- /docs/components/Header/styles.less: -------------------------------------------------------------------------------- 1 | .header { 2 | position: absolute; 3 | right: 30px; 4 | top: 20px; 5 | cursor: pointer; 6 | z-index: 1; 7 | } 8 | -------------------------------------------------------------------------------- /docs/components/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LogoProps { 4 | width?: number; 5 | height?: number; 6 | className?: string; 7 | style?: React.CSSProperties; 8 | } 9 | 10 | export default function Logo({ width, height, style, className = '' }: LogoProps) { 11 | const styles = { width, height, display: 'inline-block', ...style }; 12 | return ( 13 |
17 | 25 | React Suite 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 56 | 66 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /docs/components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import Logo from './Logo'; 2 | 3 | export default Logo; 4 | -------------------------------------------------------------------------------- /docs/components/Logo/styles.less: -------------------------------------------------------------------------------- 1 | .rsuite-logo { 2 | .cls-1 { 3 | fill: #6292f0; 4 | } 5 | .cls-2 { 6 | fill: #ec727d; 7 | } 8 | .cls-1, 9 | .cls-2 { 10 | fill-rule: evenodd; 11 | } 12 | 13 | .polyline-limb { 14 | animation: limbLineMove 3s ease-out 1; 15 | } 16 | .polyline-axis { 17 | animation: axisLineMove 2s ease-out 1; 18 | } 19 | 20 | .circle { 21 | animation: circleMove 2s ease-out 1; 22 | } 23 | } 24 | 25 | .logo-animated { 26 | animation-duration: 1s; 27 | animation-fill-mode: both; 28 | } 29 | 30 | .logo-animated-delay-half-seconds { 31 | animation-delay: 0.5s; 32 | } 33 | 34 | .bounce-in { 35 | animation-name: logo-bounce-in; 36 | } 37 | 38 | @keyframes logo-bounce-in { 39 | from, 40 | 20%, 41 | 40%, 42 | 60%, 43 | 80%, 44 | to { 45 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 46 | } 47 | 0% { 48 | opacity: 0; 49 | transform: scale3d(0.3, 0.3, 0.3); 50 | } 51 | 20% { 52 | opacity: 1; 53 | transform: scale3d(1.1, 1.1, 1.1); 54 | } 55 | 40% { 56 | transform: scale3d(0.9, 0.9, 0.9); 57 | } 58 | 60% { 59 | transform: scale3d(1.03, 1.03, 1.03); 60 | } 61 | 80% { 62 | transform: scale3d(0.97, 0.97, 0.97); 63 | } 64 | to { 65 | opacity: 1; 66 | transform: scale3d(1, 1, 1); 67 | } 68 | } 69 | 70 | @keyframes axisLineMove { 71 | 0% { 72 | stroke-dasharray: 0, 500; 73 | } 74 | 100% { 75 | stroke-dasharray: 500, 500; 76 | } 77 | } 78 | 79 | @keyframes limbLineMove { 80 | 0% { 81 | stroke-dasharray: 0, 200; 82 | stroke: transparent; 83 | } 84 | 50% { 85 | stroke-dasharray: 0, 200; 86 | } 87 | 100% { 88 | stroke-dasharray: 200, 200; 89 | } 90 | } 91 | 92 | @keyframes circleMove { 93 | 0% { 94 | fill: transparent; 95 | } 96 | 50% { 97 | fill: transparent; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /docs/data/mock.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en'; 2 | 3 | export const createUser = rowIndex => { 4 | const firstName = faker.name.firstName(); 5 | const lastName = faker.name.lastName(); 6 | const gender = faker.name.sex(); 7 | const name = faker.name.fullName({ firstName, lastName }); 8 | const avatar = 'https://i.pravatar.cc/150?u=' + name; 9 | 10 | const city = faker.address.city(); 11 | const street = faker.address.street(); 12 | const email = faker.internet.email(); 13 | const postcode = faker.address.zipCode(); 14 | const phone = faker.phone.number(); 15 | const amount = faker.finance.amount(1000, 90000); 16 | const company = faker.company.bs(); 17 | 18 | const age = Math.floor(Math.random() * 30) + 18; 19 | const stars = Math.floor(Math.random() * 10000); 20 | const followers = Math.floor(Math.random() * 10000); 21 | const rating = 2 + Math.floor(Math.random() * 3); 22 | const progress = Math.floor(Math.random() * 100); 23 | 24 | return { 25 | id: rowIndex + 1, 26 | name, 27 | firstName, 28 | lastName, 29 | avatar, 30 | company, 31 | city, 32 | street, 33 | postcode, 34 | email, 35 | phone, 36 | gender, 37 | age, 38 | stars, 39 | followers, 40 | rating, 41 | progress, 42 | amount 43 | }; 44 | }; 45 | 46 | export function mockUsers(length: number) { 47 | return Array.from({ length }).map((_, index) => { 48 | return createUser(index); 49 | }); 50 | } 51 | 52 | export function mockTreeData(options: { 53 | limits: number[]; 54 | labels: string | string[] | ((layer: number, value: string, faker) => string); 55 | getRowData?: (layer: number, value: string) => any[]; 56 | }) { 57 | const { limits, labels, getRowData } = options; 58 | const depth = limits.length; 59 | 60 | const data = []; 61 | const mock = (list, parentValue?: string, layer = 0) => { 62 | const length = limits[layer]; 63 | 64 | Array.from({ length }).forEach((_, index) => { 65 | const value = parentValue ? parentValue + '-' + (index + 1) : index + 1 + ''; 66 | const children = []; 67 | const label = Array.isArray(labels) ? labels[layer] : labels; 68 | 69 | let row: any = { 70 | label: typeof label === 'function' ? label(layer, value, faker) : label + ' ' + value, 71 | value 72 | }; 73 | 74 | if (getRowData) { 75 | row = { 76 | ...row, 77 | ...getRowData(layer, value) 78 | }; 79 | } 80 | 81 | list.push(row); 82 | 83 | if (layer < depth - 1) { 84 | row.children = children; 85 | mock(children, value, layer + 1); 86 | } 87 | }); 88 | }; 89 | 90 | mock(data); 91 | 92 | return data; 93 | } 94 | -------------------------------------------------------------------------------- /docs/examples/AffixHorizontalScrollbar.md: -------------------------------------------------------------------------------- 1 | ### Affix Horizontal Scrollbar 2 | 3 | Affix the table horizontal scrollbar to the specified position on the page 4 | 5 | 6 | 7 | ```js 8 | const App = () => { 9 | return ( 10 |
11 |
12 |
13 |

⬇️ Scroll down the page ⬇️

14 |
15 |
16 | { 23 | console.log(data); 24 | }} 25 | > 26 | 27 | Id 28 | 29 | 30 | 31 | 32 | First Name 33 | 34 | 35 | 36 | 37 | Last Name 38 | 39 | 40 | 41 | 42 | City 43 | 44 | 45 | 46 | 47 | Street 48 | 49 | 50 | 51 | 52 | Company 53 | 54 | 55 | 56 | 57 | Email 58 | 59 | 60 | 61 | 62 | Email 63 | 64 | 65 | 66 | 67 | Email 68 | 69 | 70 | 71 | 72 | Email 73 | 74 | 75 |
76 |
77 |
78 |
79 |
80 | ); 81 | }; 82 | ReactDOM.render(); 83 | ``` 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/examples/AffixTable.md: -------------------------------------------------------------------------------- 1 | ### Affix Header 2 | 3 | Affix the table header to the specified position on the page 4 | 5 | 6 | 7 | ```js 8 | const App = () => { 9 | return ( 10 |
11 | { 17 | console.log(data); 18 | }} 19 | > 20 | 21 | Id 22 | 23 | 24 | 25 | 26 | First Name 27 | 28 | 29 | 30 | 31 | Last Name 32 | 33 | 34 | 35 | 36 | City 37 | 38 | 39 | 40 | 41 | Street 42 | 43 | 44 | 45 | 46 | Company 47 | 48 | 49 | 50 | 51 | Email 52 | 53 | 54 | 55 | 56 | Email 57 | 58 | 59 | 60 | 61 | Email 62 | 63 | 64 | 65 | 66 | Email 67 | 68 | 69 |
70 |
71 | ); 72 | }; 73 | 74 | ReactDOM.render(); 75 | ``` 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/examples/AutoHeightTable.md: -------------------------------------------------------------------------------- 1 | ### Automatic height 2 | 3 | The height of the table will be automatically expanded according to the number of data rows, and no vertical scroll bar will appear. 4 | 5 | 6 | 7 | ```js 8 | const data = mockUsers(100); 9 | 10 | const App = () => { 11 | const [size, setSize] = React.useState(100); 12 | const [autoHeight, setAutoHeight] = React.useState(true); 13 | const [height, setHeight] = React.useState(400); 14 | const [maxHeight, setMaxHeight] = React.useState(500); 15 | const [minHeight, setMinHeight] = React.useState(200); 16 | 17 | const filterData = data.filter((item, index) => index < size); 18 | return ( 19 |
20 | 21 | { 24 | setAutoHeight(checked); 25 | }} 26 | > 27 | autoHeight 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | { 61 | console.log(data); 62 | }} 63 | > 64 | 65 | Id 66 | 67 | 68 | 69 | 70 | First Name 71 | 72 | 73 | 74 | 75 | Last Name 76 | 77 | 78 | 79 | 80 | City 81 | 82 | 83 | 84 | 85 | Street 86 | 87 | 88 | 89 | 90 | Company 91 | 92 | 93 | 94 | 95 | Email 96 | 97 | 98 | 99 | 100 | Email 101 | 102 | 103 | 104 | 105 | Email 106 | 107 | 108 | 109 | 110 | Email 111 | 112 | 113 |
114 |
115 | ); 116 | }; 117 | ReactDOM.render(); 118 | 119 | const Label = ({ children }) =>
{children}
; 120 | ``` 121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/examples/BigTreeTable.md: -------------------------------------------------------------------------------- 1 | ### Big Tree 2 | 3 | 4 | 5 | ```js 6 | const data = mockTreeData({ 7 | limits: [10, 20, 40], 8 | labels: layer => { 9 | if (layer === 0) { 10 | return faker.vehicle.manufacturer(); 11 | } else if (layer === 1) { 12 | return faker.vehicle.fuel(); 13 | } 14 | return faker.vehicle.vehicle(); 15 | }, 16 | getRowData: () => ({ 17 | price: faker.commerce.price(10000, 1000000, 0, '$', true), 18 | rating: faker.finance.amount(2, 5) 19 | }) 20 | }); 21 | 22 | const App = () => { 23 | const [tree, setTree] = React.useState(true); 24 | return ( 25 |
26 | }> 27 | { 30 | setTree(checked); 31 | }} 32 | > 33 | isTree 34 | 35 | 36 |
37 | 47 | 48 | Vehicle 🚗 49 | 50 | 51 | 52 | Rating ⭐️ 53 | 54 | {rowData => 55 | Array.from({ length: rowData.rating }).map((_, i) => ⭐️) 56 | } 57 | 58 | 59 | 60 | Price 💰 61 | 62 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | ReactDOM.render(); 69 | ``` 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/examples/ColspanTable.md: -------------------------------------------------------------------------------- 1 | ### Colspan Cell 2 | 3 | 4 | 5 | ```js 6 | class FixedColumnTable extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | data: fakeDataForColSpan 11 | }; 12 | } 13 | render() { 14 | return ( 15 |
16 | { 22 | console.log(data); 23 | }} 24 | > 25 | 26 | Id 27 | 28 | 29 | 30 | 31 | Name 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Address 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Company 52 | 53 | 54 | 55 | 56 | Email 57 | 58 | 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | ReactDOM.render(); 66 | ``` 67 | 68 | 69 | 70 | In some cases, you need to merge the relationships between columns to organize your data, and you can set a ColSpan attribute on the `` component, for example: 71 | 72 | ```html 73 | 74 | Name 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ``` 83 | -------------------------------------------------------------------------------- /docs/examples/ColumnGroupTable.md: -------------------------------------------------------------------------------- 1 | ### Column Group 2 | 3 | 4 | 5 | ```js 6 | const App = () => { 7 | const [sortColumn, setSortColumn] = React.useState('id'); 8 | const [sortType, setSortType] = React.useState('asc'); 9 | 10 | return ( 11 | { 21 | console.log(sortColumn, sortType); 22 | setSortType(sortType); 23 | setSortColumn(sortColumn); 24 | }} 25 | onRowClick={data => { 26 | console.log(data); 27 | }} 28 | > 29 | 30 | Id 31 | 32 | 33 | 34 | 41 | 42 | firstName 43 | 44 | 45 | 46 | 47 | lastName 48 | 49 | 50 | 51 | 52 | Email 53 | 54 | 55 | 56 | 57 | Company 58 | 59 | 60 | 61 | 62 | Company 63 | 64 | 65 | 66 | Company 67 | 68 | 69 | 70 | 71 | City 72 | 73 | 74 | 75 | 76 | Street 77 | 78 | 79 |
80 | ); 81 | }; 82 | 83 | ReactDOM.render(); 84 | ``` 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/examples/CustomColumnTable.md: -------------------------------------------------------------------------------- 1 | ### Custom column 2 | 3 | 4 | 5 | ```js 6 | const data = mockUsers(20); 7 | 8 | const CustomColumn = React.forwardRef((props, ref) => { 9 | return ; 10 | }); 11 | 12 | const App = () => { 13 | return ( 14 | 15 | 16 | Id 17 | 18 | 19 | 20 | 21 | First Name 22 | 23 | 24 | 25 | 26 | Last Name 27 | 28 | 29 | 30 | 31 | City 32 | 33 | 34 | 35 | 36 | 37 | Company flexGrow={2} 38 | 39 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | ReactDOM.render(); 46 | ``` 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/examples/CustomColumns.md: -------------------------------------------------------------------------------- 1 | ### Custom Columns 2 | 3 | 4 | 5 | ```js 6 | const App = () => { 7 | const [showColumns, setShowColumns] = React.useState(false); 8 | 9 | return ( 10 | 18 | 19 | Id 20 | 21 | 22 | 23 | 29 | 30 | 31 | firstName 32 | 33 | 34 | 35 | 36 | lastName 37 | 38 | 39 | 40 | 41 | {showColumns ? ( 42 | 43 | Email 44 | 45 | 46 | ) : null} 47 | 48 | 49 | 50 | 51 | Company 52 | 53 | 54 | 55 | 56 | Company 57 | 58 | 59 | 60 | 61 | 62 | Company 63 | 64 | 65 | 66 | 67 | City 68 | 69 | 70 | 71 | {showColumns ? ( 72 | 73 | Street 74 | 75 | 76 | ) : null} 77 |
78 | ); 79 | }; 80 | 81 | ReactDOM.render(); 82 | ``` 83 | 84 | 85 | -------------------------------------------------------------------------------- /docs/examples/EditTable.md: -------------------------------------------------------------------------------- 1 | ### Editable 2 | 3 | 4 | 5 | ```js 6 | const EditCell = ({ rowData, dataKey, onChange, ...props }) => { 7 | return ( 8 | 9 | {rowData.status === 'EDIT' ? ( 10 | { 14 | onChange && onChange(rowData.id, dataKey, event.target.value); 15 | }} 16 | /> 17 | ) : ( 18 | rowData[dataKey] 19 | )} 20 | 21 | ); 22 | }; 23 | 24 | const ActionCell = ({ rowData, dataKey, onClick, ...props }) => { 25 | return ( 26 | 27 | { 29 | onClick && onClick(rowData.id); 30 | }} 31 | > 32 | {rowData.status === 'EDIT' ? 'Save' : 'Edit'} 33 | 34 | 35 | ); 36 | }; 37 | 38 | const fakeData = mockUsers(20); 39 | 40 | const App = () => { 41 | const [data, setData] = React.useState(fakeData); 42 | const handleChange = (id, key, value) => { 43 | const nextData = clone(data); 44 | nextData.find(item => item.id === id)[key] = value; 45 | 46 | setData(nextData); 47 | }; 48 | 49 | const handleEditState = id => { 50 | const nextData = clone(data); 51 | const activeItem = nextData.find(item => item.id === id); 52 | activeItem.status = activeItem.status ? null : 'EDIT'; 53 | setData(nextData); 54 | }; 55 | 56 | return ( 57 | 58 | 59 | First Name 60 | 61 | 62 | 63 | 64 | Last Name 65 | 66 | 67 | 68 | 69 | Email 70 | 71 | 72 | 73 | 74 | Action 75 | 76 | 77 |
78 | ); 79 | }; 80 | 81 | ReactDOM.render(); 82 | ``` 83 | 84 | 85 | -------------------------------------------------------------------------------- /docs/examples/EmptyDataTable.md: -------------------------------------------------------------------------------- 1 | ### Empty data 2 | 3 | 4 | 5 | ```js 6 | const App = () => { 7 | return ( 8 | { 12 | return
No data found / 数据为空
; 13 | }} 14 | > 15 | 16 | Id 17 | 18 | 19 | 20 | 21 | First Name 22 | 23 | 24 | 25 | 26 | Last Name 27 | 28 | 29 | 30 | 31 | City 32 | 33 | 34 | 35 | 36 | Street 37 | 38 | 39 | 40 | 41 | Company 42 | 43 | 44 | 45 | 46 | Email 47 | 48 | 49 | 50 | 51 | Email 52 | 53 | 54 | 55 | 56 | Email 57 | 58 | 59 | 60 | 61 | Email 62 | 63 | 64 |
65 | ); 66 | }; 67 | ReactDOM.render(); 68 | ``` 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/examples/Expanded.md: -------------------------------------------------------------------------------- 1 | ### Expandable 2 | 3 | 4 | 5 | ```js 6 | const rowKey = 'id'; 7 | const ExpandCell = ({ rowData, dataKey, expandedRowKeys, onChange, ...props }) => ( 8 | 9 | 17 | 18 | ); 19 | 20 | const data = mockUsers(20); 21 | 22 | const ExpandedTable = () => { 23 | const [expandedRowKeys, setExpandedRowKeys] = React.useState([0]); 24 | 25 | const handleExpanded = (rowData, dataKey) => { 26 | let open = false; 27 | const nextExpandedRowKeys = []; 28 | 29 | expandedRowKeys.forEach(key => { 30 | if (key === rowData[rowKey]) { 31 | open = true; 32 | } else { 33 | nextExpandedRowKeys.push(key); 34 | } 35 | }); 36 | 37 | if (!open) { 38 | nextExpandedRowKeys.push(rowData[rowKey]); 39 | } 40 | 41 | setExpandedRowKeys(nextExpandedRowKeys); 42 | }; 43 | 44 | return ( 45 | { 52 | console.log(data); 53 | }} 54 | renderRowExpanded={rowData => { 55 | return ( 56 |
57 |
66 | 67 |
68 |

{rowData.email}

69 |

{rowData.date}

70 |
71 | ); 72 | }} 73 | > 74 | 75 | # 76 | 77 | 78 | 79 | 80 | First Name 81 | 82 | 83 | 84 | 85 | Last Name 86 | 87 | 88 | 89 | 90 | City 91 | 92 | 93 | 94 | 95 | Street 96 | 97 | 98 | 99 | 100 | Company 101 | 102 | 103 |
104 | ); 105 | }; 106 | 107 | ReactDOM.render(); 108 | ``` 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/examples/FillHeightTable.md: -------------------------------------------------------------------------------- 1 | ### Fill height 2 | 3 | Force the height of the table to be equal to the height of its parent container. Cannot be used together with `autoHeight`. 4 | 5 | 6 | 7 | ```js 8 | const data = mockUsers(50); 9 | 10 | const App = () => { 11 | const [height, setHeight] = React.useState(30); 12 | const [fillHeight, setFillHeight] = React.useState(false); 13 | 14 | return ( 15 |
16 | }> 17 | { 20 | setFillHeight(checked); 21 | }} 22 | > 23 | fillHeight 24 | 25 | 26 | 27 | Container height: 28 | rem 34 | 35 | 36 |
37 |
38 | { 43 | console.log(data); 44 | }} 45 | > 46 | 47 | Id 48 | 49 | 50 | 51 | 52 | First Name 53 | 54 | 55 | 56 | 57 | Last Name 58 | 59 | 60 | 61 | 62 | City 63 | 64 | 65 | 66 | 67 | Street 68 | 69 | 70 | 71 | 72 | Company 73 | 74 | 75 | 76 | 77 | Email 78 | 79 | 80 | 81 | 82 | Email 83 | 84 | 85 | 86 | 87 | Email 88 | 89 | 90 | 91 | 92 | Email 93 | 94 | 95 |
96 |
97 |
98 | ); 99 | }; 100 | ReactDOM.render(); 101 | ``` 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/examples/FixedColumnTable.md: -------------------------------------------------------------------------------- 1 | ### Fixed Column 2 | 3 | 4 | 5 | ```js 6 | const fakeData = mockUsers(20); 7 | 8 | const App = () => { 9 | // data is empty initially 10 | const [data, setData] = React.useState([]); 11 | 12 | React.useEffect(() => { 13 | // We load data after first render 14 | setData(fakeData); 15 | }, []); 16 | 17 | const handleScroll = (x, y) => { 18 | // This should print 200 under normal conditions 19 | console.log(data.length); 20 | }; 21 | 22 | return ( 23 | { 30 | console.log(data); 31 | }} 32 | > 33 | 34 | Id 35 | 36 | 37 | 38 | 39 | First Name 40 | 41 | 42 | 43 | 44 | Last Name 45 | 46 | 47 | 48 | 49 | City 50 | 51 | 52 | 53 | 54 | Street 55 | 56 | 57 | 58 | 59 | Company 60 | 61 | 62 | 63 | 64 | Email 65 | 66 | 67 | 68 | 69 | Email 70 | 71 | 72 | 73 | 74 | Action 75 | 76 | 77 | {rowData => { 78 | function handleAction() { 79 | alert(`id:${rowData.id}`); 80 | } 81 | return ( 82 | 83 | Edit | Remove 84 | 85 | ); 86 | }} 87 | 88 | 89 |
90 | ); 91 | }; 92 | ReactDOM.render(); 93 | ``` 94 | 95 | 96 | -------------------------------------------------------------------------------- /docs/examples/FluidColumnTable.md: -------------------------------------------------------------------------------- 1 | ### Fluid column 2 | 3 | 4 | 5 | ```js 6 | const data = mockUsers(20); 7 | 8 | const CustomTable = ({ flexGrow }) => { 9 | return ( 10 | <> 11 | { 15 | console.log(sortColumn, sortType); 16 | }} 17 | > 18 | 19 | Id 20 | 21 | 22 | 23 | 24 | First Name 25 | 26 | 27 | 28 | 29 | Last Name 30 | 31 | 32 | 33 | 34 | City {flexGrow ? (flexGrow={1}) : null} 35 | 36 | 37 | 38 | 39 | Company {flexGrow ? (flexGrow={2}) : null} 40 | 41 | 42 |
43 | 44 | ); 45 | }; 46 | 47 | const App = () => { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | ReactDOM.render(); 61 | ``` 62 | 63 | 64 | 65 | If you need to set a column to automatic width, you need to configure the `flexGrow` property. `flexGrow` is a `number` type. Will fill the `Table` remaining width according to the sum of all `flexGrow`. 66 | 67 | Note: After setting `flexGrow`, you cannot set the `width` and `resizable` properties. You can set a minimum width by `minwidth`. 68 | 69 | ```html 70 | 71 | City flexGrow={1} 72 | 73 | 74 | 75 | 76 | Company flexGrow={2} 77 | 78 | 79 | 80 | ... 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/examples/HideTableHeader.md: -------------------------------------------------------------------------------- 1 | ### Hidden header 2 | 3 | 4 | 5 | ```js 6 | const App = () => { 7 | return ( 8 | { 13 | console.log(data); 14 | }} 15 | > 16 | 17 | Id 18 | 19 | 20 | 21 | 22 | First Name 23 | 24 | 25 | 26 | 27 | Last Name 28 | 29 | 30 | 31 | 32 | City 33 | 34 | 35 | 36 | 37 | Street 38 | 39 | 40 | 41 | 42 | Company 43 | 44 | 45 | 46 | 47 | Email 48 | 49 | 50 | 51 | 52 | Email 53 | 54 | 55 | 56 | 57 | Email 58 | 59 | 60 | 61 | 62 | Email 63 | 64 | 65 |
66 | ); 67 | }; 68 | ReactDOM.render(); 69 | ``` 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/examples/InfiniteLoader.md: -------------------------------------------------------------------------------- 1 | ### Infinite Loader 2 | 3 | 4 | 5 | ```js 6 | const InfiniteLoader = () => ( 7 |
17 | loading ... 18 |
19 | ); 20 | 21 | const mockData = (length, start) => { 22 | const result = []; 23 | for (let i = 1; i <= length; i++) { 24 | result.push({ 25 | time: Date.now(), 26 | index: start + i 27 | }); 28 | } 29 | 30 | return new Promise(resolve => { 31 | setTimeout(() => { 32 | resolve(result); 33 | }, 1000); 34 | }); 35 | }; 36 | 37 | const tableHeight = 400; 38 | 39 | const App = () => { 40 | const [data, setData] = React.useState([]); 41 | const [showLoader, setShowLoader] = React.useState(false); 42 | const loading = React.useRef(false); 43 | 44 | React.useEffect(() => { 45 | mockData(50, 0).then(data => { 46 | setData(data); 47 | }); 48 | }, []); 49 | 50 | const handleScroll = (x, y) => { 51 | const contextHeight = data.length * 46; 52 | const top = Math.abs(y); 53 | 54 | if (!loading.current && contextHeight - top - tableHeight < 300) { 55 | loading.current = true; 56 | setShowLoader(true); 57 | 58 | mockData(50, data.length).then(data => { 59 | loading.current = false; 60 | setShowLoader(false); 61 | setData(prev => [...prev, ...data]); 62 | }); 63 | } 64 | console.log(data.length, 'onScroll'); 65 | }; 66 | 67 | return ( 68 |
69 | 76 | 77 | ID 78 | 79 | 80 | 81 | Time 82 | 83 | 84 |
85 | {showLoader && } 86 |
87 | ); 88 | }; 89 | 90 | ReactDOM.render(); 91 | ``` 92 | 93 | 94 | -------------------------------------------------------------------------------- /docs/examples/LoadingTable.md: -------------------------------------------------------------------------------- 1 | ### Loading 2 | 3 | 4 | 5 | ```js 6 | const fakeData = mockUsers(20); 7 | 8 | const App = () => { 9 | const [data, setData] = React.useState(fakeData); 10 | const [loading, setLoading] = React.useState(true); 11 | const [customLoader, setCustomLoader] = React.useState(false); 12 | const [usePlaceholder, setUsePlaceholder] = React.useState(false); 13 | const [loadAnimation, setLoadAnimation] = React.useState(false); 14 | 15 | const renderLoading = () => { 16 | if (usePlaceholder) { 17 | return ( 18 |
28 | 29 |
30 | ); 31 | } 32 | 33 | return ; 34 | }; 35 | 36 | return ( 37 |
38 | }> 39 | { 42 | setLoading(!loading); 43 | }} 44 | > 45 | Loading 46 | 47 | { 50 | setCustomLoader(!customLoader); 51 | }} 52 | > 53 | Use a custom loader 54 | 55 | { 58 | setUsePlaceholder(!usePlaceholder); 59 | }} 60 | > 61 | Use a placeholder 62 | 63 | { 66 | setLoadAnimation(!loadAnimation); 67 | }} 68 | > 69 | loadAnimation 70 | 71 | 72 |
73 | 74 | 81 | 82 | Id 83 | 84 | 85 | 86 | 87 | First Name 88 | 89 | 90 | 91 | 92 | Last Name 93 | 94 | 95 | 96 | 97 | City 98 | 99 | 100 | 101 | 102 | Street 103 | 104 | 105 | 106 | 107 | Company 108 | 109 | 110 | 111 | 112 | Email 113 | 114 | 115 | 116 | 117 | Email 118 | 119 | 120 | 121 | 122 | Email 123 | 124 | 125 | 126 | 127 | Email 128 | 129 | 130 |
131 |
132 | ); 133 | }; 134 | 135 | ReactDOM.render(); 136 | ``` 137 | 138 | 139 | -------------------------------------------------------------------------------- /docs/examples/ResizableColumnTable.md: -------------------------------------------------------------------------------- 1 | ### Resizable column 2 | 3 | 4 | 5 | ```js 6 | const data = mockUsers(20); 7 | 8 | const App = () => { 9 | const [width, setWidth] = React.useState(null); 10 | 11 | return ( 12 | 13 | 14 | Id 15 | 16 | 17 | 18 | { 23 | console.log(columnWidth, dataKey); 24 | }} 25 | > 26 | First Name 27 | 28 | 29 | 30 | 31 | Last Name 32 | 33 | 34 | 35 | setWidth(width)}> 36 | City 37 | 38 | 39 | 40 | 41 | Street 42 | 43 | 44 | 45 | 46 | Company 47 | 48 | 49 | 50 | 51 | Email 52 | 53 | 54 |
55 | ); 56 | }; 57 | 58 | ReactDOM.render(); 59 | ``` 60 | 61 | 62 | 63 | Columns will resize down to `minWidth` (optional) or `20`, whichever is higher. 64 | -------------------------------------------------------------------------------- /docs/examples/RowspanTable.md: -------------------------------------------------------------------------------- 1 | ### Rowspan Cell 2 | 3 | 4 | 5 | ```js 6 | const App = () => { 7 | return ( 8 | { 15 | console.log(data); 16 | }} 17 | > 18 | 19 | id 20 | 21 | 22 | { 26 | return rowData.cityRowSpan; 27 | }} 28 | > 29 | City 30 | 31 | 32 | 33 | { 37 | return rowData.streetRowSpan; 38 | }} 39 | > 40 | Street 41 | 42 | 43 | 44 | 45 | First Name 46 | 47 | 48 | 49 | 50 | Last Name 51 | 52 | 53 | 54 | 55 | Company 56 | 57 | 58 | 59 | Company 60 | 61 | 62 | 63 | Company 64 | 65 | 66 | 67 | id 68 | 69 | 70 |
71 | ); 72 | }; 73 | 74 | ReactDOM.render(); 75 | ``` 76 | 77 | 78 | 79 | ```js 80 | const data = [ 81 | { 82 | city: 'New Gust', 83 | name: 'Janis', 84 | rowspan: 2 85 | }, 86 | { 87 | city: 'New Gust', 88 | name: 'Ernest Schuppe Anderson' 89 | }, 90 | { 91 | city: 'Maria Junctions', 92 | name: 'Alessandra', 93 | rowspan: 3 94 | }, 95 | { 96 | city: 'Maria Junctions', 97 | name: 'Margret' 98 | }, 99 | { 100 | city: 'Maria Junctions', 101 | name: 'Emiliano' 102 | } 103 | ]; 104 | 105 | 106 | { 110 | return rowData.rowspan; 111 | }} 112 | > 113 | Name 114 | 115 | 116 | 117 | 118 | 119 | 120 |
; 121 | ``` 122 | -------------------------------------------------------------------------------- /docs/examples/SortTable.md: -------------------------------------------------------------------------------- 1 | ### Sort 2 | 3 | 4 | 5 | ```js 6 | const data = mockUsers(20); 7 | 8 | const App = () => { 9 | const [sortColumn, setSortColumn] = React.useState('id'); 10 | const [sortType, setSortType] = React.useState('asc'); 11 | const [loading, setLoading] = React.useState(false); 12 | 13 | const sortData = () => { 14 | if (sortColumn && sortType) { 15 | return data.sort((a, b) => { 16 | let x = a[sortColumn]; 17 | let y = b[sortColumn]; 18 | if (typeof x === 'string') { 19 | x = x.charCodeAt(); 20 | } 21 | if (typeof y === 'string') { 22 | y = y.charCodeAt(); 23 | } 24 | if (sortType === 'asc') { 25 | return x - y; 26 | } else { 27 | return y - x; 28 | } 29 | }); 30 | } 31 | return data; 32 | }; 33 | 34 | const handleSortColumn = (sortColumn, sortType) => { 35 | setLoading(true); 36 | 37 | setTimeout(() => { 38 | setLoading(false); 39 | setSortColumn(sortColumn); 40 | setSortType(sortType); 41 | }, 500); 42 | }; 43 | 44 | const renderSortIcon = sortType => { 45 | console.log(sortType); 46 | const iconStyle = { fontSize: 18 }; 47 | 48 | if (sortType === 'asc') { 49 | return ; 50 | } else if (sortType === 'desc') { 51 | return ; 52 | } 53 | 54 | return ; 55 | }; 56 | 57 | return ( 58 | { 69 | console.log(data); 70 | }} 71 | > 72 | 73 | Id 74 | 75 | 76 | 77 | 78 | 79 | First Name 80 |

Custom sort icon

81 |
82 | 83 |
84 | 85 | Last Name 86 | 87 | 88 | 95 | 96 | 97 | City Custom sort icon 98 | 99 | 100 | 101 | 102 | Street 103 | 104 | 105 | 106 | 107 | 108 | Company 109 | 110 | 111 | 112 | 113 | Email 114 | 115 | 116 |
117 | ); 118 | }; 119 | 120 | ReactDOM.render(); 121 | ``` 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/examples/TreeTable.md: -------------------------------------------------------------------------------- 1 | ### Tree 2 | 3 | 4 | 5 | ```js 6 | const data = mockTreeData({ 7 | limits: [2, 3, 3], 8 | labels: layer => { 9 | if (layer === 0) { 10 | return faker.vehicle.manufacturer(); 11 | } else if (layer === 1) { 12 | return faker.vehicle.fuel(); 13 | } 14 | return faker.vehicle.vehicle(); 15 | }, 16 | getRowData: () => ({ 17 | price: faker.commerce.price(10000, 1000000, 0, '$', true), 18 | rating: faker.finance.amount(2, 5) 19 | }) 20 | }); 21 | 22 | const App = () => { 23 | const [tree, setTree] = React.useState(true); 24 | return ( 25 |
26 | }> 27 | { 30 | setTree(checked); 31 | }} 32 | > 33 | isTree 34 | 35 | 36 |
37 | { 49 | console.log(isOpen, rowData); 50 | }} 51 | renderTreeToggle={(icon, rowData) => { 52 | if (rowData.children && rowData.children.length === 0) { 53 | return ; 54 | } 55 | return icon; 56 | }} 57 | onRowClick={rowData => { 58 | console.log(rowData); 59 | }} 60 | > 61 | 62 | Vehicle 🚗 63 | 64 | 65 | 66 | Rating ⭐️ 67 | 68 | {rowData => 69 | Array.from({ length: rowData.rating }).map((_, i) => ⭐️) 70 | } 71 | 72 | 73 | 74 | Price 💰 75 | 76 | 77 |
78 |
79 | ); 80 | }; 81 | 82 | ReactDOM.render(); 83 | ``` 84 | 85 | 86 | -------------------------------------------------------------------------------- /docs/examples/Virtualized.md: -------------------------------------------------------------------------------- 1 | ### Virtualized 2 | 3 | Virtualize Long Lists 4 | 5 | 6 | 7 | ```js 8 | const data = mockUsers(10000); 9 | 10 | const ImageCell = ({ rowData, dataKey, ...props }) => ( 11 | 12 |
23 | 24 |
25 |
26 | ); 27 | 28 | const LargeListsTable = () => { 29 | const tableRef = React.useRef(); 30 | return ( 31 |
32 | { 38 | console.log(data); 39 | }} 40 | > 41 | {({ Column, HeaderCell, Cell }) => ( 42 | <> 43 | 44 | Id 45 | 46 | 47 | 48 | 49 | Avartar 50 | 51 | 52 | 53 | 54 | First Name 55 | 56 | 57 | 58 | 59 | Last Name 60 | 61 | 62 | 63 | 64 | City 65 | 66 | 67 | 68 | 69 | Street 70 | 71 | 72 | 73 | 74 | Company 75 | 76 | 77 | 78 | 79 | phone 80 | 81 | 82 | 83 | 84 | amount 85 | 86 | 87 | 88 | 89 | age 90 | 91 | 92 | 93 | )} 94 |
95 | 102 |
103 | ); 104 | }; 105 | 106 | ReactDOM.render(); 107 | ``` 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/examples/WordWrapTable.md: -------------------------------------------------------------------------------- 1 | ### Word wrap 2 | 3 | 4 | 5 | ```js 6 | const fakeData = mockUsers(20); 7 | 8 | const App = () => { 9 | const [data, setData] = React.useState(fakeData); 10 | const [wordWrap, setWordWrap] = React.useState('break-all'); 11 | return ( 12 |
13 | 20 | {' | '} 21 | 28 | 46 |
47 | { 52 | console.log(data); 53 | }} 54 | > 55 | 56 | Id 57 | 58 | 59 | 60 | 61 | First Name 62 | 63 | 64 | 65 | 66 | Last Name 67 | 68 | 69 | 70 | 71 | City 72 | 73 | 74 | 75 | 76 | Street 77 | 78 | 79 | 80 | 81 | Company 82 | 83 | 84 | 85 | 86 | Email 87 | 88 | 89 |
90 |
91 | ); 92 | }; 93 | 94 | ReactDOM.render(); 95 | ``` 96 | 97 | 98 | -------------------------------------------------------------------------------- /docs/examples/fullText.md: -------------------------------------------------------------------------------- 1 | ### Show full text of cells 2 | 3 | 4 | 5 | ```js 6 | const data = mockUsers(20); 7 | 8 | const App = () => { 9 | return ( 10 |
11 | 12 | 13 | Name 14 | 15 | 16 | 17 | 18 | Avatar 19 | 20 | 21 | 22 | 23 | City 24 | 25 | 26 | 27 | 28 | Street 29 | 30 | 31 | 32 | 33 | Company 34 | 35 | 36 | 37 | 38 | Email 39 | 40 | 41 |
42 |
43 | ); 44 | }; 45 | 46 | ReactDOM.render(); 47 | ``` 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/gh-pages.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages'); 2 | var path = require('path'); 3 | 4 | ghpages.publish(path.join(__dirname, '../assets'), function (err) { 5 | console.log(err); 6 | }); 7 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Table - React Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/less/index.less: -------------------------------------------------------------------------------- 1 | @import '~rsuite/styles/index.less'; 2 | @import '~react-code-view/styles/react-code-view.css'; 3 | 4 | @import '../components/Frame/styles.less'; 5 | @import '../components/Logo/styles.less'; 6 | @import '../components/Header/styles.less'; 7 | 8 | @import '../../src/less/index.less'; 9 | 10 | .btn-toolbar { 11 | padding: 10px 0; 12 | } 13 | .icon-code:before { 14 | content: ''; 15 | } 16 | 17 | //rewrite base color 18 | @base-color: #34c3ff; 19 | 20 | body { 21 | background: #f5f8fa; 22 | } 23 | 24 | .text-muted { 25 | color: @B700; 26 | } 27 | 28 | .rs-sidebar { 29 | position: fixed; 30 | height: 100vh; 31 | z-index: 3; 32 | } 33 | 34 | .page-container { 35 | padding-left: 260px; 36 | transition: padding 0.5s; 37 | &.container-full { 38 | padding-left: 60px; 39 | } 40 | } 41 | 42 | .bg-gradient-orange { 43 | background: linear-gradient(87deg, #fb6340 0, #fbb140 100%); 44 | } 45 | 46 | .bg-gradient-red { 47 | background: linear-gradient(87deg, #f5365c 0, #f56036 100%); 48 | } 49 | 50 | .bg-gradient-green { 51 | background: linear-gradient(87deg, #2dce89 0, #2dcecc 100%); 52 | } 53 | 54 | .bg-gradient-blue { 55 | background: linear-gradient(87deg, #11cdef 0, #1171ef 100%); 56 | } 57 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import pluginReact from 'eslint-plugin-react'; 5 | import eslintConfigPrettier from 'eslint-config-prettier'; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | pluginJs.configs.recommended, 10 | ...tseslint.configs.recommended, 11 | pluginReact.configs.flat.recommended, 12 | eslintConfigPrettier, 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node 18 | } 19 | } 20 | }, 21 | { 22 | ignores: ['src/styles/plugins/*'] 23 | }, 24 | { 25 | files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'], 26 | rules: { 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-unused-expressions': 'off', 29 | '@typescript-eslint/no-namespace': 'off', 30 | 'react/prop-types': 'off' 31 | }, 32 | settings: { 33 | react: { 34 | version: 'detect' 35 | } 36 | } 37 | }, 38 | { 39 | // Test files 40 | files: ['**/test/**/*.{js,mjs,cjs,ts,jsx,tsx}'], 41 | languageOptions: { 42 | globals: { 43 | ...globals.mocha, 44 | ...globals.chai, 45 | expect: true, 46 | sinon: true 47 | } 48 | }, 49 | rules: { 50 | 'react/display-name': 'off' 51 | } 52 | } 53 | ]; 54 | -------------------------------------------------------------------------------- /examples/with-gatsby/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | package-lock.json -------------------------------------------------------------------------------- /examples/with-gatsby/.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /examples/with-gatsby/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-gatsby/LICENSE: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2020 Gatsby Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /examples/with-gatsby/gatsby-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure your Gatsby site with this file. 3 | * 4 | * See: https://www.gatsbyjs.com/docs/gatsby-config/ 5 | */ 6 | 7 | module.exports = { 8 | /* Your site config here */ 9 | plugins: [], 10 | } 11 | -------------------------------------------------------------------------------- /examples/with-gatsby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-hello-world", 3 | "private": true, 4 | "description": "A simplified bare-bones starter for Gatsby", 5 | "version": "0.1.0", 6 | "license": "0BSD", 7 | "scripts": { 8 | "build": "gatsby build", 9 | "develop": "gatsby develop", 10 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", 11 | "start": "npm run develop", 12 | "serve": "gatsby serve", 13 | "clean": "gatsby clean", 14 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 15 | }, 16 | "dependencies": { 17 | "gatsby": "^2.24.67", 18 | "react": "^16.12.0", 19 | "react-dom": "^16.12.0", 20 | "rsuite-table": "^3.14.5" 21 | }, 22 | "devDependencies": { 23 | "prettier": "2.1.2" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/gatsbyjs/gatsby-starter-hello-world" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/gatsbyjs/gatsby/issues" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/with-gatsby/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Table, Column, HeaderCell, Cell } from "rsuite-table" 3 | import "rsuite-table/dist/css/rsuite-table.css" 4 | 5 | const dataList = [ 6 | { id: 1, name: "a", email: "a@email.com", avartar: "..." }, 7 | { id: 2, name: "b", email: "b@email.com", avartar: "..." }, 8 | { id: 3, name: "c", email: "c@email.com", avartar: "..." }, 9 | ] 10 | 11 | export default function Home() { 12 | const [sortColumn, setSortColumn] = React.useState("id") 13 | const [sortType, setSortType] = React.useState("asc") 14 | return ( 15 |
16 | { 21 | setSortColumn(key) 22 | setSortType(type) 23 | console.log(key, type) 24 | }} 25 | > 26 | 27 | ID 28 | 29 | 30 | 31 | 32 | Name 33 | 34 | 35 | 36 | 37 | Email 38 | 39 | {(rowData, rowIndex) => { 40 | return {rowData.email} 41 | }} 42 | 43 | 44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /examples/with-gatsby/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsuite/rsuite-table/97a9c114032ea7f2d5ea912985cd26a252ec6fe4/examples/with-gatsby/static/favicon.ico -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const del = require('del'); 3 | const babel = require('gulp-babel'); 4 | const less = require('gulp-less'); 5 | const postcss = require('gulp-postcss'); 6 | const sourcemaps = require('gulp-sourcemaps'); 7 | const rename = require('gulp-rename'); 8 | const insert = require('gulp-insert'); 9 | const gulp = require('gulp'); 10 | const babelrc = require('./babel.config.js'); 11 | const STYLE_SOURCE_DIR = './src/less'; 12 | const STYLE_DIST_DIR = './dist/css'; 13 | const TS_SOURCE_DIR = ['./src/**/*.tsx', './src/**/*.ts', '!./src/**/*.d.ts']; 14 | const ESM_DIR = './es'; 15 | const CJS_DIR = './lib'; 16 | const DIST_DIR = './dist'; 17 | 18 | function buildLess() { 19 | return gulp 20 | .src(`${STYLE_SOURCE_DIR}/index.less`) 21 | .pipe(sourcemaps.init()) 22 | .pipe(less({ javascriptEnabled: true })) 23 | .pipe(postcss([require('autoprefixer')])) 24 | .pipe(sourcemaps.write('./')) 25 | .pipe(rename('rsuite-table.css')) 26 | .pipe(gulp.dest(`${STYLE_DIST_DIR}`)); 27 | } 28 | 29 | function buildCSS() { 30 | return gulp 31 | .src(`${STYLE_DIST_DIR}/rsuite-table.css`) 32 | .pipe(sourcemaps.init()) 33 | .pipe(postcss()) 34 | .pipe(rename({ suffix: '.min' })) 35 | .pipe(sourcemaps.write('./')) 36 | .pipe(gulp.dest(`${STYLE_DIST_DIR}`)); 37 | } 38 | 39 | function buildLib() { 40 | return ( 41 | gulp 42 | .src(TS_SOURCE_DIR) 43 | .pipe(babel(babelrc())) 44 | // adds the 'use-client' directive to /lib exported from rsuite-talbe 45 | .pipe(insert.prepend(`'use client';\n`)) 46 | .pipe(gulp.dest(CJS_DIR)) 47 | ); 48 | } 49 | 50 | function buildEsm() { 51 | return gulp 52 | .src(TS_SOURCE_DIR) 53 | .pipe( 54 | babel( 55 | babelrc(null, { 56 | NODE_ENV: 'esm' 57 | }) 58 | ) 59 | ) // adds the 'use-client' directive to /es exported from rsuite-talbe 60 | .pipe(insert.prepend(`'use client';\n`)) 61 | .pipe(gulp.dest(ESM_DIR)); 62 | } 63 | 64 | function copyTypescriptDeclarationFiles() { 65 | return gulp.src('./src/**/*.d.ts').pipe(gulp.dest(CJS_DIR)).pipe(gulp.dest(ESM_DIR)); 66 | } 67 | 68 | function copyLessFiles() { 69 | return gulp 70 | .src(['./src/**/*.less', './src/**/fonts/**/*']) 71 | .pipe(gulp.dest(CJS_DIR)) 72 | .pipe(gulp.dest(ESM_DIR)); 73 | } 74 | 75 | function copyFontFiles() { 76 | return gulp.src(`${STYLE_SOURCE_DIR}/fonts/**/*`).pipe(gulp.dest(`${STYLE_DIST_DIR}/fonts`)); 77 | } 78 | 79 | function clean(done) { 80 | del.sync([CJS_DIR, ESM_DIR, DIST_DIR], { force: true }); 81 | done(); 82 | } 83 | 84 | exports.build = gulp.series( 85 | clean, 86 | gulp.parallel(buildLib, buildEsm, gulp.series(buildLess, buildCSS)), 87 | gulp.parallel(copyTypescriptDeclarationFiles, copyLessFiles, copyFontFiles) 88 | ); 89 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | 3 | module.exports = config => { 4 | const { env } = process; 5 | const { M, F } = env; 6 | 7 | // Weird pattern syntax but works 8 | // @see https://github.com/karma-runner/karma/issues/1532#issuecomment-127128326 9 | let testFile = 'test/*Spec.+(js|ts|tsx)'; 10 | 11 | if (M) { 12 | testFile = `test/${M}Spec.+(js|ts|tsx)`; 13 | } else if (F) { 14 | testFile = F; 15 | } 16 | 17 | config.set({ 18 | basePath: '', 19 | files: [testFile], 20 | frameworks: ['mocha', 'chai-dom', 'sinon-chai', 'webpack'], 21 | colors: true, 22 | reporters: ['mocha', 'coverage'], 23 | 24 | logLevel: config.LOG_INFO, 25 | preprocessors: { 26 | 'test/**/*Spec.+(js|ts|tsx)': ['webpack'] 27 | }, 28 | webpack: require('./webpack.karma.js'), 29 | webpackMiddleware: { 30 | noInfo: true 31 | }, 32 | browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome'], 33 | customLaunchers: { 34 | ChromeCi: { 35 | base: 'Chrome', 36 | flags: ['--no-sandbox'] 37 | }, 38 | FirefoxAutoAllowGUM: { 39 | base: 'Firefox', 40 | prefs: { 41 | 'media.navigator.permission.disabled': true 42 | } 43 | } 44 | }, 45 | plugins: [ 46 | 'karma-webpack', 47 | 'karma-mocha', 48 | 'karma-mocha-reporter', 49 | 'karma-sinon-chai', 50 | 'karma-chai-dom', 51 | 'karma-coverage', 52 | 'karma-chrome-launcher', 53 | 'karma-firefox-launcher' 54 | ], 55 | coverageReporter: { 56 | dir: 'coverage', 57 | reporters: [ 58 | { type: 'html' }, 59 | { type: 'lcov', subdir: 'lcov' } // lcov 60 | ] 61 | } 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const plugins = [ 2 | require('autoprefixer'), 3 | require('cssnano')({ 4 | preset: [ 5 | 'default', { 6 | discardComments: { 7 | removeAll: true 8 | } 9 | } 10 | ] 11 | }) 12 | ]; 13 | 14 | module.exports = { 15 | plugins 16 | }; 17 | -------------------------------------------------------------------------------- /src/CellGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useClassNames, useTable } from './hooks'; 3 | import type { StandardProps } from './types'; 4 | export interface CellGroupProps extends StandardProps { 5 | fixed?: 'left' | 'right'; 6 | width?: number; 7 | height?: number; 8 | left?: number; 9 | } 10 | 11 | const CellGroup = React.forwardRef((props: CellGroupProps, ref: React.Ref) => { 12 | const { 13 | fixed, 14 | width, 15 | left, 16 | height, 17 | style, 18 | classPrefix = 'cell-group', 19 | className, 20 | children, 21 | ...rest 22 | } = props; 23 | 24 | const { setCssPosition } = useTable(); 25 | const { withClassPrefix, merge } = useClassNames(classPrefix); 26 | const classes = merge(className, withClassPrefix({ [`fixed-${fixed}`]: fixed, scroll: !fixed })); 27 | 28 | const styles = { 29 | width, 30 | height, 31 | ...style 32 | }; 33 | 34 | setCssPosition?.(styles as CSSStyleDeclaration, left, 0); 35 | 36 | return ( 37 |
38 | {children} 39 |
40 | ); 41 | }); 42 | 43 | CellGroup.displayName = 'Table.CellGroup'; 44 | 45 | export default CellGroup; 46 | -------------------------------------------------------------------------------- /src/Column.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { RowDataType } from './types'; 3 | 4 | export interface ColumnProps { 5 | /** Alignment */ 6 | align?: React.CSSProperties['justifyContent']; 7 | 8 | /** Merges column cells to merge when the dataKey value for the merged column is null or undefined. */ 9 | colSpan?: number; 10 | 11 | /** Merges rows on the specified column. */ 12 | rowSpan?: (rowData: Row) => number; 13 | 14 | /** Fixed column */ 15 | fixed?: boolean | 'left' | 'right'; 16 | 17 | /** Whether to display the full text of the cell content when the mouse is hovered */ 18 | fullText?: boolean; 19 | 20 | /** Vertical alignment */ 21 | verticalAlign?: React.CSSProperties['alignItems'] | 'top' | 'middle' | 'bottom'; 22 | 23 | /** Column width */ 24 | width?: number; 25 | 26 | /** Customizable Resize Column width */ 27 | resizable?: boolean; 28 | 29 | /** Sortable */ 30 | sortable?: boolean; 31 | 32 | /** A column of a tree */ 33 | treeCol?: boolean; 34 | 35 | /** Set the column width automatically adjusts, when set flexGrow cannot set resizable and width property */ 36 | flexGrow?: number; 37 | 38 | /** When you use flexGrow, you can set a minimum width by minwidth */ 39 | minWidth?: number; 40 | 41 | /** Configure the cells of the column */ 42 | children?: React.ReactNode; 43 | 44 | /** Callback function for resize the colum */ 45 | onResize?: (columnWidth?: number, dataKey?: string) => void; 46 | } 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 49 | function Column(_props: ColumnProps) { 50 | return <>; 51 | } 52 | 53 | Column.displayName = 'Table.Column'; 54 | 55 | Column.defaultProps = { 56 | width: 100 57 | }; 58 | 59 | export const columnHandledProps = [ 60 | 'align', 61 | 'verticalAlign', 62 | 'width', 63 | 'fixed', 64 | 'resizable', 65 | 'sortable', 66 | 'flexGrow', 67 | 'minWidth', 68 | 'colSpan', 69 | 'rowSpan', 70 | 'treeCol', 71 | 'onResize', 72 | 'children', 73 | 'fullText' 74 | ]; 75 | 76 | export default Column; 77 | -------------------------------------------------------------------------------- /src/ColumnGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { convertToFlex } from './utils'; 3 | import { useClassNames} from './hooks'; 4 | import type { StandardProps } from './types'; 5 | 6 | export interface ColumnGroupProps extends StandardProps { 7 | /** Alignment */ 8 | align?: 'left' | 'center' | 'right'; 9 | /** Vertical alignment */ 10 | verticalAlign?: 'top' | 'middle' | 'bottom'; 11 | /** Fixed column */ 12 | fixed?: boolean | 'left' | 'right'; 13 | 14 | /** 15 | * Height of the merged cell group header. 16 | * The default value is half of the table's `headerHeight`. 17 | **/ 18 | groupHeaderHeight?: number; 19 | 20 | /** Group header */ 21 | header?: React.ReactNode; 22 | /** Width */ 23 | width?: number; 24 | /** Header height */ 25 | headerHeight?: number; 26 | } 27 | 28 | const ColumnGroup = React.forwardRef((props: ColumnGroupProps, ref: React.Ref) => { 29 | const { 30 | header, 31 | className, 32 | children, 33 | classPrefix = 'column-group', 34 | headerHeight = 80, 35 | verticalAlign, 36 | align, 37 | width, 38 | groupHeaderHeight: groupHeightProp, 39 | ...rest 40 | } = props; 41 | 42 | const groupHeight = typeof groupHeightProp !== 'undefined' ? groupHeightProp : headerHeight / 2; 43 | const restHeight = 44 | typeof groupHeightProp !== 'undefined' ? headerHeight - groupHeightProp : headerHeight / 2; 45 | 46 | const styles: React.CSSProperties = { 47 | height: groupHeight, 48 | width 49 | }; 50 | 51 | const { withClassPrefix, merge, prefix } = useClassNames(classPrefix); 52 | const classes = merge(className, withClassPrefix()); 53 | const contentStyles = { 54 | ...convertToFlex({ verticalAlign, align }), 55 | ...styles 56 | }; 57 | 58 | return ( 59 |
60 |
61 |
62 | {header} 63 |
64 |
65 | 66 | {children 67 | ? React.Children.map(children as React.ReactElement[], (node: React.ReactElement) => { 68 | return React.cloneElement(node, { 69 | className: prefix('cell'), 70 | predefinedStyle: { height: restHeight, top: styles.height }, 71 | headerHeight: restHeight, 72 | verticalAlign: node.props.verticalAlign || verticalAlign, 73 | children: {node.props.children} 74 | }); 75 | }) 76 | : null} 77 |
78 | ); 79 | }); 80 | 81 | ColumnGroup.displayName = 'Table.ColumnGroup'; 82 | 83 | export default ColumnGroup; 84 | -------------------------------------------------------------------------------- /src/EmptyMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { TableLocaleType } from './types'; 3 | 4 | interface EmptyMessageProps extends React.HTMLAttributes { 5 | locale?: TableLocaleType; 6 | loading?: boolean; 7 | addPrefix: (...classes: any) => string; 8 | renderEmpty?: (info: React.ReactNode) => any; 9 | } 10 | 11 | const EmptyMessage = React.forwardRef( 12 | (props: EmptyMessageProps, ref: React.Ref) => { 13 | const { addPrefix, locale, renderEmpty, loading } = props; 14 | 15 | if (loading) { 16 | return null; 17 | } 18 | 19 | const emptyMessage = ( 20 |
21 | {locale?.emptyMessage} 22 |
23 | ); 24 | 25 | return renderEmpty ? renderEmpty(emptyMessage) : emptyMessage; 26 | } 27 | ); 28 | EmptyMessage.displayName = 'Table.EmptyMessage'; 29 | 30 | export default EmptyMessage; 31 | -------------------------------------------------------------------------------- /src/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { TableLocaleType } from './types'; 3 | 4 | interface LoaderProps extends React.HTMLAttributes { 5 | locale?: TableLocaleType; 6 | loadAnimation?: boolean; 7 | loading?: boolean; 8 | addPrefix: (...classes: any) => string; 9 | renderLoading?: (loading: React.ReactNode) => any; 10 | } 11 | 12 | const Loader = React.forwardRef((props: LoaderProps, ref: React.Ref) => { 13 | const { loadAnimation, loading, locale, addPrefix, renderLoading } = props; 14 | 15 | const loadingElement = ( 16 |
17 |
18 | 19 | {locale?.loading} 20 |
21 |
22 | ); 23 | 24 | // Custom render a loader 25 | if (typeof renderLoading === 'function') { 26 | return loading ? renderLoading(loadingElement) : null; 27 | } 28 | 29 | // If loadAnimation is true , it returns the DOM element, 30 | // and controls whether the loader is displayed through CSS to achieve animation effect. 31 | return loading || loadAnimation ? loadingElement : null; 32 | }); 33 | 34 | Loader.displayName = 'Table.Loader'; 35 | 36 | export default Loader; 37 | -------------------------------------------------------------------------------- /src/MouseArea.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface MouseAreaProps extends React.HTMLAttributes { 4 | addPrefix: (...classes: any) => string; 5 | height: number; 6 | headerHeight: number; 7 | } 8 | 9 | const MouseArea = React.forwardRef((props: MouseAreaProps, ref: React.Ref) => { 10 | const { addPrefix, headerHeight, height } = props; 11 | const styles = { height }; 12 | 13 | const spanStyles = { height: headerHeight - 1 }; 14 | return ( 15 |
16 | 17 |
18 | ); 19 | }); 20 | 21 | MouseArea.displayName = 'Table.MouseArea'; 22 | 23 | export default MouseArea; 24 | -------------------------------------------------------------------------------- /src/Row.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mergeRefs } from './utils'; 3 | import { useClassNames, useTable } from './hooks'; 4 | import { ROW_HEADER_HEIGHT, ROW_HEIGHT } from './constants'; 5 | import type { StandardProps } from './types'; 6 | 7 | export interface RowProps extends StandardProps { 8 | width?: number; 9 | height?: number; 10 | headerHeight?: number; 11 | top?: number; 12 | isHeaderRow?: boolean; 13 | rowRef?: any; 14 | rowSpan?: number; 15 | } 16 | 17 | const Row = React.forwardRef((props: RowProps, ref: React.Ref) => { 18 | const { 19 | classPrefix = 'row', 20 | height = ROW_HEIGHT, 21 | headerHeight = ROW_HEADER_HEIGHT, 22 | className, 23 | width, 24 | top, 25 | style, 26 | isHeaderRow, 27 | rowRef, 28 | children, 29 | rowSpan, 30 | ...rest 31 | } = props; 32 | 33 | const { setCssPosition } = useTable(); 34 | const { withClassPrefix, merge } = useClassNames(classPrefix); 35 | const classes = merge(className, withClassPrefix({ header: isHeaderRow, rowspan: rowSpan })); 36 | 37 | const styles = { 38 | minWidth: width, 39 | height: isHeaderRow ? headerHeight : height, 40 | ...style 41 | }; 42 | 43 | setCssPosition?.(styles as CSSStyleDeclaration, 0, top); 44 | 45 | return ( 46 |
47 | {children} 48 |
49 | ); 50 | }); 51 | 52 | Row.displayName = 'Table.Row'; 53 | 54 | export default Row; 55 | -------------------------------------------------------------------------------- /src/TableProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { setCssPosition, isRTL } from './utils'; 3 | 4 | /** 5 | * Callback function type for translating DOM position. 6 | * @param style - The CSSStyleDeclaration object to modify. 7 | * @param x - The x-coordinate (optional). 8 | * @param y - The y-coordinate (optional). 9 | */ 10 | type TranslateDOMPositionXYCallback = (style: CSSStyleDeclaration, x?: number, y?: number) => void; 11 | 12 | export interface TableContextProps { 13 | /** Indicates if the table is in RTL mode. */ 14 | rtl: boolean; 15 | 16 | /** Indicates if there's a custom tree column. */ 17 | hasCustomTreeCol?: boolean; 18 | 19 | /** Indicates if the table is in tree mode. */ 20 | isTree?: boolean; 21 | 22 | /** Function to translate DOM position. */ 23 | setCssPosition: TranslateDOMPositionXYCallback; 24 | 25 | /** Prefix for CSS classes. */ 26 | classPrefix?: string; 27 | } 28 | 29 | export const TableContext = React.createContext({} as TableContextProps); 30 | 31 | export const TableProvider = (props: React.PropsWithChildren>) => { 32 | const { children, rtl = isRTL(), hasCustomTreeCol = false, isTree, classPrefix } = props; 33 | const value = useMemo( 34 | () => ({ 35 | setCssPosition, 36 | rtl: rtl ?? isRTL(), 37 | hasCustomTreeCol, 38 | isTree, 39 | classPrefix 40 | }), 41 | [rtl, hasCustomTreeCol, isTree, classPrefix] 42 | ); 43 | 44 | return {children}; 45 | }; 46 | 47 | export default TableProvider; 48 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const LAYER_WIDTH = 30; 2 | export const SCROLLBAR_MIN_WIDTH = 14; 3 | export const SCROLLBAR_WIDTH = 10; 4 | export const CELL_PADDING_HEIGHT = 26; 5 | export const RESIZE_MIN_WIDTH = 20; 6 | export const SORT_TYPE = { DESC: 'desc', ASC: 'asc' }; 7 | export const ROW_HEIGHT = 46; 8 | export const ROW_HEADER_HEIGHT = 40; 9 | 10 | // transition-duration (ms) 11 | export const TRANSITION_DURATION = 1000; 12 | // transition-timing-function (ease-out) 13 | export const BEZIER = 'cubic-bezier(0, 0, .58, 1)'; 14 | 15 | // An attribute value added to the data row to identify whether it is expanded, used in Tree. 16 | export const EXPANDED_KEY: string = Symbol('expanded') as any; 17 | 18 | // An attribute value added for the data row, identifying the key of the parent node, used in Tree. 19 | export const PARENT_KEY: string = Symbol('parent') as any; 20 | 21 | // The attribute value added for the data row, which identifies the depth of the node (the number of parent nodes), 22 | // and is used in the Tree. 23 | export const TREE_DEPTH: string = Symbol('treeDepth') as any; 24 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // Hooks 2 | export { default as useUpdateEffect } from './useUpdateEffect'; 3 | export { default as useUpdateLayoutEffect } from './useUpdateLayoutEffect'; 4 | export { default as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 5 | export { default as useMount } from './useMount'; 6 | export { default as useClassNames } from './useClassNames'; 7 | export { default as useControlled } from './useControlled'; 8 | export { default as useCellDescriptor } from './useCellDescriptor'; 9 | export { default as useTableDimension } from './useTableDimension'; 10 | export { default as useTableRows } from './useTableRows'; 11 | export { default as useAffix } from './useAffix'; 12 | export { default as useScrollListener } from './useScrollListener'; 13 | export { default as usePosition } from './usePosition'; 14 | export { default as useTableData } from './useTableData'; 15 | export { default as useTable } from './useTable'; -------------------------------------------------------------------------------- /src/hooks/useAffix.ts: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback, useEffect } from 'react'; 2 | import getHeight from 'dom-lib/getHeight'; 3 | import addStyle from 'dom-lib/addStyle'; 4 | import removeStyle from 'dom-lib/removeStyle'; 5 | import on from 'dom-lib/on'; 6 | import toggleClass from '../utils/toggleClass'; 7 | import isNumberOrTrue from '../utils/isNumberOrTrue'; 8 | import useUpdateEffect from './useUpdateEffect'; 9 | import type { ListenerCallback, ElementOffset } from '../types'; 10 | import type { ScrollbarInstance } from '../Scrollbar'; 11 | 12 | interface AffixProps { 13 | getTableHeight: () => number; 14 | contentHeight: React.MutableRefObject; 15 | affixHeader?: boolean | number; 16 | affixHorizontalScrollbar?: boolean | number; 17 | tableOffset: React.RefObject; 18 | headerOffset: React.RefObject; 19 | headerHeight: number; 20 | scrollbarXRef: React.RefObject; 21 | affixHeaderWrapperRef: React.RefObject; 22 | } 23 | 24 | const useAffix = (props: AffixProps) => { 25 | const { 26 | getTableHeight, 27 | contentHeight, 28 | affixHorizontalScrollbar, 29 | affixHeader, 30 | tableOffset, 31 | headerOffset, 32 | headerHeight, 33 | scrollbarXRef, 34 | affixHeaderWrapperRef 35 | } = props; 36 | 37 | const scrollListener = useRef(); 38 | const handleAffixHorizontalScrollbar = useCallback(() => { 39 | const scrollY = window.scrollY || window.pageYOffset; 40 | const windowHeight = getHeight(window); 41 | const height = getTableHeight(); 42 | 43 | const bottom = typeof affixHorizontalScrollbar === 'number' ? affixHorizontalScrollbar : 0; 44 | const offsetTop = tableOffset.current?.top || 0; 45 | 46 | const fixedScrollbar = 47 | scrollY + windowHeight < height + (offsetTop + bottom) && 48 | scrollY + windowHeight - headerHeight > offsetTop + bottom; 49 | 50 | if (scrollbarXRef?.current?.root) { 51 | toggleClass(scrollbarXRef.current.root, 'fixed', fixedScrollbar); 52 | 53 | if (fixedScrollbar) { 54 | addStyle(scrollbarXRef.current.root, 'bottom', `${bottom}px`); 55 | } else { 56 | removeStyle(scrollbarXRef.current.root, 'bottom'); 57 | } 58 | } 59 | }, [affixHorizontalScrollbar, headerHeight, scrollbarXRef, getTableHeight, tableOffset]); 60 | 61 | const handleAffixTableHeader = useCallback(() => { 62 | const top = typeof affixHeader === 'number' ? affixHeader : 0; 63 | const scrollY = window.scrollY || window.pageYOffset; 64 | const offsetTop = headerOffset.current?.top || 0; 65 | const fixedHeader = 66 | scrollY - (offsetTop - top) >= 0 && scrollY < offsetTop - top + contentHeight.current; 67 | 68 | if (affixHeaderWrapperRef.current) { 69 | toggleClass(affixHeaderWrapperRef.current, 'fixed', fixedHeader); 70 | } 71 | }, [affixHeader, affixHeaderWrapperRef, contentHeight, headerOffset]); 72 | 73 | const handleWindowScroll = useCallback(() => { 74 | if (isNumberOrTrue(affixHeader)) { 75 | handleAffixTableHeader(); 76 | } 77 | if (isNumberOrTrue(affixHorizontalScrollbar)) { 78 | handleAffixHorizontalScrollbar(); 79 | } 80 | }, [ 81 | affixHeader, 82 | affixHorizontalScrollbar, 83 | handleAffixTableHeader, 84 | handleAffixHorizontalScrollbar 85 | ]); 86 | 87 | /** 88 | * Update the position of the fixed element after the height of the table changes. 89 | * fix: https://github.com/rsuite/rsuite/issues/1716 90 | */ 91 | useUpdateEffect(handleWindowScroll, [getTableHeight]); 92 | 93 | useEffect(() => { 94 | if (isNumberOrTrue(affixHeader) || isNumberOrTrue(affixHorizontalScrollbar)) { 95 | scrollListener.current = on(window, 'scroll', handleWindowScroll); 96 | } 97 | return () => { 98 | scrollListener.current?.off(); 99 | }; 100 | }, [affixHeader, affixHorizontalScrollbar, handleWindowScroll]); 101 | }; 102 | 103 | export default useAffix; 104 | -------------------------------------------------------------------------------- /src/hooks/useClassNames.ts: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import useTable from './useTable'; 3 | import { useCallback } from 'react'; 4 | import { prefix as addPrefix } from '../utils/prefix'; 5 | 6 | export type ClassValue = 7 | | string 8 | | number 9 | | ClassDictionary 10 | | ClassArray 11 | | undefined 12 | | null 13 | | boolean; 14 | 15 | // This is the only way I found to break circular references between ClassArray and ClassValue 16 | // https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 17 | 18 | export type ClassArray = ClassValue[]; 19 | 20 | interface ClassNameUtils { 21 | withClassPrefix: (...classes: ClassValue[]) => string; 22 | merge: (...classes: ClassValue[]) => string; 23 | prefix: (...classes: ClassValue[]) => string; 24 | rootPrefix: (...classes: ClassValue[]) => string; 25 | } 26 | 27 | export interface ClassDictionary { 28 | [id: string]: any; 29 | } 30 | /** 31 | * Add a prefix to all classNames. 32 | * 33 | * @param str prefix of className 34 | * @returns { withClassPrefix, merge, prefix } 35 | * - withClassPrefix: A function of combining className and adding a prefix to each className. 36 | * At the same time, the default `classPrefix` is the first className. 37 | * - merge: A merge className function. 38 | * - prefix: Add a prefix to className 39 | * - rootPrefix 40 | */ 41 | function useClassNames(str: string, controlled?: boolean): ClassNameUtils { 42 | const { classPrefix: contextClassPrefix = 'rs' } = useTable() || {}; 43 | const componentName = controlled ? str : addPrefix(contextClassPrefix, str); 44 | 45 | /** 46 | * @example 47 | * 48 | * if str = 'button': 49 | * prefix('red', { active: true }) => 'rs-button-red rs-button-active' 50 | */ 51 | const prefix = useCallback( 52 | (...classes: ClassValue[]) => { 53 | const mergeClasses = classes.length 54 | ? classNames(...classes) 55 | .split(' ') 56 | .map(item => addPrefix(componentName, item)) 57 | : []; 58 | 59 | return mergeClasses.filter(cls => cls).join(' '); 60 | }, 61 | [componentName] 62 | ); 63 | 64 | /** 65 | * @example 66 | * 67 | * if str = 'button': 68 | * withClassPrefix('red', { active: true }) => 'rs-button rs-button-red rs-button-active' 69 | */ 70 | const withClassPrefix = useCallback( 71 | (...classes: ClassValue[]) => { 72 | const mergeClasses = prefix(classes); 73 | return mergeClasses ? `${componentName} ${mergeClasses}` : componentName; 74 | }, 75 | [componentName, prefix] 76 | ); 77 | 78 | /** 79 | * @example 80 | * rootPrefix('btn') => 'rs-btn' 81 | * rootPrefix('btn', { active: true }) => 'rs-btn rs-active' 82 | */ 83 | const rootPrefix = (...classes: ClassValue[]) => { 84 | const mergeClasses = classes.length 85 | ? classNames(...classes) 86 | .split(' ') 87 | .map(item => addPrefix(contextClassPrefix, item)) 88 | : []; 89 | 90 | return mergeClasses.filter(cls => cls).join(' '); 91 | }; 92 | 93 | return { 94 | withClassPrefix, 95 | merge: classNames, 96 | prefix, 97 | rootPrefix 98 | }; 99 | } 100 | 101 | export default useClassNames; 102 | -------------------------------------------------------------------------------- /src/hooks/useControlled.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useCallback } from 'react'; 2 | 3 | /** 4 | * A hook for controlled value management. 5 | * In the case of passing the controlled value, the controlled value is returned, otherwise the value in state is returned. 6 | * Generally used for a component including controlled and uncontrolled modes. 7 | * @param controlledValue 8 | * @param defaultValue 9 | * @param formatValue 10 | */ 11 | function useControlled( 12 | controlledValue?: T, 13 | defaultValue?: T 14 | ): [T, (value: React.SetStateAction) => void, boolean] { 15 | const controlledRef = useRef(false); 16 | controlledRef.current = controlledValue !== undefined; 17 | 18 | const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue); 19 | 20 | // If it is controlled, this directly returns the attribute value. 21 | const value: any = controlledRef.current ? controlledValue : uncontrolledValue; 22 | 23 | const setValue = useCallback( 24 | nextValue => { 25 | // Only update the value in state when it is not under control. 26 | if (!controlledRef.current) { 27 | setUncontrolledValue(nextValue); 28 | } 29 | }, 30 | [controlledRef] 31 | ); 32 | 33 | return [value, setValue, controlledRef.current]; 34 | } 35 | 36 | export default useControlled; 37 | -------------------------------------------------------------------------------- /src/hooks/useIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, RefObject } from 'react'; 2 | 3 | /** 4 | * useIntersectionObserver Hook 5 | * 6 | * @param ref - Ref object of the element to be observed 7 | */ 8 | const useIntersectionObserver = (ref?: RefObject): boolean => { 9 | const [isVisible, setIsVisible] = useState(false); 10 | 11 | useEffect(() => { 12 | // Check if the browser supports IntersectionObserver 13 | if (!('IntersectionObserver' in window)) { 14 | // If not supported, optionally set to visible or handle fallback logic 15 | setIsVisible(true); // Fallback: Set to visible 16 | return; 17 | } 18 | 19 | // Create an IntersectionObserver instance 20 | const observer = new IntersectionObserver(entries => { 21 | entries.forEach(entry => { 22 | setIsVisible(entry.isIntersecting); 23 | }); 24 | }); 25 | 26 | const element = ref?.current; 27 | 28 | // Start observing the target element 29 | if (element) { 30 | observer.observe(element); 31 | } 32 | 33 | // Cleanup function to unobserve the element when the component unmounts or dependencies change 34 | return () => { 35 | if (element) { 36 | observer.unobserve(element); 37 | } 38 | }; 39 | }, [ref]); 40 | 41 | return isVisible; 42 | }; 43 | 44 | export default useIntersectionObserver; 45 | -------------------------------------------------------------------------------- /src/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | import canUseDOM from 'dom-lib/canUseDOM'; 3 | 4 | const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect; 5 | 6 | export default useIsomorphicLayoutEffect; 7 | -------------------------------------------------------------------------------- /src/hooks/useMount.ts: -------------------------------------------------------------------------------- 1 | import { EffectCallback } from 'react'; 2 | import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; 3 | 4 | const useMount = (effect: EffectCallback) => { 5 | useIsomorphicLayoutEffect(effect, []); 6 | }; 7 | 8 | export default useMount; 9 | -------------------------------------------------------------------------------- /src/hooks/useTable.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { TableContext } from '../TableProvider'; 3 | import setCss from '../utils/setCssPosition'; 4 | import isRTL from '../utils/isRTL'; 5 | 6 | export const useTable = () => { 7 | const { 8 | setCssPosition = setCss, 9 | rtl = isRTL(), 10 | hasCustomTreeCol, 11 | isTree, 12 | classPrefix 13 | } = useContext(TableContext); 14 | 15 | return { 16 | setCssPosition, 17 | rtl, 18 | hasCustomTreeCol, 19 | isTree, 20 | classPrefix 21 | }; 22 | }; 23 | 24 | export default useTable; 25 | -------------------------------------------------------------------------------- /src/hooks/useTableData.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import useUpdateEffect from './useUpdateEffect'; 3 | import flattenData from '../utils/flattenData'; 4 | import findAllParents from '../utils/findAllParents'; 5 | import shouldShowRowByExpanded from '../utils/shouldShowRowByExpanded'; 6 | import { EXPANDED_KEY, TREE_DEPTH } from '../constants'; 7 | import type { RowKeyType, RowDataType } from '../types'; 8 | /** 9 | * Filter those expanded nodes. 10 | * @param data 11 | * @param expandedRowKeys 12 | * @param rowKey 13 | * @returns 14 | */ 15 | const filterTreeData = ( 16 | data: readonly Row[], 17 | expandedRowKeys: readonly Key[], 18 | rowKey?: RowKeyType 19 | ) => { 20 | return flattenData(data).filter(rowData => { 21 | if (rowKey) { 22 | const parents = findAllParents(rowData, rowKey); 23 | const expanded = shouldShowRowByExpanded(expandedRowKeys, parents); 24 | 25 | // FIXME This function is supposed to be pure. 26 | // Don't mutate rowData in-place! 27 | (rowData as Record)[EXPANDED_KEY] = expanded; 28 | (rowData as Record)[TREE_DEPTH] = parents.length; 29 | 30 | return expanded; 31 | } 32 | }); 33 | }; 34 | 35 | interface UseTableDataProps { 36 | data: readonly Row[]; 37 | isTree?: boolean; 38 | expandedRowKeys: readonly Key[]; 39 | rowKey?: RowKeyType; 40 | } 41 | 42 | function useTableData( 43 | props: UseTableDataProps 44 | ) { 45 | const { data, isTree, expandedRowKeys, rowKey } = props; 46 | const [tableData, setData] = useState(() => { 47 | return isTree ? filterTreeData(data, expandedRowKeys, rowKey) : data; 48 | }); 49 | 50 | useUpdateEffect(() => { 51 | setData(isTree ? filterTreeData(data, expandedRowKeys, rowKey) : data); 52 | }, [data, expandedRowKeys, rowKey, isTree]); 53 | 54 | return tableData; 55 | } 56 | 57 | export default useTableData; 58 | -------------------------------------------------------------------------------- /src/hooks/useTableRows.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useRef } from 'react'; 2 | import getHeight from 'dom-lib/getHeight'; 3 | import useUpdateLayoutEffect from './useUpdateLayoutEffect'; 4 | import useMount from './useMount'; 5 | import isEmpty from 'lodash/isEmpty'; 6 | import defer from '../utils/defer'; 7 | import type { RowDataType } from '../types'; 8 | 9 | interface TableRowsProps { 10 | prefix: (str: string) => string; 11 | wordWrap?: boolean | 'break-all' | 'break-word' | 'keep-all'; 12 | data: readonly Row[]; 13 | expandedRowKeys: readonly Key[]; 14 | } 15 | 16 | /** 17 | * The row information of the table, get the DOM of all rows, and summarize the row height. 18 | * @param props 19 | * @returns 20 | */ 21 | const useTableRows = (props: TableRowsProps) => { 22 | const { prefix, wordWrap, data, expandedRowKeys } = props; 23 | const [tableRowsMaxHeight, setTableRowsMaxHeight] = useState([]); 24 | const tableRows = useRef<{ [key: string]: [HTMLElement, any] }>({}); 25 | 26 | const bindTableRowsRef = (index: number | string, rowData: any) => (ref: HTMLElement) => { 27 | if (ref) { 28 | tableRows.current[index] = [ref, rowData]; 29 | } 30 | }; 31 | 32 | const calculateRowMaxHeight = useCallback(() => { 33 | if (wordWrap) { 34 | const nextTableRowsMaxHeight: number[] = []; 35 | const curTableRows = Object.values(tableRows.current); 36 | 37 | for (let i = 0; i < curTableRows.length; i++) { 38 | const [row] = curTableRows[i]; 39 | if (row) { 40 | const cells = row.querySelectorAll(`.${prefix('cell-wrap')}`) || []; 41 | const cellArray = Array.from(cells); 42 | let maxHeight = 0; 43 | 44 | for (let j = 0; j < cellArray.length; j++) { 45 | const cell = cellArray[j]; 46 | const h = getHeight(cell); 47 | maxHeight = Math.max(maxHeight, h); 48 | } 49 | 50 | nextTableRowsMaxHeight.push(maxHeight); 51 | } 52 | } 53 | 54 | // Can't perform a React state update on an unmounted component 55 | if (!isEmpty(tableRows.current)) { 56 | setTableRowsMaxHeight(nextTableRowsMaxHeight); 57 | } 58 | } 59 | }, [prefix, wordWrap]); 60 | 61 | useMount(() => { 62 | defer(calculateRowMaxHeight); 63 | }); 64 | 65 | useUpdateLayoutEffect(() => { 66 | /** 67 | * After the data is updated, the height of the cell DOM needs to be re-acquired, 68 | * and what is often obtained is not the latest DOM that has been rendered. 69 | * So use `defer` to delay obtaining the height of the cell DOM. 70 | * TODO: To be improved 71 | */ 72 | defer(calculateRowMaxHeight); 73 | }, [data, expandedRowKeys]); 74 | 75 | return { 76 | bindTableRowsRef, 77 | tableRowsMaxHeight, 78 | tableRows 79 | }; 80 | }; 81 | 82 | export default useTableRows; 83 | -------------------------------------------------------------------------------- /src/hooks/useUpdateEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const useUpdateEffect: typeof useEffect = (effect, deps) => { 4 | const isMounting = useRef(true); 5 | 6 | useEffect(() => { 7 | if (isMounting.current) { 8 | isMounting.current = false; 9 | return; 10 | } 11 | effect(); 12 | }, deps); 13 | }; 14 | 15 | export default useUpdateEffect; 16 | -------------------------------------------------------------------------------- /src/hooks/useUpdateLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'; 3 | 4 | const useUpdateLayoutEffect: typeof useEffect = (effect, deps) => { 5 | const isMounting = useRef(true); 6 | 7 | useIsomorphicLayoutEffect(() => { 8 | if (isMounting.current) { 9 | isMounting.current = false; 10 | return; 11 | } 12 | effect(); 13 | }, deps); 14 | }; 15 | 16 | export default useUpdateLayoutEffect; 17 | -------------------------------------------------------------------------------- /src/icons/ArrowRight.tsx: -------------------------------------------------------------------------------- 1 | import React, { Ref, forwardRef } from 'react'; 2 | 3 | export const ArrowRight = forwardRef(function ArrowRight( 4 | props: React.SVGProps, 5 | ref: Ref 6 | ) { 7 | return ( 8 | 17 | 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/icons/Sort.tsx: -------------------------------------------------------------------------------- 1 | import React, { Ref, forwardRef } from 'react'; 2 | 3 | export const Sort = forwardRef(function Sort( 4 | props: React.SVGProps, 5 | ref: Ref 6 | ) { 7 | return ( 8 | 17 | 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/icons/SortDown.tsx: -------------------------------------------------------------------------------- 1 | import React, { Ref, forwardRef } from 'react'; 2 | 3 | export const SortDown = forwardRef(function SortDown( 4 | props: React.SVGProps, 5 | ref: Ref 6 | ) { 7 | return ( 8 | 17 | 18 | 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Table } from './Table'; 2 | export { default as Column } from './Column'; 3 | export { default as Cell } from './Cell'; 4 | export { default as HeaderCell } from './HeaderCell'; 5 | export { default as ColumnGroup } from './ColumnGroup'; 6 | 7 | export type { TableProps, TableInstance } from './Table'; 8 | export type { ColumnProps } from './Column'; 9 | export type { CellProps } from './Cell'; 10 | export type { HeaderCellProps } from './HeaderCell'; 11 | export type { ColumnGroupProps } from './ColumnGroup'; 12 | export * from './types'; 13 | -------------------------------------------------------------------------------- /src/less/column-group.less: -------------------------------------------------------------------------------- 1 | &-table { 2 | &-column-group { 3 | position: absolute; 4 | left: 0; 5 | right: 0; 6 | top: 0; 7 | width: 100%; 8 | &-header { 9 | border-bottom: 1px solid #eee; 10 | position: absolute; 11 | width: 100%; 12 | &-content { 13 | display: table-cell; 14 | padding: 8px; 15 | } 16 | } 17 | &-cell { 18 | position: absolute; 19 | border-right: 1px solid #eee; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/less/functions.less: -------------------------------------------------------------------------------- 1 | 2 | .box-shadow(@x:0px, @y:2px, @blur:3px, @color:rgba(0, 0, 0, 0.28)) { 3 | -moz-box-shadow: @arguments; 4 | -webkit-box-shadow: @arguments; 5 | box-shadow: @arguments; 6 | } 7 | .ellipsis-basic() { 8 | overflow: hidden; 9 | white-space: nowrap; 10 | text-overflow: ellipsis; 11 | -ms-text-overflow: ellipsis; 12 | -o-text-overflow: ellipsis; 13 | } 14 | .ellipsis(@substract:0) { 15 | .ellipsis-basic(); 16 | width: 100% - @substract; 17 | } 18 | 19 | .user-select(){ 20 | -webkit-user-select:none; 21 | -moz-user-select:none; 22 | -ms-user-select:none; 23 | user-select:none; 24 | } 25 | -------------------------------------------------------------------------------- /src/less/index.less: -------------------------------------------------------------------------------- 1 | @import './themes.less'; 2 | 3 | .rs { 4 | @import './variables.less'; 5 | @import './functions.less'; 6 | @import './loader.less'; 7 | @import './table.less'; 8 | @import './treetable.less'; 9 | @import './scrollbar.less'; 10 | @import './column-group.less'; 11 | } 12 | -------------------------------------------------------------------------------- /src/less/loader.less: -------------------------------------------------------------------------------- 1 | .set-side-length(@side) { 2 | width: @side; 3 | height: @side; 4 | 5 | &::before, 6 | &::after { 7 | width: @side; 8 | height: @side; 9 | } 10 | } 11 | 12 | 13 | .loader-spin() { 14 | &::before, 15 | &::after { 16 | content: ""; 17 | position: absolute; 18 | left: 0; 19 | right: 0; 20 | display: block; 21 | border-radius: 50%; 22 | } 23 | 24 | &::before { 25 | border: @loader-spin-ring-wide solid @loader-spin-ring-color; 26 | } 27 | 28 | &::after { 29 | border-width: @loader-spin-ring-wide; 30 | border-style: solid; 31 | border-color: @loader-spin-ring-active-color transparent transparent; 32 | animation: loaderSpin @loader-duration-normal infinite linear; 33 | } 34 | } 35 | 36 | //** Loader 37 | //======================== 38 | @keyframes loaderSpin { 39 | from { 40 | transform: rotate(0); 41 | } 42 | 43 | to { 44 | transform: rotate(360deg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/less/scrollbar.less: -------------------------------------------------------------------------------- 1 | &-table { 2 | &-scrollbar { 3 | background: rgba(45, 45, 45, 0.05); 4 | position: absolute; 5 | &-active { 6 | background: rgba(45, 45, 45, 0.1); 7 | } 8 | &-hide { 9 | display: none; 10 | } 11 | &-handle { 12 | position: absolute; 13 | background: rgba(45, 45, 45, 0.5); 14 | border-radius: 4px; 15 | } 16 | &-horizontal { 17 | width: 100%; 18 | height: 10px; 19 | bottom: 2px; 20 | 21 | &.fixed { 22 | position: fixed; 23 | } 24 | } 25 | 26 | &-horizontal &-handle { 27 | height: 8px; 28 | left: 0px; 29 | top: 1px; 30 | } 31 | 32 | &-horizontal &-pressed, 33 | &-horizontal:hover { 34 | height: 14px; 35 | box-shadow: 1px 1px 2px #ddd inset; 36 | } 37 | 38 | &-horizontal &-pressed &-handle, 39 | &-horizontal:hover &-handle { 40 | top: 2px; 41 | height: 10px; 42 | } 43 | 44 | &-vertical { 45 | top: 0; 46 | right: 0px; 47 | width: 10px; 48 | bottom: 2px; 49 | } 50 | 51 | &-vertical &-handle { 52 | min-height: 20px; 53 | width: 8px; 54 | top: 0px; 55 | left: 1px; 56 | } 57 | 58 | &-vertical &-pressed, 59 | &-vertical:hover { 60 | width: 14px; 61 | box-shadow: 1px 1px 2px #ddd inset; 62 | } 63 | 64 | &-vertical &-pressed &-handle, 65 | &-vertical:hover &-handle { 66 | left: 2px; 67 | width: 10px; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/less/themes.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsuite/rsuite-table/97a9c114032ea7f2d5ea912985cd26a252ec6fe4/src/less/themes.less -------------------------------------------------------------------------------- /src/less/treetable.less: -------------------------------------------------------------------------------- 1 | //treeTable 2 | 3 | &-table-cell-expand-icon { 4 | cursor: pointer; 5 | outline: none; 6 | transition: transform 0.3s ease; 7 | 8 | &:where([data-expanded='true']) { 9 | transform: rotate(90deg); 10 | } 11 | } 12 | 13 | &-table-cell-expand-wrapper { 14 | margin-right: 10px; 15 | display: inline-block; 16 | cursor: pointer; 17 | } 18 | -------------------------------------------------------------------------------- /src/less/variables.less: -------------------------------------------------------------------------------- 1 | 2 | @H500: #34c3ff; 3 | @B050: #f7f7fa; 4 | @B500: #a6a6a6; 5 | 6 | @prefix: rsuite-; 7 | @border-color: #eee; 8 | @head-background: #fff; 9 | @body-background: #fff; 10 | @pagination-background: #f5f5f5; 11 | @resize-mouse-color: @H500; 12 | @row-hover-color: #f2faff; 13 | 14 | // Loader 15 | 16 | @loader-spin-ring-wide: 3px; 17 | @loader-spin-ring-color: fade(@B050, 80); 18 | @loader-spin-ring-active-color: @B500; 19 | @loader-duration-normal: 0.6s; 20 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base interface for standard React component props in RSuite Table 3 | * @extends React.HTMLAttributes 4 | */ 5 | export interface StandardProps extends React.HTMLAttributes { 6 | /** CSS class prefix for component styling customization */ 7 | classPrefix?: string; 8 | } 9 | 10 | /** 11 | * Sort direction type for table columns 12 | * @type {'desc' | 'asc'} - 'desc' for descending, 'asc' for ascending 13 | */ 14 | export type SortType = 'desc' | 'asc'; 15 | 16 | /** 17 | * Event names for table size changes 18 | */ 19 | export type TableSizeChangeEventName = 20 | | 'bodyHeightChanged' 21 | | 'bodyWidthChanged' 22 | | 'widthChanged' 23 | | 'heightChanged'; 24 | 25 | /** 26 | * Interface for row data structure in the table 27 | * @template T - Type of the children array elements 28 | */ 29 | export interface RowDataType { 30 | /** Unique key to identify the data */ 31 | dataKey?: string; 32 | /** Nested data for hierarchical structures */ 33 | children?: T[]; 34 | /** Additional dynamic properties */ 35 | [key: string]: any; 36 | } 37 | 38 | /** Type for row key identifiers */ 39 | export type RowKeyType = string | number; 40 | 41 | /** 42 | * Interface for table localization strings 43 | */ 44 | export interface TableLocaleType { 45 | /** Message to display when table has no data */ 46 | emptyMessage?: string; 47 | /** Text to show during loading states */ 48 | loading?: string; 49 | } 50 | 51 | /** 52 | * Type for event listener cleanup function 53 | */ 54 | export type ListenerCallback = { 55 | /** Function to remove the event listener */ 56 | off: () => void; 57 | }; 58 | 59 | /** 60 | * Interface for element positioning and dimensions 61 | */ 62 | export type ElementOffset = { 63 | /** Distance from the top of the viewport in pixels */ 64 | top: number; 65 | /** Distance from the left of the viewport in pixels */ 66 | left: number; 67 | /** Element width in pixels */ 68 | width: number; 69 | /** Element height in pixels */ 70 | height: number; 71 | }; 72 | -------------------------------------------------------------------------------- /src/utils/children.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { isFragment } from './react-is'; 3 | 4 | export const flattenChildren = ( 5 | children: React.ReactNode | React.ReactNode[], 6 | flattened: React.ReactNode[] = [] 7 | ) => { 8 | for (const child of React.Children.toArray(children)) { 9 | if (isFragment(child)) { 10 | const childEl = child as React.ReactElement; 11 | if (childEl.props?.children) { 12 | flattenChildren(childEl.props.children, flattened); 13 | } 14 | } else { 15 | flattened.push(child); 16 | } 17 | } 18 | return flattened; 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/convertToFlex.ts: -------------------------------------------------------------------------------- 1 | export const verticalAlignMap = { 2 | top: 'flex-start', 3 | middle: 'center', 4 | bottom: 'flex-end' 5 | }; 6 | 7 | export const alignMap = { 8 | left: 'flex-start', 9 | center: 'center', 10 | right: 'flex-end' 11 | }; 12 | 13 | // Convert verticalAlign to alignItems. 14 | export function verticalAlignToAlignItems(verticalAlign) { 15 | return verticalAlignMap[verticalAlign] || verticalAlign; 16 | } 17 | 18 | // Convert align to justifyContent. 19 | export function alignToJustifyContent(align) { 20 | return alignMap[align] || align; 21 | } 22 | 23 | // Convert verticalAlign and align to flex styles. 24 | export default function convertToFlex(props: { 25 | verticalAlign?: string; 26 | align?: string; 27 | }): React.CSSProperties { 28 | const { verticalAlign, align } = props; 29 | 30 | if (!verticalAlign && !align) return {}; 31 | 32 | return { 33 | display: 'flex', 34 | flexWrap: 'wrap', 35 | alignItems: verticalAlignToAlignItems(verticalAlign), 36 | justifyContent: alignToJustifyContent(align) 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/defer.ts: -------------------------------------------------------------------------------- 1 | import defer from 'lodash/defer'; 2 | 3 | /** 4 | * Defer callbacks to wait for DOM rendering to complete. 5 | */ 6 | export default (callback: () => void) => { 7 | defer(callback, 'deferred'); 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/findAllParents.ts: -------------------------------------------------------------------------------- 1 | import { PARENT_KEY } from '../constants'; 2 | import type { RowDataType, RowKeyType } from '../types'; 3 | 4 | /** 5 | * Get all parent nodes of the given node in the flattened data 6 | * @param node target node 7 | */ 8 | function findAllParents(node: Row, rowKey: RowKeyType): Key[] { 9 | const parents: Key[] = []; 10 | let current = node[PARENT_KEY]; 11 | 12 | // Iterate up through the parent chain and add each parent to the result array 13 | while (current) { 14 | parents.push(current[rowKey]); 15 | current = current[PARENT_KEY]; 16 | } 17 | return parents; 18 | } 19 | 20 | export default findAllParents; 21 | -------------------------------------------------------------------------------- /src/utils/findRowKeys.ts: -------------------------------------------------------------------------------- 1 | import type { RowDataType, RowKeyType } from '../types'; 2 | 3 | export default function findRowKeys( 4 | rows: readonly Row[], 5 | rowKey?: RowKeyType, 6 | expanded?: boolean 7 | ): Key[] { 8 | let keys: Key[] = []; 9 | 10 | if (!rowKey) { 11 | return keys; 12 | } 13 | 14 | for (let i = 0; i < rows.length; i++) { 15 | const item = rows[i]; 16 | if (item.children) { 17 | keys.push(item[rowKey]); 18 | keys = [...keys, ...findRowKeys(item.children as Row[], rowKey)]; 19 | } else if (expanded) { 20 | keys.push(item[rowKey]); 21 | } 22 | } 23 | return keys; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/flattenData.ts: -------------------------------------------------------------------------------- 1 | import { PARENT_KEY } from '../constants'; 2 | import type { RowDataType } from '../types'; 3 | 4 | /** 5 | * Flatten the tree data with parent association recorded on each node 6 | * @param tree tree data 7 | */ 8 | function flattenData>(tree: readonly Row[], parent?: Row): Row[] { 9 | return tree.reduce((acc, node) => { 10 | // Create a new flattened node with parent association 11 | const flattened = { 12 | ...node, 13 | [PARENT_KEY]: parent 14 | }; 15 | 16 | // Add the flattened node and its flattened children (if any) to the result array 17 | acc.push(flattened, ...(node.children ? flattenData(node.children, flattened) : [])); 18 | return acc; 19 | }, []); 20 | } 21 | 22 | export default flattenData; 23 | -------------------------------------------------------------------------------- /src/utils/flushSync.ts: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | 3 | const majorVersion = parseInt(ReactDOM.version); 4 | 5 | /** 6 | * Force React to flush any updates inside the provided callback synchronously. 7 | * This ensures that the DOM is updated immediately. 8 | */ 9 | const flushSync = callback => { 10 | if (majorVersion >= 18) { 11 | ReactDOM.flushSync?.(callback); 12 | return; 13 | } 14 | callback(); 15 | }; 16 | 17 | export default flushSync; 18 | -------------------------------------------------------------------------------- /src/utils/getColumnProps.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { RowDataType } from '../types'; 3 | import type { ColumnProps } from '../Column'; 4 | 5 | /** 6 | * Get the union of the props of the column itself and the props of the custom column 7 | * 8 | * e.g. 9 | * const CustomColumn = React.forwardRef((props, ref) => { 10 | * return ; 11 | * }); 12 | * 13 | * 14 | * Header 15 | * Cell 16 | * 17 | * 18 | */ 19 | export default function getColumnProps( 20 | column: React.ReactElement 21 | ): ColumnProps { 22 | const columnDefaultProps = column['type']?.['render']?.()?.props || {}; 23 | 24 | return { ...columnDefaultProps, ...column?.props }; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/getTableColumns.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import flatten from 'lodash/flatten'; 3 | import ColumnGroup from '../ColumnGroup'; 4 | import { isFragment } from './react-is'; 5 | 6 | /** 7 | * Get the columns ReactElement array. 8 | * - Handling the case where there is an array of in children. 9 | * - Filter empty items in children. 10 | */ 11 | function getTableColumns(children) { 12 | const childrenArray = Array.isArray(children) ? children : [children]; 13 | 14 | const flattenColumns = flatten(childrenArray).map((column: React.ReactElement) => { 15 | // If the column is a group, we need to get the columns from the children. 16 | if (column && column.type === ColumnGroup) { 17 | const { 18 | header, 19 | children: groupChildren, 20 | align, 21 | fixed, 22 | verticalAlign, 23 | groupHeaderHeight 24 | } = column.props || {}; 25 | 26 | const childColumns = getTableColumns(groupChildren); 27 | 28 | return childColumns.map((childColumn, index) => { 29 | // Overwrite the props set by ColumnGroup to Column. 30 | const groupCellProps: any = { 31 | ...(childColumn && childColumn.props), 32 | groupHeaderHeight, 33 | fixed, 34 | 35 | // Column extends the properties of Group (align,verticalAlign) 36 | align: (childColumn && childColumn.props && childColumn.props.align) || align, 37 | verticalAlign: 38 | (childColumn && childColumn.props && childColumn.props.verticalAlign) || verticalAlign 39 | }; 40 | 41 | /** 42 | * Set attributes for the first column in the group: 43 | * @field groupCount: The number of grouping sub-items. 44 | * @field groupHeader: Group header title. 45 | * @field resizable: Set to not resizable. 46 | */ 47 | 48 | if (index === 0) { 49 | groupCellProps.groupAlign = align; 50 | groupCellProps.groupVerticalAlign = verticalAlign; 51 | groupCellProps.groupCount = childColumns.length; 52 | groupCellProps.groupHeader = header; 53 | groupCellProps.resizable = false; 54 | } 55 | 56 | return React.cloneElement(childColumn, groupCellProps); 57 | }); 58 | } else if (isFragment(column)) { 59 | // If the column is a fragment, we need to get the columns from the children. 60 | return getTableColumns(column.props && column.props.children); 61 | } else if (Array.isArray(column)) { 62 | // If the column is an array, need check item in the array. 63 | return getTableColumns(column); 64 | } 65 | 66 | // If the column is not a group, we just return the column. 67 | return column; 68 | }); 69 | 70 | // Flatten the array in Columns into a one-dimensional array, and calculate lastColumn and firstColumn. 71 | return flatten(flattenColumns).filter(Boolean); 72 | } 73 | 74 | export default getTableColumns; 75 | -------------------------------------------------------------------------------- /src/utils/getTotalByColumns.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isPlainObject from 'lodash/isPlainObject'; 3 | import getColumnProps from './getColumnProps'; 4 | import type { RowDataType } from '../types'; 5 | import type { ColumnProps } from '../Column'; 6 | 7 | function getTotalByColumns( 8 | columns: React.ReactElement> | React.ReactElement>[] 9 | ) { 10 | let totalFlexGrow = 0; 11 | let totalWidth = 0; 12 | 13 | const count = (items: React.ReactNode[]) => { 14 | Array.from(items).forEach(column => { 15 | if (React.isValidElement(column)) { 16 | const { flexGrow, width = 0 } = getColumnProps(column); 17 | totalFlexGrow += flexGrow || 0; 18 | totalWidth += flexGrow ? 0 : width; 19 | } else if (Array.isArray(column)) { 20 | count(column); 21 | } 22 | }); 23 | }; 24 | 25 | if (Array.isArray(columns)) { 26 | count(columns); 27 | } else if (isPlainObject(columns)) { 28 | const { flexGrow, width = 0 } = (columns && columns.props) || {}; 29 | 30 | totalFlexGrow = flexGrow || 0; 31 | totalWidth = flexGrow ? 0 : width; 32 | } 33 | 34 | return { 35 | totalFlexGrow, 36 | totalWidth 37 | }; 38 | } 39 | 40 | export default getTotalByColumns; 41 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as prefix } from './prefix'; 2 | export { default as getTotalByColumns } from './getTotalByColumns'; 3 | export { default as mergeCells } from './mergeCells'; 4 | export { default as toggleClass } from './toggleClass'; 5 | export { default as toggle } from './toggle'; 6 | export { default as flattenData } from './flattenData'; 7 | export { default as setCssPosition } from './setCssPosition'; 8 | export { default as isRTL } from './isRTL'; 9 | export { default as findRowKeys } from './findRowKeys'; 10 | export { default as findAllParents } from './findAllParents'; 11 | export { default as shouldShowRowByExpanded } from './shouldShowRowByExpanded'; 12 | export { default as resetLeftForCells } from './resetLeftForCells'; 13 | export { default as isNumberOrTrue } from './isNumberOrTrue'; 14 | export { default as mergeRefs } from './mergeRefs'; 15 | export { cancelAnimationTimeout, requestAnimationTimeout } from './requestAnimationTimeout'; 16 | export { default as isSupportTouchEvent } from './isSupportTouchEvent'; 17 | export { default as convertToFlex } from './convertToFlex'; 18 | export { default as defer } from './defer'; 19 | export { default as getTableColumns } from './getTableColumns'; 20 | -------------------------------------------------------------------------------- /src/utils/isNumberOrTrue.ts: -------------------------------------------------------------------------------- 1 | export default function isNumberOrTrue(value: number | boolean | undefined): boolean { 2 | if (typeof value === 'undefined') { 3 | return false; 4 | } 5 | 6 | return !!value || value === 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/isRTL.ts: -------------------------------------------------------------------------------- 1 | export default function isRTL() { 2 | return ( 3 | typeof window !== 'undefined' && (document.body.getAttribute('dir') || document.dir) === 'rtl' 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/isSupportTouchEvent.ts: -------------------------------------------------------------------------------- 1 | import canUseDOM from 'dom-lib/canUseDOM'; 2 | 3 | export default function isSupportTouchEvent() { 4 | return canUseDOM && 'ontouchstart' in window; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/mergeCells.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isFunction from 'lodash/isFunction'; 3 | import get from 'lodash/get'; 4 | import isNil from 'lodash/isNil'; 5 | import ColumnGroup from '../ColumnGroup'; 6 | import HeaderCell from '../HeaderCell'; 7 | 8 | function cloneCell(Cell, props) { 9 | return React.cloneElement(Cell, props); 10 | } 11 | 12 | function mergeCells(cells) { 13 | const nextCells: React.ReactNode[] = []; 14 | 15 | for (let i = 0; i < cells.length; i += 1) { 16 | const { 17 | width, 18 | colSpan, 19 | groupCount, 20 | groupHeader, 21 | groupAlign, 22 | groupVerticalAlign, 23 | isHeaderCell, 24 | headerHeight, 25 | groupHeaderHeight 26 | } = cells[i].props; 27 | 28 | const groupChildren: React.ReactNode[] = []; 29 | 30 | // Add grouping to column headers. 31 | if (groupCount && isHeaderCell) { 32 | let nextWidth = width; 33 | let left = 0; 34 | for (let j = 0; j < groupCount; j += 1) { 35 | const nextCell = cells[i + j]; 36 | const { 37 | width: nextCellWidth, 38 | sortable, 39 | children, 40 | dataKey, 41 | onSortColumn, 42 | sortColumn, 43 | sortType, 44 | align, 45 | verticalAlign, 46 | renderSortIcon 47 | } = nextCell.props; 48 | 49 | if (j !== 0) { 50 | nextWidth += nextCellWidth; 51 | left += cells[i + j - 1].props.width; 52 | cells[i + j] = cloneCell(nextCell, { removed: true }); 53 | } 54 | 55 | groupChildren.push( 56 | 69 | {children} 70 | 71 | ); 72 | } 73 | nextCells.push( 74 | cloneCell(cells[i], { 75 | width: nextWidth, 76 | children: ( 77 | 85 | {groupChildren} 86 | 87 | ) 88 | }) 89 | ); 90 | continue; 91 | } else if (colSpan) { 92 | // If there is a colSpan attribute, go to its next Cell. 93 | // Determine whether the value is null or undefined, then merge this cell. 94 | 95 | let nextWidth = width; 96 | for (let j = 0; j < colSpan; j += 1) { 97 | const nextCell = cells[i + j]; 98 | if (nextCell) { 99 | const { 100 | rowData, 101 | rowIndex, 102 | children, 103 | width: colSpanWidth, 104 | isHeaderCell, 105 | dataKey 106 | } = nextCell.props; 107 | 108 | const cellText = isFunction(children) 109 | ? children(rowData, rowIndex) 110 | : get(rowData, dataKey); 111 | 112 | if ((rowData && isNil(cellText)) || (isHeaderCell && isNil(children))) { 113 | nextWidth += colSpanWidth; 114 | cells[i + j] = cloneCell(nextCell, { 115 | removed: true 116 | }); 117 | } 118 | } 119 | } 120 | 121 | nextCells.push( 122 | cloneCell(cells[i], { 123 | width: nextWidth, 124 | 'aria-colspan': nextWidth > width ? colSpan : undefined 125 | }) 126 | ); 127 | continue; 128 | } 129 | nextCells.push(cells[i]); 130 | } 131 | return nextCells; 132 | } 133 | 134 | export default mergeCells; 135 | -------------------------------------------------------------------------------- /src/utils/mergeRefs.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type CallbackRef = (ref: T | null) => void; 4 | type Ref = React.MutableRefObject | CallbackRef; 5 | 6 | const toFnRef = (ref?: Ref | null) => 7 | !ref || typeof ref === 'function' 8 | ? ref 9 | : (value: T | null) => { 10 | ref.current = value; 11 | }; 12 | 13 | export default function mergeRefs( 14 | refA?: Ref | null, 15 | refB?: Ref | null 16 | ): React.RefCallback { 17 | const a = toFnRef(refA); 18 | const b = toFnRef(refB); 19 | return (value: T | null) => { 20 | if (typeof a === 'function') a(value); 21 | if (typeof b === 'function') b(value); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/prefix.ts: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import curry from 'lodash/curry'; 3 | 4 | export function prefix(pre: string, className: string | string[]): string { 5 | if (!pre || !className) { 6 | return ''; 7 | } 8 | 9 | if (Array.isArray(className)) { 10 | return classNames(className.filter(name => !!name).map(name => `${pre}-${name}`)); 11 | } 12 | 13 | // TODO Compatible with V4 14 | if (pre[pre.length - 1] === '-') { 15 | return `${pre}${className}`; 16 | } 17 | 18 | return `${pre}-${className}`; 19 | } 20 | 21 | export default curry(prefix); 22 | -------------------------------------------------------------------------------- /src/utils/react-is.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function typeOf(object: any) { 4 | if (typeof object === 'object' && object !== null) { 5 | return object.type || object.$$typeof; 6 | } 7 | } 8 | 9 | export function isFragment(children: React.ReactNode) { 10 | return React.Children.count(children) === 1 && typeOf(children) === Symbol.for('react.fragment'); 11 | } 12 | 13 | export function isElement(children: React.ReactNode) { 14 | return React.isValidElement(children); 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/requestAnimationTimeout.ts: -------------------------------------------------------------------------------- 1 | import requestAnimationFramePolyfill from 'dom-lib/requestAnimationFramePolyfill'; 2 | import cancelAnimationFramePolyfill from 'dom-lib/cancelAnimationFramePolyfill'; 3 | 4 | export const cancelAnimationTimeout = (frame: KeyframeAnimationOptions) => 5 | cancelAnimationFramePolyfill(frame.id as any); 6 | 7 | /** 8 | * Recursively calls requestAnimationFrame until a specified delay has been met or exceeded. 9 | * When the delay time has been reached the function you're timing out will be called. 10 | * 11 | * Credit: Joe Lambert (https://gist.github.com/joelambert/1002116#file-requesttimeout-js) 12 | */ 13 | export const requestAnimationTimeout = ( 14 | callback: () => void, 15 | delay: number 16 | ): KeyframeAnimationOptions => { 17 | let start; 18 | // wait for end of processing current event handler, because event handler may be long 19 | Promise.resolve().then(() => { 20 | start = Date.now(); 21 | }); 22 | 23 | let frame: KeyframeAnimationOptions = {}; 24 | 25 | const timeout = () => { 26 | if (Date.now() - start >= delay) { 27 | callback.call(null); 28 | } else { 29 | frame.id = requestAnimationFramePolyfill(timeout) as unknown as string; 30 | } 31 | }; 32 | 33 | frame = { 34 | id: requestAnimationFramePolyfill(timeout) as unknown as string 35 | }; 36 | 37 | return frame; 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/resetLeftForCells.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Resets the relative left distance of all cells in the array. 5 | * @param cells 6 | * @param extraWidth The additional width added to the last cell when there is a vertical scroll bar. 7 | */ 8 | export default function resetLeftForCells(cells, extraWidth?: number) { 9 | let left = 0; 10 | const nextCells: React.ReactNode[] = []; 11 | 12 | for (let i = 0; i < cells.length; i++) { 13 | const cell = cells[i]; 14 | const nextCell = React.cloneElement(cell, { 15 | left, 16 | width: i === cells.length - 1 && extraWidth ? cell.props.width + extraWidth : cell.props.width 17 | }); 18 | left += cell.props.width; 19 | nextCells.push(nextCell); 20 | } 21 | 22 | return nextCells; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/setCssPosition.ts: -------------------------------------------------------------------------------- 1 | import { getTranslateDOMPositionXY } from 'dom-lib/translateDOMPositionXY'; 2 | 3 | const setCssPosition = getTranslateDOMPositionXY({ enable3DTransform: true }); 4 | 5 | export default setCssPosition; 6 | -------------------------------------------------------------------------------- /src/utils/shouldShowRowByExpanded.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether a row(tree) node should be expanded. 3 | * @param expandedRowKeys An array of all expanded nodes. 4 | * @param parentKeys All parent nodes of the current node 5 | * @returns boolean 6 | */ 7 | export default function shouldShowRowByExpanded( 8 | expandedRowKeys: readonly Key[] = [], 9 | parentKeys: readonly Key[] = [] 10 | ): boolean { 11 | for (let i = 0; i < parentKeys?.length; i++) { 12 | if (expandedRowKeys?.indexOf(parentKeys[i]) === -1) { 13 | return false; 14 | } 15 | } 16 | 17 | return true; 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/toggle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Selectively calls either method a or b based on the condition. 3 | * @param a - Function to be called when the condition is true 4 | * @param b - Function to be called when the condition is false 5 | * @returns A function that takes a target element and additional values, 6 | * which in turn returns a function that takes a condition 7 | */ 8 | function toggle(a: (...args: any[]) => void, b: (...args: any[]) => void) { 9 | return (target: HTMLElement, ...value: any[]) => { 10 | const options = [target, ...value]; 11 | return (condition: boolean) => { 12 | if (condition) { 13 | a(...options); 14 | } else { 15 | b(...options); 16 | } 17 | }; 18 | }; 19 | } 20 | 21 | export default toggle; 22 | -------------------------------------------------------------------------------- /src/utils/toggleClass.ts: -------------------------------------------------------------------------------- 1 | import addClass from 'dom-lib/addClass'; 2 | import removeClass from 'dom-lib/removeClass'; 3 | 4 | const toggleClass = (node: HTMLElement, className: string, condition: boolean) => { 5 | if (condition) { 6 | addClass(node, className); 7 | } else { 8 | removeClass(node, className); 9 | } 10 | }; 11 | 12 | export default (node: HTMLElement | HTMLElement[], className: string, condition: boolean) => { 13 | if (!node) { 14 | return; 15 | } 16 | 17 | if ( 18 | Array.isArray(node) || 19 | Object.prototype.hasOwnProperty.call(Object.getPrototypeOf(node), 'length') 20 | ) { 21 | node = node as HTMLElement[]; 22 | Array.from(node).forEach(item => { 23 | toggleClass(item, className, condition); 24 | }); 25 | return; 26 | } 27 | toggleClass(node, className, condition); 28 | }; 29 | -------------------------------------------------------------------------------- /test/CellGroupSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CellGroup from '../src/CellGroup'; 3 | import { render, screen } from '@testing-library/react'; 4 | import { testStandardProps } from './utils'; 5 | 6 | describe('CellGroup', () => { 7 | testStandardProps(); 8 | 9 | it('Should render a cell group with default properties', () => { 10 | render(CellGroup); 11 | 12 | expect(screen.getByText('CellGroup')).to.have.class('rs-cell-group'); 13 | expect(screen.getByText('CellGroup').style.transform).to.equal('translate3d(0px, 0px, 0px)'); 14 | }); 15 | 16 | it('Should apply specified width to the cell group', () => { 17 | render(CellGroup); 18 | expect(screen.getByText('CellGroup')).to.have.style('width', '100px'); 19 | }); 20 | 21 | it('Should apply specified height to the cell group', () => { 22 | render(CellGroup); 23 | 24 | expect(screen.getByText('CellGroup')).to.have.style('height', '100px'); 25 | }); 26 | 27 | it('Should apply specified left position to the cell group', () => { 28 | render(CellGroup); 29 | 30 | expect(screen.getByText('CellGroup').style.transform).to.equal('translate3d(100px, 0px, 0px)'); 31 | }); 32 | 33 | it('Should apply fixed positioning to the cell group', () => { 34 | render(CellGroup); 35 | 36 | expect(screen.getByText('CellGroup')).to.have.class('rs-cell-group-fixed-left'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/ColumnGroupSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ColumnGroup from '../src/ColumnGroup'; 3 | import { render, screen } from '@testing-library/react'; 4 | import { testStandardProps } from './utils'; 5 | 6 | const Item = ({ className, style, children, headerHeight }: any) => ( 7 |
8 | {children} 9 |
10 | ); 11 | 12 | describe('ColumnGroup', () => { 13 | testStandardProps(); 14 | it('Should output a ColumnGroup', () => { 15 | render(); 16 | 17 | expect(screen.getByTestId('group')).to.have.class('rs-column-group'); 18 | }); 19 | 20 | it('Should output a header', () => { 21 | render(); 22 | 23 | expect(screen.getByTestId('group')).to.have.text('header'); 24 | }); 25 | 26 | it('Should render 2 cells', () => { 27 | render( 28 | 29 | a 30 | b 31 | 32 | ); 33 | 34 | expect(screen.getAllByRole('gridcell')).to.have.length(2); 35 | }); 36 | 37 | it('Should set height 10 for header', () => { 38 | render( 39 | 40 | a 41 | b 42 | 43 | ); 44 | 45 | expect(screen.getByText('header')).to.have.style('height', '10px'); 46 | screen.getAllByRole('gridcell').forEach(cell => { 47 | expect(cell).to.have.style('height', '10px'); 48 | }); 49 | }); 50 | 51 | it('Should render height via groupHeaderHeight', () => { 52 | render( 53 | 54 | a 55 | b 56 | 57 | ); 58 | 59 | expect(screen.getByText('header')).to.have.style('height', '5px'); 60 | screen.getAllByRole('gridcell').forEach(cell => { 61 | expect(cell).to.have.style('height', '15px'); 62 | }); 63 | }); 64 | 65 | it('Should be centered vertically', () => { 66 | render( 67 | 68 | a 69 | b 70 | 71 | ); 72 | 73 | expect(screen.getByText('header')).to.have.style('align-items', 'center'); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/ColumnResizeHandlerSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ColumnResizeHandler from '../src/ColumnResizeHandler'; 3 | import { render, screen, fireEvent } from '@testing-library/react'; 4 | import { testStandardProps } from './utils'; 5 | 6 | const handlerLeft = -2; 7 | 8 | describe('ColumnResizeHandler', () => { 9 | testStandardProps(); 10 | 11 | it('Should output a handler', () => { 12 | render(); 13 | expect(screen.getByRole('button')).to.have.class('rs-column-resize-spanner'); 14 | }); 15 | 16 | it('Should be 100 the `height` ', () => { 17 | render(); 18 | 19 | expect(screen.getByRole('button')).to.style('height', '100px'); 20 | }); 21 | 22 | it('Should have a `left` style', () => { 23 | const columnWidth = 100; 24 | const columnLeft = 100; 25 | render( 26 | 27 | ); 28 | 29 | expect(screen.getByRole('button')).to.style('left', `${columnWidth + columnLeft + handlerLeft}px`); 30 | }); 31 | 32 | it('Should call `onColumnResizeStart` callback ', done => { 33 | const doneOp = () => { 34 | done(); 35 | }; 36 | 37 | render(); 38 | 39 | fireEvent.mouseDown(screen.getByRole('button')); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/ColumnSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Column from '../src/Column'; 3 | import { render } from '@testing-library/react'; 4 | 5 | describe('Column', () => { 6 | it('Should output a null', () => { 7 | const { container } = render(); 8 | 9 | expect(container.innerHTML).to.be.equal(''); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/ExpandableTableSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '../src/Table'; 3 | import Column from '../src/Column'; 4 | import Cell from '../src/Cell'; 5 | import HeaderCell from '../src/HeaderCell'; 6 | import { render, screen } from '@testing-library/react'; 7 | 8 | describe('ExpandableTable', () => { 9 | it('Should be to avoid nested classPrefix', () => { 10 | const data = [{ id: 1, name: 'foobar' }]; 11 | const innerTable = React.createRef(); 12 | 13 | render( 14 | { 19 | return ( 20 |
21 | 22 | inner name 23 | 24 | 25 |
26 | ); 27 | }} 28 | > 29 | 30 | Name 31 | 32 | 33 | 34 | ); 35 | 36 | expect(innerTable.current.root).to.have.class('rs-table'); 37 | expect(innerTable.current.root).to.have.class('rs-table-hover'); 38 | }); 39 | 40 | it('Should set the height of rows and cells separately', () => { 41 | const data = [{ id: 1, name: 'foobar' }]; 42 | 43 | render( 44 | { 51 | return
content
; 52 | }} 53 | > 54 | 55 | Name 56 | 57 | 58 |
59 | ); 60 | 61 | expect(screen.getByRole('row')).to.have.style('height', '246px'); 62 | expect(screen.getByRole('gridcell', { name: 'foobar' })).to.have.style('height', '46px'); 63 | expect(screen.getByRole('gridcell', { name: 'foobar' }).childNodes[0]).to.have.style( 64 | 'height', 65 | '46px' 66 | ); 67 | }); 68 | 69 | it('Should the exapanable areas has different heights ', () => { 70 | const data = [ 71 | { id: 1, name: 'foobar' }, 72 | { id: 2, name: 'foobar2' }, 73 | { id: 3, name: 'foobar3' } 74 | ]; 75 | 76 | const { container } = render( 77 | { 83 | return rowData.id * 100; 84 | }} 85 | renderRowExpanded={() => { 86 | return
content
; 87 | }} 88 | > 89 | 90 | Name 91 | 92 | 93 |
94 | ); 95 | const rowExpandedHeights: number[] = []; 96 | container 97 | .querySelectorAll('.rs-table-row-expanded') 98 | .forEach((el: Element) => rowExpandedHeights.push((el as HTMLElement).offsetHeight)); 99 | 100 | expect(rowExpandedHeights).to.deep.equal([100, 200, 300]); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/HeaderCellSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HeaderCell from '../src/HeaderCell'; 3 | import { render, screen, fireEvent } from '@testing-library/react'; 4 | 5 | describe('HeaderCell', () => { 6 | it('Should output a header', () => { 7 | render(test); 8 | expect(screen.getByRole('columnheader').parentNode).to.have.class('rs-cell-header'); 9 | }); 10 | 11 | it('Should output default sort icon', () => { 12 | render( 13 | 14 | test 15 | 16 | ); 17 | expect(screen.getByRole('columnheader').querySelector('.rs-cell-header-icon-sort')).to.be.not 18 | .null; 19 | }); 20 | 21 | it('Should output default sort desc icon', () => { 22 | render( 23 | 24 | test 25 | 26 | ); 27 | 28 | expect( 29 | screen.getByRole('columnheader').querySelector('.rs-cell-header-icon-sort') 30 | ).to.have.attribute('data-sort', 'desc'); 31 | }); 32 | 33 | it('Should call `onSortColumn` callback', done => { 34 | const doneOp = dataKey => { 35 | if (dataKey === 'name') { 36 | done(); 37 | } 38 | }; 39 | 40 | render( 41 | 42 | test 43 | 44 | ); 45 | 46 | fireEvent.click(screen.getByRole('columnheader')); 47 | }); 48 | 49 | it('Should call `onColumnResizeStart` callback', done => { 50 | const doneOp = () => { 51 | done(); 52 | }; 53 | 54 | const { container } = render( 55 | 56 | test 57 | 58 | ); 59 | 60 | fireEvent.mouseDown(container.querySelector('.rs-column-resize-spanner') as Element); 61 | }); 62 | 63 | it('Should render custom sort icons', () => { 64 | const renderSortIcon = sortType => { 65 | if (sortType === 'asc') { 66 | return 1; 67 | } else if (sortType === 'desc') { 68 | return 2; 69 | } 70 | return 3; 71 | }; 72 | 73 | const { container, rerender } = render( 74 | 75 | test 76 | 77 | ); 78 | 79 | expect(container.querySelector('.rs-cell-header-sort-wrapper')).to.have.text('3'); 80 | 81 | rerender( 82 | 83 | test 84 | 85 | ); 86 | expect(container.querySelector('.rs-cell-header-sort-wrapper')).to.have.text('3'); 87 | 88 | rerender( 89 | 96 | test 97 | 98 | ); 99 | expect(container.querySelector('.rs-cell-header-sort-wrapper')).to.have.text('1'); 100 | 101 | rerender( 102 | 109 | test 110 | 111 | ); 112 | 113 | expect(container.querySelector('.rs-cell-header-sort-wrapper')).to.have.text('2'); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/RowSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Row from '../src/Row'; 3 | import { render, screen } from '@testing-library/react'; 4 | import { testStandardProps } from './utils'; 5 | 6 | describe('Row', () => { 7 | testStandardProps(); 8 | 9 | it('Should output a row', () => { 10 | render(Row); 11 | 12 | expect(screen.getByRole('row')).to.have.class('rs-row'); 13 | expect(screen.getByRole('row')).to.style('height', '46px'); 14 | expect(screen.getByRole('row')).to.text('Row'); 15 | }); 16 | 17 | it('Should have a min width', () => { 18 | render(Title); 19 | 20 | expect(screen.getByRole('row')).to.style('min-width', '100px'); 21 | }); 22 | 23 | it('Should have a height', () => { 24 | render(); 25 | 26 | expect(screen.getByRole('row')).to.style('height', '100px'); 27 | }); 28 | 29 | it('Should have a height when set isHeaderRow', () => { 30 | render(); 31 | 32 | expect(screen.getByRole('row')).to.style('height', '100px'); 33 | }); 34 | 35 | it('Should have a top', () => { 36 | render(); 37 | 38 | expect(screen.getByRole('row').style.transform).to.equal('translate3d(0px, 100px, 0px)'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/ScrollbarSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Scrollbar from '../src/Scrollbar'; 3 | import sinon from 'sinon'; 4 | import { fireEvent, render, screen, act } from '@testing-library/react'; 5 | import { testStandardProps } from './utils'; 6 | import type { ScrollbarInstance } from '../src/Scrollbar'; 7 | 8 | describe('Scrollbar', () => { 9 | testStandardProps(); 10 | 11 | it('Should render a scrollbar', async () => { 12 | await act(async () => { 13 | render(); 14 | }); 15 | 16 | expect(screen.getByRole('scrollbar')).to.have.class('rs-scrollbar'); 17 | }); 18 | 19 | it('Should render a vertical scrollbar', async () => { 20 | await act(async () => { 21 | render(); 22 | }); 23 | 24 | expect(screen.getByRole('scrollbar')).to.have.class('rs-scrollbar-vertical'); 25 | }); 26 | 27 | it('Should render a scroll handle with correct width', async () => { 28 | await act(async () => { 29 | render(); 30 | }); 31 | 32 | expect(screen.getByRole('button').style.width).to.equal('10%'); 33 | }); 34 | 35 | it('Should trigger onMouseDown callback', async () => { 36 | const onMouseDown = sinon.spy(); 37 | 38 | await act(async () => { 39 | render(); 40 | }); 41 | 42 | fireEvent.mouseDown(screen.getByRole('button')); 43 | 44 | expect(onMouseDown).to.have.been.calledOnce; 45 | }); 46 | 47 | it('Should apply custom styles', async () => { 48 | await act(async () => { 49 | render(); 50 | }); 51 | 52 | expect(screen.getByRole('scrollbar')).to.have.style('font-size', '12px'); 53 | }); 54 | 55 | it('Should update scroll handle position without triggering onScroll', async () => { 56 | const ref = React.createRef(); 57 | 58 | await act(async () => { 59 | render(); 60 | }); 61 | 62 | await act(async () => { 63 | ref.current?.onWheelScroll(100); 64 | }); 65 | 66 | expect(screen.getByRole('button').style.transform).to.equal('translate3d(10px, 0px, 0px)'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/SortTableSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '../src/Table'; 3 | import Column from '../src/Column'; 4 | import Cell from '../src/Cell'; 5 | import HeaderCell from '../src/HeaderCell'; 6 | import ColumnGroup from '../src/ColumnGroup'; 7 | import { render, screen } from '@testing-library/react'; 8 | 9 | describe('SortTable', () => { 10 | const renderSortIcon = sortType => { 11 | if (sortType === 'asc') { 12 | return 1; 13 | } else if (sortType === 'desc') { 14 | return 2; 15 | } 16 | return 3; 17 | }; 18 | 19 | it('Should render a descending icon', () => { 20 | render( 21 | 22 | 23 | sort cell 24 | 25 | 26 |
27 | ); 28 | 29 | expect(screen.getByText('sort cell').childNodes[1]).to.be.text('2'); 30 | }); 31 | 32 | it('Should render a ascending icon', () => { 33 | render( 34 | 35 | 36 | sort cell 37 | 38 | 39 |
40 | ); 41 | 42 | expect(screen.getByText('sort cell').childNodes[1]).to.be.text('1'); 43 | }); 44 | 45 | it('Should render a default icon', () => { 46 | render( 47 | 48 | 49 | sort cell 50 | 51 | 52 |
53 | ); 54 | 55 | expect(screen.getByText('sort cell').childNodes[1]).to.be.text('3'); 56 | }); 57 | 58 | describe('Sort in ColumnGroup', () => { 59 | it('Should render a descending icon', () => { 60 | render( 61 | 62 | 63 | 64 | sort cell 65 | 66 | 67 | 68 |
69 | ); 70 | 71 | expect( 72 | screen.getByRole('grid').querySelector('.rs-table-cell-header-sort-wrapper') 73 | ).to.be.text('2'); 74 | }); 75 | 76 | it('Should render a ascending icon', () => { 77 | render( 78 | 79 | 80 | 81 | sort cell 82 | 83 | 84 | 85 |
86 | ); 87 | 88 | expect( 89 | screen.getByRole('grid').querySelector('.rs-table-cell-header-sort-wrapper') 90 | ).to.be.text('1'); 91 | }); 92 | 93 | it('Should render a default icon', () => { 94 | render( 95 | 96 | 97 | 98 | sort cell 99 | 100 | 101 | 102 |
103 | ); 104 | 105 | expect( 106 | screen.getByRole('grid').querySelector('.rs-table-cell-header-sort-wrapper') 107 | ).to.be.text('3'); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/Table.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table, { TableInstance } from '../src/Table'; 3 | import Cell from '../src/Cell'; 4 | import Column from '../src/Column'; 5 | import HeaderCell from '../src/HeaderCell'; 6 | import { expectType } from 'ts-expect'; 7 | 8 | interface ItemDataType extends Record { 9 | label?: React.ReactNode; 10 | value?: T; 11 | groupBy?: string; 12 | parent?: ItemDataType; 13 | children?: ItemDataType[]; 14 | loading?: boolean; 15 | } 16 | 17 | type Row = { 18 | id: number; 19 | name: string; 20 | }; 21 | 22 | const data: Row[] = [ 23 | { id: 1, name: 'First' }, 24 | { id: 2, name: 'Second' } 25 | ]; 26 | 27 | { 30 | expectType(row); 31 | 32 | return 44; 33 | }} 34 | rowClassName={row => { 35 | expectType(row); 36 | 37 | return ''; 38 | }} 39 | renderTreeToggle={(button, row) => { 40 | expectType(row); 41 | 42 | return null; 43 | }} 44 | renderRowExpanded={row => { 45 | expectType(row); 46 | 47 | return null; 48 | }} 49 | renderRow={(node, row) => { 50 | expectType(row); 51 | 52 | return null; 53 | }} 54 | onRowClick={row => { 55 | expectType(row); 56 | }} 57 | onRowContextMenu={row => { 58 | expectType(row); 59 | }} 60 | onExpandChange={(expanded, row) => { 61 | expectType(row); 62 | }} 63 | />; 64 | 65 | // It should be possible to call instance methods via ref 66 | const ref = React.createRef>(); 67 | 68 |
; 69 | 70 |
71 | {({ Column, HeaderCell, Cell }) => ( 72 | <> 73 | 74 | Id 75 | {({ id }) => id} 76 | 77 | 78 | )} 79 |
; 80 | 81 | ref.current?.body; 82 | ref.current?.root; 83 | ref.current?.scrollLeft(100); 84 | ref.current?.scrollTop(100); 85 | 86 | interface InventoryItem { 87 | id: string; 88 | name: string; 89 | } 90 | 91 | const table = React.createRef>(); 92 | 93 | ref={table}> 94 | {({ Column, HeaderCell, Cell, ColumnGroup }) => ( 95 | <> 96 | 97 | Name 98 | {row => row.name} 99 | 100 | 101 | Type 102 | {/** @ts-expect-error Property 'type' does not exist on type 'InventoryItem' */} 103 | {row => row.type} 104 | 105 | 106 | 107 | 108 | Id 109 | {row => row.id} 110 | 111 | 112 | Name 113 | {row => row.name} 114 | 115 | 116 | 117 | )} 118 | ; 119 | 120 | interface ImageCellProps> { 121 | rowData: TRow; 122 | dataKey: TKey; 123 | // ... any other props 124 | } 125 | 126 | export const ImageCell = >({ 127 | rowData, 128 | dataKey, 129 | ...rest 130 | }: ImageCellProps) => ( 131 | {...rest}> 132 | 133 | 134 | ); 135 | 136 | const rows: ItemDataType[] = [ 137 | { id: 1, name: 'First' }, 138 | { id: 2, name: 'Second' } 139 | ]; 140 | 141 | // test case for https://github.com/rsuite/rsuite-table/issues/422 142 | 143 | 144 | Id 145 | {rowData => rowData.id} 146 | 147 |
; 148 | -------------------------------------------------------------------------------- /test/TableHeightSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import Table from '../src/Table'; 4 | import Column from '../src/Column'; 5 | import Cell from '../src/Cell'; 6 | import HeaderCell from '../src/HeaderCell'; 7 | import '../src/less/index.less'; 8 | 9 | const columns = ( 10 | <> 11 | 12 | id 13 | 14 | 15 | 16 | name 17 | 18 | 19 | 20 | ); 21 | 22 | const mockData = (size = 10) => { 23 | return Array.from({ length: size }, (_, i) => ({ id: i, name: `name${i}` })); 24 | }; 25 | 26 | describe('Table - Height ', () => { 27 | it('Should be automatic height', () => { 28 | render( 29 | 30 | {columns} 31 |
32 | ); 33 | 34 | // 2 rows + header row 35 | const height = 46 * 2 + 40; 36 | 37 | expect(screen.getByRole('grid')).to.have.style('height', `${height}px`); 38 | }); 39 | it('Should fill the height of the container', () => { 40 | render( 41 |
42 | 43 | {columns} 44 |
45 |
46 | ); 47 | 48 | expect(screen.getByRole('grid')).to.have.style('height', '300px'); 49 | }); 50 | 51 | it('Should be automatic height when there is a horizontal scroll bar', () => { 52 | render( 53 | 54 | {columns} 55 |
56 | ); 57 | // 2 rows + header row + scrollbar 58 | const height = 46 * 2 + 40 + 10; 59 | 60 | expect(screen.getByRole('grid')).to.have.style('height', `${height}px`); 61 | }); 62 | 63 | it('Should have a maximum height', () => { 64 | render( 65 | 66 | {columns} 67 |
68 | ); 69 | expect(screen.getByRole('grid')).to.have.style('height', '200px'); 70 | }); 71 | 72 | it('Should have a minimum height, when the number of data rows is less than the minimum height', () => { 73 | render( 74 | 75 | {columns} 76 |
77 | ); 78 | expect(screen.getByRole('grid')).to.have.style('height', '500px'); 79 | }); 80 | 81 | it('Should have a default height when the data is empty', () => { 82 | render( 83 | 84 | {columns} 85 |
86 | ); 87 | expect(screen.getByRole('grid')).to.have.style('height', '200px'); 88 | }); 89 | 90 | it('Should not exceed the maximum height', () => { 91 | render( 92 | 93 | {columns} 94 |
95 | ); 96 | 97 | expect(screen.getByRole('grid')).to.have.style('height', '300px'); 98 | }); 99 | 100 | it('Should not exceed the maximum height even if autoHeight is set', () => { 101 | render( 102 | 103 | {columns} 104 |
105 | ); 106 | 107 | expect(screen.getByRole('grid')).to.have.style('height', '300px'); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/VirtualizedTableSpec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Table from '../src/Table'; 3 | import Column from '../src/Column'; 4 | import Cell from '../src/Cell'; 5 | import HeaderCell from '../src/HeaderCell'; 6 | import { render, screen } from '@testing-library/react'; 7 | 8 | describe('VirtualizedTable', () => { 9 | it('Should be virtualized, and check `maximum update depth exceeded`', () => { 10 | expect(() => { 11 | render( 12 | 13 | 14 | ID 15 | 16 | 17 |
18 | ); 19 | }).to.not.throw(); 20 | }); 21 | 22 | // issue: https://github.com/rsuite/rsuite/issues/3226 23 | it('Should render correct row height when virtualized', () => { 24 | render( 25 | 26 | 27 | ID 28 | 29 | 30 |
31 | ); 32 | 33 | expect(screen.getAllByRole('row')[0]).to.have.style('height', '60px'); 34 | expect(screen.getAllByRole('row')[1]).to.have.style('height', '50px'); 35 | expect(screen.getByRole('columnheader')).to.have.style('height', '60px'); 36 | expect(screen.getByRole('gridcell')).to.have.style('height', '50px'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/build.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const glob = require('glob'); 3 | const fs = require('fs'); 4 | const { assert } = require('chai'); 5 | 6 | it('Prepends the `use client` directive to components', () => { 7 | const libfiles = glob.sync('{lib,es}/**/*.js'); 8 | 9 | libfiles.forEach(file => { 10 | const content = fs.readFileSync(file, 'utf-8'); 11 | assert.isTrue(content.startsWith(`'use client';`), `File ${file} has 'use client' directive`); 12 | }); 13 | 14 | console.log(` ✅ ${libfiles.length} files have been validated.`); 15 | }); 16 | -------------------------------------------------------------------------------- /test/findAllParentsSpec.ts: -------------------------------------------------------------------------------- 1 | import findAllParents from '../src/utils/findAllParents'; 2 | import { PARENT_KEY } from '../src/constants'; 3 | 4 | describe('findAllParents', () => { 5 | it('should find all parents of a node', () => { 6 | const tree = { 7 | id: 1, 8 | name: 'A', 9 | [PARENT_KEY]: { 10 | id: 2, 11 | name: 'B', 12 | [PARENT_KEY]: { 13 | id: 3, 14 | name: 'C', 15 | [PARENT_KEY]: undefined 16 | } 17 | } 18 | }; 19 | 20 | const result = findAllParents(tree, 'id'); 21 | 22 | expect(result).to.deep.equal([2, 3]); 23 | }); 24 | 25 | it('should return an empty array if the node has no parents', () => { 26 | const node = { id: 1, name: 'A', [PARENT_KEY]: undefined }; 27 | 28 | const result = findAllParents(node, 'id'); 29 | 30 | expect(result).to.deep.equal([]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/flattenDataSpec.ts: -------------------------------------------------------------------------------- 1 | import flattenData from '../src/utils/flattenData'; 2 | import { PARENT_KEY } from '../src/constants'; 3 | 4 | describe('flattenData', () => { 5 | it('should flatten a single-level tree', () => { 6 | const tree = [ 7 | { id: '1', name: 'A' }, 8 | { id: '2', name: 'B' }, 9 | { id: '3', name: 'C' } 10 | ]; 11 | 12 | const result = flattenData(tree); 13 | 14 | expect(result).to.deep.equal(tree.map(node => ({ ...node, [PARENT_KEY]: undefined }))); 15 | }); 16 | 17 | it('should flatten a multi-level tree', () => { 18 | const tree = [ 19 | { 20 | id: '1', 21 | name: 'A', 22 | children: [ 23 | { id: '2', name: 'B' }, 24 | { 25 | id: '3', 26 | name: 'C', 27 | children: [ 28 | { id: '4', name: 'D' }, 29 | { id: '5', name: 'E' } 30 | ] 31 | }, 32 | { id: '6', name: 'F' } 33 | ] 34 | }, 35 | { 36 | id: '7', 37 | name: 'G', 38 | children: [ 39 | { id: '8', name: 'H' }, 40 | { id: '9', name: 'I' } 41 | ] 42 | }, 43 | { id: '10', name: 'J' } 44 | ]; 45 | 46 | const result = flattenData(tree); 47 | 48 | expect(result).to.have.lengthOf(10); 49 | expect(JSON.stringify(result)).to.equal( 50 | JSON.stringify([ 51 | { 52 | id: '1', 53 | name: 'A', 54 | children: [ 55 | { id: '2', name: 'B' }, 56 | { 57 | id: '3', 58 | name: 'C', 59 | children: [ 60 | { id: '4', name: 'D' }, 61 | { id: '5', name: 'E' } 62 | ] 63 | }, 64 | { id: '6', name: 'F' } 65 | ] 66 | }, 67 | { id: '2', name: 'B' }, 68 | { 69 | id: '3', 70 | name: 'C', 71 | children: [ 72 | { id: '4', name: 'D' }, 73 | { id: '5', name: 'E' } 74 | ] 75 | }, 76 | { id: '4', name: 'D' }, 77 | { id: '5', name: 'E' }, 78 | { id: '6', name: 'F' }, 79 | { 80 | id: '7', 81 | name: 'G', 82 | children: [ 83 | { id: '8', name: 'H' }, 84 | { id: '9', name: 'I' } 85 | ] 86 | }, 87 | { id: '8', name: 'H' }, 88 | { id: '9', name: 'I' }, 89 | { id: '10', name: 'J' } 90 | ]) 91 | ); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/useCellDescriptorSpec.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useMemo } from 'react'; 2 | import useCellDescriptor from '../src/hooks/useCellDescriptor'; 3 | import Column from '../src/Column'; 4 | import HeaderCell from '../src/HeaderCell'; 5 | import Cell from '../src/Cell'; 6 | import { render, act } from '@testing-library/react'; 7 | 8 | describe('useCellDescriptor', () => { 9 | let cellDescriptor; 10 | let setWidthOfFirstColumn; 11 | 12 | // test component that wraps useCellDescriptor and stores ref to 13 | // output vars in the `cellDescriptor` var declared above 14 | const TestComponent = () => { 15 | const [width, setWidth] = useState(100); 16 | setWidthOfFirstColumn = setWidth; 17 | 18 | const columns = useMemo( 19 | () => [ 20 | 21 | Header 1 22 | 23 | , 24 | 25 | Header 2 26 | 27 | 28 | ], 29 | [width] 30 | ); 31 | 32 | const descriptor = useCellDescriptor({ 33 | children: columns, 34 | showHeader: true, 35 | headerHeight: 100, 36 | tableRef: {}, 37 | tableWidth: {}, 38 | scrollX: {}, 39 | minScrollX: {}, 40 | mouseAreaRef: {} 41 | } as any); 42 | 43 | useEffect(() => { 44 | cellDescriptor = descriptor; 45 | }, [descriptor]); 46 | 47 | return null; 48 | }; 49 | 50 | it('Should output expected vars', () => { 51 | render(); 52 | 53 | expect(cellDescriptor).to.contain.keys([ 54 | 'columns', 55 | 'headerCells', 56 | 'bodyCells', 57 | 'allColumnsWidth', 58 | 'hasCustomTreeCol' 59 | ]); 60 | }); 61 | 62 | it('Should set widths on cells', () => { 63 | render(); 64 | 65 | const { headerCells, bodyCells } = cellDescriptor; 66 | 67 | expect(headerCells[0].props.width).to.equal(100); 68 | expect(bodyCells[0].props.width).to.equal(100); 69 | expect(headerCells[1].props.width).to.equal(200); 70 | expect(bodyCells[1].props.width).to.equal(200); 71 | }); 72 | 73 | it('Should update cell width on column resize', () => { 74 | render(); 75 | 76 | const { headerCells } = cellDescriptor; 77 | const { onColumnResizeEnd } = headerCells[0].props; 78 | 79 | // (columnWidth: number, _cursorDelta: number, dataKey: any, index: number) 80 | act(() => onColumnResizeEnd(500, 5, 'key1', 0)); 81 | 82 | expect(cellDescriptor.bodyCells[0].props.width).to.equal(500); 83 | }); 84 | 85 | it('Should set proper cell width on column resize followed by column width change', () => { 86 | render(); 87 | 88 | const { headerCells } = cellDescriptor; 89 | const { onColumnResizeEnd } = headerCells[0].props; 90 | 91 | // (columnWidth: number, _cursorDelta: number, dataKey: any, index: number) 92 | act(() => onColumnResizeEnd(500, 5, 'key1', 0)); 93 | expect(cellDescriptor.bodyCells[0].props.width).to.equal(500); 94 | 95 | act(() => setWidthOfFirstColumn(420)); 96 | expect(cellDescriptor.bodyCells[0].props.width).to.equal(420); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | console.log('React version:', React.version); 4 | 5 | export * from './testStandardProps'; 6 | -------------------------------------------------------------------------------- /test/utils/testStandardProps.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | export function testTestIdProp(element, renderOptions) { 5 | it('Should accept data-testid prop', () => { 6 | const { getByTestId } = render( 7 | React.cloneElement(element, { 'data-testid': 'element' }), 8 | renderOptions 9 | ); 10 | 11 | expect(getByTestId('element')).to.exist; 12 | }); 13 | } 14 | 15 | export function testClassNameProp( 16 | element, 17 | customClassName, 18 | renderOptions, 19 | getRootElement = view => view.container.firstChild 20 | ) { 21 | it('Should accept custom className', () => { 22 | const view = render( 23 | React.cloneElement(element, { 'data-testid': 'element', className: customClassName }), 24 | renderOptions 25 | ); 26 | 27 | const rootElement = getRootElement(view); 28 | 29 | expect(rootElement).to.have.class(customClassName); 30 | expect(rootElement.className.split(customClassName).length).to.equal( 31 | 2, 32 | `className "${customClassName}" should not appear multiple times` 33 | ); 34 | }); 35 | } 36 | 37 | export function testClassPrefixProp( 38 | element, 39 | renderOptions, 40 | getRootElement = view => view.container.firstChild 41 | ) { 42 | it('Should accept custom className prefix', () => { 43 | const customClassPrefix = 'custom-prefix'; 44 | const view = render( 45 | React.cloneElement(element, { 'data-testid': 'element', classPrefix: customClassPrefix }), 46 | renderOptions 47 | ); 48 | expect(getRootElement(view)).to.have.class(new RegExp('^rs-' + customClassPrefix)); 49 | }); 50 | } 51 | 52 | export function testStyleProp( 53 | element, 54 | renderOptions, 55 | getStyleElement = view => view.container.firstChild 56 | ) { 57 | it('Should accept custom style', () => { 58 | const fontSize = '12px'; 59 | const view = render( 60 | React.cloneElement(element, { 'data-testid': 'element', style: { fontSize } }), 61 | renderOptions 62 | ); 63 | 64 | expect(getStyleElement(view)).to.have.style('font-size', fontSize); 65 | }); 66 | } 67 | 68 | interface TestStandardPropsOptions { 69 | renderOptions?: any; 70 | customClassName?: string | boolean; 71 | getRootElement?: (view: any) => HTMLElement; 72 | getStyleElement?: (view: any) => HTMLElement; 73 | } 74 | 75 | export function testStandardProps(element, options: TestStandardPropsOptions = {}) { 76 | const { displayName } = element.type; 77 | const { renderOptions, customClassName, getRootElement, getStyleElement } = options; 78 | 79 | describe(`${displayName} - Standard props`, () => { 80 | it('Should have a display name', () => { 81 | expect(displayName).to.exist; 82 | }); 83 | 84 | testTestIdProp(element, renderOptions); 85 | if (customClassName !== false) { 86 | testClassNameProp( 87 | element, 88 | typeof customClassName === 'string' ? customClassName : 'custom-class', 89 | renderOptions, 90 | getRootElement 91 | ); 92 | } 93 | testClassPrefixProp(element, renderOptions, getRootElement); 94 | testStyleProp(element, renderOptions, getStyleElement); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "allowJs": true, 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "noImplicitAny": false, 9 | "noUnusedParameters": true, 10 | "noUnusedLocals": true, 11 | "sourceMap": true, 12 | "moduleResolution": "node", 13 | "target": "esnext", 14 | "module": "esnext", 15 | "jsx": "react", 16 | "skipLibCheck": true 17 | }, 18 | "include": ["./src/**/*.ts", "./src/**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noUnusedParameters": false 5 | }, 6 | "include": ["./test/*.test.tsx"] 7 | } 8 | -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | const __DEV__ = process.env.NODE_ENV === 'development'; 6 | const filename = __DEV__ ? '[name].js' : '[name].min.js'; 7 | 8 | const plugins = [ 9 | new webpack.SourceMapDevToolPlugin({ 10 | filename: `${filename}.map` 11 | }) 12 | ]; 13 | 14 | module.exports = { 15 | entry: { 16 | ['rsuite-table']: path.join(__dirname, 'src') 17 | }, 18 | output: { 19 | path: path.join(__dirname, 'dist'), 20 | filename, 21 | library: 'rsuite-table', 22 | libraryTarget: 'umd' 23 | }, 24 | externals: { 25 | react: { 26 | root: 'React', 27 | commonjs2: 'react', 28 | commonjs: 'react', 29 | amd: 'react' 30 | }, 31 | 'react-dom': { 32 | root: 'ReactDOM', 33 | commonjs2: 'react-dom', 34 | commonjs: 'react-dom', 35 | amd: 'react-dom' 36 | } 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.ts|tsx?$/, 42 | use: ['babel-loader?babelrc'], 43 | exclude: /node_modules/ 44 | } 45 | ] 46 | }, 47 | plugins, 48 | resolve: { 49 | extensions: ['.ts', '.tsx', '.js'] 50 | } 51 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | 3 | const path = require('path'); 4 | const HtmlwebpackPlugin = require('html-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const { NODE_ENV } = process.env; 7 | 8 | const docsPath = NODE_ENV === 'development' ? './assets' : './'; 9 | 10 | module.exports = () => { 11 | return { 12 | entry: './docs/index.tsx', 13 | devtool: 'source-map', 14 | resolve: { 15 | extensions: ['.ts', '.tsx', '.js', '.json'] 16 | }, 17 | devServer: { 18 | hot: true, 19 | contentBase: path.resolve(__dirname, ''), 20 | publicPath: '/' 21 | }, 22 | output: { 23 | path: path.resolve(__dirname, 'assets'), 24 | filename: 'bundle.js', 25 | publicPath: './' 26 | }, 27 | plugins: [ 28 | new HtmlwebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'docs/index.html', 31 | inject: true, 32 | hash: true, 33 | path: docsPath 34 | }), 35 | new MiniCssExtractPlugin({ 36 | filename: '[name].css', 37 | chunkFilename: '[id].css' 38 | }) 39 | ], 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.tsx?$/, 44 | use: ['babel-loader'], 45 | exclude: /node_modules/ 46 | }, 47 | { 48 | test: /\.(less|css)$/, 49 | use: [ 50 | MiniCssExtractPlugin.loader, 51 | { 52 | loader: 'css-loader' 53 | }, 54 | { 55 | loader: 'less-loader', 56 | options: { 57 | sourceMap: true, 58 | lessOptions: { 59 | javascriptEnabled: true 60 | } 61 | } 62 | } 63 | ] 64 | }, 65 | { 66 | test: /\.md$/, 67 | loader: 'react-code-view/webpack-md-loader' 68 | }, 69 | { 70 | test: /\.(woff|woff2|eot|ttf|svg)($|\?)/, 71 | use: [ 72 | { 73 | loader: 74 | 'url-loader?limit=1&hash=sha512&digest=hex&size=16&name=resources/[hash].[ext]' 75 | } 76 | ] 77 | } 78 | ] 79 | } 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /webpack.karma.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | /** 5 | * @type {import('webpack').Configuration} 6 | */ 7 | module.exports = { 8 | mode: 'development', 9 | output: { 10 | pathinfo: true 11 | }, 12 | resolve: { 13 | extensions: ['.ts', '.tsx', '.js'] 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: [/\.tsx?$/, /\.jsx?$/], 19 | use: ['babel-loader?babelrc'], 20 | exclude: /node_modules/ 21 | }, 22 | { 23 | test: /\.(less|css)$/, 24 | use: [ 25 | MiniCssExtractPlugin.loader, 26 | { 27 | loader: 'css-loader' 28 | }, 29 | { 30 | loader: 'less-loader', 31 | options: { 32 | sourceMap: true, 33 | lessOptions: { 34 | javascriptEnabled: true 35 | } 36 | } 37 | } 38 | ] 39 | } 40 | ] 41 | }, 42 | plugins: [ 43 | new MiniCssExtractPlugin({ 44 | filename: '[name].css', 45 | chunkFilename: '[id].css' 46 | }) 47 | ], 48 | devtool: 'eval' 49 | }; 50 | --------------------------------------------------------------------------------