├── .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 |
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 |
15 | : }
19 | />
20 |
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 |
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 |
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 | {
12 | onChange(rowData);
13 | }}
14 | >
15 | {expandedRowKeys.some(key => key === rowData[rowKey]) ? '-' : '+'}
16 |
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 |
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 |
{
97 | tableRef.current.scrollTop(10000);
98 | }}
99 | >
100 | Scroll top
101 |
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 |
{
15 | setData([]);
16 | }}
17 | >
18 | Clear
19 |
20 | {' | '}
21 |
{
23 | setData(fakeData);
24 | }}
25 | >
26 | Reset
27 |
28 |
{
31 | let value = e.target.value;
32 |
33 | if (value === '' || value == 'true') {
34 | value = Boolean(value);
35 | }
36 |
37 | setWordWrap(value);
38 | }}
39 | >
40 | false
41 | true
42 | break-all
43 | break-word
44 | keep-all
45 |
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 |
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 |
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 |
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 |
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 |
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 |
86 | );
87 | expect(screen.getByRole('grid')).to.have.style('height', '200px');
88 | });
89 |
90 | it('Should not exceed the maximum height', () => {
91 | render(
92 |
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 |
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 |
--------------------------------------------------------------------------------