├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── codecov.yml ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── api-extractor.json ├── babel.config.json ├── browserslist ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── Cell.tsx ├── Columns.tsx ├── DataGrid.tsx ├── DragHandle.tsx ├── EditCell.tsx ├── GroupCell.tsx ├── GroupRow.tsx ├── HeaderCell.tsx ├── HeaderRow.tsx ├── Row.tsx ├── SummaryCell.tsx ├── SummaryRow.tsx ├── editors │ └── TextEditor.tsx ├── formatters │ ├── SelectCellFormatter.tsx │ ├── ToggleGroupFormatter.tsx │ ├── ValueFormatter.tsx │ └── index.ts ├── headerCells │ └── SortableHeaderCell.tsx ├── hooks │ ├── index.ts │ ├── useCalculatedColumns.ts │ ├── useCombinedRefs.ts │ ├── useFocusRef.ts │ ├── useGridDimensions.ts │ ├── useLatestFunc.ts │ ├── useLayoutEffect.ts │ ├── useRovingCellRef.ts │ ├── useRovingRowRef.ts │ ├── useRowSelection.ts │ ├── useViewportColumns.ts │ └── useViewportRows.ts ├── index.ts ├── style │ ├── cell.ts │ ├── core.ts │ ├── index.ts │ └── row.ts ├── types.ts └── utils │ ├── colSpanUtils.ts │ ├── domUtils.ts │ ├── index.ts │ ├── keyboardUtils.ts │ └── selectedCellUtils.ts ├── test ├── TextEditor.test.tsx ├── column │ ├── cellClass.test.ts │ ├── colSpan.test.ts │ ├── editor.test.tsx │ ├── formatter.test.tsx │ ├── frozen.test.ts │ ├── headerCellClass.test.ts │ ├── headerRenderer.test.tsx │ ├── name.test.tsx │ ├── resizable.test.tsx │ ├── summaryCellClass.test.ts │ └── summaryFormatter.test.tsx ├── columnOrder.test.ts ├── copyPaste.test.tsx ├── dragFill.test.tsx ├── grouping.test.tsx ├── keyboardNavigation.test.tsx ├── noRowsFallback.test.tsx ├── rowClass.test.ts ├── rowHeight.test.ts ├── rowSelection.test.tsx ├── setup.ts ├── sorting.test.tsx ├── ssr.test.tsx ├── utils.tsx └── virtualization.test.ts ├── tsconfig.all.json ├── tsconfig.eslint.json ├── tsconfig.json ├── webpack.config.js └── website ├── Nav.tsx ├── demos ├── AllFeatures.tsx ├── CellNavigation.tsx ├── ColumnSpanning.tsx ├── ColumnsReordering.tsx ├── CommonFeatures.tsx ├── ContextMenu.tsx ├── Grouping.tsx ├── HeaderFilters.tsx ├── InfiniteScrolling.tsx ├── MasterDetail.tsx ├── MillionCells.tsx ├── NoRows.tsx ├── Resizable.tsx ├── RowsReordering.tsx ├── ScrollToRow.tsx ├── TreeView.tsx ├── VariableRowHeight.tsx ├── components │ ├── Editors │ │ └── DropDownEditor.tsx │ ├── Formatters │ │ ├── CellExpanderFormatter.tsx │ │ ├── ChildRowDeleteButton.tsx │ │ ├── ImageFormatter.tsx │ │ └── index.ts │ ├── HeaderRenderers │ │ ├── DraggableHeaderRenderer.tsx │ │ └── index.ts │ └── RowRenderers │ │ ├── DraggableRowRenderer.tsx │ │ └── index.ts └── exportUtils.tsx ├── index.html └── root.tsx /.eslintignore: -------------------------------------------------------------------------------- 1 | /.eslintrc.js 2 | /coverage 3 | /dist 4 | /lib 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @amanmahajan7 @nstepien 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | labels: Bug 5 | --- 6 | 7 | ## Describe the bug 8 | 9 | ## To Reproduce 10 | 11 | 1. 12 | 2. 13 | 14 | Link to code example: 15 | 16 | ## Expected behavior 17 | 18 | ## Environment 19 | 20 | - `react-data-grid` version: 21 | - `react`/`react-dom` version: 22 | 23 | ## Additional context 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Questions and discussions 3 | url: https://github.com/adazzle/react-data-grid/discussions 4 | about: Please check the discussions tab for help and discussions. 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a new feature or enhancement 4 | labels: Feature Request 5 | --- 6 | 7 | ## Use case 8 | 9 | ## Proposed solution 10 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | # https://docs.codecov.com/docs/commit-status 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | informational: true 7 | patch: off 8 | 9 | # https://docs.codecov.com/docs/pull-request-comments 10 | comment: 11 | layout: 'diff, files' 12 | require_changes: true 13 | 14 | # https://docs.codecov.com/docs/node 15 | parsers: 16 | javascript: 17 | enable_partials: yes 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: 'npm' 6 | directory: '/' 7 | schedule: 8 | interval: 'weekly' 9 | 10 | - package-ecosystem: 'github-actions' 11 | directory: '/' 12 | schedule: 13 | interval: 'weekly' 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | paths-ignore: 10 | - '**.md' 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: '16.8' 20 | check-latest: true 21 | - uses: actions/cache@v2 22 | with: 23 | path: ~/.npm 24 | key: npm-${{ hashFiles('package.json') }} 25 | - name: npm install 26 | run: npm i 27 | - name: Typecheck 28 | run: npm run typecheck 29 | - name: Test 30 | run: npm t -- --coverage --colors 31 | - name: Bundle 32 | run: npm run build 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.linaria-cache 2 | /coverage 3 | /dist 4 | /lib 5 | /node_modules 6 | /tmp 7 | /.eslintcache 8 | 9 | npm-debug.log 10 | **.orig 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps = true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.linaria-cache 2 | /coverage 3 | /dist 4 | /lib 5 | /node_modules 6 | /package-lock.json 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnSaveMode": "modifications" 5 | } 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Release process 2 | 3 | For maintainers only. 4 | 5 | - `cd` to the root of the repo. 6 | - Checkout the `main` branch. 7 | - Make sure your local branch is up to date, no unpushed or missing commits, stash any changes. 8 | - Update the changelog, if necessary, and commit. 9 | - Login to the `adazzle` npm account if you haven't already done so: 10 | - `npm login` 11 | - You can use `npm whoami` to check who you are logged in as. 12 | - Bump the version and publish on npm: 13 | 16 | - To release a new `beta` version: 17 | - `npm version prerelease --preid=beta -m "Publish %s"` 18 | - `npm publish --tag beta` 19 | - Relevant docs: 20 | - https://docs.npmjs.com/cli/v7/commands/npm-version 21 | - https://docs.npmjs.com/cli/v7/commands/npm-publish 22 | - https://docs.npmjs.com/cli/v7/using-npm/scripts 23 | - https://git-scm.com/docs/git-push 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original work Copyright (c) 2014 Prometheus Research 4 | Modified work Copyright 2015 Adazzle 5 | 6 | For the original source code please see https://github.com/prometheusresearch-archive/react-grid 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /api-extractor.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 3 | "mainEntryPointFilePath": "/tmp/index.d.ts", 4 | "apiReport": { 5 | "enabled": false 6 | }, 7 | "docModel": { 8 | "enabled": false 9 | }, 10 | "dtsRollup": { 11 | "enabled": true, 12 | "untrimmedFilePath": "/lib/index.d.ts" 13 | }, 14 | "tsdocMetadata": { 15 | "enabled": false 16 | }, 17 | "newlineKind": "lf", 18 | "messages": { 19 | "compilerMessageReporting": { 20 | "default": { 21 | "logLevel": "warning" 22 | } 23 | }, 24 | "extractorMessageReporting": { 25 | "default": { 26 | "logLevel": "none" 27 | } 28 | }, 29 | "tsdocMessageReporting": { 30 | "default": { 31 | "logLevel": "none" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "loose": true, 7 | "bugfixes": true, 8 | "shippedProposals": true 9 | } 10 | ], 11 | ["@babel/react", { "runtime": "automatic" }], 12 | "@babel/typescript", 13 | "@linaria" 14 | ], 15 | "plugins": [ 16 | "@babel/transform-runtime", 17 | ["optimize-clsx", { "functionNames": ["getCellClassname"] }] 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | last 2 chrome versions 2 | last 2 edge versions 3 | last 2 firefox versions 4 | last 2 safari versions 5 | maintained node versions 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // https://jestjs.io/docs/configuration 2 | 3 | export default { 4 | collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/types.ts'], 5 | coverageReporters: ['json'], 6 | restoreMocks: true, 7 | setupFiles: ['./test/setup.ts'], 8 | setupFilesAfterEnv: ['@testing-library/jest-dom'], 9 | testEnvironment: 'jsdom', 10 | testMatch: ['/test/**/*.test.*'] 11 | }; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supabase/react-data-grid", 3 | "version": "7.1.0-beta.7", 4 | "license": "MIT", 5 | "description": "Feature-rich and customizable data grid React component", 6 | "keywords": [ 7 | "react", 8 | "data grid" 9 | ], 10 | "repository": "github:supabase/react-data-grid", 11 | "bugs": { 12 | "url": "https://github.com/supabase/react-data-grid/issues" 13 | }, 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "module": "./lib/bundle.js", 18 | "require": "./lib/bundle.cjs", 19 | "default": "./lib/bundle.js" 20 | } 21 | }, 22 | "browser": "./lib/bundle.js", 23 | "main": "./lib/bundle.cjs", 24 | "module": "./lib/bundle.js", 25 | "types": "./lib/index.d.ts", 26 | "files": [ 27 | "lib" 28 | ], 29 | "sideEffects": false, 30 | "scripts": { 31 | "start": "webpack serve --mode=development", 32 | "build:website": "webpack --mode=production", 33 | "build": "rollup --config --no-stdin", 34 | "build:types": "tsc && api-extractor run --local --verbose", 35 | "test": "jest", 36 | "test:watch": "jest --watch", 37 | "eslint": "eslint --ext js,ts,tsx --max-warnings 0 -f codeframe --cache --color src test website", 38 | "eslint:fix": "npm run eslint -- --fix", 39 | "prettier:check": "prettier --check .", 40 | "prettier:format": "prettier --write .", 41 | "typecheck": "tsc -p tsconfig.all.json", 42 | "prepublishOnly": "npm install && npm run build && npm run build:types", 43 | "postpublish": "git push --follow-tags origin HEAD", 44 | "deploy:beta": "npm publish --tag beta --access public" 45 | }, 46 | "dependencies": { 47 | "clsx": "^1.1.1" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.14.6", 51 | "@babel/plugin-transform-runtime": "^7.14.5", 52 | "@babel/preset-env": "^7.14.7", 53 | "@babel/preset-react": "^7.14.5", 54 | "@babel/preset-typescript": "^7.14.5", 55 | "@babel/runtime": "^7.14.6", 56 | "@linaria/babel-preset": "^3.0.0-beta.12", 57 | "@linaria/core": "^3.0.0-beta.4", 58 | "@linaria/rollup": "^3.0.0-beta.12", 59 | "@linaria/shaker": "^3.0.0-beta.12", 60 | "@linaria/webpack5-loader": "^3.0.0-beta.12", 61 | "@microsoft/api-extractor": "^7.16.1", 62 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.0", 63 | "@rollup/plugin-babel": "^5.3.0", 64 | "@rollup/plugin-node-resolve": "^13.0.0", 65 | "@testing-library/jest-dom": "^5.14.1", 66 | "@testing-library/react": "^12.0.0", 67 | "@testing-library/user-event": "^13.1.9", 68 | "@types/faker": "^5.5.6", 69 | "@types/hoist-non-react-statics": "^3.3.1", 70 | "@types/jest": "^27.0.1", 71 | "@types/lodash": "^4.14.172", 72 | "@types/react": "^17.0.11", 73 | "@types/react-dom": "^17.0.8", 74 | "@types/react-router-dom": "^5.1.8", 75 | "@typescript-eslint/eslint-plugin": "^4.28.0", 76 | "@typescript-eslint/parser": "^4.28.0", 77 | "babel-loader": "^8.2.2", 78 | "babel-plugin-optimize-clsx": "^2.6.2", 79 | "css-loader": "^6.2.0", 80 | "css-minimizer-webpack-plugin": "^3.0.2", 81 | "eslint": "^7.29.0", 82 | "eslint-config-prettier": "^8.3.0", 83 | "eslint-plugin-jest": "^24.3.6", 84 | "eslint-plugin-jest-dom": "^3.9.0", 85 | "eslint-plugin-node": "^11.1.0", 86 | "eslint-plugin-react": "^7.24.0", 87 | "eslint-plugin-react-hooks": "^4.2.0", 88 | "eslint-plugin-sonarjs": "^0.10.0", 89 | "faker": "^5.5.3", 90 | "html-webpack-plugin": "^5.3.2", 91 | "jest": "^27.0.5", 92 | "jspdf": "^2.3.1", 93 | "jspdf-autotable": "^3.5.14", 94 | "lodash-es": "^4.17.21", 95 | "mini-css-extract-plugin": "^2.2.2", 96 | "postcss": "^8.3.6", 97 | "postcss-loader": "^6.1.1", 98 | "postcss-nested": "^5.0.6", 99 | "prettier": "2.4.0", 100 | "react": "^17.0.2", 101 | "react-contextmenu": "^2.14.0", 102 | "react-dnd": "^14.0.2", 103 | "react-dnd-html5-backend": "^14.0.0", 104 | "react-dom": "^17.0.2", 105 | "react-refresh": "^0.10.0", 106 | "react-router-dom": "^5.3.0", 107 | "rollup": "^2.52.3", 108 | "rollup-plugin-postcss": "^4.0.0", 109 | "style-loader": "^3.2.1", 110 | "typescript": "~4.4.2", 111 | "webpack": "^5.52.1", 112 | "webpack-cli": "^4.8.0", 113 | "webpack-dev-server": "^4.2.0", 114 | "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz" 115 | }, 116 | "peerDependencies": { 117 | "react": "^16.14 || ^17.0.0", 118 | "react-dom": "^16.14 || ^17.0.0" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { isAbsolute } from 'path'; 2 | import linaria from '@linaria/rollup'; 3 | import postcss from 'rollup-plugin-postcss'; 4 | import postcssNested from 'postcss-nested'; 5 | import { babel } from '@rollup/plugin-babel'; 6 | import nodeResolve from '@rollup/plugin-node-resolve'; 7 | import pkg from './package.json'; 8 | 9 | const extensions = ['.ts', '.tsx']; 10 | 11 | export default { 12 | input: './src/index.ts', 13 | output: [ 14 | { 15 | file: './lib/bundle.js', 16 | format: 'es', 17 | preferConst: true, 18 | sourcemap: true 19 | }, 20 | { 21 | file: './lib/bundle.cjs', 22 | format: 'cjs', 23 | preferConst: true, 24 | sourcemap: true, 25 | interop: false 26 | } 27 | ], 28 | external: (id) => !id.startsWith('.') && !id.startsWith('@linaria:') && !isAbsolute(id), 29 | plugins: [ 30 | linaria({ 31 | preprocessor: 'none', 32 | classNameSlug(hash) { 33 | // We add the package version as suffix to avoid style conflicts 34 | // between multiple versions of RDG on the same page. 35 | return `${hash}${pkg.version.replace('.', '')}`; 36 | } 37 | }), 38 | postcss({ 39 | plugins: [postcssNested], 40 | minimize: true, 41 | inject: { insertAt: 'top' } 42 | }), 43 | babel({ 44 | babelHelpers: 'runtime', 45 | extensions, 46 | // remove all comments except terser annotations 47 | // https://github.com/terser/terser#annotations 48 | // https://babeljs.io/docs/en/options#shouldprintcomment 49 | shouldPrintComment: (comment) => /^[@#]__.+__$/.test(comment) 50 | }), 51 | nodeResolve({ extensions }) 52 | ] 53 | }; 54 | -------------------------------------------------------------------------------- /src/Cell.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { css } from '@linaria/core'; 3 | 4 | import { getCellStyle, getCellClassname, isCellEditable } from './utils'; 5 | import type { CellRendererProps } from './types'; 6 | import { useRovingCellRef } from './hooks'; 7 | 8 | const cellCopied = css` 9 | background-color: #ccccff; 10 | `; 11 | 12 | const cellCopiedClassname = `rdg-cell-copied ${cellCopied}`; 13 | 14 | const cellDraggedOver = css` 15 | background-color: #ccccff; 16 | 17 | &.${cellCopied} { 18 | background-color: #9999ff; 19 | } 20 | `; 21 | 22 | const cellDraggedOverClassname = `rdg-cell-dragged-over ${cellDraggedOver}`; 23 | 24 | function Cell({ 25 | column, 26 | colSpan, 27 | isCellSelected, 28 | isCopied, 29 | isDraggedOver, 30 | row, 31 | dragHandle, 32 | onRowClick, 33 | onRowDoubleClick, 34 | onRowChange, 35 | selectCell, 36 | ...props 37 | }: CellRendererProps) { 38 | const { ref, tabIndex, onFocus } = useRovingCellRef(isCellSelected); 39 | 40 | const { cellClass } = column; 41 | const className = getCellClassname( 42 | column, 43 | { 44 | [cellCopiedClassname]: isCopied, 45 | [cellDraggedOverClassname]: isDraggedOver 46 | }, 47 | typeof cellClass === 'function' ? cellClass(row) : cellClass 48 | ); 49 | 50 | function selectCellWrapper(openEditor?: boolean | null) { 51 | selectCell(row, column, openEditor); 52 | } 53 | 54 | function handleClick() { 55 | selectCellWrapper(column.editorOptions?.editOnClick); 56 | onRowClick?.(row, column); 57 | } 58 | 59 | function handleContextMenu() { 60 | selectCellWrapper(); 61 | } 62 | 63 | function handleDoubleClick() { 64 | selectCellWrapper(true); 65 | onRowDoubleClick?.(row, column); 66 | } 67 | 68 | return ( 69 |
85 | {!column.rowGroup && ( 86 | <> 87 | 93 | {dragHandle} 94 | 95 | )} 96 |
97 | ); 98 | } 99 | 100 | export default memo(Cell) as (props: CellRendererProps) => JSX.Element; 101 | -------------------------------------------------------------------------------- /src/Columns.tsx: -------------------------------------------------------------------------------- 1 | import { SelectCellFormatter } from './formatters'; 2 | import { useRowSelection } from './hooks/useRowSelection'; 3 | import type { Column, FormatterProps, GroupFormatterProps } from './types'; 4 | import { stopPropagation } from './utils'; 5 | 6 | export const SELECT_COLUMN_KEY = 'select-row'; 7 | 8 | function SelectFormatter(props: FormatterProps) { 9 | const [isRowSelected, onRowSelectionChange] = useRowSelection(); 10 | 11 | return ( 12 | { 18 | onRowSelectionChange({ row: props.row, checked, isShiftClick }); 19 | }} 20 | /> 21 | ); 22 | } 23 | 24 | function SelectGroupFormatter(props: GroupFormatterProps) { 25 | const [isRowSelected, onRowSelectionChange] = useRowSelection(); 26 | 27 | return ( 28 | { 33 | onRowSelectionChange({ row: props.row, checked, isShiftClick: false }); 34 | }} 35 | // Stop propagation to prevent row selection 36 | onClick={stopPropagation} 37 | /> 38 | ); 39 | } 40 | 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | export const SelectColumn: Column = { 43 | key: SELECT_COLUMN_KEY, 44 | name: '', 45 | width: 35, 46 | maxWidth: 35, 47 | resizable: false, 48 | sortable: false, 49 | frozen: true, 50 | headerRenderer(props) { 51 | return ( 52 | 60 | ); 61 | }, 62 | formatter: SelectFormatter, 63 | groupFormatter: SelectGroupFormatter 64 | }; 65 | -------------------------------------------------------------------------------- /src/DragHandle.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@linaria/core'; 2 | 3 | import type { CalculatedColumn, FillEvent, Position } from './types'; 4 | import type { DataGridProps, SelectCellState } from './DataGrid'; 5 | 6 | const cellDragHandle = css` 7 | cursor: move; 8 | position: absolute; 9 | right: 0; 10 | bottom: 0; 11 | width: 8px; 12 | height: 8px; 13 | background-color: var(--selection-color); 14 | 15 | &:hover { 16 | width: 16px; 17 | height: 16px; 18 | border: 2px solid var(--selection-color); 19 | background-color: var(--background-color); 20 | } 21 | `; 22 | 23 | const cellDragHandleClassname = `rdg-cell-drag-handle ${cellDragHandle}`; 24 | 25 | interface Props extends Pick, 'rows' | 'onRowsChange'> { 26 | columns: readonly CalculatedColumn[]; 27 | selectedPosition: SelectCellState; 28 | latestDraggedOverRowIdx: React.MutableRefObject; 29 | isCellEditable: (position: Position) => boolean; 30 | onFill: (event: FillEvent) => R; 31 | setDragging: (isDragging: boolean) => void; 32 | setDraggedOverRowIdx: (overRowIdx: number | undefined) => void; 33 | } 34 | 35 | export default function DragHandle({ 36 | rows, 37 | columns, 38 | selectedPosition, 39 | latestDraggedOverRowIdx, 40 | isCellEditable, 41 | onRowsChange, 42 | onFill, 43 | setDragging, 44 | setDraggedOverRowIdx 45 | }: Props) { 46 | function handleMouseDown(event: React.MouseEvent) { 47 | if (event.buttons !== 1) return; 48 | setDragging(true); 49 | window.addEventListener('mouseover', onMouseOver); 50 | window.addEventListener('mouseup', onMouseUp); 51 | 52 | function onMouseOver(event: MouseEvent) { 53 | // Trigger onMouseup in edge cases where we release the mouse button but `mouseup` isn't triggered, 54 | // for example when releasing the mouse button outside the iframe the grid is rendered in. 55 | // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons 56 | if (event.buttons !== 1) onMouseUp(); 57 | } 58 | 59 | function onMouseUp() { 60 | window.removeEventListener('mouseover', onMouseOver); 61 | window.removeEventListener('mouseup', onMouseUp); 62 | setDragging(false); 63 | handleDragEnd(); 64 | } 65 | } 66 | 67 | function handleDragEnd() { 68 | const overRowIdx = latestDraggedOverRowIdx.current; 69 | if (overRowIdx === undefined) return; 70 | 71 | const { rowIdx } = selectedPosition; 72 | const startRowIndex = rowIdx < overRowIdx ? rowIdx + 1 : overRowIdx; 73 | const endRowIndex = rowIdx < overRowIdx ? overRowIdx + 1 : rowIdx; 74 | updateRows(startRowIndex, endRowIndex); 75 | setDraggedOverRowIdx(undefined); 76 | } 77 | 78 | function handleDoubleClick(event: React.MouseEvent) { 79 | event.stopPropagation(); 80 | updateRows(selectedPosition.rowIdx + 1, rows.length); 81 | } 82 | 83 | function updateRows(startRowIdx: number, endRowIdx: number) { 84 | const { idx, rowIdx } = selectedPosition; 85 | const column = columns[idx]; 86 | const sourceRow = rows[rowIdx]; 87 | const updatedRows = [...rows]; 88 | const indexes: number[] = []; 89 | for (let i = startRowIdx; i < endRowIdx; i++) { 90 | if (isCellEditable({ rowIdx: i, idx })) { 91 | const updatedRow = onFill({ columnKey: column.key, sourceRow, targetRow: rows[i] }); 92 | if (updatedRow !== rows[i]) { 93 | updatedRows[i] = updatedRow; 94 | indexes.push(i); 95 | } 96 | } 97 | } 98 | 99 | if (indexes.length > 0) { 100 | onRowsChange?.(updatedRows, { indexes, column }); 101 | } 102 | } 103 | 104 | return ( 105 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/EditCell.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { css } from '@linaria/core'; 3 | 4 | import { useLatestFunc } from './hooks'; 5 | import { getCellStyle, getCellClassname } from './utils'; 6 | import type { CellRendererProps, EditorProps } from './types'; 7 | 8 | /* 9 | * To check for outside `mousedown` events, we listen to all `mousedown` events at their birth, 10 | * i.e. on the window during the capture phase, and at their death, i.e. on the window during the bubble phase. 11 | * 12 | * We schedule a check at the birth of the event, cancel the check when the event reaches the "inside" container, 13 | * and trigger the "outside" callback when the event bubbles back up to the window. 14 | * 15 | * The event can be `stopPropagation()`ed halfway through, so they may not always bubble back up to the window, 16 | * so an alternative check must be used. The check must happen after the event can reach the "inside" container, 17 | * and not before it run to completion. `requestAnimationFrame` is the best way we know how to achieve this. 18 | * Usually we want click event handlers from parent components to access the latest commited values, 19 | * so `mousedown` is used instead of `click`. 20 | * 21 | * We must also rely on React's event capturing/bubbling to handle elements rendered in a portal. 22 | */ 23 | 24 | const cellEditing = css` 25 | &.rdg-cell { 26 | padding: 0; 27 | } 28 | `; 29 | 30 | type SharedCellRendererProps = Pick, 'colSpan'>; 31 | 32 | interface EditCellProps extends EditorProps, SharedCellRendererProps {} 33 | 34 | export default function EditCell({ 35 | column, 36 | colSpan, 37 | row, 38 | onRowChange, 39 | onClose 40 | }: EditCellProps) { 41 | const frameRequestRef = useRef(); 42 | 43 | // We need to prevent the `useEffect` from cleaning up between re-renders, 44 | // as `onWindowCaptureMouseDown` might otherwise miss valid mousedown events. 45 | // To that end we instead access the latest props via useLatestFunc. 46 | const commitOnOutsideMouseDown = useLatestFunc(() => { 47 | onRowChange(row, true); 48 | }); 49 | 50 | function cancelFrameRequest() { 51 | cancelAnimationFrame(frameRequestRef.current!); 52 | } 53 | 54 | useEffect(() => { 55 | function onWindowCaptureMouseDown() { 56 | frameRequestRef.current = requestAnimationFrame(commitOnOutsideMouseDown); 57 | } 58 | 59 | addEventListener('mousedown', onWindowCaptureMouseDown, { capture: true }); 60 | 61 | return () => { 62 | removeEventListener('mousedown', onWindowCaptureMouseDown, { capture: true }); 63 | cancelFrameRequest(); 64 | }; 65 | }, [commitOnOutsideMouseDown]); 66 | 67 | const { cellClass } = column; 68 | const className = getCellClassname( 69 | column, 70 | 'rdg-editor-container', 71 | !column.editorOptions?.renderFormatter && cellEditing, 72 | typeof cellClass === 'function' ? cellClass(row) : cellClass 73 | ); 74 | 75 | return ( 76 |
85 | {column.editor != null && ( 86 | <> 87 | 88 | {column.editorOptions?.renderFormatter && ( 89 | 90 | )} 91 | 92 | )} 93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/GroupCell.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { getCellStyle, getCellClassname } from './utils'; 4 | import type { CalculatedColumn, GroupRow } from './types'; 5 | import type { GroupRowRendererProps } from './GroupRow'; 6 | import { useRovingCellRef } from './hooks'; 7 | 8 | type SharedGroupRowRendererProps = Pick< 9 | GroupRowRendererProps, 10 | 'id' | 'groupKey' | 'childRows' | 'isExpanded' | 'toggleGroup' 11 | >; 12 | 13 | interface GroupCellProps extends SharedGroupRowRendererProps { 14 | column: CalculatedColumn; 15 | row: GroupRow; 16 | isCellSelected: boolean; 17 | groupColumnIndex: number; 18 | } 19 | 20 | function GroupCell({ 21 | id, 22 | groupKey, 23 | childRows, 24 | isExpanded, 25 | isCellSelected, 26 | column, 27 | row, 28 | groupColumnIndex, 29 | toggleGroup: toggleGroupWrapper 30 | }: GroupCellProps) { 31 | const { ref, tabIndex, onFocus } = useRovingCellRef(isCellSelected); 32 | 33 | function toggleGroup() { 34 | toggleGroupWrapper(id); 35 | } 36 | 37 | // Only make the cell clickable if the group level matches 38 | const isLevelMatching = column.rowGroup && groupColumnIndex === column.idx; 39 | 40 | return ( 41 |
56 | {(!column.rowGroup || groupColumnIndex === column.idx) && column.groupFormatter && ( 57 | 66 | )} 67 |
68 | ); 69 | } 70 | 71 | export default memo(GroupCell) as (props: GroupCellProps) => JSX.Element; 72 | -------------------------------------------------------------------------------- /src/GroupRow.tsx: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'react'; 2 | import { memo } from 'react'; 3 | import clsx from 'clsx'; 4 | import { css } from '@linaria/core'; 5 | 6 | import { cell, cellFrozenLast, rowClassname } from './style'; 7 | import { SELECT_COLUMN_KEY } from './Columns'; 8 | import GroupCell from './GroupCell'; 9 | import type { CalculatedColumn, GroupRow, Omit } from './types'; 10 | import { RowSelectionProvider, useRovingRowRef } from './hooks'; 11 | 12 | export interface GroupRowRendererProps 13 | extends Omit, 'style' | 'children'> { 14 | id: string; 15 | groupKey: unknown; 16 | viewportColumns: readonly CalculatedColumn[]; 17 | childRows: readonly R[]; 18 | rowIdx: number; 19 | row: GroupRow; 20 | top: number; 21 | height: number; 22 | level: number; 23 | selectedCellIdx: number | undefined; 24 | isExpanded: boolean; 25 | isRowSelected: boolean; 26 | selectGroup: (rowIdx: number) => void; 27 | toggleGroup: (expandedGroupId: unknown) => void; 28 | } 29 | 30 | const groupRow = css` 31 | &:not([aria-selected='true']) { 32 | background-color: var(--header-background-color); 33 | } 34 | 35 | > .${cell}:not(:last-child):not(.${cellFrozenLast}) { 36 | border-right: none; 37 | } 38 | `; 39 | 40 | const groupRowClassname = `rdg-group-row ${groupRow}`; 41 | 42 | function GroupedRow({ 43 | id, 44 | groupKey, 45 | viewportColumns, 46 | childRows, 47 | rowIdx, 48 | row, 49 | top, 50 | height, 51 | level, 52 | isExpanded, 53 | selectedCellIdx, 54 | isRowSelected, 55 | selectGroup, 56 | toggleGroup, 57 | ...props 58 | }: GroupRowRendererProps) { 59 | const { ref, tabIndex, className } = useRovingRowRef(selectedCellIdx); 60 | 61 | // Select is always the first column 62 | const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? level + 1 : level; 63 | 64 | function handleSelectGroup() { 65 | selectGroup(rowIdx); 66 | } 67 | 68 | return ( 69 | 70 |
91 | {viewportColumns.map((column) => ( 92 | 104 | ))} 105 |
106 |
107 | ); 108 | } 109 | 110 | export default memo(GroupedRow) as (props: GroupRowRendererProps) => JSX.Element; 111 | -------------------------------------------------------------------------------- /src/HeaderCell.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@linaria/core'; 2 | 3 | import type { CalculatedColumn, SortColumn } from './types'; 4 | import type { HeaderRowProps } from './HeaderRow'; 5 | import SortableHeaderCell from './headerCells/SortableHeaderCell'; 6 | import { getCellStyle, getCellClassname } from './utils'; 7 | import { useRovingCellRef } from './hooks'; 8 | 9 | const cellResizable = css` 10 | touch-action: none; 11 | &::after { 12 | content: ''; 13 | cursor: col-resize; 14 | position: absolute; 15 | top: 0; 16 | right: 0; 17 | bottom: 0; 18 | width: 10px; 19 | } 20 | `; 21 | 22 | const cellResizableClassname = `rdg-cell-resizable ${cellResizable}`; 23 | 24 | type SharedHeaderRowProps = Pick< 25 | HeaderRowProps, 26 | | 'sortColumns' 27 | | 'onSortColumnsChange' 28 | | 'allRowsSelected' 29 | | 'onAllRowsSelectionChange' 30 | | 'selectCell' 31 | | 'onColumnResize' 32 | | 'shouldFocusGrid' 33 | >; 34 | 35 | export interface HeaderCellProps extends SharedHeaderRowProps { 36 | column: CalculatedColumn; 37 | colSpan: number | undefined; 38 | isCellSelected: boolean; 39 | } 40 | 41 | export default function HeaderCell({ 42 | column, 43 | colSpan, 44 | isCellSelected, 45 | onColumnResize, 46 | allRowsSelected, 47 | onAllRowsSelectionChange, 48 | sortColumns, 49 | onSortColumnsChange, 50 | selectCell, 51 | shouldFocusGrid 52 | }: HeaderCellProps) { 53 | const { ref, tabIndex, onFocus } = useRovingCellRef(isCellSelected); 54 | const sortIndex = sortColumns?.findIndex((sort) => sort.columnKey === column.key); 55 | const sortColumn = 56 | sortIndex !== undefined && sortIndex > -1 ? sortColumns![sortIndex] : undefined; 57 | const sortDirection = sortColumn?.direction; 58 | const priority = sortColumn !== undefined && sortColumns!.length > 1 ? sortIndex! + 1 : undefined; 59 | const ariaSort = 60 | sortDirection && !priority ? (sortDirection === 'ASC' ? 'ascending' : 'descending') : undefined; 61 | 62 | const className = getCellClassname(column, column.headerCellClass, { 63 | [cellResizableClassname]: column.resizable 64 | }); 65 | 66 | function onPointerDown(event: React.PointerEvent) { 67 | if (event.pointerType === 'mouse' && event.buttons !== 1) { 68 | return; 69 | } 70 | 71 | const { currentTarget, pointerId } = event; 72 | const { right } = currentTarget.getBoundingClientRect(); 73 | const offset = right - event.clientX; 74 | 75 | if (offset > 11) { 76 | // +1px to account for the border size 77 | return; 78 | } 79 | 80 | function onPointerMove(event: PointerEvent) { 81 | const width = event.clientX + offset - currentTarget.getBoundingClientRect().left; 82 | if (width > 0) { 83 | onColumnResize(column, width); 84 | } 85 | } 86 | 87 | function onLostPointerCapture() { 88 | currentTarget.removeEventListener('pointermove', onPointerMove); 89 | currentTarget.removeEventListener('lostpointercapture', onLostPointerCapture); 90 | } 91 | 92 | currentTarget.setPointerCapture(pointerId); 93 | currentTarget.addEventListener('pointermove', onPointerMove); 94 | currentTarget.addEventListener('lostpointercapture', onLostPointerCapture); 95 | } 96 | 97 | function onSort(ctrlClick: boolean) { 98 | if (onSortColumnsChange == null) return; 99 | const { sortDescendingFirst } = column; 100 | if (sortColumn === undefined) { 101 | // not currently sorted 102 | const nextSort: SortColumn = { 103 | columnKey: column.key, 104 | direction: sortDescendingFirst ? 'DESC' : 'ASC' 105 | }; 106 | onSortColumnsChange(sortColumns && ctrlClick ? [...sortColumns, nextSort] : [nextSort]); 107 | } else { 108 | let nextSortColumn: SortColumn | undefined; 109 | if ( 110 | (sortDescendingFirst && sortDirection === 'DESC') || 111 | (!sortDescendingFirst && sortDirection === 'ASC') 112 | ) { 113 | nextSortColumn = { 114 | columnKey: column.key, 115 | direction: sortDirection === 'ASC' ? 'DESC' : 'ASC' 116 | }; 117 | } 118 | if (ctrlClick) { 119 | const nextSortColumns = [...sortColumns!]; 120 | if (nextSortColumn) { 121 | // swap direction 122 | nextSortColumns[sortIndex!] = nextSortColumn; 123 | } else { 124 | // remove sort 125 | nextSortColumns.splice(sortIndex!, 1); 126 | } 127 | onSortColumnsChange(nextSortColumns); 128 | } else { 129 | onSortColumnsChange(nextSortColumn ? [nextSortColumn] : []); 130 | } 131 | } 132 | } 133 | 134 | function onClick() { 135 | selectCell(column.idx); 136 | } 137 | 138 | function handleFocus(event: React.FocusEvent) { 139 | onFocus(event); 140 | if (shouldFocusGrid) { 141 | // Select the first header cell if there is no selected cell 142 | selectCell(0); 143 | } 144 | } 145 | 146 | function getCell() { 147 | if (column.headerRenderer) { 148 | return ( 149 | 158 | ); 159 | } 160 | 161 | if (column.sortable) { 162 | return ( 163 | 169 | {column.name} 170 | 171 | ); 172 | } 173 | 174 | return column.name; 175 | } 176 | 177 | return ( 178 |
193 | {getCell()} 194 |
195 | ); 196 | } -------------------------------------------------------------------------------- /src/HeaderRow.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import clsx from 'clsx'; 3 | import { css } from '@linaria/core'; 4 | 5 | import HeaderCell from './HeaderCell'; 6 | import type { CalculatedColumn } from './types'; 7 | import { getColSpan } from './utils'; 8 | import type { DataGridProps } from './DataGrid'; 9 | import { useRovingRowRef } from './hooks'; 10 | 11 | type SharedDataGridProps = Pick< 12 | DataGridProps, 13 | 'sortColumns' | 'onSortColumnsChange' 14 | >; 15 | 16 | export interface HeaderRowProps extends SharedDataGridProps { 17 | columns: readonly CalculatedColumn[]; 18 | allRowsSelected: boolean; 19 | onAllRowsSelectionChange: (checked: boolean) => void; 20 | onColumnResize: (column: CalculatedColumn, width: number) => void; 21 | selectCell: (columnIdx: number) => void; 22 | lastFrozenColumnIndex: number; 23 | selectedCellIdx: number | undefined; 24 | shouldFocusGrid: boolean; 25 | } 26 | 27 | const headerRow = css` 28 | contain: strict; 29 | contain: size layout style paint; 30 | display: grid; 31 | grid-template-columns: var(--template-columns); 32 | grid-template-rows: var(--header-row-height); 33 | height: var(--header-row-height); /* needed on Firefox */ 34 | line-height: var(--header-row-height); 35 | width: var(--row-width); 36 | position: sticky; 37 | top: 0; 38 | background-color: var(--header-background-color); 39 | font-weight: bold; 40 | z-index: 3; 41 | outline: none; 42 | &[aria-selected='true'] { 43 | box-shadow: inset 0 0 0 2px var(--selection-color); 44 | } 45 | `; 46 | 47 | const headerRowClassname = `rdg-header-row ${headerRow}`; 48 | 49 | function HeaderRow({ 50 | columns, 51 | allRowsSelected, 52 | onAllRowsSelectionChange, 53 | onColumnResize, 54 | sortColumns, 55 | onSortColumnsChange, 56 | lastFrozenColumnIndex, 57 | selectedCellIdx, 58 | selectCell, 59 | shouldFocusGrid 60 | }: HeaderRowProps) { 61 | const { ref, tabIndex, className } = useRovingRowRef(selectedCellIdx); 62 | 63 | const cells = []; 64 | for (let index = 0; index < columns.length; index++) { 65 | const column = columns[index]; 66 | const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'HEADER' }); 67 | if (colSpan !== undefined) { 68 | index += colSpan - 1; 69 | } 70 | 71 | cells.push( 72 | 73 | key={column.key} 74 | column={column} 75 | colSpan={colSpan} 76 | isCellSelected={selectedCellIdx === column.idx} 77 | onColumnResize={onColumnResize} 78 | allRowsSelected={allRowsSelected} 79 | onAllRowsSelectionChange={onAllRowsSelectionChange} 80 | onSortColumnsChange={onSortColumnsChange} 81 | sortColumns={sortColumns} 82 | selectCell={selectCell} 83 | shouldFocusGrid={shouldFocusGrid && index === 0} 84 | /> 85 | ); 86 | } 87 | 88 | return ( 89 |
96 | {cells} 97 |
98 | ); 99 | } 100 | 101 | export default memo(HeaderRow) as ( 102 | props: HeaderRowProps 103 | ) => JSX.Element; -------------------------------------------------------------------------------- /src/Row.tsx: -------------------------------------------------------------------------------- 1 | import { memo, forwardRef } from 'react'; 2 | import type { RefAttributes, CSSProperties } from 'react'; 3 | import clsx from 'clsx'; 4 | 5 | import Cell from './Cell'; 6 | import { RowSelectionProvider, useLatestFunc, useCombinedRefs, useRovingRowRef } from './hooks'; 7 | import { getColSpan } from './utils'; 8 | import { rowClassname } from './style'; 9 | import type { RowRendererProps } from './types'; 10 | 11 | function Row( 12 | { 13 | className, 14 | rowIdx, 15 | selectedCellIdx, 16 | isRowSelected, 17 | copiedCellIdx, 18 | draggedOverCellIdx, 19 | lastFrozenColumnIndex, 20 | row, 21 | viewportColumns, 22 | selectedCellEditor, 23 | selectedCellDragHandle, 24 | onRowClick, 25 | onRowDoubleClick, 26 | rowClass, 27 | setDraggedOverRowIdx, 28 | onMouseEnter, 29 | top, 30 | height, 31 | onRowChange, 32 | selectCell, 33 | ...props 34 | }: RowRendererProps, 35 | ref: React.Ref 36 | ) { 37 | const { ref: rowRef, tabIndex, className: rovingClassName } = useRovingRowRef(selectedCellIdx); 38 | 39 | const handleRowChange = useLatestFunc((newRow: R) => { 40 | onRowChange(rowIdx, newRow); 41 | }); 42 | 43 | function handleDragEnter(event: React.MouseEvent) { 44 | setDraggedOverRowIdx?.(rowIdx); 45 | onMouseEnter?.(event); 46 | } 47 | 48 | className = clsx( 49 | rowClassname, 50 | `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, 51 | rovingClassName, 52 | rowClass?.(row), 53 | className 54 | ); 55 | 56 | const cells = []; 57 | 58 | for (let index = 0; index < viewportColumns.length; index++) { 59 | const column = viewportColumns[index]; 60 | const { idx } = column; 61 | const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'ROW', row }); 62 | if (colSpan !== undefined) { 63 | index += colSpan - 1; 64 | } 65 | 66 | const isCellSelected = selectedCellIdx === idx; 67 | 68 | if (isCellSelected && selectedCellEditor) { 69 | cells.push(selectedCellEditor); 70 | } else { 71 | cells.push( 72 | 86 | ); 87 | } 88 | } 89 | 90 | return ( 91 | 92 |
106 | {cells} 107 |
108 |
109 | ); 110 | } 111 | 112 | export default memo(Row) as (props: RowRendererProps) => JSX.Element; 113 | 114 | export const RowWithRef = memo(forwardRef(Row)) as ( 115 | props: RowRendererProps & RefAttributes 116 | ) => JSX.Element; 117 | -------------------------------------------------------------------------------- /src/SummaryCell.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { getCellStyle, getCellClassname } from './utils'; 4 | import type { CalculatedColumn, CellRendererProps } from './types'; 5 | import { useRovingCellRef } from './hooks'; 6 | 7 | interface SharedCellRendererProps 8 | extends Pick, 'column' | 'colSpan' | 'isCellSelected'> { 9 | selectCell: (row: SR, column: CalculatedColumn) => void; 10 | } 11 | 12 | interface SummaryCellProps extends SharedCellRendererProps { 13 | row: SR; 14 | } 15 | 16 | function SummaryCell({ 17 | column, 18 | colSpan, 19 | row, 20 | isCellSelected, 21 | selectCell 22 | }: SummaryCellProps) { 23 | const { ref, tabIndex, onFocus } = useRovingCellRef(isCellSelected); 24 | const { summaryFormatter: SummaryFormatter, summaryCellClass } = column; 25 | const className = getCellClassname( 26 | column, 27 | typeof summaryCellClass === 'function' ? summaryCellClass(row) : summaryCellClass 28 | ); 29 | 30 | function onClick() { 31 | selectCell(row, column); 32 | } 33 | 34 | return ( 35 |
47 | {SummaryFormatter && ( 48 | 49 | )} 50 |
51 | ); 52 | } 53 | 54 | export default memo(SummaryCell) as (props: SummaryCellProps) => JSX.Element; 55 | -------------------------------------------------------------------------------- /src/SummaryRow.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import clsx from 'clsx'; 3 | import { css } from '@linaria/core'; 4 | 5 | import { cell, row, rowClassname } from './style'; 6 | import { getColSpan } from './utils'; 7 | import SummaryCell from './SummaryCell'; 8 | import type { CalculatedColumn, RowRendererProps } from './types'; 9 | import { useRovingRowRef } from './hooks'; 10 | 11 | type SharedRowRendererProps = Pick, 'viewportColumns' | 'rowIdx'>; 12 | 13 | interface SummaryRowProps extends SharedRowRendererProps { 14 | 'aria-rowindex': number; 15 | row: SR; 16 | bottom: number; 17 | lastFrozenColumnIndex: number; 18 | selectedCellIdx: number | undefined; 19 | selectCell: (row: SR, column: CalculatedColumn) => void; 20 | } 21 | 22 | const summaryRow = css` 23 | &.${row} { 24 | position: sticky; 25 | z-index: 3; 26 | grid-template-rows: var(--summary-row-height); 27 | height: var(--summary-row-height); /* needed on Firefox */ 28 | line-height: var(--summary-row-height); 29 | } 30 | `; 31 | 32 | const summaryRowBorderClassname = css` 33 | & > .${cell} { 34 | border-top: 2px solid var(--summary-border-color); 35 | } 36 | `; 37 | 38 | const summaryRowClassname = `rdg-summary-row ${summaryRow}`; 39 | 40 | function SummaryRow({ 41 | rowIdx, 42 | row, 43 | viewportColumns, 44 | bottom, 45 | lastFrozenColumnIndex, 46 | selectedCellIdx, 47 | selectCell, 48 | 'aria-rowindex': ariaRowIndex 49 | }: SummaryRowProps) { 50 | const { ref, tabIndex, className } = useRovingRowRef(selectedCellIdx); 51 | const cells = []; 52 | for (let index = 0; index < viewportColumns.length; index++) { 53 | const column = viewportColumns[index]; 54 | const colSpan = getColSpan(column, lastFrozenColumnIndex, { type: 'SUMMARY', row }); 55 | if (colSpan !== undefined) { 56 | index += colSpan - 1; 57 | } 58 | 59 | const isCellSelected = selectedCellIdx === column.idx; 60 | 61 | cells.push( 62 | 63 | key={column.key} 64 | column={column} 65 | colSpan={colSpan} 66 | row={row} 67 | isCellSelected={isCellSelected} 68 | selectCell={selectCell} 69 | /> 70 | ); 71 | } 72 | 73 | return ( 74 |
88 | {cells} 89 |
90 | ); 91 | } 92 | 93 | export default memo(SummaryRow) as (props: SummaryRowProps) => JSX.Element; 94 | -------------------------------------------------------------------------------- /src/editors/TextEditor.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@linaria/core'; 2 | import type { EditorProps } from '../types'; 3 | 4 | const textEditor = css` 5 | appearance: none; 6 | 7 | box-sizing: border-box; 8 | width: 100%; 9 | height: 100%; 10 | padding: 0px 6px 0 6px; 11 | border: 2px solid #ccc; 12 | vertical-align: top; 13 | color: var(--color); 14 | background-color: var(--background-color); 15 | 16 | font-family: inherit; 17 | font-size: var(--font-size); 18 | 19 | &:focus { 20 | border-color: var(--selection-color); 21 | outline: none; 22 | } 23 | 24 | &::placeholder { 25 | color: #999; 26 | opacity: 1; 27 | } 28 | `; 29 | 30 | export const textEditorClassname = `rdg-text-editor ${textEditor}`; 31 | 32 | function autoFocusAndSelect(input: HTMLInputElement | null) { 33 | input?.focus(); 34 | input?.select(); 35 | } 36 | 37 | export default function TextEditor({ 38 | row, 39 | column, 40 | onRowChange, 41 | onClose 42 | }: EditorProps) { 43 | return ( 44 | onRowChange({ ...row, [column.key]: event.target.value })} 49 | onBlur={() => onClose(true)} 50 | /> 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/formatters/SelectCellFormatter.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { css } from '@linaria/core'; 3 | import { useFocusRef } from '../hooks/useFocusRef'; 4 | 5 | const checkboxLabel = css` 6 | cursor: pointer; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | position: absolute; 11 | inset: 0; 12 | margin-right: 1px; /* align checkbox in row group cell */ 13 | `; 14 | 15 | const checkboxLabelClassname = `rdg-checkbox-label ${checkboxLabel}`; 16 | 17 | const checkboxInput = css` 18 | all: unset; 19 | width: 0; 20 | margin: 0; 21 | `; 22 | 23 | const checkboxInputClassname = `rdg-checkbox-input ${checkboxInput}`; 24 | 25 | const checkbox = css` 26 | content: ''; 27 | width: 20px; 28 | height: 20px; 29 | border: 2px solid var(--border-color); 30 | background-color: var(--background-color); 31 | 32 | .${checkboxInput}:checked + & { 33 | background-color: var(--checkbox-color); 34 | box-shadow: inset 0px 0px 0px 4px var(--background-color); 35 | } 36 | 37 | .${checkboxInput}:focus + & { 38 | border-color: var(--checkbox-focus-color); 39 | } 40 | `; 41 | 42 | const checkboxClassname = `rdg-checkbox ${checkbox}`; 43 | 44 | const checkboxLabelDisabled = css` 45 | cursor: default; 46 | 47 | .${checkbox} { 48 | border-color: var(--checkbox-disabled-border-color); 49 | background-color: var(--checkbox-disabled-background-color); 50 | } 51 | `; 52 | 53 | const checkboxLabelDisabledClassname = `rdg-checkbox-label-disabled ${checkboxLabelDisabled}`; 54 | 55 | type SharedInputProps = Pick< 56 | React.InputHTMLAttributes, 57 | 'disabled' | 'onClick' | 'aria-label' | 'aria-labelledby' 58 | >; 59 | 60 | interface SelectCellFormatterProps extends SharedInputProps { 61 | isCellSelected: boolean; 62 | value: boolean; 63 | onChange: (value: boolean, isShiftClick: boolean) => void; 64 | } 65 | 66 | export function SelectCellFormatter({ 67 | value, 68 | isCellSelected, 69 | disabled, 70 | onClick, 71 | onChange, 72 | 'aria-label': ariaLabel, 73 | 'aria-labelledby': ariaLabelledBy 74 | }: SelectCellFormatterProps) { 75 | const { ref, tabIndex } = useFocusRef(isCellSelected); 76 | 77 | function handleChange(e: React.ChangeEvent) { 78 | onChange(e.target.checked, (e.nativeEvent as MouseEvent).shiftKey); 79 | } 80 | 81 | return ( 82 |