diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md index b9b4c54..df161d7 100644 --- a/ACCESSIBILITY.md +++ b/ACCESSIBILITY.md @@ -1,5 +1,8 @@ # Accessibility When Developing and Testing +![Author: Leon Slater](https://img.shields.io/badge/Author-Leon_Slater-blue) +![Last update: 2024/08/05](https://img.shields.io/badge/Last_updated-2024/08/25-blue) + Accessibility is a core part of this library and the components it contains. So, at a minimum, all components should be tested with the following document in mind. ## Automated testing @@ -27,7 +30,7 @@ These considerations will cover most scenarios, particularly at a component leve - does any essential complex functionality have keyboard-only alternatives? (e.g. drag and drop) 3. **Is it clear and functional for users without vision?** - points (1) and (2) above will do most of this for you - - **test using at least NVDA** as it is one of the most widely used screen-readers (bonus points for also testing with other screen-readers, such as TalkBack, Windows Narrator, and VoiceOver) + - **test using at least NVDA** as it is one of the most widely used screen-readers (bonus points for also testing with other screen-readers, such as TalkBack, Windows Narrator, and VoiceOver). In particular, **test with a screen-reader using only your keyboard**: does everything you can tab to have an announcement that clearly indicates what it's for? - Consider if there are elements that need additional text (e.g. icon buttons), or if there are elements that a screen-reader can ignore (e.g. purely presentation iconography that you could set `aria-hidden="true"` for) 4. **Does it work at a viewport width of 320px?** - This is to account for the "reflow" criterion: no loss of content or functionality occurs, and horizontal scrolling is avoided diff --git a/README.md b/README.md index 1b03bd7..5ec2b04 100755 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ![License: MIT](https://img.shields.io/npm/l/rea11y-easy-form) ![NPM version](https://img.shields.io/npm/v/rea11y-easy-form.svg) -![Types](https://img.shields.io/npm/types/rea11y-easy-form) -![Tree Shaking](https://flat.badgen.net/bundlephobia/tree-shaking/rea11y-easy-form) +![Types: TypeScript](https://img.shields.io/npm/types/rea11y-easy-form) +![Tree Shaking: Supported](https://badgen.net/bundlephobia/tree-shaking/rea11y-easy-form) ![React](https://img.shields.io/badge/React-%2320232a.svg?logo=React&logoColor=%2361DAFB) ![React Final Form](https://img.shields.io/badge/React%20Final%20Form-%23333639.svg?logo=react&logoColor=white) diff --git a/package-lock.json b/package-lock.json index 1878aec..e53028c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ }, "devDependencies": { "@babel/core": "^7.25.2", - "@babel/preset-env": "^7.25.3", + "@babel/preset-env": "^7.25.4", "@babel/preset-typescript": "^7.24.7", "@chromatic-com/storybook": "^1.7.0", "@rollup/plugin-commonjs": "^26.0.1", @@ -39,7 +39,7 @@ "@storybook/react-webpack5": "^8.2.9", "@storybook/test": "^8.2.9", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.4.8", + "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.0", "@types/dompurify": "^3.0.5", "@types/jest": "^29.5.12", @@ -72,7 +72,7 @@ "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.36.0", "storybook": "^8.2.9", - "ts-jest": "^29.2.4", + "ts-jest": "^29.2.5", "typescript": "^5.5.4" }, "peerDependencies": { @@ -136,9 +136,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -219,12 +219,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.25.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", + "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", "dev": true, "dependencies": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.4", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -296,19 +296,17 @@ "license": "ISC" }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/traverse": "^7.25.4", "semver": "^6.3.1" }, "engines": { @@ -366,31 +364,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", @@ -669,12 +642,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", + "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", "dev": true, "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -1176,15 +1149,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", - "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz", + "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-remap-async-to-generator": "^7.25.0", "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/traverse": "^7.25.0" + "@babel/traverse": "^7.25.4" }, "engines": { "node": ">=6.9.0" @@ -1241,13 +1214,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", + "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1274,16 +1247,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", - "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", + "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "globals": "^11.1.0" }, "engines": { @@ -1743,13 +1716,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", + "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2052,13 +2025,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", + "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2068,12 +2041,12 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", - "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", + "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.25.2", + "@babel/compat-data": "^7.25.4", "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", @@ -2102,13 +2075,13 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoped-functions": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", @@ -2136,7 +2109,7 @@ "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.25.4", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-property-literals": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", @@ -2149,10 +2122,10 @@ "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.37.1", "semver": "^6.3.1" @@ -2165,9 +2138,9 @@ } }, "node_modules/@babel/preset-env/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", - "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -2181,13 +2154,13 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2417,16 +2390,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", + "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.4", + "@babel/parser": "^7.25.4", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.4", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2435,9 +2408,9 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", + "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.24.8", @@ -4982,13 +4955,12 @@ "license": "MIT" }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.8", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", - "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", "dev": true, "dependencies": { "@adobe/css-tools": "^4.4.0", - "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", @@ -12323,17 +12295,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lz-string": { "version": "1.5.0", "dev": true, @@ -16045,12 +16006,10 @@ } }, "node_modules/semver": { - "version": "7.5.4", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -17022,20 +16981,20 @@ } }, "node_modules/ts-jest": { - "version": "29.2.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", - "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, "dependencies": { - "bs-logger": "0.x", + "bs-logger": "^0.2.6", "ejs": "^3.1.10", - "fast-json-stable-stringify": "2.x", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" @@ -18230,9 +18189,9 @@ } }, "@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true }, "@babel/core": { @@ -18290,12 +18249,12 @@ } }, "@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.25.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.5.tgz", + "integrity": "sha512-abd43wyLfbWoxC6ahM8xTkqLpGB2iWBVyuKC9/srhFunCd1SDNrV1s72bBpK4hLj8KLzHBBcOblvLQZBNw9r3w==", "dev": true, "requires": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.4", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -18351,19 +18310,17 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/traverse": "^7.25.4", "semver": "^6.3.1" }, "dependencies": { @@ -18401,25 +18358,6 @@ "resolve": "^1.14.2" } }, - "@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "requires": { - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "requires": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, "@babel/helper-member-expression-to-functions": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", @@ -18628,12 +18566,12 @@ } }, "@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.4.tgz", + "integrity": "sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==", "dev": true, "requires": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.4" } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { @@ -18932,15 +18870,15 @@ } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", - "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.4.tgz", + "integrity": "sha512-jz8cV2XDDTqjKPwVPJBIjORVEmSGYhdRa8e5k5+vN+uwcjSrSxUaebBRa4ko1jqNF2uxyg8G6XYk30Jv285xzg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-remap-async-to-generator": "^7.25.0", "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/traverse": "^7.25.0" + "@babel/traverse": "^7.25.4" } }, "@babel/plugin-transform-async-to-generator": { @@ -18973,13 +18911,13 @@ } }, "@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", - "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.4.tgz", + "integrity": "sha512-nZeZHyCWPfjkdU5pA/uHiTaDAFUEqkpzf1YoQT2NeSynCGYq9rxfyI3XpQbfx/a0hSnFH6TGlEXvae5Vi7GD8g==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-class-static-block": { @@ -18994,16 +18932,16 @@ } }, "@babel/plugin-transform-classes": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", - "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.4.tgz", + "integrity": "sha512-oexUfaQle2pF/b6E0dwsxQtAol9TLSO88kQvym6HHBWFliV2lGdrPieX+WgMRLSJDVzdYywk7jXbLPuO2KLTLg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.25.0", - "@babel/traverse": "^7.25.0", + "@babel/traverse": "^7.25.4", "globals": "^11.1.0" } }, @@ -19289,13 +19227,13 @@ } }, "@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", - "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.4.tgz", + "integrity": "sha512-ao8BG7E2b/URaUQGqN3Tlsg+M3KlHY6rJ1O1gXAEUnZoyNQnvKyH87Kfg+FoxSeyWUB8ISZZsC91C44ZuBFytw==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-class-features-plugin": "^7.25.4", + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-private-property-in-object": { @@ -19477,22 +19415,22 @@ } }, "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", - "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.4.tgz", + "integrity": "sha512-qesBxiWkgN1Q+31xUE9RcMk79eOXXDCv6tfyGMRSs4RGlioSg2WVyQAm07k726cSE56pa+Kb0y9epX2qaXzTvA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-create-regexp-features-plugin": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/preset-env": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", - "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.4.tgz", + "integrity": "sha512-W9Gyo+KmcxjGahtt3t9fb14vFRWvPpu5pT6GBlovAK6BTBcxgjfVMSQCfJl4oi35ODrxP6xx2Wr8LNST57Mraw==", "dev": true, "requires": { - "@babel/compat-data": "^7.25.2", + "@babel/compat-data": "^7.25.4", "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", @@ -19521,13 +19459,13 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoped-functions": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", @@ -19555,7 +19493,7 @@ "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.25.4", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-property-literals": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", @@ -19568,19 +19506,19 @@ "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.4", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-corejs3": "^0.10.6", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "dependencies": { "@babel/helper-define-polyfill-provider": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", - "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "dev": true, "requires": { "@babel/helper-compilation-targets": "^7.22.6", @@ -19591,13 +19529,13 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" } }, "babel-plugin-polyfill-regenerator": { @@ -19760,24 +19698,24 @@ } }, "@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.4.tgz", + "integrity": "sha512-VJ4XsrD+nOvlXyLzmLzUs/0qjFS4sK30te5yEFlvbbUNEgKaVb2BHZUpAL+ttLPQAHNrsI3zZisbfha5Cvr8vg==", "dev": true, "requires": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.4", + "@babel/parser": "^7.25.4", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.4", "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.4.tgz", + "integrity": "sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.24.8", @@ -21370,13 +21308,12 @@ } }, "@testing-library/jest-dom": { - "version": "6.4.8", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", - "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", "dev": true, "requires": { "@adobe/css-tools": "^4.4.0", - "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", @@ -26483,13 +26420,6 @@ "tslib": "^2.0.3" } }, - "lru-cache": { - "version": "6.0.0", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "lz-string": { "version": "1.5.0", "dev": true @@ -28884,11 +28814,10 @@ } }, "semver": { - "version": "7.5.4", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true }, "send": { "version": "0.18.0", @@ -29570,20 +29499,20 @@ "dev": true }, "ts-jest": { - "version": "29.2.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", - "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, "requires": { - "bs-logger": "0.x", + "bs-logger": "^0.2.6", "ejs": "^3.1.10", - "fast-json-stable-stringify": "2.x", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "dependencies": { "yargs-parser": { diff --git a/package.json b/package.json index 0339246..99ff8cd 100755 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@babel/core": "^7.25.2", - "@babel/preset-env": "^7.25.3", + "@babel/preset-env": "^7.25.4", "@babel/preset-typescript": "^7.24.7", "@chromatic-com/storybook": "^1.7.0", "@rollup/plugin-commonjs": "^26.0.1", @@ -83,7 +83,7 @@ "@storybook/react-webpack5": "^8.2.9", "@storybook/test": "^8.2.9", "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.4.8", + "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.0", "@types/dompurify": "^3.0.5", "@types/jest": "^29.5.12", @@ -116,7 +116,7 @@ "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-typescript2": "^0.36.0", "storybook": "^8.2.9", - "ts-jest": "^29.2.4", + "ts-jest": "^29.2.5", "typescript": "^5.5.4" }, "dependencies": { diff --git a/src/Roadmap.mdx b/src/Roadmap.mdx index d750f8e..6fdcfca 100644 --- a/src/Roadmap.mdx +++ b/src/Roadmap.mdx @@ -4,80 +4,83 @@ import { Meta } from '@storybook/blocks'; # Roadmap +![Author: Leon Slater](https://img.shields.io/badge/Author-Leon_Slater-blue) + ## For 1.0 ### Core component exports -* `EasyForm` ✓ -* `EasyField` ✓ +- `EasyForm` ✓ +- `EasyField` ✓ ### Form field components -* `Input` ✓ -* `Textarea` ✓ -* `Checkbox` ✓ -* `CheckboxList` ✓ -* `ColorInput` ✓ -* `Radio` ✓ -* `RadioList` ✓ -* `Select` ✓ -* `Switch` ✓ -* `SwitchList` ✓ -* `AutoComplete` ✓ (supports multi-select) +- `Input` ✓ +- `Textarea` ✓ +- `Checkbox` ✓ +- `CheckboxList` ✓ +- `ColorInput` ✓ +- `Radio` ✓ +- `RadioList` ✓ +- `Select` ✓ +- `Switch` ✓ +- `SwitchList` ✓ +- `AutoComplete` ✓ (supports multi-select) ### Util components (used by form components, and exported) -* `Fieldset` ✓ (also a container) -* `ErrorMessage` ✓ -* `Label` ✓ -* `Notice` ✓ (looks like an alert, but not a `role='alert'` element by default) -* `Skeleton` ✓ (for loading states) -* `ValidationSummary` ✓ -* `VisuallyHidden` ✓ +- `Fieldset` ✓ (also a container) +- `ErrorMessage` ✓ +- `Label` ✓ +- `MutatedFormSpy` ✓ +- `Notice` ✓ (looks like an alert, but not a `role='alert'` element by default) +- `Skeleton` ✓ (for loading states) +- `ValidationSummary` ✓ +- `VisuallyHidden` ✓ ### Container/unique components -* `AsHtml` ✓ -* `Disclosure` ✓ -* `Fieldset` ✓ (also a util) -* `FieldRepeater` ✓ (unit tests and thorough manual testing ongoing) -* `FieldConditional` ✓ +- `AsHtml` ✓ +- `Disclosure` ✓ +- `Fieldset` ✓ (also a util) +- `FieldRepeater` ✓ (unit tests and thorough manual testing ongoing) +- `FieldConditional` ✓ ### Utility/Form functionality hooks -* `useAnnounce` ✓ (for easy screen-reader announcements) -* `useFieldValue` ✓ (for easy form field value fetching) -* `useCheckFieldValue` ✓ (for confirming a field (or fields) has the desired value(s)) -* `useMutatedField` ✓ (for correcting async validation state issues) -* `useMutatedFormState` ✓ (for correcting async validation state issues) +- `useAnnounce` ✓ (for easy screen-reader announcements) +- `useFieldValue` ✓ (for easy form field value fetching) +- `useCheckFieldValue` ✓ (for confirming a field (or fields) has the desired value(s)) +- `useMutatedField` ✓ (for correcting async validation state issues) +- `useMutatedFormState` ✓ (for correcting async validation state issues) ### Built-in validation functions Only the most basic built-in rules to be included. We do not want to bloat the package. -* `isAlpha` ✓ -* `isAlphaNumeric` ✓ -* `isDate` ✓ -* `isEmail` ✓ -* `isEmpty` ✓ (reversed for `required` rule) -* `isHexColor` ✓ -* `isInteger` ✓ -* `isLowerCase` ✓ -* `isNumber` ✓ -* `isUpperCase` ✓ -* `isUrl` ✓ +- `isAlpha` ✓ +- `isAlphaNumeric` ✓ +- `isDate` ✓ +- `isEmail` ✓ +- `isEmpty` ✓ (reversed for `required` rule) +- `isHexColor` ✓ +- `isInteger` ✓ +- `isLowerCase` ✓ +- `isNumber` ✓ +- `isUpperCase` ✓ +- `isUrl` ✓ ### Miscellaneous / Chore -* Wide-spread CSS variables for easier theming? (not strictly necessary) -* Proper Generics handling for components that take an `as` prop +- Wide-spread CSS variables for easier theming? (not strictly necessary) +- Proper Generics handling for components that take an `as` prop ✓ ## Beyond 1.0 ideas ### Form field components -* `StarRating` (how granular? E.g. can it display 3.5 out of 5, but only allow selecting full values?) -* `RichTextEditor` (do we want this as part of the core code? Will be big!) -* `Datepicker` (do we want this? Can use native, and custom can be added) -* `Timepicker` (do we want this? Can use native, and custom can be added) -* `Telephone` (do we want this? Is complex. Can use native, and custom can be added) +- `StarRating` (how granular? E.g. can it display 3.5 out of 5, but only allow selecting full values?) +- `RichTextEditor` (do we want this as part of the core code? Will be big!) +- `Datepicker` (do we want this? Can use native, and custom can be added) +- `Timepicker` (do we want this? Can use native, and custom can be added) +- `Telephone` (do we want this? Is complex. Can use native, and custom can be added) diff --git a/src/components/CheckboxList/CheckboxList.test.tsx b/src/components/CheckboxList/CheckboxList.test.tsx index 5aca976..b672c5f 100644 --- a/src/components/CheckboxList/CheckboxList.test.tsx +++ b/src/components/CheckboxList/CheckboxList.test.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import CheckboxList from './CheckboxList'; import SwitchList, { SWITCH_LIST_TYPE } from '../SwitchList'; -import { CheckboxListProps } from './CheckboxList.types'; import { fieldClassName } from '../../utils'; +import type { SwitchListProps } from '../SwitchList/SwitchList.types'; jest.mock('../SwitchList', () => ({ __esModule: true, @@ -12,7 +12,7 @@ jest.mock('../SwitchList', () => ({ })); describe('', () => { - let props: CheckboxListProps; + let props: Omit; beforeEach(() => { props = { diff --git a/src/components/CheckboxList/CheckboxList.tsx b/src/components/CheckboxList/CheckboxList.tsx index 680db76..284b720 100644 --- a/src/components/CheckboxList/CheckboxList.tsx +++ b/src/components/CheckboxList/CheckboxList.tsx @@ -3,26 +3,25 @@ import clsx from 'clsx'; import { isEqual } from '@react-hookz/deep-equal'; import { useFieldClassName } from '../../utils'; import SwitchList, { SWITCH_LIST_TYPE } from '../SwitchList'; -import { CheckboxListProps } from './CheckboxList.types'; +import type { SwitchListProps } from '../SwitchList/SwitchList.types'; -const CheckboxList = forwardRef( - ({ className, ...other }, ref) => { - const classPrefix = useFieldClassName('checkbox-list'); - return ( - - ); - } -); +const CheckboxList = forwardRef< + HTMLFieldSetElement, + Omit +>(({ className, ...other }, ref) => { + const classPrefix = useFieldClassName('checkbox-list'); + return ( + + ); +}); // do a deep equal comparison in this case, -// to account for lazy use of the `options` prop -// @todo: confirm if there is any performance benefit by memoising here, -// as the base SwitchList already does a deepEqual props comparison +// to account for the `options` prop being an inline array const MemoisedCheckboxList = memo(CheckboxList, isEqual); MemoisedCheckboxList.displayName = 'CheckboxList'; export default MemoisedCheckboxList; diff --git a/src/components/CheckboxList/CheckboxList.types.ts b/src/components/CheckboxList/CheckboxList.types.ts deleted file mode 100644 index 87becbd..0000000 --- a/src/components/CheckboxList/CheckboxList.types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { BaseSwitchListProps } from '../SwitchList/SwitchList.types'; - -export interface CheckboxListProps extends BaseSwitchListProps {} diff --git a/src/components/EasyField/EasyFieldField/EasyFieldField.tsx b/src/components/EasyField/EasyFieldField/EasyFieldField.tsx index b2308ff..2d3fb10 100644 --- a/src/components/EasyField/EasyFieldField/EasyFieldField.tsx +++ b/src/components/EasyField/EasyFieldField/EasyFieldField.tsx @@ -130,7 +130,7 @@ const EasyFieldField = ({ // @todo: setting classes based on meta state could cause a lot of DOM updates // e.g. from the `validating` state on each keypress; - // if `validateFields` is also used, then this could + // if `validateFields` is also used, then this could // cause a lot of extra DOM updates on the page. // So we should test the impact of this, and maybe put this behind a prop const metaClassNames = useDeepCompareMemo( diff --git a/src/components/EasyField/useEasyFieldValidator.ts b/src/components/EasyField/useEasyFieldValidator.ts deleted file mode 100644 index 9ba9a8f..0000000 --- a/src/components/EasyField/useEasyFieldValidator.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { useCallback } from 'react'; -import { useDeepCompareMemo } from '@react-hookz/web'; -import { useEasyFormContext } from '../EasyForm/EasyFormContext'; -import { capitaliseFirstLetter, isNullOrUndefined } from '../../utils'; - -import type { FieldValidator } from 'final-form'; -import type { Dictionary } from '../../utils'; -import type { EasyFieldValidationRule } from './EasyField.types'; - -const useEasyFieldValidator = ( - requiredArg?: EasyFieldValidationRule, - validation?: Dictionary, - validate?: FieldValidator -) => { - const { defaultFieldValidationFunctions } = useEasyFormContext(); - const { required, ...otherValidation } = validation || {}; - const validators = useDeepCompareMemo( - () => - Object.entries(validation || {}).filter( - ([key, value]) => - typeof value === 'function' || - (Boolean(value) && - typeof defaultFieldValidationFunctions[key] === 'function') - ), - [otherValidation, defaultFieldValidationFunctions] - ); - - const requiredFunc = - typeof requiredArg === 'function' - ? requiredArg - : typeof required === 'function' - ? required - : required || requiredArg - ? defaultFieldValidationFunctions.required - : null; - - const requiredString = - typeof requiredArg === 'string' - ? requiredArg - : typeof required === 'string' - ? required - : 'Required'; - - const finalValidator: FieldValidator = useCallback( - async (...args) => { - // check required rule before anything else - if (typeof requiredFunc === 'function') { - const result = await requiredFunc(...args); - if (result) { - return typeof result === 'string' ? result : requiredString; - } - } - // if the field has no value, do not run any other validators; - // but we will allow field values that are reference value types - // (and even booleans) to trigger further rules - // as these may indicate more complex field components - const [value] = args; - if (isNullOrUndefined(value) || value === '') { - return; - } - // cycle through chosen validator functions - for (const [key, val] of validators) { - const validatorFunc = - typeof val === 'function' - ? val - : defaultFieldValidationFunctions[key]; - const result = await validatorFunc(...args); - if (result) { - return typeof result === 'string' - ? result - : typeof val === 'string' - ? val - : capitaliseFirstLetter(key); - } - } - // call provided validate method last - if (typeof validate === 'function') { - return validate(...args); - } - }, - [ - defaultFieldValidationFunctions, - requiredString, - requiredFunc, - validators, - validate, - ] - ); - - const validatorNeeded = Boolean( - requiredFunc || validators.length || typeof validate === 'function' - ); - return { - isRequired: Boolean(requiredFunc), - handleValidate: validatorNeeded ? finalValidator : undefined, - }; -}; - -export default useEasyFieldValidator; diff --git a/src/components/EasyField/useEasyFieldValidator/handleValidatorOutcome.ts b/src/components/EasyField/useEasyFieldValidator/handleValidatorOutcome.ts new file mode 100644 index 0000000..682fa67 --- /dev/null +++ b/src/components/EasyField/useEasyFieldValidator/handleValidatorOutcome.ts @@ -0,0 +1,17 @@ +import { capitaliseFirstLetter } from '../../../utils'; + +const handleValidatorOutcome = ( + result?: any, + key?: string, + val?: any +): string | undefined => { + if (result) { + return typeof result === 'string' + ? result + : typeof val === 'string' + ? val + : capitaliseFirstLetter(key); + } +}; + +export default handleValidatorOutcome; diff --git a/src/components/EasyField/useEasyFieldValidator/handleValidatorsAndValidateArg.ts b/src/components/EasyField/useEasyFieldValidator/handleValidatorsAndValidateArg.ts new file mode 100644 index 0000000..d152b6d --- /dev/null +++ b/src/components/EasyField/useEasyFieldValidator/handleValidatorsAndValidateArg.ts @@ -0,0 +1,105 @@ +import type { FieldValidator } from 'final-form'; +import { useEasyFormContext } from '../../EasyForm'; +import type { EasyFieldValidationRule } from '../EasyField.types'; +import { isNullOrUndefined, isThenable } from '../../../utils'; +import handleValidatorOutcome from './handleValidatorOutcome'; + +const handleValidateArg = ( + validate: FieldValidator | undefined, + args: Parameters> +) => { + if (typeof validate === 'function') { + return validate(...args); + } +}; + +const handleValidators = ( + validators: [string, EasyFieldValidationRule][], + defaultFieldValidationFunctions: ReturnType< + typeof useEasyFormContext + >['defaultFieldValidationFunctions'], + args: Parameters> +): string | Promise | undefined => { + // if the field has no value, do not run any other validators; + // but we will allow field values that are reference value types + // (and even booleans) to trigger further rules + // as these may indicate more complex field components + const [value] = args; + if (isNullOrUndefined(value) || value === '') { + return; + } + + // set up promise storage for Promise variant handling later... + const promiseResults: Promise[] = []; + + // cycle through chosen validator functions + for (const [key, val] of validators) { + const validatorFunc = + typeof val === 'function' ? val : defaultFieldValidationFunctions[key]; + const result = validatorFunc(...args); + + // add to Promise storage, + // and then only return a Promise if we need to + if (isThenable(result)) { + promiseResults.push( + result.then((outcome: any) => { + if (outcome) { + return handleValidatorOutcome(outcome, key, val); + } + }) + ); + } + + // if the function is not async, and returned a value, + // return immediately and do not execute any other validation funcs; + // this allows us to keep this as a sync function + else if (result) { + return handleValidatorOutcome(result, key, val); + } + } + + // now handle all the Promise validators... + // we will build a Promise chain + const promisesLength = promiseResults.length; + if (promisesLength) { + let promiseResult: Promise = Promise.resolve(); + for (let i = 0; i < promisesLength; i += 1) { + const promiseValidator = promiseResults[i]; + promiseResult = promiseResult.then((outcome) => { + if (outcome) return outcome; + return promiseValidator; + }); + } + return promiseResult; + } +}; + +const handleValidatorsAndValidateArg = ( + validators: [string, EasyFieldValidationRule][], + validate: FieldValidator | undefined, + defaultFieldValidationFunctions: ReturnType< + typeof useEasyFormContext + >['defaultFieldValidationFunctions'], + args: Parameters> +): any | Promise => { + const handleValidatorsResult = handleValidators( + validators, + defaultFieldValidationFunctions, + args + ); + if (isThenable(handleValidatorsResult)) { + return (handleValidatorsResult as Promise).then((outcome) => { + if (outcome) return outcome; + return handleValidateArg(validate, args); + }); + } + + if (handleValidatorsResult) { + return handleValidatorOutcome(handleValidatorsResult); + } + + // call provided validate method last + return handleValidateArg(validate, args); +}; + +export default handleValidatorsAndValidateArg; diff --git a/src/components/EasyField/useEasyFieldValidator/index.ts b/src/components/EasyField/useEasyFieldValidator/index.ts new file mode 100644 index 0000000..b28eff4 --- /dev/null +++ b/src/components/EasyField/useEasyFieldValidator/index.ts @@ -0,0 +1 @@ +export { default } from './useEasyFieldValidator'; diff --git a/src/components/EasyField/useEasyFieldValidator/useEasyFieldValidator.ts b/src/components/EasyField/useEasyFieldValidator/useEasyFieldValidator.ts new file mode 100644 index 0000000..979d5ae --- /dev/null +++ b/src/components/EasyField/useEasyFieldValidator/useEasyFieldValidator.ts @@ -0,0 +1,73 @@ +import { useCallback } from 'react'; +import type { FieldValidator } from 'final-form'; +import type { EasyFieldValidationRule } from '../EasyField.types'; +import { type Dictionary, isThenable } from '../../../utils'; +import handleValidatorsAndValidateArg from './handleValidatorsAndValidateArg'; +import useEasyFieldValidatorSetup from './useEasyFieldValidatorSetup'; +import handleValidatorOutcome from './handleValidatorOutcome'; + +const useEasyFieldValidator = ( + requiredArg?: EasyFieldValidationRule, + validation?: Dictionary, + validate?: FieldValidator +) => { + const { + defaultFieldValidationFunctions, + requiredString, + requiredFunc, + validators, + } = useEasyFieldValidatorSetup(requiredArg, validation); + + const finalValidator: FieldValidator = useCallback( + (...args) => { + // check required rule before anything else + if (typeof requiredFunc === 'function') { + const requiredFuncResult = requiredFunc(...args); + // if the `requiredFunc` is thenable, + // then we can just use Promise.resolve() on the rest, + // as we will be returning a Promise anyway + if (isThenable(requiredFuncResult)) { + return (requiredFuncResult as Promise).then((outcome) => { + if (outcome) { + return handleValidatorOutcome(outcome, requiredString); + } + return handleValidatorsAndValidateArg( + validators, + validate, + defaultFieldValidationFunctions, + args + ); + }); + } + // if the requiredFuncResult is truthy otherwise, return as the required error message + if (requiredFuncResult) { + return handleValidatorOutcome(requiredFuncResult, requiredString); + } + } + + return handleValidatorsAndValidateArg( + validators, + validate, + defaultFieldValidationFunctions, + args + ); + }, + [ + defaultFieldValidationFunctions, + requiredString, + requiredFunc, + validators, + validate, + ] + ); + + const validatorNeeded = Boolean( + requiredFunc || validators.length || typeof validate === 'function' + ); + return { + isRequired: Boolean(requiredFunc), + handleValidate: validatorNeeded ? finalValidator : undefined, + }; +}; + +export default useEasyFieldValidator; diff --git a/src/components/EasyField/useEasyFieldValidator/useEasyFieldValidatorSetup.ts b/src/components/EasyField/useEasyFieldValidator/useEasyFieldValidatorSetup.ts new file mode 100644 index 0000000..e1351f0 --- /dev/null +++ b/src/components/EasyField/useEasyFieldValidator/useEasyFieldValidatorSetup.ts @@ -0,0 +1,47 @@ +import { useDeepCompareMemo } from '@react-hookz/web'; +import { useEasyFormContext } from '../../EasyForm/EasyFormContext'; +import type { EasyFieldValidationRule } from '../EasyField.types'; +import type { Dictionary } from '../../../utils'; + +const useEasyFieldValidatorSetup = ( + requiredArg?: EasyFieldValidationRule, + validation?: Dictionary +) => { + const { defaultFieldValidationFunctions } = useEasyFormContext(); + const { required, ...otherValidation } = validation || {}; + const validators = useDeepCompareMemo( + () => + Object.entries(validation || {}).filter( + ([key, value]) => + typeof value === 'function' || + (Boolean(value) && + typeof defaultFieldValidationFunctions[key] === 'function') + ), + [otherValidation, defaultFieldValidationFunctions] + ); + + const requiredFunc = + typeof requiredArg === 'function' + ? requiredArg + : typeof required === 'function' + ? required + : required || requiredArg + ? defaultFieldValidationFunctions.required + : null; + + const requiredString = + typeof requiredArg === 'string' + ? requiredArg + : typeof required === 'string' + ? required + : 'Required'; + + return { + defaultFieldValidationFunctions, + requiredString, + requiredFunc, + validators, + }; +}; + +export default useEasyFieldValidatorSetup; diff --git a/src/components/EasyForm/EasyFormBuilder.tsx b/src/components/EasyForm/EasyFormBuilder.tsx index 8571e27..76f14cb 100644 --- a/src/components/EasyForm/EasyFormBuilder.tsx +++ b/src/components/EasyForm/EasyFormBuilder.tsx @@ -7,7 +7,7 @@ import Skeleton, { SKELETON_TYPE } from '../Skeleton'; import { useEasyFormContext } from './EasyFormContext'; import Notice, { NOTICE_TYPE } from '../Notice'; import CONTROL_TYPE from '../../controlTypes'; -import EasyField from '../EasyField'; +import EasyField from '../EasyField/EasyField'; /** * @note `` and `` diff --git a/src/components/EasyForm/EasyFormValidationSummary.tsx b/src/components/EasyForm/EasyFormValidationSummary.tsx index 2922e6d..6f1b2c7 100644 --- a/src/components/EasyForm/EasyFormValidationSummary.tsx +++ b/src/components/EasyForm/EasyFormValidationSummary.tsx @@ -52,6 +52,9 @@ const EasyFormValidationSummary = forwardRef< >(({ staticErrors, validationSummary }, ref) => { const formApi = useForm('EasyFormValidationSummary'); const { + // destructure `position` out so that it does not + // end up as an attribute on the rendered div + position, content: contentType, renderLogic, render, @@ -62,8 +65,9 @@ const EasyFormValidationSummary = forwardRef< const subscription = useDeepCompareMemo( () => ({ ...extractKeysForSubscription(renderLogic, (key: string) => key), - // always subscribe to the submitError + // always subscribe to the submitError, and validating states submitError: true, + validating: true, }), [renderLogic] ); @@ -92,11 +96,18 @@ const EasyFormValidationSummary = forwardRef< return render(ref) || null; } + const { validating } = formState; + // ignore other validationSummary behaviour when a whole form submit error exists, // as we always want to prioritise this, just in case if (formState.submitError) { return ( - + ); } @@ -110,6 +121,7 @@ const EasyFormValidationSummary = forwardRef< const finalErrors = determineErrors(contentType, staticErrors, formApi); return ( + Decorator, ] => { const isMounted = useIsMounted(); @@ -75,8 +75,8 @@ const useEasyFormDecorator = ({ setErrors(undefined); // handle async or sync submit variant const result = originalSubmit.call(form); - if (result && typeof result.then === 'function') { - result.then(afterSubmit, () => {}); + if (isThenable(result)) { + (result as Promise).then(afterSubmit, () => {}); } else { afterSubmit(); } diff --git a/src/components/ErrorMessage/ErrorMessage.test.tsx b/src/components/ErrorMessage/ErrorMessage.test.tsx index f3baadd..b8450c5 100644 --- a/src/components/ErrorMessage/ErrorMessage.test.tsx +++ b/src/components/ErrorMessage/ErrorMessage.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import ErrorMessage from './ErrorMessage'; import type { ErrorMessageProps } from './ErrorMessage.types'; import { NoticeIcon, NOTICE_TYPE } from '../Notice'; @@ -50,16 +50,28 @@ describe('', () => { ); }); - it('renders a loading type NoticeIcon if `loading` is true', () => { + it('does not immediately render a loading type NoticeIcon if `loading` is true', () => { props.text = 'text'; props.loading = true; renderComponent(); expect(NoticeIcon).toHaveBeenCalledWith( - expect.objectContaining({ type: NOTICE_TYPE.LOADING }), + expect.objectContaining({ type: NOTICE_TYPE.ERROR }), expect.any(Object) // context ); }); + it('renders a loading type NoticeIcon if `loading` is true after a tiny delay', async () => { + props.text = 'text'; + props.loading = true; + renderComponent(); + await waitFor(() => { + expect(NoticeIcon).toHaveBeenCalledWith( + expect.objectContaining({ type: NOTICE_TYPE.ERROR, loading: true }), + expect.any(Object) // context + ); + }); + }); + it('passes other props onto the element', () => { props.text = 'text'; props['data-something'] = 'something'; diff --git a/src/components/ErrorMessage/ErrorMessage.tsx b/src/components/ErrorMessage/ErrorMessage.tsx index e450232..aa45de1 100644 --- a/src/components/ErrorMessage/ErrorMessage.tsx +++ b/src/components/ErrorMessage/ErrorMessage.tsx @@ -1,27 +1,34 @@ import React, { forwardRef, memo } from 'react'; import clsx from 'clsx'; import { ErrorMessageProps } from './ErrorMessage.types'; -import { useFieldClassName } from '../../utils'; +import { useFieldClassName, useDebouncedValue } from '../../utils'; import { NoticeIcon, NOTICE_TYPE } from '../Notice'; import './ErrorMessage.less'; const ErrorMessage = forwardRef( - ({ children, text, className, loading, ...other }, ref) => { + ({ children, text, className, loading: loadingProp, ...other }, ref) => { const classPrefix = useFieldClassName('error-message'); + // we will debounce the loading state to + // minimise DOM updates between rapid successive changes + // e.g. if the `loading` prop is set based on an instantly fulfilled Promise + const loading = useDebouncedValue(loadingProp); + if (!children && !text) { return null; } + return (
{children || text}
diff --git a/src/components/FieldRepeater/FieldRepeater.stories.tsx b/src/components/FieldRepeater/FieldRepeater.stories.tsx index 2f3be6f..809463e 100644 --- a/src/components/FieldRepeater/FieldRepeater.stories.tsx +++ b/src/components/FieldRepeater/FieldRepeater.stories.tsx @@ -24,12 +24,14 @@ export const StandardUsage = (props: FieldRepeaterProps) => ( label="Name" name={`${args.name}.name`} component={Input} + disabled={args.disabled} />

FieldRepeater children function received arg:{' '} @@ -54,9 +56,14 @@ StandardUsage.args = { /** * ![Peer dependency: react-beautiful-dnd](https://img.shields.io/badge/Peer_dependency-react--beautiful--dnd-blue) * - * The `FieldRepeater` **must** be used within a `

` or ``, + * The `FieldRepeater` is a complex field container for repeatable groupings + * of fields. A good example usage would be a list of contacts. + * + * Please note: the `FieldRepeater` **must** be used within a `` or ``, * and its `children` **must** be a function. The function will receive - * an object argument containing `name`, `length`, and `index` properties. + * an object argument containing `name`, `length`, `index`, and `disabled` properties. + * The `disabled` property reflects the `disabled` prop on the `FieldRepeater` itself. + * And if used within an `EasyForm` that is disabled, it will reflect that too. * * The `name` provided will be based on the `name` prop used for the `FieldRepeater`. * @@ -65,10 +72,11 @@ StandardUsage.args = { * * ``` * - * {({ name }) => ( + * {({ name, disabled }) => ( * * )} @@ -78,6 +86,12 @@ StandardUsage.args = { export default { title: 'Components/FieldRepeater', component: StandardUsage, + argTypes: { + disabled: { + description: + 'Disables the add, delete, and re-ordering controls. Fields rendered within the control will need to be disabled separately', + }, + }, }; export const EasyFormStructureExample = (props: EasyFormProps) => ( diff --git a/src/components/FieldRepeater/FieldRepeater.tsx b/src/components/FieldRepeater/FieldRepeater.tsx index 7a79e27..ce6a384 100644 --- a/src/components/FieldRepeater/FieldRepeater.tsx +++ b/src/components/FieldRepeater/FieldRepeater.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { isValidElementType } from 'react-is'; import Fieldset from '../Fieldset'; -import EasyField from '../EasyField'; +import EasyField from '../EasyField/EasyField'; import FieldRepeaterContent from './FieldRepeaterContent'; import FieldRepeaterContext from './FieldRepeaterContext'; import type { FieldRepeaterProps } from './FieldRepeater.types'; diff --git a/src/components/FieldRepeater/FieldRepeater.types.ts b/src/components/FieldRepeater/FieldRepeater.types.ts index c29e8e4..0213b1a 100644 --- a/src/components/FieldRepeater/FieldRepeater.types.ts +++ b/src/components/FieldRepeater/FieldRepeater.types.ts @@ -8,7 +8,7 @@ import type { FieldRepeaterContentProps } from './FieldRepeaterContent.types'; export interface FieldRepeaterProps extends FieldRepeaterContextPropsBase, - FieldRepeaterContentProps, + Omit, UseFieldConfig { strings?: FieldRepeaterContextStrings; name: string; diff --git a/src/components/FieldRepeater/FieldRepeaterItemRenderer.tsx b/src/components/FieldRepeater/FieldRepeaterItemRenderer.tsx index 9392a29..67de4a3 100644 --- a/src/components/FieldRepeater/FieldRepeaterItemRenderer.tsx +++ b/src/components/FieldRepeater/FieldRepeaterItemRenderer.tsx @@ -51,6 +51,7 @@ const FieldRepeaterItemRenderer = ({ tabIndex: isLastItem ? -1 : undefined, children: children({ length: fields.length || 0, + disabled, index, name, }), diff --git a/src/components/FieldRepeater/FieldRepeaterItemRenderer.types.ts b/src/components/FieldRepeater/FieldRepeaterItemRenderer.types.ts index 5a2c780..59fe7dc 100644 --- a/src/components/FieldRepeater/FieldRepeaterItemRenderer.types.ts +++ b/src/components/FieldRepeater/FieldRepeaterItemRenderer.types.ts @@ -2,6 +2,7 @@ import type { ReactNode } from 'react'; import type { FieldArrayInput } from '../../utils/useFieldArray/useFieldArray.types'; type FieldRepeaterItemRendererChildrenArg = { + disabled: boolean; length: number; index: number; name: string; diff --git a/src/components/Fieldset/Fieldset.stories.tsx b/src/components/Fieldset/Fieldset.stories.tsx index 86368f3..a4518fd 100644 --- a/src/components/Fieldset/Fieldset.stories.tsx +++ b/src/components/Fieldset/Fieldset.stories.tsx @@ -1,21 +1,42 @@ import React from 'react'; import Fieldset from './Fieldset'; import { FieldsetProps } from './Fieldset.types'; +import Input from '../Input'; +import EasyField from '../EasyField'; +import EasyForm from '../EasyForm'; + +const children = ( + <> + + + + +); export default { title: 'Components/Fieldset', component: Fieldset, }; -export const StandardUsage = (props: FieldsetProps) =>
; +export const StandardUsage = (props: FieldsetProps) => ( + +
+ +); + StandardUsage.args = { - legend: 'Legend text', - children: 'Children go here', + legend: 'Contact details', + children, }; export const VisuallyHiddenLegend = StandardUsage.bind({}); VisuallyHiddenLegend.args = { visuallyHiddenLegend: true, - children: 'Children go here', - label: 'Legend text', + label: 'Contact details', + children, }; diff --git a/src/components/Notice/Notice.tsx b/src/components/Notice/Notice.tsx index 7ef9966..61b84ae 100644 --- a/src/components/Notice/Notice.tsx +++ b/src/components/Notice/Notice.tsx @@ -1,7 +1,11 @@ import clsx from 'clsx'; import React, { memo, useMemo } from 'react'; import { type NoticeProps, NOTICE_TYPE } from './Notice.types'; -import { polymorphicForwardRef, useFieldClassName } from '../../utils'; +import { + polymorphicForwardRef, + useDebouncedValue, + useFieldClassName, +} from '../../utils'; import NoticeIcon from './NoticeIcon'; import './Notice.less'; @@ -12,6 +16,7 @@ const Notice = polymorphicForwardRef<'div', NoticeProps>( ( { as: Component = 'div', + loading: loadingProp, type: typeProp, className, children, @@ -33,6 +38,11 @@ const Notice = polymorphicForwardRef<'div', NoticeProps>( return DEFAULT_NOTICE_TYPE; }, [typeProp, variant]); + // we will debounce the loading state to + // minimise DOM updates between rapid successive changes + // e.g. if the `loading` prop is set based on an instantly fulfilled Promise + const loading = useDebouncedValue(loadingProp); + if (!text && !children) { return null; } @@ -43,6 +53,9 @@ const Notice = polymorphicForwardRef<'div', NoticeProps>( {...other} ref={ref} className={clsx(className, classPrefix, `${classPrefix}--${type}`)} + // aria-busy is only really relevant if this is a live region, + // but we will set it anyway just in case + aria-busy={loading} > ( diff --git a/src/components/Notice/Notice.types.ts b/src/components/Notice/Notice.types.ts index 5a74a77..e5b0221 100644 --- a/src/components/Notice/Notice.types.ts +++ b/src/components/Notice/Notice.types.ts @@ -1,7 +1,6 @@ import type { ComponentPropsWithoutRef, SVGAttributes, ReactNode } from 'react'; export enum NOTICE_TYPE { - LOADING = 'loading', SUCCESS = 'success', WARNING = 'warning', ERROR = 'error', @@ -9,6 +8,7 @@ export enum NOTICE_TYPE { } export interface NoticeIconProps extends SVGAttributes { + loading?: boolean; type?: NOTICE_TYPE; } @@ -18,5 +18,6 @@ export type NoticeProps = { * Alias for `type` */ variant?: NOTICE_TYPE; + loading?: boolean; text?: ReactNode; } & ComponentPropsWithoutRef<'div'>; diff --git a/src/components/Notice/NoticeIcon.tsx b/src/components/Notice/NoticeIcon.tsx index 27792f3..b4149e3 100644 --- a/src/components/Notice/NoticeIcon.tsx +++ b/src/components/Notice/NoticeIcon.tsx @@ -2,15 +2,15 @@ import React, { memo } from 'react'; import { NoticeIconProps, NOTICE_TYPE } from './Notice.types'; import { ExclamationIcon, TickIcon, InfoIcon, LoadingIcon } from '../../icons'; -const NoticeIcon = ({ type, ...other }: NoticeIconProps) => { +const NoticeIcon = ({ type, loading, ...other }: NoticeIconProps) => { + if (loading) { + return ; + } switch (type) { case NOTICE_TYPE.ERROR: case NOTICE_TYPE.WARNING: return ; - case NOTICE_TYPE.LOADING: - return ; - case NOTICE_TYPE.SUCCESS: return ; diff --git a/src/components/RadioList/RadioList.test.tsx b/src/components/RadioList/RadioList.test.tsx index a1bc5a0..7a43679 100644 --- a/src/components/RadioList/RadioList.test.tsx +++ b/src/components/RadioList/RadioList.test.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import RadioList from './RadioList'; import SwitchList, { SWITCH_LIST_TYPE } from '../SwitchList'; -import { RadioListProps } from './RadioList.types'; import { fieldClassName } from '../../utils'; +import type { SwitchListProps } from '../SwitchList/SwitchList.types'; jest.mock('../SwitchList', () => ({ __esModule: true, @@ -12,7 +12,7 @@ jest.mock('../SwitchList', () => ({ })); describe('', () => { - let props: RadioListProps; + let props: Omit; beforeEach(() => { props = { diff --git a/src/components/RadioList/RadioList.tsx b/src/components/RadioList/RadioList.tsx index 502b120..6609f1c 100644 --- a/src/components/RadioList/RadioList.tsx +++ b/src/components/RadioList/RadioList.tsx @@ -3,21 +3,22 @@ import clsx from 'clsx'; import { isEqual } from '@react-hookz/deep-equal'; import { useFieldClassName } from '../../utils'; import SwitchList, { SWITCH_LIST_TYPE } from '../SwitchList'; -import { RadioListProps } from './RadioList.types'; +import type { SwitchListProps } from '../SwitchList/SwitchList.types'; -const RadioList = forwardRef( - ({ className, ...other }, ref) => { - const classPrefix = useFieldClassName('radio-list'); - return ( - - ); - } -); +const RadioList = forwardRef< + HTMLFieldSetElement, + Omit +>(({ className, ...other }, ref) => { + const classPrefix = useFieldClassName('radio-list'); + return ( + + ); +}); // do a deep equal comparison in this case, // to account for lazy use of the `options` prop diff --git a/src/components/RadioList/RadioList.types.ts b/src/components/RadioList/RadioList.types.ts deleted file mode 100644 index b8626c3..0000000 --- a/src/components/RadioList/RadioList.types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { BaseSwitchListProps } from '../SwitchList/SwitchList.types'; - -export interface RadioListProps extends BaseSwitchListProps {} diff --git a/src/components/SwitchList/SwitchList.test.tsx b/src/components/SwitchList/SwitchList.test.tsx index 2404efb..db7c250 100644 --- a/src/components/SwitchList/SwitchList.test.tsx +++ b/src/components/SwitchList/SwitchList.test.tsx @@ -1,12 +1,21 @@ import React from 'react'; import { render } from '@testing-library/react'; import SwitchList from './SwitchList'; -import { SwitchListProps } from './SwitchList.types'; +import { SWITCH_LIST_TYPE, type SwitchListProps } from './SwitchList.types'; +import useSwitchListOptions from './useSwitchListOptions'; +import Fieldset from '../Fieldset'; +import { fieldClassName } from '../../utils'; + +jest.mock('./useSwitchListOptions', () => jest.fn(() =>
result
)); +jest.mock('../Fieldset', () => jest.fn(({ children }) => children)); + +const classPrefix = fieldClassName('switch-list'); describe('', () => { let props: SwitchListProps; beforeEach(() => { + jest.clearAllMocks(); props = { name: 'SwitchList', }; @@ -14,10 +23,54 @@ describe('', () => { const renderComponent = () => render(); - it.skip('should render', () => { - props.children = 'leon was here'; - const { getByTestId } = renderComponent(); - const component = getByTestId('SwitchList'); - expect(component).toHaveTextContent('leon was here'); + it('passes `options`, `type`, `name`, and `disabled` to the `useSwitchListOptions` hook', () => { + props.options = [{ label: 'test label', value: 'test-value' }, 'test']; + props.type = SWITCH_LIST_TYPE.RADIO; + props.name = 'some-name'; + props.disabled = false; + + renderComponent(); + expect(useSwitchListOptions).toHaveBeenCalledWith( + expect.objectContaining(props) + ); + }); + + it('renders using the Fieldset component as the container, with a visuallyHiddenLegend by default', () => { + renderComponent(); + expect(Fieldset).toHaveBeenCalledWith( + expect.objectContaining({ + 'data-testid': 'SwitchList', + visuallyHiddenLegend: true, + }), + expect.any(Object) + ); + }); + + it('will pass other props onto the Fieldset container', () => { + props['data-something'] = 'something'; + props.className = 'test-class'; + props.hidden = true; + renderComponent(); + expect(Fieldset).toHaveBeenCalledWith( + expect.objectContaining({ + className: expect.stringContaining(props.className), + 'data-something': props['data-something'], + hidden: props.hidden, + }), + expect.any(Object) // context + ); + }); + + it('renders an unordered list containing the options', () => { + const { container } = renderComponent(); + const ul = container.querySelector('ul'); + expect(ul).toBeInTheDocument(); + expect(ul).toHaveClass(`${classPrefix}__list`); + }); + + it('renders nothing if `useSwitchListOptions` returns nothing', () => { + (useSwitchListOptions as any).mockReturnValue(null); + const { container } = renderComponent(); + expect(container.querySelector('ul')).not.toBeInTheDocument(); }); }); diff --git a/src/components/SwitchList/SwitchList.types.ts b/src/components/SwitchList/SwitchList.types.ts index 337a5a6..a8287e4 100644 --- a/src/components/SwitchList/SwitchList.types.ts +++ b/src/components/SwitchList/SwitchList.types.ts @@ -16,15 +16,12 @@ export type DetailedSwitchListOption = { export type SwitchListOptions = (string | DetailedSwitchListOption)[]; -export interface BaseSwitchListProps extends FieldsetProps { +export interface SwitchListProps extends FieldsetProps { options?: SwitchListOptions; className?: string; disabled?: boolean; - name: string; -} - -export interface SwitchListProps extends BaseSwitchListProps { type?: SWITCH_LIST_TYPE; + name: string; } export type useSwitchListOptionsArg = { diff --git a/src/components/ValidationSummary/ValidationSummary.test.tsx b/src/components/ValidationSummary/ValidationSummary.test.tsx index cd00b29..e61bd5a 100644 --- a/src/components/ValidationSummary/ValidationSummary.test.tsx +++ b/src/components/ValidationSummary/ValidationSummary.test.tsx @@ -20,7 +20,7 @@ describe('', () => { expect(component).toBeNull(); }); - it('renders an error div with a default className and a tabIndex of -1', () => { + it('renders an error div with a default className', () => { props.error = 'error'; const { getByTestId } = renderComponent(); const component = getByTestId('ValidationSummary'); @@ -29,7 +29,6 @@ describe('', () => { fieldClassName(`notice--${NOTICE_TYPE.ERROR}`) ); expect(component).toHaveClass(fieldClassName('validation-summary')); - expect(component).toHaveAttribute('tabindex', '-1'); expect(component.nodeName).toStrictEqual('DIV'); }); diff --git a/src/components/ValidationSummary/ValidationSummary.tsx b/src/components/ValidationSummary/ValidationSummary.tsx index cfdbf59..6ce845a 100644 --- a/src/components/ValidationSummary/ValidationSummary.tsx +++ b/src/components/ValidationSummary/ValidationSummary.tsx @@ -51,7 +51,6 @@ const ValidationSummary = forwardRef( {...other} className={clsx(className, classPrefix)} type={NOTICE_TYPE.ERROR} - tabIndex={-1} ref={ref} as="div" > diff --git a/src/components/ValidationSummary/ValidationSummary.types.ts b/src/components/ValidationSummary/ValidationSummary.types.ts index aaa5a0b..ef7f521 100644 --- a/src/components/ValidationSummary/ValidationSummary.types.ts +++ b/src/components/ValidationSummary/ValidationSummary.types.ts @@ -1,5 +1,6 @@ import { ComponentPropsWithRef, ReactNode } from 'react'; import { Dictionary } from '../../utils'; +import { NoticeProps } from '../Notice/Notice.types'; export type ValidationSummaryError = | ReactNode @@ -10,7 +11,9 @@ export type ValidationSummaryErrors = | ValidationSummaryError[] | Dictionary; -export interface ValidationSummaryProps extends ComponentPropsWithRef<'div'> { +export interface ValidationSummaryProps + extends Omit, + ComponentPropsWithRef<'div'> { header?: ReactNode; footer?: ReactNode; error?: ValidationSummaryError; diff --git a/src/index.ts b/src/index.ts index 28d2c16..29a7e64 100755 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export { focusElement, isElementInViewport, isNullOrUndefined, + isThenable, mergeRefs, polymorphicForwardRef, smoothlyScrollIntoView, diff --git a/src/utils/index.ts b/src/utils/index.ts index b154be0..0353dba 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,6 +6,7 @@ export { default as focusAndSmoothlyScrollIntoView } from './focusAndSmoothlyScr export { default as focusElement } from './focusElement'; export { default as isElementInViewport } from './isElementInViewport'; export { default as isNullOrUndefined } from './isNullOrUndefined'; +export { default as isThenable } from './isThenable'; export { default as mergeRefs } from './mergeRefs'; export { default as polymorphicForwardRef } from './polymorphicForwardRef'; export { default as reactKeyFrom } from './reactKeyFrom'; @@ -14,12 +15,13 @@ export { default as smoothlyScrollIntoView } from './smoothlyScrollIntoView'; export { default as useAnnounce } from './useAnnounce'; export { default as useAutoId } from './useAutoId'; export { default as useCheckFieldValue } from './useCheckFieldValue'; +export { default as useDebouncedValue } from './useDebouncedValue'; export { default as useFieldArray } from './useFieldArray'; export { default as useFieldClassName } from './useFieldClassName'; +export { default as useFieldData, useSetFieldData } from './useFieldData'; export { default as useFieldValue } from './useFieldValue'; export { default as useMutatedField } from './useMutatedField'; export { default as useMutatedFormState } from './useMutatedFormState'; -export { default as useFieldData, useSetFieldData } from './useFieldData'; export * from './validation'; export * from './constants'; export * from './types'; diff --git a/src/utils/isThenable/index.ts b/src/utils/isThenable/index.ts new file mode 100644 index 0000000..9567fc7 --- /dev/null +++ b/src/utils/isThenable/index.ts @@ -0,0 +1 @@ +export { default } from './isThenable'; diff --git a/src/utils/isThenable/isThenable.test.ts b/src/utils/isThenable/isThenable.test.ts new file mode 100644 index 0000000..a8d0a93 --- /dev/null +++ b/src/utils/isThenable/isThenable.test.ts @@ -0,0 +1,32 @@ +import isThenable from './isThenable'; + +describe('isThenable', () => { + const fnWithThen = () => {}; + fnWithThen.then = () => {}; + + it.each([ + [null, false], + [undefined, false], + [0, false], + [-42, false], + [42, false], + ['', false], + ['then', false], + [false, false], + [true, false], + [{}, false], + [{ then: true }, false], + [[], false], + [[true], false], + [() => {}, false], + [{ then: function () {} }, true], + [fnWithThen, true], + [new Promise((resolve) => resolve('test')), true], + [Promise.resolve('test'), true], + ])( + 'returns a boolean indicating if value %s is thenable', + (val, expected) => { + expect(isThenable(val)).toBe(expected); + } + ); +}); diff --git a/src/utils/isThenable/isThenable.ts b/src/utils/isThenable/isThenable.ts new file mode 100644 index 0000000..524db1a --- /dev/null +++ b/src/utils/isThenable/isThenable.ts @@ -0,0 +1,9 @@ +// same as the 'is-promise' library, +// but no point adding badly named 3kb lib +// when can be done in couple hundred bytes +const isThenable = (obj: any): boolean => + Boolean(obj) && + (typeof obj === 'object' || typeof obj === 'function') && + typeof obj.then === 'function'; + +export default isThenable; diff --git a/src/utils/useCheckFieldValue/useCheckFieldValue.ts b/src/utils/useCheckFieldValue/useCheckFieldValue.ts index 948258a..0198724 100644 --- a/src/utils/useCheckFieldValue/useCheckFieldValue.ts +++ b/src/utils/useCheckFieldValue/useCheckFieldValue.ts @@ -93,7 +93,7 @@ const useCheckFieldValue = ( logic = USE_CHECK_FIELD_VALUE_LOGIC_TYPE.AND, } = config; - const formApi = useForm(); + const formApi = useForm('useCheckFieldValue'); const ifNotPasses = useCheckFieldValueEvaluator( config.ifNot || {}, ifNotLogic, diff --git a/src/utils/useDebouncedValue/index.ts b/src/utils/useDebouncedValue/index.ts new file mode 100644 index 0000000..9adcf05 --- /dev/null +++ b/src/utils/useDebouncedValue/index.ts @@ -0,0 +1 @@ +export { default } from './useDebouncedValue'; diff --git a/src/utils/useDebouncedValue/useDebouncedValue.ts b/src/utils/useDebouncedValue/useDebouncedValue.ts new file mode 100644 index 0000000..94aebfc --- /dev/null +++ b/src/utils/useDebouncedValue/useDebouncedValue.ts @@ -0,0 +1,10 @@ +import { useEffect } from 'react'; +import { useDebouncedState } from '@react-hookz/web'; + +const useDebouncedValue = (value?: any, delay = 0) => { + const [state, setState] = useDebouncedState(value, delay); + useEffect(() => setState(value), [setState, value]); + return state; +}; + +export default useDebouncedValue; diff --git a/src/utils/useMutatedField/useMutatedField.ts b/src/utils/useMutatedField/useMutatedField.ts index 5424fcf..2d2537b 100644 --- a/src/utils/useMutatedField/useMutatedField.ts +++ b/src/utils/useMutatedField/useMutatedField.ts @@ -1,12 +1,17 @@ import { useRef, useMemo } from 'react'; -import { UseFieldConfig, useField, useFormState } from 'react-final-form'; +import { type UseFieldConfig, useFormState, useField } from 'react-final-form'; const useMutatedField = (name: string, config?: UseFieldConfig) => { const { input, meta } = useField(name, config); const { error, validating, invalid, valid } = meta; + + // for optimisation, we should ideally only subscribe to the form's + // validating state when the field has asynchronous validation, + // but we cannot know if `validate` returns a Promise without calling it, + // so as a middle ground we will just subscribe if the function exists const { validating: formValidating } = useFormState({ subscription: { - validating: true, + validating: typeof config?.validate === 'function', }, }); @@ -16,13 +21,13 @@ const useMutatedField = (name: string, config?: UseFieldConfig) => { const mutatedMeta = useMemo(() => ({ ...meta }), [meta]); // the Field validating state resets even when async validation is happening, - // so we will store validating state based on the whole form still validating, - // and reset the ref when the whole form has finished validating too + // so we will store validating state based on the whole form still validating + // and reset the ref when the whole form has finished validating const validatingRef = useRef(false); - if (validating && formValidating) { + if (validating || formValidating) { validatingRef.current = true; } - if (validatingRef.current && !formValidating) { + if (!validating && !formValidating) { validatingRef.current = false; } // update mutatedMeta @@ -33,7 +38,7 @@ const useMutatedField = (name: string, config?: UseFieldConfig) => { // using a ref here and relying on the Field and Form States for updates // instead of useState, which would cause extra re-renders const errorRef = useRef(error); - if ((!validating && error) || !mutatedMeta.validating) { + if (!mutatedMeta.validating) { errorRef.current = error; } // now mutate the new object in place so that we do not cause extra re-renders diff --git a/src/utils/validation/isEmpty/isEmpty.ts b/src/utils/validation/isEmpty/isEmpty.ts index d99d5d9..8e8ff1c 100644 --- a/src/utils/validation/isEmpty/isEmpty.ts +++ b/src/utils/validation/isEmpty/isEmpty.ts @@ -1,5 +1,7 @@ +import isNullOrUndefined from '../../isNullOrUndefined'; + const isEmpty = (value?: any): boolean => { - if (typeof value === 'undefined' || value === null) { + if (isNullOrUndefined(value)) { return true; } // consider empty white-space as still empty