diff --git a/module/package-lock.json b/module/package-lock.json index 1495dbf6..99df6e7f 100644 --- a/module/package-lock.json +++ b/module/package-lock.json @@ -42,9 +42,9 @@ "@types/glob": "8.1.0", "@types/jest": "29.5.3", "@types/lodash": "4.14.195", - "@types/react": "18.3.1", + "@types/react": "^19.1.12", "@types/react-datepicker": "4.11.2", - "@types/react-dom": "18.3.1", + "@types/react-dom": "^19.1.9", "chokidar": "3.5.3", "concurrently": "8.2.0", "http-server": "14.1.1", @@ -53,8 +53,8 @@ "jest-config": "29.6.1", "jest-environment-jsdom": "29.6.1", "jest-junit": "16.0.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.1.1", + "react-dom": "^19.1.1", "sass": "1.63.6", "scss-concat": "0.3.9", "semantic-release": "21.0.7", @@ -72,8 +72,8 @@ }, "peerDependencies": { "date-fns": "2.x", - "react": "18.x", - "react-dom": "18.x", + "react": "19.x", + "react-dom": "19.x", "zod": "3.*" } }, @@ -6660,6 +6660,43 @@ "react": ">=16" } }, + "node_modules/@storybook/addon-docs/node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@storybook/addon-docs/node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/@storybook/addon-docs/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/@storybook/blocks": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.7.tgz", @@ -7036,6 +7073,16 @@ "node": ">=14" } }, + "node_modules/@testing-library/react/node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@testing-library/react/node_modules/aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -7302,17 +7349,12 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" - }, "node_modules/@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", + "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, @@ -7329,12 +7371,13 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "devOptional": true, - "dependencies": { - "@types/react": "*" + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" } }, "node_modules/@types/react-transition-group": { @@ -19569,12 +19612,11 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -19597,15 +19639,16 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "dev": true, + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.1" } }, "node_modules/react-fast-compare": { @@ -20305,12 +20348,11 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "dev": true, + "license": "MIT" }, "node_modules/scss-concat": { "version": "0.3.9", @@ -22645,22 +22687,6 @@ } } }, - "node_modules/vite-plugin-sdk/node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "dev": true, - "peer": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/vite-plugin-sdk/node_modules/rollup-plugin-rename-node-modules": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/rollup-plugin-rename-node-modules/-/rollup-plugin-rename-node-modules-1.3.1.tgz", @@ -23926,8 +23952,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", - "dev": true, - "requires": {} + "dev": true }, "@emotion/babel-plugin": { "version": "11.13.5", @@ -23999,8 +24024,7 @@ "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "requires": {} + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==" } } }, @@ -24844,8 +24868,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "requires": {} + "dev": true }, "fs-extra": { "version": "7.0.1", @@ -24963,8 +24986,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "requires": {} + "dev": true }, "fs-extra": { "version": "7.0.1", @@ -27140,8 +27162,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", - "dev": true, - "requires": {} + "dev": true }, "fs-extra": { "version": "7.0.1", @@ -27585,6 +27606,34 @@ "requires": { "@types/mdx": "^2.0.0" } + }, + "react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0" + } } } }, @@ -27603,8 +27652,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.3.0.tgz", "integrity": "sha512-Nz/UzeYQdUZUhacrPyfkiiysSjydyjgg/p0P9HxB4p/WaJUUjMAcaoaLgy3EXx61zZJ3iD36WPuDkZs5QYrA0A==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -27612,8 +27660,7 @@ "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.7.tgz", "integrity": "sha512-uyJIcoyeMWKAvjrG9tJBUCKxr2WZk+PomgrgrUwejkIfXMO76i6jw9BwLa0NZjYdlthDv30r9FfbYZyeNPmF0g==", - "dev": true, - "requires": {} + "dev": true }, "@storybook/core": { "version": "8.4.7", @@ -27672,15 +27719,13 @@ "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.7.tgz", "integrity": "sha512-ELqemTviCxAsZ5tqUz39sDmQkvhVAvAgiplYy9Uf15kO0SP2+HKsCMzlrm2ue2FfkUNyqbDayCPPCB0Cdn/mpQ==", - "dev": true, - "requires": {} + "dev": true }, "@storybook/preview-api": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.7.tgz", "integrity": "sha512-0QVQwHw+OyZGHAJEXo6Knx+6/4er7n2rTDE5RYJ9F2E2Lg42E19pfdLlq2Jhoods2Xrclo3wj6GWR//Ahi39Eg==", - "dev": true, - "requires": {} + "dev": true }, "@storybook/react": { "version": "8.4.7", @@ -27700,8 +27745,7 @@ "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.7.tgz", "integrity": "sha512-6bkG2jvKTmWrmVzCgwpTxwIugd7Lu+2btsLAqhQSzDyIj2/uhMNp8xIMr/NBDtLgq3nomt9gefNa9xxLwk/OMg==", - "dev": true, - "requires": {} + "dev": true }, "@storybook/test": { "version": "8.4.7", @@ -27756,8 +27800,7 @@ "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", - "dev": true, - "requires": {} + "dev": true }, "@testing-library/dom": { "version": "10.4.0", @@ -27831,6 +27874,12 @@ "pretty-format": "^27.0.2" } }, + "@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true + }, "aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", @@ -27846,8 +27895,7 @@ "version": "14.5.2", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", - "dev": true, - "requires": {} + "dev": true }, "@tootallnate/once": { "version": "2.0.0", @@ -28083,17 +28131,11 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, - "@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" - }, "@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "requires": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, @@ -28110,13 +28152,10 @@ } }, "@types/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "devOptional": true, - "requires": { - "@types/react": "*" - } + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "dev": true }, "@types/react-transition-group": { "version": "4.4.11", @@ -28661,8 +28700,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "8.3.4", @@ -29851,8 +29889,7 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, - "requires": {} + "dev": true }, "deep-eql": { "version": "5.0.2", @@ -30504,8 +30541,7 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.9", @@ -30681,8 +30717,7 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.11.2.tgz", "integrity": "sha512-uRLRLk7uTzc8NE6t4wBU8dijQwHvC66R/h7xwdM779jsJjMUtSmeaB8ayRkkpfwi+UU5BEfwvDANwmE+ccMVDw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-prettier": { "version": "4.2.1", @@ -30748,15 +30783,13 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-simple-import-sort": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-8.0.0.tgz", "integrity": "sha512-bXgJQ+lqhtQBCuWY/FUWdB27j4+lqcvXv5rUARkzbeWLwea+S5eBZEQrhnO+WgX3ZoJHVj0cn943iyXwByHHQw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-storybook": { "version": "0.6.8", @@ -32750,8 +32783,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "29.6.3", @@ -36647,15 +36679,13 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-scss": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dev": true, - "requires": {} + "dev": true }, "postcss-selector-parser": { "version": "6.1.2", @@ -36827,12 +36857,10 @@ } }, "react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "requires": { - "loose-envify": "^1.1.0" - } + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "dev": true }, "react-datepicker": { "version": "4.16.0", @@ -36848,12 +36876,12 @@ } }, "react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "dev": true, "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" } }, "react-fast-compare": { @@ -36864,8 +36892,7 @@ "react-icons": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", - "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", - "requires": {} + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==" }, "react-is": { "version": "17.0.2", @@ -36876,8 +36903,7 @@ "react-onclickoutside": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", - "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", - "requires": {} + "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==" }, "react-popper": { "version": "2.3.0", @@ -37347,12 +37373,10 @@ } }, "scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "requires": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "dev": true }, "scss-concat": { "version": "0.3.9", @@ -38272,15 +38296,13 @@ "version": "9.0.4", "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-9.0.4.tgz", "integrity": "sha512-38nIGTGpFOiK5LjJ8Ma1yUgpKENxoKSOhbDNSemY7Ep0VsJoXIW9Iq/2hSt699oB9tReynfWicTAoIHiq8Rvbg==", - "dev": true, - "requires": {} + "dev": true }, "stylelint-config-recommended": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-9.0.0.tgz", "integrity": "sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==", - "dev": true, - "requires": {} + "dev": true }, "stylelint-config-recommended-scss": { "version": "8.0.0", @@ -38827,8 +38849,7 @@ "use-isomorphic-layout-effect": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz", - "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==", - "requires": {} + "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==" }, "util": { "version": "0.12.5", @@ -39179,16 +39200,6 @@ "resolve": "^1.22.1" } }, - "rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", - "dev": true, - "peer": true, - "requires": { - "fsevents": "~2.3.2" - } - }, "rollup-plugin-rename-node-modules": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/rollup-plugin-rename-node-modules/-/rollup-plugin-rename-node-modules-1.3.1.tgz", @@ -39436,8 +39447,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "requires": {} + "dev": true }, "xml": { "version": "1.0.1", diff --git a/module/package.json b/module/package.json index 4cce1c29..6c1d59ef 100644 --- a/module/package.json +++ b/module/package.json @@ -19,8 +19,8 @@ }, "peerDependencies": { "date-fns": "2.x", - "react": "18.x", - "react-dom": "18.x", + "react": "19.x", + "react-dom": "19.x", "zod": "3.*" }, "files": [ @@ -74,9 +74,9 @@ "@types/glob": "8.1.0", "@types/jest": "29.5.3", "@types/lodash": "4.14.195", - "@types/react": "18.3.1", + "@types/react": "^19.1.12", "@types/react-datepicker": "4.11.2", - "@types/react-dom": "18.3.1", + "@types/react-dom": "^19.1.9", "chokidar": "3.5.3", "concurrently": "8.2.0", "http-server": "14.1.1", @@ -85,8 +85,8 @@ "jest-config": "29.6.1", "jest-environment-jsdom": "29.6.1", "jest-junit": "16.0.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.1.1", + "react-dom": "^19.1.1", "sass": "1.63.6", "scss-concat": "0.3.9", "semantic-release": "21.0.7", diff --git a/module/src/components/button/button.component.tsx b/module/src/components/button/button.component.tsx index cdb3876c..1418e716 100644 --- a/module/src/components/button/button.component.tsx +++ b/module/src/components/button/button.component.tsx @@ -7,10 +7,7 @@ import { Spinner } from '../spinner/spinner.component'; import './button.theme.css'; -type ButtonHTMLProps = Omit< - React.DetailedHTMLProps, HTMLButtonElement>, - 'ref' ->; +type ButtonHTMLProps = React.DetailedHTMLProps, HTMLButtonElement>; export type ButtonDisplayStyle = 'primary' | 'secondary' | 'outline' | 'blank' | CustomString; export type ButtonDisplayStatus = 'normal' | 'positive' | 'negative' | 'warning' | 'info' | CustomString; @@ -41,14 +38,14 @@ export interface IButtonProps extends ButtonHTMLProps { displayStatus?: ButtonDisplayStatus; /** icon definition for left icon, optionally uses custom JSX */ - leftOverlay?: JSX.Element; + leftOverlay?: React.ReactElement; /** icon definition for right icon, optionally uses custom JSX */ - rightOverlay?: JSX.Element; + rightOverlay?: React.ReactElement; } /** Renders an HTML button element with some useful additions */ -export const Button = React.forwardRef>((props, ref) => { +export const Button = (props: React.PropsWithChildren) => { const { className, disabled, @@ -56,11 +53,12 @@ export const Button = React.forwardRef ); -}); - -Button.defaultProps = { - pendingPosition: 'right', }; Button.displayName = 'Button'; diff --git a/module/src/components/characterLimit/characterLimit.component.tsx b/module/src/components/characterLimit/characterLimit.component.tsx index d3d96128..ee4d4521 100644 --- a/module/src/components/characterLimit/characterLimit.component.tsx +++ b/module/src/components/characterLimit/characterLimit.component.tsx @@ -25,7 +25,7 @@ export interface ICharacterLimitProps> className?: string; /** the icon to use for the validation errors */ - validationErrorIcon?: JSX.Element; + validationErrorIcon?: React.ReactElement; /** (Optional) Class name for the validation errors */ validationErrorsClassName?: string; @@ -38,22 +38,20 @@ export interface ICharacterLimitProps> } /** Render a character limit from a bound value, showing as an error if the user */ -export const CharacterLimit = React.forwardRef>>( - ( - { - bind, - limit, - shouldEnforce, - value, - className, - validationErrorIcon, - validationErrorsClassName, - validationErrorsTitle, - validationMode, - ...nativeProps - }, - ref - ) => { +export const CharacterLimit = (props: React.PropsWithChildren> & { ref?: React.Ref }>) => { + const { + bind, + limit, + shouldEnforce, + value, + className, + validationErrorIcon, + validationErrorsClassName, + validationErrorsTitle, + validationMode, + ref, + ...nativeProps + } = props; const globals = useArmstrongConfig({ validationErrorIcon, validationMode, @@ -84,12 +82,11 @@ export const CharacterLimit = React.forwardRef ); - } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as (>( - props: ArmstrongFCProps, HTMLDivElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>>; +}; + +// Type assertion for generic support - updated for React 19 ref as prop pattern +(CharacterLimit as any) = (CharacterLimit as (>( + props: React.PropsWithChildren & { ref?: React.Ref }> +) => React.ReactElement) & ArmstrongFCExtensions>>); CharacterLimit.displayName = 'CharacterLimit'; diff --git a/module/src/components/checkbox/checkbox.component.tsx b/module/src/components/checkbox/checkbox.component.tsx index 6e630932..9b54b920 100644 --- a/module/src/components/checkbox/checkbox.component.tsx +++ b/module/src/components/checkbox/checkbox.component.tsx @@ -25,11 +25,11 @@ export interface ICheckboxProps /** (Optional) A TData value representing the initial checked state of the checkbox. This can be true, false, or 'indeterminate'. */ checked?: TData; - /** (Optional) A custom JSX.Element for the indeterminate state indicator. */ - customIndeterminateIndicator?: JSX.Element; + /** (Optional) A custom React.ReactElement for the indeterminate state indicator. */ + customIndeterminateIndicator?: React.ReactElement; - /** (Optional) A custom JSX.Element for the checked indicator. */ - customIndicator?: JSX.Element; + /** (Optional) A custom React.ReactElement for the checked indicator. */ + customIndicator?: React.ReactElement; /** (Optional) A boolean flag to disable the checkbox input. */ disabled?: boolean; @@ -68,34 +68,32 @@ export interface ICheckboxProps autoValidate?: boolean; } -export const Checkbox = React.forwardRef>( - ( - { - bind, - checked, - customIndicator, - className, - customIndeterminateIndicator, - disabled, - onCheckedChange, - label, - labelClassName, - labelId, - scrollValidationErrorsIntoView, - statusClassName, - testId, - validationErrorsClassName, - validationErrorMessages, - validationMode, - displaySize, - required, - requiredIndicator, - statusPosition, - autoValidate, - ...nativeProps - }, - ref - ) => { +export const Checkbox = (props: React.PropsWithChildren & { ref?: React.Ref }>) => { + const { + bind, + checked, + customIndicator, + className, + customIndeterminateIndicator, + disabled, + onCheckedChange, + label, + labelClassName, + labelId, + scrollValidationErrorsIntoView, + statusClassName, + testId, + validationErrorsClassName, + validationErrorMessages, + validationMode, + displaySize, + required, + requiredIndicator, + statusPosition, + autoValidate, + ref, + ...nativeProps + } = props; const reactId = React.useId(); const id = nativeProps.id ?? reactId; @@ -189,10 +187,11 @@ export const Checkbox = React.forwardRef ); - } -) as (>( - props: ArmstrongFCProps, HTMLInputElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>>; +}; + +// Type assertion for generic support - updated for React 19 ref as prop pattern +(Checkbox as any) = (Checkbox as (>( + props: React.PropsWithChildren & { ref?: React.Ref }> +) => React.ReactElement) & ArmstrongFCExtensions>>); Checkbox.displayName = 'Checkbox'; diff --git a/module/src/components/checkboxList/checkboxList.component.tsx b/module/src/components/checkboxList/checkboxList.component.tsx index 616a3f31..29bab4fc 100644 --- a/module/src/components/checkboxList/checkboxList.component.tsx +++ b/module/src/components/checkboxList/checkboxList.component.tsx @@ -61,8 +61,8 @@ export interface ICheckboxListProps /** show an error state icon on the component (will be true automatically if validationErrorMessages are passed in or errors are in the binder) */ error?: boolean; - /** (Optional) A custom JSX.Element for the checked indicator. */ - customIndicator?: JSX.Element; + /** (Optional) A custom React.ReactElement for the checked indicator. */ + customIndicator?: React.ReactElement; /** which size variant to use */ displaySize?: DisplaySize; @@ -90,126 +90,120 @@ export interface ICheckboxListProps } /** Render a list of radio inputs which binds to a single string */ -export const CheckboxList = React.forwardRef>( - ( - { - bind, - options, - className, - value, - errorIcon, - validationMode, - validationErrorMessages, - onChange, - customIndicator, - error, - displaySize, - label, - labelClassName, - labelId, - required, - disabled, - scrollValidationErrorsIntoView, - validationErrorsClassName, - requiredIndicator, - autoValidate, - ...nativeProps +export const CheckboxList = (props: ICheckboxListProps & { ref?: React.Ref }) => { + const { + bind, + options, + className, + value, + errorIcon, + validationMode, + validationErrorMessages, + onChange, + customIndicator, + error, + displaySize, + label, + labelClassName, + labelId, + required, + disabled, + scrollValidationErrorsIntoView, + validationErrorsClassName, + requiredIndicator, + autoValidate, + ref, + ...nativeProps + } = props; + const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { + value, + validationErrorMessages, + validationErrorIcon: errorIcon, + validationMode, + onChange, + autoValidate, + }); + + const globals = useArmstrongConfig({ + validationMode: bindConfig.validationMode, + requiredIndicator, + scrollValidationErrorsIntoView, + validationErrorIcon: bindConfig.validationErrorIcon, + inputDisplaySize: displaySize, + autoValidate: bindConfig.autoValidate, + }); + + const onCheckedChange = React.useCallback( + (option: typeof options[0], newValue: boolean) => { + // Always mapping the bound value from the original array of options. + // It's a bit more cumbersome, but it means that the bound value ids will always be in the same order as the options, which is what you'd expect as a consuming app. + const newBoundValue = options.reduce((arr, op) => { + const isSelectedOption = op.id === option.id; + const isCurrentlyBound = boundValue?.includes(op.id); + const addIfCurrentlyBound = !isSelectedOption || newValue; + const addIfNotCurrentlyBound = isSelectedOption && newValue; + const shouldAdd = isCurrentlyBound ? addIfCurrentlyBound : addIfNotCurrentlyBound; + return [...arr, ...(shouldAdd ? [op.id] : [])]; + }, [] as ArmstrongId[]); + setBoundValue?.(newBoundValue); }, - ref - ) => { - const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { - value, - validationErrorMessages, - validationErrorIcon: errorIcon, - validationMode, - onChange, - autoValidate, - }); - - const globals = useArmstrongConfig({ - validationMode: bindConfig.validationMode, - requiredIndicator, - scrollValidationErrorsIntoView, - validationErrorIcon: bindConfig.validationErrorIcon, - inputDisplaySize: displaySize, - autoValidate: bindConfig.autoValidate, - }); - - const onCheckedChange = React.useCallback( - (option: typeof options[0], newValue: boolean) => { - // Always mapping the bound value from the original array of options. - // It's a bit more cumbersome, but it means that the bound value ids will always be in the same order as the options, which is what you'd expect as a consuming app. - const newBoundValue = options.reduce((arr, op) => { - const isSelectedOption = op.id === option.id; - const isCurrentlyBound = boundValue?.includes(op.id); - const addIfCurrentlyBound = !isSelectedOption || newValue; - const addIfNotCurrentlyBound = isSelectedOption && newValue; - const shouldAdd = isCurrentlyBound ? addIfCurrentlyBound : addIfNotCurrentlyBound; - return [...arr, ...(shouldAdd ? [op.id] : [])]; - }, [] as ArmstrongId[]); - setBoundValue?.(newBoundValue); - }, - [setBoundValue, boundValue, options] - ); - - useDidUpdateEffect(() => { - if (globals.autoValidate) { - bindConfig.validate(); - } - bindConfig.setTouched(true); - }, [boundValue]); - - return ( - <> - {label && ( - - )} -
{ + if (globals.autoValidate) { + bindConfig.validate(); + } + bindConfig.setTouched(true); + }, [boundValue]); + + return ( + <> + {label && ( +
+ + ); +}; CheckboxList.displayName = 'CheckboxList'; diff --git a/module/src/components/codeInput/codeInput.component.tsx b/module/src/components/codeInput/codeInput.component.tsx index 42bf70b4..76c6a61d 100644 --- a/module/src/components/codeInput/codeInput.component.tsx +++ b/module/src/components/codeInput/codeInput.component.tsx @@ -15,63 +15,57 @@ import { getLengthFromPart } from './codeInput.utils'; import './codeInput.theme.css'; -export interface ICodeInputPartProps> extends ITextInputProps { +export interface ICodeInputPartProps> extends Omit, 'part'> { /** The given length of this part. If this is a string, the string will be rendered. */ part: CodeInputPartDefinition; } /** an individual input from the CodeInput */ -const CodeInputPart = React.forwardRef>>( - ({ bind, part, ...inputProps }, ref) => { - const length = React.useMemo(() => getLengthFromPart(part), [part]); +const CodeInputPart = >(props: ICodeInputPartProps & { ref?: React.Ref }) => { + const { bind, part, ref, ...inputProps } = props; + const length = React.useMemo(() => getLengthFromPart(part), [part]); - if (typeof part === 'string') { - return

{part}

; - } - - if (typeof part === 'number') { - return ( - } - {...inputProps} - className="arm-code-input-part-input" - style={{ '--arm-code-input-length': length } as React.CSSProperties} - data-length={length} - onClick={event => event.currentTarget.select()} - maxLength={part} - /> - ); - } - - const { className, ...textInputProps } = part; + if (typeof part === 'string') { + return

{part}

; + } + if (typeof part === 'number') { return ( } - className={concat('arm-code-input-part-input', className)} - style={ - { - ...(textInputProps.style || {}), - '--arm-code-input-length': length, - } as React.CSSProperties - } + {...inputProps} + className="arm-code-input-part-input" + style={{ '--arm-code-input-length': length } as React.CSSProperties} data-length={length} onClick={event => event.currentTarget.select()} - {...inputProps} - {...textInputProps} - maxLength={part.length} - displaySize={part.displaySize} + maxLength={part} /> ); } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as (>( - props: ArmstrongVFCProps, HTMLInputElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>>; + + const { className, ...textInputProps } = part; + + return ( + } + className={concat('arm-code-input-part-input', className)} + style={ + { + ...(textInputProps.style || {}), + '--arm-code-input-length': length, + } as React.CSSProperties + } + data-length={length} + onClick={event => event.currentTarget.select()} + {...inputProps} + {...textInputProps} + maxLength={part.length} + displaySize={part.displaySize} + /> + ); +}; CodeInputPart.displayName = 'CodeInputPart'; @@ -133,272 +127,264 @@ export interface ICodeInputProps> autoValidate?: boolean; } -export const CodeInput = React.forwardRef>>( - ( - { - className, - parts, - bind, - onChange, - validationMode, - validationErrorMessages, - errorIcon, - error, - value, - pending, - statusPosition, - leftOverlay, - rightOverlay, - scrollValidationErrorsIntoView, - displaySize, - label, - required, - requiredIndicator, - hideIconOnStatus, - statusClassName, - validationErrorsClassName, - labelClassName, - labelId, - disableOnPending, - disabled, - autoValidate, - ...nativeProps +export const CodeInput = >(props: ICodeInputProps & { ref?: React.Ref }) => { + const { + className, + parts, + bind, + onChange, + validationMode, + validationErrorMessages, + errorIcon, + error, + value, + pending, + statusPosition, + leftOverlay, + rightOverlay, + scrollValidationErrorsIntoView, + displaySize, + label, + required, + requiredIndicator, + hideIconOnStatus, + statusClassName, + validationErrorsClassName, + labelClassName, + labelId, + disableOnPending, + disabled, + autoValidate, + ref, + ...nativeProps + } = props; + const inputRefs = React.useRef<(HTMLInputElement | null | undefined)[]>([]); + + const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { + onChange, + validationErrorMessages, + validationMode, + value, + autoValidate, + }); + + const globals = useArmstrongConfig({ + validationMode: bindConfig.validationMode, + inputStatusPosition: statusPosition, + inputDisplaySize: displaySize, + requiredIndicator, + hideInputErrorIconOnStatus: hideIconOnStatus, + disableControlOnPending: disableOnPending, + scrollValidationErrorsIntoView, + validationErrorIcon: errorIcon, + autoValidate: bindConfig.autoValidate, + }); + + const goNextPart = React.useCallback( + (partIndex: number) => { + const nextIndex = parts.slice(partIndex + 1).findIndex(part => typeof part !== 'string') + partIndex + 1; + + if (nextIndex !== -1) { + inputRefs.current[nextIndex]?.focus(); + } }, - ref - ) => { - const inputRefs = React.useRef<(HTMLInputElement | null | undefined)[]>([]); - - const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { - onChange, - validationErrorMessages, - validationMode, - value, - autoValidate, - }); - - const globals = useArmstrongConfig({ - validationMode: bindConfig.validationMode, - inputStatusPosition: statusPosition, - inputDisplaySize: displaySize, - requiredIndicator, - hideInputErrorIconOnStatus: hideIconOnStatus, - disableControlOnPending: disableOnPending, - scrollValidationErrorsIntoView, - validationErrorIcon: errorIcon, - autoValidate: bindConfig.autoValidate, - }); - - const goNextPart = React.useCallback( - (partIndex: number) => { - const nextIndex = parts.slice(partIndex + 1).findIndex(part => typeof part !== 'string') + partIndex + 1; - - if (nextIndex !== -1) { - inputRefs.current[nextIndex]?.focus(); - } - }, - [parts] - ); + [parts] + ); - /** @todo - why is focussing selecting before the final character? */ - const goPreviousPart = React.useCallback( - (partIndex: number) => { - const previousIndex = findLastIndex(parts.slice(0, partIndex), part => typeof part !== 'string'); + /** @todo - why is focussing selecting before the final character? */ + const goPreviousPart = React.useCallback( + (partIndex: number) => { + const previousIndex = findLastIndex(parts.slice(0, partIndex), part => typeof part !== 'string'); - if (previousIndex !== -1) { - inputRefs.current[previousIndex]?.focus(); - } - }, - [parts] - ); + if (previousIndex !== -1) { + inputRefs.current[previousIndex]?.focus(); + } + }, + [parts] + ); - const getValueForPart = React.useCallback( - (partIndex: number, incomingValue: NullOrUndefined) => { - const sliceStart = parts - .slice(0, partIndex) - .reduce((output, part) => output + getLengthFromPart(part), 0); - const sliceEnd = sliceStart + getLengthFromPart(parts[partIndex]); + const getValueForPart = React.useCallback( + (partIndex: number, incomingValue: NullOrUndefined) => { + const sliceStart = parts + .slice(0, partIndex) + .reduce((output, part) => output + getLengthFromPart(part), 0); + const sliceEnd = sliceStart + getLengthFromPart(parts[partIndex]); - return incomingValue?.slice(sliceStart, sliceEnd) || ''; - }, - [parts] - ); + return incomingValue?.slice(sliceStart, sliceEnd) || ''; + }, + [parts] + ); - const onPartValueChange = React.useCallback( - (event: React.ChangeEvent, partIndex: number) => { - const currentPartLength = getLengthFromPart(parts[partIndex]); + const onPartValueChange = React.useCallback( + (event: React.ChangeEvent, partIndex: number) => { + const currentPartLength = getLengthFromPart(parts[partIndex]); - const currentPartValue = event.currentTarget.value || ''; + const currentPartValue = event.currentTarget.value || ''; - if (currentPartValue.length >= currentPartLength) { - goNextPart(partIndex); - } - }, - [parts, goNextPart] - ); + if (currentPartValue.length >= currentPartLength) { + goNextPart(partIndex); + } + }, + [parts, goNextPart] + ); - const onPaste = React.useCallback( - (event: React.ClipboardEvent) => { - event.preventDefault(); - const pasteValue = event.clipboardData.getData('text/plain'); - const target = event.target as HTMLInputElement; + const onPaste = React.useCallback( + (event: React.ClipboardEvent) => { + event.preventDefault(); + const pasteValue = event.clipboardData.getData('text/plain'); + const target = event.target as HTMLInputElement; - const partIndex = inputRefs.current.indexOf(target); + const partIndex = inputRefs.current.indexOf(target); - const selectionStart = Math.min(target.selectionStart ?? 0, target.selectionEnd ?? 0); - const selectionEnd = Math.max(target.selectionStart ?? 0, target.selectionEnd ?? 0); + const selectionStart = Math.min(target.selectionStart ?? 0, target.selectionEnd ?? 0); + const selectionEnd = Math.max(target.selectionStart ?? 0, target.selectionEnd ?? 0); - const insertionStart = - new Array(partIndex).fill(null).reduce((memo, _, i) => { - const part = parts[i]; - return memo + getLengthFromPart(part); - }, 0) + selectionStart; + const insertionStart = + new Array(partIndex).fill(null).reduce((memo, _, i) => { + const part = parts[i]; + return memo + getLengthFromPart(part); + }, 0) + selectionStart; - const insertionEnd = insertionStart + (selectionEnd - selectionStart); + const insertionEnd = insertionStart + (selectionEnd - selectionStart); - const startSlice = boundValue?.slice(0, insertionStart) ?? ''; - const endSlice = boundValue?.slice(insertionEnd) ?? ''; + const startSlice = boundValue?.slice(0, insertionStart) ?? ''; + const endSlice = boundValue?.slice(insertionEnd) ?? ''; - const newValue = startSlice + pasteValue + endSlice; + const newValue = startSlice + pasteValue + endSlice; - setBoundValue(newValue); - }, - [setBoundValue, boundValue, parts] - ); - - const boundValueArray = React.useMemo(() => { - return parts.map((_, partIndex) => getValueForPart(partIndex, boundValue)); - }, [getValueForPart, parts, boundValue]); - - const onKeyDown = React.useCallback( - (event: React.KeyboardEvent, partIndex: number, part: number) => { - switch (event.key) { - case 'Backspace': { - if (event.currentTarget.value?.length <= 0 && partIndex > 0) { - goPreviousPart(partIndex); - } - break; - } - case 'ArrowLeft': { - if (event.currentTarget.selectionStart === 0 && partIndex > 0) { - goPreviousPart(partIndex); - } - break; + setBoundValue(newValue); + }, + [setBoundValue, boundValue, parts] + ); + + const boundValueArray = React.useMemo(() => { + return parts.map((_, partIndex) => getValueForPart(partIndex, boundValue)); + }, [getValueForPart, parts, boundValue]); + + const onKeyDown = React.useCallback( + (event: React.KeyboardEvent, partIndex: number, part: number) => { + switch (event.key) { + case 'Backspace': { + if (event.currentTarget.value?.length <= 0 && partIndex > 0) { + goPreviousPart(partIndex); } - case 'ArrowRight': { - if (event.currentTarget.selectionEnd === getLengthFromPart(part) && partIndex < parts.length) { - goNextPart(partIndex); - } - break; + break; + } + case 'ArrowLeft': { + if (event.currentTarget.selectionStart === 0 && partIndex > 0) { + goPreviousPart(partIndex); } - default: { - break; + break; + } + case 'ArrowRight': { + if (event.currentTarget.selectionEnd === getLengthFromPart(part) && partIndex < parts.length) { + goNextPart(partIndex); } + break; + } + default: { + break; } - }, - [goPreviousPart, goNextPart, parts] - ); - - interface IFormState { - parts: string[]; - } - - const { formProp, formState } = useForm({ - parts: boundValueArray, - }); - - React.useEffect(() => { - setBoundValue?.(formState?.parts?.join('')); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want the trigger the effect when the function is re-defined - }, [formState]); - - useDidUpdateEffect(() => { - if (globals.autoValidate && bindConfig.isTouched) { - bindConfig.validate(); } - }, [boundValue]); + }, + [goPreviousPart, goNextPart, parts] + ); - const onBlur = React.useCallback(() => { - if (formState?.parts.every(p => p) && globals.autoValidate && !bindConfig.isTouched) { - bindConfig.validate(); - bindConfig.setTouched(true); - } - }, [bindConfig, formState?.parts, globals.autoValidate]); + interface IFormState { + parts: string[]; + } - const showLeftOverlay = - leftOverlay && - (globals.inputStatusPosition !== 'left' || - !globals.hideInputErrorIconOnStatus || - (!pending && (!bindConfig.shouldShowValidationErrorIcon || !bindConfig.validationErrorMessages.length))); + const { formProp, formState } = useForm({ + parts: boundValueArray, + }); - const showRightOverlay = - rightOverlay && - (globals.inputStatusPosition !== 'right' || - !globals.hideInputErrorIconOnStatus || - (!pending && (!bindConfig.shouldShowValidationErrorIcon || !bindConfig.validationErrorMessages.length))); + React.useEffect(() => { + setBoundValue?.(formState?.parts?.join('')); + // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want the trigger the effect when the function is re-defined + }, [formState]); - return ( - <> -
- {label && ( - - )} -
- - <> - {showLeftOverlay && leftOverlay} - {parts?.map((part, index) => ( - - type="text" - bind={formProp('parts', index).bind()} - part={part} - key={index} - onChange={event => onPartValueChange(event, index)} - onKeyDown={event => onKeyDown(event, index, +part)} - onPaste={onPaste} - onBlur={onBlur} - disabled={disabled || (pending && globals.disableControlOnPending)} - ref={r => { - inputRefs.current[index] = r; - }} - displaySize={globals.inputDisplaySize} - /> - ))} - {showRightOverlay && rightOverlay} - - -
-
+ useDidUpdateEffect(() => { + if (globals.autoValidate && bindConfig.isTouched) { + bindConfig.validate(); + } + }, [boundValue]); - {!!bindConfig.validationErrorMessages?.length && bindConfig.shouldShowValidationErrorMessage && ( - + const onBlur = React.useCallback(() => { + if (formState?.parts.every(p => p) && globals.autoValidate && !bindConfig.isTouched) { + bindConfig.validate(); + bindConfig.setTouched(true); + } + }, [bindConfig, formState?.parts, globals.autoValidate]); + + const showLeftOverlay = + leftOverlay && + (globals.inputStatusPosition !== 'left' || + !globals.hideInputErrorIconOnStatus || + (!pending && (!bindConfig.shouldShowValidationErrorIcon || !bindConfig.validationErrorMessages.length))); + + const showRightOverlay = + rightOverlay && + (globals.inputStatusPosition !== 'right' || + !globals.hideInputErrorIconOnStatus || + (!pending && (!bindConfig.shouldShowValidationErrorIcon || !bindConfig.validationErrorMessages.length))); + + return ( + <> +
+ {label && ( + )} - - ); - } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as (>( - props: ArmstrongVFCProps, HTMLDivElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>>; +
+ + <> + {showLeftOverlay && leftOverlay} + {parts?.map((part, index) => ( + + type="text" + bind={formProp('parts', index).bind()} + part={part} + key={index} + onChange={event => onPartValueChange(event, index)} + onKeyDown={event => onKeyDown(event, index, +part)} + onPaste={onPaste} + onBlur={onBlur} + disabled={disabled || (pending && globals.disableControlOnPending)} + ref={r => { + inputRefs.current[index] = r; + }} + displaySize={globals.inputDisplaySize} + /> + ))} + {showRightOverlay && rightOverlay} + + +
+
+ + {!!bindConfig.validationErrorMessages?.length && bindConfig.shouldShowValidationErrorMessage && ( + + )} + + ); +}; CodeInput.displayName = 'Code Input'; diff --git a/module/src/components/config/config.context.tsx b/module/src/components/config/config.context.tsx index 7bdef613..5ed98646 100644 --- a/module/src/components/config/config.context.tsx +++ b/module/src/components/config/config.context.tsx @@ -46,16 +46,16 @@ export interface IArmstrongConfig { requiredIndicator?: React.ReactNode; /** the icon to use for the validation errors */ - validationErrorIcon?: JSX.Element; + validationErrorIcon?: React.ReactElement; /** the icon to use for the spinner */ - spinnerIcon?: JSX.Element; + spinnerIcon?: React.ReactElement; /** the element to portal global overlays to (like toast and modal), defaults to the body element */ globalPortalTo?: Element; /** the icon to use for the dialog close button */ - dialogCloseButtonIcon?: JSX.Element | false; + dialogCloseButtonIcon?: React.ReactElement | false; /** how long ot show toast messages for in ms, defaults to 5000 */ toastDuration?: number; @@ -64,7 +64,7 @@ export interface IArmstrongConfig { toastPosition?: ToastPosition; /** the icon to use for the dialog close button */ - toastCloseButtonIcon?: JSX.Element | false; + toastCloseButtonIcon?: React.ReactElement | false; /** whether to add toasts to a stack or display one at a time */ toastDisplayMode?: ToastDisplayMode; @@ -72,11 +72,11 @@ export interface IArmstrongConfig { /** ignore toasts if an existing toast matches this predicate */ toastIgnorePredicate?: (existingToasts: IToast[], incomingToast: IToast) => boolean; - /** A custom JSX.Element for the checked indicator. (Optional) */ - checkboxCustomIndicator?: JSX.Element; + /** A custom React.ReactElement for the checked indicator. (Optional) */ + checkboxCustomIndicator?: React.ReactElement; - /** A custom JSX.Element for the indeterminate state indicator. (Optional) */ - checkboxCustomIndeterminateIndicator?: JSX.Element; + /** A custom React.ReactElement for the indeterminate state indicator. (Optional) */ + checkboxCustomIndeterminateIndicator?: React.ReactElement; /** How long in ms to wait after hover before displaying the tooltip, defaults to 700 */ tooltipDelay?: number; diff --git a/module/src/components/dateTimeInput/dateTimeInput.component.tsx b/module/src/components/dateTimeInput/dateTimeInput.component.tsx index 11be4715..34e1430d 100644 --- a/module/src/components/dateTimeInput/dateTimeInput.component.tsx +++ b/module/src/components/dateTimeInput/dateTimeInput.component.tsx @@ -270,29 +270,27 @@ const CenteredHeader: React.FC = props => { ); }; -export const SingleDateTimeInput = React.forwardRef>( - ( - { - config, - selectsRange, - className, - bind, - value, - onChange, - validationErrorMessages, - monthSelectVariant, - mode, - native, - format, - locale, - statusClassName, - autoValidate, - showCalendarOnLeftOverlayClick, - onBlur, - ...inputProps - }, - ref - ) => { +export const SingleDateTimeInput = (props: IDateOrTimeInputSingleProps & { ref?: React.Ref }) => { + const { + config, + selectsRange, + className, + bind, + value, + onChange, + validationErrorMessages, + monthSelectVariant, + mode, + native, + format, + locale, + statusClassName, + autoValidate, + showCalendarOnLeftOverlayClick, + onBlur, + ref, + ...inputProps + } = props; const datePickerRef = React.useRef(null); const [date, setDate, bindDateConfig] = useBindingState(bind, { @@ -417,37 +415,30 @@ export const SingleDateTimeInput = React.forwardRef setDate?.(formatDate(newValue as Date, compiledFormat))} /> ); - } -); +}; SingleDateTimeInput.displayName = 'SingleDateTimeInput'; -SingleDateTimeInput.defaultProps = { - locale: defaultLocale, - showCalendarOnLeftOverlayClick: true, -}; -export const RangeDateTimeInput = React.forwardRef>( - ( - { - config, - selectsRange, - startBind, - endBind, - startValue, - endValue, - className, - onChange, - validationErrorMessages, - native, - format, - locale, - autoValidate, - showCalendarOnLeftOverlayClick, - ...inputProps - }, - ref - ) => { +export const RangeDateTimeInput = (props: IDateTimeInputRangeProps & { ref?: React.Ref }) => { + const { + config, + selectsRange, + startBind, + endBind, + startValue, + endValue, + className, + onChange, + validationErrorMessages, + native, + format, + locale, + autoValidate, + showCalendarOnLeftOverlayClick, + ref, + ...inputProps + } = props; const datePickerRef = React.useRef>(null); const [startDate, setStartDate, bindStartDateConfig] = useBindingState(startBind, { @@ -547,67 +538,57 @@ export const RangeDateTimeInput = React.forwardRef ); - } -); +}; RangeDateTimeInput.displayName = 'RangeDateTimeInput'; -RangeDateTimeInput.defaultProps = { - leftOverlay: , - format: defaultDateFormat, - locale: defaultLocale, - showCalendarOnLeftOverlayClick: true, -}; -const NativeDateTimeInput = React.forwardRef>( - ({ mode, selectsRange, native, ...props }, ref) => { - return ; - } -); +const NativeDateTimeInput = (props: IDateTimeInputNativeProps & { ref?: React.Ref }) => { + const { mode, selectsRange, native, ref, ...otherProps } = props; + return ; +}; NativeDateTimeInput.displayName = 'NativeDateTimeInput'; -export const SingleDateAndTimeInput = React.forwardRef>( - ( - { - className, - bind, - mode, - validationErrorMessages, - disableOnPending, - hideIconOnStatus, - displaySize, - statusPosition, - requiredIndicator, - scrollValidationErrorsIntoView, - validationMode, - errorIcon, - validationErrorsClassName, - value, - onChange, - dateInputConfig, - timeInputConfig, - format, - locale, - disabled, - required, - dateInputProps = {}, - timeInputProps = {}, - timeInputDisplayFormat, - dateInputDisplayFormat, - timeInputRef, - selectsRange, - native, - monthSelectVariant, - error, - label, - labelId, - labelClassName, - autoValidate, - ...nativeProps - }, - ref - ) => { +export const SingleDateAndTimeInput = (props: IDateAndTimeInputSingleProps & { ref?: React.Ref }) => { + const { + className, + bind, + mode, + validationErrorMessages, + disableOnPending, + hideIconOnStatus, + displaySize, + statusPosition, + requiredIndicator, + scrollValidationErrorsIntoView, + validationMode, + errorIcon, + validationErrorsClassName, + value, + onChange, + dateInputConfig, + timeInputConfig, + format, + locale, + disabled, + required, + dateInputProps = {}, + timeInputProps = {}, + timeInputDisplayFormat, + dateInputDisplayFormat, + timeInputRef, + selectsRange, + native, + monthSelectVariant, + error, + label, + labelId, + labelClassName, + autoValidate, + ref, + ...nativeProps + } = props; const [dateTime, setDateTime, bindConfig] = useBindingState(bind, { validationErrorMessages, value, @@ -739,43 +720,25 @@ export const SingleDateAndTimeInput = React.forwardRef ); - } -); +}; SingleDateAndTimeInput.displayName = 'SingleDateAndTimeInput'; -SingleDateAndTimeInput.defaultProps = { - format: defaultDateAndTimeFormat, - locale: defaultLocale, - timeInputDisplayFormat: defaultTimeFormat, - dateInputDisplayFormat: defaultDateFormat, -}; /** third-party docs: https://reactdatepicker.com */ -export const DateTimeInput = React.forwardRef>( - (props, ref) => { - if (props.selectsRange) { - return ; - } - if (props.native) { - return ; - } - if (props.mode === 'date-time') { - return ; - } - return ; +export const DateTimeInput = >(props: DateTimeInputProps & { ref?: React.Ref }) => { + const { ref, ...otherProps } = props; + if (props.selectsRange) { + return ; + } + if (props.native) { + return ; } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as (>( - props: ArmstrongFCProps, HTMLInputElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>; + if (props.mode === 'date-time') { + return ; + } + return ; +}; DateTimeInput.displayName = 'DateTimeInput'; -DateTimeInput.defaultProps = { - selectsRange: false, - mode: 'date', - native: false, -}; diff --git a/module/src/components/dialog/dialog.component.tsx b/module/src/components/dialog/dialog.component.tsx index e086ccec..974d5fc3 100644 --- a/module/src/components/dialog/dialog.component.tsx +++ b/module/src/components/dialog/dialog.component.tsx @@ -17,7 +17,7 @@ export interface IDialogProps /** Optional description to show in the body of the dialog in a P tag */ description?: React.ReactNode; /** Icon to use as the close button. Send `false` to hide the close button entirely */ - closeButtonIcon?: JSX.Element | false; + closeButtonIcon?: React.ReactElement | false; /** Bool denoting whether the dialog is open or closed - for state controlled dialogs */ open?: boolean; /** Function called when the dialog is opened/closed - for state controlled dialogs */ @@ -74,192 +74,188 @@ export interface DialogElement { * - Can be async by assigning a ref and calling the utility functions (a `useDialog` helper hook is available for this.) * - Supports dynamic data in async mode, so that a form can be built as a reusable async dialog. */ -export const Dialog = React.forwardRef( - (props: React.PropsWithChildren>, ref: React.ForwardedRef>) => { - const { - children, - title, - description, - closeButtonIcon, - open, - onOpenChange, - onClose, - className, - data, - overlayClassName, - testId, - delayCloseFor, - onBeforeUnload, - ...nativeProps - } = props; +export const Dialog = (props: React.PropsWithChildren & { ref?: React.Ref> }>) => { + const { + children, + title, + description, + closeButtonIcon, + open, + onOpenChange, + onClose, + className, + data, + overlayClassName, + testId, + delayCloseFor, + onBeforeUnload, + ref, + ...nativeProps + } = props; - /** Pull globals from context */ - const globals = useArmstrongConfig({ - dialogCloseButtonIcon: closeButtonIcon, - }); + /** Pull globals from context */ + const globals = useArmstrongConfig({ + dialogCloseButtonIcon: closeButtonIcon, + }); - /** Finish action state is set when the dialog is closed */ - const [finishAction, setFinishAction] = React.useState(open ? undefined : 'close'); + /** Finish action state is set when the dialog is closed */ + const [finishAction, setFinishAction] = React.useState(open ? undefined : 'close'); - /** Store state for whether the dialog is visible */ - const [visible, setVisible] = React.useState(open); + /** Store state for whether the dialog is visible */ + const [visible, setVisible] = React.useState(open); - /** Stores a reference to the promise resolver function */ - const resolverRef = - React.useRef<(value: IDialogOpenResponse | PromiseLike>) => void>(); + /** Stores a reference to the promise resolver function */ + const resolverRef = + React.useRef<((value: IDialogOpenResponse | PromiseLike>) => void) | undefined>(undefined); - /** Used to create prop comparisons to use as effect triggers */ - const finishActionChanged = useCompareValues(finishAction); - const openHasChanged = useCompareValues(open); + /** Used to create prop comparisons to use as effect triggers */ + const finishActionChanged = useCompareValues(finishAction); + const openHasChanged = useCompareValues(open); - /** Store for open change listeners */ - const openChangeListeners = React.useRef([]); + /** Store for open change listeners */ + const openChangeListeners = React.useRef([]); - /** Function to call the onBeforeUnload function, if it exists and unwrap the promise */ - const getBeforeUnloadValue = React.useCallback( - async (action: DialogFinishAction) => { - if (onBeforeUnload) { - const response = onBeforeUnload(action); - if (response instanceof Promise) { - return response; - } - return Promise.resolve(response); + /** Function to call the onBeforeUnload function, if it exists and unwrap the promise */ + const getBeforeUnloadValue = React.useCallback( + async (action: DialogFinishAction) => { + if (onBeforeUnload) { + const response = onBeforeUnload(action); + if (response instanceof Promise) { + return response; } - return true; - }, - [onBeforeUnload] - ); + return Promise.resolve(response); + } + return true; + }, + [onBeforeUnload] + ); - /** Called when the internal open/close state of the dialog changes */ - const onInnerOpenChange = React.useCallback( - async (val: boolean) => { - if (val === false) { - const response = await getBeforeUnloadValue?.('close'); - if (response === false) return; - } - setFinishAction(val ? undefined : 'close'); - if (!val && delayCloseFor) { - setTimeout(() => { - setVisible(false); - }, delayCloseFor); - return; - } - setVisible(val); - }, - [delayCloseFor, getBeforeUnloadValue] - ); + /** Called when the internal open/close state of the dialog changes */ + const onInnerOpenChange = React.useCallback( + async (val: boolean) => { + if (val === false) { + const response = await getBeforeUnloadValue?.('close'); + if (response === false) return; + } + setFinishAction(val ? undefined : 'close'); + if (!val && delayCloseFor) { + setTimeout(() => { + setVisible(false); + }, delayCloseFor); + return; + } + setVisible(val); + }, + [delayCloseFor, getBeforeUnloadValue] + ); - /** This might seem odd, but these functions are often used as dependencies so we want to memoize them as much as possible */ - const onOpen = React.useCallback['open']>( - () => - new Promise(res => { - onInnerOpenChange(true); - resolverRef.current = res; - }), - [onInnerOpenChange] - ); - const setOk = React.useCallback(async () => { - if ((await getBeforeUnloadValue?.('ok')) === false) return; - setFinishAction('ok'); - }, [getBeforeUnloadValue]); + /** This might seem odd, but these functions are often used as dependencies so we want to memoize them as much as possible */ + const onOpen = React.useCallback['open']>( + () => + new Promise(res => { + onInnerOpenChange(true); + resolverRef.current = res; + }), + [onInnerOpenChange] + ); + const setOk = React.useCallback(async () => { + if ((await getBeforeUnloadValue?.('ok')) === false) return; + setFinishAction('ok'); + }, [getBeforeUnloadValue]); - const setClose = React.useCallback(async () => { - if ((await getBeforeUnloadValue?.('close')) === false) return; - setFinishAction('close'); - }, [getBeforeUnloadValue]); + const setClose = React.useCallback(async () => { + if ((await getBeforeUnloadValue?.('close')) === false) return; + setFinishAction('close'); + }, [getBeforeUnloadValue]); - const setCancel = React.useCallback(async () => { - if ((await getBeforeUnloadValue?.('cancel')) === false) return; - setFinishAction('cancel'); - }, [getBeforeUnloadValue]); + const setCancel = React.useCallback(async () => { + if ((await getBeforeUnloadValue?.('cancel')) === false) return; + setFinishAction('cancel'); + }, [getBeforeUnloadValue]); - /** Exposes the DialogElement utility functions to the ref */ - React.useImperativeHandle( - ref, - () => ({ - open: onOpen, - close: setClose, - ok: setOk, - cancel: setCancel, - isOpen: !!visible, - addOpenChangeListener: (listener: OpenChangeListener) => { - openChangeListeners.current.push(listener); - return () => { - openChangeListeners.current = openChangeListeners.current.filter(l => l !== listener); - }; - }, - }), - [onOpen, setCancel, setClose, setOk, visible] - ); + /** Exposes the DialogElement utility functions to the ref */ + React.useImperativeHandle( + ref, + () => ({ + open: onOpen, + close: setClose, + ok: setOk, + cancel: setCancel, + isOpen: !!visible, + addOpenChangeListener: (listener: OpenChangeListener) => { + openChangeListeners.current.push(listener); + return () => { + openChangeListeners.current = openChangeListeners.current.filter(l => l !== listener); + }; + }, + }), + [onOpen, setCancel, setClose, setOk, visible] + ); - /** Listens to the `finishAction` state and runs the appropriate functions */ - React.useEffect(() => { - if (finishAction && resolverRef.current) { - resolverRef.current({ action: finishAction, data }); - resolverRef.current = undefined; - } - if (finishActionChanged) { - onOpenChange?.(!finishAction); - if (finishAction) { - onClose?.(); - if (delayCloseFor) { - setTimeout(() => { - setVisible(false); - }, delayCloseFor); - } else { + /** Listens to the `finishAction` state and runs the appropriate functions */ + React.useEffect(() => { + if (finishAction && resolverRef.current) { + resolverRef.current({ action: finishAction, data }); + resolverRef.current = undefined; + } + if (finishActionChanged) { + onOpenChange?.(!finishAction); + if (finishAction) { + onClose?.(); + if (delayCloseFor) { + setTimeout(() => { setVisible(false); - } + }, delayCloseFor); + } else { + setVisible(false); } } - }, [finishActionChanged, finishAction, data, onOpenChange, onClose, delayCloseFor]); + } + }, [finishActionChanged, finishAction, data, onOpenChange, onClose, delayCloseFor]); - /** Listens to changes on the incoming `open` prop for controlled dialogs, and runs the appropriate functions */ - React.useEffect(() => { - if (openHasChanged && open !== undefined) { - onInnerOpenChange(open); - } - }, [open, onInnerOpenChange, openHasChanged]); + /** Listens to changes on the incoming `open` prop for controlled dialogs, and runs the appropriate functions */ + React.useEffect(() => { + if (openHasChanged && open !== undefined) { + onInnerOpenChange(open); + } + }, [open, onInnerOpenChange, openHasChanged]); - /** Listens to any change in visibility and calls any open change listeners */ - React.useEffect(() => { - openChangeListeners.current.forEach(listener => listener(!!visible)); - }, [visible]); + /** Listens to any change in visibility and calls any open change listeners */ + React.useEffect(() => { + openChangeListeners.current.forEach(listener => listener(!!visible)); + }, [visible]); - return ( - - {globals.globalPortalTo && - ReactDOM.createPortal( - + {globals.globalPortalTo && + ReactDOM.createPortal( + + - - {title && {title}} - {description && ( - {description} - )} - {children &&
{children}
} - {globals.dialogCloseButtonIcon !== false && ( - - {globals.dialogCloseButtonIcon} - - )} -
-
, - globals.globalPortalTo - )} -
- ); - } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as ((props: ArmstrongFCProps, DialogElement>) => ArmstrongFCReturn) & - ArmstrongFCExtensions>; + {title && {title}} + {description && ( + {description} + )} + {children &&
{children}
} + {globals.dialogCloseButtonIcon !== false && ( + + {globals.dialogCloseButtonIcon} + + )} + + , + globals.globalPortalTo + )} + + ); +}; Dialog.displayName = 'Dialog'; diff --git a/module/src/components/dialog/dialog.hooks.ts b/module/src/components/dialog/dialog.hooks.ts index 7d3adb1d..a016e585 100644 --- a/module/src/components/dialog/dialog.hooks.ts +++ b/module/src/components/dialog/dialog.hooks.ts @@ -20,7 +20,7 @@ export type UseDialogReturn = [React.RefObject>, Dia */ export const useDialog = ( forwardRef?: React.ForwardedRef> -): [React.RefObject>, Omit, 'addOpenChangeListener'>] => { +): [React.RefObject | null>, Omit, 'addOpenChangeListener'>] => { const dialogRef = React.useRef>(null); const [isOpen, setIsOpen] = React.useState(false); React.useImperativeHandle(forwardRef, () => dialogRef.current as DialogElement); diff --git a/module/src/components/dropdownMenu/dropdownMenu.component.tsx b/module/src/components/dropdownMenu/dropdownMenu.component.tsx index 03bb13a4..1a827059 100644 --- a/module/src/components/dropdownMenu/dropdownMenu.component.tsx +++ b/module/src/components/dropdownMenu/dropdownMenu.component.tsx @@ -77,107 +77,104 @@ export interface IDropdownMenuProps footerContent?: React.ReactNode; } -export const DropdownMenu = React.forwardRef>( - ( - { - items, - children, - className, - showArrow, - open, - defaultOpen, - onOpenChange, - footerContent, - headerContent, - disabled, - modal, - ...nativeProps - }, - ref - ) => { - const [internalOpen, setInternalOpen] = React.useState(open ?? defaultOpen ?? false); - React.useEffect(() => { - if (open !== undefined) { - setInternalOpen(open); - } - }, [open]); - const onOpenChangeInner = React.useCallback( - (isOpen: boolean) => { - if (onOpenChange) { - onOpenChange(isOpen); - } else { - setInternalOpen(isOpen); - } - }, - [onOpenChange] - ); - const parsedContent = React.useMemo(() => { - if (React.isValidElement(items)) { - return items; +export const DropdownMenu = (props: React.PropsWithChildren }>) => { + const { + items, + children, + className, + showArrow, + open, + defaultOpen, + onOpenChange, + footerContent, + headerContent, + disabled, + modal, + ref, + ...nativeProps + } = props; + const [internalOpen, setInternalOpen] = React.useState(open ?? defaultOpen ?? false); + React.useEffect(() => { + if (open !== undefined) { + setInternalOpen(open); + } + }, [open]); + const onOpenChangeInner = React.useCallback( + (isOpen: boolean) => { + if (onOpenChange) { + onOpenChange(isOpen); + } else { + setInternalOpen(isOpen); } - if (Array.isArray(items)) { - return items.map((item, index) => { - return ( - - { - setInternalOpen(false); - item.onClick?.(index, event); - }) - } - > - {item.leftOverlay &&
{item.leftOverlay}
} - {item.label &&
{item.label}
} - {item.rightOverlay &&
{item.rightOverlay}
} -
- {item.addSeparatorUnder && } -
- ); - }); - } - throw new Error('Invalid content passed to DropdownMenu. Must be an array of items or a single React element.'); - }, [items]); - - return ( - { + if (React.isValidElement(items)) { + return items; + } + if (Array.isArray(items)) { + return items.map((item, index) => { + return ( + + { + setInternalOpen(false); + item.onClick?.(index, event); + }) + } + > + {item.leftOverlay &&
{item.leftOverlay}
} + {item.label &&
{item.label}
} + {item.rightOverlay &&
{item.rightOverlay}
} +
+ {item.addSeparatorUnder && } +
+ ); + }); + } + throw new Error('Invalid content passed to DropdownMenu. Must be an array of items or a single React element.'); + }, [items]); + + return ( + + {children && ( + + {children} + + )} + - {children && ( - - {children} - + {!footerContent && !headerContent ? ( + parsedContent + ) : ( + <> + {headerContent &&
{headerContent}
} +
{parsedContent}
+ {footerContent &&
{footerContent}
} + )} - - {!footerContent && !headerContent ? ( - parsedContent - ) : ( - <> - {headerContent &&
{headerContent}
} -
{parsedContent}
- {footerContent &&
{footerContent}
} - - )} - {showArrow &&
} - - - ); - } -); + {showArrow &&
} + + + ); +}; DropdownMenu.displayName = 'DropdownMenu'; diff --git a/module/src/components/expandable/expandable.component.tsx b/module/src/components/expandable/expandable.component.tsx index 51b5bca5..d26c03ab 100644 --- a/module/src/components/expandable/expandable.component.tsx +++ b/module/src/components/expandable/expandable.component.tsx @@ -14,35 +14,31 @@ export interface IExpandableProps extends React.DetailedHTMLProps>( - ({ className, children, style, animate, isOpen, ...nativeProps }, ref) => { - const contentRef = React.useRef(null); - const [{ height }] = useBoundingClientRect(contentRef); - - return ( -
-
- {children} -
+export const Expandable = (props: React.PropsWithChildren }>) => { + const { className, children, style, animate, isOpen, ref, ...nativeProps } = props; + const contentRef = React.useRef(null); + const [{ height }] = useBoundingClientRect(contentRef); + + return ( +
+
+ {children}
- ); - } -); +
+ ); +}; Expandable.displayName = 'Expandable'; -Expandable.defaultProps = { - animate: true, -}; diff --git a/module/src/components/input/input.component.tsx b/module/src/components/input/input.component.tsx index 6b2105db..f4f6d592 100644 --- a/module/src/components/input/input.component.tsx +++ b/module/src/components/input/input.component.tsx @@ -29,21 +29,20 @@ interface IDelayedInputBaseProps extends NativeInputProps { value?: TValue; } -const DebounceInputBase = React.forwardRef>( - ({ milliseconds, value, onValueChange, onChange, ...nativeProps }, ref) => { - const [actualValue, setActualValue] = useDebounce(milliseconds, value, onValueChange); - - const onChangeEvent = React.useCallback( - (e: React.ChangeEvent) => { - setActualValue(e.currentTarget.value); - onChange?.(e); - }, - [setActualValue, onChange] - ); +const DebounceInputBase = (props: React.PropsWithChildren & { ref?: React.Ref }>) => { + const { milliseconds, value, onValueChange, onChange, ref, ...nativeProps } = props; + const [actualValue, setActualValue] = useDebounce(milliseconds, value, onValueChange); + + const onChangeEvent = React.useCallback( + (e: React.ChangeEvent) => { + setActualValue(e.currentTarget.value); + onChange?.(e); + }, + [setActualValue, onChange] + ); - return ; - } -); + return ; +}; DebounceInputBase.displayName = 'DebounceInput'; @@ -101,45 +100,40 @@ export interface INumberInputProps> exten } /** A component which wraps up a native input element with some binding logic and some repeated elements (icons and stuff) for components which only contain a single input element. */ -export const Input = React.forwardRef< - HTMLInputElement, - IInputProps & { type?: HTMLInputTypeAttribute } ->( - ( - { - bind, - onChange, - value, - className, - leftOverlay, - rightOverlay, - validationErrorMessages, - validationMode, - errorIcon: validationErrorIcon, - pending, - disabled, - disableOnPending, - statusPosition, - hideIconOnStatus, - onValueChange, - scrollValidationErrorsIntoView, - delay, - validationErrorsClassName, - statusClassName, - inputClassName, - label, - required, - requiredIndicator, - displaySize, - labelClassName, - labelId, - wrapperTestId, - error, - autoValidate, - ...nativeProps - }, - ref - ) => { +export const Input = (props: React.PropsWithChildren & { type?: HTMLInputTypeAttribute; ref?: React.Ref }>) => { + const { + bind, + onChange, + value, + className, + leftOverlay, + rightOverlay, + validationErrorMessages, + validationMode, + errorIcon: validationErrorIcon, + pending, + disabled, + disableOnPending, + statusPosition, + hideIconOnStatus, + onValueChange, + scrollValidationErrorsIntoView, + delay, + validationErrorsClassName, + statusClassName, + inputClassName, + label, + required, + requiredIndicator, + displaySize, + labelClassName, + labelId, + wrapperTestId, + error, + autoValidate, + ref, + ...nativeProps + } = props; const reactId = React.useId(); const id = nativeProps.id ?? reactId; @@ -300,12 +294,11 @@ export const Input = React.forwardRef< )} ); - } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as (, TNumberValue extends NullOrUndefined>( - props: ArmstrongFCProps | INumberInputProps, HTMLInputElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions | INumberInputProps>; +}; + +// Type assertion for generic support - updated for React 19 ref as prop pattern +(Input as any) = (Input as (, TNumberValue extends NullOrUndefined>( + props: React.PropsWithChildren<(ITextInputProps | INumberInputProps) & { ref?: React.Ref }> +) => React.ReactElement) & ArmstrongFCExtensions | INumberInputProps>); Input.displayName = 'Input'; diff --git a/module/src/components/inputWrapper/inputWrapper.component.tsx b/module/src/components/inputWrapper/inputWrapper.component.tsx index 1a069a63..00d9f01c 100644 --- a/module/src/components/inputWrapper/inputWrapper.component.tsx +++ b/module/src/components/inputWrapper/inputWrapper.component.tsx @@ -58,121 +58,118 @@ export interface IInputWrapperProps extends IStatusWrapperProps { } /** Wrapper for individual input elements, allowing them to be styled consistently] */ -export const InputWrapper = React.forwardRef>( - ( - { - className, - children, - leftOverlay, - rightOverlay, - validationMode, - validationErrorMessages, - errorIcon, - disabled, - pending, - error, - statusPosition, - hideIconOnStatus, - disableOnPending, - scrollValidationErrorsIntoView, - statusClassName, - validationErrorsClassName, - label, - required, - requiredIndicator, - labelClassName, - labelId, - displaySize, - ...nativeProps - }, - ref - ) => { - const globals = useArmstrongConfig({ - validationMode, - hideInputErrorIconOnStatus: hideIconOnStatus, - disableControlOnPending: disableOnPending, - requiredIndicator, - scrollValidationErrorsIntoView, - inputStatusPosition: statusPosition, - validationErrorIcon: errorIcon, - inputDisplaySize: displaySize, - }); - - const shouldShowValidationErrorsList = globals.validationMode === 'both' || globals.validationMode === 'message'; - const shouldShowErrorIcon = - (!!validationErrorMessages?.length && (globals.validationMode === 'both' || globals.validationMode === 'icon')) || - error; - - const showLeftOverlay = - leftOverlay && - (globals.inputStatusPosition !== 'left' || - !globals.hideInputErrorIconOnStatus || - (!pending && !shouldShowErrorIcon)); - - const showRightOverlay = - rightOverlay && - (globals.inputStatusPosition !== 'right' || - !globals.hideInputErrorIconOnStatus || - (!pending && !shouldShowErrorIcon)); - - return ( - <> -
- {label && ( - - )} -
- - <> - {showLeftOverlay &&
{leftOverlay}
} - {children} - {showRightOverlay &&
{rightOverlay}
} - -
-
- {!!validationErrorMessages?.length && shouldShowValidationErrorsList && ( - - )} +export const InputWrapper = (props: React.PropsWithChildren }>) => { + const { + className, + children, + leftOverlay, + rightOverlay, + validationMode, + validationErrorMessages, + errorIcon, + disabled, + pending, + error, + statusPosition, + hideIconOnStatus, + disableOnPending, + scrollValidationErrorsIntoView, + statusClassName, + validationErrorsClassName, + label, + required, + requiredIndicator, + labelClassName, + labelId, + displaySize, + ref, + ...nativeProps + } = props; + const globals = useArmstrongConfig({ + validationMode, + hideInputErrorIconOnStatus: hideIconOnStatus, + disableControlOnPending: disableOnPending, + requiredIndicator, + scrollValidationErrorsIntoView, + inputStatusPosition: statusPosition, + validationErrorIcon: errorIcon, + inputDisplaySize: displaySize, + }); + + const shouldShowValidationErrorsList = globals.validationMode === 'both' || globals.validationMode === 'message'; + const shouldShowErrorIcon = + (!!validationErrorMessages?.length && (globals.validationMode === 'both' || globals.validationMode === 'icon')) || + error; + + const showLeftOverlay = + leftOverlay && + (globals.inputStatusPosition !== 'left' || + !globals.hideInputErrorIconOnStatus || + (!pending && !shouldShowErrorIcon)); + + const showRightOverlay = + rightOverlay && + (globals.inputStatusPosition !== 'right' || + !globals.hideInputErrorIconOnStatus || + (!pending && !shouldShowErrorIcon)); + + return ( + <> +
+ {label && ( + + )} +
+ + <> + {showLeftOverlay &&
{leftOverlay}
} + {children} + {showRightOverlay &&
{rightOverlay}
} + +
- - ); - } -); + {!!validationErrorMessages?.length && shouldShowValidationErrorsList && ( + + )} +
+ + ); +}; InputWrapper.displayName = 'InputWrapper'; diff --git a/module/src/components/label/label.component.tsx b/module/src/components/label/label.component.tsx index e677220d..c51c09ef 100644 --- a/module/src/components/label/label.component.tsx +++ b/module/src/components/label/label.component.tsx @@ -20,24 +20,23 @@ export interface ILabelProps extends Omit, } /** Render a status icon which can either be pending or errored */ -export const Label = React.forwardRef( - ({ required, className, requiredIndicator, children, displaySize, ...nativeProps }, ref) => { - const globals = useArmstrongConfig({ - inputDisplaySize: displaySize, - requiredIndicator, - }); - return ( - - {children} - {required &&  {globals.requiredIndicator}} - - ); - } -); +export const Label = (props: React.PropsWithChildren }>) => { + const { required, className, requiredIndicator, children, displaySize, ref, ...nativeProps } = props; + const globals = useArmstrongConfig({ + inputDisplaySize: displaySize, + requiredIndicator, + }); + return ( + + {children} + {required &&  {globals.requiredIndicator}} + + ); +}; Label.displayName = 'Label'; diff --git a/module/src/components/progressBar/progressBar.component.tsx b/module/src/components/progressBar/progressBar.component.tsx index a6771ff4..c0622e6f 100644 --- a/module/src/components/progressBar/progressBar.component.tsx +++ b/module/src/components/progressBar/progressBar.component.tsx @@ -13,18 +13,17 @@ export interface IProgressBarProps extends React.HTMLAttributes indicatorClassName?: string; } -export const ProgressBar = React.forwardRef( - ({ progress, className, indicatorClassName, ...props }, ref) => { - const style = { - '--arm-progress-bar-value': `${clamp(progress ?? 0, 0, 100)}%`, - } as React.CSSProperties; +export const ProgressBar = (props: IProgressBarProps & { ref?: React.Ref }) => { + const { progress, className, indicatorClassName, ref, ...restProps } = props; + const style = { + '--arm-progress-bar-value': `${clamp(progress ?? 0, 0, 100)}%`, + } as React.CSSProperties; - return ( - - - - ); - } -); + return ( + + + + ); +}; ProgressBar.displayName = 'ProgressBar'; diff --git a/module/src/components/radioGroup/radioGroup.component.tsx b/module/src/components/radioGroup/radioGroup.component.tsx index ec045a49..83cc7c28 100644 --- a/module/src/components/radioGroup/radioGroup.component.tsx +++ b/module/src/components/radioGroup/radioGroup.component.tsx @@ -56,8 +56,8 @@ export interface IRadioGroupProps /** show an error state icon on the component (will be true automatically if validationErrorMessages are passed in or errors are in the binder) */ error?: boolean; - /** (Optional) A custom JSX.Element for the checked indicator. */ - customIndicator?: JSX.Element; + /** (Optional) A custom React.ReactElement for the checked indicator. */ + customIndicator?: React.ReactElement; /** which size variant to use */ displaySize?: DisplaySize; @@ -85,138 +85,129 @@ export interface IRadioGroupProps } /** Render a list of radio inputs which binds to a single string */ -export const RadioGroup = React.forwardRef>( - ( - { - bind, - options, - className, - value, - errorIcon, - validationMode, - validationErrorMessages, - onChange, - customIndicator, - error, - displaySize, - label, - labelClassName, - required, - disabled, - scrollValidationErrorsIntoView, - requiredIndicator, - displayMode, - validationErrorsClassName, - labelId, - autoValidate, - ...nativeProps - }, - ref - ) => { - const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { - value, - validationErrorMessages, - validationErrorIcon: errorIcon, - validationMode, - onChange, - autoValidate, - }); - - const globals = useArmstrongConfig({ - validationMode: bindConfig.validationMode, - requiredIndicator, - scrollValidationErrorsIntoView, - validationErrorIcon: bindConfig.validationErrorIcon, - inputDisplaySize: displaySize, - autoValidate: bindConfig.autoValidate, - }); - - useDidUpdateEffect(() => { - if (globals.autoValidate) { - bindConfig.validate(); - } - bindConfig.setTouched(true); - }, [boundValue]); - - return ( - <> - {label && ( - - )} - - setBoundValue( - typeof boundValue === 'number' && newValue !== null && newValue !== undefined ? +newValue : newValue - ) - } - disabled={disabled} - ref={ref} - {...nativeProps} +export const RadioGroup = (props: IRadioGroupProps & { ref?: React.Ref }) => { + const { + bind, + options, + className, + value, + errorIcon, + validationMode, + validationErrorMessages, + onChange, + customIndicator, + error, + displaySize, + label, + labelClassName, + required, + disabled, + scrollValidationErrorsIntoView, + requiredIndicator, + displayMode, + validationErrorsClassName, + labelId, + autoValidate, + ref, + ...nativeProps + } = props; + const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { + value, + validationErrorMessages, + validationErrorIcon: errorIcon, + validationMode, + onChange, + autoValidate, + }); + + const globals = useArmstrongConfig({ + validationMode: bindConfig.validationMode, + requiredIndicator, + scrollValidationErrorsIntoView, + validationErrorIcon: bindConfig.validationErrorIcon, + inputDisplaySize: displaySize, + autoValidate: bindConfig.autoValidate, + }); + + useDidUpdateEffect(() => { + if (globals.autoValidate) { + bindConfig.validate(); + } + bindConfig.setTouched(true); + }, [boundValue]); + + return ( + <> + {label && ( + + {bindConfig.shouldShowValidationErrorMessage && !!bindConfig.validationErrorMessages.length && ( + + )} + + ); }; + RadioGroup.displayName = 'RadioGroup'; diff --git a/module/src/components/rangeInput/rangeInput.component.tsx b/module/src/components/rangeInput/rangeInput.component.tsx index 57901723..27fc95fd 100644 --- a/module/src/components/rangeInput/rangeInput.component.tsx +++ b/module/src/components/rangeInput/rangeInput.component.tsx @@ -22,8 +22,8 @@ export interface IRangeInputProps> /** (Optional) A TData value representing the value of the input. */ value?: TData; - /** (Optional) A custom JSX.Element for the thumb of the slider. */ - customThumb?: JSX.Element; + /** (Optional) A custom React.ReactElement for the thumb of the slider. */ + customThumb?: React.ReactElement; /** (Optional) A boolean flag to disable the checkbox input. */ disabled?: boolean; @@ -74,34 +74,32 @@ export interface IRangeInputProps> autoValidate?: boolean; } -export const RangeInput = React.forwardRef>>( - ( - { - bind, - className, - disabled, - onChange, - label, - labelClassName, - labelId, - scrollValidationErrorsIntoView, - statusClassName, - validationErrorsClassName, - validationErrorMessages, - displaySize, - min, - max, - value, - validationMode, - step, - required, - customThumb, - requiredIndicator, - autoValidate, - ...nativeProps - }, - ref - ) => { +export const RangeInput = >(props: IRangeInputProps & { ref?: React.Ref }) => { + const { + bind, + className, + disabled, + onChange, + label, + labelClassName, + labelId, + scrollValidationErrorsIntoView, + statusClassName, + validationErrorsClassName, + validationErrorMessages, + displaySize, + min, + max, + value, + validationMode, + step, + required, + customThumb, + requiredIndicator, + autoValidate, + ref, + ...nativeProps + } = props; const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { value, onChange, @@ -191,10 +189,6 @@ export const RangeInput = React.forwardRef ); - } -) as (>( - props: ArmstrongFCProps, HTMLInputElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>>; +}; RangeInput.displayName = 'RangeInput'; diff --git a/module/src/components/rating/rating.component.tsx b/module/src/components/rating/rating.component.tsx index 43c47e02..bb64f859 100644 --- a/module/src/components/rating/rating.component.tsx +++ b/module/src/components/rating/rating.component.tsx @@ -32,14 +32,14 @@ export interface IRatingPartProps readOnly?: boolean; } -const RatingPart = React.forwardRef( - ({ index, value, onSelectPart, filledIcon, emptyIcon, step, mode, readOnly, disabled }, ref) => { - const steps = Math.floor(1 / (step || 1)); +const RatingPart = (props: IRatingPartProps & { ref?: React.Ref }) => { + const { index, value, onSelectPart, filledIcon, emptyIcon, step, mode, readOnly, disabled, ref } = props; + const steps = Math.floor(1 / (step || 1)); - const isFilled = value && value >= index + 1; - const isPart = value && value < index + 1 && value > index; + const isFilled = value && value >= index + 1; + const isPart = value && value < index + 1 && value > index; - return ( + return (
( )}
); - } -); +}; RatingPart.displayName = 'RatingPart'; -export type RatingIconDefinition = JSX.Element | ((index: number) => JSX.Element); +export type RatingIconDefinition = React.ReactElement | ((index: number) => React.ReactElement); export interface IRatingProps> extends Omit, HTMLDivElement>, 'onChange'>, @@ -171,41 +170,39 @@ export interface IRatingProps> autoValidate?: boolean; } -export const Rating = React.forwardRef>>( - ( - { - bind, - value, - onValueChange, - filledIcon, - emptyIcon, - maximum, - className, - validationErrorMessages, - validationMode, - errorIcon, - scrollValidationErrorsIntoView, - step, - error, - statusPosition, - pending, - leftOverlay, - rightOverlay, - mode, - disabled, - statusClassName, - validationErrorsClassName, - labelClassName, - labelId, - label, - required, - requiredIndicator, - displaySize, - autoValidate, - ...htmlProps - }, - ref - ) => { +export const Rating = >(props: IRatingProps & { ref?: React.Ref }) => { + const { + bind, + value, + onValueChange, + filledIcon, + emptyIcon, + maximum, + className, + validationErrorMessages, + validationMode, + errorIcon, + scrollValidationErrorsIntoView, + step, + error, + statusPosition, + pending, + leftOverlay, + rightOverlay, + mode, + disabled, + statusClassName, + validationErrorsClassName, + labelClassName, + labelId, + label, + required, + requiredIndicator, + displaySize, + autoValidate, + ref, + ...htmlProps + } = props; const [boundValue, setBoundValue, bindConfig] = useBindingState(bind, { value, onChange: onValueChange, @@ -311,20 +308,7 @@ export const Rating = React.forwardRef ); - } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as (>( - props: ArmstrongVFCProps, HTMLDivElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>>; - -Rating.defaultProps = { - maximum: 5, - filledIcon: , - emptyIcon: , - step: 1, - mode: 'buttons', }; + Rating.displayName = 'Rating'; diff --git a/module/src/components/rating/rating.utils.spec.ts b/module/src/components/rating/rating.utils.spec.ts index 20cf9fc2..c0d8ae1d 100644 --- a/module/src/components/rating/rating.utils.spec.ts +++ b/module/src/components/rating/rating.utils.spec.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { iconJsxFromDefinition } from './rating.utils'; -const iconFn1 = (index: number): JSX.Element => React.createElement('span', { key: index }, 'Icon 1'); +const iconFn1 = (index: number): React.ReactElement => React.createElement('span', { key: index }, 'Icon 1'); describe('iconJsxFromDefinition', () => { it('should convert a function icon definition to JSX element', () => { diff --git a/module/src/components/rating/rating.utils.ts b/module/src/components/rating/rating.utils.ts index f24976ad..5887bf4d 100644 --- a/module/src/components/rating/rating.utils.ts +++ b/module/src/components/rating/rating.utils.ts @@ -1,13 +1,13 @@ import type { RatingIconDefinition } from './rating.component'; /** - * Converts a RatingIconDefinition to a JSX.Element. + * Converts a RatingIconDefinition to a React.ReactElement. * * @param {RatingIconDefinition} icon - The icon definition to convert. * @param {number} index - The index of the item in the rating array. - * @returns {JSX.Element} The JSX element representing the icon. + * @returns {React.ReactElement} The JSX element representing the icon. */ -export const iconJsxFromDefinition = (icon: RatingIconDefinition, index: number): JSX.Element => { +export const iconJsxFromDefinition = (icon: RatingIconDefinition, index: number): React.ReactElement => { if (typeof icon === 'function') { return icon(index); } diff --git a/module/src/components/select/select.component.tsx b/module/src/components/select/select.component.tsx index f80c91b4..6dba037a 100644 --- a/module/src/components/select/select.component.tsx +++ b/module/src/components/select/select.component.tsx @@ -129,13 +129,13 @@ export interface ISingleSelectProps position?: 'auto' | 'bottom' | 'top'; /** overrides the dropdown icon in the input */ - dropdownIcon?: JSX.Element; + dropdownIcon?: React.ReactElement; /** overrides the loading icon in the input */ - loadingIcon?: JSX.Element; + loadingIcon?: React.ReactElement; /** overrides the selected icon in the input */ - selectedIcon?: JSX.Element; + selectedIcon?: React.ReactElement; /** close the select menu when the user selects an option. Set to true as default */ closeMenuOnSelect?: boolean; @@ -259,65 +259,58 @@ export interface IMultiSelectProps * - Not exported because single and multi are exported separately */ -const ReactSelectComponent = React.forwardRef< - ReactSelectRef, - (ISingleSelectProps | IMultiSelectProps) & { - multi: boolean; - } ->( - ( - { - className, - bind, - options, - placeholder, - validationMode, - validationErrorMessages: errorMessages, - errorIcon, - ariaLabel, - currentValue, - onSelectOption, - getOptionValue, - clearable, - disabled, - searchable, - dropdownIcon, - loadingIcon, - selectedIcon, - position, - formatOptionLabel, - closeMenuOnSelect, - displaySize, - label, - required, - requiredIndicator, - scrollValidationErrorsIntoView, - statusPosition, - pending, - multi, - allowCreate, - createText, - onOptionCreated, - labelId, - labelClassName, - validationErrorsClassName, - statusClassName, - disableOnPending, - hideIconOnStatus, - leftOverlay, - autoValidate, - onInputChange, - inputValue, - caseSensitive, - filterOption, - isInModal, - inModalZIndex, - inModalPortalTarget, - noOptionsMessage, - ...nativeProps - }, - ref - ) => { +const ReactSelectComponent = (props: ((ISingleSelectProps | IMultiSelectProps) & { multi: boolean }) & { ref?: React.Ref> }) => { + const { + className, + bind, + options, + placeholder, + validationMode, + validationErrorMessages: errorMessages, + errorIcon, + ariaLabel, + currentValue, + onSelectOption, + getOptionValue, + clearable, + disabled, + searchable, + dropdownIcon, + loadingIcon, + selectedIcon, + position, + formatOptionLabel, + closeMenuOnSelect, + displaySize, + label, + required, + requiredIndicator, + scrollValidationErrorsIntoView, + statusPosition, + pending, + multi, + allowCreate, + createText, + onOptionCreated, + labelId, + labelClassName, + validationErrorsClassName, + statusClassName, + disableOnPending, + hideIconOnStatus, + leftOverlay, + autoValidate, + onInputChange, + inputValue, + caseSensitive, + filterOption, + isInModal, + inModalZIndex, + inModalPortalTarget, + noOptionsMessage, + ref, + ...nativeProps + } = props; const internalRef = React.useRef>(null); React.useImperativeHandle(ref, () => internalRef.current as ReactSelectRef, [internalRef]); @@ -573,56 +566,46 @@ const ReactSelectComponent = React.forwardRef< )}
); - } -); +}; ReactSelectComponent.displayName = 'ReactSelect'; -ReactSelectComponent.defaultProps = { - clearable: true, - searchable: true, - selectedIcon: , - dropdownIcon: , - createText: 'Create:', -}; /** * Native select export */ -export const NativeSelect = React.forwardRef>( - ( - { - validationMode, - requiredIndicator, - scrollValidationErrorsIntoView, - errorIcon, - validationErrorMessages, - currentValue, - onSelectOption, - bind, - className, - displaySize, - label, - required, - options, - onChange, - placeholderOption, - placeholderOptionEnabled, - disabled, - statusPosition, - dropdownIcon, - labelId, - labelClassName, - validationErrorsClassName, - statusClassName, - hideIconOnStatus, - leftOverlay, - autoValidate, - ...nativeProps - }, - ref - ) => { +export const NativeSelect = (props: INativeSelectProps & { ref?: React.Ref }) => { + const { + validationMode, + requiredIndicator, + scrollValidationErrorsIntoView, + errorIcon, + validationErrorMessages, + currentValue, + onSelectOption, + bind, + className, + displaySize, + label, + required, + options, + onChange, + placeholderOption, + placeholderOptionEnabled, + disabled, + statusPosition, + dropdownIcon, + labelId, + labelClassName, + validationErrorsClassName, + statusClassName, + hideIconOnStatus, + leftOverlay, + autoValidate, + ref, + ...nativeProps + } = props; const internalRef = React.useRef(null); React.useImperativeHandle(ref, () => internalRef.current as HTMLSelectElement, [internalRef]); @@ -747,28 +730,19 @@ export const NativeSelect = React.forwardRef ); - } -) as (( - props: ArmstrongVFCProps, HTMLSelectElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>; +}; NativeSelect.displayName = 'NativeSelect'; -NativeSelect.defaultProps = { - dropdownIcon: , -}; /** * Single select export */ -export const Select = React.forwardRef, ISingleSelectProps>((props, ref) => { - return ; -}) as (( - props: ArmstrongVFCProps, ReactSelectRef> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>; +export const Select = (props: ISingleSelectProps & { ref?: React.Ref> }) => { + const { ref, ...otherProps } = props; + return ; +}; Select.displayName = 'Select'; @@ -776,13 +750,9 @@ Select.displayName = 'Select'; * Multi select export */ -export const MultiSelect = React.forwardRef, IMultiSelectProps>( - (props, ref) => { - return ; - } -) as (( - props: ArmstrongVFCProps, ReactSelectRef> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>; +export const MultiSelect = (props: IMultiSelectProps & { ref?: React.Ref> }) => { + const { ref, ...otherProps } = props; + return ; +}; MultiSelect.displayName = 'MultiSelect'; diff --git a/module/src/components/spinner/spinner.component.tsx b/module/src/components/spinner/spinner.component.tsx index ffbfb75f..ad3d1531 100644 --- a/module/src/components/spinner/spinner.component.tsx +++ b/module/src/components/spinner/spinner.component.tsx @@ -7,7 +7,7 @@ import './spinner.theme.css'; export interface ISpinnerProps extends Omit, 'label'> { /** icon definition for icon to spin in middle of div, can be overridden using children */ - icon?: JSX.Element; + icon?: React.ReactElement; /** should the spinner wrapper fill the container, meaning the icon is centred */ fillContainer?: boolean; @@ -17,32 +17,28 @@ export interface ISpinnerProps extends Omit, 'la } /** Renders a spinner centred in the div that's being wrapped */ -export const Spinner = React.forwardRef>( - ({ children, className, icon, fillContainer, label, ...HTMLProps }, ref) => { - const { spinnerIcon } = useArmstrongConfig({ spinnerIcon: icon }); - return ( -
-
{children || spinnerIcon}
- {label && ( -
- {label} -
- )} -
- ); - } -); +export const Spinner = (props: React.PropsWithChildren }>) => { + const { children, className, icon, fillContainer, label, ref, ...HTMLProps } = props; + const { spinnerIcon } = useArmstrongConfig({ spinnerIcon: icon }); + return ( +
+
{children || spinnerIcon}
+ {label && ( +
+ {label} +
+ )} +
+ ); +}; Spinner.displayName = 'Spinner'; -Spinner.defaultProps = { - fillContainer: true, -}; diff --git a/module/src/components/status/status.component.tsx b/module/src/components/status/status.component.tsx index 61d07f97..9ef16c49 100644 --- a/module/src/components/status/status.component.tsx +++ b/module/src/components/status/status.component.tsx @@ -14,41 +14,40 @@ export interface IStatusProps { error?: boolean; /** the icon to use for the error */ - errorIcon?: JSX.Element; + errorIcon?: React.ReactElement; /** the icon to use for the spinner */ - spinnerIcon?: JSX.Element; + spinnerIcon?: React.ReactElement; /** an optional CSS className for the rendered status */ className?: string; } /** Render a status icon which can either be pending or errored */ -export const Status = React.forwardRef( - ({ pending, error, errorIcon, spinnerIcon, className, ...rest }, ref) => { - const globals = useArmstrongConfig({ - validationErrorIcon: errorIcon, - spinnerIcon, - }); - - if (!error && !pending) { - return null; - } - return ( -
- {error && !pending && globals.validationErrorIcon} - {pending && } -
- ); +export const Status = (props: IStatusProps & { ref?: React.Ref }) => { + const { pending, error, errorIcon, spinnerIcon, className, ref, ...rest } = props; + const globals = useArmstrongConfig({ + validationErrorIcon: errorIcon, + spinnerIcon, + }); + + if (!error && !pending) { + return null; } -); + return ( +
+ {error && !pending && globals.validationErrorIcon} + {pending && } +
+ ); +}; Status.displayName = 'Status'; diff --git a/module/src/components/statusWrapper/statusWrapper.component.tsx b/module/src/components/statusWrapper/statusWrapper.component.tsx index 6feb80c9..c41db696 100644 --- a/module/src/components/statusWrapper/statusWrapper.component.tsx +++ b/module/src/components/statusWrapper/statusWrapper.component.tsx @@ -18,7 +18,7 @@ export interface IStatusWrapperProps { validationMode?: FormValidationMode; /** the icon to use for validation errors */ - errorIcon?: JSX.Element; + errorIcon?: React.ReactElement; /** an optional CSS className for the rendered status */ className?: string; diff --git a/module/src/components/switch/switch.component.tsx b/module/src/components/switch/switch.component.tsx index fc8ae19e..c1ec6af4 100644 --- a/module/src/components/switch/switch.component.tsx +++ b/module/src/components/switch/switch.component.tsx @@ -60,30 +60,28 @@ export interface ISwitchProps> autoValidate?: boolean; } -export const Switch = React.forwardRef>>( - ( - { - bind, - checked, - onCheckedChange, - defaultChecked, - disabled, - className, - labelClassName, - label, - validationErrorMessages, - validationErrorsClassName, - scrollValidationErrorsIntoView, - validationMode, - displaySize, - labelId, - required, - requiredIndicator, - autoValidate, - ...nativeProps - }, - ref - ) => { +export const Switch = (props: React.PropsWithChildren> & { ref?: React.Ref }>) => { + const { + bind, + checked, + onCheckedChange, + defaultChecked, + disabled, + className, + labelClassName, + label, + validationErrorMessages, + validationErrorsClassName, + scrollValidationErrorsIntoView, + validationMode, + displaySize, + labelId, + required, + requiredIndicator, + autoValidate, + ref, + ...nativeProps + } = props; const generatedId = React.useId(); const id = nativeProps.id ?? generatedId; @@ -157,12 +155,11 @@ export const Switch = React.forwardRef ); - } - // type assertion to ensure generic works with RefForwarded component - // DO NOT CHANGE TYPE WITHOUT CHANGING THIS, FIND TYPE BY INSPECTING React.forwardRef -) as (>( - props: ArmstrongFCProps, HTMLInputElement> -) => ArmstrongFCReturn) & - ArmstrongFCExtensions>>; +}; + +// Type assertion for generic support - updated for React 19 ref as prop pattern +(Switch as any) = (Switch as (>( + props: React.PropsWithChildren & { ref?: React.Ref }> +) => React.ReactElement) & ArmstrongFCExtensions>>); Switch.displayName = 'Switch'; diff --git a/module/src/components/textArea/textArea.component.tsx b/module/src/components/textArea/textArea.component.tsx index 4350d997..fee8828d 100644 --- a/module/src/components/textArea/textArea.component.tsx +++ b/module/src/components/textArea/textArea.component.tsx @@ -27,21 +27,20 @@ interface IDelayedTextAreaBaseProps extends NativeTextAreaProps { value?: TValue; } -const DebounceTextAreaBase = React.forwardRef>( - ({ milliseconds, value, onValueChange, onChange, ...nativeProps }, ref) => { - const [actualValue, setActualValue] = useDebounce(milliseconds, value, onValueChange); - - const onChangeEvent = React.useCallback( - (e: React.ChangeEvent) => { - setActualValue(e.currentTarget.value); - onChange?.(e); - }, - [setActualValue, onChange] - ); +const DebounceTextAreaBase = (props: React.PropsWithChildren & { ref?: React.Ref }>) => { + const { milliseconds, value, onValueChange, onChange, ref, ...nativeProps } = props; + const [actualValue, setActualValue] = useDebounce(milliseconds, value, onValueChange); + + const onChangeEvent = React.useCallback( + (e: React.ChangeEvent) => { + setActualValue(e.currentTarget.value); + onChange?.(e); + }, + [setActualValue, onChange] + ); - return