├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── npm-publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .sass-lint.yml ├── .storybook ├── main.js ├── manager.js ├── preview-head.html └── preview.js ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── images ├── table.png └── tableSelection.png ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── components │ ├── constants.ts │ ├── index.ts │ ├── responsive-container.tsx │ ├── scroller.tsx │ ├── styled-table │ │ ├── bubble.tsx │ │ ├── cell-with-icon.tsx │ │ ├── editable-cell.tsx │ │ ├── header-cell.tsx │ │ └── index.ts │ ├── table-interactions-manager │ │ ├── actions-types.ts │ │ ├── actions.ts │ │ ├── cell-dimensions-controller.tsx │ │ ├── column-id-scroll-controller.tsx │ │ ├── column-visibility-controller.tsx │ │ ├── fixed-column-controller.tsx │ │ ├── fixed-row-controller.tsx │ │ ├── index.ts │ │ ├── reducers.ts │ │ └── table-interactions-manager.tsx │ ├── table-selection │ │ ├── context-menu-handler.tsx │ │ ├── index.ts │ │ ├── selection-handler.tsx │ │ └── table-selection-menu.tsx │ ├── table │ │ ├── cell.tsx │ │ ├── elementary-table.tsx │ │ ├── index.ts │ │ ├── row-span.tsx │ │ ├── row.tsx │ │ └── table.tsx │ ├── typing.ts │ ├── utils │ │ ├── common.ts │ │ ├── index.ts │ │ ├── shallowEqual.ts │ │ ├── table-selection.ts │ │ └── table.tsx │ └── virtualizer.tsx ├── hooks │ ├── index.ts │ └── useComponent.ts ├── index.ts └── style │ ├── _responsive-container.scss │ ├── _scroller.scss │ ├── fonts │ ├── material-design-icons │ │ ├── MaterialIcons-Regular.eot │ │ ├── MaterialIcons-Regular.ijmap │ │ ├── MaterialIcons-Regular.svg │ │ ├── MaterialIcons-Regular.ttf │ │ ├── MaterialIcons-Regular.woff │ │ ├── MaterialIcons-Regular.woff2 │ │ └── material-icons.scss │ └── roboto │ │ ├── Apache License.txt │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-BlackItalic.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-LightItalic.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-MediumItalic.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-ThinItalic.ttf │ │ ├── RobotoCondensed-Bold.ttf │ │ ├── RobotoCondensed-BoldItalic.ttf │ │ ├── RobotoCondensed-Italic.ttf │ │ ├── RobotoCondensed-Light.ttf │ │ ├── RobotoCondensed-LightItalic.ttf │ │ └── RobotoCondensed-Regular.ttf │ ├── index.scss │ ├── styled-table │ ├── _bubble.scss │ ├── _cell-with-icon.scss │ ├── _editable-cell.scss │ ├── _header-cell.scss │ └── index.scss │ ├── table-interactions-manager │ ├── _column-id-scroll-controller.scss │ ├── _column-visibility-controller.scss │ ├── _common.scss │ └── index.scss │ ├── table-selection │ ├── _selection-handler.scss │ ├── _selection-menu.scss │ └── index.scss │ ├── table │ ├── _cell.scss │ ├── _row-span.scss │ ├── _row.scss │ └── index.scss │ └── variable │ ├── _color.scss │ ├── _font.scss │ ├── _global.scss │ ├── _mixin.scss │ └── index.scss ├── stories ├── components │ ├── responsive-cotainer.stories.tsx │ ├── scroller.stories.tsx │ ├── styled-table │ │ ├── bubble.stories.tsx │ │ ├── cell-with-icon.stories.tsx │ │ ├── editable-cell.stories.tsx │ │ ├── header-cell.stories.tsx │ │ └── tables.ts │ ├── table-interactions-manager │ │ └── table-interactions-manager.stories.tsx │ ├── table-selection │ │ └── table.stories.tsx │ ├── table │ │ ├── table.md │ │ ├── table.stories.tsx │ │ └── virtualized-table.stories.tsx │ ├── virtualizer.md │ └── virtualizer.stories.tsx ├── stories-components │ └── selection-menu.tsx ├── typings.d.ts └── utils │ ├── decorators.tsx │ └── tables.ts ├── test ├── components │ ├── __snapshots__ │ │ ├── responsive-container.test.tsx.snap │ │ ├── scroller.test.tsx.snap │ │ └── virtualizer.test.tsx.snap │ ├── responsive-container.test.tsx │ ├── scroller.test.tsx │ ├── styled-table │ │ ├── __snapshots__ │ │ │ ├── bubble.test.tsx.snap │ │ │ ├── cell-with-icon.test.tsx.snap │ │ │ ├── editable-cell.test.tsx.snap │ │ │ └── header-cell.test.tsx.snap │ │ ├── bubble.test.tsx │ │ ├── cell-with-icon.test.tsx │ │ ├── editable-cell.test.tsx │ │ └── header-cell.test.tsx │ ├── table-interactions-manager │ │ ├── __snapshots__ │ │ │ ├── cell-dimensions-controller.test.tsx.snap │ │ │ ├── column-visibility-controller.test.tsx.snap │ │ │ ├── fixed-column-controller.test.tsx.snap │ │ │ └── fixed-row-controller.test.tsx.snap │ │ ├── cell-dimensions-controller.test.tsx │ │ ├── column-visibility-controller.test.tsx │ │ ├── fixed-column-controller.test.tsx │ │ └── fixed-row-controller.test.tsx │ ├── table-selection │ │ ├── __snapshots__ │ │ │ ├── context-menu-handler.test.tsx.snap │ │ │ ├── selection-handler.test.tsx.snap │ │ │ └── table-selection-menu.test.tsx.snap │ │ ├── context-menu-handler.test.tsx │ │ ├── selection-handler.test.tsx │ │ └── table-selection-menu.test.tsx │ ├── table │ │ ├── __snapshots__ │ │ │ ├── cell.test.tsx.snap │ │ │ ├── elementary-table.test.tsx.snap │ │ │ ├── row-span.test.tsx.snap │ │ │ ├── row.test.tsx.snap │ │ │ └── table.test.tsx.snap │ │ ├── cell.test.tsx │ │ ├── elementary-table.test.tsx │ │ ├── row-span.test.tsx │ │ ├── row.test.tsx │ │ ├── table.test.tsx │ │ └── utils.test.tsx │ └── virtualizer.test.tsx ├── global-test-setup.js ├── it-tests │ ├── cell-dimensions-controller.test.tsx │ ├── column-visibility-controller.test.tsx │ ├── editable-cell-it.test.tsx │ ├── elementary-table-it.test.tsx │ ├── fixed-column-controller.test.tsx │ ├── fixed-row-controller.test.tsx │ ├── row-it.test.tsx │ ├── rows-controller.test.tsx │ ├── selectable-table.test.tsx │ └── virtualized-table.test.tsx ├── polyfill │ └── array.find.polyfill.js ├── tests-utils │ ├── date.ts │ ├── react-testing-library-utils.tsx │ └── table.ts ├── tests.entry.js ├── typings │ └── tests-entry.d.ts └── utils │ └── common.test.ts ├── tsconfig.eslint.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | dist/* 3 | test/polyfill/array.find.polyfill.js 4 | test/tests.entry.js 5 | test/global-test-setup 6 | node_modules/* 7 | README.md 8 | stats.json 9 | jest-test-results.json 10 | jest.config.js 11 | .eslintrc.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: `@typescript-eslint/parser`, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es2020: true, 7 | jest: true, 8 | }, 9 | extends: [ 10 | "airbnb-typescript", 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/recommended", 13 | "plugin:react/recommended", 14 | "prettier", 15 | ], 16 | parserOptions: { 17 | project: `./tsconfig.eslint.json`, 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 11, 22 | sourceType: "module", 23 | }, 24 | settings: { 25 | "import/parsers": { 26 | "@typescript-eslint/parser": [".ts", ".tsx", ".js", ".jsx", ".svg"], 27 | }, 28 | "import/resolver": { 29 | typescript: { 30 | alwaysTryTypes: true, // always try to resolve types under `@types` directory even it doesn't contain any source code, like `@types/unist` 31 | }, 32 | }, 33 | }, 34 | plugins: ["@typescript-eslint", "prettier", "react", "jsx-a11y", "import"], 35 | rules: { 36 | "prettier/prettier": ["error"], 37 | "import/prefer-default-export": 0, 38 | "react/jsx-props-no-spreading": 0, 39 | "react/prop-types": 0, 40 | "arrow-body-style": 0, 41 | "react/self-closing-comp": 0, 42 | "react/react-in-jsx-scope": 0, 43 | "react/jsx-fragments": 0, 44 | "react/require-default-props": 0, 45 | "react/display-name": 1, 46 | "@typescript-eslint/naming-convention": 0, 47 | "@typescript-eslint/no-redeclare": 0, 48 | "@typescript-eslint/ban-types": 0, 49 | "@typescript-eslint/no-shadow": 0, 50 | "@typescript-eslint/no-use-before-define": 0, 51 | "@typescript-eslint/no-var-requires": 1, 52 | "@typescript-eslint/ban-ts-comment": 1, 53 | "@typescript-eslint/triple-slash-reference": 0, 54 | "@typescript-eslint/no-unused-expressions": 0, 55 | "@typescript-eslint/ban-ts-comment": 0, 56 | "import/no-extraneous-dependencies": [ 57 | "error", 58 | { 59 | devDependencies: true, 60 | }, 61 | ], 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @amen-souissi 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ### Code of Conduct 2 | 3 | #### Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | #### Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | #### Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | #### Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | #### Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [amen.souissi@decathlon.com](mailto:amen.souissi@decathlon.com). All complaints will be reviewed and investigated and will result in a response that 59 | is deemed necessary and appropriate to the circumstances. The project team is 60 | obligated to maintain confidentiality with regard to the reporter of an incident. 61 | Further details of specific enforcement policies may be posted separately. 62 | 63 | Project maintainers who do not follow or enforce the Code of Conduct in good 64 | faith may face temporary or permanent repercussions as determined by other 65 | members of the project's leadership. 66 | 67 | #### Attribution 68 | 69 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, 70 | available [here](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | A clear and concise description of what the bug is. 8 | 9 | **To Reproduce** 10 | Steps to reproduce the behavior: 11 | 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | 25 | - OS: [e.g. iOS] 26 | - Browser [e.g. chrome, safari] 27 | - Version [e.g. 22] 28 | 29 | **Smartphone (please complete the following information):** 30 | 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser name and version [e.g. stock browser, safari 10] 34 | - NodeJS version [e.g. 8.11.1] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 8 | 9 | **Describe the solution you'd like** 10 | A clear and concise description of what you want to happen. 11 | 12 | **Describe alternatives you've considered** 13 | A clear and concise description of any alternative solutions or features you've considered. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the feature request here. 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Your checklist for this pull request 2 | 3 | 🚨Please review the [guidelines for contributing](../CONTRIBUTING.md) to this repository. 4 | 5 | - [ ] Make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master! 6 | - [ ] Make sure you are making a pull request against the **master branch** (left side). Also you should start _your branch_ off _our master_. 7 | - [ ] Check the commit's or even all commits' message styles matches our requested structure. 8 | - [ ] Check your code additions will pass linting. 9 | 10 | ### Description 11 | 12 | Please describe your pull request. 13 | 14 | 💔Thank you! 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Run tests 12 | run: | 13 | npm ci 14 | npm run test 15 | npm run lint 16 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | registry-url: https://registry.npmjs.org/ 16 | - run: npm ci 17 | - run: npm publish --access public 18 | env: 19 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 20 | -------------------------------------------------------------------------------- /.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 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (https://nodejs.org/api/addons.html) 30 | build/Release 31 | dist/ 32 | 33 | # Dependency directories 34 | node_modules/ 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional eslint cache 40 | .eslintcache 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Output of 'npm pack' 46 | *.tgz 47 | 48 | # Yarn Integrity file 49 | .yarn-integrity 50 | 51 | # dotenv environment variables file 52 | .env 53 | 54 | # next.js build output 55 | .next 56 | 57 | # Webpack analyzer 58 | stats.json 59 | 60 | # Storybook Jest 61 | jest-test-results.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | dist/* 3 | node_modules/* 4 | README.md 5 | stats.json 6 | jest-test-results.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 130 3 | } 4 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | # Linter Options 2 | options: 3 | formatter: stylish 4 | 5 | # File Options 6 | files: 7 | include: 8 | - "src/stye/*.s+(a|c)ss" 9 | - "src/stye/**/*.s+(a|c)ss" 10 | 11 | # Rule Configuration 12 | rules: 13 | # Disallows 14 | no-ids: 2 15 | no-important: 2 16 | 17 | # Style Guide 18 | quotes: 19 | - 1 20 | - style: double 21 | indentation: 22 | - 1 23 | - size: 2 24 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const toPath = (filePath) => path.join(process.cwd(), filePath); 3 | 4 | module.exports = { 5 | stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"], 6 | addons: [ 7 | "@storybook/addon-actions", 8 | "@storybook/addon-viewport", 9 | "@storybook/addon-knobs", 10 | "@storybook/addon-jest", 11 | "@storybook/addon-a11y", 12 | "@storybook/addon-storysource", 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons"; 2 | import { themes } from "@storybook/theming"; 3 | 4 | addons.setConfig({ 5 | panelPosition: "right", 6 | theme: { 7 | ...themes.normal, 8 | brandTitle: "@decathlon/react-table", 9 | brandUrl: "#", 10 | 11 | // UI 12 | appBg: "white", 13 | appBorderRadius: 4, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import "../dist/style/index.css"; 2 | import results from "../jest-test-results.json"; 3 | import { withTests } from "@storybook/addon-jest"; 4 | 5 | const withStoryStyles = (Story) => { 6 | return ; 7 | }; 8 | 9 | export const decorators = [withStoryStyles, withTests({ results })]; 10 | 11 | export const parameters = { 12 | actions: { argTypesRegex: "^on[A-Z].*" }, 13 | }; 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to @decathlon/react-table 2 | 3 | First off, thanks for taking the time to contribute! 4 | 5 | The following is a set of guidelines for contributing to this project. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ### I don't care about this whole thing, I just have a simple question! 8 | 9 | If you need an answer, you can either: 10 | 11 | 1. Open an issue and assign to it the `question` label, or 12 | 2. Slack (TODO) 13 | 14 | ### How can I contribute? 15 | 16 | #### Reporting bugs 17 | 18 | Before reporting a bug, please make sure it hasn't already been reported by visiting the 19 | [issue section](todo). 20 | 21 | If the bug you found hasn't been reported yet, create a new issue and assign it the proper label(s). 22 | Besides this, there isn't any specific guideline on how the bugs should be reported, Just be sure 23 | to be as clear as possible when describing it. 24 | 25 | #### Suggesting enhancements 26 | 27 | Same as the bug reporting. First of all, check if the enhancement has already been suggested. 28 | If it doesn't exist, create a new issue and give it the `enhancement` label, plus any other proper label. 29 | 30 | Keep in mind that what you may find useful might be completely useless for other users, 31 | so please, make sure that the enhancement can actually be useful for everyone before proposing it. 32 | If you find that it is actually useful only for you, consider forking the project and implementing that 33 | enhancement just for yourself. 34 | 35 | ### Styleguides 36 | 37 | #### Commit messages 38 | 39 | - Use the present tense ("Add feature" not "Added feature") 40 | - Limit the first line to 72 characters or less 41 | - Reference issues and pull requests liberally after the first line 42 | 43 | #### Pull Requests 44 | 45 | - Specify what has been changed/added/removed 46 | - Write a short and concise title. Be more specific in the description 47 | - Do not include issue numbers in the PR title 48 | - Be sure to follow all the project coding guidelines. ESLint will definitely give you a big help with this 49 | - End all files with a newline 50 | - Add configuration dependencies as devDependencies, and frontend dependencies as normal dependencies 51 | - Avoid platform-dependent code 52 | - npm run format 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # @decathlon/react-table 3 | 4 | React components for efficiently rendering large tabular data 5 | 6 | 7 | 8 | *A table with 15 000 rows and 1 000 columns (with sub rows, fixed rows and fixed columns)* 9 | 10 | 11 | 12 | 13 | *A table with custom right click menu* 14 | 15 | ![Storybook](https://cdn.jsdelivr.net/gh/storybooks/brand@master/badge/badge-storybook.svg) 16 | 17 | ### Installing 18 | 19 | ``` 20 | npm install -S @decathlon/react-table 21 | ``` 22 | 23 | ### Usage 24 | 25 | 26 | ```js 27 | import { Table } from "@decathlon/react-table"; 28 | import "@decathlon/react-table/dist/style/index.css"; 29 | 30 | const rows = [ 31 | { 32 | id: "header", 33 | isHeader: true, 34 | cells: [ 35 | { 36 | id: "wawoo", 37 | value: "Wawooo!" 38 | }, 39 | ... 40 | ] 41 | }, 42 | { 43 | id: "row1", 44 | cells: [ 45 | { 46 | id: "react", 47 | value: "React" 48 | }, 49 | ... 50 | ] 51 | }, 52 | ... 53 | ]; 54 | 55 | function MyComponent() { 56 | return ( 57 | 69 | ); 70 | } 71 | ``` 72 | 73 | ## Getting Started (Devs) 74 | 75 | ```bash 76 | git clone ... 77 | cd react-table 78 | npm ci 79 | npm run storybook 80 | ``` 81 | 82 | 🚀 Storybook ready at http://localhost:9001/ 83 | 84 | ## Running the tests 85 | 86 | ``` 87 | npm run test 88 | npm run lint 89 | ``` 90 | ## Contributing 91 | 92 | **PRs are welcome!** 93 | You noticed a bug, a possible improvement or whatever? 94 | Any help is always appreciated, so don't hesitate opening one! 95 | 96 | Be sure to check out the [contributing guidelines](CONTRIBUTING.md) to fasten 97 | up the merging process. 98 | 99 | ## Active authors 100 | 101 | * **Amen Souissi** [amen-souissi](https://github.com/amen-souissi) 102 | * **Hyacinthe Knobloch** [hyacintheknobloch](https://github.com/hyacintheknobloch) 103 | * **Benjamin Wintrebert** [Ben-Wintrebert](https://github.com/Ben-Wintrebert) 104 | 105 | See also the list of [contributors](https://github.com/Decathlon/react-table/graphs/contributors) who participated in this project. 106 | 107 | ## License 108 | 109 | This project is licensed under the Apache-2.0 License - see the [LICENSE.md](https://github.com/Decathlon/react-table/blob/master/LICENSE) file for details 110 | -------------------------------------------------------------------------------- /images/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/images/table.png -------------------------------------------------------------------------------- /images/tableSelection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/images/tableSelection.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: false, 3 | testEnvironment: "jsdom", 4 | roots: ["test"], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest", 7 | }, 8 | testPathIgnorePatterns: ["/node_modules/", "/dist/"], 9 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 10 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 11 | moduleDirectories: ["node_modules", "src"], 12 | globalSetup: "./test/global-test-setup.js", 13 | setupFilesAfterEnv: ["./test/polyfill/array.find.polyfill.js", "./test/tests.entry.js"], 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@decathlon/react-table", 3 | "version": "2.1.1", 4 | "description": "React components for efficiently rendering large tabular data", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Decathlon/react-table" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Decathlon/react-table/issues" 13 | }, 14 | "homepage": "https://github.com/Decathlon/react-table/blob/master/README.md", 15 | "files": [ 16 | "dist" 17 | ], 18 | "dependencies": { 19 | "classnames": "^2.3.1", 20 | "immutability-helper": "^3.1.1", 21 | "lodash": "^4.17.21", 22 | "memoize-one": "^5.2.1", 23 | "react-number-format": "^4.7.3", 24 | "react-resize-detector": "^6.7.7" 25 | }, 26 | "peerDependencies": { 27 | "@emotion/react": "^11.4.1", 28 | "@emotion/styled": "^11.3.0", 29 | "@mui/icons-material": "^5.0.3", 30 | "@mui/material": "^5.0.3", 31 | "@mui/styles": "^5.0.1", 32 | "react": "^17.0.2", 33 | "react-dom": "^17.0.2" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.15.8", 37 | "@emotion/react": "^11.4.1", 38 | "@emotion/styled": "^11.3.0", 39 | "@mui/icons-material": "^5.0.3", 40 | "@mui/material": "^5.0.3", 41 | "@mui/styles": "^5.0.1", 42 | "@storybook/addon-a11y": "~6.3.10", 43 | "@storybook/addon-actions": "~6.3.10", 44 | "@storybook/addon-jest": "~6.3.10", 45 | "@storybook/addon-knobs": "~6.3.1", 46 | "@storybook/addon-storysource": "~6.3.10", 47 | "@storybook/addon-viewport": "~6.3.10", 48 | "@storybook/addons": "~6.3.10", 49 | "@storybook/react": "~6.3.10", 50 | "@testing-library/react": "^12.1.2", 51 | "@types/classnames": "^2.3.1", 52 | "@types/enzyme": "^3.10.9", 53 | "@types/jest": "^27.0.2", 54 | "@types/lodash": "^4.14.175", 55 | "@types/node": "^16.10.3", 56 | "@types/react": "^17.0.27", 57 | "@types/react-dom": "^17.0.9", 58 | "@types/react-test-renderer": "^17.0.1", 59 | "@typescript-eslint/eslint-plugin": "^4.33.0", 60 | "@typescript-eslint/parser": "^4.33.0", 61 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.3", 62 | "babel-loader": "^8.2.2", 63 | "css-loader": "^6.3.0", 64 | "enzyme": "^3.11.0", 65 | "enzyme-adapter-react-16": "^1.15.6", 66 | "eslint": "^7.32.0", 67 | "eslint-config-airbnb": "^18.2.1", 68 | "eslint-config-airbnb-typescript": "^14.0.1", 69 | "eslint-config-prettier": "^8.3.0", 70 | "eslint-formatter-json": "^0.1.0", 71 | "eslint-formatter-pretty": "^4.1.0", 72 | "eslint-import-resolver-typescript": "^2.4.0", 73 | "eslint-plugin-import": "^2.24.2", 74 | "eslint-plugin-jest": "^24.5.2", 75 | "eslint-plugin-jsx-a11y": "^6.4.1", 76 | "eslint-plugin-prettier": "^4.0.0", 77 | "eslint-plugin-react": "^7.26.1", 78 | "eslint-plugin-react-hooks": "^4.2.0", 79 | "husky": "~3.1.0", 80 | "isomorphic-fetch": "^3.0.0", 81 | "jest": "^27.2.5", 82 | "markdown-loader": "^6.0.0", 83 | "node-sass": "^6.0.1", 84 | "path": "~0.12.7", 85 | "prettier": "^2.4.1", 86 | "prettier-check": "~2.0.0", 87 | "react": "^17.0.2", 88 | "react-docgen-typescript-loader": "^3.7.2", 89 | "react-dom": "^17.0.2", 90 | "react-test-renderer": "^17.0.2", 91 | "rimraf": "^3.0.2", 92 | "sass-lint": "~1.13.1", 93 | "sass-loader": "^12.1.0", 94 | "ts-jest": "^27.0.5", 95 | "ts-loader": "^9.2.6", 96 | "typescript": "^4.4.3" 97 | }, 98 | "scripts": { 99 | "build": "rimraf dist && tsc && npm run build:scss", 100 | "build:scss": "node-sass src/style/index.scss -o dist/style && cp -r src/style/fonts dist/style", 101 | "prepublish": "npm run build", 102 | "test": "npm run test:unit", 103 | "test:unit": "jest", 104 | "test:watch": "npm run test:unit -- --watch", 105 | "test:generate-output": "jest --json --outputFile=jest-test-results.json", 106 | "precoverage": "npm run pretest && rimraf coverage/", 107 | "coverage": "npm run test:unit -- --coverage", 108 | "pretest": "rimraf tests.entry.js.map test/results.xml", 109 | "lint": "npm run lint:eslint && npm run lint:css && npm run lint:prettier", 110 | "lint:eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./", 111 | "lint:css": "sass-lint --verbose", 112 | "lint:prettier": "prettier-check \"src/**/*.{css,html,js,json,less,md,mdx,scss,ts,tsx}\"", 113 | "format": "npm run format:prettier && npm run format:eslint", 114 | "format:eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx --fix ./", 115 | "format:prettier": "prettier --write \"./**/*.{css,html,js,json,less,md,mdx,scss,ts,tsx}\"", 116 | "storybook:start": "start-storybook -p 9001 -c .storybook", 117 | "storybook": "npm run test:generate-output && npm run storybook:start" 118 | }, 119 | "keywords": [ 120 | "reactjs", 121 | "table", 122 | "virtualized", 123 | "customization", 124 | "performance" 125 | ], 126 | "author": "Decathlon", 127 | "license": "Apache-2.0", 128 | "husky": { 129 | "hooks": { 130 | "pre-commit": "npm test && npm run lint" 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/components/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_ROW_HEIGHT = 56; 2 | 3 | export const DEFAULT_COLUMN_WIDTH = "auto"; 4 | 5 | export const DEFAULT_COLSPAN = 1; 6 | 7 | export const MIN_COLUMN_WIDTH = 100; 8 | 9 | export const SUBCELL_PADDING = 30; 10 | 11 | // must have the same value as max-row-level in style/table/_row.scss 12 | export const MAX_ROW_LEVEL = 2; 13 | 14 | export const ROW_SPAN_WIDTH = 110; 15 | 16 | export enum MouseClickButtons { 17 | right = "right", 18 | left = "left", 19 | } 20 | 21 | export const RowHeight = { 22 | small: 60, 23 | medium: 80, 24 | large: 100, 25 | }; 26 | 27 | export const ColumnWidth = { 28 | small: 90, 29 | medium: 160, 30 | large: 230, 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ResponsiveContainer } from "./responsive-container"; 2 | export * from "./responsive-container"; 3 | 4 | export { default as Scroller } from "./scroller"; 5 | export * from "./scroller"; 6 | 7 | export { default as Virtualizer } from "./virtualizer"; 8 | export * from "./virtualizer"; 9 | 10 | export * from "./styled-table"; 11 | export * from "./table"; 12 | export * from "./table-interactions-manager"; 13 | export * from "./table-selection"; 14 | export * from "./utils"; 15 | export * from "./constants"; 16 | export * from "./typing"; 17 | -------------------------------------------------------------------------------- /src/components/responsive-container.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import ResizeObserver from "react-resize-detector"; 3 | 4 | interface IElementSize { 5 | width: number; 6 | height: number; 7 | } 8 | 9 | export interface IResponsiveContainerOptionalProps { 10 | className?: string; 11 | } 12 | 13 | export interface IResponsiveContainerProps extends IResponsiveContainerOptionalProps { 14 | children: (size: IElementSize) => JSX.Element; 15 | } 16 | 17 | const ResponsiveContainer = ({ className, children }: IResponsiveContainerProps): JSX.Element => { 18 | return ( 19 | 20 | {({ width, height }) => { 21 | return ( 22 |
23 | {width && height ? children({ width, height }) : null} 24 |
25 | ); 26 | }} 27 |
28 | ); 29 | }; 30 | 31 | export default ResponsiveContainer; 32 | -------------------------------------------------------------------------------- /src/components/styled-table/bubble.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import classNames from "classnames"; 4 | 5 | export enum BubbleType { 6 | info = "info", 7 | success = "success", 8 | warning = "warning", 9 | error = "error", 10 | } 11 | 12 | export interface IBubbleProps { 13 | className?: string; 14 | badge?: string; 15 | type?: BubbleType; 16 | children?: JSX.Element; 17 | } 18 | 19 | const Bubble = ({ className, badge, type, children }: IBubbleProps) => ( 20 |
21 | {children} 22 | 23 | 24 | {badge ? ( 25 | <> 26 | 27 | 28 | {badge} 29 | 30 | 31 | ) : null} 32 | 33 |
34 | ); 35 | 36 | Bubble.defaultProps = { 37 | type: BubbleType.info, 38 | }; 39 | 40 | export default React.memo(Bubble); 41 | -------------------------------------------------------------------------------- /src/components/styled-table/cell-with-icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Tooltip, IconButton, Icon } from "@mui/material"; 3 | import classnames from "classnames"; 4 | 5 | import { IContentCellProps } from "../table/cell"; 6 | 7 | export interface ICellWithIconProps extends IContentCellProps { 8 | /** The material icon name for the icon button component. */ 9 | iconName: string; 10 | /** value of the cell */ 11 | value: string; 12 | /** The CSS class name of the button. */ 13 | className?: string; 14 | /** The Tooltip title */ 15 | tooltipTitle?: string; 16 | /** Callback fired when a "click" event is detected. */ 17 | onClick?: (event: React.MouseEvent) => void; 18 | } 19 | 20 | export const CellWithIcon: React.FunctionComponent = ({ 21 | id, 22 | value, 23 | tooltipTitle, 24 | iconName, 25 | className, 26 | onClick, 27 | }) => { 28 | const icon = {iconName}; 29 | const action = onClick ? ( 30 | 37 | {icon} 38 | 39 | ) : ( 40 | icon 41 | ); 42 | 43 | return ( 44 |
45 |
46 | {value} 47 |
48 | {tooltipTitle ? {action} : action} 49 |
50 | ); 51 | }; 52 | 53 | export default CellWithIcon; 54 | -------------------------------------------------------------------------------- /src/components/styled-table/header-cell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import classNames from "classnames"; 3 | 4 | import Bubble from "./bubble"; 5 | 6 | export interface IHeaderCellProps { 7 | title: string; 8 | value: string; 9 | className?: string; 10 | badge?: string; 11 | isCurrent?: boolean; 12 | } 13 | 14 | const HeaderCell = ({ title, value, className = "", badge, isCurrent }: IHeaderCellProps) => { 15 | const cellContent = ( 16 |
17 |
{title}
18 |
19 | {value} 20 |
21 |
22 | ); 23 | 24 | return ( 25 |
30 | {isCurrent ? ( 31 | <> 32 |
33 | 34 | {cellContent} 35 | 36 | 37 | ) : ( 38 | cellContent 39 | )} 40 |
41 | ); 42 | }; 43 | 44 | export default React.memo(HeaderCell); 45 | -------------------------------------------------------------------------------- /src/components/styled-table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Bubble } from "./bubble"; 2 | export * from "./bubble"; 3 | 4 | export { default as CellWithIcon } from "./cell-with-icon"; 5 | export * from "./cell-with-icon"; 6 | 7 | export { default as HeaderCell } from "./header-cell"; 8 | export * from "./header-cell"; 9 | 10 | export { default as EditableCell } from "./editable-cell"; 11 | export * from "./editable-cell"; 12 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/actions-types.ts: -------------------------------------------------------------------------------- 1 | export const UPDATE_CELL_WIDTH = "UPDATE_CELL_WIDTH"; 2 | export type UPDATE_CELL_WIDTH = typeof UPDATE_CELL_WIDTH; 3 | 4 | export const UPDATE_ROW_HEIGHT = "UPDATE_ROW_HEIGHT"; 5 | export type UPDATE_ROW_HEIGHT = typeof UPDATE_ROW_HEIGHT; 6 | 7 | export const UPDATE_HIDDEN_COLUMNS_INDEXES = "UPDATE_HIDDEN_COLUMNS_INDEXES"; 8 | export type UPDATE_HIDDEN_COLUMNS_INDEXES = typeof UPDATE_HIDDEN_COLUMNS_INDEXES; 9 | 10 | export const UPDATE_FIXED_COLUMNS_INDEXES = "UPDATE_FIXED_COLUMNS_INDEXES"; 11 | export type UPDATE_FIXED_COLUMNS_INDEXES = typeof UPDATE_FIXED_COLUMNS_INDEXES; 12 | 13 | export const UPDATE_FIXED_ROWS_INDEXES = "UPDATE_FIXED_ROWS_INDEXES"; 14 | export type UPDATE_FIXED_ROWS_INDEXES = typeof UPDATE_FIXED_ROWS_INDEXES; 15 | 16 | export const UPDATE_COLUMNS_CURSOR = "UPDATE_COLUMNS_CURSOR"; 17 | export type UPDATE_COLUMNS_CURSOR = typeof UPDATE_COLUMNS_CURSOR; 18 | 19 | export const UPDATE_HIDDEN_ROW_INDEXES = "UPDATE_HIDDEN_ROW_INDEXES"; 20 | export type UPDATE_HIDDEN_ROW_INDEXES = typeof UPDATE_HIDDEN_ROW_INDEXES; 21 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/actions.ts: -------------------------------------------------------------------------------- 1 | import * as actionTypes from "./actions-types"; 2 | import { CellValue, CellDimension } from "./reducers"; 3 | 4 | export interface UpdateCellWidth { 5 | type: actionTypes.UPDATE_CELL_WIDTH; 6 | value: CellDimension; 7 | } 8 | 9 | export const updateCellWidth = (value: CellDimension): UpdateCellWidth => ({ 10 | type: actionTypes.UPDATE_CELL_WIDTH, 11 | value, 12 | }); 13 | 14 | export interface UpdateRowHeight { 15 | type: actionTypes.UPDATE_ROW_HEIGHT; 16 | value: CellDimension; 17 | } 18 | 19 | export const updateRowHeight = (value: CellDimension): UpdateRowHeight => ({ 20 | type: actionTypes.UPDATE_ROW_HEIGHT, 21 | value, 22 | }); 23 | 24 | export interface UpdateHiddenColumns { 25 | type: actionTypes.UPDATE_HIDDEN_COLUMNS_INDEXES; 26 | hiddenColumnsIds: string[]; 27 | } 28 | 29 | export const updateHiddenColumns = (hiddenColumnsIds: string[]): UpdateHiddenColumns => ({ 30 | type: actionTypes.UPDATE_HIDDEN_COLUMNS_INDEXES, 31 | hiddenColumnsIds, 32 | }); 33 | 34 | export interface UpdateHiddenRows { 35 | type: actionTypes.UPDATE_HIDDEN_ROW_INDEXES; 36 | hiddenRowIndexes: number[]; 37 | } 38 | 39 | export const updateHiddenRows = (hiddenRowIndexes: number[]): UpdateHiddenRows => ({ 40 | type: actionTypes.UPDATE_HIDDEN_ROW_INDEXES, 41 | hiddenRowIndexes, 42 | }); 43 | 44 | export interface UpdateFixedColumns { 45 | type: actionTypes.UPDATE_FIXED_COLUMNS_INDEXES; 46 | fixedColumnsIds: string[]; 47 | } 48 | 49 | export const updateFixedColumns = (fixedColumnsIds: string[]): UpdateFixedColumns => ({ 50 | type: actionTypes.UPDATE_FIXED_COLUMNS_INDEXES, 51 | fixedColumnsIds, 52 | }); 53 | 54 | export interface UpdateFixedRows { 55 | type: actionTypes.UPDATE_FIXED_ROWS_INDEXES; 56 | fixedRowsIndexes: number[]; 57 | } 58 | 59 | export const updateFixedRows = (fixedRowsIndexes: number[]): UpdateFixedRows => ({ 60 | type: actionTypes.UPDATE_FIXED_ROWS_INDEXES, 61 | fixedRowsIndexes, 62 | }); 63 | 64 | export interface UpdateColumnsCursor { 65 | type: actionTypes.UPDATE_COLUMNS_CURSOR; 66 | columnsCursor: CellValue; 67 | } 68 | 69 | export const updateColumnsCursor = (columnsCursor: CellValue): UpdateColumnsCursor => ({ 70 | type: actionTypes.UPDATE_COLUMNS_CURSOR, 71 | columnsCursor, 72 | }); 73 | 74 | export type TableInteractionsAction = 75 | | UpdateCellWidth 76 | | UpdateRowHeight 77 | | UpdateHiddenColumns 78 | | UpdateHiddenRows 79 | | UpdateColumnsCursor 80 | | UpdateFixedColumns 81 | | UpdateFixedRows; 82 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/cell-dimensions-controller.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Menu from "@mui/material/Menu"; 3 | import Divider from "@mui/material/Divider"; 4 | import ListSubheader from "@mui/material/ListSubheader"; 5 | import { Icon, ListItemText, List, MenuItem, ListItemIcon } from "@mui/material"; 6 | 7 | import { TableInteractionsContext } from "./table-interactions-manager"; 8 | import { CellDimension } from "./reducers"; 9 | import { Nullable } from "../typing"; 10 | import { RowHeight, ColumnWidth } from "../constants"; 11 | 12 | export interface ICellDimensionControllerProps { 13 | /** Keys / values for row height */ 14 | rowHeightOptions?: Record; 15 | /** Keys / values for column width */ 16 | cellWidthOptions?: Record; 17 | /** The current cell width of the table. */ 18 | cellWidth: CellDimension; 19 | /** The current row Height of the table. */ 20 | rowHeight: CellDimension; 21 | /** The menu button activator renderer */ 22 | buttonRenderer: (toggleMenu: (event: React.MouseEvent) => void) => JSX.Element; 23 | /** The row height controler. Please see the CellDimensionController */ 24 | updateRowHeight: (size: CellDimension) => void; 25 | /** The cell width controler. Please see the CellDimensionController */ 26 | updateCellWidth: (size: CellDimension) => void; 27 | } 28 | 29 | export const DumbCellDimensionController: React.FunctionComponent = React.memo( 30 | ({ 31 | updateCellWidth, 32 | updateRowHeight, 33 | buttonRenderer, 34 | rowHeight, 35 | cellWidth, 36 | rowHeightOptions = RowHeight, 37 | cellWidthOptions = ColumnWidth, 38 | }) => { 39 | const [anchorEl, setAnchorEl] = React.useState>(null); 40 | const isOpen = Boolean(anchorEl); 41 | 42 | const onClose = () => { 43 | setAnchorEl(null); 44 | }; 45 | 46 | const toggleMenu = (event: React.MouseEvent) => { 47 | setAnchorEl(anchorEl ? null : event.currentTarget); 48 | }; 49 | 50 | const getColumnWidthUpdater = (size: string) => () => { 51 | updateCellWidth({ size, value: cellWidthOptions[size] }); 52 | }; 53 | 54 | const getRowHeightUpdater = (size: string) => () => { 55 | updateRowHeight({ size, value: rowHeightOptions[size] }); 56 | }; 57 | return ( 58 | <> 59 | {buttonRenderer(toggleMenu)} 60 | 69 | 70 | Columns 71 | {Object.keys(cellWidthOptions).map((size) => ( 72 | 77 | 78 | {cellWidth.size === size ? ( 79 | radio_button_checked 80 | ) : ( 81 | radio_button_unchecked 82 | )} 83 | 84 | 85 | 86 | 87 | ))} 88 | 89 | Rows 90 | {Object.keys(rowHeightOptions).map((size) => ( 91 | 92 | 93 | {rowHeight.size === size ? ( 94 | radio_button_checked 95 | ) : ( 96 | radio_button_unchecked 97 | )} 98 | 99 | 100 | 101 | ))} 102 | 103 | 104 | 105 | ); 106 | } 107 | ); 108 | 109 | const CellDimensionController: React.FunctionComponent< 110 | Pick 111 | > = (props) => { 112 | return ( 113 | 114 | {({ cellWidth, rowHeight, updateCellWidth, updateRowHeight }) => { 115 | return ( 116 | 123 | ); 124 | }} 125 | 126 | ); 127 | }; 128 | 129 | export default React.memo(CellDimensionController); 130 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/column-id-scroll-controller.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { IconButton, Icon } from "@mui/material"; 3 | import Select from "@mui/material/Select"; 4 | import MenuItem from "@mui/material/MenuItem"; 5 | 6 | import { TableInteractionsContext } from "./table-interactions-manager"; 7 | 8 | export interface IColumnLabel { 9 | id: string; 10 | label: string; 11 | } 12 | 13 | export interface IColumnIdScrollControllerProps { 14 | /** Scrollable columns to control */ 15 | columns: IColumnLabel[]; 16 | } 17 | 18 | export interface IDumbIColumnIdScrollControllerPropsProps extends IColumnIdScrollControllerProps { 19 | /** The current columns cursor of the table (current scroll). */ 20 | columnsCursorId?: string; 21 | /** The scroll controler (scrolling by column id). Please see the WeekScrollerController. */ 22 | goToColumnId: (columnId: string) => void; 23 | } 24 | 25 | export const DumbColumnIdScrollController: React.FunctionComponent = ({ 26 | columns, 27 | columnsCursorId, 28 | goToColumnId, 29 | }) => { 30 | const selectedColumnIndex = React.useMemo( 31 | () => columns.findIndex((column) => column.id === columnsCursorId), 32 | [columnsCursorId] 33 | ); 34 | 35 | const gotToColumn = (columnIndex: number) => { 36 | const column = columns[columnIndex]; 37 | if (column) { 38 | goToColumnId(column.id); 39 | } 40 | }; 41 | 42 | const onChange = (event: React.ChangeEvent<{ name?: string; value: number }>) => { 43 | gotToColumn(event.target.value); 44 | }; 45 | 46 | return ( 47 |
48 | gotToColumn(0)} 52 | size="large" 53 | > 54 | first_page 55 | 56 | 57 | gotToColumn(selectedColumnIndex - 1)} 61 | size="large" 62 | > 63 | chevron_left 64 | 65 | 66 | 82 | 83 | gotToColumn(selectedColumnIndex + 1)} 87 | size="large" 88 | > 89 | chevron_right 90 | 91 | 92 | gotToColumn(columns.length - 1)} 96 | size="large" 97 | > 98 | last_page 99 | 100 |
101 | ); 102 | }; 103 | 104 | const ColumnIdScrollController: React.FunctionComponent = (props) => { 105 | return ( 106 | 107 | {({ columnsCursor, goToColumnId }) => { 108 | return ( 109 | 114 | ); 115 | }} 116 | 117 | ); 118 | }; 119 | 120 | export default React.memo(ColumnIdScrollController); 121 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/column-visibility-controller.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Menu from "@mui/material/Menu"; 3 | import { List, ListItemText, Icon, MenuItem, ListItemIcon } from "@mui/material"; 4 | 5 | import { TableInteractionsContext } from "./table-interactions-manager"; 6 | import { Nullable } from "../typing"; 7 | 8 | interface Column { 9 | id: string; 10 | index: number; 11 | label: string; 12 | } 13 | 14 | interface IColumnVisibilityControllerProps { 15 | /** toggleable columns to control */ 16 | columns: Column[]; 17 | /** The menu button activator renderer */ 18 | buttonRenderer: (toggleMenu: (event: React.MouseEvent) => void) => JSX.Element; 19 | /** The column visibility handler, called when the column visibility has changed */ 20 | onColumnVisibilityChange?: (columnIndex: number, isVisible: boolean) => void; 21 | } 22 | 23 | interface IDumbColumnVisibilityControllerProps extends IColumnVisibilityControllerProps { 24 | /** The current hidden columns of the table (indexes). */ 25 | hiddenColumnsIds: string[]; 26 | /** The hidden columns controller. Please see the ColumnVisibilityController. */ 27 | updateHiddenIds: (hiddenIds: string[]) => void; 28 | } 29 | 30 | export const DumbColumnVisibilityController: React.FunctionComponent = React.memo( 31 | ({ buttonRenderer, columns, hiddenColumnsIds, updateHiddenIds, onColumnVisibilityChange }) => { 32 | const [anchorEl, setAnchorEl] = React.useState>(null); 33 | const isOpen = Boolean(anchorEl); 34 | 35 | const columnsIdsMapping: Record = React.useMemo( 36 | () => 37 | columns.reduce((mapping, column) => { 38 | mapping[column.id] = column.index; 39 | return mapping; 40 | }, {}), 41 | [columns] 42 | ); 43 | 44 | const onClose = () => { 45 | setAnchorEl(null); 46 | }; 47 | 48 | const toggleMenu = (event: React.MouseEvent) => { 49 | setAnchorEl(anchorEl ? null : event.currentTarget); 50 | }; 51 | 52 | const updateHiddenColumnsIds = (columnId: string) => { 53 | const newColumns = hiddenColumnsIds.filter((hiddenColumnId) => hiddenColumnId !== columnId); 54 | let isVisible = true; 55 | if (newColumns.length === hiddenColumnsIds.length) { 56 | newColumns.push(columnId); 57 | isVisible = false; 58 | } 59 | if (onColumnVisibilityChange) { 60 | onColumnVisibilityChange(columnsIdsMapping[columnId], isVisible); 61 | } 62 | updateHiddenIds(newColumns); 63 | }; 64 | 65 | return ( 66 | <> 67 | {buttonRenderer(toggleMenu)} 68 | 79 | 80 | {columns.length > 0 81 | ? columns.map((col, index) => { 82 | return ( 83 | { 87 | updateHiddenColumnsIds(col.id); 88 | }} 89 | > 90 | 91 | {hiddenColumnsIds.includes(col.id) ? ( 92 | visibility_off 93 | ) : ( 94 | visibility 95 | )} 96 | 97 | 98 | 99 | ); 100 | }) 101 | : null} 102 | 103 | 104 | 105 | ); 106 | } 107 | ); 108 | 109 | const ColumnVisibilityController: React.FunctionComponent = (props) => { 110 | return ( 111 | 112 | {({ hiddenColumnsIds, updateHiddenIds }) => { 113 | return ( 114 | 115 | ); 116 | }} 117 | 118 | ); 119 | }; 120 | 121 | export default React.memo(ColumnVisibilityController); 122 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/fixed-column-controller.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { TableInteractionsContext } from "./table-interactions-manager"; 4 | 5 | interface IFixedColumnChildrenProps { 6 | toggleFixedColumnId: (event: React.MouseEvent) => void; 7 | isFixed: boolean; 8 | } 9 | 10 | interface IFixedColumnControllerProps { 11 | columnId: string; 12 | children: (props: IFixedColumnChildrenProps) => JSX.Element; 13 | } 14 | 15 | interface IDumbFixedColumnControllerProps extends IFixedColumnControllerProps { 16 | /** The current fixed columns of the table (indexes). */ 17 | fixedColumnsIds: string[]; 18 | /** The fixed columns controller. Please see the FixedColumnController. */ 19 | updateFixedColumnsIds: (fixedIds: string[]) => void; 20 | } 21 | 22 | export const DumbFixedColumnController: React.FunctionComponent = React.memo( 23 | ({ children, columnId, updateFixedColumnsIds, fixedColumnsIds }) => { 24 | const isFixed = React.useMemo(() => { 25 | return fixedColumnsIds.includes(columnId); 26 | }, [columnId, fixedColumnsIds]); 27 | const toggleFixedColumnId = () => { 28 | const newColumns = fixedColumnsIds.filter((fixedColumnId) => fixedColumnId !== columnId); 29 | if (newColumns.length === fixedColumnsIds.length) { 30 | newColumns.push(columnId); 31 | } 32 | updateFixedColumnsIds(newColumns); 33 | }; 34 | 35 | return children({ toggleFixedColumnId, isFixed }); 36 | } 37 | ); 38 | 39 | const FixedColumnController: React.FunctionComponent = (props) => { 40 | const { fixedColumnsIds, updateFixedColumnsIds } = React.useContext(TableInteractionsContext); 41 | return ; 42 | }; 43 | 44 | export default React.memo(FixedColumnController); 45 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/fixed-row-controller.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { TableInteractionsContext } from "./table-interactions-manager"; 4 | 5 | interface IFixedRowChildrenProps { 6 | toggleFixedRowIndex: (event: React.MouseEvent) => void; 7 | isFixed: boolean; 8 | } 9 | 10 | interface IFixedRowControllerProps { 11 | rowIndex: number; 12 | children: (props: IFixedRowChildrenProps) => JSX.Element; 13 | } 14 | 15 | interface IDumbFixedRowControllerProps extends IFixedRowControllerProps { 16 | /** The current fixed Rows of the table (indexes). */ 17 | fixedRowsIndexes: number[]; 18 | /** The fixed Rows controller. Please see the FixedRowController. */ 19 | updateFixedRowsIndexes: (fixedRowsIndexes: number[]) => void; 20 | } 21 | 22 | export const DumbFixedRowController: React.FunctionComponent = React.memo( 23 | ({ children, rowIndex, updateFixedRowsIndexes, fixedRowsIndexes }) => { 24 | const isFixed = React.useMemo(() => { 25 | return fixedRowsIndexes.includes(rowIndex); 26 | }, [rowIndex, fixedRowsIndexes]); 27 | const toggleFixedRowIndex = () => { 28 | const newRows = fixedRowsIndexes.filter((fixedRowIndex) => fixedRowIndex !== rowIndex); 29 | if (newRows.length === fixedRowsIndexes.length) { 30 | newRows.push(rowIndex); 31 | } 32 | updateFixedRowsIndexes(newRows); 33 | }; 34 | 35 | return children({ toggleFixedRowIndex, isFixed }); 36 | } 37 | ); 38 | 39 | const FixedRowController: React.FunctionComponent = (props) => { 40 | const { fixedRowsIndexes, updateFixedRowsIndexes } = React.useContext(TableInteractionsContext); 41 | return ( 42 | 43 | ); 44 | }; 45 | 46 | export default React.memo(FixedRowController); 47 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CellDimensionController } from "./cell-dimensions-controller"; 2 | export * from "./cell-dimensions-controller"; 3 | 4 | export { default as ColumnVisibilityController } from "./column-visibility-controller"; 5 | export * from "./column-visibility-controller"; 6 | 7 | export { default as ColumnIdScrollController } from "./column-id-scroll-controller"; 8 | export * from "./column-id-scroll-controller"; 9 | 10 | export { default as FixedColumnController } from "./fixed-column-controller"; 11 | export * from "./fixed-column-controller"; 12 | 13 | export { default as FixedRowController } from "./fixed-row-controller"; 14 | export * from "./fixed-row-controller"; 15 | 16 | export { default as TableInteractionsManager } from "./table-interactions-manager"; 17 | export * from "./table-interactions-manager"; 18 | 19 | export { default as tableManagerReducer } from "./reducers"; 20 | export * from "./reducers"; 21 | -------------------------------------------------------------------------------- /src/components/table-interactions-manager/reducers.ts: -------------------------------------------------------------------------------- 1 | import { TableInteractionsAction } from "./actions"; 2 | import * as actionTypes from "./actions-types"; 3 | import { ColumnWidth, RowHeight } from "../constants"; 4 | import { Nullable } from "../typing"; 5 | 6 | export enum CellSize { 7 | small = "small", 8 | medium = "medium", 9 | large = "large", 10 | } 11 | 12 | export interface CellValue { 13 | id: string; 14 | index: number; 15 | } 16 | 17 | export interface CellDimension { 18 | value: number; 19 | size: string; 20 | } 21 | 22 | export interface ITableInteractionManagerState { 23 | /** The current cell width of the table. */ 24 | cellWidth: CellDimension; 25 | /** The current row Height of the table. */ 26 | rowHeight: CellDimension; 27 | /** The current hidden columns of the table (indexes). */ 28 | hiddenColumnsIds: string[]; 29 | /** The current hidden rows of the table (indexes). */ 30 | hiddenRowIndexes: number[]; 31 | /** The current fixed columns of the table (indexes). */ 32 | fixedColumnsIds: string[]; 33 | /** The current fixed rows of the table (indexes). */ 34 | fixedRowsIndexes: number[]; 35 | /** The current columns cursor of the table (current scroll). */ 36 | columnsCursor: Nullable; 37 | } 38 | 39 | export const initialState: ITableInteractionManagerState = { 40 | cellWidth: { value: ColumnWidth[CellSize.medium], size: CellSize.medium }, 41 | rowHeight: { value: RowHeight[CellSize.medium], size: CellSize.medium }, 42 | columnsCursor: null, 43 | hiddenColumnsIds: [], 44 | fixedColumnsIds: [], 45 | fixedRowsIndexes: [], 46 | hiddenRowIndexes: [], 47 | }; 48 | 49 | const tableManagerReducer = (state: ITableInteractionManagerState = initialState, action: TableInteractionsAction) => { 50 | switch (action.type) { 51 | case actionTypes.UPDATE_CELL_WIDTH: 52 | return { 53 | ...state, 54 | cellWidth: action.value, 55 | }; 56 | case actionTypes.UPDATE_ROW_HEIGHT: 57 | return { 58 | ...state, 59 | rowHeight: action.value, 60 | }; 61 | case actionTypes.UPDATE_HIDDEN_COLUMNS_INDEXES: 62 | return { 63 | ...state, 64 | hiddenColumnsIds: action.hiddenColumnsIds, 65 | }; 66 | case actionTypes.UPDATE_HIDDEN_ROW_INDEXES: 67 | return { 68 | ...state, 69 | hiddenRowIndexes: action.hiddenRowIndexes, 70 | }; 71 | case actionTypes.UPDATE_FIXED_COLUMNS_INDEXES: 72 | return { 73 | ...state, 74 | fixedColumnsIds: action.fixedColumnsIds, 75 | }; 76 | case actionTypes.UPDATE_FIXED_ROWS_INDEXES: 77 | return { 78 | ...state, 79 | fixedRowsIndexes: action.fixedRowsIndexes, 80 | }; 81 | case actionTypes.UPDATE_COLUMNS_CURSOR: 82 | return { 83 | ...state, 84 | columnsCursor: action.columnsCursor, 85 | }; 86 | default: 87 | return state; 88 | } 89 | }; 90 | 91 | export default tableManagerReducer; 92 | -------------------------------------------------------------------------------- /src/components/table-selection/context-menu-handler.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { ISelectedCells } from "./selection-handler"; 4 | import { ICellCoordinates } from "../table/cell"; 5 | import { Nullable } from "../typing"; 6 | 7 | export interface ISelectionContext { 8 | anchorEl: Nullable; 9 | contextCell: Nullable; 10 | } 11 | 12 | interface IChildrenProps { 13 | onContextMenu: (selectionContext: ISelectionContext) => void; 14 | } 15 | 16 | export interface IContextMenuHandlerProps { 17 | selectedCells: ISelectedCells; 18 | children: (props: IChildrenProps) => JSX.Element; 19 | menuComponent?: React.ComponentType; 20 | } 21 | 22 | const defaultSelectionContext = { anchorEl: null, contextCell: null }; 23 | 24 | const ContextMenuHandler: React.FunctionComponent = ({ 25 | children, 26 | selectedCells, 27 | menuComponent: MenuComponent, 28 | }) => { 29 | const [context, setContext] = React.useState(defaultSelectionContext); 30 | const { anchorEl } = context; 31 | const isMenuOpened = !!anchorEl; 32 | 33 | const closeMenu = () => { 34 | setContext(defaultSelectionContext); 35 | }; 36 | 37 | const menuProps = { 38 | isMenuOpened, 39 | selectedCells, 40 | selectionContext: context, 41 | closeMenu, 42 | }; 43 | return ( 44 | <> 45 | {children({ onContextMenu: setContext })} 46 | {MenuComponent ? : null} 47 | 48 | ); 49 | }; 50 | 51 | export default ContextMenuHandler; 52 | -------------------------------------------------------------------------------- /src/components/table-selection/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ContextMenuHandler } from "./context-menu-handler"; 2 | export * from "./context-menu-handler"; 3 | 4 | export { default as SelectionHandler } from "./selection-handler"; 5 | export * from "./selection-handler"; 6 | 7 | export { default as TableSelectionMenu } from "./table-selection-menu"; 8 | export * from "./table-selection-menu"; 9 | -------------------------------------------------------------------------------- /src/components/table-selection/selection-handler.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { MouseClickButtons } from "../constants"; 4 | import { ICellCoordinates } from "../table/cell"; 5 | import ContextMenuHandler, { ISelectionContext } from "./context-menu-handler"; 6 | import { Nullable } from "../typing"; 7 | 8 | export interface ISelection { 9 | /** callback when we are clicking on the cell */ 10 | onCellMouseDown?: (coordinates: ICellCoordinates, mouseClickButton: MouseClickButtons) => void; 11 | /** callback when we are hovering into the cell */ 12 | onCellMouseEnter?: (coordinates: ICellCoordinates) => void; 13 | /** callback when we release the mouse button above the cell */ 14 | onCellMouseUp?: () => void; 15 | /** on right click handler */ 16 | onCellContextMenu?: (selectionContext: ISelectionContext) => void; 17 | selectedCells: ISelectedCells; 18 | } 19 | 20 | export interface ISelectionHandlerOptionalProps { 21 | isDisabledVerticalSelection?: boolean; 22 | isDisabledHorizontalSelection?: boolean; 23 | menuComponent?: React.ComponentType; 24 | } 25 | 26 | export interface ISelectionHandlerProps extends ISelectionHandlerOptionalProps { 27 | children: (props: ISelection) => JSX.Element; 28 | } 29 | 30 | export interface ISelectedCells { 31 | [rowIndex: string]: number[]; 32 | } 33 | 34 | interface IState { 35 | selectedCells: ISelectedCells; 36 | } 37 | 38 | class SelectionHandler extends React.Component { 39 | private startingCell: Nullable = null; 40 | 41 | constructor(props: ISelectionHandlerProps) { 42 | super(props); 43 | this.state = { selectedCells: {} }; 44 | } 45 | 46 | private onCellMouseDown = (coordinates: ICellCoordinates, mouseClickButton: MouseClickButtons) => { 47 | const { selectedCells } = this.state; 48 | let newSelectedCells: ISelectedCells = selectedCells; 49 | const currentRow = selectedCells[coordinates.rowIndex]; 50 | const isSelected = currentRow && currentRow.includes(coordinates.cellIndex); 51 | const isRightClick = mouseClickButton === MouseClickButtons.right; 52 | const isLeftClick = mouseClickButton === MouseClickButtons.left; 53 | if (isLeftClick || (!isSelected && isRightClick)) { 54 | this.startingCell = coordinates; 55 | newSelectedCells = { [coordinates.rowIndex]: [coordinates.cellIndex] }; 56 | this.setState({ selectedCells: newSelectedCells }); 57 | } 58 | }; 59 | 60 | private onCellMouseEnter = (coordinates: ICellCoordinates) => { 61 | const { isDisabledVerticalSelection, isDisabledHorizontalSelection } = this.props; 62 | if (this.startingCell) { 63 | const { rowIndex, cellIndex } = this.startingCell; 64 | const selectedCells: ISelectedCells = {}; 65 | const rowStart = isDisabledVerticalSelection ? rowIndex : Math.min(rowIndex, coordinates.rowIndex); 66 | const rowEnd = isDisabledVerticalSelection ? rowIndex : Math.max(rowIndex, coordinates.rowIndex); 67 | const colStart = isDisabledHorizontalSelection ? cellIndex : Math.min(cellIndex, coordinates.cellIndex); 68 | const colEnd = isDisabledHorizontalSelection ? cellIndex : Math.max(cellIndex, coordinates.cellIndex); 69 | for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex += 1) { 70 | selectedCells[rowIndex] = []; 71 | for (let cellIndex = colStart; cellIndex <= colEnd; cellIndex += 1) { 72 | selectedCells[rowIndex].push(cellIndex); 73 | } 74 | } 75 | this.setState({ selectedCells }); 76 | } 77 | }; 78 | 79 | private onCellMouseUp = () => { 80 | this.startingCell = null; 81 | }; 82 | 83 | public render() { 84 | const { children, menuComponent } = this.props; 85 | const { selectedCells } = this.state; 86 | const { onCellMouseDown, onCellMouseEnter, onCellMouseUp } = this; 87 | return ( 88 |
89 | 90 | {({ onContextMenu }) => { 91 | return children({ 92 | onCellMouseDown, 93 | onCellMouseEnter, 94 | onCellMouseUp, 95 | onCellContextMenu: onContextMenu, 96 | selectedCells, 97 | }); 98 | }} 99 | 100 |
101 | ); 102 | } 103 | } 104 | 105 | export default SelectionHandler; 106 | -------------------------------------------------------------------------------- /src/components/table-selection/table-selection-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { MenuItem, Menu } from "@mui/material"; 3 | 4 | import { ISelectionContext } from "./context-menu-handler"; 5 | import { ISelectedCells } from "./selection-handler"; 6 | 7 | export interface IMenuProps { 8 | closeMenu: () => void; 9 | selectedCells: ISelectedCells; 10 | selectionContext: ISelectionContext; 11 | isMenuOpened: boolean; 12 | } 13 | 14 | export interface IActionMenuComponent { 15 | onClose: () => void; 16 | selectedCells: ISelectedCells; 17 | } 18 | 19 | export interface IMenuItemProps { 20 | onClick: () => void; 21 | selectedCells: ISelectedCells; 22 | } 23 | 24 | export interface IMenuAction { 25 | id: string; 26 | title: string; 27 | component: React.ComponentType; 28 | menuItem?: React.ComponentType; 29 | } 30 | 31 | export interface ITableSelectionMenuProps extends IMenuProps { 32 | actions: IMenuAction[]; 33 | } 34 | 35 | const DefaultMenuItem: React.FunctionComponent = ({ onClick, children }) => { 36 | return {children}; 37 | }; 38 | 39 | const TableSelectionMenu: React.FunctionComponent = ({ 40 | actions, 41 | isMenuOpened, 42 | closeMenu, 43 | selectedCells, 44 | selectionContext: { anchorEl }, 45 | }) => { 46 | const [activeActionId, setActiveActionId] = React.useState(""); 47 | 48 | const getMenuAction = (menuActionId: string) => () => { 49 | setActiveActionId(menuActionId); 50 | closeMenu(); 51 | }; 52 | 53 | const closeAction = () => { 54 | setActiveActionId(""); 55 | closeMenu(); 56 | }; 57 | const activeAction = actions.find((action) => action.id === activeActionId); 58 | const ActiveActionComponent = activeAction && activeAction.component; 59 | 60 | return ( 61 | <> 62 | {ActiveActionComponent ? : null} 63 | 71 | {actions.map((action) => { 72 | const MenuItemComponent = action.menuItem || DefaultMenuItem; 73 | return ( 74 | 75 | {action.title} 76 | 77 | ); 78 | })} 79 | 80 | 81 | ); 82 | }; 83 | 84 | export default TableSelectionMenu; 85 | -------------------------------------------------------------------------------- /src/components/table/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Table } from "./table"; 2 | export * from "./table"; 3 | 4 | export { default as Row } from "./row"; 5 | export * from "./row"; 6 | 7 | export { default as RowSpan } from "./row-span"; 8 | export * from "./row-span"; 9 | 10 | export { default as Cell } from "./cell"; 11 | export * from "./cell"; 12 | 13 | export { default as ElementaryTable } from "./elementary-table"; 14 | export * from "./elementary-table"; 15 | -------------------------------------------------------------------------------- /src/components/table/row-span.tsx: -------------------------------------------------------------------------------- 1 | import IconButton from "@mui/material/IconButton"; 2 | import { Icon } from "@mui/material"; 3 | 4 | import { ROW_SPAN_WIDTH } from "../constants"; 5 | 6 | export interface IRowSpan { 7 | /** Text being displayed when row-span is opened */ 8 | title?: string; 9 | /** The width of the cell */ 10 | width?: number; 11 | /** The width of the cell */ 12 | height?: number; 13 | /** the background color of the cell */ 14 | color?: string; 15 | } 16 | 17 | export interface IRowSpanProps extends IRowSpan { 18 | /** callback function called when butten is clicked on */ 19 | toggle: () => void; 20 | /** status of the component */ 21 | opened: boolean; 22 | /** Size of the rowSpan, determining the number of rows it will cover */ 23 | length: number; 24 | } 25 | 26 | const RowSpan = ({ toggle, opened, length, title, width, height, color }: IRowSpanProps) => { 27 | return ( 28 |
45 | ); 46 | }; 47 | 48 | RowSpan.defaultProps = { 49 | width: ROW_SPAN_WIDTH, 50 | color: "initial", 51 | }; 52 | 53 | export default RowSpan; 54 | -------------------------------------------------------------------------------- /src/components/typing.ts: -------------------------------------------------------------------------------- 1 | export type Nullable

