diff --git a/.babelrc b/.babelrc index ea374ae0..2c6a9fec 100644 --- a/.babelrc +++ b/.babelrc @@ -30,7 +30,8 @@ ], [ "@babel/preset-react" - ] + ], + "@babel/preset-typescript" ], "plugins": [ [ diff --git a/eslint.config.js b/eslint.config.js index 42f6a216..fbbbfe7a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,4 @@ +const tseslint = require('typescript-eslint'); const pluginReact = require('eslint-plugin-react'); const pluginJsxA11y = require('eslint-plugin-jsx-a11y'); const pluginReactHooks = require('eslint-plugin-react-hooks'); @@ -7,16 +8,12 @@ module.exports = [ { ignores: ['dist/**', 'build/**', 'coverage/**', 'docs/**'], }, + ...tseslint.configs.recommended, { - files: ['src/**/*.{js,jsx}', 'examples/**/*.{js,jsx}', 'test/**/*.{js,jsx}', '*.js'], + files: ['src/**/*.{ts,tsx}', 'test/**/*.{ts,tsx}', '*.{js,ts}'], languageOptions: { ecmaVersion: 2020, sourceType: 'module', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, globals: { ...globals.browser, ...globals.node, @@ -38,8 +35,6 @@ module.exports = [ ...pluginJsxA11y.configs.recommended.rules, 'no-console': 'off', semi: 2, - 'no-undef': 2, - 'no-undef-init': 2, 'no-tabs': 2, 'react/self-closing-comp': 2, 'react/no-typos': 2, diff --git a/package-lock.json b/package-lock.json index 78afe91e..49a17540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.3", "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@babel/register": "^7.29.3", "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@emotion/react": "^11.14.0", @@ -48,6 +49,18 @@ "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", + "@types/chai": "^5.2.3", + "@types/lodash.assignwith": "^4.2.9", + "@types/lodash.clonedeep": "^4.5.9", + "@types/lodash.find": "^4.6.9", + "@types/lodash.isundefined": "^3.0.9", + "@types/lodash.merge": "^4.6.9", + "@types/mocha": "^10.0.10", + "@types/node": "^25.6.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/sinon": "^21.0.1", + "ajv": "^8.20.0", "babel-loader": "^10.1.1", "babel-plugin-istanbul": "^8.0.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", @@ -86,6 +99,8 @@ "simulant": "^0.2.2", "sinon": "^21.1.2", "style-loader": "^4.0.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.2", "webpack": "^5.106.2", "webpack-cli": "^7.0.2", "webpack-dev-server": "^5.2.3" @@ -713,6 +728,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", @@ -1609,6 +1640,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", @@ -1812,6 +1863,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/register": { "version": "7.29.3", "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.3.tgz", @@ -4726,6 +4797,17 @@ "@types/node": "*" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4747,6 +4829,13 @@ "@types/node": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -4847,6 +4936,63 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.assignwith": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@types/lodash.assignwith/-/lodash.assignwith-4.2.9.tgz", + "integrity": "sha512-VbhcGOLjk/XPlF74Yfa1AUC2TrR8xYur28V8FWLTWRkAsxqXeyiJcTG7dCzZAjHXAUM3gRT8opYX0/wFlL0+ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.find": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.find/-/lodash.find-4.6.9.tgz", + "integrity": "sha512-v1qcNZs0ksl0+BO+aflReb8oIDHLc+rSc4n+RYZ3pMaxIFw2/mcW/wlTYFqrh+8dY1ZqRbX22ytFfUgs4sAXtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.isundefined": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.isundefined/-/lodash.isundefined-3.0.9.tgz", + "integrity": "sha512-nF+msH20xJzIJyZMBfArDnYxibF/Kt80y9naFYULzZ2yc4eBDAyxVDaf8DXyDWefHDh9CAaUoFp6XhMn38a26g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4854,10 +5000,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "version": "25.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", + "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", "dev": true, "license": "MIT", "dependencies": { @@ -4892,6 +5045,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", @@ -4959,6 +5132,23 @@ "@types/node": "*" } }, + "node_modules/@types/sinon": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz", + "integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -4979,6 +5169,262 @@ "@types/node": "*" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -5229,16 +5675,16 @@ } }, "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -5263,30 +5709,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", @@ -5612,6 +6034,16 @@ "node": ">=12.0.0" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -7924,6 +8356,23 @@ "webpack": "^5.0.0" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -7956,6 +8405,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", @@ -10328,9 +10784,9 @@ "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, @@ -12488,6 +12944,23 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/raw-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/raw-loader/node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -12498,6 +12971,13 @@ "ajv": "^6.9.1" } }, + "node_modules/raw-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/raw-loader/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -13419,30 +13899,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14574,6 +15030,23 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tldts": { "version": "7.0.30", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", @@ -14660,6 +15133,19 @@ "tslib": "2" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -14884,6 +15370,44 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.2", + "@typescript-eslint/parser": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index a238c6c7..83341cda 100644 --- a/package.json +++ b/package.json @@ -3,22 +3,23 @@ "version": "7.1.1", "description": "Datatables for React using Material-UI", "main": "dist/index.js", + "types": "dist/index.d.ts", "files": [ "dist" ], "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --progress --color", - "test": "cross-env NODE_ENV=test mocha --require ./test/babel-register.js --extensions js,jsx test/**/*.test.js", + "typecheck": "tsc --noEmit", + "test": "cross-env NODE_ENV=test mocha --require ./test/babel-register.js --extensions js,jsx,ts,tsx \"test/**/*.test.{js,jsx,ts,tsx}\"", "docs:dev": "next docs", "docs:build": "cross-env NODE_ENV=production next build docs", "docs:export": "next export docs -o docs/export", "docs:deploy": "npm run docs:build && npm run docs:export && firebase deploy", "docs:start": "next start docs", - "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text mocha --require ./test/babel-register.js --extensions js,jsx test/*.js && nyc report --reporter=html", - "coverage:html": "cross-env NODE_ENV=test nyc check-coverage --lines 55 --reporter=html --reporter=text mocha --require ./test/babel-register.js --extensions js,jsx test/*.js && nyc report --reporter=html", - "prettier": "find src/ docs/ test/ -type f -name \"*.js\" ! -path \"*/.next/*\" | xargs prettier --write", - "lint": "eslint src", - "build": "cross-env NODE_ENV=production npm run prettier && rollup -c", + "coverage": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text mocha --require ./test/babel-register.js --extensions js,jsx,ts,tsx \"test/**/*.test.{js,jsx,ts,tsx}\"", + "prettier": "prettier --write \"{src,test}/**/*.{js,jsx,ts,tsx}\" \"*.{js,ts,json,md}\"", + "lint": "eslint src test", + "build": "cross-env NODE_ENV=production npm run prettier && npm run typecheck && rollup -c && tsc -p tsconfig.build.json", "prepare": "npm run build" }, "repository": { @@ -48,6 +49,7 @@ "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.29.3", "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@babel/register": "^7.29.3", "@cfaester/enzyme-adapter-react-18": "^0.8.0", "@emotion/react": "^11.14.0", @@ -59,6 +61,18 @@ "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-replace": "^6.0.3", + "@types/chai": "^5.2.3", + "@types/lodash.assignwith": "^4.2.9", + "@types/lodash.clonedeep": "^4.5.9", + "@types/lodash.find": "^4.6.9", + "@types/lodash.isundefined": "^3.0.9", + "@types/lodash.merge": "^4.6.9", + "@types/mocha": "^10.0.10", + "@types/node": "^25.6.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@types/sinon": "^21.0.1", + "ajv": "^8.20.0", "babel-loader": "^10.1.1", "babel-plugin-istanbul": "^8.0.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", @@ -97,6 +111,8 @@ "simulant": "^0.2.2", "sinon": "^21.1.2", "style-loader": "^4.0.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.2", "webpack": "^5.106.2", "webpack-cli": "^7.0.2", "webpack-dev-server": "^5.2.3" @@ -145,7 +161,7 @@ "check-coverage": true, "all": true, "include": [ - "src/**/*.js" + "src/**/*.{ts,tsx}" ], "exclude": [ "test/**/*.test.js" diff --git a/prettier.config.js b/prettier.config.js index 195dea07..d0a0839c 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -4,6 +4,13 @@ module.exports = { trailingComma: 'all', bracketSpacing: true, bracketSameLine: true, - parser: 'babel', + overrides: [ + { + files: ['*.ts', '*.tsx'], + options: { + parser: 'babel-ts', + }, + }, + ], semi: true, }; diff --git a/rollup.config.js b/rollup.config.js index 5210ddab..e26ed742 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,6 @@ import babel from '@rollup/plugin-babel'; import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import uglify from '@lopatnov/rollup-plugin-uglify'; import { createRequire } from 'module'; @@ -9,28 +10,29 @@ const pkg = createRequire(import.meta.url)('./package.json'); // Treat anything declared as a dependency or peerDependency as external — // including subpath imports like `@babel/runtime-corejs3/helpers/extends` — // since they belong to the consumer's resolution graph, not our bundle. -const externals = [ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), -]; +const externals = [...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {})]; const externalRegex = new RegExp( '^(' + externals.map((d) => d.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')).join('|') + ')(/|$)', ); export default { - input: 'src/index.js', + input: 'src/index.ts', external: (id) => externalRegex.test(id), plugins: [ replace({ 'process.env.NODE_ENV': JSON.stringify('production'), preventAssignment: true, }), + resolve({ + extensions: ['.ts', '.tsx'], + }), commonjs({ include: ['node_modules/**'], }), babel({ babelHelpers: 'runtime', babelrc: true, + extensions: ['.ts', '.tsx'], }), uglify({ compress: { diff --git a/src/MUIDataTable.js b/src/MUIDataTable.tsx similarity index 84% rename from src/MUIDataTable.js rename to src/MUIDataTable.tsx index d32a3f67..fb429d10 100644 --- a/src/MUIDataTable.js +++ b/src/MUIDataTable.tsx @@ -6,7 +6,6 @@ import cloneDeep from 'lodash.clonedeep'; import find from 'lodash.find'; import isUndefined from 'lodash.isundefined'; import merge from 'lodash.merge'; -import PropTypes from 'prop-types'; import React from 'react'; import { flushSync } from 'react-dom'; import DefaultTableBody from './components/TableBody'; @@ -22,8 +21,45 @@ import { buildMap, getCollatorComparator, getPageValue, sortCompare, warnDepreca import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { load, save } from './localStorage'; +import type { ReactNode, ComponentType } from 'react'; +import type { Theme } from '@mui/material/styles'; +import type { MUIDataTableData } from './types/data'; + +interface MUIDataTableProps { + title: ReactNode; + data: MUIDataTableData; + columns: (string | Record)[]; + options?: Record; + className?: string; + components?: Record; + classes: Record; +} + +interface MUIDataTableInternalState { + activeColumn: number | null; + announceText: string | null; + count: number; + columns: Record[]; + expandedRows: { data: { index: number; dataIndex: number }[]; lookup: Record }; + data: { index: number; data: unknown[] }[]; + displayData: { data: unknown[]; dataIndex: number }[]; + filterData: string[][]; + filterList: string[][]; + page: number; + previousSelectedRow: { index: number; dataIndex: number } | null; + rowsPerPage: number; + searchProps: Record; + searchText: string | null; + selectedRows: { data: { index: number; dataIndex: number }[]; lookup: Record }; + showResponsive: boolean; + sortOrder: Record; + columnOrder: number[]; + curSelectedRows?: { index: number; dataIndex: number }[]; + curExpandedRows?: { index: number; dataIndex: number }[]; + [key: string]: unknown; +} -const defaultTableStyles = (theme) => ({ +const defaultTableStyles = (theme: Theme) => ({ root: { '& .datatables-noprint': { '@media print': { @@ -95,7 +131,7 @@ const TABLE_LOAD = { // Populate this list with anything that might render in the toolbar to determine if we hide the toolbar const TOOLBAR_ITEMS = ['title', 'filter', 'search', 'print', 'download', 'viewColumns', 'customToolbar']; -const hasToolbarItem = (options, title) => { +const hasToolbarItem = (options: any, title: any) => { options.title = title; return !isUndefined(find(TOOLBAR_ITEMS, (i) => options[i])); @@ -109,152 +145,14 @@ const STP = { ALWAYS: 'always', }; -class MUIDataTable extends React.Component { - static propTypes = { - /** Title of the table */ - title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, - /** Data used to describe table */ - data: PropTypes.array.isRequired, - /** Columns used to describe table */ - columns: PropTypes.PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - label: PropTypes.string, - name: PropTypes.string.isRequired, - options: PropTypes.shape({ - display: PropTypes.oneOf(['true', 'false', 'excluded', 'always', true, false]), - empty: PropTypes.bool, - filter: PropTypes.bool, - sort: PropTypes.bool, - print: PropTypes.bool, - searchable: PropTypes.bool, - download: PropTypes.bool, - viewColumns: PropTypes.bool, - filterList: PropTypes.array, - filterOptions: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.shape({ - names: PropTypes.array, - logic: PropTypes.func, - display: PropTypes.func, - }), - ]), - filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']), - customHeadRender: PropTypes.func, - customBodyRender: PropTypes.func, - customBodyRenderLite: PropTypes.func, - customHeadLabelRender: PropTypes.func, - customFilterListOptions: PropTypes.oneOfType([ - PropTypes.shape({ - render: PropTypes.func, - update: PropTypes.func, - }), - ]), - customFilterListRender: PropTypes.func, - setCellProps: PropTypes.func, - setCellHeaderProps: PropTypes.func, - sortThirdClickReset: PropTypes.bool, - sortDescFirst: PropTypes.bool, - }), - }), - ]), - ).isRequired, - /** Options used to describe table */ - options: PropTypes.shape({ - caseSensitive: PropTypes.bool, - columnOrder: PropTypes.array, - count: PropTypes.number, - confirmFilters: PropTypes.bool, - consoleWarnings: PropTypes.bool, - customFilterDialogFooter: PropTypes.func, - customFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - customRowRender: PropTypes.func, - customSearch: PropTypes.func, - customSearchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - customSort: PropTypes.func, - customToolbar: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - customToolbarSelect: PropTypes.oneOfType([PropTypes.func, PropTypes.element]), - draggableColumns: PropTypes.object, - enableNestedDataAccess: PropTypes.string, - expandableRows: PropTypes.bool, - expandableRowsHeader: PropTypes.bool, - expandableRowsOnClick: PropTypes.bool, - disableToolbarSelect: PropTypes.bool, - download: PropTypes.oneOf([true, false, 'true', 'false', 'disabled']), - downloadOptions: PropTypes.shape({ - filename: PropTypes.string, - separator: PropTypes.string, - filterOptions: PropTypes.shape({ - useDisplayedColumnsOnly: PropTypes.bool, - useDisplayedRowsOnly: PropTypes.bool, - }), - }), - filter: PropTypes.oneOf([true, false, 'true', 'false', 'disabled']), - filterArrayFullMatch: PropTypes.bool, - filterType: PropTypes.oneOf(['dropdown', 'checkbox', 'multiselect', 'textField', 'custom']), - fixedHeader: PropTypes.bool, - fixedSelectColumn: PropTypes.bool, - getTextLabels: PropTypes.func, - isRowExpandable: PropTypes.func, - isRowSelectable: PropTypes.func, - jumpToPage: PropTypes.bool, - onDownload: PropTypes.func, - onFilterChange: PropTypes.func, - onFilterChipClose: PropTypes.func, - onFilterConfirm: PropTypes.func, - onFilterDialogOpen: PropTypes.func, - onFilterDialogClose: PropTypes.func, - onRowClick: PropTypes.func, - onRowsExpand: PropTypes.func, - onRowExpansionChange: PropTypes.func, - onRowsSelect: PropTypes.func, - onRowSelectionChange: PropTypes.func, - onTableChange: PropTypes.func, - onTableInit: PropTypes.func, - page: PropTypes.number, - pagination: PropTypes.bool, - print: PropTypes.oneOf([true, false, 'true', 'false', 'disabled']), - searchProps: PropTypes.object, - selectableRows: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['none', 'single', 'multiple'])]), - selectableRowsHeader: PropTypes.bool, - selectableRowsHideCheckboxes: PropTypes.bool, - selectableRowsOnClick: PropTypes.bool, - serverSide: PropTypes.bool, - tableId: PropTypes.string, - tableBodyHeight: PropTypes.string, - tableBodyMaxHeight: PropTypes.string, - renderExpandableRow: PropTypes.func, - resizableColumns: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), - responsive: PropTypes.oneOf(['standard', 'vertical', 'verticalAlways', 'simple']), - rowHover: PropTypes.bool, - rowsExpanded: PropTypes.array, - rowsPerPage: PropTypes.number, - rowsPerPageOptions: PropTypes.array, - rowsSelected: PropTypes.array, - search: PropTypes.oneOf([true, false, 'true', 'false', 'disabled']), - searchOpen: PropTypes.bool, - searchAlwaysOpen: PropTypes.bool, - searchPlaceholder: PropTypes.string, - searchText: PropTypes.string, - setFilterChipProps: PropTypes.func, - setRowProps: PropTypes.func, - selectToolbarPlacement: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.oneOf([STP.REPLACE, STP.ABOVE, STP.NONE, STP.ALWAYS]), - ]), - setTableProps: PropTypes.func, - sort: PropTypes.bool, - sortOrder: PropTypes.object, - storageKey: PropTypes.string, - viewColumns: PropTypes.oneOf([true, false, 'true', 'false', 'disabled']), - }), - /** Pass and use className to style MUIDataTable as desired */ - className: PropTypes.string, - components: PropTypes.objectOf(PropTypes.any), - }; - - static defaultProps = { +class MUIDataTable extends React.Component { + static defaultProps: { + title: ReactNode; + options: Record; + data: unknown[][]; + columns: (string | Record)[]; + components: Record; + } = { title: '', options: {}, data: [], @@ -273,7 +171,19 @@ class MUIDataTable extends React.Component { }, }; - constructor(props) { + options: any = {}; + tableRef: React.RefObject = React.createRef(); + tableContent: React.RefObject = React.createRef(); + draggableHeadCellRefs: Record = {}; + resizeHeadCellRefs: Record = {}; + timers: Record = {}; + setHeadResizeable: ( + cellsRef: Record, + tableRef: React.RefObject, + ) => void = () => {}; + updateDividers: () => void = () => {}; + + constructor(props: MUIDataTableProps) { super(props); this.tableRef = React.createRef(); this.tableContent = React.createRef(); @@ -283,11 +193,12 @@ class MUIDataTable extends React.Component { this.setHeadResizeable = () => {}; this.updateDividers = () => {}; - let defaultState = { + let defaultState: any = { activeColumn: null, announceText: null, count: 0, columns: [], + columnOrder: [], expandedRows: { data: [], lookup: {}, @@ -311,24 +222,24 @@ class MUIDataTable extends React.Component { this.mergeDefaultOptions(props); - const restoredState = load(props.options.storageKey); + const restoredState = load((props.options as any)?.storageKey); this.state = Object.assign(defaultState, restoredState ? restoredState : this.getInitTableOptions()); this.setTableData = this.setTableData.bind(this); - this.setTableData(props, TABLE_LOAD.INITIAL, true, null, true); + this.setTableData(props, TABLE_LOAD.INITIAL, true, undefined, true); } componentDidMount() { this.setHeadResizeable(this.resizeHeadCellRefs, this.tableRef); // When we have a search, we must reset page to view it unless on serverSide since paging is handled by the user. - if (this.props.options.searchText && !this.props.options.serverSide) this.setState({ page: 0 }); + if ((this.props.options as any)?.searchText && !(this.props.options as any)?.serverSide) this.setState({ page: 0 }); this.setTableInit('tableInitialized'); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: MUIDataTableProps) { if ( this.props.data !== prevProps.data || this.props.columns !== prevProps.columns || @@ -346,7 +257,10 @@ class MUIDataTable extends React.Component { }); } - if (this.props.options.searchText !== prevProps.options.searchText && !this.props.options.serverSide) { + if ( + (this.props.options as any)?.searchText !== (prevProps.options as any)?.searchText && + !(this.props.options as any)?.serverSide + ) { // When we have a search, we must reset page to view it unless on serverSide since paging is handled by the user. this.setState({ page: 0 }); } @@ -360,7 +274,7 @@ class MUIDataTable extends React.Component { } } - updateOptions(options, props) { + updateOptions(options: any, props: any) { // set backwards compatibility options if (props.options.disableToolbarSelect === true && props.options.selectToolbarPlacement === undefined) { // if deprecated option disableToolbarSelect is set and selectToolbarPlacement is default then use it @@ -429,15 +343,15 @@ class MUIDataTable extends React.Component { selectToolbarPlacement: STP.REPLACE, }); - warnDep = (msg, consoleWarnings) => { + warnDep = (msg: string, _consoleWarnings?: any) => { warnDeprecated(msg, this.options.consoleWarnings); }; - warnInfo = (msg, consoleWarnings) => { + warnInfoMsg = (msg: string, _consoleWarnings?: any) => { warnInfo(msg, this.options.consoleWarnings); }; - handleOptionDeprecation = (props) => { + handleOptionDeprecation = (props: any) => { if (typeof this.options.selectableRows === 'boolean') { this.warnDep( 'Using a boolean for selectableRows has been deprecated. Please use string option: multiple | single | none', @@ -460,7 +374,7 @@ class MUIDataTable extends React.Component { ' has been deprecated, but will still work in version 3.x. Please use string option: standard | vertical | simple. More info: https://github.com/gregnb/mui-datatables/tree/master/docs/v2_to_v3_guide.md', ); } else { - this.warnInfo( + this.warnInfoMsg( this.options.responsive + ' is not recognized as a valid input for responsive option. Please use string option: standard | vertical | simple. More info: https://github.com/gregnb/mui-datatables/tree/master/docs/v2_to_v3_guide.md', ); @@ -499,7 +413,7 @@ class MUIDataTable extends React.Component { ); } - props.columns.map((c) => { + props.columns.map((c: any) => { if (c.options && c.options.customFilterListRender) { this.warnDep( 'The customFilterListRender option has been deprecated. It is being replaced by customFilterListOptions.render (Specify customFilterListOptions: { render: Function } in column options.)', @@ -526,7 +440,7 @@ class MUIDataTable extends React.Component { /* * React currently does not support deep merge for defaultProps. Objects are overwritten */ - mergeDefaultOptions(props) { + mergeDefaultOptions(props: any) { const defaultOptions = this.getDefaultOptions(); const theProps = Object.assign({}, props); theProps.options = theProps.options || {}; @@ -534,7 +448,7 @@ class MUIDataTable extends React.Component { this.updateOptions(defaultOptions, theProps); } - validateOptions(options) { + validateOptions(options: any) { if (options.serverSide && options.onTableChange === undefined) { throw Error('onTableChange callback must be provided when using serverSide option'); } @@ -546,16 +460,16 @@ class MUIDataTable extends React.Component { } } - setTableAction = (action) => { + setTableAction = (action: string) => { if (typeof this.options.onTableChange === 'function') { - this.options.onTableChange(action, this.state); + this.options.onTableChange(action, this.state as any); } if (this.options.storageKey) { - save(this.options.storageKey, this.state); + save(this.options.storageKey, this.state as any); } }; - setTableInit = (action) => { + setTableInit = (action: string) => { if (typeof this.options.onTableInit === 'function') { this.options.onTableInit(action, this.state); } @@ -563,7 +477,7 @@ class MUIDataTable extends React.Component { getInitTableOptions() { const optionNames = ['rowsPerPage', 'page', 'rowsSelected', 'rowsPerPageOptions']; - const optState = optionNames.reduce((acc, cur) => { + const optState = optionNames.reduce((acc: any, cur) => { if (this.options[cur] !== undefined) { acc[cur] = this.options[cur]; } @@ -574,7 +488,7 @@ class MUIDataTable extends React.Component { return optState; } - setHeadCellRef = (index, pos, el) => { + setHeadCellRef = (index: number, pos: number, el: HTMLElement | null) => { this.draggableHeadCellRefs[index] = el; this.resizeHeadCellRefs[pos] = el; }; @@ -592,11 +506,11 @@ class MUIDataTable extends React.Component { * prevColumnOrder - columnOrder object saved onto the state. */ - buildColumns = (newColumns, prevColumns = [], newColumnOrder, prevColumnOrder = []) => { - let columnData = []; - let filterData = []; - let filterList = []; - let columnOrder = []; + buildColumns = (newColumns: any[], prevColumns: any[] = [], newColumnOrder: any, prevColumnOrder: any[] = []) => { + let columnData: any[] = []; + let filterData: any[] = []; + let filterList: any[] = []; + let columnOrder: any[] = []; newColumns.forEach((column, colIndex) => { let columnOptions = { @@ -674,10 +588,10 @@ class MUIDataTable extends React.Component { return { columns: columnData, filterData, filterList, columnOrder }; }; - transformData = (columns, data) => { + transformData = (columns: any[], data: any[]) => { const { enableNestedDataAccess } = this.options; - const leaf = (obj, path) => - (enableNestedDataAccess ? path.split(enableNestedDataAccess) : path.split()).reduce( + const leaf = (obj: any, path: string) => + (enableNestedDataAccess ? path.split(enableNestedDataAccess as string) : [path]).reduce( (value, el) => (value ? value[el] : undefined), obj, ); @@ -696,8 +610,14 @@ class MUIDataTable extends React.Component { return transformedData; }; - setTableData(props, status, dataUpdated, callback = () => {}, fromConstructor = false) { - let tableData = []; + setTableData( + props: any, + status: number, + dataUpdated: boolean, + callback: (() => void) | undefined = () => {}, + fromConstructor = false, + ) { + let tableData: any[] = []; let { columns, filterData, filterList, columnOrder } = this.buildColumns( props.columns, this.state.columns, @@ -709,7 +629,7 @@ class MUIDataTable extends React.Component { let sortDirection = 'none'; let tableMeta; - let sortOrder; + let sortOrder: any; if (this.options.sortOrder && this.options.sortOrder.direction && this.options.sortOrder.name) { sortOrder = Object.assign({}, this.options.sortOrder); } else { @@ -717,7 +637,7 @@ class MUIDataTable extends React.Component { // if no sortOrder, check and see if there's a sortDirection on one of the columns (deprecation notice for this is given above) if (!sortOrder.direction) { - props.columns.forEach((column, colIndex) => { + props.columns.forEach((column: any, colIndex: number) => { if (column.options && (column.options.sortDirection === 'asc' || column.options.sortDirection === 'desc')) { sortOrder.name = column.name; sortOrder.sortDirection = column.sortDirection; @@ -760,8 +680,8 @@ class MUIDataTable extends React.Component { tableMeta = this.getTableMeta(rowIndex, colIndex, rowData, column, data, this.state, tableData); const funcResult = column.customBodyRender(value, tableMeta); - if (React.isValidElement(funcResult) && funcResult.props.value) { - value = funcResult.props.value; + if (React.isValidElement(funcResult) && (funcResult.props as any).value) { + value = (funcResult.props as any).value; } else if (typeof funcResult === 'string') { value = funcResult; } @@ -824,12 +744,12 @@ class MUIDataTable extends React.Component { } }); - let selectedRowsData = { + let selectedRowsData: any = { data: [], lookup: {}, }; - let expandedRowsData = { + let expandedRowsData: any = { data: [], lookup: {}, }; @@ -838,8 +758,10 @@ class MUIDataTable extends React.Component { // Multiple row selection customization if (this.options.rowsSelected && this.options.rowsSelected.length && this.options.selectableRows === 'multiple') { this.options.rowsSelected - .filter((selectedRowIndex) => selectedRowIndex === 0 || (Number(selectedRowIndex) && selectedRowIndex > 0)) - .forEach((row) => { + .filter( + (selectedRowIndex: any) => selectedRowIndex === 0 || (Number(selectedRowIndex) && selectedRowIndex > 0), + ) + .forEach((row: any) => { let rowPos = row; for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) { @@ -885,7 +807,7 @@ class MUIDataTable extends React.Component { } if (this.options.rowsExpanded && this.options.rowsExpanded.length && this.options.expandableRows) { - this.options.rowsExpanded.forEach((row) => { + this.options.rowsExpanded.forEach((row: any) => { let rowPos = row; for (let cIndex = 0; cIndex < this.state.displayData.length; cIndex++) { @@ -936,15 +858,15 @@ class MUIDataTable extends React.Component { * Build the table data used to display to the user (ie: after filter/search applied) */ computeDisplayRow( - columns, - row, - rowIndex, - filterList, - searchText, - dataForTableMeta, - options, - props, - currentTableData, + columns: any[], + row: any[], + rowIndex: number, + filterList: any[], + searchText: string | null, + dataForTableMeta: any, + options: any, + props: any, + currentTableData: any, ) { let isFiltered = false; let isSearchFound = false; @@ -1011,7 +933,7 @@ class MUIDataTable extends React.Component { } else if (filterType !== 'textField' && Array.isArray(columnValue)) { if (options.filterArrayFullMatch) { //true if every filterVal exists in columnVal, false otherwise - const isFullMatch = filterVal.every((el) => { + const isFullMatch = filterVal.every((el: any) => { return columnValue.indexOf(el) >= 0; }); //if it is not a fullMatch, filter row out @@ -1019,7 +941,7 @@ class MUIDataTable extends React.Component { isFiltered = true; } } else { - const isAnyMatch = filterVal.some((el) => { + const isAnyMatch = filterVal.some((el: any) => { return columnValue.indexOf(el) >= 0; }); //if no value matches, filter row out @@ -1064,7 +986,7 @@ class MUIDataTable extends React.Component { else return displayRow; } - hasSearchText = (toSearch, toFind, caseSensitive) => { + hasSearchText = (toSearch: any, toFind: any, caseSensitive: boolean) => { let stack = toSearch.toString(); let needle = toFind.toString(); @@ -1076,7 +998,7 @@ class MUIDataTable extends React.Component { return stack.indexOf(needle) >= 0; }; - updateDataCol = (row, index, value) => { + updateDataCol = (row: number, index: number, value: any) => { this.setState((prevState) => { let changedData = cloneDeep(prevState.data); let filterData = cloneDeep(prevState.filterData); @@ -1090,12 +1012,12 @@ class MUIDataTable extends React.Component { prevState, prevState.data, ); - const funcResult = prevState.columns[index].customBodyRender(value, tableMeta); + const funcResult = (prevState.columns[index] as any).customBodyRender(value, tableMeta); const filterValue = - React.isValidElement(funcResult) && funcResult.props.value - ? funcResult.props.value - : prevState['data'][row][index]; + React.isValidElement(funcResult) && (funcResult.props as any).value + ? (funcResult.props as any).value + : (prevState['data'] as any)[row][index]; const prevFilterIndex = filterData[index].indexOf(filterValue); filterData[index].splice(prevFilterIndex, 1, filterValue); @@ -1122,7 +1044,15 @@ class MUIDataTable extends React.Component { }); }; - getTableMeta = (rowIndex, colIndex, rowData, columnData, tableData, curState, currentTableData) => { + getTableMeta = ( + rowIndex: any, + colIndex: any, + rowData: any, + columnData: any, + tableData: any, + curState: any, + currentTableData: any, + ) => { const { columns, data, displayData, filterData, ...tableState } = curState; return { @@ -1136,7 +1066,7 @@ class MUIDataTable extends React.Component { }; }; - getDisplayData(columns, data, filterList, searchText, tableMeta, props) { + getDisplayData(columns: any[], data: any[], filterList: any[], searchText: any, tableMeta: any, props: any) { let newRows = []; const dataForTableMeta = tableMeta ? tableMeta.tableData : props.data; @@ -1164,7 +1094,7 @@ class MUIDataTable extends React.Component { return newRows; } - toggleViewColumn = (index) => { + toggleViewColumn = (index: number) => { this.setState( (prevState) => { const columns = cloneDeep(prevState.columns); @@ -1184,7 +1114,7 @@ class MUIDataTable extends React.Component { ); }; - updateColumns = (newColumns) => { + updateColumns = (newColumns: any[]) => { this.setState( (prevState) => { return { @@ -1202,7 +1132,7 @@ class MUIDataTable extends React.Component { ); }; - getSortDirectionLabel(sortOrder) { + getSortDirectionLabel(sortOrder: any) { switch (sortOrder.direction) { case 'asc': return 'ascending'; @@ -1224,7 +1154,7 @@ class MUIDataTable extends React.Component { return tableProps; } - toggleSortColumn = (index) => { + toggleSortColumn = (index: number) => { flushSync(() => { this.setState( (prevState) => { @@ -1241,7 +1171,7 @@ class MUIDataTable extends React.Component { } if (columns[index].name === this.state.sortOrder.name) { - let pos = sequenceOrder.indexOf(this.state.sortOrder.direction); + let pos = sequenceOrder.indexOf(this.state.sortOrder.direction as string); if (pos !== -1) { pos++; if (pos >= sequenceOrder.length) pos = 0; @@ -1257,7 +1187,7 @@ class MUIDataTable extends React.Component { const orderLabel = this.getSortDirectionLabel(newSortOrder); const announceText = `Table now sorted by ${columns[index].name} : ${orderLabel}`; - let newState = { + let newState: any = { columns: columns, announceText: announceText, activeColumn: index, @@ -1304,7 +1234,7 @@ class MUIDataTable extends React.Component { }); }; - changeRowsPerPage = (rows) => { + changeRowsPerPage = (rows: number) => { const rowCount = this.options.count || this.state.displayData.length; flushSync(() => { @@ -1324,7 +1254,7 @@ class MUIDataTable extends React.Component { }); }; - changePage = (page) => { + changePage = (page: number) => { flushSync(() => { this.setState( () => ({ @@ -1357,7 +1287,7 @@ class MUIDataTable extends React.Component { ); }; - searchTextUpdate = (text) => { + searchTextUpdate = (text: string) => { this.setState( (prevState) => ({ searchText: text && text.length ? text : null, @@ -1403,8 +1333,8 @@ class MUIDataTable extends React.Component { ); }; - updateFilterByType = (filterList, index, value, type, customUpdate) => { - const filterPos = filterList[index].findIndex((filter) => JSON.stringify(filter) === JSON.stringify(value)); + updateFilterByType = (filterList: any, index: number, value: any, type: string, customUpdate: any) => { + const filterPos = filterList[index].findIndex((filter: any) => JSON.stringify(filter) === JSON.stringify(value)); switch (type) { case 'checkbox': @@ -1431,7 +1361,7 @@ class MUIDataTable extends React.Component { } }; - filterUpdate = (index, value, column, type, customUpdate, next) => { + filterUpdate = (index: number, value: any, column: any, type: string, customUpdate: any, next: any) => { this.setState( (prevState) => { const filterList = cloneDeep(prevState.filterList); @@ -1481,7 +1411,10 @@ class MUIDataTable extends React.Component { // expand all for (let ii = 0; ii < this.state.data.length; ii++) { let item = this.state.data[ii]; - if (!isRowExpandable || (isRowExpandable && isRowExpandable(item.dataIndex, this.state.expandedRows))) { + if ( + !isRowExpandable || + (isRowExpandable && isRowExpandable((item as any).dataIndex, this.state.expandedRows)) + ) { if (this.state.expandedRows.lookup[item.index] !== true) { let newItem = { index: ii, @@ -1518,7 +1451,7 @@ class MUIDataTable extends React.Component { return this.state.expandedRows.data.length === this.state.data.length; }; - updateColumnOrder = (columnOrder, columnIndex, newPosition) => { + updateColumnOrder = (columnOrder: number[], columnIndex: number, newPosition: number) => { this.setState( (prevState) => { return { @@ -1566,14 +1499,14 @@ class MUIDataTable extends React.Component { ); }; - toggleExpandRow = (row) => { + toggleExpandRow = (row: any) => { const { dataIndex } = row; const { isRowExpandable } = this.options; let { expandedRows } = this.state; const expandedRowsData = [...expandedRows.data]; let shouldCollapseExpandedRow = false; let hasRemovedRow = false; - let removedRow = []; + let removedRow: any[] = []; for (var cIndex = 0; cIndex < expandedRowsData.length; cIndex++) { if (expandedRowsData[cIndex].dataIndex === dataIndex) { @@ -1610,7 +1543,7 @@ class MUIDataTable extends React.Component { ); }; - selectRowUpdate = (type, value, shiftAdjacentRows = []) => { + selectRowUpdate = (type: string, value: any, shiftAdjacentRows: any[] = []) => { // safety check const { selectableRows } = this.options; if (selectableRows === 'none') { @@ -1626,7 +1559,7 @@ class MUIDataTable extends React.Component { let isDeselect = selectedRowsLen === displayData.length || (selectedRowsLen < displayData.length && selectedRowsLen > 0); - let selectedRows = displayData.reduce((arr, d, i) => { + let selectedRows: any[] = displayData.reduce((arr: any[], d: any, i: number) => { const selected = isRowSelectable ? isRowSelectable(displayData[i].dataIndex, prevSelectedRows) : true; selected && arr.push({ index: i, dataIndex: displayData[i].dataIndex }); return arr; @@ -1750,7 +1683,7 @@ class MUIDataTable extends React.Component { } else if (type === 'custom') { const { displayData } = this.state; - const data = value.map((row) => ({ index: row, dataIndex: displayData[row].dataIndex })); + const data = value.map((row: any) => ({ index: row, dataIndex: displayData[row].dataIndex })); const lookup = buildMap(data); this.setState( @@ -1778,7 +1711,7 @@ class MUIDataTable extends React.Component { } }; - sortTable(data, col, order, columnSortCompare = null) { + sortTable(data: any[], col: number, order: string, columnSortCompare: any = null) { let hasCustomTableSort = this.options.customSort && !columnSortCompare; let meta = { selectedRows: this.state.selectedRows }; // meta for customSort let dataSrc = hasCustomTableSort @@ -1794,7 +1727,7 @@ class MUIDataTable extends React.Component { }, []); } - let sortedData = dataSrc.map((row, sIndex) => ({ + let sortedData = dataSrc.map((row: any, sIndex: number) => ({ data: row.data[col], rowData: row.data, position: sIndex, @@ -1840,7 +1773,7 @@ class MUIDataTable extends React.Component { TableToolbar, TableToolbarSelect, DragDropBackend = HTML5Backend, - }, + } = {} as any, } = this.props; const { announceText, @@ -1918,7 +1851,7 @@ class MUIDataTable extends React.Component { break; } - var tableHeightVal = {}; + var tableHeightVal: any = {}; if (maxHeight) { tableHeightVal.maxHeight = maxHeight; } @@ -1930,13 +1863,13 @@ class MUIDataTable extends React.Component { const tableClassNames = clsx(classes.tableRoot, tableProps.className); delete tableProps.className; // remove className from props to avoid the className being applied twice - const dndProps = {}; + const dndProps: any = {}; if (typeof window !== 'undefined') { dndProps.context = window; } return ( - + {(this.options.selectToolbarPlacement === STP.ALWAYS || (selectedRows.data.length > 0 && this.options.selectToolbarPlacement !== STP.NONE)) && ( { + serverSideFilterList={(this.props.options as any)?.serverSideFilterList} + filterListRenderers={columns.map((c: any) => { if (c.customFilterListOptions && c.customFilterListOptions.render) return c.customFilterListOptions.render; // DEPRECATED: This option is being replaced with customFilterListOptions.render if (c.customFilterListRender) return c.customFilterListRender; - return (f) => f; + return (f: any) => f; })} - customFilterListUpdate={columns.map((c) => { + customFilterListUpdate={columns.map((c: any) => { return c.customFilterListOptions && c.customFilterListOptions.update ? c.customFilterListOptions.update : null; @@ -1998,8 +1931,8 @@ class MUIDataTable extends React.Component { (this.updateDividers = fn)} - setResizeable={(fn) => (this.setHeadResizeable = fn)} + updateDividers={(fn: any) => (this.updateDividers = fn)} + setResizeable={(fn: any) => (this.setHeadResizeable = fn)} options={this.props.options} tableId={this.options.tableId} /> @@ -2007,7 +1940,7 @@ class MUIDataTable extends React.Component { {(() => { const components = ( (this.tableRef = el)} + ref={(el: any) => (this.tableRef = el)} tabIndex={'0'} role={'grid'} className={tableClassNames} @@ -2092,4 +2025,4 @@ class MUIDataTable extends React.Component { } } -export default withStyles(MUIDataTable, defaultTableStyles, { name: 'MUIDataTable' }); +export default withStyles(MUIDataTable, defaultTableStyles as any, { name: 'MUIDataTable' }); diff --git a/src/components/ExpandButton.js b/src/components/ExpandButton.tsx similarity index 68% rename from src/components/ExpandButton.js rename to src/components/ExpandButton.tsx index cc589936..d2712017 100644 --- a/src/components/ExpandButton.js +++ b/src/components/ExpandButton.tsx @@ -1,6 +1,18 @@ import React from 'react'; import { IconButton } from '@mui/material'; import { KeyboardArrowRight as KeyboardArrowRightIcon, Remove as RemoveIcon } from '@mui/icons-material'; +import type { MUIDataTableExpandedRows } from '../types/options'; + +interface ExpandButtonProps { + areAllRowsExpanded: () => boolean; + buttonClass: string; + expandableRowsHeader: boolean; + expandedRows: MUIDataTableExpandedRows; + iconClass: string; + iconIndeterminateClass: string; + isHeaderCell: boolean; + onExpand: () => void; +} const ExpandButton = ({ areAllRowsExpanded, @@ -11,10 +23,10 @@ const ExpandButton = ({ iconIndeterminateClass, isHeaderCell, onExpand, -}) => { +}: ExpandButtonProps) => { return ( <> - {isHeaderCell && !areAllRowsExpanded() && areAllRowsExpanded && expandedRows.data.length > 0 ? ( + {isHeaderCell && !areAllRowsExpanded() && expandedRows.data.length > 0 ? ( ({ root: { @@ -12,7 +13,6 @@ const useStyles = makeStyles({ name: 'MUIDataTableJumpToPage' })((theme) => ({ caption: { flexShrink: 0, }, - /*  Styles applied to the Select component root element */ selectRoot: { marginRight: 32, marginLeft: 8, @@ -26,36 +26,43 @@ const useStyles = makeStyles({ name: 'MUIDataTableJumpToPage' })((theme) => ({ textAlignLast: 'right', fontSize: theme.typography.pxToRem(14), }, - /* Styles applied to Select component icon class */ selectIcon: {}, - /* Styles applied to InputBase component */ input: { - color: 'inhert', - fontSize: 'inhert', + color: 'inherit', + fontSize: 'inherit', flexShrink: 0, }, + menuItem: {}, })); -function JumpToPage(props) { +interface JumpToPageProps { + count: number; + page: number; + rowsPerPage: number; + textLabels: MUIDataTableTextLabels; + changePage: (page: number) => void; +} + +function JumpToPage(props: JumpToPageProps) { const { classes } = useStyles(); - const handlePageChange = (event) => { - props.changePage(parseInt(event.target.value, 10)); + const handlePageChange = (event: SelectChangeEvent) => { + props.changePage(parseInt(event.target.value as unknown as string, 10)); }; - const { count, textLabels, rowsPerPage, page, changePage } = props; + const { count, textLabels, rowsPerPage, page } = props; const textLabel = textLabels.pagination.jumpToPage; - let pages = []; - let lastPage = Math.min(1000, getPageValue(count, rowsPerPage, 1000000)); + const pages: number[] = []; + const lastPage = Math.min(1000, getPageValue(count, rowsPerPage, 1000000)); for (let ii = 0; ii <= lastPage; ii++) { pages.push(ii); } const MenuItemComponent = MenuItem; - let myStyle = { + const myStyle = { display: 'flex', minHeight: '52px', alignItems: 'center', @@ -82,11 +89,4 @@ function JumpToPage(props) { ); } -JumpToPage.propTypes = { - count: PropTypes.number.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, - textLabels: PropTypes.object.isRequired, -}; - export default JumpToPage; diff --git a/src/components/Popover.js b/src/components/Popover.tsx similarity index 53% rename from src/components/Popover.js rename to src/components/Popover.tsx index 1dd29186..817b0e9e 100644 --- a/src/components/Popover.js +++ b/src/components/Popover.tsx @@ -1,11 +1,22 @@ import React, { useEffect, useRef, useState } from 'react'; -import PropTypes from 'prop-types'; import { Popover as MuiPopover, IconButton } from '@mui/material'; import { Close as CloseIcon } from '@mui/icons-material'; +import type { ReactNode } from 'react'; +import type { PopoverOrigin } from '@mui/material/Popover'; -const Popover = ({ className, trigger, refExit, hide, content, ...providedProps }) => { +interface PopoverProps { + className?: string; + trigger: ReactNode; + refExit?: () => void; + hide?: boolean; + content: ReactNode; + classes?: Record; + [key: string]: unknown; +} + +const Popover = ({ className, trigger, refExit, hide, content, ...providedProps }: PopoverProps) => { const [isOpen, open] = useState(false); - const anchorEl = useRef(null); + const anchorEl = useRef(null); useEffect(() => { if (isOpen) { @@ -16,7 +27,7 @@ const Popover = ({ className, trigger, refExit, hide, content, ...providedProps } }, [hide, isOpen, open]); - const handleClick = (event) => { + const handleClick = (event: React.MouseEvent) => { anchorEl.current = event.currentTarget; open(true); }; @@ -25,15 +36,20 @@ const Popover = ({ className, trigger, refExit, hide, content, ...providedProps open(false); }; - const closeIconClass = providedProps.classes.closeIcon; - delete providedProps.classes.closeIcon; // remove non-standard class from being passed to the popover component + const classes = (providedProps.classes || {}) as Record; + const closeIconClass = classes.closeIcon; + const passedProps = { ...providedProps }; + if (passedProps.classes && typeof passedProps.classes === 'object') { + const { closeIcon, ...restClasses } = passedProps.classes as Record; + passedProps.classes = restClasses; + } - const transformOriginSpecs = { + const transformOriginSpecs: PopoverOrigin = { vertical: 'top', horizontal: 'center', }; - const anchorOriginSpecs = { + const anchorOriginSpecs: PopoverOrigin = { vertical: 'bottom', horizontal: 'center', }; @@ -45,8 +61,10 @@ const Popover = ({ className, trigger, refExit, hide, content, ...providedProps }; const triggerProps = { - onClick: (event) => { - if (trigger.props.onClick) trigger.props.onClick(); + onClick: (event: React.MouseEvent) => { + if (React.isValidElement(trigger)) { + (trigger.props as { onClick?: (event: React.MouseEvent) => void }).onClick?.(event); + } handleClick(event); }, }; @@ -59,17 +77,21 @@ const Popover = ({ className, trigger, refExit, hide, content, ...providedProps + {...passedProps}> + style={{ position: 'absolute', right: '4px', top: '4px', zIndex: 1000 }}> {content} @@ -78,11 +100,4 @@ const Popover = ({ className, trigger, refExit, hide, content, ...providedProps ); }; -Popover.propTypes = { - refExit: PropTypes.func, - trigger: PropTypes.node.isRequired, - content: PropTypes.node.isRequired, - hide: PropTypes.bool, -}; - export default Popover; diff --git a/src/components/TableBody.js b/src/components/TableBody.js deleted file mode 100644 index a6bc4659..00000000 --- a/src/components/TableBody.js +++ /dev/null @@ -1,337 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Typography, TableBody as MuiTableBody } from '@mui/material'; -import TableBodyCell from './TableBodyCell'; -import TableBodyRow from './TableBodyRow'; -import TableSelectCell from './TableSelectCell'; -import { withStyles } from 'tss-react/mui'; -import cloneDeep from 'lodash.clonedeep'; -import { getPageValue } from '../utils'; -import clsx from 'clsx'; - -const defaultBodyStyles = (theme) => ({ - root: {}, - emptyTitle: { - textAlign: 'center', - }, - lastStackedCell: { - [theme.breakpoints.down('md')]: { - '& td:last-child': { - borderBottom: 'none', - }, - }, - }, - lastSimpleCell: { - [theme.breakpoints.down('sm')]: { - '& td:last-child': { - borderBottom: 'none', - }, - }, - }, -}); - -class TableBody extends React.Component { - static propTypes = { - /** Data used to describe table */ - data: PropTypes.array.isRequired, - /** Total number of data rows */ - count: PropTypes.number.isRequired, - /** Columns used to describe table */ - columns: PropTypes.array.isRequired, - /** Options used to describe table */ - options: PropTypes.object.isRequired, - /** Data used to filter table against */ - filterList: PropTypes.array, - /** Callback to execute when row is clicked */ - onRowClick: PropTypes.func, - /** Table rows expanded */ - expandedRows: PropTypes.object, - /** Table rows selected */ - selectedRows: PropTypes.object, - /** Callback to trigger table row select */ - selectRowUpdate: PropTypes.func, - /** The most recent row to have been selected/unselected */ - previousSelectedRow: PropTypes.object, - /** Data used to search table against */ - searchText: PropTypes.string, - /** Toggle row expandable */ - toggleExpandRow: PropTypes.func, - /** Extend the style applied to components */ - classes: PropTypes.object, - }; - - static defaultProps = { - toggleExpandRow: () => {}, - }; - - buildRows() { - const { data, page, rowsPerPage, count } = this.props; - - if (this.props.options.serverSide) return data.length ? data : null; - - let rows = []; - const highestPageInRange = getPageValue(count, rowsPerPage, page); - const fromIndex = highestPageInRange === 0 ? 0 : highestPageInRange * rowsPerPage; - const toIndex = Math.min(count, (highestPageInRange + 1) * rowsPerPage); - - if (page > highestPageInRange) { - console.warn('Current page is out of range, using the highest page that is in range instead.'); - } - - for (let rowIndex = fromIndex; rowIndex < count && rowIndex < toIndex; rowIndex++) { - if (data[rowIndex] !== undefined) rows.push(data[rowIndex]); - } - - return rows.length ? rows : null; - } - - getRowIndex(index) { - const { page, rowsPerPage, options } = this.props; - - if (options.serverSide) { - return index; - } - - const startIndex = page === 0 ? 0 : page * rowsPerPage; - return startIndex + index; - } - - isRowSelected(dataIndex) { - const { selectedRows } = this.props; - return selectedRows.lookup && selectedRows.lookup[dataIndex] ? true : false; - } - - isRowExpanded(dataIndex) { - const { expandedRows } = this.props; - return expandedRows.lookup && expandedRows.lookup[dataIndex] ? true : false; - } - - isRowSelectable(dataIndex, selectedRows) { - const { options } = this.props; - selectedRows = selectedRows || this.props.selectedRows; - - if (options.isRowSelectable) { - return options.isRowSelectable(dataIndex, selectedRows); - } else { - return true; - } - } - - isRowExpandable(dataIndex) { - const { options, expandedRows } = this.props; - if (options.isRowExpandable) { - return options.isRowExpandable(dataIndex, expandedRows); - } else { - return true; - } - } - - handleRowSelect = (data, event) => { - let shiftKey = event && event.nativeEvent ? event.nativeEvent.shiftKey : false; - let shiftAdjacentRows = []; - let previousSelectedRow = this.props.previousSelectedRow; - - // If the user is pressing shift and has previously clicked another row. - if (shiftKey && previousSelectedRow && previousSelectedRow.index < this.props.data.length) { - let curIndex = previousSelectedRow.index; - - // Create a copy of the selectedRows object. This will be used and modified - // below when we see if we can add adjacent rows. - let selectedRows = cloneDeep(this.props.selectedRows); - - // Add the clicked on row to our copy of selectedRows (if it isn't already present). - let clickedDataIndex = this.props.data[data.index].dataIndex; - if (selectedRows.data.filter((d) => d.dataIndex === clickedDataIndex).length === 0) { - selectedRows.data.push({ - index: data.index, - dataIndex: clickedDataIndex, - }); - selectedRows.lookup[clickedDataIndex] = true; - } - - while (curIndex !== data.index) { - let dataIndex = this.props.data[curIndex].dataIndex; - - if (this.isRowSelectable(dataIndex, selectedRows)) { - let lookup = { - index: curIndex, - dataIndex: dataIndex, - }; - - // Add adjacent row to temp selectedRow object if it isn't present. - if (selectedRows.data.filter((d) => d.dataIndex === dataIndex).length === 0) { - selectedRows.data.push(lookup); - selectedRows.lookup[dataIndex] = true; - } - - shiftAdjacentRows.push(lookup); - } - curIndex = data.index > curIndex ? curIndex + 1 : curIndex - 1; - } - } - this.props.selectRowUpdate('cell', data, shiftAdjacentRows); - }; - - handleRowClick = (row, data, event) => { - // Don't trigger onRowClick if the event was actually the expandable icon. - if ( - event.target.id === 'expandable-button' || - (event.target.nodeName === 'path' && event.target.parentNode.id === 'expandable-button') - ) { - return; - } - - // Don't trigger onRowClick if the event was actually a row selection via checkbox - if (event.target.id && event.target.id.startsWith('MUIDataTableSelectCell')) return; - - // Check if we should toggle row select when row is clicked anywhere - if ( - this.props.options.selectableRowsOnClick && - this.props.options.selectableRows !== 'none' && - this.isRowSelectable(data.dataIndex, this.props.selectedRows) - ) { - const selectRow = { index: data.rowIndex, dataIndex: data.dataIndex }; - this.handleRowSelect(selectRow, event); - } - // Check if we should trigger row expand when row is clicked anywhere - if ( - this.props.options.expandableRowsOnClick && - this.props.options.expandableRows && - this.isRowExpandable(data.dataIndex, this.props.expandedRows) - ) { - const expandRow = { index: data.rowIndex, dataIndex: data.dataIndex }; - this.props.toggleExpandRow(expandRow); - } - - // Don't trigger onRowClick if the event was actually a row selection via click - if (this.props.options.selectableRowsOnClick) return; - - this.props.options.onRowClick && this.props.options.onRowClick(row, data, event); - }; - - processRow = (row, columnOrder) => { - let ret = []; - for (let ii = 0; ii < row.length; ii++) { - ret.push({ - value: row[columnOrder[ii]], - index: columnOrder[ii], - }); - } - return ret; - }; - - render() { - const { - classes, - columns, - toggleExpandRow, - options, - columnOrder = this.props.columns.map((item, idx) => idx), - components = {}, - tableId, - } = this.props; - const tableRows = this.buildRows(); - const visibleColCnt = columns.filter((c) => c.display === 'true').length; - - return ( - - {tableRows && tableRows.length > 0 ? ( - tableRows.map((data, rowIndex) => { - const { data: row, dataIndex } = data; - - if (options.customRowRender) { - return options.customRowRender(row, dataIndex, rowIndex); - } - - let isRowSelected = options.selectableRows !== 'none' ? this.isRowSelected(dataIndex) : false; - let isRowSelectable = this.isRowSelectable(dataIndex); - let bodyClasses = options.setRowProps ? options.setRowProps(row, dataIndex, rowIndex) || {} : {}; - - const processedRow = this.processRow(row, columnOrder); - - return ( - - - - {processedRow.map( - (column) => - columns[column.index].display === 'true' && ( - - {column.value} - - ), - )} - - {this.isRowExpanded(dataIndex) && options.renderExpandableRow(row, { rowIndex, dataIndex })} - - ); - }) - ) : ( - - - - {options.textLabels.body.noMatch} - - - - )} - - ); - } -} - -export default withStyles(TableBody, defaultBodyStyles, { name: 'MUIDataTableBody' }); diff --git a/src/components/TableBody.tsx b/src/components/TableBody.tsx new file mode 100644 index 00000000..2b17a4d9 --- /dev/null +++ b/src/components/TableBody.tsx @@ -0,0 +1,321 @@ +import React from 'react'; +import { Typography, TableBody as MuiTableBody } from '@mui/material'; +import TableBodyCell from './TableBodyCell'; +import TableBodyRow from './TableBodyRow'; +import TableSelectCell from './TableSelectCell'; +import { withStyles } from 'tss-react/mui'; +import cloneDeep from 'lodash.clonedeep'; +import { getPageValue } from '../utils'; +import clsx from 'clsx'; +import type { ComponentType } from 'react'; +import type { CSSObject } from 'tss-react'; +import type { MUIDataTableColumnState } from '../types/columns'; +import type { + MUIDataTableOptions, + MUIDataTableSelectedRows, + MUIDataTableExpandedRows, + MUIDataTableDisplayRow, + MUIDataTableSelectedRow, + MUIDataTableCellValue, +} from '../types/options'; +import type { Theme } from '@mui/material/styles'; + +const defaultBodyStyles = (theme: Theme) => + ({ + root: {}, + emptyTitle: { + textAlign: 'center', + }, + lastStackedCell: { + [theme.breakpoints.down('md')]: { + '& td:last-child': { + borderBottom: 'none', + }, + }, + }, + lastSimpleCell: { + [theme.breakpoints.down('sm')]: { + '& td:last-child': { + borderBottom: 'none', + }, + }, + }, + }) satisfies Record; + +interface TableBodyProps { + data: MUIDataTableDisplayRow[]; + count: number; + columns: MUIDataTableColumnState[]; + options: MUIDataTableOptions; + filterList?: string[][]; + expandedRows: MUIDataTableExpandedRows; + selectedRows: MUIDataTableSelectedRows; + selectRowUpdate: (type: string, data: unknown, adjacentRows?: MUIDataTableSelectedRow[]) => void; + previousSelectedRow: MUIDataTableSelectedRow | null; + searchText?: string | null; + toggleExpandRow: (row: { index: number; dataIndex: number }) => void; + classes: Record, string>; + columnOrder?: number[]; + components?: { Checkbox?: ComponentType; ExpandButton?: ComponentType }; + tableId?: string; + page: number; + rowsPerPage: number; +} + +class TableBody extends React.Component { + static defaultProps = { + toggleExpandRow: () => {}, + }; + + buildRows(): MUIDataTableDisplayRow[] | null { + const { data, page, rowsPerPage, count } = this.props; + + if (this.props.options.serverSide) return data.length ? data : null; + + const rows: MUIDataTableDisplayRow[] = []; + const highestPageInRange = getPageValue(count, rowsPerPage, page); + const fromIndex = highestPageInRange === 0 ? 0 : highestPageInRange * rowsPerPage; + const toIndex = Math.min(count, (highestPageInRange + 1) * rowsPerPage); + + if (page > highestPageInRange) { + console.warn('Current page is out of range, using the highest page that is in range instead.'); + } + + for (let rowIndex = fromIndex; rowIndex < count && rowIndex < toIndex; rowIndex++) { + if (data[rowIndex] !== undefined) rows.push(data[rowIndex]); + } + + return rows.length ? rows : null; + } + + getRowIndex(index: number): number { + const { page, rowsPerPage, options } = this.props; + if (options.serverSide) return index; + const startIndex = page === 0 ? 0 : page * rowsPerPage; + return startIndex + index; + } + + isRowSelected(dataIndex: number): boolean { + const { selectedRows } = this.props; + return selectedRows.lookup && selectedRows.lookup[dataIndex] ? true : false; + } + + isRowExpanded(dataIndex: number): boolean { + const { expandedRows } = this.props; + return expandedRows.lookup && expandedRows.lookup[dataIndex] ? true : false; + } + + isRowSelectable(dataIndex: number, selectedRows?: MUIDataTableSelectedRows): boolean { + const { options } = this.props; + const rows = selectedRows || this.props.selectedRows; + if (options.isRowSelectable) { + return options.isRowSelectable(dataIndex, rows); + } + return true; + } + + isRowExpandable(dataIndex: number): boolean { + const { options, expandedRows } = this.props; + if (options.isRowExpandable) { + return options.isRowExpandable(dataIndex, expandedRows); + } + return true; + } + + handleRowSelect = (data: { index: number; dataIndex: number }, event: React.MouseEvent) => { + const shiftKey = event && event.nativeEvent ? (event.nativeEvent as MouseEvent).shiftKey : false; + let shiftAdjacentRows: MUIDataTableSelectedRow[] = []; + const previousSelectedRow = this.props.previousSelectedRow; + + if (shiftKey && previousSelectedRow && previousSelectedRow.index < this.props.data.length) { + let curIndex = previousSelectedRow.index; + const selectedRows = cloneDeep(this.props.selectedRows); + const clickedDataIndex = this.props.data[data.index].dataIndex; + + if (selectedRows.data.filter((d) => d.dataIndex === clickedDataIndex).length === 0) { + selectedRows.data.push({ index: data.index, dataIndex: clickedDataIndex }); + selectedRows.lookup[clickedDataIndex] = true; + } + + while (curIndex !== data.index) { + const dataIndex = this.props.data[curIndex].dataIndex; + if (this.isRowSelectable(dataIndex, selectedRows)) { + const lookup = { index: curIndex, dataIndex }; + if (selectedRows.data.filter((d) => d.dataIndex === dataIndex).length === 0) { + selectedRows.data.push(lookup); + selectedRows.lookup[dataIndex] = true; + } + shiftAdjacentRows.push(lookup); + } + curIndex = data.index > curIndex ? curIndex + 1 : curIndex - 1; + } + } + this.props.selectRowUpdate('cell', data, shiftAdjacentRows); + }; + + handleRowClick = ( + row: MUIDataTableCellValue[], + data: { rowIndex: number; dataIndex: number }, + event: React.MouseEvent, + ) => { + const target = event.target as HTMLElement; + if ( + target.id === 'expandable-button' || + (target.nodeName === 'path' && (target.parentNode as HTMLElement)?.id === 'expandable-button') + ) { + return; + } + + if (target.id && target.id.startsWith('MUIDataTableSelectCell')) return; + + if ( + this.props.options.selectableRowsOnClick && + this.props.options.selectableRows !== 'none' && + this.isRowSelectable(data.dataIndex, this.props.selectedRows) + ) { + const selectRow = { index: data.rowIndex, dataIndex: data.dataIndex }; + this.handleRowSelect(selectRow, event as unknown as React.MouseEvent); + } + + if ( + this.props.options.expandableRowsOnClick && + this.props.options.expandableRows && + this.isRowExpandable(data.dataIndex) + ) { + const expandRow = { index: data.rowIndex, dataIndex: data.dataIndex }; + this.props.toggleExpandRow(expandRow); + } + + if (this.props.options.selectableRowsOnClick) return; + + this.props.options.onRowClick && this.props.options.onRowClick(row, data); + }; + + processRow = (row: MUIDataTableCellValue[], columnOrder: number[]) => { + const ret: { value: MUIDataTableCellValue; index: number }[] = []; + for (let ii = 0; ii < row.length; ii++) { + ret.push({ value: row[columnOrder[ii]], index: columnOrder[ii] }); + } + return ret; + }; + + render() { + const { + classes, + columns, + toggleExpandRow, + options, + columnOrder = this.props.columns.map((_item, idx) => idx), + components = {}, + tableId, + } = this.props; + const tableRows = this.buildRows(); + const visibleColCnt = columns.filter((c) => c.display === 'true').length; + + return ( + + {tableRows && tableRows.length > 0 ? ( + tableRows.map((data, rowIndex) => { + const { data: row, dataIndex } = data; + + if (options.customRowRender) { + return options.customRowRender(row, dataIndex, rowIndex); + } + + const isRowSelected = options.selectableRows !== 'none' ? this.isRowSelected(dataIndex) : false; + const isRowSelectable = this.isRowSelectable(dataIndex); + const bodyClasses = ( + options.setRowProps ? options.setRowProps(row, dataIndex, rowIndex) || {} : {} + ) as Record; + + const processedRow = this.processRow(row, columnOrder); + + return ( + + ) => + this.handleRowClick(row, { rowIndex, dataIndex }, e) + } + className={clsx({ + [classes.lastStackedCell]: + options.responsive === 'vertical' || + options.responsive === 'stacked' || + options.responsive === 'stackedFullWidth', + [classes.lastSimpleCell]: options.responsive === 'simple', + [bodyClasses.className as string]: !!bodyClasses.className, + })} + data-testid={'MUIDataTableBodyRow-' + dataIndex} + id={`MUIDataTableBodyRow-${tableId}-${dataIndex}`}> + + this.handleRowSelect({ index: this.getRowIndex(rowIndex), dataIndex }, e as React.MouseEvent) + } + onExpand={() => toggleExpandRow({ index: this.getRowIndex(rowIndex), dataIndex })} + fixedHeader={options.fixedHeader} + fixedSelectColumn={options.fixedSelectColumn} + checked={isRowSelected} + expandableOn={options.expandableRows} + hideExpandButton={!this.isRowExpandable(dataIndex) && options.expandableRows} + selectableOn={options.selectableRows as string} + selectableRowsHideCheckboxes={options.selectableRowsHideCheckboxes} + isRowExpanded={this.isRowExpanded(dataIndex)} + isRowSelectable={isRowSelectable} + dataIndex={dataIndex} + id={`MUIDataTableSelectCell-${tableId}-${dataIndex}`} + components={components} + /> + {processedRow.map((column) => { + if (columns[column.index].display !== 'true') return null; + const cellValue = column.value instanceof Date ? String(column.value) : column.value; + + return ( + + {cellValue} + + ); + })} + + {this.isRowExpanded(dataIndex) && + options.renderExpandableRow && + options.renderExpandableRow(row, { rowIndex, dataIndex })} + + ); + }) + ) : ( + + + + {options.textLabels?.body?.noMatch} + + + + )} + + ); + } +} + +export default withStyles(TableBody, defaultBodyStyles, { name: 'MUIDataTableBody' }); diff --git a/src/components/TableBodyCell.js b/src/components/TableBodyCell.tsx similarity index 81% rename from src/components/TableBodyCell.js rename to src/components/TableBodyCell.tsx index 800440bd..6bdc6189 100644 --- a/src/components/TableBodyCell.js +++ b/src/components/TableBodyCell.tsx @@ -2,6 +2,8 @@ import React, { useCallback } from 'react'; import clsx from 'clsx'; import { TableCell } from '@mui/material'; import { makeStyles } from 'tss-react/mui'; +import type { ReactNode } from 'react'; +import type { MUIDataTableOptions } from '../types/options'; const useStyles = makeStyles({ name: 'MUIDataTableBodyCell' })((theme) => ({ root: {}, @@ -90,26 +92,40 @@ const useStyles = makeStyles({ name: 'MUIDataTableBodyCell' })((theme) => ({ }, })); -function TableBodyCell(props) { +interface TableBodyCellProps { + children: ReactNode | ((dataIndex: number, rowIndex: number) => ReactNode); + colIndex: number; + columnHeader: ReactNode; + options: MUIDataTableOptions; + dataIndex: number; + rowIndex: number; + className?: string; + print: boolean; + tableId?: string; + [key: string]: unknown; +} + +function TableBodyCell(props: TableBodyCellProps) { const { classes } = useStyles(); const { children, colIndex, columnHeader, options, dataIndex, rowIndex, className, print, tableId, ...otherProps } = props; const onCellClick = options.onCellClick; const handleClick = useCallback( - (event) => { - onCellClick(children, { colIndex, rowIndex, dataIndex, event }); + (event: React.MouseEvent) => { + if (onCellClick) onCellClick(children as unknown as string, { colIndex, rowIndex, dataIndex, event }); }, [onCellClick, children, colIndex, rowIndex, dataIndex], ); - // Event listeners. Avoid attaching them if they're not necessary. - let methods = {}; + const methods: Record = {}; if (onCellClick) { methods.onClick = handleClick; } - let cells = [ + const tableProps = options.setTableProps ? options.setTableProps() : {}; + + const cells = [
, ]; - var innerCells; + let innerCells; if ( - ['standard', 'scrollMaxHeight', 'scrollFullHeight', 'scrollFullHeightFullWidth'].indexOf(options.responsive) !== -1 + ['standard', 'scrollMaxHeight', 'scrollFullHeight', 'scrollFullHeightFullWidth'].indexOf( + options.responsive as string, + ) !== -1 ) { innerCells = cells.slice(1, 2); } else { @@ -183,7 +201,7 @@ function TableBodyCell(props) { options.responsive === 'vertical' || options.responsive === 'stacked' || (options.responsive === 'stackedFullWidth' && - (options.setTableProps().padding === 'none' || options.setTableProps().size === 'small')), + (tableProps.padding === 'none' || tableProps.size === 'small')), [classes.simpleCell]: options.responsive === 'simple', 'datatables-noprint': !print, }, diff --git a/src/components/TableBodyRow.js b/src/components/TableBodyRow.tsx similarity index 66% rename from src/components/TableBodyRow.js rename to src/components/TableBodyRow.tsx index 01544bc5..ec9d688a 100644 --- a/src/components/TableBodyRow.js +++ b/src/components/TableBodyRow.tsx @@ -1,17 +1,16 @@ import React from 'react'; -import PropTypes from 'prop-types'; import clsx from 'clsx'; import { TableRow } from '@mui/material'; import { withStyles } from 'tss-react/mui'; +import type { ReactNode } from 'react'; +import type { MUIDataTableOptions } from '../types/options'; +import type { Theme } from '@mui/material/styles'; -const defaultBodyRowStyles = (theme) => ({ +const defaultBodyRowStyles = (theme: Theme) => ({ root: { - // material v4 '&.Mui-selected': { backgroundColor: theme.palette.action.selected, }, - - // material v3 workaround '&.mui-row-selected': { backgroundColor: theme.palette.action.selected, }, @@ -35,22 +34,30 @@ const defaultBodyRowStyles = (theme) => ({ }, }); -class TableBodyRow extends React.Component { - static propTypes = { - /** Options used to describe table */ - options: PropTypes.object.isRequired, - /** Callback to execute when row is clicked */ - onClick: PropTypes.func, - /** Current row selected or not */ - rowSelected: PropTypes.bool, - /** Extend the style applied to components */ - classes: PropTypes.object, - }; +interface TableBodyRowProps { + options: MUIDataTableOptions; + onClick?: (event: React.MouseEvent) => void; + rowSelected?: boolean; + classes?: Record, string>; + className?: string; + isRowSelectable?: boolean; + children?: ReactNode; + [key: string]: unknown; +} +class TableBodyRow extends React.Component { render() { - const { classes, options, rowSelected, onClick, className, isRowSelectable, ...rest } = this.props; + const { + classes = {} as Record, string>, + options, + rowSelected, + onClick, + className, + isRowSelectable, + ...rest + } = this.props; - var methods = {}; + const methods: Record = {}; if (onClick) { methods.onClick = onClick; } @@ -62,7 +69,6 @@ class TableBodyRow extends React.Component { className={clsx( { [classes.root]: true, - [classes.hover]: options.rowHover, [classes.hoverCursor]: (options.selectableRowsOnClick && isRowSelectable) || options.expandableRowsOnClick, [classes.responsiveSimple]: options.responsive === 'simple', [classes.responsiveStacked]: diff --git a/src/components/TableFilter.js b/src/components/TableFilter.tsx similarity index 54% rename from src/components/TableFilter.js rename to src/components/TableFilter.tsx index 5a2b7ca9..3b5fff16 100644 --- a/src/components/TableFilter.js +++ b/src/components/TableFilter.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { Button, Checkbox, @@ -13,158 +14,176 @@ import { TextField, Typography, } from '@mui/material'; -import PropTypes from 'prop-types'; -import React from 'react'; import clsx from 'clsx'; import { withStyles } from 'tss-react/mui'; import cloneDeep from 'lodash.clonedeep'; +import type { ComponentType } from 'react'; +import type { CSSObject } from 'tss-react'; +import type { MUIDataTableColumnState } from '../types/columns'; +import type { MUIDataTableOptions, MUIDataTableFilterList } from '../types/options'; +import type { Theme } from '@mui/material/styles'; + +export const defaultFilterStyles = (theme: Theme) => + ({ + root: { + backgroundColor: theme.palette.background.default, + padding: '24px 24px 36px 24px', + fontFamily: 'Roboto', + }, + header: { + flex: '0 0 auto', + marginBottom: '16px', + width: '100%', + display: 'flex', + justifyContent: 'space-between', + }, + title: { + display: 'inline-block', + marginLeft: '7px', + color: theme.palette.text.primary, + fontSize: '14px', + fontWeight: 500, + }, + noMargin: { + marginLeft: '0px', + }, + reset: { + alignSelf: 'left', + }, + resetLink: { + marginLeft: '16px', + fontSize: '12px', + cursor: 'pointer', + }, + filtersSelected: { + alignSelf: 'right', + }, + checkboxListTitle: { + marginLeft: '7px', + marginBottom: '8px', + fontSize: '14px', + color: theme.palette.text.secondary, + textAlign: 'left', + fontWeight: 500, + }, + checkboxFormGroup: { + marginTop: '8px', + }, + checkboxFormControl: { + margin: '0px', + }, + checkboxFormControlLabel: { + fontSize: '15px', + marginLeft: '8px', + color: theme.palette.text.primary, + }, + checkboxIcon: { + width: '32px', + height: '32px', + }, + checkbox: {}, + checked: {}, + gridListTile: { + marginTop: '16px', + }, + }) satisfies Record; + +interface TableFilterProps { + filterData: string[][]; + filterList: MUIDataTableFilterList; + options: MUIDataTableOptions; + columns: MUIDataTableColumnState[]; + onFilterUpdate: ( + index: number, + value: string | string[], + column: string | MUIDataTableColumnState, + type: string, + ) => void; + onFilterReset: () => void; + handleClose: () => void; + updateFilterByType: ( + filterList: MUIDataTableFilterList, + index: number, + value: string | string[], + type: string, + customUpdate: unknown, + ) => void; + classes: Record, string>; + customFooter?: (filterList: MUIDataTableFilterList, applyFilters: () => MUIDataTableFilterList) => React.ReactNode; + components?: { Checkbox?: ComponentType }; +} -export const defaultFilterStyles = (theme) => ({ - root: { - backgroundColor: theme.palette.background.default, - padding: '24px 24px 36px 24px', - fontFamily: 'Roboto', - }, - header: { - flex: '0 0 auto', - marginBottom: '16px', - width: '100%', - display: 'flex', - justifyContent: 'space-between', - }, - title: { - display: 'inline-block', - marginLeft: '7px', - color: theme.palette.text.primary, - fontSize: '14px', - fontWeight: 500, - }, - noMargin: { - marginLeft: '0px', - }, - reset: { - alignSelf: 'left', - }, - resetLink: { - marginLeft: '16px', - fontSize: '12px', - cursor: 'pointer', - }, - filtersSelected: { - alignSelf: 'right', - }, - /* checkbox */ - checkboxListTitle: { - marginLeft: '7px', - marginBottom: '8px', - fontSize: '14px', - color: theme.palette.text.secondary, - textAlign: 'left', - fontWeight: 500, - }, - checkboxFormGroup: { - marginTop: '8px', - }, - checkboxFormControl: { - margin: '0px', - }, - checkboxFormControlLabel: { - fontSize: '15px', - marginLeft: '8px', - color: theme.palette.text.primary, - }, - checkboxIcon: { - width: '32px', - height: '32px', - }, - checkbox: {}, - checked: {}, - gridListTile: { - marginTop: '16px', - }, -}); - -class TableFilter extends React.Component { - static propTypes = { - /** Data used to populate filter dropdown/checkbox */ - filterData: PropTypes.array.isRequired, - /** Data selected to be filtered against dropdown/checkbox */ - filterList: PropTypes.array.isRequired, - /** Options used to describe table */ - options: PropTypes.object.isRequired, - /** Callback to trigger filter update */ - onFilterUpdate: PropTypes.func, - /** Callback to trigger filter reset */ - onFilterReset: PropTypes.func, - /** Extend the style applied to components */ - classes: PropTypes.object, - }; +interface TableFilterState { + filterList: MUIDataTableFilterList; +} - constructor(props) { +class TableFilter extends React.Component { + constructor(props: TableFilterProps) { super(props); this.state = { filterList: cloneDeep(props.filterList), }; } - filterUpdate = (index, value, column, type, customUpdate) => { - let newFilterList = this.state.filterList.slice(0); - + filterUpdate = ( + index: number, + value: string | string[], + column: string | MUIDataTableColumnState, + type: string, + customUpdate?: unknown, + ) => { + const newFilterList = this.state.filterList.slice(0); this.props.updateFilterByType(newFilterList, index, value, type, customUpdate); - this.setState({ - filterList: newFilterList, - }); + this.setState({ filterList: newFilterList }); }; - handleCheckboxChange = (index, value, column) => { + handleCheckboxChange = (index: number, value: string, column: string) => { this.filterUpdate(index, value, column, 'checkbox'); - if (this.props.options.confirmFilters !== true) { this.props.onFilterUpdate(index, value, column, 'checkbox'); } }; - handleDropdownChange = (event, index, column) => { - const labelFilterAll = this.props.options.textLabels.filter.all; - const value = event.target.value === labelFilterAll ? [] : [event.target.value]; + handleDropdownChange = (event: React.ChangeEvent<{ value: unknown }>, index: number, column: string) => { + const labelFilterAll = this.props.options.textLabels?.filter?.all || ''; + const value = event.target.value === labelFilterAll ? [] : [event.target.value as string]; this.filterUpdate(index, value, column, 'dropdown'); - if (this.props.options.confirmFilters !== true) { this.props.onFilterUpdate(index, value, column, 'dropdown'); } }; - handleMultiselectChange = (index, value, column) => { + handleMultiselectChange = (index: number, value: string | string[], column: string) => { this.filterUpdate(index, value, column, 'multiselect'); - if (this.props.options.confirmFilters !== true) { this.props.onFilterUpdate(index, value, column, 'multiselect'); } }; - handleTextFieldChange = (event, index, column) => { + handleTextFieldChange = (event: React.ChangeEvent, index: number, column: string) => { this.filterUpdate(index, event.target.value, column, 'textField'); - if (this.props.options.confirmFilters !== true) { this.props.onFilterUpdate(index, event.target.value, column, 'textField'); } }; - handleCustomChange = (value, index, column) => { - this.filterUpdate(index, value, column.name, column.filterType); - + handleCustomChange = (value: string | string[], index: number, column: MUIDataTableColumnState) => { + this.filterUpdate(index, value, column.name, column.filterType as string); if (this.props.options.confirmFilters !== true) { - this.props.onFilterUpdate(index, value, column.name, column.filterType); + this.props.onFilterUpdate(index, value, column.name, column.filterType as string); } }; - renderCheckbox(column, index, components = {}) { + renderCheckbox( + column: MUIDataTableColumnState, + index: number, + components: { Checkbox?: ComponentType } = {}, + ) { const CheckboxComponent = components.Checkbox || Checkbox; - const { classes, filterData } = this.props; const { filterList } = this.state; - const renderItem = - column.filterOptions && column.filterOptions.renderValue ? column.filterOptions.renderValue : (v) => v; + const filterOpts = column.filterOptions as Record | undefined; + const renderItem = filterOpts?.renderValue ? (filterOpts.renderValue as (v: string) => string) : (v: string) => v; return ( @@ -190,10 +209,7 @@ class TableFilter extends React.Component { className={classes.checkboxIcon} onChange={this.handleCheckboxChange.bind(null, index, filterValue, column.name)} checked={filterList[index].indexOf(filterValue) >= 0} - classes={{ - root: classes.checkbox, - checked: classes.checked, - }} + classes={{ root: classes.checkbox, checked: classes.checked }} value={filterValue != null ? filterValue.toString() : ''} /> } @@ -207,15 +223,15 @@ class TableFilter extends React.Component { ); } - renderSelect(column, index) { + renderSelect(column: MUIDataTableColumnState, index: number) { const { classes, filterData, options } = this.props; const { filterList } = this.state; - const textLabels = options.textLabels.filter; - const renderItem = - column.filterOptions && column.filterOptions.renderValue - ? column.filterOptions.renderValue - : (v) => (v != null ? v.toString() : ''); - const width = (column.filterOptions && column.filterOptions.fullWidth) === true ? 12 : 6; + const textLabels = options.textLabels?.filter; + const filterOpts = column.filterOptions as Record | undefined; + const renderItem = filterOpts?.renderValue + ? (filterOpts.renderValue as (v: string) => string) + : (v: string) => (v != null ? v.toString() : ''); + const width = filterOpts?.fullWidth === true ? 12 : 6; return ( @@ -223,12 +239,14 @@ class TableFilter extends React.Component { {column.label} }> - - {textLabels.all} + + {textLabels?.all} {filterData[index].map((filterValue, filterIndex) => ( @@ -241,13 +259,14 @@ class TableFilter extends React.Component { ); } - renderTextField(column, index) { + renderTextField(column: MUIDataTableColumnState, index: number) { const { classes } = this.props; const { filterList } = this.state; - if (column.filterOptions && column.filterOptions.renderValue) { + const filterOpts = column.filterOptions as Record | undefined; + if (filterOpts?.renderValue) { console.warn('Custom renderValue not supported for textField filters'); } - const width = (column.filterOptions && column.filterOptions.fullWidth) === true ? 12 : 6; + const width = filterOpts?.fullWidth === true ? 12 : 6; return ( @@ -258,21 +277,27 @@ class TableFilter extends React.Component { label={column.label} value={filterList[index].toString() || ''} data-testid={'filtertextfield-' + column.name} - onChange={(event) => this.handleTextFieldChange(event, index, column.name)} + onChange={(event) => + this.handleTextFieldChange(event as React.ChangeEvent, index, column.name) + } /> ); } - renderMultiselect(column, index, components = {}) { + renderMultiselect( + column: MUIDataTableColumnState, + index: number, + components: { Checkbox?: ComponentType } = {}, + ) { const CheckboxComponent = components.Checkbox || Checkbox; - const { classes, filterData } = this.props; const { filterList } = this.state; - const renderItem = - column.filterOptions && column.filterOptions.renderValue ? column.filterOptions.renderValue : (v) => v; - const width = (column.filterOptions && column.filterOptions.fullWidth) === true ? 12 : 6; + const filterOpts = column.filterOptions as Record | undefined; + const renderItem = filterOpts?.renderValue ? (filterOpts.renderValue as (v: string) => string) : (v: string) => v; + const width = filterOpts?.fullWidth === true ? 12 : 6; + return ( @@ -281,9 +306,9 @@ class TableFilter extends React.Component { multiple fullWidth value={filterList[index] || []} - renderValue={(selected) => selected.map(renderItem).join(', ')} + renderValue={(selected) => (selected as string[]).map(renderItem).join(', ')} name={column.name} - onChange={(event) => this.handleMultiselectChange(index, event.target.value, column.name)} + onChange={(event) => this.handleMultiselectChange(index, event.target.value as string[], column.name)} input={}> {filterData[index].map((filterValue, filterIndex) => ( @@ -293,10 +318,7 @@ class TableFilter extends React.Component { checked={filterList[index].indexOf(filterValue) >= 0} value={filterValue != null ? filterValue.toString() : ''} className={classes.checkboxIcon} - classes={{ - root: classes.checkbox, - checked: classes.checked, - }} + classes={{ root: classes.checkbox, checked: classes.checked }} /> @@ -307,26 +329,31 @@ class TableFilter extends React.Component { ); } - renderCustomField(column, index) { + renderCustomField(column: MUIDataTableColumnState, index: number) { const { classes, filterData, options } = this.props; const { filterList } = this.state; - const width = (column.filterOptions && column.filterOptions.fullWidth) === true ? 12 : 6; + const filterOpts = column.filterOptions as Record | undefined; + const width = filterOpts?.fullWidth === true ? 12 : 6; const display = - (column.filterOptions && column.filterOptions.display) || - (options.filterOptions && options.filterOptions.display); + filterOpts?.display || (options.filterOptions && (options.filterOptions as Record).display); if (!display) { console.error('Property "display" is required when using custom filter type.'); return; } - if (column.filterListOptions && column.filterListOptions.renderValue) { - console.warning('"renderValue" is ignored for custom filter fields'); - } return ( - {display(filterList, this.handleCustomChange, index, column, filterData)} + {( + display as ( + filterList: MUIDataTableFilterList, + onChange: unknown, + index: number, + column: MUIDataTableColumnState, + filterData: string[][], + ) => React.ReactNode + )(filterList, this.handleCustomChange, index, column, filterData)} ); @@ -336,20 +363,15 @@ class TableFilter extends React.Component { this.state.filterList.forEach((filter, index) => { this.props.onFilterUpdate(index, filter, this.props.columns[index], 'custom'); }); - - this.props.handleClose(); // close filter dialog popover - + this.props.handleClose(); if (this.props.options.onFilterConfirm) { this.props.options.onFilterConfirm(this.state.filterList); } - return this.state.filterList; }; resetFilters = () => { - this.setState({ - filterList: this.props.columns.map(() => []), - }); + this.setState({ filterList: this.props.columns.map(() => []) }); if (this.props.options.confirmFilters !== true) { this.props.onFilterReset(); } @@ -357,32 +379,28 @@ class TableFilter extends React.Component { render() { const { classes, columns, options, customFooter, filterList, components = {} } = this.props; - const textLabels = options.textLabels.filter; + const textLabels = options.textLabels?.filter; return (
- - {textLabels.title} + + {textLabels?.title}
- + {columns.map((column, index) => { if (column.filter) { const filterType = column.filterType || options.filterType; diff --git a/src/components/TableFilterList.js b/src/components/TableFilterList.tsx similarity index 55% rename from src/components/TableFilterList.js rename to src/components/TableFilterList.tsx index 3c7ecd9a..83c7a8d5 100644 --- a/src/components/TableFilterList.js +++ b/src/components/TableFilterList.tsx @@ -1,7 +1,9 @@ -import { makeStyles } from 'tss-react/mui'; -import PropTypes from 'prop-types'; import React from 'react'; +import { makeStyles } from 'tss-react/mui'; import TableFilterListItem from './TableFilterListItem'; +import type { ComponentType } from 'react'; +import type { MUIDataTableOptions, MUIDataTableFilterList } from '../types/options'; +import type { TableFilterListItemProps } from '../types/components'; const useStyles = makeStyles({ name: 'MUIDataTableFilterList' })(() => ({ root: { @@ -15,6 +17,34 @@ const useStyles = makeStyles({ name: 'MUIDataTableFilterList' })(() => ({ }, })); +interface ColumnName { + name: string; + filterType: string; +} + +interface TableFilterListProps { + options: MUIDataTableOptions; + filterList: MUIDataTableFilterList; + filterUpdate: ( + index: number, + value: string | string[], + columnName: string, + filterType: string, + customUpdate: + | ((filterList: MUIDataTableFilterList, filterPos: number, index: number) => MUIDataTableFilterList) + | null, + callback: (filterList: MUIDataTableFilterList) => void, + ) => void; + filterListRenderers: ((value: string | string[]) => string | string[])[]; + columnNames: ColumnName[]; + serverSideFilterList?: MUIDataTableFilterList | null; + customFilterListUpdate: ( + | ((filterList: MUIDataTableFilterList, filterPos: number, index: number) => MUIDataTableFilterList) + | null + )[]; + ItemComponent?: ComponentType; +} + const TableFilterList = ({ options, filterList, @@ -24,31 +54,38 @@ const TableFilterList = ({ serverSideFilterList, customFilterListUpdate, ItemComponent = TableFilterListItem, -}) => { +}: TableFilterListProps) => { const { classes } = useStyles(); const { serverSide } = options; - - const removeFilter = (index, filterValue, columnName, filterType, customFilterListUpdate = null) => { - let removedFilter = filterValue; + const removeFilter = ( + index: number, + filterValue: string | string[], + columnName: string, + filterType: string, + customUpdate: + | ((filterList: MUIDataTableFilterList, filterPos: number, idx: number) => MUIDataTableFilterList) + | null = null, + ) => { + let removedFilter: string | string[] = filterValue; if (Array.isArray(removedFilter) && removedFilter.length === 0) { removedFilter = filterList[index]; } - filterUpdate(index, filterValue, columnName, filterType, customFilterListUpdate, (filterList) => { + filterUpdate(index, filterValue, columnName, filterType, customUpdate, (fl) => { if (options.onFilterChipClose) { - options.onFilterChipClose(index, removedFilter, filterList); + options.onFilterChipClose(index, removedFilter as string, fl); } }); }; - const customFilterChip = (customFilterItem, index, customFilterItemIndex, item, isArray) => { - let type; - // If our custom filter list is an array, we need to check for custom update functions to determine - // default type. Otherwise we use the supplied type in options. - if (isArray) { - type = customFilterListUpdate[index] ? 'custom' : 'chip'; - } else { - type = columnNames[index].filterType; - } + + const customFilterChip = ( + customFilterItem: string, + index: number, + customFilterItemIndex: number, + item: string[], + isArray: boolean, + ) => { + const type = isArray ? (customFilterListUpdate[index] ? 'custom' : 'chip') : columnNames[index].filterType; return ( ); }; - const filterChip = (index, data, colIndex) => ( + const filterChip = (index: number, data: string, colIndex: number) => ( removeFilter(index, data, columnNames[index].name, 'chip')} className={classes.chip} @@ -91,9 +128,9 @@ const TableFilterList = ({ /> ); - const getFilterList = (filterList) => { - return filterList.map((item, index) => { - if (columnNames[index].filterType === 'custom' && filterList[index].length) { + const getFilterList = (fl: MUIDataTableFilterList) => { + return fl.map((item, index) => { + if (columnNames[index].filterType === 'custom' && fl[index].length) { const filterListRenderersValue = filterListRenderers[index](item); if (Array.isArray(filterListRenderersValue)) { @@ -116,21 +153,4 @@ const TableFilterList = ({ ); }; -TableFilterList.propTypes = { - /** Data used to filter table against */ - filterList: PropTypes.array.isRequired, - /** Filter List value renderers */ - filterListRenderers: PropTypes.array.isRequired, - /** Columns used to describe table */ - columnNames: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ name: PropTypes.string.isRequired, filterType: PropTypes.string }), - ]), - ).isRequired, - /** Callback to trigger filter update */ - onFilterUpdate: PropTypes.func, - ItemComponent: PropTypes.any, -}; - export default TableFilterList; diff --git a/src/components/TableFilterListItem.js b/src/components/TableFilterListItem.js deleted file mode 100644 index 7531cbf6..00000000 --- a/src/components/TableFilterListItem.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Chip } from '@mui/material'; -import PropTypes from 'prop-types'; -import React from 'react'; -import clsx from 'clsx'; - -const TableFilterListItem = ({ label, onDelete, className, filterProps }) => { - filterProps = filterProps || {}; - if (filterProps.className) { - className = clsx(className, filterProps.className); - } - return ; -}; - -TableFilterListItem.propTypes = { - label: PropTypes.node, - onDelete: PropTypes.func.isRequired, - className: PropTypes.string.isRequired, -}; - -export default TableFilterListItem; diff --git a/src/components/TableFilterListItem.tsx b/src/components/TableFilterListItem.tsx new file mode 100644 index 00000000..9cfa948e --- /dev/null +++ b/src/components/TableFilterListItem.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Chip } from '@mui/material'; +import clsx from 'clsx'; +import type { ComponentProps } from 'react'; +import type { TableFilterListItemProps } from '../types/components'; + +const TableFilterListItem = ({ label, onDelete, className, filterProps }: TableFilterListItemProps) => { + const mergedProps = (filterProps || {}) as ComponentProps; + let mergedClassName = className; + if (typeof mergedProps.className === 'string') { + mergedClassName = clsx(className, mergedProps.className); + } + return ; +}; + +export default TableFilterListItem; diff --git a/src/components/TableFooter.js b/src/components/TableFooter.tsx similarity index 55% rename from src/components/TableFooter.js rename to src/components/TableFooter.tsx index f9ca2314..1a743dd5 100644 --- a/src/components/TableFooter.js +++ b/src/components/TableFooter.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Table as MuiTable } from '@mui/material'; import TablePagination from './TablePagination'; import { makeStyles } from 'tss-react/mui'; -import PropTypes from 'prop-types'; +import type { MUIDataTableOptions } from '../types/options'; const useStyles = makeStyles({ name: 'MUIDataTableFooter' })(() => ({ root: { @@ -12,20 +12,29 @@ const useStyles = makeStyles({ name: 'MUIDataTableFooter' })(() => ({ }, })); -const TableFooter = ({ options, rowCount, page, rowsPerPage, changeRowsPerPage, changePage }) => { +interface TableFooterProps { + options: MUIDataTableOptions; + rowCount: number; + page: number; + rowsPerPage: number; + changeRowsPerPage: (rowsPerPage: number) => void; + changePage: (page: number) => void; +} + +const TableFooter = ({ options, rowCount, page, rowsPerPage, changeRowsPerPage, changePage }: TableFooterProps) => { const { classes } = useStyles(); const { customFooter, pagination = true } = options; - if (customFooter) { + if (customFooter && typeof customFooter === 'function') { return ( - {options.customFooter( + {customFooter( rowCount, page, rowsPerPage, changeRowsPerPage, changePage, - options.textLabels.pagination, + options.textLabels?.pagination as never, )} ); @@ -40,7 +49,6 @@ const TableFooter = ({ options, rowCount, page, rowsPerPage, changeRowsPerPage, rowsPerPage={rowsPerPage} changeRowsPerPage={changeRowsPerPage} changePage={changePage} - component={'div'} options={options} /> @@ -50,25 +58,4 @@ const TableFooter = ({ options, rowCount, page, rowsPerPage, changeRowsPerPage, return null; }; -TableFooter.propTypes = { - /** Total number of table rows */ - rowCount: PropTypes.number.isRequired, - /** Options used to describe table */ - options: PropTypes.shape({ - customFooter: PropTypes.func, - pagination: PropTypes.bool, - textLabels: PropTypes.shape({ - pagination: PropTypes.object, - }), - }), - /** Current page index */ - page: PropTypes.number.isRequired, - /** Total number allowed of rows per page */ - rowsPerPage: PropTypes.number.isRequired, - /** Callback to trigger rows per page change */ - changeRowsPerPage: PropTypes.func.isRequired, - /** Callback to trigger page change */ - changePage: PropTypes.func.isRequired, -}; - export default TableFooter; diff --git a/src/components/TableHead.js b/src/components/TableHead.tsx similarity index 69% rename from src/components/TableHead.js rename to src/components/TableHead.tsx index b74bee82..e398d7a2 100644 --- a/src/components/TableHead.js +++ b/src/components/TableHead.tsx @@ -1,10 +1,20 @@ +import React, { useState } from 'react'; import { makeStyles } from 'tss-react/mui'; import clsx from 'clsx'; import { TableHead as MuiTableHead } from '@mui/material'; -import React, { useState } from 'react'; import TableHeadCell from './TableHeadCell'; import TableHeadRow from './TableHeadRow'; import TableSelectCell from './TableSelectCell'; +import type { ComponentType } from 'react'; +import type { MUIDataTableColumnState } from '../types/columns'; +import type { + MUIDataTableOptions, + MUIDataTableSelectedRows, + MUIDataTableExpandedRows, + MUIDataTableDisplayRow, + MUIDataTableSortOrder, +} from '../types/options'; +import type { HeadCellRefs, ColumnDropTimers } from '../types/drag'; const useStyles = makeStyles({ name: 'MUIDataTableHead' })((theme) => ({ main: {}, @@ -23,6 +33,27 @@ const useStyles = makeStyles({ name: 'MUIDataTableHead' })((theme) => ({ }, })); +interface TableHeadProps { + columnOrder?: number[] | null; + columns: MUIDataTableColumnState[]; + components?: { Tooltip?: ComponentType; Checkbox?: ComponentType }; + count: number; + data: MUIDataTableDisplayRow[]; + draggableHeadCellRefs?: HeadCellRefs; + expandedRows?: MUIDataTableExpandedRows; + options: MUIDataTableOptions; + selectedRows: MUIDataTableSelectedRows; + selectRowUpdate: (type: string, val: null) => void; + setCellRef?: (index: number, pos: number, el: HTMLTableCellElement | null) => void; + sortOrder?: Partial; + tableRef?: () => HTMLElement | null; + tableId?: string; + timers?: ColumnDropTimers; + toggleAllExpandableRows?: () => void; + toggleSort: (index: number) => void; + updateColumnOrder?: (columnOrder: number[], src: number, target: number) => void; +} + const TableHead = ({ columnOrder = null, columns, @@ -42,16 +73,17 @@ const TableHead = ({ toggleAllExpandableRows, toggleSort, updateColumnOrder, -}) => { +}: TableHeadProps) => { const { classes } = useStyles(); - if (columnOrder === null) { - columnOrder = columns ? columns.map((item, idx) => idx) : []; + let resolvedColumnOrder = columnOrder; + if (resolvedColumnOrder === null) { + resolvedColumnOrder = columns ? columns.map((_item, idx) => idx) : []; } const [dragging, setDragging] = useState(false); - const handleToggleColumn = (index) => { + const handleToggleColumn = (index: number) => { toggleSort(index); }; @@ -63,9 +95,6 @@ const TableHead = ({ let isIndeterminate = numSelected > 0 && numSelected < count; let isChecked = numSelected > 0 && numSelected >= count; - // When the disableToolbarSelect option is true, there can be - // selected items that aren't visible, so we need to be more - // precise when determining if the head checkbox should be checked. if ( options.disableToolbarSelect === true || options.selectToolbarPlacement === 'none' || @@ -86,13 +115,11 @@ const TableHead = ({ } } - let orderedColumns = columnOrder.map((colIndex, idx) => { - return { - column: columns[colIndex], - index: colIndex, - colPos: idx, - }; - }); + const orderedColumns = resolvedColumnOrder.map((colIndex, idx) => ({ + column: columns[colIndex], + index: colIndex, + colPos: idx, + })); return ( column.display === 'true' && (column.customHeadRender ? ( - column.customHeadRender({ index, ...column }, handleToggleColumn, sortOrder) + column.customHeadRender({ index, ...column }, handleToggleColumn, sortOrder as MUIDataTableSortOrder) ) : ( ({ root: {}, @@ -60,6 +63,30 @@ const useStyles = makeStyles({ name: 'MUIDataTableHeadCell' })((theme) => ({ }, })); +interface TableHeadCellProps { + cellHeaderProps?: ComponentProps; + children?: ReactNode; + colPosition?: number; + column?: MUIDataTableColumnState; + columns?: MUIDataTableColumnState[]; + columnOrder?: number[]; + components?: { Tooltip?: ComponentType }; + draggableHeadCellRefs?: HeadCellRefs; + draggingHook?: [boolean, (val: boolean) => void]; + hint?: string; + index: number; + options: MUIDataTableOptions; + print: boolean; + setCellRef?: (index: number, pos: number, el: HTMLTableCellElement | null) => void; + sort: boolean; + sortDirection?: MUIDataTableSortDirection; + tableRef?: () => HTMLElement | null; + tableId?: string; + timers?: ColumnDropTimers; + toggleSort: (index: number) => void; + updateColumnOrder?: (columnOrder: number[], src: number, target: number) => void; +} + const TableHeadCell = ({ cellHeaderProps = {}, children, @@ -82,13 +109,13 @@ const TableHeadCell = ({ timers, toggleSort, updateColumnOrder, -}) => { +}: TableHeadCellProps) => { const [sortTooltipOpen, setSortTooltipOpen] = useState(false); const [hintTooltipOpen, setHintTooltipOpen] = useState(false); const { classes } = useStyles(); - const handleKeyboardSortInput = (e) => { + const handleKeyboardSortInput = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { toggleSort(index); } @@ -100,16 +127,32 @@ const TableHeadCell = ({ toggleSort(index); }; - const [dragging, setDragging] = draggingHook ? draggingHook : []; + const [dragging, setDragging] = draggingHook ? draggingHook : ([] as unknown as [boolean, (val: boolean) => void]); - const { className, ...otherProps } = cellHeaderProps; - const Tooltip = components.Tooltip || MuiTooltip; + const { className, ...otherProps } = cellHeaderProps as { className?: string; [key: string]: unknown }; + const Tooltip = (components.Tooltip || MuiTooltip) as typeof MuiTooltip; const sortActive = sortDirection !== 'none' && sortDirection !== undefined; const ariaSortDirection = sortDirection === 'none' ? false : sortDirection; const isDraggingEnabled = () => { if (!draggingHook) return false; - return options.draggableColumns && options.draggableColumns.enabled && column.draggable !== false; + return ( + options.draggableColumns && + options.draggableColumns.enabled && + (column as unknown as Record)?.draggable !== false + ); + }; + + const setDragRefButton: RefCallback = (node) => { + if (isDraggingEnabled()) { + dragRef(node); + } + }; + + const setDragRefDiv: RefCallback = (node) => { + if (isDraggingEnabled()) { + dragRef(node); + } }; const sortLabelProps = { @@ -117,24 +160,24 @@ const TableHeadCell = ({ tabIndex: -1, active: sortActive, hideSortIcon: true, - ...(ariaSortDirection ? { direction: sortDirection } : {}), + ...(ariaSortDirection ? { direction: sortDirection as 'asc' | 'desc' } : {}), }; - const [{ opacity }, dragRef, preview] = useDrag({ + const [{ opacity }, dragRef] = useDrag({ type: 'HEADER', item: () => { setTimeout(() => { setHintTooltipOpen(false); setSortTooltipOpen(false); - setDragging(true); + if (setDragging) setDragging(true); }, 0); return { colIndex: index, headCellRefs: draggableHeadCellRefs, }; }, - end: (item, monitor) => { - setDragging(false); + end: () => { + if (setDragging) setDragging(false); }, collect: (monitor) => { return { @@ -144,40 +187,36 @@ const TableHeadCell = ({ }); const [drop] = useColumnDrop({ - drop: (item, mon) => { + drop: () => { setSortTooltipOpen(false); setHintTooltipOpen(false); - setDragging(false); + if (setDragging) setDragging(false); }, index, - headCellRefs: draggableHeadCellRefs, - updateColumnOrder, + headCellRefs: draggableHeadCellRefs || {}, + updateColumnOrder: updateColumnOrder || (() => {}), columnOrder, - columns, + columns: columns || [], transitionTime: options.draggableColumns ? options.draggableColumns.transitionTime : 300, tableRef: tableRef ? tableRef() : null, tableId: tableId || 'none', - timers, + timers: timers || {}, }); const cellClass = clsx({ [classes.root]: true, [classes.fixedHeader]: options.fixedHeader, 'datatables-noprint': !print, - [className]: className, + [className as string]: !!className, }); - const showHintTooltip = () => { - setSortTooltipOpen(false); - setHintTooltipOpen(true); - }; - const getTooltipTitle = () => { if (dragging) return ''; if (!options.textLabels) return ''; - return options.textLabels.body.columnHeaderTooltip - ? options.textLabels.body.columnHeaderTooltip(column) - : options.textLabels.body.toolTip; + if (column && options.textLabels.body?.columnHeaderTooltip) { + return (options.textLabels.body as { columnHeaderTooltip: (col: unknown) => string }).columnHeaderTooltip(column); + } + return options.textLabels.body?.toolTip || ''; }; const closeTooltip = () => { @@ -186,13 +225,13 @@ const TableHeadCell = ({ return ( { + ref={(ref: HTMLTableCellElement | null) => { drop && drop(ref); - setCellRef && setCellRef(index + 1, colPosition + 1, ref); + setCellRef && setCellRef(index + 1, (colPosition || 0) + 1, ref); }} className={cellClass} scope={'col'} - sortDirection={ariaSortDirection} + sortDirection={ariaSortDirection || undefined} data-colindex={index} data-tableid={tableId} onMouseDown={closeTooltip} @@ -210,12 +249,12 @@ const TableHeadCell = ({ popper: classes.mypopper, }}>