= P | null; 2 | -------------------------------------------------------------------------------- /src/components/utils/common.ts: -------------------------------------------------------------------------------- 1 | const objectProto = Object.prototype; 2 | const { hasOwnProperty } = objectProto; 3 | 4 | export const isEmptyObj = (value: object) => { 5 | for (const key in value) { 6 | if (hasOwnProperty.call(value, key)) { 7 | return false; 8 | } 9 | } 10 | return true; 11 | }; 12 | 13 | export const getStringNumberWithoutTrailingZeros = (value: number, decimals: number): string => 14 | Number(value.toFixed(decimals)).toString(); 15 | -------------------------------------------------------------------------------- /src/components/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./table"; 2 | export * from "./table-selection"; 3 | export * from "./common"; 4 | 5 | export { default as shallowEqual } from "./shallowEqual"; 6 | -------------------------------------------------------------------------------- /src/components/utils/shallowEqual.ts: -------------------------------------------------------------------------------- 1 | const hasOwn = Object.prototype.hasOwnProperty; 2 | 3 | function is(x: any, y: any) { 4 | if (x === y) { 5 | return x !== 0 || y !== 0 || 1 / x === 1 / y; 6 | } 7 | return x !== x && y !== y; 8 | } 9 | 10 | export default function shallowEqual(objA: object, objB: object) { 11 | if (is(objA, objB)) return true; 12 | 13 | if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) { 14 | return false; 15 | } 16 | 17 | const keysA = Object.keys(objA); 18 | const keysB = Object.keys(objB); 19 | 20 | if (keysA.length !== keysB.length) return false; 21 | 22 | for (let i = 0; i < keysA.length; i++) { 23 | if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { 24 | return false; 25 | } 26 | } 27 | 28 | return true; 29 | } 30 | -------------------------------------------------------------------------------- /src/components/utils/table-selection.ts: -------------------------------------------------------------------------------- 1 | import { ISelectedCells } from "../table-selection/selection-handler"; 2 | import { ICellCoordinates, ICell } from "../table/cell"; 3 | 4 | export const isVerticalSelection = (selectedCells: ISelectedCells) => { 5 | const rowIds = Object.keys(selectedCells); 6 | return rowIds.length >= 1 && rowIds.some((rowId) => selectedCells[rowId].length === 1); 7 | }; 8 | 9 | export const isHorizontalSelection = (selectedCells: ISelectedCells) => { 10 | const rowIds = Object.keys(selectedCells); 11 | return rowIds.length === 1 && rowIds.some((rowId) => selectedCells[rowId].length === 1); 12 | }; 13 | 14 | export const isMultiDimensionSelection = (selectedCells: ISelectedCells) => { 15 | const rowIds = Object.keys(selectedCells); 16 | return rowIds.length > 1 && rowIds.some((rowId) => selectedCells[rowId].length > 1); 17 | }; 18 | 19 | export function getSelectedCellsProps( 20 | selectedCells: ISelectedCells, 21 | getCell: (cellCoordinates: ICellCoordinates) => ICell 22 | ) { 23 | return Object.keys(selectedCells).reduce[]>((result, rowIndex) => { 24 | result.push(...selectedCells[rowIndex].map((cellIndex) => getCell({ rowIndex: parseInt(rowIndex), cellIndex }))); 25 | return result; 26 | }, []); 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useComponent } from "./useComponent"; 2 | export * from "./useComponent"; 3 | -------------------------------------------------------------------------------- /src/hooks/useComponent.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface ComponentRef { 4 | current?: Component; 5 | } 6 | 7 | export type MutableRefComponent = (element: Component) => void; 8 | 9 | const useComponent = (): [MutableRefComponent, ComponentRef, () => void] => { 10 | const [component, setComponent] = React.useState({ current: undefined }); 11 | const componentRef: MutableRefComponent = React.useCallback((element: Component) => { 12 | setComponent({ current: element }); 13 | }, []); 14 | 15 | const [componentUpdated, setComponentUpdated] = React.useState(); 16 | 17 | const onComponentUpdate = React.useCallback(() => { 18 | setComponentUpdated(new Date()); 19 | }, [componentUpdated]); 20 | 21 | return [componentRef, component, onComponentUpdate]; 22 | }; 23 | 24 | export default useComponent; 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export * from "./hooks"; 3 | -------------------------------------------------------------------------------- /src/style/_responsive-container.scss: -------------------------------------------------------------------------------- 1 | .responsive-container { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/style/_scroller.scss: -------------------------------------------------------------------------------- 1 | .scroller-container { 2 | display: flex; 3 | flex: 1; 4 | flex-direction: column; 5 | text-align: center; 6 | position: relative; 7 | 8 | .scroller-content { 9 | position: sticky; 10 | left: 0; 11 | top: 0; 12 | z-index: 10; 13 | overflow: hidden; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/style/fonts/material-design-icons/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/material-design-icons/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /src/style/fonts/material-design-icons/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/material-design-icons/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /src/style/fonts/material-design-icons/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/material-design-icons/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /src/style/fonts/material-design-icons/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/material-design-icons/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /src/style/fonts/material-design-icons/material-icons.scss: -------------------------------------------------------------------------------- 1 | $material-icon-font-path: "fonts/material-design-icons" !default; 2 | 3 | @font-face { 4 | font-family: "Material Icons"; 5 | font-style: normal; 6 | font-weight: 400; 7 | src: url("#{$material-icon-font-path}/MaterialIcons-Regular.eot"); /* For IE6-8 */ 8 | src: local("Material Icons"), local("MaterialIcons-Regular"), 9 | url("#{$material-icon-font-path}/MaterialIcons-Regular.woff2") format("woff2"), 10 | url("#{$material-icon-font-path}/MaterialIcons-Regular.woff") format("woff"), 11 | url("#{$material-icon-font-path}/MaterialIcons-Regular.ttf") format("truetype"); 12 | } 13 | 14 | .material-icons { 15 | font-family: "Material Icons"; 16 | font-weight: normal; 17 | font-style: normal; 18 | font-size: 24px; /* Preferred icon size */ 19 | display: inline-block; 20 | line-height: 1; 21 | text-transform: none; 22 | letter-spacing: normal; 23 | word-wrap: normal; 24 | white-space: nowrap; 25 | direction: ltr; 26 | 27 | /* Support for all WebKit browsers. */ 28 | -webkit-font-smoothing: antialiased; 29 | /* Support for Safari and Chrome. */ 30 | text-rendering: optimizeLegibility; 31 | 32 | /* Support for Firefox. */ 33 | -moz-osx-font-smoothing: grayscale; 34 | 35 | /* Support for IE. */ 36 | font-feature-settings: "liga"; 37 | } 38 | -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/RobotoCondensed-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/RobotoCondensed-BoldItalic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/RobotoCondensed-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/RobotoCondensed-Italic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/RobotoCondensed-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/RobotoCondensed-Light.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/RobotoCondensed-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/RobotoCondensed-LightItalic.ttf -------------------------------------------------------------------------------- /src/style/fonts/roboto/RobotoCondensed-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decathlon/react-table/397be2793efab6d0cec12cf4fb73b16104133f54/src/style/fonts/roboto/RobotoCondensed-Regular.ttf -------------------------------------------------------------------------------- /src/style/index.scss: -------------------------------------------------------------------------------- 1 | @import "fonts/material-design-icons/material-icons"; 2 | @import "variable/index"; 3 | @import "responsive-container"; 4 | @import "styled-table/index"; 5 | @import "scroller"; 6 | @import "table/index"; 7 | @import "table-selection/index"; 8 | @import "table-interactions-manager/index"; 9 | 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | font-family: "Roboto Condensed", Helvetica, Arial, sans-serif; 14 | font-size: 14px; 15 | } 16 | 17 | * { 18 | box-sizing: border-box; 19 | outline: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/style/styled-table/_bubble.scss: -------------------------------------------------------------------------------- 1 | .bubble-container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | text-align: center; 7 | position: relative; 8 | height: 100%; 9 | width: 100%; 10 | 11 | .bubble-circle { 12 | pointer-events: none; 13 | position: absolute; 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | right: 0; 18 | 19 | .bubble-circle-main { 20 | stroke: $icon-grey-color; 21 | stroke-width: 1; 22 | fill: none; 23 | } 24 | 25 | .bubble-circle-content { 26 | fill: $icon-grey-color; 27 | } 28 | 29 | .bubble-circle-text-content { 30 | fill: white; 31 | text-anchor: middle; 32 | font-size: 85%; 33 | } 34 | 35 | &.error { 36 | .bubble-circle-main { 37 | stroke: $red-color; 38 | } 39 | 40 | .bubble-circle-content { 41 | fill: $red-color; 42 | } 43 | } 44 | 45 | &.success { 46 | .bubble-circle-main { 47 | stroke: $green-color; 48 | } 49 | 50 | .bubble-circle-content { 51 | fill: $green-color; 52 | } 53 | } 54 | 55 | &.info { 56 | .bubble-circle-main { 57 | stroke: $blue-color; 58 | } 59 | 60 | .bubble-circle-content { 61 | fill: $blue-color; 62 | } 63 | } 64 | 65 | &.warning { 66 | .bubble-circle-main { 67 | stroke: $orange-color; 68 | } 69 | 70 | .bubble-circle-content { 71 | fill: $orange-color; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/style/styled-table/_cell-with-icon.scss: -------------------------------------------------------------------------------- 1 | .cell-with-icon { 2 | width: 100%; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | 7 | &__value { 8 | overflow: hidden; 9 | text-overflow: ellipsis; 10 | } 11 | 12 | &__icon { 13 | color: $icon-grey-color; 14 | font-size: medium !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/style/styled-table/_editable-cell.scss: -------------------------------------------------------------------------------- 1 | .editable-cell { 2 | display: flex; 3 | justify-content: center; 4 | min-width: 100%; 5 | &:hover ::before { 6 | border-bottom-width: 2px !important; 7 | } 8 | &__underline { 9 | &::after { 10 | border-bottom: 2px solid $mint-color !important; 11 | } 12 | } 13 | &.error { 14 | .editable-cell__underline { 15 | &::after { 16 | border-bottom: 2px solid $red-color !important; 17 | } 18 | } 19 | } 20 | 21 | &__value { 22 | position: relative; 23 | display: flex; 24 | align-items: center; 25 | max-width: 100%; 26 | min-width: 20px; 27 | &.edited-cell { 28 | .text { 29 | &:before { 30 | border-bottom-width: 2px; 31 | } 32 | } 33 | } 34 | .text { 35 | flex: 1; 36 | overflow: hidden; 37 | text-overflow: ellipsis; 38 | text-align: center; 39 | 40 | &:before { 41 | left: 0; 42 | right: 0; 43 | bottom: -5px; 44 | content: ""; 45 | position: absolute; 46 | border-bottom: 1px solid $mint-color; 47 | } 48 | } 49 | } 50 | 51 | &.empty { 52 | cursor: text; 53 | } 54 | } 55 | 56 | .edited-cell { 57 | color: $mint-color; 58 | font-weight: bold !important; 59 | } 60 | -------------------------------------------------------------------------------- /src/style/styled-table/_header-cell.scss: -------------------------------------------------------------------------------- 1 | .header-cell { 2 | margin-top: 12px; 3 | height: 90%; 4 | width: 100%; 5 | 6 | &:not(.basic) { 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | text-align: center; 12 | } 13 | 14 | .header-cell-data { 15 | z-index: 1; 16 | display: flex; 17 | flex-direction: column; 18 | align-items: baseline; 19 | color: $dark-black-color; 20 | max-width: 100%; 21 | 22 | .header-cell-title { 23 | line-height: 12px; 24 | } 25 | 26 | .header-cell-value { 27 | font-weight: 500; 28 | font-size: 1.8em; 29 | line-height: 25px; 30 | width: 100%; 31 | white-space: nowrap; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | text-align: start; 35 | } 36 | } 37 | 38 | .header-cell-line { 39 | position: absolute; 40 | height: 100%; 41 | width: 1px; 42 | background-color: $white-grey-color; 43 | } 44 | 45 | .header-cell-bubble { 46 | .bubble-circle-main { 47 | stroke: $white-grey-color !important; 48 | fill: $white-color !important; 49 | } 50 | 51 | .bubble-circle-content { 52 | fill: $white-grey-color !important; 53 | } 54 | } 55 | 56 | &.header-cell-current { 57 | margin-top: 0; 58 | .header-cell-value { 59 | border-bottom: solid 3px $mint-color; 60 | } 61 | } 62 | } 63 | 64 | .small-table { 65 | .header-cell.header-cell-current { 66 | margin-top: 15px; 67 | .header-cell-bubble, 68 | .header-cell-line { 69 | display: none; 70 | } 71 | } 72 | 73 | &.small-header { 74 | .header-cell .header-cell-data .header-cell-value { 75 | font-weight: 600; 76 | font-size: 1.2em; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/style/styled-table/index.scss: -------------------------------------------------------------------------------- 1 | @import "cell-with-icon"; 2 | @import "editable-cell"; 3 | @import "header-cell"; 4 | @import "bubble"; 5 | -------------------------------------------------------------------------------- /src/style/table-interactions-manager/_column-id-scroll-controller.scss: -------------------------------------------------------------------------------- 1 | .scroll-controlller { 2 | display: flex; 3 | justify-content: center; 4 | align-content: center; 5 | 6 | &__field { 7 | width: 275px !important; 8 | top: -4px; 9 | } 10 | 11 | &__button { 12 | padding: 0 !important; 13 | height: 24px !important; 14 | width: 24px !important; 15 | } 16 | } 17 | 18 | #column-id-scroll-controller > .MuiMenu-paper { 19 | max-height: 512px; 20 | margin-top: 32px; 21 | } 22 | -------------------------------------------------------------------------------- /src/style/table-interactions-manager/_column-visibility-controller.scss: -------------------------------------------------------------------------------- 1 | .visibility-controller-button { 2 | margin-top: 64px; 3 | } 4 | -------------------------------------------------------------------------------- /src/style/table-interactions-manager/_common.scss: -------------------------------------------------------------------------------- 1 | .table-interaction-menu { 2 | min-width: 150px; 3 | max-width: 360px; 4 | } 5 | -------------------------------------------------------------------------------- /src/style/table-interactions-manager/index.scss: -------------------------------------------------------------------------------- 1 | @import "column-id-scroll-controller"; 2 | @import "column-visibility-controller"; 3 | @import "common"; 4 | -------------------------------------------------------------------------------- /src/style/table-selection/_selection-handler.scss: -------------------------------------------------------------------------------- 1 | .selection-handler-container { 2 | *::selection { 3 | background-color: transparent; 4 | color: inherit; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/style/table-selection/_selection-menu.scss: -------------------------------------------------------------------------------- 1 | .selection-menu-icon { 2 | min-width: 35px !important; 3 | 4 | .text-icon { 5 | font-size: 18px; 6 | font-weight: 800; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/style/table-selection/index.scss: -------------------------------------------------------------------------------- 1 | @import "selection-handler"; 2 | @import "selection-menu"; 3 | -------------------------------------------------------------------------------- /src/style/table/_cell.scss: -------------------------------------------------------------------------------- 1 | .table-root { 2 | width: 100%; 3 | display: table; 4 | border-spacing: 0; 5 | border-collapse: collapse; 6 | 7 | .table-cell { 8 | &-container { 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | } 14 | .table-overflow-wrapper { 15 | overflow: hidden; 16 | padding-left: 24px; 17 | padding-right: 24px; 18 | white-space: nowrap; 19 | vertical-align: middle; 20 | box-shadow: inset 0 1px 0 0 rgba(236, 236, 236, 0.5); 21 | border-width: 0px !important; 22 | min-width: 100%; 23 | } 24 | 25 | .table-cell-sub-item-toggle { 26 | margin-left: -20px !important; 27 | } 28 | 29 | .table-cell-container { 30 | height: 100%; 31 | position: relative; 32 | } 33 | 34 | .table-column { 35 | font-size: 0.875rem; 36 | text-align: left; 37 | font-weight: 400; 38 | line-height: 1.43; 39 | vertical-align: inherit; 40 | padding: 4px 16px 4px 16px; 41 | box-sizing: border-box; 42 | outline: none; 43 | position: relative; 44 | 45 | &.disabled { 46 | background-color: rgba(195, 195, 195, 0.17); 47 | } 48 | 49 | &.selected { 50 | background-color: rgba(2, 171, 255, 0.1) !important; 51 | // gets rid of the blue selection overlay, but prevents value selection, which may suck 52 | // -webkit-touch-callout: none; 53 | // -webkit-user-select: none; 54 | // -khtml-user-select: none; 55 | // -moz-user-select: none; 56 | // -ms-user-select: none; 57 | // user-select: none; 58 | } 59 | &.elevated-start + .table-column { 60 | .table-overflow-wrapper { 61 | position: relative; 62 | &::after { 63 | content: ""; 64 | border: solid 1px transparent; 65 | box-shadow: 6px 0px 9px 1px rgba(0, 0, 0, 0.1); 66 | position: absolute; 67 | height: calc(100% + 5px); 68 | top: 0; 69 | right: calc(100% + 5px); 70 | } 71 | } 72 | } 73 | &.elevated-end { 74 | .table-overflow-wrapper { 75 | position: relative; 76 | &::before { 77 | content: ""; 78 | border: solid 1px transparent; 79 | box-shadow: 0px 0 9px 1px rgba(0, 0, 0, 0.1); 80 | position: absolute; 81 | height: calc(100% + 5px); 82 | top: 0; 83 | right: -5px; 84 | width: 3px; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | .cell-value { 92 | overflow: hidden; 93 | text-overflow: ellipsis; 94 | 95 | .cell-skeleton-container { 96 | display: flex; 97 | width: 100%; 98 | height: 100%; 99 | justify-content: center; 100 | align-items: center; 101 | } 102 | } 103 | 104 | th { 105 | .cell-value { 106 | width: 100%; 107 | white-space: nowrap; 108 | text-align: start; 109 | 110 | .cell-skeleton-container { 111 | justify-content: start; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/style/table/_row-span.scss: -------------------------------------------------------------------------------- 1 | .row-span-column { 2 | padding-top: 0 !important; 3 | padding-bottom: 0 !important; 4 | .row-span-container { 5 | display: flex; 6 | align-items: center; 7 | 8 | &.open { 9 | flex-direction: column; 10 | } 11 | 12 | .row-span-text { 13 | text-transform: uppercase; 14 | &.vertical { 15 | display: inline-table; 16 | writing-mode: vertical-lr; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/style/table/_row.scss: -------------------------------------------------------------------------------- 1 | // must have the same value as MAX_ROW_LEVEL in constants.ts 2 | $max-row-level: 2; 3 | $max-row-level-1: $max-row-level - 1; 4 | $bg-color: $light-grey-color; 5 | 6 | // Ideally we'd want to color the odd ones, but we need a thead/tbody separation 7 | .table-row:nth-of-type(even) { 8 | background-color: rgba(193, 193, 193, 0.05); 9 | } 10 | 11 | .table-row { 12 | color: inherit; 13 | outline: 0; 14 | vertical-align: middle; 15 | &.head { 16 | .table-column { 17 | color: rgba(0, 0, 0, 0.54); 18 | font-size: 0.75rem; 19 | font-weight: 500; 20 | line-height: 1.3125rem; 21 | } 22 | } 23 | &.elevated-start { 24 | box-shadow: 0px 3px 7px -1px rgba(113, 113, 113, 0.1); 25 | } 26 | &.elevated-end { 27 | box-shadow: 0px -3px 7px -1px rgba(113, 113, 113, 0.1); 28 | } 29 | 30 | &.last-sub-row { 31 | .table-column:not(.row-span-column):first-child::after { 32 | content: ""; 33 | width: 10px; 34 | height: 10px; 35 | border-radius: 100%; 36 | position: absolute; 37 | top: calc(15px + 100% / 2); 38 | background-color: $bg-color; 39 | right: calc(100% - 35px); 40 | box-shadow: 1px 0px 3px 0px rgba(0, 0, 0, 0.5); 41 | } 42 | } 43 | 44 | @for $level from 2 through $max-row-level { 45 | $padding: ($level * 30)+5; 46 | &.sub-row__#{$level} { 47 | .table-column:first-child::after { 48 | right: calc(100% - #{$padding}px) !important; 49 | } 50 | } 51 | } 52 | 53 | &.opened { 54 | .table-column.row-span-column + .table-column::after, 55 | .table-column:not(.row-span-column):first-child::after { 56 | content: ""; 57 | width: 10px; 58 | height: 10px; 59 | border-radius: 100%; 60 | position: absolute; 61 | top: calc(15px + 100% / 2); 62 | background-color: $bg-color; 63 | right: calc(100% - 35px); 64 | box-shadow: 1px 0px 3px 0px rgba(0, 0, 0, 0.5); 65 | } 66 | @for $level from 1 through $max-row-level-1 { 67 | $padding: (($level + 1) * 30)+5; 68 | &.sub-row__#{$level} { 69 | .table-column.row-span-column + .table-column::after, 70 | .table-column:not(.row-span-column):first-child::after { 71 | right: calc(100% - #{$padding}px) !important; 72 | } 73 | } 74 | } 75 | } 76 | 77 | &.sub-row { 78 | .table-column:not(.row-span-column):first-child { 79 | &::before { 80 | content: ""; 81 | border-left: solid 1px $bg-color; 82 | position: absolute; 83 | height: 100%; 84 | top: calc(15px - 100% / 2); 85 | box-shadow: 1px 0px 3px 0px rgba(0, 0, 0, 0.5); 86 | } 87 | .table-cell-container { 88 | padding-left: 30px; 89 | } 90 | } 91 | 92 | @for $level from 1 through $max-row-level { 93 | $padding: ($level * 30); 94 | &__#{$level} { 95 | .table-column:not(.row-span-column):first-child { 96 | &::before { 97 | right: calc(100% - #{$padding}px) !important; 98 | } 99 | .table-cell-container { 100 | padding-left: #{$padding}px; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/style/table/index.scss: -------------------------------------------------------------------------------- 1 | @import "cell"; 2 | @import "row"; 3 | @import "row-span"; 4 | -------------------------------------------------------------------------------- /src/style/variable/_color.scss: -------------------------------------------------------------------------------- 1 | $blue-color: #0082c3; 2 | $dark-blue-color: #004876; 3 | $blue-light-color: #4eb0ed; 4 | $blue-white-color: #b5e6ff; 5 | $white-blue-color: #d1e8f4; 6 | 7 | $yellow-color: #ffc702; 8 | $orange-color: #e86430; 9 | $orange-light-color: #f18a41; 10 | $red-color: #e12617; 11 | $green-color: #00a453; 12 | $light-green-color: #62b608; 13 | $mint-color: #02c18d; 14 | 15 | $true-black-color: #1a171b; 16 | $dark-black-color: #242323; 17 | $wall-grey-color: #575d5e; 18 | $icon-grey-color: #737373; 19 | $dark-grey-color: #737373; 20 | $light-grey-color: #aaa; 21 | $white-grey-color: #ccc; 22 | $white-light-grey-color: #eee; 23 | $disabled-grey-color: rgba(0, 0, 0, 0.26); 24 | $white-light-darken: #fafafa; 25 | $white-color: #fff; 26 | $notif-red-color: #ff3131; 27 | 28 | $text-color: $dark-grey-color; 29 | $text-color-light: $light-grey-color; 30 | 31 | $link-color: $dark-grey-color; 32 | $link-hover-color: $blue-color; 33 | $link-icon-color: $dark-grey-color; 34 | $link-icon-hover-color: $blue-color; 35 | 36 | $line-color: $white-color; 37 | $line-hover-color: #d4d4d4; 38 | $odd-line-color: #e8e8e8; 39 | $odd-line-hover-color: $line-hover-color; 40 | 41 | $border-color: #e0e0e0; 42 | 43 | $rating-highest: #f44336; 44 | $rating-higher: #ef9a9a; 45 | $rating-high: #fff176; 46 | $rating-normal: white; 47 | $rating-low: #aed581; 48 | -------------------------------------------------------------------------------- /src/style/variable/_font.scss: -------------------------------------------------------------------------------- 1 | $path-font-roboto: "fonts/roboto/"; 2 | 3 | @font-face { 4 | font-family: "Roboto"; 5 | src: url($path-font-roboto + "/Roboto-Thin.ttf") format("truetype"); 6 | font-weight: 100; 7 | } 8 | @font-face { 9 | font-family: "Roboto"; 10 | src: url($path-font-roboto + "/Roboto-ThinItalic.ttf") format("truetype"); 11 | font-weight: 100; 12 | font-style: italic; 13 | } 14 | @font-face { 15 | font-family: "Roboto"; 16 | src: url($path-font-roboto + "/Roboto-Light.ttf") format("truetype"); 17 | font-weight: 200; 18 | } 19 | @font-face { 20 | font-family: "Roboto"; 21 | src: url($path-font-roboto + "/Roboto-LightItalic.ttf") format("truetype"); 22 | font-weight: 200; 23 | font-style: italic; 24 | } 25 | @font-face { 26 | font-family: "Roboto"; 27 | src: url($path-font-roboto + "/Roboto-Regular.ttf") format("truetype"); 28 | font-weight: 400; 29 | } 30 | @font-face { 31 | font-family: "Roboto"; 32 | src: url($path-font-roboto + "/Roboto-Italic.ttf") format("truetype"); 33 | font-weight: 400; 34 | font-style: italic; 35 | } 36 | @font-face { 37 | font-family: "Roboto"; 38 | src: url($path-font-roboto + "/Roboto-Medium.ttf") format("truetype"); 39 | font-weight: 600; 40 | } 41 | @font-face { 42 | font-family: "Roboto"; 43 | src: url($path-font-roboto + "/Roboto-MediumItalic.ttf") format("truetype"); 44 | font-weight: 600; 45 | font-style: italic; 46 | } 47 | @font-face { 48 | font-family: "Roboto"; 49 | src: url($path-font-roboto + "/Roboto-Bold.ttf") format("truetype"); 50 | font-weight: 700; 51 | } 52 | @font-face { 53 | font-family: "Roboto"; 54 | src: url($path-font-roboto + "/Roboto-BoldItalic.ttf") format("truetype"); 55 | font-weight: 700; 56 | font-style: italic; 57 | } 58 | @font-face { 59 | font-family: "Roboto"; 60 | src: url($path-font-roboto + "/Roboto-Black.ttf") format("truetype"); 61 | font-weight: 900; 62 | } 63 | @font-face { 64 | font-family: "Roboto"; 65 | src: url($path-font-roboto + "/Roboto-BlackItalic.ttf") format("truetype"); 66 | font-weight: 900; 67 | font-style: italic; 68 | } 69 | 70 | @font-face { 71 | font-family: "Roboto Condensed"; 72 | src: url($path-font-roboto + "/RobotoCondensed-Light.ttf") format("truetype"); 73 | font-weight: 200; 74 | } 75 | @font-face { 76 | font-family: "Roboto Condensed"; 77 | src: url($path-font-roboto + "/RobotoCondensed-LightItalic.ttf") format("truetype"); 78 | font-weight: 200; 79 | font-style: italic; 80 | } 81 | @font-face { 82 | font-family: "Roboto Condensed"; 83 | src: url($path-font-roboto + "/RobotoCondensed-Regular.ttf") format("truetype"); 84 | font-weight: 400; 85 | } 86 | @font-face { 87 | font-family: "Roboto Condensed"; 88 | src: url($path-font-roboto + "/RobotoCondensed-Italic.ttf") format("truetype"); 89 | font-weight: 400; 90 | font-style: italic; 91 | } 92 | @font-face { 93 | font-family: "Roboto Condensed"; 94 | src: url($path-font-roboto + "/RobotoCondensed-Bold.ttf") format("truetype"); 95 | font-weight: 700; 96 | } 97 | @font-face { 98 | font-family: "Roboto Condensed"; 99 | src: url($path-font-roboto + "/RobotoCondensed-BoldItalic.ttf") format("truetype"); 100 | font-weight: 700; 101 | font-style: italic; 102 | } 103 | 104 | $default-font: "Roboto Condensed", Helvetica, Arial, sans-serif; 105 | -------------------------------------------------------------------------------- /src/style/variable/_global.scss: -------------------------------------------------------------------------------- 1 | $media: screen; 2 | $media_feature: min-width; 3 | 4 | $spacing: 10px; 5 | $spacing-lg: 25px; 6 | $spacing-lg-2x: 50px; 7 | $gutter-s: 2px; 8 | $gutter: 4px; 9 | $gutter-lg: 8px; 10 | 11 | $media_value_large_device: 600px; 12 | $media_value_medium_device: 430px; 13 | $media_value_small_device: 360px; 14 | 15 | $z-index-header: 1500 !important; 16 | 17 | $topbar-height: 55px; 18 | 19 | $topbar-height-small: 50px; 20 | 21 | $table-height: calc(100vh - 160px); // 160px = $topbar-height + $topbar-height + $topbar-height-small 22 | 23 | @mixin appear { 24 | @keyframes appear { 25 | 0% { 26 | opacity: 0; 27 | } 28 | 100% { 29 | opacity: 1; 30 | } 31 | } 32 | } 33 | 34 | @mixin appear-bottom { 35 | @keyframes appear-bottom { 36 | 0% { 37 | transform: translateY(20px); 38 | opacity: 0; 39 | } 40 | 100% { 41 | transform: translateY(0); 42 | opacity: 1; 43 | } 44 | } 45 | } 46 | 47 | @mixin appear-right { 48 | @keyframes appear-right { 49 | 0% { 50 | transform: translateX(20px); 51 | opacity: 0; 52 | } 53 | 100% { 54 | transform: translateX(0); 55 | opacity: 1; 56 | } 57 | } 58 | } 59 | 60 | @mixin appear-hor-expand { 61 | @keyframes appear-hor-expand { 62 | 0% { 63 | max-width: 0; 64 | } 65 | 100% { 66 | max-width: 100%; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/style/variable/_mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin flex-column($direction: column, $display-fix: block) { 2 | display: $display-fix; /* IE fix */ 3 | display: flex; 4 | flex-direction: $direction; 5 | } 6 | 7 | @mixin flex-grow-column($direction: column) { 8 | @include flex-column($direction); 9 | flex-grow: 1; /* Mozilla fix */ 10 | } 11 | 12 | @mixin overlay($position: fixed, $index: 100) { 13 | position: $position; 14 | z-index: $index; 15 | top: 0; 16 | left: 0; 17 | right: 0; 18 | bottom: 0; 19 | @include flex-column(); 20 | justify-content: center; 21 | background: rgba(138, 138, 138, 0.5); 22 | } 23 | -------------------------------------------------------------------------------- /src/style/variable/index.scss: -------------------------------------------------------------------------------- 1 | @import "color"; 2 | @import "font"; 3 | @import "global"; 4 | @import "mixin"; 5 | -------------------------------------------------------------------------------- /stories/components/responsive-cotainer.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import ResponsiveContainer from "../../src/components/responsive-container"; 5 | 6 | const minItemHeight = 100; 7 | 8 | const styles = { 9 | sizeContainer: { 10 | display: "flex", 11 | justifyContent: "center", 12 | fontSize: 30, 13 | fontWeight: 900, 14 | }, 15 | listContainer: { 16 | display: "flex", 17 | justifyContent: "center", 18 | flexDirection: "column", 19 | alignItems: "center", 20 | }, 21 | listItem: { 22 | width: "100%", 23 | textAlign: "center", 24 | verticalAlign: "middle", 25 | fontSize: 30, 26 | borderBottom: "solid 1px #d7d4d4", 27 | color: "gray", 28 | }, 29 | }; 30 | 31 | storiesOf("Responsive container", module) 32 | .addParameters({ jest: ["responsive-container"] }) 33 | .add("Default", () => ( 34 |

35 | 36 | {({ width, height }) => { 37 | return ( 38 |
39 |
40 | {width} X {height} 41 |
42 |
43 | ); 44 | }} 45 |
46 |
47 | )) 48 | .add("List", () => ( 49 |
50 | 51 | {({ height }) => { 52 | const items = Array.from(Array(Math.ceil(height / minItemHeight)), (_, index) => ({ 53 | id: index, 54 | title: `Item ${index}`, 55 | })); 56 | const itemHeight = height / items.length; 57 | return ; 58 | }} 59 | 60 |
61 | )); 62 | 63 | const List = ({ items, itemHeighet }) => { 64 | return ( 65 | // @ts-ignore 66 |
67 | {items.map((item) => ( 68 |
77 | {item.title} 78 |
79 | ))} 80 |
81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /stories/components/styled-table/bubble.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { storiesOf } from "@storybook/react"; 3 | import { text, select } from "@storybook/addon-knobs"; 4 | 5 | import { withThemeProvider } from "../../utils/decorators"; 6 | import Bubble, { BubbleType } from "../../../src/components/styled-table/bubble"; 7 | 8 | const BubbleTypeOptions = { 9 | info: BubbleType.info, 10 | success: BubbleType.success, 11 | warning: BubbleType.warning, 12 | error: BubbleType.error, 13 | }; 14 | 15 | storiesOf("Bubble", module) 16 | .addDecorator(withThemeProvider) 17 | .addParameters({ jest: ["bubble"] }) 18 | .add( 19 | "Default", 20 | () => ( 21 |
22 | 23 |
24 | ), 25 | { 26 | info: { inline: true }, 27 | } 28 | ) 29 | .add( 30 | "Without badge", 31 | () => ( 32 |
33 | 34 |
35 | ), 36 | { 37 | info: { inline: true }, 38 | } 39 | ) 40 | .add( 41 | "With content", 42 | () => ( 43 |
44 | 45 |
{text("content", "My content")}
46 |
47 |
48 | ), 49 | { 50 | info: { inline: true }, 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /stories/components/styled-table/cell-with-icon.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { storiesOf } from "@storybook/react"; 3 | import { number, object } from "@storybook/addon-knobs"; 4 | import { action } from "@storybook/addon-actions"; 5 | 6 | import { withThemeProvider } from "../../utils/decorators"; 7 | import CellWithIcon from "../../../src/components/styled-table/cell-with-icon"; 8 | import Table from "../../../src/components/table/table"; 9 | import { getTable } from "./tables"; 10 | 11 | const defaultProps = getTable({ 12 | 1: { 13 | 0: { 14 | value: "CA TTC OMNI", 15 | cellContent: CellWithIcon, 16 | cellContentProps: { 17 | iconName: "edit", 18 | }, 19 | }, 20 | }, 21 | 2: { 22 | 0: { 23 | value: "PROG CA TTC", 24 | cellContent: CellWithIcon, 25 | cellContentProps: { 26 | iconName: "edit", 27 | }, 28 | }, 29 | }, 30 | }); 31 | 32 | storiesOf("Styled Table/Cell with icon", module) 33 | .addDecorator(withThemeProvider) 34 | .add( 35 | "Default", 36 | () => ( 37 |
38 | 39 |
40 | ), 41 | { 42 | info: { inline: true }, 43 | } 44 | ) 45 | .add( 46 | "With action", 47 | () => ( 48 |
49 | action("onClick icon")("Click")} /> 50 |
51 | ), 52 | { 53 | info: { inline: true }, 54 | } 55 | ) 56 | .add( 57 | "With tooltip", 58 | () => ( 59 |
60 | action("onClick icon")("Click")} 64 | tooltipTitle="Hello Foo" 65 | /> 66 |
67 | ), 68 | { 69 | info: { inline: true }, 70 | } 71 | ) 72 | .add("Integrated", () => ( 73 |
38 |
39 |
{title}
40 | 41 | {opened ? "keyboard_arrow_down" : "keyboard_arrow_right"} 42 | 43 |
44 |
87 | )); 88 | -------------------------------------------------------------------------------- /stories/components/styled-table/editable-cell.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import * as React from "react"; 3 | import { storiesOf } from "@storybook/react"; 4 | import { number, object } from "@storybook/addon-knobs"; 5 | import { action } from "@storybook/addon-actions"; 6 | 7 | import { withThemeProvider } from "../../utils/decorators"; 8 | import EditableCell, { IMask } from "../../../src/components/styled-table/editable-cell"; 9 | import Table from "../../../src/components/table/table"; 10 | import { IContentCellProps } from "../../../src/components/table/cell"; 11 | import { getTable } from "./tables"; 12 | import { Nullable } from "../../../src/components/typing"; 13 | 14 | interface IProps extends IContentCellProps { 15 | defaultValue: number; 16 | alreadyEdited?: boolean; 17 | maxValue?: number; 18 | isDisabled?: boolean; 19 | } 20 | 21 | const mask: IMask = { 22 | is_percentage: false, 23 | is_negative: true, 24 | decimals: 2, 25 | }; 26 | 27 | export const formatValue = (value: Nullable, mask?: IMask) => 28 | mask && value === null 29 | ? "-" 30 | : new Intl.NumberFormat("fr-FR", { 31 | //@ts-ignore mask is defined 32 | style: mask.is_percentage ? "percent" : undefined, 33 | //@ts-ignore mask is defined 34 | maximumFractionDigits: mask.decimals, 35 | //@ts-ignore mask is defined 36 | minimumFractionDigits: mask.decimals, 37 | //@ts-ignore value is defined 38 | }).format(value); 39 | 40 | const EditableCellParent = (props: IProps) => { 41 | const { defaultValue, alreadyEdited, maxValue, isDisabled } = props; 42 | 43 | const [isEdited, setIsEdited] = React.useState(alreadyEdited || false); 44 | const [value, setValue] = React.useState>(defaultValue); 45 | 46 | const handleOnConfirmValue = (value: Nullable) => { 47 | action("onConfirmValue")(value); 48 | setValue(value); 49 | setIsEdited(defaultValue !== value); 50 | }; 51 | 52 | return ( 53 | ) => (value ? value <= maxValue : false) : undefined} 60 | onConfirmValue={handleOnConfirmValue} 61 | isDisabled={isDisabled} 62 | /> 63 | ); 64 | }; 65 | 66 | const defaultProps = getTable({ 67 | 1: { 68 | 1: { 69 | cellContent: EditableCellParent, 70 | cellContentProps: { 71 | defaultValue: null, 72 | }, 73 | }, 74 | }, 75 | }); 76 | 77 | storiesOf("Styled Table/editable cell", module) 78 | .addDecorator(withThemeProvider) 79 | .add( 80 | "Default", 81 | () => ( 82 |
83 | 84 |
85 | ), 86 | { 87 | info: { inline: true }, 88 | } 89 | ) 90 | .add( 91 | "Edited", 92 | () => ( 93 |
94 | 95 |
96 | ), 97 | { 98 | info: { inline: true }, 99 | } 100 | ) 101 | .add( 102 | "Disabled", 103 | () => ( 104 |
105 | 106 |
107 | ), 108 | { 109 | info: { inline: true }, 110 | } 111 | ) 112 | .add( 113 | "Error with invalid value", 114 | () => ( 115 |
116 | 117 |
118 | ), 119 | { 120 | info: { inline: true }, 121 | } 122 | ) 123 | .add( 124 | "Integrated", 125 | () => ( 126 |
140 | ), 141 | { 142 | info: { inline: true }, 143 | } 144 | ); 145 | -------------------------------------------------------------------------------- /stories/components/styled-table/header-cell.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { storiesOf } from "@storybook/react"; 3 | import { object, number, text, boolean } from "@storybook/addon-knobs"; 4 | 5 | import Table from "../../../src/components/table/table"; 6 | import { withThemeProvider } from "../../utils/decorators"; 7 | import HeaderCell from "../../../src/components/styled-table/header-cell"; 8 | import { getTable } from "./tables"; 9 | 10 | const defaultProps = getTable(); 11 | 12 | storiesOf("Styled Table/Header cell", module) 13 | .addDecorator(withThemeProvider) 14 | .addParameters({ jest: ["header-cell", "bubble"] }) 15 | .add( 16 | "Default", 17 | () => ( 18 |
19 | 20 |
21 | ), 22 | { 23 | info: { inline: true }, 24 | } 25 | ) 26 | .add( 27 | "Not current (playground)", 28 | () => ( 29 |
30 | 37 |
38 | ), 39 | { 40 | info: { inline: true }, 41 | } 42 | ) 43 | .add( 44 | "Integrated", 45 | () => ( 46 |
59 | ), 60 | { 61 | info: { inline: true }, 62 | } 63 | ); 64 | -------------------------------------------------------------------------------- /stories/components/styled-table/tables.ts: -------------------------------------------------------------------------------- 1 | import { IRow } from "../../../src/components/table/row"; 2 | import HeaderCell from "../../../src/components/styled-table/header-cell"; 3 | 4 | const indicatorNames = [ 5 | "CA_TTC_OMNICANAL", 6 | "PROG_CA_TTC", 7 | "MARGIN_RATE", 8 | "HOURS", 9 | "FEES_PROGRESSION", 10 | "TURNOVER", 11 | "RATE_SHRINKAGE", 12 | "PERSONAL_FEES", 13 | "SQUARE_METERS", 14 | "SURFACE_COST", 15 | "TOTAL_FEES", 16 | "CONTRIBUTION", 17 | "OVERHEADS", 18 | "ADVISED_TURNOVER", 19 | "CA_TTC_OMNICANALF", 20 | "PROG_CA_TTCF", 21 | "MARGIN_RATEF", 22 | "HOURSF", 23 | "FEES_PROGRESSIONF", 24 | "TURNOVERF", 25 | "RATE_SHRINKAGEF", 26 | "PERSONAL_FEESF", 27 | "SQUARE_METERSF", 28 | "SURFACE_COSTF", 29 | "TOTAL_FEESF", 30 | "CONTRIBUTIONF", 31 | "OVERHEADSF", 32 | "ADVISED_TURNOVERF", 33 | ]; 34 | 35 | const text = "Lorem Ipsum is that it has a more-or-less normal"; 36 | 37 | export function getTable(cells = {}) { 38 | const weeks = Array.from(Array(52), (_, rowIndex) => ({ 39 | id: rowIndex < 9 ? `0${rowIndex + 1}` : `${rowIndex + 1}`, 40 | value: `W${rowIndex + 1}${rowIndex === 3 ? text : ""}`, 41 | })); 42 | const headerRow: IRow = { 43 | id: "headers", 44 | isHeader: true, 45 | size: 126, 46 | cells: [ 47 | { 48 | id: "indicators", 49 | value: `Indicateurs ${text}`, 50 | }, 51 | ], 52 | }; 53 | weeks.forEach((week) => 54 | headerRow.cells.push({ 55 | id: week.id, 56 | value: week.value, 57 | cellContent: HeaderCell, 58 | cellContentProps: { 59 | title: "2019", 60 | isCurrent: week.value === "W12", 61 | subtitle: "1 éphéméride", 62 | value: week.value, 63 | }, 64 | }) 65 | ); 66 | const rows: IRow[] = [headerRow]; 67 | 68 | indicatorNames.forEach((indicatorName) => { 69 | const row: IRow = { 70 | id: indicatorName, 71 | cells: [ 72 | { 73 | id: indicatorName, 74 | value: indicatorName, 75 | }, 76 | ], 77 | }; 78 | weeks.forEach((week) => { 79 | row.cells.push({ 80 | id: `${indicatorName}-${week.value}`, 81 | value: "0.00", 82 | }); 83 | }); 84 | rows.push(row); 85 | }); 86 | 87 | Object.keys(cells).forEach((row) => { 88 | Object.keys(cells[row]).forEach((cell) => { 89 | rows[row].cells[cell] = cells[row][cell]; 90 | }); 91 | }); 92 | 93 | return { id: "styled-tabel", rows }; 94 | } 95 | -------------------------------------------------------------------------------- /stories/components/table-selection/table.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { storiesOf } from "@storybook/react"; 3 | import { object } from "@storybook/addon-knobs"; 4 | 5 | import Table from "../../../src/components/table/table"; 6 | import Row from "../../../src/components/table/row"; 7 | import { withThemeProvider } from "../../utils/decorators"; 8 | import SelectionHandler from "../../../src/components/table-selection/selection-handler"; 9 | import { SelectionMenu } from "../../stories-components/selection-menu"; 10 | import { getTable } from "../styled-table/tables"; 11 | 12 | const storyInfoDefault = { 13 | inline: true, 14 | propTables: [Row, SelectionHandler, Table], 15 | }; 16 | 17 | storiesOf("Table/Selection", module) 18 | .addDecorator(withThemeProvider) 19 | .addParameters({ 20 | jest: [ 21 | "row", 22 | "row-span", 23 | "row-it", 24 | "cell", 25 | "selection-handler", 26 | "elementary-table", 27 | "elementary-table-it", 28 | "selectable-table", 29 | "virtualized-table", 30 | "utils", 31 | ], 32 | }) 33 | .add( 34 | "Default", 35 | () => ( 36 |
49 | ), 50 | { 51 | info: storyInfoDefault, 52 | } 53 | ) 54 | .add( 55 | "Horizontal cell selection only", 56 | () => { 57 | return ( 58 |
74 | ); 75 | }, 76 | { 77 | info: storyInfoDefault, 78 | } 79 | ) 80 | .add( 81 | "Right click menu", 82 | () => { 83 | return ( 84 |
100 | ); 101 | }, 102 | { 103 | info: storyInfoDefault, 104 | } 105 | ); 106 | -------------------------------------------------------------------------------- /stories/components/table/table.md: -------------------------------------------------------------------------------- 1 | # Table 2 | 3 | Default table for your application. 4 | 5 | Able to handle customized content, different row/column size, and child rows. 6 | -------------------------------------------------------------------------------- /stories/components/table/table.stories.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /// 3 | import { storiesOf } from "@storybook/react"; 4 | import { object } from "@storybook/addon-knobs"; 5 | 6 | import Readme from "./table.md"; 7 | import Table from "../../../src/components/table/table"; 8 | import Row from "../../../src/components/table/row"; 9 | import { withThemeProvider } from "../../utils/decorators"; 10 | import { simpleTable, tableWithSubItems, subRows, subMiam, tableWithDifferentRowSizes } from "../../utils/tables"; 11 | import { CustomCellContent } from "../../stories-components/selection-menu"; 12 | 13 | const defaultProps = { 14 | id: "table-foo", 15 | rows: simpleTable({}), 16 | }; 17 | 18 | const table2Levels = tableWithSubItems({ firstSubRows: subRows({}) }); 19 | 20 | export const table3Levels = tableWithSubItems({ 21 | firstSubRows: subRows({ subsubRows: subMiam }), 22 | }); 23 | 24 | const storyInfoDefault = { 25 | inline: true, 26 | propTables: [Row, Table], 27 | }; 28 | 29 | storiesOf("Table/Default", module) 30 | .addDecorator(withThemeProvider) 31 | .addParameters({ 32 | jest: [ 33 | "row", 34 | "row-span", 35 | "row-it", 36 | "cell", 37 | "selection-handler", 38 | "elementary-table", 39 | "elementary-table-it", 40 | "selectable-table", 41 | "virtualized-table", 42 | "utils", 43 | ], 44 | }) 45 | .add("Default", () =>
, { 46 | notes: { markdown: Readme }, 47 | info: storyInfoDefault, 48 | }) 49 | .add( 50 | "With loading", 51 | () => ( 52 |
58 | ), 59 | { 60 | info: storyInfoDefault, 61 | } 62 | ) 63 | .add( 64 | "With cell custom content", 65 | () => ( 66 |
74 | ), 75 | { 76 | info: storyInfoDefault, 77 | } 78 | ) 79 | .add( 80 | "With custom row heights", 81 | () => ( 82 |
88 | ), 89 | { 90 | info: storyInfoDefault, 91 | } 92 | ) 93 | .add( 94 | "With columns props", 95 | () => ( 96 |
105 | ), 106 | { 107 | info: storyInfoDefault, 108 | } 109 | ) 110 | .add( 111 | "With sub-row", 112 | () => ( 113 |
119 | ), 120 | { 121 | info: storyInfoDefault, 122 | } 123 | ) 124 | .add("With imbricated sub-rows", () =>
, { 125 | info: storyInfoDefault, 126 | }) 127 | 128 | .add( 129 | "With span", 130 | () => { 131 | const rows = tableWithSubItems({ 132 | firstSubRows: subRows({}), 133 | secondSubRows: subRows({}), 134 | }); 135 | rows[1].rowSpanProps = { title: "foo", color: "#0082c3" }; 136 | rows[2].rowSpanProps = { title: "bar", color: "#e86430" }; 137 | return
; 138 | }, 139 | { 140 | info: storyInfoDefault, 141 | } 142 | ) 143 | .add( 144 | "With opened sub-rows", 145 | () => ( 146 |
159 | ), 160 | { 161 | info: storyInfoDefault, 162 | } 163 | ) 164 | .add( 165 | "With visibleRows and visibleColumns", 166 | () => ( 167 |
173 | ), 174 | { 175 | notes: { markdown: Readme }, 176 | info: storyInfoDefault, 177 | } 178 | ) 179 | .add( 180 | "With dynamic className", 181 | () => { 182 | const { rows } = defaultProps; 183 | rows[1].cells[1].getClassName = () => { 184 | return "selected"; 185 | }; 186 | return
; 187 | }, 188 | { 189 | notes: { markdown: Readme }, 190 | info: storyInfoDefault, 191 | } 192 | ); 193 | -------------------------------------------------------------------------------- /stories/components/virtualizer.md: -------------------------------------------------------------------------------- 1 | # Virtualizer 2 | 3 | Combine the `` component with a table/grid, to determine which part of it is visible. 4 | -------------------------------------------------------------------------------- /stories/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.md" { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /stories/utils/decorators.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider, StyledEngineProvider, createTheme, adaptV4Theme } from "@mui/material/styles"; 2 | 3 | export const blueDkt = "#0082c3"; 4 | 5 | const muiDktTheme = createTheme( 6 | adaptV4Theme({ 7 | typography: { 8 | fontFamily: "Roboto Condensed", 9 | }, 10 | palette: { 11 | primary: { 12 | light: blueDkt, 13 | dark: blueDkt, 14 | main: blueDkt, 15 | }, 16 | secondary: { 17 | light: blueDkt, 18 | dark: blueDkt, 19 | main: blueDkt, 20 | }, 21 | }, 22 | }) 23 | ); 24 | 25 | /** 26 | * The MUI theme provider wrapper 27 | * 28 | * @param {() => JSX.Element} attributes a function to render the child component. 29 | * 30 | * @return {JSX.Element} The child wrapped by the MUI theme provider. 31 | */ 32 | export function withThemeProvider(renderChild: () => JSX.Element): JSX.Element { 33 | return ( 34 | 35 | {renderChild()} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /test/components/__snapshots__/responsive-container.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Cell component should render the default responsive 1`] = ` 4 | 5 | [Function] 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /test/components/__snapshots__/scroller.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Scroller component should render a vertical scroller 1`] = ` 4 |
17 |
26 |
35 |
36 | `; 37 | 38 | exports[`Scroller component should render an horizontal scroller 1`] = ` 39 |
52 |
61 |
70 |
71 | `; 72 | 73 | exports[`Scroller component should render the default scroller 1`] = ` 74 |
87 |
96 |
105 |
106 | `; 107 | -------------------------------------------------------------------------------- /test/components/__snapshots__/virtualizer.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Virtualizer should render children 1`] = ` 4 | 13 |
14 | Foo 15 |
16 |
17 | `; 18 | -------------------------------------------------------------------------------- /test/components/responsive-container.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createRenderer } from "react-test-renderer/shallow"; 3 | 4 | import ResponsiveContainer from "../../src/components/responsive-container"; 5 | 6 | describe("Cell component", () => { 7 | test("should render the default responsive", () => { 8 | const props = { 9 | className: "foo-class-name", 10 | children: () =>
Foo
, 11 | }; 12 | const shallowRenderer = createRenderer(); 13 | shallowRenderer.render(); 14 | const rendered = shallowRenderer.getRenderOutput(); 15 | expect(rendered).toMatchSnapshot(); 16 | }); 17 | // "behaviours dependent on rendered element sizes cannot be tested with jest/enzyme/jsdom" 18 | // "jsdom doesn't support layout. This means measurements like this will always return 0 as it does here" 19 | }); 20 | -------------------------------------------------------------------------------- /test/components/styled-table/__snapshots__/bubble.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Bubble component should render a bubble with content 1`] = ` 4 |
7 |
8 | Foo 9 |
10 | 16 | 22 | 23 | 29 | 34 | 30 35 | 36 | 37 | 38 |
39 | `; 40 | 41 | exports[`Bubble component should render a bubble without badge 1`] = ` 42 |
45 | 51 | 57 | 58 |
59 | `; 60 | 61 | exports[`Bubble component should render a success bubble with content 1`] = ` 62 |
65 |
66 | Foo 67 |
68 | 74 | 80 | 81 | 87 | 92 | 30 93 | 94 | 95 | 96 |
97 | `; 98 | 99 | exports[`Bubble component should render the default bubble 1`] = ` 100 |
103 | 109 | 115 | 116 | 122 | 127 | 30 128 | 129 | 130 | 131 |
132 | `; 133 | -------------------------------------------------------------------------------- /test/components/styled-table/__snapshots__/cell-with-icon.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CellWithIcon component should render the CellWithIcon with a tooltip 1`] = ` 4 |
7 |
11 | TUNING 12 |
13 | 16 | 19 | edit 20 | 21 | 22 |
23 | `; 24 | 25 | exports[`CellWithIcon component should render the CellWithIcon with a tooltip and an action 1`] = ` 26 |
29 |
33 | TUNING 34 |
35 | 38 | 44 | 47 | edit 48 | 49 | 50 | 51 |
52 | `; 53 | 54 | exports[`CellWithIcon component should render the CellWithIcon with an action 1`] = ` 55 |
58 |
62 | TUNING 63 |
64 | 70 | 73 | edit 74 | 75 | 76 |
77 | `; 78 | 79 | exports[`CellWithIcon component should render the default CellWithIcon 1`] = ` 80 |
83 |
87 | TUNING 88 |
89 | 92 | edit 93 | 94 |
95 | `; 96 | -------------------------------------------------------------------------------- /test/components/styled-table/__snapshots__/editable-cell.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EdiTableCell component should render a disabled editable cell 1`] = ` 4 |
8 |
12 | 15 | 0,00 16 | 17 |
18 |
19 | `; 20 | 21 | exports[`EdiTableCell component should render editable cell with null value : '-' 1`] = ` 22 |
27 |
31 | 34 | - 35 | 36 |
37 |
38 | `; 39 | 40 | exports[`EdiTableCell component should render editable cell with percentage value : '100.00 %' 1`] = ` 41 |
46 |
50 | 53 | 100,00 % 54 | 55 |
56 |
57 | `; 58 | 59 | exports[`EdiTableCell component should render edited cell 1`] = ` 60 |
65 |
69 | 72 | 50,00 73 | 74 |
75 |
76 | `; 77 | 78 | exports[`EdiTableCell component should render the default editable cell 1`] = ` 79 |
84 |
88 | 91 | 0,00 92 | 93 |
94 |
95 | `; 96 | -------------------------------------------------------------------------------- /test/components/styled-table/__snapshots__/header-cell.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`HeaderCell component should render the HeaderCell (with custom className) 1`] = ` 4 |
7 |
10 |
13 | Foo 14 |
15 |
19 | Bar 20 |
21 |
22 |
23 | `; 24 | 25 | exports[`HeaderCell component should render the current HeaderCell 1`] = ` 26 |
29 | 30 |
33 | 37 |
40 |
43 | Foo 44 |
45 |
49 | Bar 50 |
51 |
52 |
53 | 54 |
55 | `; 56 | 57 | exports[`HeaderCell component should render the default HeaderCell (with subtitle) 1`] = ` 58 |
61 |
64 |
67 | Foo 68 |
69 |
73 | Bar 74 |
75 |
76 |
77 | `; 78 | 79 | exports[`HeaderCell component should render the default HeaderCell 1`] = ` 80 |
83 |
86 |
89 | Foo 90 |
91 |
95 | Bar 96 |
97 |
98 |
99 | `; 100 | -------------------------------------------------------------------------------- /test/components/styled-table/bubble.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createRenderer } from "react-test-renderer/shallow"; 3 | 4 | import Bubble, { BubbleType } from "../../../src/components/styled-table/bubble"; 5 | 6 | describe("Bubble component", () => { 7 | test("should render the default bubble", () => { 8 | const shallowRenderer = createRenderer(); 9 | shallowRenderer.render(); 10 | const rendered = shallowRenderer.getRenderOutput(); 11 | expect(rendered).toMatchSnapshot(); 12 | }); 13 | 14 | test("should render a bubble without badge", () => { 15 | const shallowRenderer = createRenderer(); 16 | shallowRenderer.render(); 17 | const rendered = shallowRenderer.getRenderOutput(); 18 | expect(rendered).toMatchSnapshot(); 19 | }); 20 | 21 | test("should render a bubble with content", () => { 22 | const shallowRenderer = createRenderer(); 23 | shallowRenderer.render( 24 | 25 |
Foo
26 |
27 | ); 28 | const rendered = shallowRenderer.getRenderOutput(); 29 | expect(rendered).toMatchSnapshot(); 30 | }); 31 | 32 | test("should render a success bubble with content", () => { 33 | const shallowRenderer = createRenderer(); 34 | shallowRenderer.render( 35 | 36 |
Foo
37 |
38 | ); 39 | const rendered = shallowRenderer.getRenderOutput(); 40 | expect(rendered).toMatchSnapshot(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/components/styled-table/cell-with-icon.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { createRenderer } from "react-test-renderer/shallow"; 4 | import { mount } from "enzyme"; 5 | 6 | import CellWithIcon from "../../../src/components/styled-table/cell-with-icon"; 7 | import { withThemeProvider } from "../../../stories/utils/decorators"; 8 | 9 | describe("CellWithIcon component", () => { 10 | test("should render the default CellWithIcon", () => { 11 | const shallowRenderer = createRenderer(); 12 | shallowRenderer.render(); 13 | const rendered = shallowRenderer.getRenderOutput(); 14 | expect(rendered).toMatchSnapshot(); 15 | }); 16 | 17 | test("should render the CellWithIcon with an action", () => { 18 | const shallowRenderer = createRenderer(); 19 | shallowRenderer.render(); 20 | const rendered = shallowRenderer.getRenderOutput(); 21 | expect(rendered).toMatchSnapshot(); 22 | }); 23 | 24 | test("should render the CellWithIcon with a tooltip", () => { 25 | const shallowRenderer = createRenderer(); 26 | shallowRenderer.render(); 27 | const rendered = shallowRenderer.getRenderOutput(); 28 | expect(rendered).toMatchSnapshot(); 29 | }); 30 | 31 | test("should render the CellWithIcon with a tooltip and an action", () => { 32 | const shallowRenderer = createRenderer(); 33 | shallowRenderer.render(); 34 | const rendered = shallowRenderer.getRenderOutput(); 35 | expect(rendered).toMatchSnapshot(); 36 | }); 37 | 38 | test("should call onclick", () => { 39 | const props = { 40 | iconName: "edit", 41 | value: "Foo", 42 | onClick: jest.fn(), 43 | }; 44 | const wrapper = mount(withThemeProvider(() => )); 45 | // onClick 46 | wrapper.find("[data-testid='toolbar-action-btn']").last().simulate("click"); 47 | expect(props.onClick).toBeCalledTimes(1); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/components/styled-table/header-cell.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { createRenderer } from "react-test-renderer/shallow"; 4 | 5 | import HeaderCell from "../../../src/components/styled-table/header-cell"; 6 | 7 | describe("HeaderCell component", () => { 8 | test("should render the default HeaderCell", () => { 9 | const shallowRenderer = createRenderer(); 10 | shallowRenderer.render(); 11 | const rendered = shallowRenderer.getRenderOutput(); 12 | expect(rendered).toMatchSnapshot(); 13 | }); 14 | 15 | test("should render the default HeaderCell (with subtitle)", () => { 16 | const shallowRenderer = createRenderer(); 17 | shallowRenderer.render(); 18 | const rendered = shallowRenderer.getRenderOutput(); 19 | expect(rendered).toMatchSnapshot(); 20 | }); 21 | 22 | test("should render the HeaderCell (with custom className)", () => { 23 | const shallowRenderer = createRenderer(); 24 | shallowRenderer.render(); 25 | const rendered = shallowRenderer.getRenderOutput(); 26 | expect(rendered).toMatchSnapshot(); 27 | }); 28 | 29 | test("should render the current HeaderCell", () => { 30 | const shallowRenderer = createRenderer(); 31 | shallowRenderer.render(); 32 | const rendered = shallowRenderer.getRenderOutput(); 33 | expect(rendered).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/components/table-interactions-manager/__snapshots__/column-visibility-controller.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ColumnVisisbilityController component should render the default ColumnVisisbilityController 1`] = ` 4 | 5 | 8 | Foo 9 | 10 | 31 | 38 | 42 | 43 | 46 | visibility_off 47 | 48 | 49 | 53 | 54 | 58 | 59 | 62 | visibility 63 | 64 | 65 | 69 | 70 | 71 | 72 | 73 | `; 74 | -------------------------------------------------------------------------------- /test/components/table-interactions-manager/__snapshots__/fixed-column-controller.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FixedColumnController component should render the default FixedColumnController 1`] = ` 4 | 7 | Bar 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /test/components/table-interactions-manager/__snapshots__/fixed-row-controller.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FixedRowController component should render the default FixedRowController 1`] = ` 4 | 7 | Bar 8 | 9 | `; 10 | -------------------------------------------------------------------------------- /test/components/table-interactions-manager/cell-dimensions-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { createRenderer } from "react-test-renderer/shallow"; 4 | 5 | import { DumbCellDimensionController } from "../../../src/components/table-interactions-manager/cell-dimensions-controller"; 6 | import { CellSize } from "../../../src/components/table-interactions-manager/reducers"; 7 | 8 | describe("CellDimensionController component", () => { 9 | test("should render the default CellDimensionController", () => { 10 | const props = { 11 | cellWidth: { value: 60, size: CellSize.small }, 12 | rowHeight: { value: 60, size: CellSize.small }, 13 | updateRowHeight: jest.fn(), 14 | updateCellWidth: jest.fn(), 15 | }; 16 | const shallowRenderer = createRenderer(); 17 | shallowRenderer.render( 18 | Foo} /> 19 | ); 20 | const rendered = shallowRenderer.getRenderOutput(); 21 | expect(rendered).toMatchSnapshot(); 22 | }); 23 | test("should render CellDimensionController with default cell sizes", () => { 24 | const props = { 25 | cellWidth: { value: 100, size: "xs" }, 26 | rowHeight: { value: 100, size: "s" }, 27 | rowHeightOptions: { s: 100, m: 200 }, 28 | cellWidthOptions: { xs: 100, xl: 200 }, 29 | updateRowHeight: jest.fn(), 30 | updateCellWidth: jest.fn(), 31 | }; 32 | const shallowRenderer = createRenderer(); 33 | shallowRenderer.render( 34 | Foo} /> 35 | ); 36 | const rendered = shallowRenderer.getRenderOutput(); 37 | expect(rendered).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/components/table-interactions-manager/column-visibility-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { createRenderer } from "react-test-renderer/shallow"; 4 | 5 | import { DumbColumnVisibilityController } from "../../../src/components/table-interactions-manager/column-visibility-controller"; 6 | 7 | describe("ColumnVisisbilityController component", () => { 8 | test("should render the default ColumnVisisbilityController", () => { 9 | const props = { 10 | columns: [ 11 | { id: "foo", index: 1, label: "FOO" }, 12 | { id: "bar", index: 2, label: "BAR" }, 13 | ], 14 | hiddenColumnsIndexes: [1], 15 | hiddenColumnsIds: ["foo"], 16 | updateHiddenIds: jest.fn(), 17 | }; 18 | const shallowRenderer = createRenderer(); 19 | shallowRenderer.render( 20 | Foo} /> 21 | ); 22 | const rendered = shallowRenderer.getRenderOutput(); 23 | expect(rendered).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/components/table-interactions-manager/fixed-column-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { createRenderer } from "react-test-renderer/shallow"; 4 | 5 | import { DumbFixedColumnController } from "../../../src/components/table-interactions-manager/fixed-column-controller"; 6 | 7 | describe("FixedColumnController component", () => { 8 | test("should render the default FixedColumnController", () => { 9 | const props = { 10 | columnId: "bar", 11 | fixedColumnsIndexes: [1], 12 | fixedColumnsIds: ["foo"], 13 | updateFixedColumnsIds: jest.fn(), 14 | }; 15 | const shallowRenderer = createRenderer(); 16 | shallowRenderer.render( 17 | 18 | {({ toggleFixedColumnId }) => Bar} 19 | 20 | ); 21 | const rendered = shallowRenderer.getRenderOutput(); 22 | expect(rendered).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/components/table-interactions-manager/fixed-row-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { createRenderer } from "react-test-renderer/shallow"; 4 | 5 | import { DumbFixedRowController } from "../../../src/components/table-interactions-manager/fixed-row-controller"; 6 | 7 | describe("FixedRowController component", () => { 8 | test("should render the default FixedRowController", () => { 9 | const props = { 10 | rowIndex: 2, 11 | fixedRowsIndexes: [1], 12 | updateFixedRowsIndexes: jest.fn(), 13 | }; 14 | const shallowRenderer = createRenderer(); 15 | shallowRenderer.render( 16 | 17 | {({ toggleFixedRowIndex }) => Bar} 18 | 19 | ); 20 | const rendered = shallowRenderer.getRenderOutput(); 21 | expect(rendered).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/components/table-selection/__snapshots__/context-menu-handler.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ContextMenuHandler component should render children 1`] = ` 4 | 5 |
6 | Foo 7 |
8 |
9 | `; 10 | -------------------------------------------------------------------------------- /test/components/table-selection/__snapshots__/selection-handler.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SelectionHandler component should render children 1`] = ` 4 |
7 | 10 | [Function] 11 | 12 |
13 | `; 14 | -------------------------------------------------------------------------------- /test/components/table-selection/__snapshots__/table-selection-menu.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TableSelectionMenu component should render TableSelectionMenu closed menu 1`] = ` 4 | 5 | 18 | 19 | `; 20 | 21 | exports[`TableSelectionMenu component should render TableSelectionMenu opened menu 1`] = ` 22 | 23 | 36 | 37 | `; 38 | 39 | exports[`TableSelectionMenu component should render TableSelectionMenu opened menu with actions 1`] = ` 40 | 41 | 54 | 71 | Foo item 72 | 73 | 90 | Bar item 91 | 92 | 93 | 94 | `; 95 | -------------------------------------------------------------------------------- /test/components/table-selection/context-menu-handler.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createRenderer } from "react-test-renderer/shallow"; 3 | 4 | import ContextMenuHandler from "../../../src/components/table-selection/context-menu-handler"; 5 | 6 | describe("ContextMenuHandler component", () => { 7 | test("should render children", () => { 8 | const children = () =>
Foo
; 9 | const props = { 10 | children, 11 | selectedCells: {}, 12 | }; 13 | const shallowRenderer = createRenderer(); 14 | shallowRenderer.render(); 15 | const rendered = shallowRenderer.getRenderOutput(); 16 | expect(rendered).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/components/table-selection/selection-handler.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createRenderer } from "react-test-renderer/shallow"; 3 | import { mount } from "enzyme"; 4 | 5 | import SelectionHandler from "../../../src/components/table-selection/selection-handler"; 6 | import { MouseClickButtons } from "../../../src/components/constants"; 7 | 8 | describe("SelectionHandler component", () => { 9 | test("should render children", () => { 10 | const children = () =>
Foo
; 11 | const props = { 12 | children, 13 | }; 14 | const shallowRenderer = createRenderer(); 15 | shallowRenderer.render(); 16 | const rendered = shallowRenderer.getRenderOutput(); 17 | expect(rendered).toMatchSnapshot(); 18 | }); 19 | 20 | test("should select cells", () => { 21 | const source = { rowIndex: 1, cellIndex: 1 }; 22 | const target = { rowIndex: 2, cellIndex: 2 }; 23 | const props = { 24 | children: jest.fn(), 25 | }; 26 | const wrapper = mount(); 27 | const instance: SelectionHandler = wrapper.instance() as SelectionHandler; 28 | // @ts-ignore private functions 29 | instance.onCellMouseDown(source, MouseClickButtons.right); 30 | expect(instance.state.selectedCells).toEqual({ 1: [1] }); 31 | // @ts-ignore private prop 32 | expect(instance.startingCell).toEqual(source); 33 | // @ts-ignore private functions 34 | instance.onCellMouseEnter(target); 35 | expect(instance.state.selectedCells).toEqual({ 1: [1, 2], 2: [1, 2] }); 36 | }); 37 | 38 | test("should select cells (isDisabledVerticalSelection)", () => { 39 | const source = { rowIndex: 1, cellIndex: 1 }; 40 | const target = { rowIndex: 2, cellIndex: 2 }; 41 | const props = { 42 | children: jest.fn(), 43 | isDisabledVerticalSelection: true, 44 | }; 45 | const wrapper = mount(); 46 | const instance: SelectionHandler = wrapper.instance() as SelectionHandler; 47 | // @ts-ignore private functions 48 | instance.onCellMouseDown(source, MouseClickButtons.right); 49 | expect(instance.state.selectedCells).toEqual({ 1: [1] }); 50 | // @ts-ignore private functions 51 | instance.onCellMouseEnter(target); 52 | expect(instance.state.selectedCells).toEqual({ 1: [1, 2] }); 53 | }); 54 | 55 | test("should select cells (isDisabledHorizontalSelection)", () => { 56 | const source = { rowIndex: 1, cellIndex: 1 }; 57 | const target = { rowIndex: 2, cellIndex: 2 }; 58 | const props = { 59 | children: jest.fn(), 60 | isDisabledHorizontalSelection: true, 61 | }; 62 | const wrapper = mount(); 63 | const instance: SelectionHandler = wrapper.instance() as SelectionHandler; 64 | // @ts-ignore private functions 65 | instance.onCellMouseDown(source, MouseClickButtons.right); 66 | expect(instance.state.selectedCells).toEqual({ 1: [1] }); 67 | // @ts-ignore private functions 68 | instance.onCellMouseEnter(target); 69 | expect(instance.state.selectedCells).toEqual({ 1: [1], 2: [1] }); 70 | }); 71 | 72 | test("on cell mouse up", () => { 73 | const source = { rowIndex: 1, cellIndex: 1 }; 74 | const target = { rowIndex: 2, cellIndex: 2 }; 75 | const props = { 76 | children: jest.fn(), 77 | onContextMenu: jest.fn(), 78 | }; 79 | const wrapper = mount(); 80 | const instance: SelectionHandler = wrapper.instance() as SelectionHandler; 81 | // @ts-ignore private functions 82 | instance.onCellMouseDown(source, MouseClickButtons.right); 83 | // @ts-ignore private functions 84 | instance.onCellMouseEnter(target); 85 | // @ts-ignore mocked event 86 | instance.onCellMouseUp(); 87 | // @ts-ignore private prop 88 | expect(instance.startingCell).toEqual(null); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/components/table-selection/table-selection-menu.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createRenderer } from "react-test-renderer/shallow"; 3 | 4 | import TableSelectionMenu, { IMenuAction } from "../../../src/components/table-selection/table-selection-menu"; 5 | 6 | const selectedCells = { 1: [0, 1, 2], 2: [0, 1, 2] }; 7 | 8 | const selectionCell = { 9 | anchorEl: null, 10 | contextCell: { rowIndex: 0, cellIndex: 2 }, 11 | }; 12 | 13 | describe("TableSelectionMenu component", () => { 14 | test("should render TableSelectionMenu closed menu", () => { 15 | const props = { 16 | closeMenu: jest.fn(), 17 | selectedCells, 18 | selectionContext: selectionCell, 19 | isMenuOpened: false, 20 | actions: [], 21 | }; 22 | const shallowRenderer = createRenderer(); 23 | shallowRenderer.render(); 24 | const rendered = shallowRenderer.getRenderOutput(); 25 | expect(rendered).toMatchSnapshot(); 26 | }); 27 | 28 | test("should render TableSelectionMenu opened menu", () => { 29 | const props = { 30 | closeMenu: jest.fn(), 31 | selectedCells, 32 | selectionContext: selectionCell, 33 | isMenuOpened: true, 34 | actions: [], 35 | }; 36 | const shallowRenderer = createRenderer(); 37 | shallowRenderer.render(); 38 | const rendered = shallowRenderer.getRenderOutput(); 39 | expect(rendered).toMatchSnapshot(); 40 | }); 41 | 42 | test("should render TableSelectionMenu opened menu with actions", () => { 43 | const actions: IMenuAction[] = [ 44 | { 45 | id: "foo", 46 | title: "Foo item", 47 | component: () =>
Foo
, 48 | menuItem: ({ children }) =>
{children}
, 49 | }, 50 | { 51 | id: "bar", 52 | title: "Bar item", 53 | component: () =>
Foo
, 54 | }, 55 | ]; 56 | const props = { 57 | closeMenu: jest.fn(), 58 | selectedCells, 59 | selectionContext: selectionCell, 60 | isMenuOpened: true, 61 | actions, 62 | }; 63 | const shallowRenderer = createRenderer(); 64 | shallowRenderer.render(); 65 | const rendered = shallowRenderer.getRenderOutput(); 66 | expect(rendered).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/components/table/__snapshots__/row-span.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RowSpan component should render a closed span 1`] = ` 4 |
35 | `; 36 | 37 | exports[`RowSpan component should render an opened span 1`] = ` 38 | 69 | `; 70 | -------------------------------------------------------------------------------- /test/components/table/elementary-table.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createRenderer } from "react-test-renderer/shallow"; 3 | import { mount } from "enzyme"; 4 | 5 | import * as Utils from "../../../src/components/utils/table"; 6 | import ElementaryTable from "../../../src/components/table/elementary-table"; 7 | import { tableWithSubItems, subRows } from "../../../stories/utils/tables"; 8 | import { withThemeProvider } from "../../../stories/utils/decorators"; 9 | import { IRow } from "../../../src/components/table/row"; 10 | 11 | const rows = tableWithSubItems({ 12 | firstSubRows: subRows({ subsubRows: subRows({}) }), 13 | secondSubRows: subRows({ subsubRows: subRows({}) }), 14 | }); 15 | 16 | const fixedRowsIndexes = [1]; 17 | 18 | describe("elementary table component", () => { 19 | test("should render the default table", () => { 20 | const indexesMap = Utils.getAllIndexesMap([], rows); 21 | const props = { id: "foo", rows, indexesMapping: indexesMap }; 22 | const shallowRenderer = createRenderer(); 23 | shallowRenderer.render(); 24 | const rendered = shallowRenderer.getRenderOutput(); 25 | expect(rendered).toMatchSnapshot(); 26 | }); 27 | 28 | test("should render a table with custom cell size", () => { 29 | const rows = tableWithSubItems({ 30 | firstSubRows: subRows({ subsubRows: subRows({}) }), 31 | secondSubRows: subRows({ subsubRows: subRows({}) }), 32 | }); 33 | const indexesMap = Utils.getAllIndexesMap([], rows); 34 | // @ts-ignore 35 | rows[0].size = 24; 36 | // @ts-ignore 37 | rows[2].size = 150; 38 | const props = { 39 | id: "foo", 40 | rows, 41 | indexesMapping: indexesMap, 42 | columns: { 0: { size: 320 }, 4: { size: 300 } }, 43 | }; 44 | const shallowRenderer = createRenderer(); 45 | shallowRenderer.render(); 46 | const rendered = shallowRenderer.getRenderOutput(); 47 | expect(rendered).toMatchSnapshot(); 48 | }); 49 | 50 | test("should return the Tree Length", () => { 51 | const opendTrees = { 1: { rowIndex: 1, columnIndex: 0 } }; 52 | const indexesMap = Utils.getAllIndexesMap(opendTrees, rows); 53 | const props = { 54 | id: "foo", 55 | rows, 56 | indexesMapping: indexesMap, 57 | opendTrees, 58 | fixedRowsIndexes, 59 | visibleRowIndexes: [0, 1, 2, 3, 4], 60 | }; 61 | const wrapper = mount(withThemeProvider(() => )); 62 | const instance: ElementaryTable = wrapper.find(ElementaryTable).instance() as ElementaryTable; 63 | expect(instance.getRowTreeLength(0)).toEqual(0); 64 | expect(instance.getRowTreeLength(1)).toEqual(2); 65 | expect(instance.getRowTreeLength(2)).toEqual(0); 66 | }); 67 | 68 | test("should return the visible rows", () => { 69 | const opendTrees = { 1: { rowIndex: 1, columnIndex: 0 } }; 70 | const indexesMap = Utils.getAllIndexesMap(opendTrees, rows); 71 | const props = { 72 | id: "foo", 73 | rows, 74 | indexesMapping: indexesMap, 75 | opendTrees, 76 | fixedRowsIndexes, 77 | visibleRowIndexes: [0, 1, 2, 3], // the third row is hidden 78 | }; 79 | const wrapper = mount(withThemeProvider(() => )); 80 | const instance: ElementaryTable = wrapper.find(ElementaryTable).instance() as ElementaryTable; 81 | // @ts-ignore subItems is defined 82 | const secondLevel = props.rows[1].cells[0].subItems; 83 | // first level 84 | // the third row is hidden 85 | expect(instance.getVisibleRows(props.rows, null, fixedRowsIndexes)).toEqual([[0, 1], props.rows.slice(0, 2)]); 86 | // second level 87 | expect(instance.getVisibleRows(secondLevel as IRow[], 1)).toEqual([[0, 1], secondLevel]); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/components/table/row-span.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { createRenderer } from "react-test-renderer/shallow"; 3 | import { cleanup, fireEvent } from "@testing-library/react"; 4 | 5 | import RowSpan from "../../../src/components/table/row-span"; 6 | import { customRender } from "../../tests-utils/react-testing-library-utils"; 7 | 8 | describe("RowSpan component", () => { 9 | test("should render a closed span", () => { 10 | const props = { 11 | opened: false, 12 | length: 2, 13 | title: "Foo", 14 | color: "red", 15 | toggle: () => null, 16 | }; 17 | const shallowRenderer = createRenderer(); 18 | shallowRenderer.render(); 19 | const rendered = shallowRenderer.getRenderOutput(); 20 | expect(rendered).toMatchSnapshot(); 21 | }); 22 | 23 | test("should render an opened span", () => { 24 | const props = { 25 | opened: true, 26 | length: 2, 27 | title: "Foo", 28 | toggle: () => null, 29 | }; 30 | const shallowRenderer = createRenderer(); 31 | shallowRenderer.render(); 32 | const rendered = shallowRenderer.getRenderOutput(); 33 | expect(rendered).toMatchSnapshot(); 34 | }); 35 | 36 | test("should call the span toggle callback", () => { 37 | const props = { 38 | opened: true, 39 | length: 2, 40 | title: "Foo", 41 | toggle: jest.fn(), 42 | }; 43 | const { container } = customRender(); 44 | fireEvent.click(container.getElementsByTagName("button")[0]); 45 | expect(props.toggle).toBeCalledTimes(1); 46 | cleanup(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/global-test-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | process.env.TZ = "UTC"; 3 | }; 4 | -------------------------------------------------------------------------------- /test/it-tests/cell-dimensions-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { fireEvent, getByTestId as globalGetByTestId, getByText as globalGetByText, waitFor } from "@testing-library/react"; 4 | 5 | import CellDimensionController from "../../src/components/table-interactions-manager/cell-dimensions-controller"; 6 | import TabeInteractionManager, { 7 | TableInteractionsContext, 8 | } from "../../src/components/table-interactions-manager/table-interactions-manager"; 9 | import { customRender } from "../tests-utils/react-testing-library-utils"; 10 | import Table from "../../src/components/table/table"; 11 | import { getTable } from "../../stories/components/styled-table/tables"; 12 | import { getCellsOfRow, getRows } from "../tests-utils/table"; 13 | 14 | const defaultProps = getTable(); 15 | 16 | jest.useFakeTimers(); 17 | 18 | describe("CellDimensionController component", () => { 19 | test("should render the default CellDimensionController", () => { 20 | const { getByTestId, getByText } = customRender( 21 | 22 |
Dimension Controller
} /> 23 |
24 | ); 25 | fireEvent.click(getByText("Dimension Controller")); 26 | 27 | const columnWidthSamll = getByTestId("column-width-dimension-small"); 28 | const columnWidthMedium = getByTestId("column-width-dimension-medium"); 29 | const columnWidthLarge = getByTestId("column-width-dimension-large"); 30 | const rowHeightSamll = getByTestId("row-height-dimension-small"); 31 | const rowHeightMedium = getByTestId("row-height-dimension-medium"); 32 | const rowHeightLarge = getByTestId("row-height-dimension-large"); 33 | // medium values are checked 34 | expect(globalGetByTestId(columnWidthMedium, "column-width-dimension-checked")).toBeTruthy(); 35 | expect(globalGetByTestId(rowHeightMedium, "row-height-dimension-checked")).toBeTruthy(); 36 | 37 | fireEvent.click(columnWidthSamll); 38 | // cell width small 39 | expect(globalGetByTestId(columnWidthSamll, "column-width-dimension-checked")).toBeTruthy(); 40 | expect(globalGetByTestId(rowHeightMedium, "row-height-dimension-checked")).toBeTruthy(); 41 | 42 | fireEvent.click(rowHeightSamll); 43 | // row height small 44 | expect(globalGetByTestId(columnWidthSamll, "column-width-dimension-checked")).toBeTruthy(); 45 | expect(globalGetByTestId(rowHeightSamll, "row-height-dimension-checked")).toBeTruthy(); 46 | 47 | fireEvent.click(columnWidthLarge); 48 | fireEvent.click(rowHeightLarge); 49 | // cell width and row height large 50 | expect(globalGetByTestId(columnWidthLarge, "column-width-dimension-checked")).toBeTruthy(); 51 | expect(globalGetByTestId(rowHeightLarge, "row-height-dimension-checked")).toBeTruthy(); 52 | }); 53 | 54 | test("should scroll to the current column", async () => { 55 | const { getByTestId, getByText } = customRender( 56 | // init scroll to the week number 12 57 | 58 | 59 | {({ onHorizontallyScroll, tableRef, columnsCursor, cellWidth, rowHeight }) => { 60 | return ( 61 | <> 62 |
Dimension Controller
} /> 63 |
16 |
19 |
22 | Foo 23 |
24 | 29 | 30 | keyboard_arrow_right 31 | 32 | 33 |
34 |
50 |
53 |
56 | Foo 57 |
58 | 63 | 64 | keyboard_arrow_down 65 | 66 | 67 |
68 |
80 | 81 | ); 82 | }} 83 | 84 | 85 | ); 86 | 87 | // The initial scroll (week number 12) 88 | fireEvent.scroll(getByTestId("scroller-container")); 89 | let header = getRows(true); 90 | expect(globalGetByText(getCellsOfRow(header[0])[1], "W12")).toBeTruthy(); 91 | 92 | fireEvent.click(getByText("Dimension Controller")); 93 | const columnWidthSamll = getByTestId("column-width-dimension-small"); 94 | 95 | // small cell width 96 | fireEvent.click(columnWidthSamll); 97 | header = getRows(true); 98 | await waitFor(() => { 99 | fireEvent.scroll(getByTestId("scroller-container")); 100 | return expect(globalGetByText(getCellsOfRow(header[0])[1], "W12")).toBeTruthy(); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/it-tests/column-visibility-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { fireEvent, getByTestId as globalGetByTestId, getByText as globalGetByText } from "@testing-library/react"; 3 | 4 | import ColumnVisisbilityController from "../../src/components/table-interactions-manager/column-visibility-controller"; 5 | import TabeInteractionManager, { 6 | TableInteractionsContext, 7 | } from "../../src/components/table-interactions-manager/table-interactions-manager"; 8 | import { customRender } from "../tests-utils/react-testing-library-utils"; 9 | import Table from "../../src/components/table/table"; 10 | import { getTable } from "../../stories/components/styled-table/tables"; 11 | import { getCellsOfRow, getRows } from "../tests-utils/table"; 12 | 13 | const defaultProps = getTable(); 14 | 15 | describe("ColumnVisisbilityController component", () => { 16 | test("should render the default ColumnVisisbilityController", () => { 17 | const { getByTestId, getByText } = customRender( 18 | 19 |
Visisbility Controller
} 25 | /> 26 |
27 | ); 28 | fireEvent.click(getByText("Visisbility Controller")); 29 | const fooItem = getByTestId("column-visibility-foo"); 30 | const barItem = getByTestId("column-visibility-bar"); 31 | // Foo and Bar are visible 32 | expect(globalGetByTestId(fooItem, "visibility-on")).toBeTruthy(); 33 | expect(globalGetByTestId(barItem, "visibility-on")).toBeTruthy(); 34 | // hide Foo 35 | fireEvent.click(fooItem); 36 | expect(globalGetByTestId(fooItem, "visibility-off")).toBeTruthy(); 37 | expect(globalGetByTestId(barItem, "visibility-on")).toBeTruthy(); 38 | // hide Bar 39 | fireEvent.click(barItem); 40 | expect(globalGetByTestId(fooItem, "visibility-off")).toBeTruthy(); 41 | expect(globalGetByTestId(barItem, "visibility-off")).toBeTruthy(); 42 | }); 43 | 44 | test("should update hiddencolumns", () => { 45 | const toggleableColumns = [ 46 | { id: "foo", index: 1, label: "FOO" }, 47 | { id: "bar", index: 2, label: "BAR" }, 48 | ]; 49 | const onColumnVisibilityChange = jest.fn(); 50 | const { getByText, getByTestId } = customRender( 51 | 52 | 53 | {({ onHorizontallyScroll, tableRef, columnsCursor, hiddenColumnsIndexes }) => { 54 | return ( 55 | <> 56 |
Visisbility Controller
} 59 | onColumnVisibilityChange={onColumnVisibilityChange} 60 | /> 61 |
77 | 78 | ); 79 | }} 80 | 81 | 82 | ); 83 | // The initial scroll (week number 1) 84 | fireEvent.scroll(getByTestId("scroller-container")); 85 | let header = getRows(true); 86 | expect(globalGetByText(getCellsOfRow(header[0])[1], "W1")).toBeTruthy(); 87 | 88 | // hide the FOO column 89 | fireEvent.click(getByText("Visisbility Controller")); 90 | fireEvent.click(getByText("FOO")); 91 | fireEvent.scroll(getByTestId("scroller-container")); 92 | header = getRows(true); 93 | expect(globalGetByText(getCellsOfRow(header[0])[1], "W2")).toBeTruthy(); 94 | expect(onColumnVisibilityChange).toBeCalledWith(1, false); 95 | 96 | // hide the BAR column 97 | fireEvent.click(getByText("BAR")); 98 | fireEvent.scroll(getByTestId("scroller-container")); 99 | header = getRows(true); 100 | expect(globalGetByText(getCellsOfRow(header[0])[1], "W3")).toBeTruthy(); 101 | expect(onColumnVisibilityChange).toBeCalledWith(2, false); 102 | 103 | // display the FOO column 104 | fireEvent.click(getByText("FOO")); 105 | fireEvent.scroll(getByTestId("scroller-container")); 106 | header = getRows(true); 107 | expect(globalGetByText(getCellsOfRow(header[0])[1], "W1")).toBeTruthy(); 108 | expect(onColumnVisibilityChange).toBeCalledWith(1, true); 109 | 110 | // display the BAR column 111 | fireEvent.click(getByText("BAR")); 112 | fireEvent.scroll(getByTestId("scroller-container")); 113 | header = getRows(true); 114 | expect(globalGetByText(getCellsOfRow(header[0])[1], "W1")).toBeTruthy(); 115 | expect(onColumnVisibilityChange).toBeCalledWith(2, true); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/it-tests/editable-cell-it.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /// 3 | import * as React from "react"; 4 | import { fireEvent } from "@testing-library/react"; 5 | 6 | import { customRender } from "../tests-utils/react-testing-library-utils"; 7 | import EdiTableCell, { KEYCODE_ENTER } from "../../src/components/styled-table/editable-cell"; 8 | import { IContentCellProps } from "../../src/components/table/cell"; 9 | import { Nullable } from "../../src/components/typing"; 10 | 11 | interface IProps extends IContentCellProps { 12 | defaultValue: Nullable; 13 | alreadyEdited?: boolean; 14 | checkValue?: (value: Nullable) => boolean; 15 | } 16 | 17 | // https://stackoverflow.com/questions/52329629/intl-numberformat-behaves-incorrectly-in-jest-unit-test 18 | const formatValue = (value: Nullable) => (value === null ? "-" : `${value.toFixed(2)} €`); 19 | 20 | //@ts-ignore 21 | const focus = () => {}; 22 | 23 | const mask = { 24 | style: "currency", 25 | currency: "EUR", 26 | decimals: 2, 27 | is_percentage: false, 28 | is_negative: false, 29 | }; 30 | 31 | const EditableCellParent = (props: IProps) => { 32 | const { defaultValue, alreadyEdited, checkValue } = props; 33 | 34 | const [isEdited, setIsEdited] = React.useState(alreadyEdited || false); 35 | const [value, setValue] = React.useState(defaultValue); 36 | 37 | const handleOnConfirmValue = (value: Nullable) => { 38 | setValue(value); 39 | setIsEdited(defaultValue !== value); 40 | }; 41 | 42 | const handleValidateValue = (value: Nullable) => { 43 | let isValidValue = true; 44 | if (checkValue) { 45 | isValidValue = checkValue(value); 46 | } 47 | return isValidValue; 48 | }; 49 | 50 | return ( 51 | 60 | ); 61 | }; 62 | 63 | describe("EditableCell component integration tests", () => { 64 | test("should render textInput on click", () => { 65 | const props = { 66 | defaultValue: null, 67 | alreadyEdited: false, 68 | }; 69 | 70 | const { getByText, getByTestId } = customRender(); 71 | fireEvent.click(getByText("-")); 72 | let input: HTMLInputElement = getByTestId("editable-cell-text-field").querySelector("input") as HTMLInputElement; 73 | fireEvent.change(input, { target: { value: 22, focus } }); 74 | fireEvent.keyPress(input, { keyCode: KEYCODE_ENTER }); 75 | expect(getByText("22.00 €")).toBeTruthy(); 76 | 77 | fireEvent.click(getByText("22.00 €")); 78 | input = getByTestId("editable-cell-text-field").querySelector("input") as HTMLInputElement; 79 | fireEvent.change(input, { target: { value: NaN, focus } }); 80 | fireEvent.keyPress(input, { keyCode: KEYCODE_ENTER }); 81 | expect(getByText("-")).toBeTruthy(); 82 | }); 83 | 84 | test("should display 0 when cell is cleared and it initial value is not null", () => { 85 | const props = { 86 | defaultValue: 3, 87 | alreadyEdited: false, 88 | }; 89 | 90 | const { getByText, getByTestId } = customRender(); 91 | fireEvent.click(getByText("3.00 €")); 92 | const input: HTMLInputElement = getByTestId("editable-cell-text-field").querySelector("input") as HTMLInputElement; 93 | fireEvent.change(input, { target: { value: null, focus } }); 94 | fireEvent.keyPress(input, { keyCode: KEYCODE_ENTER }); 95 | expect(getByText("0.00 €")).toBeTruthy(); 96 | }); 97 | 98 | test("should render input with defaultValue", () => { 99 | const props = { 100 | defaultValue: 3.01, 101 | alreadyEdited: false, 102 | }; 103 | 104 | const { getByText, getByDisplayValue } = customRender(); 105 | fireEvent.click(getByText("3.01 €")); 106 | expect(getByDisplayValue("3,01")).toBeTruthy(); 107 | }); 108 | 109 | test("should render input with defaultValue formatted without its two trailing zeros", () => { 110 | const props = { 111 | defaultValue: 3, 112 | alreadyEdited: false, 113 | }; 114 | 115 | const { getByText, getByDisplayValue } = customRender(); 116 | fireEvent.click(getByText("3.00 €")); 117 | expect(getByDisplayValue("3")).toBeTruthy(); 118 | }); 119 | 120 | test("should render input with defaultValue without the trailing 0", () => { 121 | const props = { 122 | defaultValue: 3.1, 123 | alreadyEdited: false, 124 | }; 125 | 126 | const { getByText, getByDisplayValue } = customRender(); 127 | fireEvent.click(getByText("3.10 €")); 128 | expect(getByDisplayValue("3,1")).toBeTruthy(); 129 | }); 130 | 131 | test("shouldn't update inputValue with incorrect value", () => { 132 | const props = { 133 | defaultValue: 12, 134 | alreadyEdited: false, 135 | checkValue: (value: Nullable) => (value ? value > 10 : false), 136 | }; 137 | 138 | const { getByText, getByTestId } = customRender(); 139 | 140 | fireEvent.click(getByText("12.00 €")); 141 | let input: HTMLInputElement = getByTestId("editable-cell-text-field").querySelector("input") as HTMLInputElement; 142 | fireEvent.change(input, { target: { value: 8, focus } }); 143 | fireEvent.keyPress(input, { keyCode: KEYCODE_ENTER }); 144 | expect(getByText("12.00 €")).toBeTruthy(); 145 | 146 | fireEvent.click(getByText("12.00 €")); 147 | input = getByTestId("editable-cell-text-field").querySelector("input") as HTMLInputElement; 148 | fireEvent.change(input, { target: { value: 15, focus } }); 149 | input = getByTestId("editable-cell-text-field").querySelector("input") as HTMLInputElement; 150 | fireEvent.keyPress(input, { keyCode: KEYCODE_ENTER }); 151 | expect(getByText("15.00 €")).toBeTruthy(); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/it-tests/elementary-table-it.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { cleanup, fireEvent, getByTestId } from "@testing-library/react"; 3 | 4 | import { customRender } from "../tests-utils/react-testing-library-utils"; 5 | import { generateTable } from "../../stories/utils/tables"; 6 | import Table from "../../src/components/table/table"; 7 | import { getCellsOfRow, getRows } from "../tests-utils/table"; 8 | 9 | describe("elementary table component", () => { 10 | test("should render a table with all of rows and all of columns", () => { 11 | customRender(
); 12 | const rows = getRows(); 13 | const header = getRows(true); 14 | expect(rows).toHaveLength(19); 15 | expect(header).toHaveLength(1); 16 | expect(getCellsOfRow(rows[1])).toHaveLength(20); 17 | expect(getCellsOfRow(header[0])[1].textContent).toEqual("(0,1)"); 18 | expect(getCellsOfRow(rows[6])[1].textContent).toEqual("(7,1)"); 19 | expect(getCellsOfRow(rows[18])[1].textContent).toEqual("(19,1)"); 20 | 21 | cleanup(); 22 | }); 23 | 24 | test("Should display sub-rows", () => { 25 | customRender(
); 26 | // Open the first level 27 | fireEvent.click(getByTestId(getRows()[1], "table-cell-sub-item-toggle")); 28 | let rows = getRows(); 29 | let header = getRows(true); 30 | expect(getCellsOfRow(header[0])[0].textContent).toEqual("(0,0)"); 31 | expect(getCellsOfRow(rows[0])[1].textContent).toEqual("(1,1)"); 32 | expect(getCellsOfRow(rows[1])[1].textContent).toEqual("(2,1)"); 33 | // first level 34 | expect(getCellsOfRow(header[1])[1].textContent).toEqual("(0,1)"); 35 | // Open the second level 36 | fireEvent.click(getByTestId(rows[3], "table-cell-sub-item-toggle")); 37 | rows = getRows(); 38 | header = getRows(true); 39 | expect(getCellsOfRow(header[1])[1].textContent).toEqual("(0,1)"); 40 | // second level 41 | expect(getCellsOfRow(header[2])[1].textContent).toEqual("(0,1)"); 42 | // Open the therd level 43 | fireEvent.click(getByTestId(rows[5], "table-cell-sub-item-toggle")); 44 | rows = getRows(); 45 | header = getRows(true); 46 | // therd level 47 | expect(getCellsOfRow(header[3])[1].textContent).toEqual("(0,1)"); 48 | // Close the first level 49 | fireEvent.click(getByTestId(rows[1], "table-cell-sub-item-toggle")); 50 | rows = getRows(); 51 | header = getRows(true); 52 | 53 | expect(getCellsOfRow(header[0])[1].textContent).toEqual("(0,1)"); 54 | expect(getCellsOfRow(rows[2])[1].textContent).toEqual("(3,1)"); 55 | expect(getCellsOfRow(rows[5])[1].textContent).toEqual("(6,1)"); 56 | expect(getCellsOfRow(rows[8])[1].textContent).toEqual("(9,1)"); 57 | cleanup(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/it-tests/fixed-column-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { fireEvent, queryByText } from "@testing-library/react"; 3 | 4 | import FixedColumnController from "../../src/components/table-interactions-manager/fixed-column-controller"; 5 | import TabeInteractionManager, { 6 | TableInteractionsContext, 7 | } from "../../src/components/table-interactions-manager/table-interactions-manager"; 8 | import { customRender } from "../tests-utils/react-testing-library-utils"; 9 | import Table from "../../src/components/table/table"; 10 | import { getTable } from "../../stories/components/styled-table/tables"; 11 | 12 | const defaultProps = getTable(); 13 | 14 | describe("FixedColumnController component", () => { 15 | test("should update fixedColumns", () => { 16 | const { container, getByTestId } = customRender( 17 | 18 | 19 | {({ onHorizontallyScroll, tableRef, columnsCursor, fixedColumnsIndexes }) => { 20 | return ( 21 | <> 22 | 23 | {({ toggleFixedColumnId, isFixed }) => ( 24 |
25 | {isFixed ? "Unpin" : "Pin"} w30 26 |
27 | )} 28 |
29 |
44 | 45 | ); 46 | }} 47 | 48 | 49 | ); 50 | 51 | const pinBtn = getByTestId("pin-column"); 52 | // The initial scroll (week number 1) 53 | fireEvent.scroll(getByTestId("scroller-container")); 54 | expect(queryByText(container, "W30")).toBeFalsy(); 55 | // Pin the w30 56 | fireEvent.click(pinBtn); 57 | expect(queryByText(container, "W30")).toBeTruthy(); 58 | // Unpin the w30 59 | fireEvent.click(pinBtn); 60 | expect(queryByText(container, "W30")).toBeFalsy(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/it-tests/fixed-row-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { fireEvent, queryByText } from "@testing-library/react"; 3 | 4 | import FixedRowController from "../../src/components/table-interactions-manager/fixed-row-controller"; 5 | import TabeInteractionManager, { 6 | TableInteractionsContext, 7 | } from "../../src/components/table-interactions-manager/table-interactions-manager"; 8 | import { customRender } from "../tests-utils/react-testing-library-utils"; 9 | import Table from "../../src/components/table/table"; 10 | import { getTable } from "../../stories/components/styled-table/tables"; 11 | 12 | const defaultProps = getTable(); 13 | 14 | describe("FixedColumnController component", () => { 15 | test("should update fixedColumns", () => { 16 | const { container, getByTestId } = customRender( 17 | 18 | 19 | {({ onHorizontallyScroll, tableRef, columnsCursor, fixedRowsIndexes }) => { 20 | return ( 21 | <> 22 | 23 | {({ toggleFixedRowIndex, isFixed }) => ( 24 |
25 | {isFixed ? "Unpin" : "Pin"} 26 |
27 | )} 28 |
29 |
44 | 45 | ); 46 | }} 47 | 48 | 49 | ); 50 | 51 | const pinBtn = getByTestId("pin-column"); 52 | expect(queryByText(container, "CA_TTC_OMNICANALF")).toBeFalsy(); 53 | // Pin the CA_TTC_OMNICANALF row 54 | fireEvent.click(pinBtn); 55 | expect(queryByText(container, "CA_TTC_OMNICANALF")).toBeTruthy(); 56 | // Unpin the CA_TTC_OMNICANALF row 57 | fireEvent.click(pinBtn); 58 | expect(queryByText(container, "CA_TTC_OMNICANALF")).toBeFalsy(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/it-tests/row-it.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { cleanup, fireEvent, getByTestId } from "@testing-library/react"; 3 | 4 | import { subRows, subMiam } from "../../stories/utils/tables"; 5 | import { customRender } from "../tests-utils/react-testing-library-utils"; 6 | import Table from "../../src/components/table/table"; 7 | 8 | describe("Row component integration tests", () => { 9 | test("should open and render a child row", () => { 10 | const props = subRows({ subsubRows: subMiam })[0]; 11 | const { container } = customRender(
); 12 | fireEvent.click(getByTestId(container, "table-cell-sub-item-toggle")); 13 | expect(container.getElementsByClassName("table-row")).toHaveLength(2); 14 | cleanup(); 15 | }); 16 | 17 | test("should open and render a child row openable with a rowSpan", () => { 18 | const props = subRows({ subsubRows: subMiam })[0]; 19 | const { container } = customRender(
); 20 | fireEvent.click(getByTestId(container, "table-toggle-row-btn")); 21 | expect(container.getElementsByClassName("table-row")).toHaveLength(2); 22 | cleanup(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/it-tests/rows-controller.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { fireEvent } from "@testing-library/react"; 4 | import { Button } from "@mui/material"; 5 | 6 | import TabeInteractionManager, { 7 | TableInteractionsContext, 8 | } from "../../src/components/table-interactions-manager/table-interactions-manager"; 9 | import { customRender } from "../tests-utils/react-testing-library-utils"; 10 | import Table from "../../src/components/table/table"; 11 | import { getTable } from "../../stories/components/styled-table/tables"; 12 | import { ITrees } from "../../src/components/table/elementary-table"; 13 | import { tableWithSubItems, subRows, subMiam } from "../../stories/utils/tables"; 14 | 15 | const defaultProps = getTable(); 16 | 17 | const table3Levels = tableWithSubItems({ 18 | firstSubRows: subRows({ subsubRows: subMiam }), 19 | }); 20 | 21 | describe("Rows controller", () => { 22 | test("should open and close the row", () => { 23 | const trees: ITrees = { 24 | 1: { 25 | rowIndex: 1, 26 | columnIndex: 0, 27 | subTrees: { 0: { rowIndex: 1, columnIndex: 2 } }, 28 | }, 29 | }; 30 | const { container, getByText } = customRender( 31 | 32 | 33 | {({ tableRef, openTrees, closeTrees }) => { 34 | return ( 35 | <> 36 | 39 | 42 |
43 | 44 | ); 45 | }} 46 | 47 | 48 | ); 49 | const cellSelector = '[data-testid="table-cell-wrapper-pizza"]'; 50 | expect(container.querySelector(cellSelector)).toBeFalsy(); 51 | // open the row 52 | fireEvent.click(getByText("Open the row")); 53 | 54 | expect(container.querySelector(cellSelector)).toBeTruthy(); 55 | // close the row 56 | fireEvent.click(getByText("Close the row")); 57 | 58 | expect(container.querySelector(cellSelector)).toBeFalsy(); 59 | }); 60 | 61 | test("should open and close the row with toggle btn", () => { 62 | const trees: ITrees = { 63 | 1: { 64 | rowIndex: 1, 65 | columnIndex: 0, 66 | subTrees: { 0: { rowIndex: 1, columnIndex: 2 } }, 67 | }, 68 | }; 69 | const { container, getByText } = customRender( 70 | 71 | 72 | {({ tableRef, openTrees, closeTrees, onTableUpdate, table }) => { 73 | const openedTrees = table?.current ? table.current.state.openedTrees : {}; 74 | const hasTableOpenedTrees = !!Object.keys(openedTrees).length; 75 | return ( 76 | <> 77 | {hasTableOpenedTrees ? ( 78 | 81 | ) : ( 82 | 85 | )} 86 |
94 | 95 | ); 96 | }} 97 | 98 | 99 | ); 100 | const closeBtnSelector = '[data-testid="close"]'; 101 | const openBtnSelector = '[data-testid="open"]'; 102 | const cellSelector = '[data-testid="table-cell-wrapper-pizza"]'; 103 | expect(container.querySelector(cellSelector)).toBeFalsy(); 104 | expect(container.querySelector(closeBtnSelector)).toBeFalsy(); 105 | // open the row 106 | fireEvent.click(getByText("Open the row")); 107 | 108 | expect(container.querySelector(cellSelector)).toBeTruthy(); 109 | expect(container.querySelector(closeBtnSelector)).toBeTruthy(); 110 | expect(container.querySelector(openBtnSelector)).toBeFalsy(); 111 | // close the row 112 | fireEvent.click(getByText("Close the row")); 113 | 114 | expect(container.querySelector(cellSelector)).toBeFalsy(); 115 | expect(container.querySelector(openBtnSelector)).toBeTruthy(); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test/it-tests/selectable-table.test.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import * as React from "react"; 3 | import { cleanup, fireEvent } from "@testing-library/react"; 4 | 5 | import { customRender } from "../tests-utils/react-testing-library-utils"; 6 | import { generateTable } from "../../stories/utils/tables"; 7 | import Table from "../../src/components/table/table"; 8 | import { fireMouseEvent, getCellsOfRow, getRows } from "../tests-utils/table"; 9 | import TableSelectionMenu, { IMenuProps, IMenuAction } from "../../src/components/table-selection/table-selection-menu"; 10 | 11 | const selectionClassName = "selected"; 12 | 13 | const actions: IMenuAction[] = [ 14 | { 15 | id: "foo", 16 | title: "Foo item", 17 | component: () =>
Foo
, 18 | }, 19 | { 20 | id: "bar", 21 | title: "Bar item", 22 | component: () =>
Foo
, 23 | }, 24 | ]; 25 | 26 | const MyTableSelectionMenu: React.FunctionComponent = (props) => { 27 | return ; 28 | }; 29 | 30 | describe("Selection behaviour", () => { 31 | test("should start selecting cells on mousedown", () => { 32 | const { container } = customRender(
); 33 | const rows = getRows(); 34 | const firstCell = getCellsOfRow(rows[0])[4]; 35 | fireMouseEvent(firstCell, "mousedown"); 36 | expect(firstCell.className.includes(selectionClassName)).toBeTruthy(); 37 | expect(container.getElementsByClassName(selectionClassName)).toHaveLength(1); 38 | cleanup(); 39 | }); 40 | 41 | test("should select cells between the mousedown and the last mouseover", () => { 42 | const { container } = customRender(
); 43 | const rows = getRows(); 44 | const firstCell = getCellsOfRow(rows[0])[4]; 45 | const secondCell = getCellsOfRow(rows[0])[5]; 46 | const lastCell = getCellsOfRow(rows[1])[6]; 47 | fireMouseEvent(firstCell, "mousedown"); 48 | fireMouseEvent(lastCell, "mouseover"); 49 | expect(secondCell.className.includes(selectionClassName)).toBeTruthy(); 50 | expect(lastCell.className.includes(selectionClassName)).toBeTruthy(); 51 | expect(container.getElementsByClassName(selectionClassName)).toHaveLength(6); 52 | cleanup(); 53 | }); 54 | 55 | test("should stop selecting cells after mouseup", () => { 56 | const { container } = customRender(
); 57 | const rows = getRows(); 58 | const firstCell = getCellsOfRow(rows[0])[4]; 59 | const secondCell = getCellsOfRow(rows[1])[6]; 60 | const lastCell = getCellsOfRow(rows[2])[6]; 61 | fireMouseEvent(firstCell, "mousedown"); 62 | fireMouseEvent(secondCell, "mouseover"); 63 | fireMouseEvent(secondCell, "mouseup"); 64 | fireMouseEvent(lastCell, "mouseover"); 65 | expect(lastCell.className.includes(selectionClassName)).toBeFalsy(); 66 | expect(container.getElementsByClassName(selectionClassName)).toHaveLength(6); 67 | cleanup(); 68 | }); 69 | 70 | test("should only select a row of cells", () => { 71 | const { container } = customRender( 72 |
79 | ); 80 | const rows = getRows(); 81 | const firstCell = getCellsOfRow(rows[0])[4]; 82 | const lastCell = getCellsOfRow(rows[1])[6]; 83 | fireMouseEvent(firstCell, "mousedown"); 84 | fireMouseEvent(lastCell, "mouseover"); 85 | fireMouseEvent(lastCell, "mouseup"); 86 | expect(lastCell.className.includes(selectionClassName)).toBeFalsy(); 87 | expect(container.getElementsByClassName(selectionClassName)).toHaveLength(3); 88 | cleanup(); 89 | }); 90 | 91 | test("should only select a column of cells", () => { 92 | const { container } = customRender( 93 |
100 | ); 101 | const rows = getRows(); 102 | const firstCell = getCellsOfRow(rows[0])[4]; 103 | const lastCell = getCellsOfRow(rows[1])[6]; 104 | fireMouseEvent(firstCell, "mousedown"); 105 | fireMouseEvent(lastCell, "mouseover"); 106 | fireMouseEvent(lastCell, "mouseup"); 107 | expect(lastCell.className.includes(selectionClassName)).toBeFalsy(); 108 | expect(container.getElementsByClassName(selectionClassName)).toHaveLength(2); 109 | cleanup(); 110 | }); 111 | 112 | test("should display menu", () => { 113 | const { getByText } = customRender( 114 |
120 | ); 121 | const rows = getRows(); 122 | const firstCell = getCellsOfRow(rows[0])[4]; 123 | const lastCell = getCellsOfRow(rows[1])[6]; 124 | fireMouseEvent(firstCell, "mousedown"); 125 | fireMouseEvent(lastCell, "mouseover"); 126 | fireMouseEvent(lastCell, "mouseup"); 127 | // open the menu 128 | fireEvent.contextMenu(lastCell); 129 | // select Foo item menu 130 | fireEvent.click(getByText("Foo item")); 131 | // menu closed => open the menu 132 | fireEvent.contextMenu(lastCell); 133 | // select Bar item menu 134 | fireEvent.click(getByText("Bar item")); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/polyfill/array.find.polyfill.js: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.find) { 2 | Object.defineProperty(Array.prototype, "find", { 3 | value(predicate) { 4 | // 1. Let O be ? ToObject(this value). 5 | if (this == null) { 6 | throw new TypeError('"this" is null or not defined'); 7 | } 8 | const o = new Object(this); 9 | // 2. Let len be ? ToLength(? Get(O, "length")). 10 | const len = o.length >>> 0; 11 | // 3. If IsCallable(predicate) is false, throw a TypeError exception. 12 | if (typeof predicate !== "function") { 13 | throw new TypeError("predicate must be a function"); 14 | } 15 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 16 | const thisArg = arguments[1]; 17 | // 5. Let k be 0. 18 | let k = 0; 19 | // 6. Repeat, while k < len 20 | while (k < len) { 21 | // a. Let Pk be ! ToString(k). 22 | // b. Let kValue be ? Get(O, Pk). 23 | // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). 24 | // d. If testResult is true, return kValue. 25 | const kValue = o[k]; 26 | if (predicate.call(thisArg, kValue, k, o)) { 27 | return kValue; 28 | } 29 | // e. Increase k by 1. 30 | k++; 31 | } 32 | // 7. Return undefined. 33 | return undefined; 34 | }, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /test/tests-utils/date.ts: -------------------------------------------------------------------------------- 1 | const RealDate = Date; 2 | 3 | export function mockDate(isoDate) { 4 | // @ts-ignore Type 'typeof Date' is not assignable to type 'DateConstructor' 5 | global.Date = class extends RealDate { 6 | constructor(...date) { 7 | // @ts-ignore Expected 0-7 arguments, but got 0 or more.ts(2556) 8 | super(...date); 9 | // @ts-ignore Expected 0-7 arguments, but got 0 or more.ts(2556) 10 | return date.length ? new RealDate(...date) : new RealDate(isoDate); 11 | } 12 | 13 | static now() { 14 | return new RealDate(isoDate).valueOf(); 15 | } 16 | }; 17 | } 18 | 19 | export function clearMockDate() { 20 | global.Date = RealDate; 21 | } 22 | -------------------------------------------------------------------------------- /test/tests-utils/react-testing-library-utils.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import * as React from "react"; 3 | import { render, RenderOptions } from "@testing-library/react"; 4 | import { createTheme, adaptV4Theme, StyledEngineProvider } from "@mui/material/styles"; 5 | import { ThemeProvider } from "@mui/styles"; 6 | 7 | export const blueDkt = "#0082c3"; 8 | 9 | const muiDktTheme = createTheme( 10 | adaptV4Theme({ 11 | typography: { 12 | fontFamily: "Roboto Condensed", 13 | }, 14 | palette: { 15 | primary: { 16 | light: blueDkt, 17 | dark: blueDkt, 18 | main: blueDkt, 19 | }, 20 | secondary: { 21 | light: blueDkt, 22 | dark: blueDkt, 23 | main: blueDkt, 24 | }, 25 | }, 26 | }) 27 | ); 28 | 29 | const AllTheProviders = ({ children }: { children: JSX.Element | JSX.Element[] }) => { 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }; 36 | 37 | // Just using the same typing as react-testing-library render method. 38 | type Omit = Pick>; 39 | 40 | export const customRender = (ui: React.ReactElement, options?: Omit) => 41 | // @ts-ignore 42 | render(ui, { wrapper: AllTheProviders, ...options }); 43 | -------------------------------------------------------------------------------- /test/tests-utils/table.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import { screen, fireEvent, getAllByTestId } from "@testing-library/react"; 3 | 4 | export const getRows = (isHeader = false, rowId?: string) => { 5 | const textMatch = `table-${isHeader ? "header" : "row"}${rowId !== undefined ? `-${rowId}` : ""}`; 6 | return screen.getAllByTestId(textMatch, { exact: rowId !== undefined }); 7 | }; 8 | 9 | export const getCellsOfRow = (row: HTMLElement) => getAllByTestId(row, "table-column"); 10 | 11 | export const fireMouseEvent = (element: Document | Element | Window, eventName: string) => 12 | fireEvent( 13 | element, 14 | new MouseEvent(eventName, { 15 | bubbles: true, 16 | cancelable: true, 17 | }) 18 | ); 19 | -------------------------------------------------------------------------------- /test/tests.entry.js: -------------------------------------------------------------------------------- 1 | require("isomorphic-fetch"); 2 | Object.assign = require("lodash").assign; 3 | const Enzyme = require("enzyme"); 4 | const Adapter = require("@wojtekmaj/enzyme-adapter-react-17"); 5 | 6 | const CommonUtils = require("../src/components/utils/table"); 7 | 8 | Enzyme.configure({ adapter: new Adapter() }); 9 | 10 | window.ga = function () {}; 11 | 12 | CommonUtils.getScrollbarSize = jest.fn(() => 15); 13 | -------------------------------------------------------------------------------- /test/typings/tests-entry.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | // / 3 | // / 4 | -------------------------------------------------------------------------------- /test/utils/common.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { getStringNumberWithoutTrailingZeros } from "../../src/components/utils"; 4 | 5 | describe("getStringNumberWithoutTrailingZeros", () => { 6 | test("should return 100 without its 1 trailing zero", () => { 7 | const decimals = 1; 8 | const value = 100.01; 9 | const result = getStringNumberWithoutTrailingZeros(value, decimals); 10 | expect(result).toEqual("100"); 11 | }); 12 | test("should return 100 without its 2 trailing zeros", () => { 13 | const decimals = 2; 14 | const value = 100.001; 15 | const result = getStringNumberWithoutTrailingZeros(value, decimals); 16 | expect(result).toEqual("100"); 17 | }); 18 | test("should round decimals and return 100.11", () => { 19 | const decimals = 2; 20 | const value = 100.105; 21 | const result = getStringNumberWithoutTrailingZeros(value, decimals); 22 | expect(result).toEqual("100.11"); 23 | }); 24 | test("should return 100 without its 1 trailing zero", () => { 25 | const decimals = 2; 26 | const value = 100.10001; 27 | const result = getStringNumberWithoutTrailingZeros(value, decimals); 28 | expect(result).toEqual("100.1"); 29 | }); 30 | test("should return number with its whole decimals", () => { 31 | const decimals = 4; 32 | const value = 100.0001; 33 | const result = getStringNumberWithoutTrailingZeros(value, decimals); 34 | expect(result).toEqual("100.0001"); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src", "./stories", "./test"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", 5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "module": "commonjs", 7 | /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "lib": [ 9 | /* Specify library files to be included in the compilation: */ 10 | "esnext", 11 | "dom", 12 | "es2016", 13 | "es2017" 14 | ], 15 | "jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 16 | "declaration": true /* Generates corresponding '.d.ts' file. */, 17 | "sourceMap": true, 18 | /* Generates corresponding '.map' file. */ 19 | "outDir": "./dist/", 20 | /* Strict Type-Checking Options */ 21 | "strict": true, 22 | /* Enable all strict type-checking options. */ 23 | "noImplicitAny": true, 24 | /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true /* Enable strict null checks. */, 26 | /* Additional Checks */ 27 | "esModuleInterop": true, 28 | "skipLibCheck": true, 29 | "forceConsistentCasingInFileNames": true, 30 | "noImplicitReturns": true, 31 | "noImplicitThis": true, 32 | "suppressImplicitAnyIndexErrors": true, 33 | "noUnusedLocals": true, 34 | "noUnusedParameters": true, 35 | /* Module Resolution Options */ 36 | "moduleResolution": "node" 37 | }, 38 | "include": ["./src"], 39 | "exclude": ["node_modules", "build"] 40 | } 41 | --------------------------------------------------------------------------------