From c67d2c4c8eeb0a379fd58f62bad72cf8cea1b1c2 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Mon, 28 Oct 2024 15:38:07 +0000 Subject: [PATCH 01/30] setup local npm package environment --- .gitignore | 3 +- package-lock.json | 1131 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1133 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 4c3e209..e4eb1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ yarn-debug.log* yarn-error.log* # vercel -.vercel \ No newline at end of file +.vercel +/.idea diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8091516 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1131 @@ +{ + "name": "onborda", + "version": "1.2.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "onborda", + "version": "1.2.4", + "license": "MIT", + "devDependencies": { + "@types/react": "^18.2.63", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@radix-ui/react-portal": ">=1.1.1", + "framer-motion": ">=11", + "next": ">=13", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.1.tgz", + "integrity": "sha512-lc4HeDUKO9gxxlM5G2knTRifqhsY6yYpwuHspBZdboZe0Gp+rZHBNNSIjmQKDJIdRXiXGyVnSD6gafrbQPvILQ==", + "license": "MIT", + "peer": true + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.1.tgz", + "integrity": "sha512-C9k/Xv4sxkQRTA37Z6MzNq3Yb1BJMmSqjmwowoWEpbXTkAdfOwnoKOpAb71ItSzoA26yUTIo6ZhN8rKGu4ExQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.1.tgz", + "integrity": "sha512-uHl13HXOuq1G7ovWFxCACDJHTSDVbn/sbLv8V1p+7KIvTrYQ5HNoSmKBdYeEKRRCbEmd+OohOgg9YOp8Ux3MBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.1.tgz", + "integrity": "sha512-LvyhvxHOihFTEIbb35KxOc3q8w8G4xAAAH/AQnsYDEnOvwawjL2eawsB59AX02ki6LJdgDaHoTEnC54Gw+82xw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.1.tgz", + "integrity": "sha512-vFmCGUFNyk/A5/BYcQNhAQqPIw01RJaK6dRO+ZEhz0DncoW+hJW1kZ8aH2UvTX27zPq3m85zN5waMSbZEmANcQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.1.tgz", + "integrity": "sha512-5by7IYq0NCF8rouz6Qg9T97jYU68kaClHPfGpQG2lCZpSYHtSPQF1kjnqBTd34RIqPKMbCa4DqCufirgr8HM5w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.1.tgz", + "integrity": "sha512-lmYr6H3JyDNBJLzklGXLfbehU3ay78a+b6UmBGlHls4xhDXBNZfgb0aI67sflrX+cGBnv1LgmWzFlYrAYxS1Qw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.1.tgz", + "integrity": "sha512-DS8wQtl6diAj0eZTdH0sefykm4iXMbHT4MOvLwqZiIkeezKpkgPFcEdFlz3vKvXa2R/2UEgMh48z1nEpNhjeOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.1.tgz", + "integrity": "sha512-4Ho2ggvDdMKlZ/0e9HNdZ9ngeaBwtc+2VS5oCeqrbXqOgutX6I4U2X/42VBw0o+M5evn4/7v3zKgGHo+9v/VjA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "peer": true, + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001673", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001673.tgz", + "integrity": "sha512-WTrjUCSMp3LYX0nE12ECkV0a+e6LC85E0Auz75555/qr78Oc8YWhEPNfDd6SHdtlCMSzqtuXY0uyEMNRcsKpKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT", + "peer": true + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/framer-motion": { + "version": "11.11.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.10.tgz", + "integrity": "sha512-061Bt1jL/vIm+diYIiA4dP/Yld7vD47ROextS7ESBW5hr4wQFhxB5D5T5zAc3c/5me3cOa+iO5LqhA38WDln/A==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/next/-/next-15.0.1.tgz", + "integrity": "sha512-PSkFkr/w7UnFWm+EP8y/QpHrJXMqpZzAXpergB/EqLPOh4SGPJXv1wj4mslr2hUZBAS9pX7/9YLIdxTv6fwytw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@next/env": "15.0.1", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.13", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.18.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.0.1", + "@next/swc-darwin-x64": "15.0.1", + "@next/swc-linux-arm64-gnu": "15.0.1", + "@next/swc-linux-arm64-musl": "15.0.1", + "@next/swc-linux-x64-gnu": "15.0.1", + "@next/swc-linux-x64-musl": "15.0.1", + "@next/swc-win32-arm64-msvc": "15.0.1", + "@next/swc-win32-x64-msvc": "15.0.1", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-69d4b800-20241021", + "react-dom": "^18.2.0 || 19.0.0-rc-69d4b800-20241021", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC", + "peer": true + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "peer": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "peer": true, + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD", + "peer": true + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} From 81a99e1df34dc8ce0dbb457b4f2170003f2d9046 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 29 Oct 2024 11:48:02 +0000 Subject: [PATCH 02/30] Added nodemon for --watch on 'npm run dev' --- README.md | 46 +++++- package-lock.json | 364 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 8 +- 3 files changed, 410 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c5fe7ac..d5ea198 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Onborda is a lightweight onboarding flow that utilises [framer-motion](https://w ## Getting started ```bash # npm -npm i onborda +npm i alexc-harrap/onborda # pnpm -pnpm add onborda +pnpm add alexc-harrap/onborda # yarn -yarn add onborda +yarn add alexc-harrap/onborda ``` ### Global `layout.tsx` @@ -174,3 +174,43 @@ Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" {children} ``` +### Contribution +To setup the project locally, clone the repository and run the following commands: +```bash +# Install dependencies +npm install +``` + +To test the local library in a local Next.js project, run the following command in this project to setup npm link +```bash +# Create a symlink to the package +npm link +``` + +Then in your Next.js project run the following command to link the package +```bash +# Link the package +npm link onborda +``` + +To unlink the package, run the following command in your Next.js project +```bash +# Unlink the package +npm unlink onborda +``` + +If you already have the published package installed in your project, the symlink will take precedence over the published package. Ensure to push changes to this repo before pushing changes to your project. + +Now you can make changes to the package and see them reflected in your Next.js project. The package must be build after every change to see the changes in your Next.js project. +```bash +# Build the package +npm run build +``` + +To save rebuilding the package after every change, you can run the following command in this project to watch for changes and rebuild the package, which will in turn update the symlink in your Next.js project. +```bash +# Watch for changes and rebuild the package +npm run dev +# Cancel the watch process +^c +``` diff --git a/package-lock.json b/package-lock.json index 8091516..26fa357 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "devDependencies": { "@types/react": "^18.2.63", + "nodemon": "^3.1.7", "typescript": "^5.3.3" }, "peerDependencies": { @@ -689,6 +690,64 @@ "csstype": "^3.0.2" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -722,6 +781,31 @@ "license": "CC-BY-4.0", "peer": true }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -778,6 +862,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -785,6 +876,24 @@ "devOptional": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -796,6 +905,19 @@ "node": ">=8" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/framer-motion": { "version": "11.11.10", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.10.tgz", @@ -822,6 +944,51 @@ } } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -830,6 +997,52 @@ "optional": true, "peer": true }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -850,6 +1063,26 @@ "loose-envify": "cli.js" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -924,6 +1157,45 @@ } } }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -931,6 +1203,19 @@ "license": "ISC", "peer": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -960,6 +1245,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -987,6 +1279,19 @@ "react": "^18.3.1" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1001,9 +1306,8 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "devOptional": true, "license": "ISC", - "optional": true, - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -1063,6 +1367,19 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1106,6 +1423,42 @@ } } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tslib": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", @@ -1126,6 +1479,13 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 735ee31..0ea1b73 100644 --- a/package.json +++ b/package.json @@ -25,17 +25,19 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsc --project tsconfig.json", - "start": "npm run build && node dist/index.js" + "start": "npm run build && node dist/index.js", + "dev": "nodemon --watch \"src/**/*\" --ext \"ts,tsx,js,json\" --exec \"npm run build\"" }, "peerDependencies": { + "@radix-ui/react-portal": ">=1.1.1", "framer-motion": ">=11", "next": ">=13", "react": ">=18", - "react-dom": ">=18", - "@radix-ui/react-portal": ">=1.1.1" + "react-dom": ">=18" }, "devDependencies": { "@types/react": "^18.2.63", + "nodemon": "^3.1.7", "typescript": "^5.3.3" } } From 72dcd426eb6b05b6040d59111a564a129855a164 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 29 Oct 2024 13:17:46 +0000 Subject: [PATCH 03/30] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5ea198..d7c55a1 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Onborda is a lightweight onboarding flow that utilises [framer-motion](https://w ## Getting started ```bash # npm -npm i alexc-harrap/onborda +npm i Libresoft-UK/onborda # pnpm -pnpm add alexc-harrap/onborda +pnpm add Libresoft-UK/onborda # yarn -yarn add alexc-harrap/onborda +yarn add Libresoft-UK/onborda ``` ### Global `layout.tsx` From 934dbe966766c87d4cc7b22c384c143ad6e2d061 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 29 Oct 2024 13:27:16 +0000 Subject: [PATCH 04/30] Added optional TourComponent Its now possible to have a second element on the page while a tour is active, displaying information about the current Tour. --- dist/Onborda.js | 40 +++++------ dist/types/index.d.ts | 7 +- src/Onborda.tsx | 151 ++++++++++++++++++++++-------------------- src/types/index.ts | 8 +++ 4 files changed, 114 insertions(+), 92 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 489d5e6..ffd39aa 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -1,11 +1,11 @@ "use client"; -import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useState, useEffect, useRef } from "react"; import { useOnborda } from "./OnbordaContext"; import { motion, useInView } from "framer-motion"; import { useRouter } from "next/navigation"; import { Portal } from "@radix-ui/react-portal"; -const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, }) => { +const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, }) => { const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); const currentTourSteps = steps.find((tour) => tour.tour === currentTour)?.steps; const [elementToScroll, setElementToScroll] = useState(null); @@ -371,23 +371,23 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; const pointerPadOffset = pointerPadding / 2; const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; - return (_jsxs("div", { "data-name": "onborda-wrapper", className: "relative w-full", "data-onborda": "dev", children: [_jsx("div", { "data-name": "onborda-site", className: "block w-full", children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsx(Portal, { children: _jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 ", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[999]", style: { - boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, - borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, - }, initial: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, animate: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) }))] })); + return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-name": "onborda-wrapper", className: "relative w-full", "data-onborda": "dev", children: [_jsx("div", { "data-name": "onborda-site", className: "block w-full", children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsx(Portal, { children: _jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 ", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[998]", style: { + boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, + borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, + }, initial: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, animate: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) }))] }), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'absolute top-0 left-0 z-[999] w-full h-full pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); }; export default Onborda; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 8857579..04ad824 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -1,4 +1,3 @@ -/// import { Transition } from "framer-motion"; export interface OnbordaContextType { currentStep: number; @@ -32,6 +31,7 @@ export interface OnbordaProps { shadowOpacity?: string; cardTransition?: Transition; cardComponent?: React.ComponentType; + tourComponent?: React.ComponentType; } export interface CardComponentProps { step: Step; @@ -41,3 +41,8 @@ export interface CardComponentProps { prevStep: () => void; arrow: JSX.Element; } +export interface TourComponentProps { + currentTour: string | null; + currentStep: number; + steps: Step[]; +} diff --git a/src/Onborda.tsx b/src/Onborda.tsx index fa71fdb..bc1e35c 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -15,6 +15,7 @@ const Onborda: React.FC = ({ shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, + tourComponent: TourComponent, }) => { const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); @@ -430,79 +431,87 @@ const Onborda: React.FC = ({ const pointerPadOffset = pointerPadding / 2; const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; - return ( -
- {/* Container for the Website content */} -
- {children} -
- - {/* Onborda Overlay Step Content */} - {pointerPosition && isOnbordaVisible && CardComponent && ( - - +
- - {/* Card */} -
- } - /> + {/* Container for the Website content */} +
+ {children}
- - - - )} -
+ + {/* Onborda Overlay Step Content */} + {pointerPosition && isOnbordaVisible && CardComponent && ( + + + + {/* Card */} +
+ } + /> +
+
+
+
+ )} +
+
+ {TourComponent && currentTourSteps && ( +
+ +
+ )} +
+ ); }; diff --git a/src/types/index.ts b/src/types/index.ts index 3d82486..493b6ed 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -43,6 +43,7 @@ export interface OnbordaProps { shadowOpacity?: string; cardTransition?: Transition; cardComponent?: React.ComponentType; + tourComponent?: React.ComponentType; } // Custom Card @@ -54,3 +55,10 @@ export interface CardComponentProps { prevStep: () => void; arrow: JSX.Element; } + +// Tour Component +export interface TourComponentProps { + currentTour: string | null; + currentStep: number; + steps: Step[]; +} From 4614fe625baf5413fc5cf924511f56bf74f7336e Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 29 Oct 2024 16:48:31 +0000 Subject: [PATCH 05/30] Added interactable toggle to Step to control pointer events of step selector contents --- dist/Onborda.js | 51 ++++++++----- dist/types/index.d.ts | 1 + src/Onborda.tsx | 166 ++++++++++++++++++++++-------------------- src/types/index.ts | 1 + 4 files changed, 122 insertions(+), 97 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index ffd39aa..e37111f 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -1,5 +1,5 @@ "use client"; -import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useEffect, useRef } from "react"; import { useOnborda } from "./OnbordaContext"; import { motion, useInView } from "framer-motion"; @@ -28,6 +28,11 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" if (element) { setPointerPosition(getElementPosition(element)); currentElementRef.current = element; + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element; + htmlElement.style.pointerEvents = "auto"; + } setElementToScroll(element); const rect = element.getBoundingClientRect(); const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; @@ -37,6 +42,13 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" } } } + return () => { + // Disable pointer events on the element on cleanup + if (currentElementRef.current) { + const htmlElement = currentElementRef.current; + htmlElement.style.pointerEvents = ""; + } + }; }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); // - - // Helper function to get element position @@ -371,23 +383,24 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; const pointerPadOffset = pointerPadding / 2; const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; - return (_jsxs(_Fragment, { children: [_jsxs("div", { "data-name": "onborda-wrapper", className: "relative w-full", "data-onborda": "dev", children: [_jsx("div", { "data-name": "onborda-site", className: "block w-full", children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsx(Portal, { children: _jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 ", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[998]", style: { - boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, - borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, - }, initial: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, animate: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) }))] }), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'absolute top-0 left-0 z-[999] w-full h-full pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); + const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : ''; + return (_jsxs(_Fragment, { children: [_jsx("div", { "data-name": "onborda-site-wrapper", className: ` ${pointerEvents} `, children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsx(Portal, { children: _jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 pointer-events-none", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[998]", style: { + boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, + borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, + }, initial: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, animate: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'absolute top-0 left-0 z-[999] w-full h-full pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); }; export default Onborda; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 04ad824..db3d2f6 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -16,6 +16,7 @@ export interface Step { showControls?: boolean; pointerPadding?: number; pointerRadius?: number; + interactable?: boolean; nextRoute?: string; prevRoute?: string; } diff --git a/src/Onborda.tsx b/src/Onborda.tsx index bc1e35c..950bdfa 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -50,6 +50,13 @@ const Onborda: React.FC = ({ if (element) { setPointerPosition(getElementPosition(element)); currentElementRef.current = element; + + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element as HTMLElement; + htmlElement.style.pointerEvents = "auto"; + } + setElementToScroll(element); const rect = element.getBoundingClientRect(); @@ -62,6 +69,13 @@ const Onborda: React.FC = ({ } } } + return () => { + // Disable pointer events on the element on cleanup + if (currentElementRef.current) { + const htmlElement = currentElementRef.current as HTMLElement; + htmlElement.style.pointerEvents = ""; + } + } }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); // - - @@ -430,89 +444,85 @@ const Onborda: React.FC = ({ const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; const pointerPadOffset = pointerPadding / 2; const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; + const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : ''; return (<> -
- {/* Container for the Website content */} -
- {children} -
- {/* Onborda Overlay Step Content */} - {pointerPosition && isOnbordaVisible && CardComponent && ( - - - + {children} +
+ + {/* Onborda Overlay Step Content */} + {pointerPosition && isOnbordaVisible && CardComponent && ( + + + - {/* Card */} -
- } - /> -
-
-
-
- )} -
-
- {TourComponent && currentTourSteps && ( -
- -
- )} -
- - ); + : {} + } + transition={cardTransition} + > + {/* Card */} +
+ } + /> +
+ + + + )} + +
+ {TourComponent && currentTourSteps && ( +
+ +
+ )} +
+ ); }; export default Onborda; diff --git a/src/types/index.ts b/src/types/index.ts index 493b6ed..fb49958 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -22,6 +22,7 @@ export interface Step { showControls?: boolean; pointerPadding?: number; pointerRadius?: number; + interactable?: boolean; // Routing nextRoute?: string; prevRoute?: string; From 12ade6314d6b7adfdfb1dcbdb9b8457493eef2aa Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 29 Oct 2024 16:50:33 +0000 Subject: [PATCH 06/30] Fixed TourCard going off screen. tiur-wrapper now fixed to viewport --- dist/Onborda.js | 2 +- src/Onborda.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index e37111f..3722c21 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -401,6 +401,6 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'absolute top-0 left-0 z-[999] w-full h-full pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[999] w-screen h-screen pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); }; export default Onborda; diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 950bdfa..9492906 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -515,7 +515,7 @@ const Onborda: React.FC = ({ )} -
+
{TourComponent && currentTourSteps && (
From 89cbd5e16d874b4ea7a068c4b37008af7f5e7fa3 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Mon, 4 Nov 2024 13:28:36 +0000 Subject: [PATCH 07/30] Step.selector now optional. When no selector is provided, card and pointer is centered to the page and arrow is hidden for a generic modal step. --- dist/Onborda.js | 220 ++++++--- dist/types/index.d.ts | 2 +- src/Onborda.tsx | 1079 ++++++++++++++++++++++------------------- src/types/index.ts | 2 +- 4 files changed, 727 insertions(+), 576 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 3722c21..9091b13 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -1,6 +1,6 @@ "use client"; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; -import { useState, useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useOnborda } from "./OnbordaContext"; import { motion, useInView } from "framer-motion"; import { useRouter } from "next/navigation"; @@ -24,22 +24,36 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { - const element = document.querySelector(step.selector); - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - // Enable pointer events on the element - if (step.interactable) { - const htmlElement = element; - htmlElement.style.pointerEvents = "auto"; - } - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); + const selector = step.selector; + if (selector) { + const element = document.querySelector(selector); + if (element) { + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element; + htmlElement.style.pointerEvents = "auto"; + } + setElementToScroll(element); + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); + } } } + else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } } } return () => { @@ -67,20 +81,34 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" // Update pointerPosition when currentStep changes useEffect(() => { if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; + console.log("Onborda: Current Step Changed", step); if (step) { - const element = document.querySelector(step.selector); - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); + const selector = step.selector; + if (selector) { + const element = document.querySelector(selector); + if (element) { + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; + setElementToScroll(element); + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); + } } } + else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } } } }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); @@ -102,9 +130,23 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" if (currentTourSteps) { const step = currentTourSteps[currentStep]; if (step) { - const element = document.querySelector(step.selector); - if (element) { - setPointerPosition(getElementPosition(element)); + const selector = step.selector; + if (selector) { + const element = document.querySelector(selector); + if (element) { + setPointerPosition(getElementPosition(element)); + } + } + else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; } } } @@ -127,22 +169,28 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" if (route) { await router.push(route); const targetSelector = currentTourSteps[nextStepIndex].selector; - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); - // Stop observing after the element is found - observer.disconnect(); - } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); + if (targetSelector) { + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const element = document.querySelector(targetSelector); + if (element) { + // Once the element is found, update the step and scroll to the element + setCurrentStep(nextStepIndex); + scrollToElement(nextStepIndex); + // Stop observing after the element is found + observer.disconnect(); + } + }); + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + } + else { + // If no selector is provided, just update the step + setCurrentStep(nextStepIndex); + } } else { setCurrentStep(nextStepIndex); @@ -162,22 +210,28 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" if (route) { await router.push(route); const targetSelector = currentTourSteps[prevStepIndex].selector; - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); - // Stop observing after the element is found - observer.disconnect(); - } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); + if (targetSelector) { + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const element = document.querySelector(targetSelector); + if (element) { + // Once the element is found, update the step and scroll to the element + setCurrentStep(prevStepIndex); + scrollToElement(prevStepIndex); + // Stop observing after the element is found + observer.disconnect(); + } + }); + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + } + else { + // If no selector is provided, just update the step + setCurrentStep(prevStepIndex); + } } else { setCurrentStep(prevStepIndex); @@ -193,15 +247,29 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" // Scroll to the correct element when the step changes const scrollToElement = (stepIndex) => { if (currentTourSteps) { - const element = document.querySelector(currentTourSteps[stepIndex].selector); - if (element) { - const { top } = element.getBoundingClientRect(); - const isInViewport = top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); + const selector = currentTourSteps[stepIndex].selector; + if (selector) { + const element = document.querySelector(selector); + if (element) { + const { top } = element.getBoundingClientRect(); + const isInViewport = top >= -offset && top <= window.innerHeight + offset; + if (!isInViewport) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); + } + // Update pointer position after scrolling + setPointerPosition(getElementPosition(element)); } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); + } + else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; } } }; @@ -284,7 +352,14 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" marginRight: "25px", }; default: - return {}; // Default case if no side is specified + // Default case if no side is specified. Center the card to the screen + return { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', // Center the card + position: 'fixed', // Make sure it's positioned relative to the viewport + margin: '0', + }; } }; // - - @@ -369,7 +444,10 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" }; // - - // Card Arrow - const CardArrow = () => { + const CardArrow = ({ isVisible }) => { + if (!isVisible) { + return null; + } return (_jsx("svg", { viewBox: "0 0 54 54", "data-name": "onborda-arrow", className: "absolute w-6 h-6 origin-center", style: getArrowStyle(currentTourSteps?.[currentStep]?.side), children: _jsx("path", { id: "triangle", d: "M27 27L0 0V54L27 27Z", fill: "currentColor" }) })); }; // - - @@ -401,6 +479,6 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, {}) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[999] w-screen h-screen pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: !!(currentTourSteps?.[currentStep]?.selector) }) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[999] w-screen h-screen pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); }; export default Onborda; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index db3d2f6..c8e5edd 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -11,7 +11,7 @@ export interface Step { icon: React.ReactNode | string | null; title: string; content: React.ReactNode; - selector: string; + selector?: string; side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; showControls?: boolean; pointerPadding?: number; diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 9492906..4de1598 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -1,528 +1,601 @@ "use client"; -import React, { useState, useEffect, useRef } from "react"; -import { useOnborda } from "./OnbordaContext"; -import { motion, useInView } from "framer-motion"; -import { useRouter } from "next/navigation"; -import { Portal } from "@radix-ui/react-portal"; +import React, {useEffect, useRef, useState} from "react"; +import {useOnborda} from "./OnbordaContext"; +import {motion, useInView} from "framer-motion"; +import {useRouter} from "next/navigation"; +import {Portal} from "@radix-ui/react-portal"; // Types -import { OnbordaProps } from "./types"; +import {OnbordaProps} from "./types"; const Onborda: React.FC = ({ - children, - steps, - shadowRgb = "0, 0, 0", - shadowOpacity = "0.2", - cardTransition = { ease: "anticipate", duration: 0.6 }, - cardComponent: CardComponent, - tourComponent: TourComponent, + children, + steps, + shadowRgb = "0, 0, 0", + shadowOpacity = "0.2", + cardTransition = {ease: "anticipate", duration: 0.6}, + cardComponent: CardComponent, + tourComponent: TourComponent, }) => { - const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = - useOnborda(); - const currentTourSteps = steps.find( - (tour) => tour.tour === currentTour - )?.steps; - - const [elementToScroll, setElementToScroll] = useState(null); - const [pointerPosition, setPointerPosition] = useState<{ - x: number; - y: number; - width: number; - height: number; - } | null>(null); - const currentElementRef = useRef(null); - const observeRef = useRef(null); // Ref for the observer element - const isInView = useInView(observeRef); - const offset = 20; - - // - - - // Route Changes - const router = useRouter(); - - // - - - // Initialisze - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); - const step = currentTourSteps[currentStep]; - if (step) { - const element = document.querySelector(step.selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - - // Enable pointer events on the element - if (step.interactable) { - const htmlElement = element as HTMLElement; - htmlElement.style.pointerEvents = "auto"; - } - - setElementToScroll(element); - - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } + const {currentTour, currentStep, setCurrentStep, isOnbordaVisible} = + useOnborda(); + const currentTourSteps = steps.find( + (tour) => tour.tour === currentTour + )?.steps; + + const [elementToScroll, setElementToScroll] = useState(null); + const [pointerPosition, setPointerPosition] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + const currentElementRef = useRef(null); + const observeRef = useRef(null); // Ref for the observer element + const isInView = useInView(observeRef); + const offset = 20; + + // - - + // Route Changes + const router = useRouter(); + + // - - + // Initialisze + useEffect(() => { + if (isOnbordaVisible && currentTourSteps) { + console.log("Onborda: Current Step Changed"); + const step = currentTourSteps[currentStep]; + if (step) { + const selector = step.selector; + if (selector) { + const element = document.querySelector(selector) as Element | null; + if (element) { + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; + + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element as HTMLElement; + htmlElement.style.pointerEvents = "auto"; + } + + setElementToScroll(element); + + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = + rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({behavior: "smooth", block: "center"}); + } + } + }else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } + } } - } - } - return () => { - // Disable pointer events on the element on cleanup - if (currentElementRef.current) { - const htmlElement = currentElementRef.current as HTMLElement; - htmlElement.style.pointerEvents = ""; + return () => { + // Disable pointer events on the element on cleanup + if (currentElementRef.current) { + const htmlElement = currentElementRef.current as HTMLElement; + htmlElement.style.pointerEvents = ""; + } } - } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); - - // - - - // Helper function to get element position - const getElementPosition = (element: Element) => { - const { top, left, width, height } = element.getBoundingClientRect(); - const scrollTop = window.scrollY || document.documentElement.scrollTop; - const scrollLeft = window.scrollX || document.documentElement.scrollLeft; - return { - x: left + scrollLeft, - y: top + scrollTop, - width, - height, + }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); + + // - - + // Helper function to get element position + const getElementPosition = (element: Element) => { + const {top, left, width, height} = element.getBoundingClientRect(); + const scrollTop = window.scrollY || document.documentElement.scrollTop; + const scrollLeft = window.scrollX || document.documentElement.scrollLeft; + return { + x: left + scrollLeft, + y: top + scrollTop, + width, + height, + }; }; - }; - - // - - - // Update pointerPosition when currentStep changes - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); - const step = currentTourSteps[currentStep]; - if (step) { - const element = document.querySelector(step.selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } + + // - - + // Update pointerPosition when currentStep changes + useEffect(() => { + if (isOnbordaVisible && currentTourSteps) { + const step = currentTourSteps[currentStep]; + console.log("Onborda: Current Step Changed", step); + if (step) { + const selector = step.selector; + if (selector) { + const element = document.querySelector(selector) as Element | null; + if (element) { + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; + setElementToScroll(element); + + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = + rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({behavior: "smooth", block: "center"}); + } + } + }else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } + } } - } - } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); - - useEffect(() => { - if (elementToScroll && !isInView && isOnbordaVisible) { - console.log("Onborda: Element to Scroll Changed"); - const rect = elementToScroll.getBoundingClientRect(); - const isAbove = rect.top < 0; - elementToScroll.scrollIntoView({ - behavior: "smooth", - block: isAbove ? "center" : "center", - inline: "center", - }); - } - }, [elementToScroll, isInView, isOnbordaVisible]); - - // - - - // Update pointer position on window resize - const updatePointerPosition = () => { - if (currentTourSteps) { - const step = currentTourSteps[currentStep]; - if (step) { - const element = document.querySelector(step.selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); + }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); + + useEffect(() => { + if (elementToScroll && !isInView && isOnbordaVisible) { + console.log("Onborda: Element to Scroll Changed"); + const rect = elementToScroll.getBoundingClientRect(); + const isAbove = rect.top < 0; + elementToScroll.scrollIntoView({ + behavior: "smooth", + block: isAbove ? "center" : "center", + inline: "center", + }); } - } - } - }; - - // - - - // Update pointer position on window resize - useEffect(() => { - if (isOnbordaVisible) { - window.addEventListener("resize", updatePointerPosition); - return () => window.removeEventListener("resize", updatePointerPosition); - } - }, [currentStep, currentTourSteps, isOnbordaVisible]); - - // - - - // Step Controls - const nextStep = async () => { - if (currentTourSteps && currentStep < currentTourSteps.length - 1) { - try { - const nextStepIndex = currentStep + 1; - const route = currentTourSteps[currentStep].nextRoute; - - if (route) { - await router.push(route); - - const targetSelector = currentTourSteps[nextStepIndex].selector; - - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); - - // Stop observing after the element is found - observer.disconnect(); + }, [elementToScroll, isInView, isOnbordaVisible]); + + // - - + // Update pointer position on window resize + const updatePointerPosition = () => { + if (currentTourSteps) { + const step = currentTourSteps[currentStep]; + if (step) { + const selector = step.selector; + if (selector) { + const element = document.querySelector(selector) as Element | null; + if (element) { + setPointerPosition(getElementPosition(element)); + } + } else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; + } } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } else { - setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); } - } catch (error) { - console.error("Error navigating to next route", error); - } - } - }; - - const prevStep = async () => { - if (currentTourSteps && currentStep > 0) { - try { - const prevStepIndex = currentStep - 1; - const route = currentTourSteps[currentStep].prevRoute; - - if (route) { - await router.push(route); + }; - const targetSelector = currentTourSteps[prevStepIndex].selector; + // - - + // Update pointer position on window resize + useEffect(() => { + if (isOnbordaVisible) { + window.addEventListener("resize", updatePointerPosition); + return () => window.removeEventListener("resize", updatePointerPosition); + } + }, [currentStep, currentTourSteps, isOnbordaVisible]); + + // - - + // Step Controls + const nextStep = async () => { + if (currentTourSteps && currentStep < currentTourSteps.length - 1) { + try { + const nextStepIndex = currentStep + 1; + const route = currentTourSteps[currentStep].nextRoute; + + if (route) { + await router.push(route); + + const targetSelector = currentTourSteps[nextStepIndex].selector; + if (targetSelector) { + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const element = document.querySelector(targetSelector); + if (element) { + // Once the element is found, update the step and scroll to the element + setCurrentStep(nextStepIndex); + scrollToElement(nextStepIndex); + + // Stop observing after the element is found + observer.disconnect(); + } + }); + + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + } else { + // If no selector is provided, just update the step + setCurrentStep(nextStepIndex); + } + } else { + setCurrentStep(nextStepIndex); + scrollToElement(nextStepIndex); + } + } catch (error) { + console.error("Error navigating to next route", error); + } + } + }; - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); + const prevStep = async () => { + if (currentTourSteps && currentStep > 0) { + try { + const prevStepIndex = currentStep - 1; + const route = currentTourSteps[currentStep].prevRoute; + + if (route) { + await router.push(route); + + const targetSelector = currentTourSteps[prevStepIndex].selector; + if (targetSelector) { + + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const element = document.querySelector(targetSelector); + if (element) { + // Once the element is found, update the step and scroll to the element + setCurrentStep(prevStepIndex); + scrollToElement(prevStepIndex); + + // Stop observing after the element is found + observer.disconnect(); + } + }); + + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + } else { + // If no selector is provided, just update the step + setCurrentStep(prevStepIndex); + } + } else { + setCurrentStep(prevStepIndex); + scrollToElement(prevStepIndex); + } + } catch (error) { + console.error("Error navigating to previous route", error); + } + } + }; - // Stop observing after the element is found - observer.disconnect(); + // - - + // Scroll to the correct element when the step changes + const scrollToElement = (stepIndex: number) => { + if (currentTourSteps) { + const selector = currentTourSteps[stepIndex].selector; + if (selector) { + const element = document.querySelector(selector) as Element | null; + if (element) { + const {top} = element.getBoundingClientRect(); + const isInViewport = top >= -offset && top <= window.innerHeight + offset; + if (!isInViewport) { + element.scrollIntoView({behavior: "smooth", block: "center"}); + } + // Update pointer position after scrolling + setPointerPosition(getElementPosition(element)); + } + } else { + // if the element is not found, place the pointer at the center of the screen + setPointerPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 2, + width: 0, + height: 0, + }); + setElementToScroll(null); + currentElementRef.current = null; } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } else { - setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); } - } catch (error) { - console.error("Error navigating to previous route", error); - } - } - }; - - // - - - // Scroll to the correct element when the step changes - const scrollToElement = (stepIndex: number) => { - if (currentTourSteps) { - const element = document.querySelector( - currentTourSteps[stepIndex].selector - ) as Element | null; - if (element) { - const { top } = element.getBoundingClientRect(); - const isInViewport = - top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); + }; + + // - - + // Card Side + const getCardStyle: (side: string) => React.CSSProperties = (side: string) => { + switch (side) { + case "top": + return { + transform: `translate(-50%, 0)`, + left: "50%", + bottom: "100%", + marginBottom: "25px", + }; + case "bottom": + return { + transform: `translate(-50%, 0)`, + left: "50%", + top: "100%", + marginTop: "25px", + }; + case "left": + return { + transform: `translate(0, -50%)`, + right: "100%", + top: "50%", + marginRight: "25px", + }; + case "right": + return { + transform: `translate(0, -50%)`, + left: "100%", + top: "50%", + marginLeft: "25px", + }; + case "top-left": + return { + bottom: "100%", + marginBottom: "25px", + }; + case "top-right": + return { + right: 0, + bottom: "100%", + marginBottom: "25px", + }; + case "bottom-left": + return { + top: "100%", + marginTop: "25px", + }; + case "bottom-right": + return { + right: 0, + top: "100%", + marginTop: "25px", + }; + case "right-bottom": + return { + left: "100%", + bottom: 0, + marginLeft: "25px", + }; + case "right-top": + return { + left: "100%", + top: 0, + marginLeft: "25px", + }; + case "left-bottom": + return { + right: "100%", + bottom: 0, + marginRight: "25px", + }; + case "left-top": + return { + right: "100%", + top: 0, + marginRight: "25px", + }; + default: + // Default case if no side is specified. Center the card to the screen + return { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', // Center the card + position: 'fixed', // Make sure it's positioned relative to the viewport + margin: '0', + }; } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); - } } - }; - // - - - // Card Side - const getCardStyle = (side: string) => { - switch (side) { - case "top": - return { - transform: `translate(-50%, 0)`, - left: "50%", - bottom: "100%", - marginBottom: "25px", - }; - case "bottom": - return { - transform: `translate(-50%, 0)`, - left: "50%", - top: "100%", - marginTop: "25px", - }; - case "left": - return { - transform: `translate(0, -50%)`, - right: "100%", - top: "50%", - marginRight: "25px", - }; - case "right": - return { - transform: `translate(0, -50%)`, - left: "100%", - top: "50%", - marginLeft: "25px", - }; - case "top-left": - return { - bottom: "100%", - marginBottom: "25px", - }; - case "top-right": - return { - right: 0, - bottom: "100%", - marginBottom: "25px", - }; - case "bottom-left": - return { - top: "100%", - marginTop: "25px", - }; - case "bottom-right": - return { - right: 0, - top: "100%", - marginTop: "25px", - }; - case "right-bottom": - return { - left: "100%", - bottom: 0, - marginLeft: "25px", - }; - case "right-top": - return { - left: "100%", - top: 0, - marginLeft: "25px", - }; - case "left-bottom": - return { - right: "100%", - bottom: 0, - marginRight: "25px", - }; - case "left-top": - return { - right: "100%", - top: 0, - marginRight: "25px", - }; - default: - return {}; // Default case if no side is specified - } - }; + // - - + // Arrow position based on card side + const getArrowStyle: (side: string) => React.CSSProperties = (side: string) => { + switch (side) { + case "bottom": + return { + transform: `translate(-50%, 0) rotate(270deg)`, + left: "50%", + top: "-23px", + }; + case "top": + return { + transform: `translate(-50%, 0) rotate(90deg)`, + left: "50%", + bottom: "-23px", + }; + case "right": + return { + transform: `translate(0, -50%) rotate(180deg)`, + top: "50%", + left: "-23px", + }; + case "left": + return { + transform: `translate(0, -50%) rotate(0deg)`, + top: "50%", + right: "-23px", + }; + case "top-left": + return { + transform: `rotate(90deg)`, + left: "10px", + bottom: "-23px", + }; + case "top-right": + return { + transform: `rotate(90deg)`, + right: "10px", + bottom: "-23px", + }; + case "bottom-left": + return { + transform: `rotate(270deg)`, + left: "10px", + top: "-23px", + }; + case "bottom-right": + return { + transform: `rotate(270deg)`, + right: "10px", + top: "-23px", + }; + case "right-bottom": + return { + transform: `rotate(180deg)`, + left: "-23px", + bottom: "10px", + }; + case "right-top": + return { + transform: `rotate(180deg)`, + left: "-23px", + top: "10px", + }; + case "left-bottom": + return { + transform: `rotate(0deg)`, + right: "-23px", + bottom: "10px", + }; + case "left-top": + return { + transform: `rotate(0deg)`, + right: "-23px", + top: "10px", + }; + default: + return {}; // Default case if no side is specified + } + }; - // - - - // Arrow position based on card side - const getArrowStyle = (side: string) => { - switch (side) { - case "bottom": - return { - transform: `translate(-50%, 0) rotate(270deg)`, - left: "50%", - top: "-23px", - }; - case "top": - return { - transform: `translate(-50%, 0) rotate(90deg)`, - left: "50%", - bottom: "-23px", - }; - case "right": - return { - transform: `translate(0, -50%) rotate(180deg)`, - top: "50%", - left: "-23px", - }; - case "left": - return { - transform: `translate(0, -50%) rotate(0deg)`, - top: "50%", - right: "-23px", - }; - case "top-left": - return { - transform: `rotate(90deg)`, - left: "10px", - bottom: "-23px", - }; - case "top-right": - return { - transform: `rotate(90deg)`, - right: "10px", - bottom: "-23px", - }; - case "bottom-left": - return { - transform: `rotate(270deg)`, - left: "10px", - top: "-23px", - }; - case "bottom-right": - return { - transform: `rotate(270deg)`, - right: "10px", - top: "-23px", - }; - case "right-bottom": - return { - transform: `rotate(180deg)`, - left: "-23px", - bottom: "10px", - }; - case "right-top": - return { - transform: `rotate(180deg)`, - left: "-23px", - top: "10px", - }; - case "left-bottom": - return { - transform: `rotate(0deg)`, - right: "-23px", - bottom: "10px", - }; - case "left-top": - return { - transform: `rotate(0deg)`, - right: "-23px", - top: "10px", - }; - default: - return {}; // Default case if no side is specified - } - }; - - // - - - // Card Arrow - const CardArrow = () => { - return ( - - - - ); - }; - - // - - - // Overlay Variants - const variants = { - visible: { opacity: 1 }, - hidden: { opacity: 0 }, - }; - - // - - - // Pointer Options - const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; - const pointerPadOffset = pointerPadding / 2; - const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; - const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : ''; - - return (<> - - {/* Container for the Website content */} -
- {children} -
- - {/* Onborda Overlay Step Content */} - {pointerPosition && isOnbordaVisible && CardComponent && ( - - - - {/* Card */} -
- } - /> -
-
-
-
- )} - -
- {TourComponent && currentTourSteps && ( -
- -
- )} -
- ); + // - - + // Card Arrow + const CardArrow = ({ isVisible }: { isVisible: boolean }) => { + if (!isVisible) { + return null; + } + return ( + + + + ); + }; + + // - - + // Overlay Variants + const variants = { + visible: {opacity: 1}, + hidden: {opacity: 0}, + }; + + // - - + // Pointer Options + const pointerPadding = currentTourSteps?.[currentStep]?.pointerPadding ?? 30; + const pointerPadOffset = pointerPadding / 2; + const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; + const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : ''; + + return (<> + + {/* Container for the Website content */} +
+ {children} +
+ + {/* Onborda Overlay Step Content */} + {pointerPosition && isOnbordaVisible && CardComponent && ( + + + + {/* Card */} +
+ } + /> +
+
+
+
+ )} + +
+ {TourComponent && currentTourSteps && ( +
+ +
+ )} +
+ ); }; export default Onborda; diff --git a/src/types/index.ts b/src/types/index.ts index fb49958..1cb9ee9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,7 +16,7 @@ export interface Step { icon: React.ReactNode | string | null; title: string; content: React.ReactNode; - selector: string; + selector?: string; // Options side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; showControls?: boolean; From 524f638423cc9f1fd6e6c6f580f55f367a2067ac Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Mon, 4 Nov 2024 14:30:58 +0000 Subject: [PATCH 08/30] Added optional Step.customQuerySelector for passing a custom querySelector function per step for setting the pointer to more complex elements, eg call .closest --- dist/Onborda.js | 82 ++++++++++++++++++-------------------- dist/types/index.d.ts | 1 + src/Onborda.tsx | 91 ++++++++++++++++++++----------------------- src/types/index.ts | 1 + 4 files changed, 82 insertions(+), 93 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 9091b13..6d1c05c 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -14,6 +14,12 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const observeRef = useRef(null); // Ref for the observer element const isInView = useInView(observeRef); const offset = 20; + const hasSelector = (step) => { + return !!step?.selector || !!step?.customQuerySelector; + }; + const getStepSelectorElement = (step) => { + return step?.selector ? document.querySelector(step.selector) : step?.customQuerySelector ? step.customQuerySelector() : null; + }; // - - // Route Changes const router = useRouter(); @@ -24,23 +30,20 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { - const selector = step.selector; - if (selector) { - const element = document.querySelector(selector); - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - // Enable pointer events on the element - if (step.interactable) { - const htmlElement = element; - htmlElement.style.pointerEvents = "auto"; - } - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } + const element = getStepSelectorElement(step); + if (element) { + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element; + htmlElement.style.pointerEvents = "auto"; + } + setElementToScroll(element); + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); } } else { @@ -84,9 +87,8 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const step = currentTourSteps[currentStep]; console.log("Onborda: Current Step Changed", step); if (step) { - const selector = step.selector; - if (selector) { - const element = document.querySelector(selector); + const element = getStepSelectorElement(step); + if (element) { if (element) { setPointerPosition(getElementPosition(element)); currentElementRef.current = element; @@ -130,12 +132,9 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" if (currentTourSteps) { const step = currentTourSteps[currentStep]; if (step) { - const selector = step.selector; - if (selector) { - const element = document.querySelector(selector); - if (element) { - setPointerPosition(getElementPosition(element)); - } + const element = getStepSelectorElement(step); + if (element) { + setPointerPosition(getElementPosition(element)); } else { // if the element is not found, place the pointer at the center of the screen @@ -168,11 +167,10 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const route = currentTourSteps[currentStep].nextRoute; if (route) { await router.push(route); - const targetSelector = currentTourSteps[nextStepIndex].selector; - if (targetSelector) { + const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); + if (element) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(nextStepIndex); @@ -209,11 +207,10 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const route = currentTourSteps[currentStep].prevRoute; if (route) { await router.push(route); - const targetSelector = currentTourSteps[prevStepIndex].selector; - if (targetSelector) { + const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); + if (element) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(prevStepIndex); @@ -247,18 +244,15 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" // Scroll to the correct element when the step changes const scrollToElement = (stepIndex) => { if (currentTourSteps) { - const selector = currentTourSteps[stepIndex].selector; - if (selector) { - const element = document.querySelector(selector); - if (element) { - const { top } = element.getBoundingClientRect(); - const isInViewport = top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); + const element = getStepSelectorElement(currentTourSteps[stepIndex]); + if (element) { + const { top } = element.getBoundingClientRect(); + const isInViewport = top >= -offset && top <= window.innerHeight + offset; + if (!isInViewport) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); } + // Update pointer position after scrolling + setPointerPosition(getElementPosition(element)); } else { // if the element is not found, place the pointer at the center of the screen @@ -479,6 +473,6 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: !!(currentTourSteps?.[currentStep]?.selector) }) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[999] w-screen h-screen pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[999] w-screen h-screen pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); }; export default Onborda; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index c8e5edd..e91601c 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -12,6 +12,7 @@ export interface Step { title: string; content: React.ReactNode; selector?: string; + customQuerySelector?: () => Element | null; side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; showControls?: boolean; pointerPadding?: number; diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 4de1598..275f1d5 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -6,7 +6,8 @@ import {useRouter} from "next/navigation"; import {Portal} from "@radix-ui/react-portal"; // Types -import {OnbordaProps} from "./types"; +import {OnbordaProps, Step} from "./types"; +import {step} from "next/dist/experimental/testmode/playwright/step"; const Onborda: React.FC = ({ children, @@ -35,6 +36,13 @@ const Onborda: React.FC = ({ const isInView = useInView(observeRef); const offset = 20; + const hasSelector = (step: Step): boolean => { + return !!step?.selector || !!step?.customQuerySelector; + } + const getStepSelectorElement = (step: Step): Element | null => { + return step?.selector ? document.querySelector(step.selector) : step?.customQuerySelector ? step.customQuerySelector() : null; + } + // - - // Route Changes const router = useRouter(); @@ -46,28 +54,25 @@ const Onborda: React.FC = ({ console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { - const selector = step.selector; - if (selector) { - const element = document.querySelector(selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; + const element = getStepSelectorElement(step) + if (element) { + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; - // Enable pointer events on the element - if (step.interactable) { - const htmlElement = element as HTMLElement; - htmlElement.style.pointerEvents = "auto"; - } + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element as HTMLElement; + htmlElement.style.pointerEvents = "auto"; + } - setElementToScroll(element); + setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = + rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({behavior: "smooth", block: "center"}); - } + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({behavior: "smooth", block: "center"}); } }else { // if the element is not found, place the pointer at the center of the screen @@ -112,9 +117,8 @@ const Onborda: React.FC = ({ const step = currentTourSteps[currentStep]; console.log("Onborda: Current Step Changed", step); if (step) { - const selector = step.selector; - if (selector) { - const element = document.querySelector(selector) as Element | null; + const element = getStepSelectorElement(step); + if (element) { if (element) { setPointerPosition(getElementPosition(element)); currentElementRef.current = element; @@ -162,12 +166,9 @@ const Onborda: React.FC = ({ if (currentTourSteps) { const step = currentTourSteps[currentStep]; if (step) { - const selector = step.selector; - if (selector) { - const element = document.querySelector(selector) as Element | null; - if (element) { - setPointerPosition(getElementPosition(element)); - } + const element = getStepSelectorElement(step); + if (element) { + setPointerPosition(getElementPosition(element)); } else { // if the element is not found, place the pointer at the center of the screen setPointerPosition({ @@ -203,11 +204,10 @@ const Onborda: React.FC = ({ if (route) { await router.push(route); - const targetSelector = currentTourSteps[nextStepIndex].selector; - if (targetSelector) { + const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); + if (element) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(nextStepIndex); @@ -246,12 +246,10 @@ const Onborda: React.FC = ({ if (route) { await router.push(route); - const targetSelector = currentTourSteps[prevStepIndex].selector; - if (targetSelector) { - + const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); + if (element) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { - const element = document.querySelector(targetSelector); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(prevStepIndex); @@ -285,18 +283,15 @@ const Onborda: React.FC = ({ // Scroll to the correct element when the step changes const scrollToElement = (stepIndex: number) => { if (currentTourSteps) { - const selector = currentTourSteps[stepIndex].selector; - if (selector) { - const element = document.querySelector(selector) as Element | null; - if (element) { - const {top} = element.getBoundingClientRect(); - const isInViewport = top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({behavior: "smooth", block: "center"}); - } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); + const element = getStepSelectorElement(currentTourSteps[stepIndex]); + if (element) { + const {top} = element.getBoundingClientRect(); + const isInViewport = top >= -offset && top <= window.innerHeight + offset; + if (!isInViewport) { + element.scrollIntoView({behavior: "smooth", block: "center"}); } + // Update pointer position after scrolling + setPointerPosition(getElementPosition(element)); } else { // if the element is not found, place the pointer at the center of the screen setPointerPosition({ @@ -576,9 +571,7 @@ const Onborda: React.FC = ({ nextStep={nextStep} prevStep={prevStep} arrow={} />
diff --git a/src/types/index.ts b/src/types/index.ts index 1cb9ee9..e0fc265 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,6 +17,7 @@ export interface Step { title: string; content: React.ReactNode; selector?: string; + customQuerySelector?: () => Element | null; // Options side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; showControls?: boolean; From 49afe4eb7c670d990bfb460730d59dd5b368a09e Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 5 Nov 2024 15:05:50 +0000 Subject: [PATCH 09/30] Fixed MutationObserver not running for steps on nextRoute pages. Added router.prefetch. Updated README --- README.md | 91 ++++++++++++++++++++++++++++++------------- dist/Onborda.js | 57 ++++++++++++++++++--------- dist/types/index.d.ts | 1 + src/Onborda.tsx | 60 ++++++++++++++++++---------- src/types/index.ts | 1 + 5 files changed, 144 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index d7c55a1..1f0bfc9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ yarn add Libresoft-UK/onborda ``` ### Components & `page.tsx` -Target anything in your app using the elements `id` attribute. +Optionally target anything in your app using the elements `id` attribute to overlay a backdrop and highlight the target element. If no `selector` or `customQuerySelector` is provided, the overlay will cover the entire page and the step will display as a traditional modal. ```tsx
Onboard Step
``` @@ -43,7 +43,7 @@ const config: Config = { } ``` -### Custom Card +### Card Component If you require greater control over the card design or simply wish to create a totally custom component then you can do so easily. | Prop | Type | Description | @@ -80,6 +80,38 @@ export const CustomCard = ({ } ``` +### Tour Component +If you require greater control over the tour design or simply wish to create a totally custom component then you can do so easily. + +| Prop | Type | Description | +|---------------|----------|----------------------------------------------------------------------| +| `currentTour` | `string` | The current tour name. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `steps` | `Step[]` | The steps array for the current tour. | + +```tsx +"use client" +import type { TourComponentProps } from "onborda"; + +export const CustomTour = ({ + currentTour, + currentStep, + steps, +}: TourComponentProps) => { + return ( +
+

{currentTour}

+

{currentStep} of {steps.length}

+
    + {steps.map((step, index) => ( +
  • {step.title}
  • + ))} +
+
+ ) +} +``` + ### Steps object Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" so you have the option to create multple product tours should you need to! The original Step format remains but with some additional content as shown in the example below! @@ -98,20 +130,20 @@ Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" ### Step object -| Prop | Type | Description | -|----------------|-------------------------------|---------------------------------------------------------------------------------------| -| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | -| `title` | `string` | The title of your step | -| `content` | `React.ReactNode` | The main content or body of the step. | -| `selector` | `string` | A string used to target an `id` that this step refers to. | -| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | -| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | -| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | -| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | -| `nextRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the next step. | -| `prevRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the previous step. | - -> **Note** _Both `nextRoute` and `prevRoute` have a `500`ms delay before setting the next step, a function will be added soon to control the delay in case your application loads slower than this._ +| Prop | Type | Description | +|-----------------------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | +| `title` | `string` | The title of your step | +| `content` | `React.ReactNode` | The main content or body of the step. | +| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | +| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | +| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | +| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | +| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | +| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | +| `nextRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the next step. | +| `prevRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the previous step. | +| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | ### Example `steps` @@ -138,28 +170,32 @@ Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" icon: <>👋👋, title: "Second tour, Step 1", content: <>Second tour, first step!, - selector: "#onborda-step1", + //selector: "#onborda-step1", + customQuerySelector: () => document.getElementById("onborda-step1").closest("div"), // get the parent div side: "top", showControls: true, pointerPadding: 10, pointerRadius: 10, nextRoute: "/foo", - prevRoute: "/bar" + prevRoute: "/bar", + interactable: true ] } ``` ### Onborda Props -| Property | Type | Description | -|-----------------|-----------------------|---------------------------------------------------------------------------------------| -| `children` | `React.ReactNode` | Your website or application content. | -| `steps` | `Array[]` | An array of `Step` objects defining each step of the onboarding process. | -| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | -| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | -| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | -| `customCard` | `React.ReactNode` | Optional. A custom card (or tooltip) that can be used to replace the default TailwindCSS card. | -| `cardTransition`| `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | +| Property | Type | Description | +|------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `steps` | `Array[]` | An array of `Step` objects defining each step of the onboarding process. | +| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | +| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | +| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | +| `cardTransition` | `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | +| `cardComponent` | `React.ReactNode` | The React component to use as the card for each step. | +| `tourComponent` | `React.ReactNode` | The React component to use as a list of steps for the current tour. | +| `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | ```tsx @@ -169,6 +205,7 @@ Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" shadowRgb="55,48,163" shadowOpacity="0.8" cardComponent={CustomCard} + tourComponent={CustomTour} cardTransition={{ duration: 2, type: "tween" }} > {children} diff --git a/dist/Onborda.js b/dist/Onborda.js index 6d1c05c..fe21598 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -5,7 +5,7 @@ import { useOnborda } from "./OnbordaContext"; import { motion, useInView } from "framer-motion"; import { useRouter } from "next/navigation"; import { Portal } from "@radix-ui/react-portal"; -const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, }) => { +const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, }) => { const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); const currentTourSteps = steps.find((tour) => tour.tour === currentTour)?.steps; const [elementToScroll, setElementToScroll] = useState(null); @@ -27,7 +27,7 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" // Initialisze useEffect(() => { if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); + debug && console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { const element = getStepSelectorElement(step); @@ -85,19 +85,17 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" useEffect(() => { if (isOnbordaVisible && currentTourSteps) { const step = currentTourSteps[currentStep]; - console.log("Onborda: Current Step Changed", step); + debug && console.log("Onborda: Current Step Changed", step); if (step) { const element = getStepSelectorElement(step); if (element) { - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; + setElementToScroll(element); + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); } } else { @@ -111,12 +109,22 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" setElementToScroll(null); currentElementRef.current = null; } + // Prefetch the next route + if (step?.nextRoute) { + debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); + router.prefetch(step.nextRoute); + } + // Prefetch the previous route + if (step?.prevRoute) { + debug && console.log("Onborda: Prefetching Previous Route", step.prevRoute); + router.prefetch(step.prevRoute); + } } } }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); useEffect(() => { if (elementToScroll && !isInView && isOnbordaVisible) { - console.log("Onborda: Element to Scroll Changed"); + debug && console.log("Onborda: Element to Scroll Changed"); const rect = elementToScroll.getBoundingClientRect(); const isAbove = rect.top < 0; elementToScroll.scrollIntoView({ @@ -167,17 +175,26 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const route = currentTourSteps[currentStep].nextRoute; if (route) { await router.push(route); - const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); - if (element) { + const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); + if (shouldSelect) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { - if (element) { + const nextStep = currentTourSteps?.[nextStepIndex]; + if (!nextStep) { + debug && console.log("Onborda: Next Step not found while observing"); + observer.disconnect(); + } + const elementFound = getStepSelectorElement(nextStep); + if (elementFound) { // Once the element is found, update the step and scroll to the element setCurrentStep(nextStepIndex); scrollToElement(nextStepIndex); // Stop observing after the element is found observer.disconnect(); } + else { + debug && console.log("Onborda: Element not found", currentTourSteps[nextStepIndex]); + } }); // Start observing the document body for changes observer.observe(document.body, { @@ -207,10 +224,11 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const route = currentTourSteps[currentStep].prevRoute; if (route) { await router.push(route); - const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); - if (element) { + const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); + if (shouldSelect) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { + const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(prevStepIndex); @@ -218,6 +236,9 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" // Stop observing after the element is found observer.disconnect(); } + else { + debug && console.log("Onborda: Element not found", currentTourSteps[prevStepIndex]); + } }); // Start observing the document body for changes observer.observe(document.body, { diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index e91601c..61da74d 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -34,6 +34,7 @@ export interface OnbordaProps { cardTransition?: Transition; cardComponent?: React.ComponentType; tourComponent?: React.ComponentType; + debug?: boolean; } export interface CardComponentProps { step: Step; diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 275f1d5..f9a9d78 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -7,7 +7,6 @@ import {Portal} from "@radix-ui/react-portal"; // Types import {OnbordaProps, Step} from "./types"; -import {step} from "next/dist/experimental/testmode/playwright/step"; const Onborda: React.FC = ({ children, @@ -17,9 +16,9 @@ const Onborda: React.FC = ({ cardTransition = {ease: "anticipate", duration: 0.6}, cardComponent: CardComponent, tourComponent: TourComponent, + debug = false, }) => { - const {currentTour, currentStep, setCurrentStep, isOnbordaVisible} = - useOnborda(); + const {currentTour, currentStep, setCurrentStep, isOnbordaVisible} = useOnborda(); const currentTourSteps = steps.find( (tour) => tour.tour === currentTour )?.steps; @@ -51,7 +50,7 @@ const Onborda: React.FC = ({ // Initialisze useEffect(() => { if (isOnbordaVisible && currentTourSteps) { - console.log("Onborda: Current Step Changed"); + debug && console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { const element = getStepSelectorElement(step) @@ -115,22 +114,20 @@ const Onborda: React.FC = ({ useEffect(() => { if (isOnbordaVisible && currentTourSteps) { const step = currentTourSteps[currentStep]; - console.log("Onborda: Current Step Changed", step); + debug && console.log("Onborda: Current Step Changed", step); if (step) { const element = getStepSelectorElement(step); if (element) { - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); + setPointerPosition(getElementPosition(element)); + currentElementRef.current = element; + setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; + const rect = element.getBoundingClientRect(); + const isInViewportWithOffset = + rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({behavior: "smooth", block: "center"}); - } + if (!isInView || !isInViewportWithOffset) { + element.scrollIntoView({behavior: "smooth", block: "center"}); } }else { // if the element is not found, place the pointer at the center of the screen @@ -143,13 +140,23 @@ const Onborda: React.FC = ({ setElementToScroll(null); currentElementRef.current = null; } + // Prefetch the next route + if (step?.nextRoute) { + debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); + router.prefetch(step.nextRoute); + } + // Prefetch the previous route + if (step?.prevRoute) { + debug && console.log("Onborda: Prefetching Previous Route", step.prevRoute); + router.prefetch(step.prevRoute); + } } } }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); useEffect(() => { if (elementToScroll && !isInView && isOnbordaVisible) { - console.log("Onborda: Element to Scroll Changed"); + debug && console.log("Onborda: Element to Scroll Changed"); const rect = elementToScroll.getBoundingClientRect(); const isAbove = rect.top < 0; elementToScroll.scrollIntoView({ @@ -204,17 +211,25 @@ const Onborda: React.FC = ({ if (route) { await router.push(route); - const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); - if (element) { + const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); + if (shouldSelect) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { - if (element) { + const nextStep = currentTourSteps?.[nextStepIndex]; + if (!nextStep) { + debug && console.log("Onborda: Next Step not found while observing"); + observer.disconnect(); + } + const elementFound = getStepSelectorElement(nextStep); + if (elementFound) { // Once the element is found, update the step and scroll to the element setCurrentStep(nextStepIndex); scrollToElement(nextStepIndex); // Stop observing after the element is found observer.disconnect(); + }else { + debug && console.log("Onborda: Element not found", currentTourSteps[nextStepIndex]); } }); @@ -246,10 +261,11 @@ const Onborda: React.FC = ({ if (route) { await router.push(route); - const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); - if (element) { + const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); + if (shouldSelect) { // Use MutationObserver to detect when the target element is available in the DOM const observer = new MutationObserver((mutations, observer) => { + const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(prevStepIndex); @@ -257,6 +273,8 @@ const Onborda: React.FC = ({ // Stop observing after the element is found observer.disconnect(); + }else { + debug && console.log("Onborda: Element not found", currentTourSteps[prevStepIndex]); } }); diff --git a/src/types/index.ts b/src/types/index.ts index e0fc265..6161d70 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -46,6 +46,7 @@ export interface OnbordaProps { cardTransition?: Transition; cardComponent?: React.ComponentType; tourComponent?: React.ComponentType; + debug?: boolean; } // Custom Card From 23e80af1c2dadb01af257ae4220342b4f124c404 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Wed, 6 Nov 2024 13:34:20 +0000 Subject: [PATCH 10/30] Cleaned up useEffects repeated/reduentant code. Moved TourComponent into . Added timout to MutationObserver --- dist/Onborda.d.ts | 5 + dist/Onborda.js | 412 +++++++++----------------------------- dist/OnbordaStyles.d.ts | 3 + dist/OnbordaStyles.js | 165 +++++++++++++++ dist/types/index.d.ts | 1 + src/Onborda.tsx | 434 +++++++++++----------------------------- src/OnbordaStyles.tsx | 168 ++++++++++++++++ src/types/index.ts | 39 +++- 8 files changed, 594 insertions(+), 633 deletions(-) create mode 100644 dist/OnbordaStyles.d.ts create mode 100644 dist/OnbordaStyles.js create mode 100644 src/OnbordaStyles.tsx diff --git a/dist/Onborda.d.ts b/dist/Onborda.d.ts index 6abc7bc..83e3bce 100644 --- a/dist/Onborda.d.ts +++ b/dist/Onborda.d.ts @@ -1,4 +1,9 @@ import React from "react"; import { OnbordaProps } from "./types"; +/** + * Onborda Component + * @param {OnbordaProps} props + * @constructor + */ declare const Onborda: React.FC; export default Onborda; diff --git a/dist/Onborda.js b/dist/Onborda.js index fe21598..6db66dd 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -1,18 +1,22 @@ "use client"; -import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useEffect, useRef, useState } from "react"; import { useOnborda } from "./OnbordaContext"; -import { motion, useInView } from "framer-motion"; +import { motion } from "framer-motion"; import { useRouter } from "next/navigation"; import { Portal } from "@radix-ui/react-portal"; -const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, }) => { +import { getCardStyle, getArrowStyle } from "./OnbordaStyles"; +/** + * Onborda Component + * @param {OnbordaProps} props + * @constructor + */ +const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, observerTimeout = 5000, }) => { const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); const currentTourSteps = steps.find((tour) => tour.tour === currentTour)?.steps; const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); - const observeRef = useRef(null); // Ref for the observer element - const isInView = useInView(observeRef); const offset = 20; const hasSelector = (step) => { return !!step?.selector || !!step?.customQuerySelector; @@ -33,18 +37,13 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const element = getStepSelectorElement(step); if (element) { setPointerPosition(getElementPosition(element)); + setElementToScroll(element); currentElementRef.current = element; // Enable pointer events on the element if (step.interactable) { const htmlElement = element; htmlElement.style.pointerEvents = "auto"; } - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } } else { // if the element is not found, place the pointer at the center of the screen @@ -57,6 +56,11 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" setElementToScroll(null); currentElementRef.current = null; } + // Prefetch the next route + if (step?.nextRoute) { + debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); + router.prefetch(step.nextRoute); + } } } return () => { @@ -66,7 +70,7 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" htmlElement.style.pointerEvents = ""; } }; - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); + }, [currentStep, currentTourSteps, offset, isOnbordaVisible]); // - - // Helper function to get element position const getElementPosition = (element) => { @@ -81,49 +85,9 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" }; }; // - - - // Update pointerPosition when currentStep changes - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - const step = currentTourSteps[currentStep]; - debug && console.log("Onborda: Current Step Changed", step); - if (step) { - const element = getStepSelectorElement(step); - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - } - else { - // if the element is not found, place the pointer at the center of the screen - setPointerPosition({ - x: window.innerWidth / 2, - y: window.innerHeight / 2, - width: 0, - height: 0, - }); - setElementToScroll(null); - currentElementRef.current = null; - } - // Prefetch the next route - if (step?.nextRoute) { - debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); - router.prefetch(step.nextRoute); - } - // Prefetch the previous route - if (step?.prevRoute) { - debug && console.log("Onborda: Prefetching Previous Route", step.prevRoute); - router.prefetch(step.prevRoute); - } - } - } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); + // Scroll to the element when the elementToScroll changes useEffect(() => { - if (elementToScroll && !isInView && isOnbordaVisible) { + if (elementToScroll && isOnbordaVisible) { debug && console.log("Onborda: Element to Scroll Changed"); const rect = elementToScroll.getBoundingClientRect(); const isAbove = rect.top < 0; @@ -133,7 +97,7 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" inline: "center", }); } - }, [elementToScroll, isInView, isOnbordaVisible]); + }, [elementToScroll, isOnbordaVisible]); // - - // Update pointer position on window resize const updatePointerPosition = () => { @@ -174,42 +138,49 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const nextStepIndex = currentStep + 1; const route = currentTourSteps[currentStep].nextRoute; if (route) { + // Trigger the next route await router.push(route); - const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); - if (shouldSelect) { - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const nextStep = currentTourSteps?.[nextStepIndex]; - if (!nextStep) { - debug && console.log("Onborda: Next Step not found while observing"); - observer.disconnect(); - } - const elementFound = getStepSelectorElement(nextStep); - if (elementFound) { + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); + if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); // Stop observing after the element is found observer.disconnect(); } else { - debug && console.log("Onborda: Element not found", currentTourSteps[nextStepIndex]); + debug && console.log("Onborda: Observing for element...", currentTourSteps[nextStepIndex]); } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } - else { - // If no selector is provided, just update the step - setCurrentStep(nextStepIndex); - } + } + else { + console.log("Onborda: No selector set for next step while observing", currentTourSteps[nextStepIndex]); + setCurrentStep(nextStepIndex); + observer.disconnect(); + } + }); + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; } else { + // If no route is set, update the step instantly setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); } } catch (error) { @@ -223,37 +194,50 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const prevStepIndex = currentStep - 1; const route = currentTourSteps[currentStep].prevRoute; if (route) { + // Trigger the previous route await router.push(route); - const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); - if (shouldSelect) { - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); + if (shouldSelect) { const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); // Stop observing after the element is found observer.disconnect(); } else { - debug && console.log("Onborda: Element not found", currentTourSteps[prevStepIndex]); + // Continue observing until the element is found + debug && console.log("Onborda: Observing for element...", currentTourSteps[prevStepIndex]); } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } - else { - // If no selector is provided, just update the step - setCurrentStep(prevStepIndex); - } + } + else { + debug && console.log("Onborda: No selector set for previous step while observing", currentTourSteps[prevStepIndex]); + setCurrentStep(prevStepIndex); + observer.disconnect(); + } + }); + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; } else { + // If no route is set, update the step instantly setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); } } catch (error) { @@ -262,202 +246,6 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" } }; // - - - // Scroll to the correct element when the step changes - const scrollToElement = (stepIndex) => { - if (currentTourSteps) { - const element = getStepSelectorElement(currentTourSteps[stepIndex]); - if (element) { - const { top } = element.getBoundingClientRect(); - const isInViewport = top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({ behavior: "smooth", block: "center" }); - } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); - } - else { - // if the element is not found, place the pointer at the center of the screen - setPointerPosition({ - x: window.innerWidth / 2, - y: window.innerHeight / 2, - width: 0, - height: 0, - }); - setElementToScroll(null); - currentElementRef.current = null; - } - } - }; - // - - - // Card Side - const getCardStyle = (side) => { - switch (side) { - case "top": - return { - transform: `translate(-50%, 0)`, - left: "50%", - bottom: "100%", - marginBottom: "25px", - }; - case "bottom": - return { - transform: `translate(-50%, 0)`, - left: "50%", - top: "100%", - marginTop: "25px", - }; - case "left": - return { - transform: `translate(0, -50%)`, - right: "100%", - top: "50%", - marginRight: "25px", - }; - case "right": - return { - transform: `translate(0, -50%)`, - left: "100%", - top: "50%", - marginLeft: "25px", - }; - case "top-left": - return { - bottom: "100%", - marginBottom: "25px", - }; - case "top-right": - return { - right: 0, - bottom: "100%", - marginBottom: "25px", - }; - case "bottom-left": - return { - top: "100%", - marginTop: "25px", - }; - case "bottom-right": - return { - right: 0, - top: "100%", - marginTop: "25px", - }; - case "right-bottom": - return { - left: "100%", - bottom: 0, - marginLeft: "25px", - }; - case "right-top": - return { - left: "100%", - top: 0, - marginLeft: "25px", - }; - case "left-bottom": - return { - right: "100%", - bottom: 0, - marginRight: "25px", - }; - case "left-top": - return { - right: "100%", - top: 0, - marginRight: "25px", - }; - default: - // Default case if no side is specified. Center the card to the screen - return { - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', // Center the card - position: 'fixed', // Make sure it's positioned relative to the viewport - margin: '0', - }; - } - }; - // - - - // Arrow position based on card side - const getArrowStyle = (side) => { - switch (side) { - case "bottom": - return { - transform: `translate(-50%, 0) rotate(270deg)`, - left: "50%", - top: "-23px", - }; - case "top": - return { - transform: `translate(-50%, 0) rotate(90deg)`, - left: "50%", - bottom: "-23px", - }; - case "right": - return { - transform: `translate(0, -50%) rotate(180deg)`, - top: "50%", - left: "-23px", - }; - case "left": - return { - transform: `translate(0, -50%) rotate(0deg)`, - top: "50%", - right: "-23px", - }; - case "top-left": - return { - transform: `rotate(90deg)`, - left: "10px", - bottom: "-23px", - }; - case "top-right": - return { - transform: `rotate(90deg)`, - right: "10px", - bottom: "-23px", - }; - case "bottom-left": - return { - transform: `rotate(270deg)`, - left: "10px", - top: "-23px", - }; - case "bottom-right": - return { - transform: `rotate(270deg)`, - right: "10px", - top: "-23px", - }; - case "right-bottom": - return { - transform: `rotate(180deg)`, - left: "-23px", - bottom: "10px", - }; - case "right-top": - return { - transform: `rotate(180deg)`, - left: "-23px", - top: "10px", - }; - case "left-bottom": - return { - transform: `rotate(0deg)`, - right: "-23px", - bottom: "10px", - }; - case "left-top": - return { - transform: `rotate(0deg)`, - right: "-23px", - top: "10px", - }; - default: - return {}; // Default case if no side is specified - } - }; - // - - // Card Arrow const CardArrow = ({ isVisible }) => { if (!isVisible) { @@ -477,23 +265,23 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const pointerPadOffset = pointerPadding / 2; const pointerRadius = currentTourSteps?.[currentStep]?.pointerRadius ?? 28; const pointerEvents = pointerPosition && isOnbordaVisible ? 'pointer-events-none' : ''; - return (_jsxs(_Fragment, { children: [_jsx("div", { "data-name": "onborda-site-wrapper", className: ` ${pointerEvents} `, children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsx(Portal, { children: _jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 pointer-events-none", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[998]", style: { - boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, - borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, - }, initial: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, animate: pointerPosition - ? { - x: pointerPosition.x - pointerPadOffset, - y: pointerPosition.y - pointerPadOffset, - width: pointerPosition.width + pointerPadding, - height: pointerPosition.height + pointerPadding, - } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }) }) }) }) }) })), _jsx("div", { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[999] w-screen h-screen pointer-events-none', children: TourComponent && currentTourSteps && (_jsx("div", { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) })) })] })); + return (_jsxs(_Fragment, { children: [_jsx("div", { "data-name": "onborda-site-wrapper", className: ` ${pointerEvents} `, children: children }), pointerPosition && isOnbordaVisible && CardComponent && (_jsxs(Portal, { children: [_jsx(motion.div, { "data-name": "onborda-overlay", className: "absolute inset-0 pointer-events-none z-[997]", initial: "hidden", animate: isOnbordaVisible ? "visible" : "hidden", variants: variants, transition: { duration: 0.5 }, children: _jsx(motion.div, { "data-name": "onborda-pointer", className: "relative z-[998]", style: { + boxShadow: `0 0 200vw 200vh rgba(${shadowRgb}, ${shadowOpacity})`, + borderRadius: `${pointerRadius}px ${pointerRadius}px ${pointerRadius}px ${pointerRadius}px`, + }, initial: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, animate: pointerPosition + ? { + x: pointerPosition.x - pointerPadOffset, + y: pointerPosition.y - pointerPadOffset, + width: pointerPosition.width + pointerPadding, + height: pointerPosition.height + pointerPadding, + } + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }) }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) }) }))] }))] })); }; export default Onborda; diff --git a/dist/OnbordaStyles.d.ts b/dist/OnbordaStyles.d.ts new file mode 100644 index 0000000..b077379 --- /dev/null +++ b/dist/OnbordaStyles.d.ts @@ -0,0 +1,3 @@ +import React from "react"; +export declare const getCardStyle: (side: string) => React.CSSProperties; +export declare const getArrowStyle: (side: string) => React.CSSProperties; diff --git a/dist/OnbordaStyles.js b/dist/OnbordaStyles.js new file mode 100644 index 0000000..8a40e2b --- /dev/null +++ b/dist/OnbordaStyles.js @@ -0,0 +1,165 @@ +export const getCardStyle = (side) => { + switch (side) { + case "top": + return { + transform: `translate(-50%, 0)`, + left: "50%", + bottom: "100%", + marginBottom: "25px", + }; + case "bottom": + return { + transform: `translate(-50%, 0)`, + left: "50%", + top: "100%", + marginTop: "25px", + }; + case "left": + return { + transform: `translate(0, -50%)`, + right: "100%", + top: "50%", + marginRight: "25px", + }; + case "right": + return { + transform: `translate(0, -50%)`, + left: "100%", + top: "50%", + marginLeft: "25px", + }; + case "top-left": + return { + bottom: "100%", + marginBottom: "25px", + }; + case "top-right": + return { + right: 0, + bottom: "100%", + marginBottom: "25px", + }; + case "bottom-left": + return { + top: "100%", + marginTop: "25px", + }; + case "bottom-right": + return { + right: 0, + top: "100%", + marginTop: "25px", + }; + case "right-bottom": + return { + left: "100%", + bottom: 0, + marginLeft: "25px", + }; + case "right-top": + return { + left: "100%", + top: 0, + marginLeft: "25px", + }; + case "left-bottom": + return { + right: "100%", + bottom: 0, + marginRight: "25px", + }; + case "left-top": + return { + right: "100%", + top: 0, + marginRight: "25px", + }; + default: + // Default case if no side is specified. Center the card to the screen + return { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', // Center the card + position: 'fixed', // Make sure it's positioned relative to the viewport + margin: '0', + }; + } +}; +export const getArrowStyle = (side) => { + switch (side) { + case "bottom": + return { + transform: `translate(-50%, 0) rotate(270deg)`, + left: "50%", + top: "-23px", + }; + case "top": + return { + transform: `translate(-50%, 0) rotate(90deg)`, + left: "50%", + bottom: "-23px", + }; + case "right": + return { + transform: `translate(0, -50%) rotate(180deg)`, + top: "50%", + left: "-23px", + }; + case "left": + return { + transform: `translate(0, -50%) rotate(0deg)`, + top: "50%", + right: "-23px", + }; + case "top-left": + return { + transform: `rotate(90deg)`, + left: "10px", + bottom: "-23px", + }; + case "top-right": + return { + transform: `rotate(90deg)`, + right: "10px", + bottom: "-23px", + }; + case "bottom-left": + return { + transform: `rotate(270deg)`, + left: "10px", + top: "-23px", + }; + case "bottom-right": + return { + transform: `rotate(270deg)`, + right: "10px", + top: "-23px", + }; + case "right-bottom": + return { + transform: `rotate(180deg)`, + left: "-23px", + bottom: "10px", + }; + case "right-top": + return { + transform: `rotate(180deg)`, + left: "-23px", + top: "10px", + }; + case "left-bottom": + return { + transform: `rotate(0deg)`, + right: "-23px", + bottom: "10px", + }; + case "left-top": + return { + transform: `rotate(0deg)`, + right: "-23px", + top: "10px", + }; + default: + return {}; // Default case if no side is specified + } +}; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 61da74d..87ab4f6 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -35,6 +35,7 @@ export interface OnbordaProps { cardComponent?: React.ComponentType; tourComponent?: React.ComponentType; debug?: boolean; + observerTimeout?: number; } export interface CardComponentProps { step: Step; diff --git a/src/Onborda.tsx b/src/Onborda.tsx index f9a9d78..872b664 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -1,13 +1,19 @@ "use client"; import React, {useEffect, useRef, useState} from "react"; import {useOnborda} from "./OnbordaContext"; -import {motion, useInView} from "framer-motion"; +import {motion} from "framer-motion"; import {useRouter} from "next/navigation"; import {Portal} from "@radix-ui/react-portal"; // Types import {OnbordaProps, Step} from "./types"; +import {getCardStyle, getArrowStyle} from "./OnbordaStyles"; +/** + * Onborda Component + * @param {OnbordaProps} props + * @constructor + */ const Onborda: React.FC = ({ children, steps, @@ -17,7 +23,8 @@ const Onborda: React.FC = ({ cardComponent: CardComponent, tourComponent: TourComponent, debug = false, -}) => { + observerTimeout = 5000, +}: OnbordaProps) => { const {currentTour, currentStep, setCurrentStep, isOnbordaVisible} = useOnborda(); const currentTourSteps = steps.find( (tour) => tour.tour === currentTour @@ -31,8 +38,6 @@ const Onborda: React.FC = ({ height: number; } | null>(null); const currentElementRef = useRef(null); - const observeRef = useRef(null); // Ref for the observer element - const isInView = useInView(observeRef); const offset = 20; const hasSelector = (step: Step): boolean => { @@ -56,6 +61,7 @@ const Onborda: React.FC = ({ const element = getStepSelectorElement(step) if (element) { setPointerPosition(getElementPosition(element)); + setElementToScroll(element); currentElementRef.current = element; // Enable pointer events on the element @@ -64,15 +70,6 @@ const Onborda: React.FC = ({ htmlElement.style.pointerEvents = "auto"; } - setElementToScroll(element); - - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({behavior: "smooth", block: "center"}); - } }else { // if the element is not found, place the pointer at the center of the screen setPointerPosition({ @@ -84,6 +81,11 @@ const Onborda: React.FC = ({ setElementToScroll(null); currentElementRef.current = null; } + // Prefetch the next route + if (step?.nextRoute) { + debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); + router.prefetch(step.nextRoute); + } } } return () => { @@ -93,7 +95,7 @@ const Onborda: React.FC = ({ htmlElement.style.pointerEvents = ""; } } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); + }, [currentStep, currentTourSteps, offset, isOnbordaVisible]); // - - // Helper function to get element position @@ -110,52 +112,9 @@ const Onborda: React.FC = ({ }; // - - - // Update pointerPosition when currentStep changes - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - const step = currentTourSteps[currentStep]; - debug && console.log("Onborda: Current Step Changed", step); - if (step) { - const element = getStepSelectorElement(step); - if (element) { - setPointerPosition(getElementPosition(element)); - currentElementRef.current = element; - setElementToScroll(element); - - const rect = element.getBoundingClientRect(); - const isInViewportWithOffset = - rect.top >= -offset && rect.bottom <= window.innerHeight + offset; - - if (!isInView || !isInViewportWithOffset) { - element.scrollIntoView({behavior: "smooth", block: "center"}); - } - }else { - // if the element is not found, place the pointer at the center of the screen - setPointerPosition({ - x: window.innerWidth / 2, - y: window.innerHeight / 2, - width: 0, - height: 0, - }); - setElementToScroll(null); - currentElementRef.current = null; - } - // Prefetch the next route - if (step?.nextRoute) { - debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); - router.prefetch(step.nextRoute); - } - // Prefetch the previous route - if (step?.prevRoute) { - debug && console.log("Onborda: Prefetching Previous Route", step.prevRoute); - router.prefetch(step.prevRoute); - } - } - } - }, [currentStep, currentTourSteps, isInView, offset, isOnbordaVisible]); - + // Scroll to the element when the elementToScroll changes useEffect(() => { - if (elementToScroll && !isInView && isOnbordaVisible) { + if (elementToScroll && isOnbordaVisible) { debug && console.log("Onborda: Element to Scroll Changed"); const rect = elementToScroll.getBoundingClientRect(); const isAbove = rect.top < 0; @@ -165,7 +124,7 @@ const Onborda: React.FC = ({ inline: "center", }); } - }, [elementToScroll, isInView, isOnbordaVisible]); + }, [elementToScroll, isOnbordaVisible]); // - - // Update pointer position on window resize @@ -208,43 +167,56 @@ const Onborda: React.FC = ({ const nextStepIndex = currentStep + 1; const route = currentTourSteps[currentStep].nextRoute; + if (route) { + + // Trigger the next route await router.push(route); - const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); - if (shouldSelect) { - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const nextStep = currentTourSteps?.[nextStepIndex]; - if (!nextStep) { - debug && console.log("Onborda: Next Step not found while observing"); - observer.disconnect(); - } - const elementFound = getStepSelectorElement(nextStep); - if (elementFound) { + + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); + if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); // Stop observing after the element is found observer.disconnect(); - }else { - debug && console.log("Onborda: Element not found", currentTourSteps[nextStepIndex]); + } else { + debug && console.log("Onborda: Observing for element...", currentTourSteps[nextStepIndex]); } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } else { - // If no selector is provided, just update the step - setCurrentStep(nextStepIndex); - } + }else { + console.log("Onborda: No selector set for next step while observing", currentTourSteps[nextStepIndex]); + setCurrentStep(nextStepIndex); + observer.disconnect(); + } + }); + + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; + } else { + // If no route is set, update the step instantly setCurrentStep(nextStepIndex); - scrollToElement(nextStepIndex); } } catch (error) { console.error("Error navigating to next route", error); @@ -259,37 +231,54 @@ const Onborda: React.FC = ({ const route = currentTourSteps[currentStep].prevRoute; if (route) { + + // Trigger the previous route await router.push(route); - const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); - if (shouldSelect) { - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); + if (shouldSelect) { const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); if (element) { // Once the element is found, update the step and scroll to the element setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); // Stop observing after the element is found observer.disconnect(); - }else { - debug && console.log("Onborda: Element not found", currentTourSteps[prevStepIndex]); + } else { + // Continue observing until the element is found + debug && console.log("Onborda: Observing for element...", currentTourSteps[prevStepIndex]); } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - } else { - // If no selector is provided, just update the step - setCurrentStep(prevStepIndex); - } + }else { + debug && console.log("Onborda: No selector set for previous step while observing", currentTourSteps[prevStepIndex]); + setCurrentStep(prevStepIndex); + observer.disconnect(); + } + }); + + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; + } else { + // If no route is set, update the step instantly setCurrentStep(prevStepIndex); - scrollToElement(prevStepIndex); } } catch (error) { console.error("Error navigating to previous route", error); @@ -297,203 +286,6 @@ const Onborda: React.FC = ({ } }; - // - - - // Scroll to the correct element when the step changes - const scrollToElement = (stepIndex: number) => { - if (currentTourSteps) { - const element = getStepSelectorElement(currentTourSteps[stepIndex]); - if (element) { - const {top} = element.getBoundingClientRect(); - const isInViewport = top >= -offset && top <= window.innerHeight + offset; - if (!isInViewport) { - element.scrollIntoView({behavior: "smooth", block: "center"}); - } - // Update pointer position after scrolling - setPointerPosition(getElementPosition(element)); - } else { - // if the element is not found, place the pointer at the center of the screen - setPointerPosition({ - x: window.innerWidth / 2, - y: window.innerHeight / 2, - width: 0, - height: 0, - }); - setElementToScroll(null); - currentElementRef.current = null; - } - } - }; - - // - - - // Card Side - const getCardStyle: (side: string) => React.CSSProperties = (side: string) => { - switch (side) { - case "top": - return { - transform: `translate(-50%, 0)`, - left: "50%", - bottom: "100%", - marginBottom: "25px", - }; - case "bottom": - return { - transform: `translate(-50%, 0)`, - left: "50%", - top: "100%", - marginTop: "25px", - }; - case "left": - return { - transform: `translate(0, -50%)`, - right: "100%", - top: "50%", - marginRight: "25px", - }; - case "right": - return { - transform: `translate(0, -50%)`, - left: "100%", - top: "50%", - marginLeft: "25px", - }; - case "top-left": - return { - bottom: "100%", - marginBottom: "25px", - }; - case "top-right": - return { - right: 0, - bottom: "100%", - marginBottom: "25px", - }; - case "bottom-left": - return { - top: "100%", - marginTop: "25px", - }; - case "bottom-right": - return { - right: 0, - top: "100%", - marginTop: "25px", - }; - case "right-bottom": - return { - left: "100%", - bottom: 0, - marginLeft: "25px", - }; - case "right-top": - return { - left: "100%", - top: 0, - marginLeft: "25px", - }; - case "left-bottom": - return { - right: "100%", - bottom: 0, - marginRight: "25px", - }; - case "left-top": - return { - right: "100%", - top: 0, - marginRight: "25px", - }; - default: - // Default case if no side is specified. Center the card to the screen - return { - top: '50%', - left: '50%', - transform: 'translate(-50%, -50%)', // Center the card - position: 'fixed', // Make sure it's positioned relative to the viewport - margin: '0', - }; - } - } - - // - - - // Arrow position based on card side - const getArrowStyle: (side: string) => React.CSSProperties = (side: string) => { - switch (side) { - case "bottom": - return { - transform: `translate(-50%, 0) rotate(270deg)`, - left: "50%", - top: "-23px", - }; - case "top": - return { - transform: `translate(-50%, 0) rotate(90deg)`, - left: "50%", - bottom: "-23px", - }; - case "right": - return { - transform: `translate(0, -50%) rotate(180deg)`, - top: "50%", - left: "-23px", - }; - case "left": - return { - transform: `translate(0, -50%) rotate(0deg)`, - top: "50%", - right: "-23px", - }; - case "top-left": - return { - transform: `rotate(90deg)`, - left: "10px", - bottom: "-23px", - }; - case "top-right": - return { - transform: `rotate(90deg)`, - right: "10px", - bottom: "-23px", - }; - case "bottom-left": - return { - transform: `rotate(270deg)`, - left: "10px", - top: "-23px", - }; - case "bottom-right": - return { - transform: `rotate(270deg)`, - right: "10px", - top: "-23px", - }; - case "right-bottom": - return { - transform: `rotate(180deg)`, - left: "-23px", - bottom: "10px", - }; - case "right-top": - return { - transform: `rotate(180deg)`, - left: "-23px", - top: "10px", - }; - case "left-bottom": - return { - transform: `rotate(0deg)`, - right: "-23px", - bottom: "10px", - }; - case "left-top": - return { - transform: `rotate(0deg)`, - right: "-23px", - top: "10px", - }; - default: - return {}; // Default case if no side is specified - } - }; // - - // Card Arrow @@ -534,12 +326,22 @@ const Onborda: React.FC = ({ {children}
+ {/* Onborda Tour */} + {/*
*/} + {/* {TourComponent && currentTourSteps && (*/} + {/*
*/} + {/* */} + {/*
*/} + {/* )}*/} + {/*
*/} + {/* Onborda Overlay Step Content */} {pointerPosition && isOnbordaVisible && CardComponent && ( = ({
+ {TourComponent && currentTourSteps && ( + + + + + + )} )} - -
- {TourComponent && currentTourSteps && ( -
- -
- )} -
); }; diff --git a/src/OnbordaStyles.tsx b/src/OnbordaStyles.tsx new file mode 100644 index 0000000..757128c --- /dev/null +++ b/src/OnbordaStyles.tsx @@ -0,0 +1,168 @@ +import React from "react"; + +export const getCardStyle: (side: string) => React.CSSProperties = (side: string) => { + switch (side) { + case "top": + return { + transform: `translate(-50%, 0)`, + left: "50%", + bottom: "100%", + marginBottom: "25px", + }; + case "bottom": + return { + transform: `translate(-50%, 0)`, + left: "50%", + top: "100%", + marginTop: "25px", + }; + case "left": + return { + transform: `translate(0, -50%)`, + right: "100%", + top: "50%", + marginRight: "25px", + }; + case "right": + return { + transform: `translate(0, -50%)`, + left: "100%", + top: "50%", + marginLeft: "25px", + }; + case "top-left": + return { + bottom: "100%", + marginBottom: "25px", + }; + case "top-right": + return { + right: 0, + bottom: "100%", + marginBottom: "25px", + }; + case "bottom-left": + return { + top: "100%", + marginTop: "25px", + }; + case "bottom-right": + return { + right: 0, + top: "100%", + marginTop: "25px", + }; + case "right-bottom": + return { + left: "100%", + bottom: 0, + marginLeft: "25px", + }; + case "right-top": + return { + left: "100%", + top: 0, + marginLeft: "25px", + }; + case "left-bottom": + return { + right: "100%", + bottom: 0, + marginRight: "25px", + }; + case "left-top": + return { + right: "100%", + top: 0, + marginRight: "25px", + }; + default: + // Default case if no side is specified. Center the card to the screen + return { + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', // Center the card + position: 'fixed', // Make sure it's positioned relative to the viewport + margin: '0', + }; + } +} + +export const getArrowStyle: (side: string) => React.CSSProperties = (side: string) => { + switch (side) { + case "bottom": + return { + transform: `translate(-50%, 0) rotate(270deg)`, + left: "50%", + top: "-23px", + }; + case "top": + return { + transform: `translate(-50%, 0) rotate(90deg)`, + left: "50%", + bottom: "-23px", + }; + case "right": + return { + transform: `translate(0, -50%) rotate(180deg)`, + top: "50%", + left: "-23px", + }; + case "left": + return { + transform: `translate(0, -50%) rotate(0deg)`, + top: "50%", + right: "-23px", + }; + case "top-left": + return { + transform: `rotate(90deg)`, + left: "10px", + bottom: "-23px", + }; + case "top-right": + return { + transform: `rotate(90deg)`, + right: "10px", + bottom: "-23px", + }; + case "bottom-left": + return { + transform: `rotate(270deg)`, + left: "10px", + top: "-23px", + }; + case "bottom-right": + return { + transform: `rotate(270deg)`, + right: "10px", + top: "-23px", + }; + case "right-bottom": + return { + transform: `rotate(180deg)`, + left: "-23px", + bottom: "10px", + }; + case "right-top": + return { + transform: `rotate(180deg)`, + left: "-23px", + top: "10px", + }; + case "left-bottom": + return { + transform: `rotate(0deg)`, + right: "-23px", + bottom: "10px", + }; + case "left-top": + return { + transform: `rotate(0deg)`, + right: "-23px", + top: "10px", + }; + default: + return {}; // Default case if no side is specified + } +}; diff --git a/src/types/index.ts b/src/types/index.ts index 6161d70..9ff8ce8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,16 +37,37 @@ export interface Tour { } // Onborda +// export interface OnbordaProps { - children: React.ReactNode; - steps: Tour[]; - showOnborda?: boolean; - shadowRgb?: string; - shadowOpacity?: string; - cardTransition?: Transition; - cardComponent?: React.ComponentType; - tourComponent?: React.ComponentType; - debug?: boolean; + // The children elements to be rendered inside the Onborda component + children: React.ReactNode; + + // An array of tours, each containing multiple steps + steps: Tour[]; + + // Flag to show or hide the Onborda component + showOnborda?: boolean; + + // RGB value for the shadow color + shadowRgb?: string; + + // Opacity value for the shadow + shadowOpacity?: string; + + // Transition settings for the card component + cardTransition?: Transition; + + // Custom card component to be used in the Onborda + cardComponent?: React.ComponentType; + + // Custom tour component to be used in the Onborda + tourComponent?: React.ComponentType; + + // Flag to enable or disable debug mode + debug?: boolean; + + // Timeout value for the observer when observing for the target element + observerTimeout?: number; } // Custom Card From 5a523b7e04dfbba0fcae3701fbfd8cce52fcbdcf Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Wed, 6 Nov 2024 14:59:23 +0000 Subject: [PATCH 11/30] Added nextStepConditions to Step which enabled the ability to restrict the next step by user-provided condidtions --- dist/Onborda.js | 28 ++++++++++++- dist/types/index.d.ts | 32 +++++++++++++++ src/Onborda.tsx | 42 +++++++++++++++---- src/types/index.ts | 93 ++++++++++++++++++++++++++++--------------- 4 files changed, 156 insertions(+), 39 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 6db66dd..c49952d 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -17,6 +17,7 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); + const [canProceed, setCanProceed] = useState(true); const offset = 20; const hasSelector = (step) => { return !!step?.selector || !!step?.customQuerySelector; @@ -71,6 +72,31 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" } }; }, [currentStep, currentTourSteps, offset, isOnbordaVisible]); + // Update the canProceed state based on the nextStepConditions + useEffect(() => { + const step = currentTourSteps?.[currentStep]; + const element = step ? getStepSelectorElement(step) : null; + if (element && step?.nextStepConditions) { + const handleInteraction = () => { + const canProceed = step?.nextStepConditions?.(element) ?? true; + setCanProceed(canProceed); + }; + // Initial check + handleInteraction(); + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + return () => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + }; + } + else { + setCanProceed(true); + } + }, [currentStep, currentTourSteps]); // - - // Helper function to get element position const getElementPosition = (element) => { @@ -282,6 +308,6 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }) }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) }) }))] }))] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), canProceed: canProceed }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) }) }))] }))] })); }; export default Onborda; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 87ab4f6..6ae1b16 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -8,17 +8,31 @@ export interface OnbordaContextType { isOnbordaVisible: boolean; } export interface Step { + /** The icon to be displayed in the step */ icon: React.ReactNode | string | null; + /** The title of the step */ title: string; + /** The content to be displayed in the step */ content: React.ReactNode; + /** The CSS selector for the element to highlight. Takes precedence over customQuerySelector if both are provided. */ selector?: string; + /** A custom function to query the target element. Ignored if selector is provided. */ customQuerySelector?: () => Element | null; + /** The side where the step should be displayed */ side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; + /** Flag to show or hide the controls */ showControls?: boolean; + /** Padding around the pointer */ pointerPadding?: number; + /** Radius of the pointer */ pointerRadius?: number; + /** Flag to make the step interactable */ interactable?: boolean; + /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ + nextStepConditions?: (element: Element | null) => boolean; + /** The route to navigate to for the next step */ nextRoute?: string; + /** The route to navigate to for the previous step */ prevRoute?: string; } export interface Tour { @@ -26,24 +40,42 @@ export interface Tour { steps: Step[]; } export interface OnbordaProps { + /** The children elements to be rendered inside the Onborda component */ children: React.ReactNode; + /** An array of tours, each containing multiple steps */ steps: Tour[]; + /** Flag to show or hide the Onborda component */ showOnborda?: boolean; + /** RGB value for the shadow color */ shadowRgb?: string; + /** Opacity value for the shadow */ shadowOpacity?: string; + /** Transition settings for the card component */ cardTransition?: Transition; + /** Custom card component to be used in the Onborda */ cardComponent?: React.ComponentType; + /** Custom tour component to be used in the Onborda */ tourComponent?: React.ComponentType; + /** Flag to enable or disable debug mode */ debug?: boolean; + /** Timeout value for the observer when observing for the target element */ observerTimeout?: number; } export interface CardComponentProps { + /** The current step object containing details of the step */ step: Step; + /** The index of the current step */ currentStep: number; + /** The total number of steps in the tour */ totalSteps: number; + /** Function to navigate to the next step */ nextStep: () => void; + /** Function to navigate to the previous step */ prevStep: () => void; + /** The arrow element to be displayed in the card */ arrow: JSX.Element; + /** Boolean for whether the nextStepConditions are met */ + canProceed: boolean; } export interface TourComponentProps { currentTour: string | null; diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 872b664..4c6965c 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -38,6 +38,7 @@ const Onborda: React.FC = ({ height: number; } | null>(null); const currentElementRef = useRef(null); + const [canProceed, setCanProceed] = useState(true); const offset = 20; const hasSelector = (step: Step): boolean => { @@ -70,7 +71,7 @@ const Onborda: React.FC = ({ htmlElement.style.pointerEvents = "auto"; } - }else { + } else { // if the element is not found, place the pointer at the center of the screen setPointerPosition({ x: window.innerWidth / 2, @@ -97,6 +98,34 @@ const Onborda: React.FC = ({ } }, [currentStep, currentTourSteps, offset, isOnbordaVisible]); + // Update the canProceed state based on the nextStepConditions + useEffect(() => { + const step = currentTourSteps?.[currentStep]; + const element = step ? getStepSelectorElement(step) : null; + + if (element && step?.nextStepConditions) { + const handleInteraction = () => { + const canProceed = step?.nextStepConditions?.(element) ?? true; + setCanProceed(canProceed); + }; + // Initial check + handleInteraction(); + + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + + return () => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + }; + }else { + setCanProceed(true); + } + }, [currentStep, currentTourSteps]); + // - - // Helper function to get element position const getElementPosition = (element: Element) => { @@ -188,7 +217,7 @@ const Onborda: React.FC = ({ } else { debug && console.log("Onborda: Observing for element...", currentTourSteps[nextStepIndex]); } - }else { + } else { console.log("Onborda: No selector set for next step while observing", currentTourSteps[nextStepIndex]); setCurrentStep(nextStepIndex); observer.disconnect(); @@ -250,7 +279,7 @@ const Onborda: React.FC = ({ // Continue observing until the element is found debug && console.log("Onborda: Observing for element...", currentTourSteps[prevStepIndex]); } - }else { + } else { debug && console.log("Onborda: No selector set for previous step while observing", currentTourSteps[prevStepIndex]); setCurrentStep(prevStepIndex); observer.disconnect(); @@ -289,7 +318,7 @@ const Onborda: React.FC = ({ // - - // Card Arrow - const CardArrow = ({ isVisible }: { isVisible: boolean }) => { + const CardArrow = ({isVisible}: { isVisible: boolean }) => { if (!isVisible) { return null; } @@ -390,9 +419,8 @@ const Onborda: React.FC = ({ totalSteps={currentTourSteps?.length ?? 0} nextStep={nextStep} prevStep={prevStep} - arrow={} + arrow={} + canProceed={canProceed} /> diff --git a/src/types/index.ts b/src/types/index.ts index 9ff8ce8..a639bd2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,21 +12,38 @@ export interface OnbordaContextType { // Step export interface Step { - // Step Content - icon: React.ReactNode | string | null; - title: string; - content: React.ReactNode; - selector?: string; - customQuerySelector?: () => Element | null; - // Options - side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; - showControls?: boolean; - pointerPadding?: number; - pointerRadius?: number; - interactable?: boolean; - // Routing - nextRoute?: string; - prevRoute?: string; + // Step Content + /** The icon to be displayed in the step */ + icon: React.ReactNode | string | null; + /** The title of the step */ + title: string; + /** The content to be displayed in the step */ + content: React.ReactNode; + /** The CSS selector for the element to highlight. Takes precedence over customQuerySelector if both are provided. */ + selector?: string; + /** A custom function to query the target element. Ignored if selector is provided. */ + customQuerySelector?: () => Element | null; + + // Options + /** The side where the step should be displayed */ + side?: "top" | "bottom" | "left" | "right" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom" | "right-top" | "right-bottom"; + /** Flag to show or hide the controls */ + showControls?: boolean; + /** Padding around the pointer */ + pointerPadding?: number; + /** Radius of the pointer */ + pointerRadius?: number; + /** Flag to make the step interactable */ + interactable?: boolean; + /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ + nextStepConditions?: (element: Element | null) => boolean; + + // Routing + /** The route to navigate to for the next step */ + nextRoute?: string; + /** The route to navigate to for the previous step */ + prevRoute?: string; + } // Tour @@ -39,45 +56,59 @@ export interface Tour { // Onborda // export interface OnbordaProps { - // The children elements to be rendered inside the Onborda component + /** The children elements to be rendered inside the Onborda component */ children: React.ReactNode; - // An array of tours, each containing multiple steps + /** An array of tours, each containing multiple steps */ steps: Tour[]; - // Flag to show or hide the Onborda component + /** Flag to show or hide the Onborda component */ showOnborda?: boolean; - // RGB value for the shadow color + /** RGB value for the shadow color */ shadowRgb?: string; - // Opacity value for the shadow + /** Opacity value for the shadow */ shadowOpacity?: string; - // Transition settings for the card component + /** Transition settings for the card component */ cardTransition?: Transition; - // Custom card component to be used in the Onborda + /** Custom card component to be used in the Onborda */ cardComponent?: React.ComponentType; - // Custom tour component to be used in the Onborda + /** Custom tour component to be used in the Onborda */ tourComponent?: React.ComponentType; - // Flag to enable or disable debug mode + /** Flag to enable or disable debug mode */ debug?: boolean; - // Timeout value for the observer when observing for the target element + /** Timeout value for the observer when observing for the target element */ observerTimeout?: number; } // Custom Card export interface CardComponentProps { - step: Step; - currentStep: number; - totalSteps: number; - nextStep: () => void; - prevStep: () => void; - arrow: JSX.Element; + /** The current step object containing details of the step */ + step: Step; + + /** The index of the current step */ + currentStep: number; + + /** The total number of steps in the tour */ + totalSteps: number; + + /** Function to navigate to the next step */ + nextStep: () => void; + + /** Function to navigate to the previous step */ + prevStep: () => void; + + /** The arrow element to be displayed in the card */ + arrow: JSX.Element; + + /** Boolean for whether the nextStepConditions are met */ + canProceed: boolean; } // Tour Component From fff97398f360d0203d7acdd6d3d9609f7527e03e Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Wed, 6 Nov 2024 15:39:40 +0000 Subject: [PATCH 12/30] Updated README --- README.md | 104 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 1f0bfc9..b877687 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ export const CustomCard = ({ nextStep, prevStep, arrow, + canProceed }: CardComponentProps) => { return (
@@ -73,7 +74,7 @@ export const CustomCard = ({

{currentStep} of {totalSteps}

{step.content}

- + {arrow}
) @@ -130,20 +131,36 @@ Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" ### Step object -| Prop | Type | Description | -|-----------------------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | -| `title` | `string` | The title of your step | -| `content` | `React.ReactNode` | The main content or body of the step. | -| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | -| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | -| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | -| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | -| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | -| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | -| `nextRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the next step. | -| `prevRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the previous step. | -| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | +| Prop | Type | Description | +|-----------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | +| `title` | `string` | The title of your step | +| `content` | `React.ReactNode` | The main content or body of the step. | +| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | +| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | +| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | +| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | +| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | +| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | +| `nextRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the next step. | +| `prevRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the previous step. | +| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | +| `nextStepConditions` | `(element: Element \| null)=>boolean` | Optional. A client function that returns a boolean thats passed to the CardComponents `canProceed` prop. Gets bound to an event listener on the target element on `click`,`input` and `change`. | + +```tsx +{ + icon: <>👋, + title: "Tour 1, Step 1", + content: <>First tour, first step, + selector: "#tour1-step1", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + nextRoute: "/foo", + prevRoute: "/bar" +} +``` ### Example `steps` @@ -162,12 +179,39 @@ Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" pointerRadius: 10, nextRoute: "/foo", prevRoute: "/bar" + }, + { + icon: <>👋, + title: "Tour 1, Step 2", + content: <>First tour, second step. To proceed please provide a value, + selector: "#tour1-step2-input", // target the input + nextStepConditions: (element) => ((element as HTMLInputElement)?.value?.trim() !== ''), // only allow next step if targatted element has a value + interactable: true, // allow user to interact with the input + side: "right", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + nextRoute: "/foo", + prevRoute: "/bar" + }, + { + icon: <>👋, + title: "Tour 1, Step 3", + content: <>Thanks for settings the value, + selector: "#tour1-step3", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + nextRoute: "/foo", + prevRoute: "/bar" } ... ], tour: "secondtour", steps: [ - icon: <>👋👋, + { + icon: <>👋👋, title: "Second tour, Step 1", content: <>Second tour, first step!, //selector: "#onborda-step1", @@ -177,25 +221,27 @@ Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" pointerPadding: 10, pointerRadius: 10, nextRoute: "/foo", - prevRoute: "/bar", - interactable: true + prevRoute: "/bar", + interactable: true, + } ] } ``` ### Onborda Props -| Property | Type | Description | -|------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `children` | `React.ReactNode` | Your website or application content. | -| `steps` | `Array[]` | An array of `Step` objects defining each step of the onboarding process. | -| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | -| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | -| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | -| `cardTransition` | `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | -| `cardComponent` | `React.ReactNode` | The React component to use as the card for each step. | -| `tourComponent` | `React.ReactNode` | The React component to use as a list of steps for the current tour. | -| `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | +| Property | Type | Description | +|------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `steps` | `Array[]` | An array of `Step` objects defining each step of the onboarding process. | +| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | +| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | +| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | +| `cardTransition` | `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | +| `cardComponent` | `React.ReactNode` | The React component to use as the card for each step. | +| `tourComponent` | `React.ReactNode` | The React component to use as a list of steps for the current tour. | +| `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | +| `observerTimeout`| `number` | Optional. The timeout in milliseconds for the observer to wait for the target element to be available. Defaults to `5000`. Observer is used to wait for the target element to be available before proceeding to the next step e.g. when the target element is on a different route. | ```tsx From d705077f824386fa37e46ca51d7fc62ebbb347e2 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Mon, 11 Nov 2024 13:22:19 +0000 Subject: [PATCH 13/30] Added support for setting any step as current from TourCard. Deprecated .nextRoute and .prevRoute in favour of .route Deprecated Onborda.steps in favor of OnbordaProvider.tours. currentTourSteps now stored in useState within useOnborda. --- dist/Onborda.js | 74 ++++++++++++++++++++++++++++++++----- dist/OnbordaContext.d.ts | 6 +-- dist/OnbordaContext.js | 14 ++++++- dist/types/index.d.ts | 34 ++++++++++++++++- src/Onborda.tsx | 79 +++++++++++++++++++++++++++++++++++----- src/OnbordaContext.tsx | 19 ++++++++-- src/types/index.ts | 57 +++++++++++++++++++++++------ 7 files changed, 242 insertions(+), 41 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index c49952d..140ca95 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -11,9 +11,8 @@ import { getCardStyle, getArrowStyle } from "./OnbordaStyles"; * @param {OnbordaProps} props * @constructor */ -const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, observerTimeout = 5000, }) => { - const { currentTour, currentStep, setCurrentStep, isOnbordaVisible } = useOnborda(); - const currentTourSteps = steps.find((tour) => tour.tour === currentTour)?.steps; +const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, observerTimeout = 5000, }) => { + const { currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps } = useOnborda(); const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); @@ -58,9 +57,10 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" currentElementRef.current = null; } // Prefetch the next route - if (step?.nextRoute) { - debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); - router.prefetch(step.nextRoute); + const nextStep = currentTourSteps[currentStep + 1]; + if (nextStep && nextStep?.route) { + debug && console.log("Onborda: Prefetching Next Route", nextStep.route); + router.prefetch(nextStep.route); } } } @@ -162,7 +162,7 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" if (currentTourSteps && currentStep < currentTourSteps.length - 1) { try { const nextStepIndex = currentStep + 1; - const route = currentTourSteps[currentStep].nextRoute; + const route = currentTourSteps[nextStepIndex].route; if (route) { // Trigger the next route await router.push(route); @@ -218,7 +218,7 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" if (currentTourSteps && currentStep > 0) { try { const prevStepIndex = currentStep - 1; - const route = currentTourSteps[currentStep].prevRoute; + const route = currentTourSteps[prevStepIndex].route; if (route) { // Trigger the previous route await router.push(route); @@ -271,6 +271,62 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" } } }; + const setStep = async (step) => { + if (currentTourSteps) { + try { + const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; + const route = currentTourSteps[setStepIndex].route; + if (route) { + // Trigger the next route + await router.push(route); + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[setStepIndex]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[setStepIndex]); + if (element) { + // Once the element is found, update the step and scroll to the element + setCurrentStep(setStepIndex); + // Stop observing after the element is found + observer.disconnect(); + } + else { + debug && console.log("Onborda: Observing for element...", currentTourSteps[setStepIndex]); + } + } + else { + console.log("Onborda: No selector set for next step while observing", currentTourSteps[setStepIndex]); + setCurrentStep(setStepIndex); + observer.disconnect(); + } + }); + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; + } + else { + // If no route is set, update the step instantly + setCurrentStep(setStepIndex); + } + } + catch (error) { + console.error("Error navigating to next route", error); + } + } + }; // - - // Card Arrow const CardArrow = ({ isVisible }) => { @@ -308,6 +364,6 @@ const Onborda = ({ children, steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2" width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), canProceed: canProceed }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep }) }) }))] }))] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), canProceed: canProceed }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep, setStep: setStep }) }) }))] }))] })); }; export default Onborda; diff --git a/dist/OnbordaContext.d.ts b/dist/OnbordaContext.d.ts index a7ef976..b2f432e 100644 --- a/dist/OnbordaContext.d.ts +++ b/dist/OnbordaContext.d.ts @@ -1,7 +1,5 @@ import React from "react"; -import { OnbordaContextType } from "./types"; +import { OnbordaContextType, OnbordaProviderProps } from "./types"; declare const useOnborda: () => OnbordaContextType; -declare const OnbordaProvider: React.FC<{ - children: React.ReactNode; -}>; +declare const OnbordaProvider: React.FC; export { OnbordaProvider, useOnborda }; diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index dce5940..8a0e4f2 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -16,11 +16,21 @@ const useOnborda = () => { } return context; }; -const OnbordaProvider = ({ children, }) => { +const OnbordaProvider = ({ children, tours = [], }) => { const [currentTour, setCurrentTour] = useState(null); const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); + const [currentTourSteps, setCurrentTourSteps] = useState([]); const setCurrentStep = useCallback((step, delay) => { + // If step is a string, find the index of the step with that id + if (typeof step === 'string') { + const index = currentTourSteps.findIndex((s) => s?.id === step); + if (index === -1) { + throw new Error(`Step with id ${step} not found`); + } + step = index; + } + console.log('setCurrentStep', step); if (delay) { setTimeout(() => { setCurrentStepState(step); @@ -39,11 +49,13 @@ const OnbordaProvider = ({ children, }) => { const startOnborda = useCallback((tourName) => { setCurrentTour(tourName); setCurrentStepState(0); + setCurrentTourSteps(tours.find((tour) => tour.tour === tourName)?.steps || []); setOnbordaVisible(true); }, []); return (_jsx(OnbordaContext.Provider, { value: { currentTour, currentStep, + currentTourSteps, setCurrentStep, closeOnborda, startOnborda, diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 6ae1b16..76f14e4 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -1,13 +1,29 @@ import { Transition } from "framer-motion"; +export interface OnbordaProviderProps { + /** The children elements to be rendered inside the OnbordaProvider component */ + children: React.ReactNode; + /** An array of tours, each containing multiple steps */ + tours: Tour[]; +} export interface OnbordaContextType { + /** current step index */ currentStep: number; + /** current tour name */ currentTour: string | null; - setCurrentStep: (step: number, delay?: number) => void; + /** current tour steps */ + currentTourSteps: Step[]; + /** function to set the current step */ + setCurrentStep: (step: number | string, delay?: number) => void; + /** function to close Onborda */ closeOnborda: () => void; + /** function to start Onborda */ startOnborda: (tourName: string) => void; + /** flag to check if Onborda is visible */ isOnbordaVisible: boolean; } export interface Step { + /** The unique identifier for the step */ + id?: string; /** The icon to be displayed in the step */ icon: React.ReactNode | string | null; /** The title of the step */ @@ -30,20 +46,27 @@ export interface Step { interactable?: boolean; /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ nextStepConditions?: (element: Element | null) => boolean; + /** The route for this step */ + route?: string; /** The route to navigate to for the next step */ + /** @deprecated Use `route` instead */ nextRoute?: string; /** The route to navigate to for the previous step */ + /** @deprecated Use `route` instead */ prevRoute?: string; } export interface Tour { + /** The name of the tour */ tour: string; + /** An array of steps in the tour */ steps: Step[]; } export interface OnbordaProps { /** The children elements to be rendered inside the Onborda component */ children: React.ReactNode; /** An array of tours, each containing multiple steps */ - steps: Tour[]; + /** @deprecated Use `OnbordaProvider.tours` instead */ + steps?: Tour[]; /** Flag to show or hide the Onborda component */ showOnborda?: boolean; /** RGB value for the shadow color */ @@ -68,6 +91,8 @@ export interface CardComponentProps { currentStep: number; /** The total number of steps in the tour */ totalSteps: number; + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; /** Function to navigate to the next step */ nextStep: () => void; /** Function to navigate to the previous step */ @@ -78,7 +103,12 @@ export interface CardComponentProps { canProceed: boolean; } export interface TourComponentProps { + /** The current tour name */ currentTour: string | null; + /** The index of the current step */ currentStep: number; + /** An array of steps in the current tour */ steps: Step[]; + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; } diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 4c6965c..bd4c7bc 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -16,7 +16,6 @@ import {getCardStyle, getArrowStyle} from "./OnbordaStyles"; */ const Onborda: React.FC = ({ children, - steps, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = {ease: "anticipate", duration: 0.6}, @@ -25,10 +24,7 @@ const Onborda: React.FC = ({ debug = false, observerTimeout = 5000, }: OnbordaProps) => { - const {currentTour, currentStep, setCurrentStep, isOnbordaVisible} = useOnborda(); - const currentTourSteps = steps.find( - (tour) => tour.tour === currentTour - )?.steps; + const {currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps} = useOnborda(); const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState<{ @@ -83,9 +79,10 @@ const Onborda: React.FC = ({ currentElementRef.current = null; } // Prefetch the next route - if (step?.nextRoute) { - debug && console.log("Onborda: Prefetching Next Route", step.nextRoute); - router.prefetch(step.nextRoute); + const nextStep = currentTourSteps[currentStep + 1]; + if (nextStep && nextStep?.route) { + debug && console.log("Onborda: Prefetching Next Route", nextStep.route); + router.prefetch(nextStep.route); } } } @@ -194,7 +191,7 @@ const Onborda: React.FC = ({ if (currentTourSteps && currentStep < currentTourSteps.length - 1) { try { const nextStepIndex = currentStep + 1; - const route = currentTourSteps[currentStep].nextRoute; + const route = currentTourSteps[nextStepIndex].route; if (route) { @@ -257,7 +254,7 @@ const Onborda: React.FC = ({ if (currentTourSteps && currentStep > 0) { try { const prevStepIndex = currentStep - 1; - const route = currentTourSteps[currentStep].prevRoute; + const route = currentTourSteps[prevStepIndex].route; if (route) { @@ -315,6 +312,66 @@ const Onborda: React.FC = ({ } }; + const setStep = async (step: number | string) => { + if (currentTourSteps){ + try { + const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; + const route = currentTourSteps[setStepIndex].route; + + if (route) { + // Trigger the next route + await router.push(route); + + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[setStepIndex]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[setStepIndex]); + if (element) { + // Once the element is found, update the step and scroll to the element + setCurrentStep(setStepIndex); + + // Stop observing after the element is found + observer.disconnect(); + } else { + debug && console.log("Onborda: Observing for element...", currentTourSteps[setStepIndex]); + } + } else { + console.log("Onborda: No selector set for next step while observing", currentTourSteps[setStepIndex]); + setCurrentStep(setStepIndex); + observer.disconnect(); + } + }); + + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; + + } else { + // If no route is set, update the step instantly + setCurrentStep(setStepIndex); + } + } catch (error) { + console.error("Error navigating to next route", error); + } + } + } + // - - // Card Arrow @@ -419,6 +476,7 @@ const Onborda: React.FC = ({ totalSteps={currentTourSteps?.length ?? 0} nextStep={nextStep} prevStep={prevStep} + setStep={setStep} arrow={} canProceed={canProceed} /> @@ -438,6 +496,7 @@ const Onborda: React.FC = ({ steps={currentTourSteps} currentTour={currentTour} currentStep={currentStep} + setStep={setStep} /> diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index c3c48c7..57beb74 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useState, useCallback } from "react"; // Types -import { OnbordaContextType } from "./types"; +import {OnbordaContextType, OnbordaProviderProps, Step, Tour} from "./types"; // Example Hooks Usage: // const { setCurrentStep, closeOnborda, startOnborda } = useOnborda(); @@ -24,14 +24,25 @@ const useOnborda = () => { return context; }; -const OnbordaProvider: React.FC<{ children: React.ReactNode }> = ({ +const OnbordaProvider: React.FC = ({ children, + tours = [], }) => { const [currentTour, setCurrentTour] = useState(null); const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); + const [currentTourSteps, setCurrentTourSteps] = useState([]); - const setCurrentStep = useCallback((step: number, delay?: number) => { + const setCurrentStep = useCallback((step: number | string, delay?: number) => { + // If step is a string, find the index of the step with that id + if (typeof step === 'string') { + const index = currentTourSteps.findIndex((s) => s?.id === step); + if (index === -1) { + throw new Error(`Step with id ${step} not found`); + } + step = index; + } + console.log('setCurrentStep', step); if (delay) { setTimeout(() => { setCurrentStepState(step); @@ -51,6 +62,7 @@ const OnbordaProvider: React.FC<{ children: React.ReactNode }> = ({ const startOnborda = useCallback((tourName: string) => { setCurrentTour(tourName); setCurrentStepState(0); + setCurrentTourSteps(tours.find((tour) => tour.tour === tourName)?.steps || []); setOnbordaVisible(true); }, []); @@ -59,6 +71,7 @@ const OnbordaProvider: React.FC<{ children: React.ReactNode }> = ({ value={{ currentTour, currentStep, + currentTourSteps, setCurrentStep, closeOnborda, startOnborda, diff --git a/src/types/index.ts b/src/types/index.ts index a639bd2..1bd857c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,18 +1,36 @@ import { Transition } from "framer-motion"; +// Provider +export interface OnbordaProviderProps { + /** The children elements to be rendered inside the OnbordaProvider component */ + children: React.ReactNode; + /** An array of tours, each containing multiple steps */ + tours: Tour[]; +} + // Context export interface OnbordaContextType { - currentStep: number; - currentTour: string | null; - setCurrentStep: (step: number, delay?: number) => void; - closeOnborda: () => void; - startOnborda: (tourName: string) => void; - isOnbordaVisible: boolean; + /** current step index */ + currentStep: number; + /** current tour name */ + currentTour: string | null; + /** current tour steps */ + currentTourSteps: Step[]; + /** function to set the current step */ + setCurrentStep: (step: number | string, delay?: number) => void; + /** function to close Onborda */ + closeOnborda: () => void; + /** function to start Onborda */ + startOnborda: (tourName: string) => void; + /** flag to check if Onborda is visible */ + isOnbordaVisible: boolean; } // Step export interface Step { // Step Content + /** The unique identifier for the step */ + id?: string; /** The icon to be displayed in the step */ icon: React.ReactNode | string | null; /** The title of the step */ @@ -39,9 +57,13 @@ export interface Step { nextStepConditions?: (element: Element | null) => boolean; // Routing + /** The route for this step */ + route?: string; /** The route to navigate to for the next step */ + /** @deprecated Use `route` instead */ nextRoute?: string; /** The route to navigate to for the previous step */ + /** @deprecated Use `route` instead */ prevRoute?: string; } @@ -49,8 +71,10 @@ export interface Step { // Tour // export interface Tour { - tour: string; - steps: Step[]; + /** The name of the tour */ + tour: string; + /** An array of steps in the tour */ + steps: Step[]; } // Onborda @@ -60,7 +84,8 @@ export interface OnbordaProps { children: React.ReactNode; /** An array of tours, each containing multiple steps */ - steps: Tour[]; + /** @deprecated Use `OnbordaProvider.tours` instead */ + steps?: Tour[]; /** Flag to show or hide the Onborda component */ showOnborda?: boolean; @@ -98,6 +123,9 @@ export interface CardComponentProps { /** The total number of steps in the tour */ totalSteps: number; + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; + /** Function to navigate to the next step */ nextStep: () => void; @@ -113,7 +141,12 @@ export interface CardComponentProps { // Tour Component export interface TourComponentProps { - currentTour: string | null; - currentStep: number; - steps: Step[]; + /** The current tour name */ + currentTour: string | null; + /** The index of the current step */ + currentStep: number; + /** An array of steps in the current tour */ + steps: Step[]; + /** Function to set the current step by step index or step.id */ + setStep: (step: number | string) => void; } From 6f17dd2a1ef631812a53031d4595010704340fa9 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Mon, 11 Nov 2024 13:30:19 +0000 Subject: [PATCH 14/30] Fixed observer timeout when new route is the same as current route --- dist/Onborda.js | 9 ++++++--- dist/OnbordaContext.js | 1 - src/Onborda.tsx | 10 ++++++---- src/OnbordaContext.tsx | 1 - 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 140ca95..0f9bf8a 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -163,7 +163,8 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT try { const nextStepIndex = currentStep + 1; const route = currentTourSteps[nextStepIndex].route; - if (route) { + // Check if the route is set and different from the current route + if (route && currentTourSteps[currentStep].route !== route) { // Trigger the next route await router.push(route); // Use MutationObserver to detect when the target element is available in the DOM @@ -219,7 +220,8 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT try { const prevStepIndex = currentStep - 1; const route = currentTourSteps[prevStepIndex].route; - if (route) { + // Check if the route is set and different from the current route + if (route && currentTourSteps[currentStep].route !== route) { // Trigger the previous route await router.push(route); // Use MutationObserver to detect when the target element is available in the DOM @@ -276,7 +278,8 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT try { const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; const route = currentTourSteps[setStepIndex].route; - if (route) { + // Check if the route is set and different from the current route + if (route && currentTourSteps[currentStep].route !== route) { // Trigger the next route await router.push(route); // Use MutationObserver to detect when the target element is available in the DOM diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index 8a0e4f2..481b2a9 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -30,7 +30,6 @@ const OnbordaProvider = ({ children, tours = [], }) => { } step = index; } - console.log('setCurrentStep', step); if (delay) { setTimeout(() => { setCurrentStepState(step); diff --git a/src/Onborda.tsx b/src/Onborda.tsx index bd4c7bc..bf591db 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -193,8 +193,8 @@ const Onborda: React.FC = ({ const nextStepIndex = currentStep + 1; const route = currentTourSteps[nextStepIndex].route; - - if (route) { + // Check if the route is set and different from the current route + if (route && currentTourSteps[currentStep].route !== route) { // Trigger the next route await router.push(route); @@ -256,7 +256,8 @@ const Onborda: React.FC = ({ const prevStepIndex = currentStep - 1; const route = currentTourSteps[prevStepIndex].route; - if (route) { + // Check if the route is set and different from the current route + if (route && currentTourSteps[currentStep].route !== route) { // Trigger the previous route await router.push(route); @@ -318,7 +319,8 @@ const Onborda: React.FC = ({ const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; const route = currentTourSteps[setStepIndex].route; - if (route) { + // Check if the route is set and different from the current route + if (route && currentTourSteps[currentStep].route !== route) { // Trigger the next route await router.push(route); diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index 57beb74..d4ac824 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -42,7 +42,6 @@ const OnbordaProvider: React.FC = ({ } step = index; } - console.log('setCurrentStep', step); if (delay) { setTimeout(() => { setCurrentStepState(step); From b45b0788c182d9d6a290947f589a2fd38df3a7d0 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Mon, 11 Nov 2024 16:12:34 +0000 Subject: [PATCH 15/30] Added ability to store 'completed' steps in a new useState --- dist/Onborda.js | 14 +++++++------- dist/OnbordaContext.js | 4 ++++ dist/types/index.d.ts | 14 +++++++++++--- src/Onborda.tsx | 15 ++++++++------- src/OnbordaContext.tsx | 4 ++++ src/types/index.ts | 14 +++++++++++--- 6 files changed, 45 insertions(+), 20 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 0f9bf8a..62956e8 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -12,11 +12,10 @@ import { getCardStyle, getArrowStyle } from "./OnbordaStyles"; * @constructor */ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, observerTimeout = 5000, }) => { - const { currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps } = useOnborda(); + const { currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps } = useOnborda(); const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); - const [canProceed, setCanProceed] = useState(true); const offset = 20; const hasSelector = (step) => { return !!step?.selector || !!step?.customQuerySelector; @@ -76,10 +75,11 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT useEffect(() => { const step = currentTourSteps?.[currentStep]; const element = step ? getStepSelectorElement(step) : null; - if (element && step?.nextStepConditions) { + if (element && step?.isCompleteConditions) { const handleInteraction = () => { - const canProceed = step?.nextStepConditions?.(element) ?? true; - setCanProceed(canProceed); + if (step?.isCompleteConditions?.(element) ?? true) { + setCompletedSteps((prev) => { return prev.add(step?.id ?? currentStep); }); + } }; // Initial check handleInteraction(); @@ -94,7 +94,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT }; } else { - setCanProceed(true); + setCompletedSteps((prev) => { return prev.add(step?.id ?? currentStep); }); } }, [currentStep, currentTourSteps]); // - - @@ -367,6 +367,6 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), canProceed: canProceed }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep, setStep: setStep }) }) }))] }))] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), completedSteps: Array.from(completedSteps) }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep, setStep: setStep, completedSteps: Array.from(completedSteps) }) }) }))] }))] })); }; export default Onborda; diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index 481b2a9..fa97d0c 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -21,6 +21,7 @@ const OnbordaProvider = ({ children, tours = [], }) => { const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); const [currentTourSteps, setCurrentTourSteps] = useState([]); + const [completedSteps, setCompletedSteps] = useState(new Set()); const setCurrentStep = useCallback((step, delay) => { // If step is a string, find the index of the step with that id if (typeof step === 'string') { @@ -49,6 +50,7 @@ const OnbordaProvider = ({ children, tours = [], }) => { setCurrentTour(tourName); setCurrentStepState(0); setCurrentTourSteps(tours.find((tour) => tour.tour === tourName)?.steps || []); + setCompletedSteps(new Set([...(tours.find((tour) => tour.tour === tourName)?.completedSteps || [])])); setOnbordaVisible(true); }, []); return (_jsx(OnbordaContext.Provider, { value: { @@ -59,6 +61,8 @@ const OnbordaProvider = ({ children, tours = [], }) => { closeOnborda, startOnborda, isOnbordaVisible, + completedSteps, + setCompletedSteps }, children: children })); }; export { OnbordaProvider, useOnborda }; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 76f14e4..a01760e 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -20,6 +20,10 @@ export interface OnbordaContextType { startOnborda: (tourName: string) => void; /** flag to check if Onborda is visible */ isOnbordaVisible: boolean; + /** default completed steps */ + completedSteps: Set; + /** setstate function to set the completed steps */ + setCompletedSteps: React.Dispatch>>; } export interface Step { /** The unique identifier for the step */ @@ -45,7 +49,7 @@ export interface Step { /** Flag to make the step interactable */ interactable?: boolean; /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ - nextStepConditions?: (element: Element | null) => boolean; + isCompleteConditions?: (element: Element | null) => boolean; /** The route for this step */ route?: string; /** The route to navigate to for the next step */ @@ -60,6 +64,8 @@ export interface Tour { tour: string; /** An array of steps in the tour */ steps: Step[]; + /** Default completed steps */ + completedSteps?: (string | number)[]; } export interface OnbordaProps { /** The children elements to be rendered inside the Onborda component */ @@ -99,8 +105,8 @@ export interface CardComponentProps { prevStep: () => void; /** The arrow element to be displayed in the card */ arrow: JSX.Element; - /** Boolean for whether the nextStepConditions are met */ - canProceed: boolean; + /** Array of completed steps */ + completedSteps: (string | number)[]; } export interface TourComponentProps { /** The current tour name */ @@ -111,4 +117,6 @@ export interface TourComponentProps { steps: Step[]; /** Function to set the current step by step index or step.id */ setStep: (step: number | string) => void; + /** Array of completed steps */ + completedSteps: (string | number)[]; } diff --git a/src/Onborda.tsx b/src/Onborda.tsx index bf591db..1a0a2b5 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -24,7 +24,7 @@ const Onborda: React.FC = ({ debug = false, observerTimeout = 5000, }: OnbordaProps) => { - const {currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps} = useOnborda(); + const {currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps} = useOnborda(); const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState<{ @@ -34,7 +34,6 @@ const Onborda: React.FC = ({ height: number; } | null>(null); const currentElementRef = useRef(null); - const [canProceed, setCanProceed] = useState(true); const offset = 20; const hasSelector = (step: Step): boolean => { @@ -100,10 +99,11 @@ const Onborda: React.FC = ({ const step = currentTourSteps?.[currentStep]; const element = step ? getStepSelectorElement(step) : null; - if (element && step?.nextStepConditions) { + if (element && step?.isCompleteConditions) { const handleInteraction = () => { - const canProceed = step?.nextStepConditions?.(element) ?? true; - setCanProceed(canProceed); + if (step?.isCompleteConditions?.(element) ?? true) { + setCompletedSteps((prev) => {return prev.add(step?.id ?? currentStep);}); + } }; // Initial check handleInteraction(); @@ -119,7 +119,7 @@ const Onborda: React.FC = ({ element.removeEventListener("change", handleInteraction); }; }else { - setCanProceed(true); + setCompletedSteps((prev) => {return prev.add(step?.id ?? currentStep);}); } }, [currentStep, currentTourSteps]); @@ -480,7 +480,7 @@ const Onborda: React.FC = ({ prevStep={prevStep} setStep={setStep} arrow={} - canProceed={canProceed} + completedSteps={Array.from(completedSteps)} /> @@ -499,6 +499,7 @@ const Onborda: React.FC = ({ currentTour={currentTour} currentStep={currentStep} setStep={setStep} + completedSteps={Array.from(completedSteps)} /> diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index d4ac824..55b7974 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -32,6 +32,7 @@ const OnbordaProvider: React.FC = ({ const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); const [currentTourSteps, setCurrentTourSteps] = useState([]); + const [completedSteps, setCompletedSteps] = useState>(new Set()); const setCurrentStep = useCallback((step: number | string, delay?: number) => { // If step is a string, find the index of the step with that id @@ -62,6 +63,7 @@ const OnbordaProvider: React.FC = ({ setCurrentTour(tourName); setCurrentStepState(0); setCurrentTourSteps(tours.find((tour) => tour.tour === tourName)?.steps || []); + setCompletedSteps(new Set([...(tours.find((tour) => tour.tour === tourName)?.completedSteps || [])])); setOnbordaVisible(true); }, []); @@ -75,6 +77,8 @@ const OnbordaProvider: React.FC = ({ closeOnborda, startOnborda, isOnbordaVisible, + completedSteps, + setCompletedSteps }} > {children} diff --git a/src/types/index.ts b/src/types/index.ts index 1bd857c..cbd0290 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,6 +24,10 @@ export interface OnbordaContextType { startOnborda: (tourName: string) => void; /** flag to check if Onborda is visible */ isOnbordaVisible: boolean; + /** default completed steps */ + completedSteps: Set; + /** setstate function to set the completed steps */ + setCompletedSteps: React.Dispatch>>; } // Step @@ -54,7 +58,7 @@ export interface Step { /** Flag to make the step interactable */ interactable?: boolean; /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ - nextStepConditions?: (element: Element | null) => boolean; + isCompleteConditions?: (element: Element | null) => boolean; // Routing /** The route for this step */ @@ -75,6 +79,8 @@ export interface Tour { tour: string; /** An array of steps in the tour */ steps: Step[]; + /** Default completed steps */ + completedSteps?: (string|number)[]; } // Onborda @@ -135,8 +141,8 @@ export interface CardComponentProps { /** The arrow element to be displayed in the card */ arrow: JSX.Element; - /** Boolean for whether the nextStepConditions are met */ - canProceed: boolean; + /** Array of completed steps */ + completedSteps: (string|number)[]; } // Tour Component @@ -149,4 +155,6 @@ export interface TourComponentProps { steps: Step[]; /** Function to set the current step by step index or step.id */ setStep: (step: number | string) => void; + /** Array of completed steps */ + completedSteps: (string|number)[]; } From bec0eecfe37075739c2c8fb24998a4639e52a6e2 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Thu, 14 Nov 2024 11:11:19 +0000 Subject: [PATCH 16/30] Added ability to set pre-completed status per step Each step can now have an initialCompletedState via async function. This will be called when the tour starts via a Promise.all on every step. User will start tour on the first incomplete step. Handling of routing and finding element to focus has been consolidated into the main useEffect to reduce repeated code. nextStep, prevState and setStep now just change the currentStep useState, triggering the useEffect to change route and find the element if set. --- dist/Onborda.js | 323 +++++++++++++++----------------------- dist/OnbordaContext.js | 43 ++++- dist/types/index.d.ts | 2 + src/Onborda.tsx | 345 ++++++++++++++++------------------------- src/OnbordaContext.tsx | 53 +++++-- src/types/index.ts | 2 + 6 files changed, 336 insertions(+), 432 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 62956e8..26e1e53 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -16,6 +16,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); + const [currentRoute, setCurrentRoute] = useState(null); const offset = 20; const hasSelector = (step) => { return !!step?.selector || !!step?.customQuerySelector; @@ -33,19 +34,97 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT debug && console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { - const element = getStepSelectorElement(step); - if (element) { - setPointerPosition(getElementPosition(element)); - setElementToScroll(element); - currentElementRef.current = element; - // Enable pointer events on the element - if (step.interactable) { - const htmlElement = element; - htmlElement.style.pointerEvents = "auto"; + let elementFound = false; + // Check if the step has a selector + if (hasSelector(step)) { + // This step has a selector. Lets find the element + const element = getStepSelectorElement(step); + // Check if the element is found + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element; + htmlElement.style.pointerEvents = "auto"; + } + elementFound = true; + } + // Even if the element is already found, we still need to check if the route is different from the current route + // do we have a route to navigate to? + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || currentRoute !== step.route) { + debug && console.log("Onborda: Navigating to route", step.route); + setCurrentRoute(step.route); + // Trigger the next route + router.push(step.route); + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[currentStep]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[currentStep]); + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element; + htmlElement.style.pointerEvents = "auto"; + } + // Stop observing after the element is found + observer.disconnect(); + } + else { + debug && console.log("Onborda: Observing for element...", currentTourSteps[currentStep]); + } + } + else { + debug && console.log("Onborda: No selector set for next step while observing", currentTourSteps[currentStep]); + setCurrentStep(currentStep); + observer.disconnect(); + } + }); + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; + } + else if (!elementFound) { + console.error("Onborda: Element not found on same route", currentTourSteps[currentStep]); + } } } else { - // if the element is not found, place the pointer at the center of the screen + // no selector, but might still need to navigate to a route + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || currentRoute !== step.route) { + debug && console.log("Onborda: Navigating to route", step.route); + setCurrentRoute(step.route); + // Trigger the next route + router.push(step.route); + } + } + } + // No element set for this step? Place the pointer at the center of the screen + if (!elementFound) { setPointerPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2, @@ -73,30 +152,36 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT }, [currentStep, currentTourSteps, offset, isOnbordaVisible]); // Update the canProceed state based on the nextStepConditions useEffect(() => { - const step = currentTourSteps?.[currentStep]; - const element = step ? getStepSelectorElement(step) : null; - if (element && step?.isCompleteConditions) { - const handleInteraction = () => { - if (step?.isCompleteConditions?.(element) ?? true) { - setCompletedSteps((prev) => { return prev.add(step?.id ?? currentStep); }); - } - }; - // Initial check - handleInteraction(); - element.addEventListener("click", handleInteraction); - element.addEventListener("input", handleInteraction); - element.addEventListener("change", handleInteraction); - return () => { - // Cleanup the event listeners - element.removeEventListener("click", handleInteraction); - element.removeEventListener("input", handleInteraction); - element.removeEventListener("change", handleInteraction); - }; - } - else { - setCompletedSteps((prev) => { return prev.add(step?.id ?? currentStep); }); + if (isOnbordaVisible && currentTourSteps) { + const step = currentTourSteps?.[currentStep]; + const element = step ? getStepSelectorElement(step) : null; + if (element && step?.isCompleteConditions) { + const handleInteraction = () => { + if (step?.isCompleteConditions?.(element) ?? true) { + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + } + }; + // Initial check + handleInteraction(); + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + return () => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + }; + } + else { + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + } } - }, [currentStep, currentTourSteps]); + }, [currentStep, currentTourSteps, isOnbordaVisible]); // - - // Helper function to get element position const getElementPosition = (element) => { @@ -159,176 +244,16 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // - - // Step Controls const nextStep = async () => { - if (currentTourSteps && currentStep < currentTourSteps.length - 1) { - try { - const nextStepIndex = currentStep + 1; - const route = currentTourSteps[nextStepIndex].route; - // Check if the route is set and different from the current route - if (route && currentTourSteps[currentStep].route !== route) { - // Trigger the next route - await router.push(route); - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); - if (shouldSelect) { - const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(nextStepIndex); - // Stop observing after the element is found - observer.disconnect(); - } - else { - debug && console.log("Onborda: Observing for element...", currentTourSteps[nextStepIndex]); - } - } - else { - console.log("Onborda: No selector set for next step while observing", currentTourSteps[nextStepIndex]); - setCurrentStep(nextStepIndex); - observer.disconnect(); - } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - // Set a timeout to disconnect the observer if the element is not found within a certain period - const timeoutId = setTimeout(() => { - observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); - }, observerTimeout); // Adjust the timeout period as needed - // Clear the timeout if the observer disconnects successfully - const originalDisconnect = observer.disconnect.bind(observer); - observer.disconnect = () => { - clearTimeout(timeoutId); - originalDisconnect(); - }; - } - else { - // If no route is set, update the step instantly - setCurrentStep(nextStepIndex); - } - } - catch (error) { - console.error("Error navigating to next route", error); - } - } + const nextStepIndex = currentStep + 1; + await setStep(nextStepIndex); }; const prevStep = async () => { - if (currentTourSteps && currentStep > 0) { - try { - const prevStepIndex = currentStep - 1; - const route = currentTourSteps[prevStepIndex].route; - // Check if the route is set and different from the current route - if (route && currentTourSteps[currentStep].route !== route) { - // Trigger the previous route - await router.push(route); - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); - if (shouldSelect) { - const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(prevStepIndex); - // Stop observing after the element is found - observer.disconnect(); - } - else { - // Continue observing until the element is found - debug && console.log("Onborda: Observing for element...", currentTourSteps[prevStepIndex]); - } - } - else { - debug && console.log("Onborda: No selector set for previous step while observing", currentTourSteps[prevStepIndex]); - setCurrentStep(prevStepIndex); - observer.disconnect(); - } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - // Set a timeout to disconnect the observer if the element is not found within a certain period - const timeoutId = setTimeout(() => { - observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); - }, observerTimeout); // Adjust the timeout period as needed - // Clear the timeout if the observer disconnects successfully - const originalDisconnect = observer.disconnect.bind(observer); - observer.disconnect = () => { - clearTimeout(timeoutId); - originalDisconnect(); - }; - } - else { - // If no route is set, update the step instantly - setCurrentStep(prevStepIndex); - } - } - catch (error) { - console.error("Error navigating to previous route", error); - } - } + const prevStepIndex = currentStep - 1; + await setStep(prevStepIndex); }; const setStep = async (step) => { - if (currentTourSteps) { - try { - const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; - const route = currentTourSteps[setStepIndex].route; - // Check if the route is set and different from the current route - if (route && currentTourSteps[currentStep].route !== route) { - // Trigger the next route - await router.push(route); - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const shouldSelect = hasSelector(currentTourSteps[setStepIndex]); - if (shouldSelect) { - const element = getStepSelectorElement(currentTourSteps[setStepIndex]); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(setStepIndex); - // Stop observing after the element is found - observer.disconnect(); - } - else { - debug && console.log("Onborda: Observing for element...", currentTourSteps[setStepIndex]); - } - } - else { - console.log("Onborda: No selector set for next step while observing", currentTourSteps[setStepIndex]); - setCurrentStep(setStepIndex); - observer.disconnect(); - } - }); - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - // Set a timeout to disconnect the observer if the element is not found within a certain period - const timeoutId = setTimeout(() => { - observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); - }, observerTimeout); // Adjust the timeout period as needed - // Clear the timeout if the observer disconnects successfully - const originalDisconnect = observer.disconnect.bind(observer); - observer.disconnect = () => { - clearTimeout(timeoutId); - originalDisconnect(); - }; - } - else { - // If no route is set, update the step instantly - setCurrentStep(setStepIndex); - } - } - catch (error) { - console.error("Error navigating to next route", error); - } - } + const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; + setCurrentStep(setStepIndex); }; // - - // Card Arrow diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index fa97d0c..d4395a9 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -17,10 +17,10 @@ const useOnborda = () => { return context; }; const OnbordaProvider = ({ children, tours = [], }) => { - const [currentTour, setCurrentTour] = useState(null); + const [currentTour, setCurrentTourState] = useState(null); const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); - const [currentTourSteps, setCurrentTourSteps] = useState([]); + const [currentTourSteps, setCurrentTourStepsState] = useState([]); const [completedSteps, setCompletedSteps] = useState(new Set()); const setCurrentStep = useCallback((step, delay) => { // If step is a string, find the index of the step with that id @@ -44,15 +44,42 @@ const OnbordaProvider = ({ children, tours = [], }) => { }, []); const closeOnborda = useCallback(() => { setOnbordaVisible(false); - setCurrentTour(null); + setCurrentTourState(null); + setCurrentTourStepsState([]); + setCurrentStepState(0); + setCompletedSteps(new Set()); + }, []); + const initializeCompletedSteps = useCallback(async (tourSteps) => { + return Promise.all(tourSteps.map(async (step) => { + return step.initialCompletedState ? step.initialCompletedState() : false; + })).then((results) => { + const firstIncomplete = results.findIndex((result) => !result); + const completed = results.reduce((acc, result, index) => { + if (result) { + acc.push(tourSteps[index]?.id || index); + } + return acc; + }, []); + setCompletedSteps(new Set(completed)); + return firstIncomplete; + }); }, []); + const setCurrentTour = useCallback((tourName) => { + if (!tourName) { + closeOnborda(); + return; + } + setCurrentTourState(tourName); + const tourSteps = tours.find((tour) => tour.tour === tourName)?.steps || []; + setCurrentTourStepsState(tourSteps); + initializeCompletedSteps(tourSteps).then(r => { + setCurrentStep(r); + setOnbordaVisible(true); + }); + }, [tours]); const startOnborda = useCallback((tourName) => { setCurrentTour(tourName); - setCurrentStepState(0); - setCurrentTourSteps(tours.find((tour) => tour.tour === tourName)?.steps || []); - setCompletedSteps(new Set([...(tours.find((tour) => tour.tour === tourName)?.completedSteps || [])])); - setOnbordaVisible(true); - }, []); + }, [setCurrentTour]); return (_jsx(OnbordaContext.Provider, { value: { currentTour, currentStep, diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index a01760e..6b18306 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -50,6 +50,8 @@ export interface Step { interactable?: boolean; /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ isCompleteConditions?: (element: Element | null) => boolean; + /** Initial completed state of the step. an async function called on tour started.*/ + initialCompletedState?: () => Promise; /** The route for this step */ route?: string; /** The route to navigate to for the next step */ diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 1a0a2b5..ff6d87c 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -34,6 +34,7 @@ const Onborda: React.FC = ({ height: number; } | null>(null); const currentElementRef = useRef(null); + const [currentRoute, setCurrentRoute] = useState(null); const offset = 20; const hasSelector = (step: Step): boolean => { @@ -54,20 +55,101 @@ const Onborda: React.FC = ({ debug && console.log("Onborda: Current Step Changed"); const step = currentTourSteps[currentStep]; if (step) { - const element = getStepSelectorElement(step) - if (element) { - setPointerPosition(getElementPosition(element)); - setElementToScroll(element); - currentElementRef.current = element; + let elementFound = false; + // Check if the step has a selector + if (hasSelector(step)) { + // This step has a selector. Lets find the element + const element = getStepSelectorElement(step) + // Check if the element is found + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element as HTMLElement; + htmlElement.style.pointerEvents = "auto"; + } + elementFound = true; + } + // Even if the element is already found, we still need to check if the route is different from the current route + // do we have a route to navigate to? + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || currentRoute !== step.route) { + debug && console.log("Onborda: Navigating to route", step.route); + setCurrentRoute(step.route); + // Trigger the next route + router.push(step.route); + + // Use MutationObserver to detect when the target element is available in the DOM + const observer = new MutationObserver((mutations, observer) => { + const shouldSelect = hasSelector(currentTourSteps[currentStep]); + if (shouldSelect) { + const element = getStepSelectorElement(currentTourSteps[currentStep]); + if (element) { + // Once the element is found, update the step and scroll to the element + setPointerPosition(getElementPosition(element)); + setElementToScroll(element); + currentElementRef.current = element; + + // Enable pointer events on the element + if (step.interactable) { + const htmlElement = element as HTMLElement; + htmlElement.style.pointerEvents = "auto"; + } + + // Stop observing after the element is found + observer.disconnect(); + } else { + debug && console.log("Onborda: Observing for element...", currentTourSteps[currentStep]); + } + } else { + debug && console.log("Onborda: No selector set for next step while observing", currentTourSteps[currentStep]); + setCurrentStep(currentStep); + observer.disconnect(); + } + }); + + // Start observing the document body for changes + observer.observe(document.body, { + childList: true, + subtree: true, + }); - // Enable pointer events on the element - if (step.interactable) { - const htmlElement = element as HTMLElement; - htmlElement.style.pointerEvents = "auto"; + // Set a timeout to disconnect the observer if the element is not found within a certain period + const timeoutId = setTimeout(() => { + observer.disconnect(); + console.error("Onborda: Element not found within the timeout period"); + }, observerTimeout); // Adjust the timeout period as needed + + // Clear the timeout if the observer disconnects successfully + const originalDisconnect = observer.disconnect.bind(observer); + observer.disconnect = () => { + clearTimeout(timeoutId); + originalDisconnect(); + }; + } else if (!elementFound) { + console.error("Onborda: Element not found on same route", currentTourSteps[currentStep]); + } } + }else { + // no selector, but might still need to navigate to a route + if (step.route) { + // Check if the route is set and different from the current route + if (currentRoute == null || currentRoute !== step.route) { + debug && console.log("Onborda: Navigating to route", step.route); + setCurrentRoute(step.route); + // Trigger the next route + router.push(step.route); + } + } + } - } else { - // if the element is not found, place the pointer at the center of the screen + // No element set for this step? Place the pointer at the center of the screen + if (!elementFound) { setPointerPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2, @@ -77,6 +159,7 @@ const Onborda: React.FC = ({ setElementToScroll(null); currentElementRef.current = null; } + // Prefetch the next route const nextStep = currentTourSteps[currentStep + 1]; if (nextStep && nextStep?.route) { @@ -96,32 +179,38 @@ const Onborda: React.FC = ({ // Update the canProceed state based on the nextStepConditions useEffect(() => { - const step = currentTourSteps?.[currentStep]; - const element = step ? getStepSelectorElement(step) : null; - - if (element && step?.isCompleteConditions) { - const handleInteraction = () => { - if (step?.isCompleteConditions?.(element) ?? true) { - setCompletedSteps((prev) => {return prev.add(step?.id ?? currentStep);}); - } - }; - // Initial check - handleInteraction(); - - element.addEventListener("click", handleInteraction); - element.addEventListener("input", handleInteraction); - element.addEventListener("change", handleInteraction); - - return () => { - // Cleanup the event listeners - element.removeEventListener("click", handleInteraction); - element.removeEventListener("input", handleInteraction); - element.removeEventListener("change", handleInteraction); - }; - }else { - setCompletedSteps((prev) => {return prev.add(step?.id ?? currentStep);}); + if (isOnbordaVisible && currentTourSteps) { + const step = currentTourSteps?.[currentStep]; + const element = step ? getStepSelectorElement(step) : null; + + if (element && step?.isCompleteConditions) { + const handleInteraction = () => { + if (step?.isCompleteConditions?.(element) ?? true) { + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + } + }; + // Initial check + handleInteraction(); + + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + + return () => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + }; + } else { + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + } } - }, [currentStep, currentTourSteps]); + }, [currentStep, currentTourSteps, isOnbordaVisible]); // - - // Helper function to get element position @@ -188,190 +277,18 @@ const Onborda: React.FC = ({ // - - // Step Controls const nextStep = async () => { - if (currentTourSteps && currentStep < currentTourSteps.length - 1) { - try { - const nextStepIndex = currentStep + 1; - const route = currentTourSteps[nextStepIndex].route; - - // Check if the route is set and different from the current route - if (route && currentTourSteps[currentStep].route !== route) { - - // Trigger the next route - await router.push(route); - - - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const shouldSelect = hasSelector(currentTourSteps[nextStepIndex]); - if (shouldSelect) { - const element = getStepSelectorElement(currentTourSteps[nextStepIndex]); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(nextStepIndex); - - // Stop observing after the element is found - observer.disconnect(); - } else { - debug && console.log("Onborda: Observing for element...", currentTourSteps[nextStepIndex]); - } - } else { - console.log("Onborda: No selector set for next step while observing", currentTourSteps[nextStepIndex]); - setCurrentStep(nextStepIndex); - observer.disconnect(); - } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - // Set a timeout to disconnect the observer if the element is not found within a certain period - const timeoutId = setTimeout(() => { - observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); - }, observerTimeout); // Adjust the timeout period as needed - - // Clear the timeout if the observer disconnects successfully - const originalDisconnect = observer.disconnect.bind(observer); - observer.disconnect = () => { - clearTimeout(timeoutId); - originalDisconnect(); - }; - - } else { - // If no route is set, update the step instantly - setCurrentStep(nextStepIndex); - } - } catch (error) { - console.error("Error navigating to next route", error); - } - } + const nextStepIndex = currentStep + 1; + await setStep(nextStepIndex); }; const prevStep = async () => { - if (currentTourSteps && currentStep > 0) { - try { - const prevStepIndex = currentStep - 1; - const route = currentTourSteps[prevStepIndex].route; - - // Check if the route is set and different from the current route - if (route && currentTourSteps[currentStep].route !== route) { - - // Trigger the previous route - await router.push(route); - - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const shouldSelect = hasSelector(currentTourSteps[prevStepIndex]); - if (shouldSelect) { - const element = getStepSelectorElement(currentTourSteps[prevStepIndex]); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(prevStepIndex); - - // Stop observing after the element is found - observer.disconnect(); - } else { - // Continue observing until the element is found - debug && console.log("Onborda: Observing for element...", currentTourSteps[prevStepIndex]); - } - } else { - debug && console.log("Onborda: No selector set for previous step while observing", currentTourSteps[prevStepIndex]); - setCurrentStep(prevStepIndex); - observer.disconnect(); - } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - // Set a timeout to disconnect the observer if the element is not found within a certain period - const timeoutId = setTimeout(() => { - observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); - }, observerTimeout); // Adjust the timeout period as needed - - // Clear the timeout if the observer disconnects successfully - const originalDisconnect = observer.disconnect.bind(observer); - observer.disconnect = () => { - clearTimeout(timeoutId); - originalDisconnect(); - }; - - } else { - // If no route is set, update the step instantly - setCurrentStep(prevStepIndex); - } - } catch (error) { - console.error("Error navigating to previous route", error); - } - } + const prevStepIndex = currentStep - 1; + await setStep(prevStepIndex); }; const setStep = async (step: number | string) => { - if (currentTourSteps){ - try { - const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; - const route = currentTourSteps[setStepIndex].route; - - // Check if the route is set and different from the current route - if (route && currentTourSteps[currentStep].route !== route) { - // Trigger the next route - await router.push(route); - - // Use MutationObserver to detect when the target element is available in the DOM - const observer = new MutationObserver((mutations, observer) => { - const shouldSelect = hasSelector(currentTourSteps[setStepIndex]); - if (shouldSelect) { - const element = getStepSelectorElement(currentTourSteps[setStepIndex]); - if (element) { - // Once the element is found, update the step and scroll to the element - setCurrentStep(setStepIndex); - - // Stop observing after the element is found - observer.disconnect(); - } else { - debug && console.log("Onborda: Observing for element...", currentTourSteps[setStepIndex]); - } - } else { - console.log("Onborda: No selector set for next step while observing", currentTourSteps[setStepIndex]); - setCurrentStep(setStepIndex); - observer.disconnect(); - } - }); - - // Start observing the document body for changes - observer.observe(document.body, { - childList: true, - subtree: true, - }); - - // Set a timeout to disconnect the observer if the element is not found within a certain period - const timeoutId = setTimeout(() => { - observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); - }, observerTimeout); // Adjust the timeout period as needed - - // Clear the timeout if the observer disconnects successfully - const originalDisconnect = observer.disconnect.bind(observer); - observer.disconnect = () => { - clearTimeout(timeoutId); - originalDisconnect(); - }; - - } else { - // If no route is set, update the step instantly - setCurrentStep(setStepIndex); - } - } catch (error) { - console.error("Error navigating to next route", error); - } - } + const setStepIndex = typeof step === 'string' ? currentTourSteps.findIndex((s) => s?.id === step) : step; + setCurrentStep(setStepIndex); } diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index 55b7974..db32875 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { createContext, useContext, useState, useCallback } from "react"; +import React, {createContext, useContext, useState, useCallback, useEffect} from "react"; // Types import {OnbordaContextType, OnbordaProviderProps, Step, Tour} from "./types"; @@ -28,10 +28,10 @@ const OnbordaProvider: React.FC = ({ children, tours = [], }) => { - const [currentTour, setCurrentTour] = useState(null); + const [currentTour, setCurrentTourState] = useState(null); const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); - const [currentTourSteps, setCurrentTourSteps] = useState([]); + const [currentTourSteps, setCurrentTourStepsState] = useState([]); const [completedSteps, setCompletedSteps] = useState>(new Set()); const setCurrentStep = useCallback((step: number | string, delay?: number) => { @@ -56,17 +56,48 @@ const OnbordaProvider: React.FC = ({ const closeOnborda = useCallback(() => { setOnbordaVisible(false); - setCurrentTour(null); - }, []); - - const startOnborda = useCallback((tourName: string) => { - setCurrentTour(tourName); + setCurrentTourState(null); + setCurrentTourStepsState([]); setCurrentStepState(0); - setCurrentTourSteps(tours.find((tour) => tour.tour === tourName)?.steps || []); - setCompletedSteps(new Set([...(tours.find((tour) => tour.tour === tourName)?.completedSteps || [])])); - setOnbordaVisible(true); + setCompletedSteps(new Set()); }, []); + + const initializeCompletedSteps = useCallback(async (tourSteps: Step[]) => { + return Promise.all(tourSteps.map(async (step) => { + return step.initialCompletedState ? step.initialCompletedState() : false; + })).then((results) => { + const firstIncomplete = results.findIndex((result) => !result); + const completed = results.reduce<(string | number)[]>((acc, result, index) => { + if (result) { + acc.push(tourSteps[index]?.id || index); + } + return acc; + }, []); + setCompletedSteps(new Set(completed)); + return firstIncomplete; + }); + },[]); + + const setCurrentTour = useCallback((tourName: string | null) => { + if (!tourName) { + closeOnborda(); + return + } + setCurrentTourState(tourName); + const tourSteps = tours.find((tour) => tour.tour === tourName)?.steps || []; + setCurrentTourStepsState(tourSteps); + initializeCompletedSteps(tourSteps).then(r => { + setCurrentStep(r); + setOnbordaVisible(true); + }); + }, [tours]); + + const startOnborda = useCallback((tourName: string) => { + setCurrentTour(tourName); + }, [setCurrentTour]); + + return ( boolean; + /** Initial completed state of the step. an async function called on tour started.*/ + initialCompletedState?: () => Promise; // Routing /** The route for this step */ From 9425d8832ee339f026ef0c5a07c62724e239c07b Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Thu, 14 Nov 2024 12:44:42 +0000 Subject: [PATCH 17/30] Removed unused Tour.completedSteps --- dist/types/index.d.ts | 2 -- src/types/index.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 6b18306..e3e84f3 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -66,8 +66,6 @@ export interface Tour { tour: string; /** An array of steps in the tour */ steps: Step[]; - /** Default completed steps */ - completedSteps?: (string | number)[]; } export interface OnbordaProps { /** The children elements to be rendered inside the Onborda component */ diff --git a/src/types/index.ts b/src/types/index.ts index 1c333d9..bc2d637 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -81,8 +81,6 @@ export interface Tour { tour: string; /** An array of steps in the tour */ steps: Step[]; - /** Default completed steps */ - completedSteps?: (string|number)[]; } // Onborda From dbded2e749c355bbe35d6a4c68cb6cb9bf3d30ee Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Thu, 14 Nov 2024 14:07:45 +0000 Subject: [PATCH 18/30] Update README.md --- README.md | 324 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 191 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index b877687..6517f9d 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,10 @@ yarn add Libresoft-UK/onborda ### Global `layout.tsx` ```tsx - - - {children} - + + + {children} + ``` @@ -44,37 +44,42 @@ const config: Config = { ``` ### Card Component -If you require greater control over the card design or simply wish to create a totally custom component then you can do so easily. +If you require greater control over the card design or simply wish to create a totally custom component then you can do so easily. The following props can be used to customise your card component. + +| Prop | Type | Description | +|---------------|--------------------------------------|---------------------------------------------------------------------------------| +| `step` | `Object` | The current `Step` object from your steps array, including content, title, etc. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `totalSteps` | `number` | The total number of steps in the onboarding process. | +| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `nextStep` | `() => void` | A function to advance to the next step in the onboarding process. | +| `prevStep` | `() => void` | A function to go back to the previous step in the onboarding process. | +| `arrow` | `JSX.Element` | An SVG object, the orientation is controlled by the steps side prop | +| `completedSteps`| `number[]` | An array of completed step indexes/ids. | -| Prop | Type | Description | -|---------------|------------------|----------------------------------------------------------------------| -| `step` | `Object` | The current `Step` object from your steps array, including content, title, etc. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `totalSteps` | `number` | The total number of steps in the onboarding process. | -| `nextStep` | | A function to advance to the next step in the onboarding process. | -| `prevStep` | | A function to go back to the previous step in the onboarding process.| -| `arrow` | | Returns an SVG object, the orientation is controlled by the steps side prop | ```tsx "use client" import type { CardComponentProps } from "onborda"; -export const CustomCard = ({ +export const CustomCardComponent = ({ step, currentStep, - totalSteps, + totalSteps, + setStep, nextStep, prevStep, arrow, - canProceed + completedSteps }: CardComponentProps) => { return (
-

{step.icon} {step.title}

+

{step.icon} {step.title} {completedSteps.contains(currentStep) ? <>✅ : <>}

{currentStep} of {totalSteps}

{step.content}

+ {arrow}
) @@ -84,20 +89,24 @@ export const CustomCard = ({ ### Tour Component If you require greater control over the tour design or simply wish to create a totally custom component then you can do so easily. -| Prop | Type | Description | -|---------------|----------|----------------------------------------------------------------------| -| `currentTour` | `string` | The current tour name. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `steps` | `Step[]` | The steps array for the current tour. | +| Prop | Type | Description | +|---------------|----------|----------------------------------------------------------------| +| `currentTour` | `string` | The current tour name. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `steps` | `Step[]` | The steps array for the current tour. | +| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `completedSteps`| `number[]`| An array of completed step indexes/ids. | ```tsx "use client" import type { TourComponentProps } from "onborda"; -export const CustomTour = ({ +export const CustomTourComponent = ({ currentTour, currentStep, steps, + setStep, + completedSteps }: TourComponentProps) => { return (
@@ -105,7 +114,12 @@ export const CustomTour = ({

{currentStep} of {steps.length}

    {steps.map((step, index) => ( -
  • {step.title}
  • +
  • setStep(index)} + > + {step.title} {completedSteps.contans(index) ? <>✅ : <>} +
  • ))}
@@ -113,136 +127,150 @@ export const CustomTour = ({ } ``` -### Steps object -Steps have changed since Onborda v1.2.3 and now fully supports multiple "tours" so you have the option to create multple product tours should you need to! The original Step format remains but with some additional content as shown in the example below! +### Tour object +| Prop | Type | Description | +|---------------|----------|----------------------------------------------------------------| +| `tour` | `string` | The name of the tour. | +| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | + ```tsx { - tour: "firstyour", - steps: [ - Step - ], - tour: "secondtour", - steps: [ - Step - ] + tour: "firstyour", + steps: [ + Step + ] } ``` ### Step object -| Prop | Type | Description | -|-----------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | -| `title` | `string` | The title of your step | -| `content` | `React.ReactNode` | The main content or body of the step. | -| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | -| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | -| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | -| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | -| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | -| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | -| `nextRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the next step. | -| `prevRoute` | `string` | Optional. The route to navigate to using `next/navigation` when moving to the previous step. | -| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | -| `nextStepConditions` | `(element: Element \| null)=>boolean` | Optional. A client function that returns a boolean thats passed to the CardComponents `canProceed` prop. Gets bound to an event listener on the target element on `click`,`input` and `change`. | +| Prop | Type | Description | +|------------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | `string` | Optional. A unique identifier for the step. If set, step can be acitavted using `setStep(step.id)`. | +| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | +| `title` | `string` | The title of your step | +| `content` | `React.ReactNode` | The main content or body of the step. | +| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | +| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | +| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | +| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | +| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | +| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | +| `nextRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the next step. | +| `prevRoute` | `string`>/del> | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | +| `route` | `string` | Optional. The route to navigate to using `next/navigation` when this step is set. | +| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | +| `initialCompletedState`| `() => Promise` | Optional. A client function that returns a promise that resolves to a boolean. If true, the step is marked as completed. Called via a `Promise.all()` on each step of a tour when started. | ```tsx { - icon: <>👋, - title: "Tour 1, Step 1", - content: <>First tour, first step, - selector: "#tour1-step1", - side: "top", - showControls: true, - pointerPadding: 10, - pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar" + icon: <>👋, + title: "Tour 1, Step 1", + content: <>First tour, first step, + selector: "#tour1-step1", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + nextRoute: "/foo", + prevRoute: "/bar" } ``` -### Example `steps` +### Example `tours` array ```tsx -{ - tour: "firsttour", - steps: [ - { - icon: <>👋, - title: "Tour 1, Step 1", - content: <>First tour, first step, - selector: "#tour1-step1", - side: "top", - showControls: true, - pointerPadding: 10, - pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar" - }, +[ { - icon: <>👋, - title: "Tour 1, Step 2", - content: <>First tour, second step. To proceed please provide a value, - selector: "#tour1-step2-input", // target the input - nextStepConditions: (element) => ((element as HTMLInputElement)?.value?.trim() !== ''), // only allow next step if targatted element has a value - interactable: true, // allow user to interact with the input - side: "right", - showControls: true, - pointerPadding: 10, - pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar" + tour: "firsttour", + steps: [ + { + icon: <>👋, + title: "Tour 1, Step 1", + content: <>First tour, first step, + selector: "#tour1-step1", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/foo" + }, + { + icon: <>👋, + title: "Tour 1, Step 2", + content: <>First tour, second step. To proceed please provide a value, + selector: "#tour1-step2-input", // target the input + isCompleteConditions: (element) => ((element as HTMLInputElement)?.value?.trim() !== ''), // check if the step is completed when element is interacted with + initialCompletedState: async () => { await getDatabaseValue('some-required-value') === ''}, // check if the step is already completed when the tour starts + interactable: true, // allow user to interact with the input + side: "right", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/bar" + }, + { + icon: <>👋, + title: "Tour 1, Step 3", + content: <>Thanks for settings the value, + selector: "#tour1-step3", + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/bar" + } + ], }, { - icon: <>👋, - title: "Tour 1, Step 3", - content: <>Thanks for settings the value, - selector: "#tour1-step3", - side: "top", - showControls: true, - pointerPadding: 10, - pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar" - } - ... - ], - tour: "secondtour", - steps: [ - { - icon: <>👋👋, - title: "Second tour, Step 1", - content: <>Second tour, first step!, - //selector: "#onborda-step1", - customQuerySelector: () => document.getElementById("onborda-step1").closest("div"), // get the parent div - side: "top", - showControls: true, - pointerPadding: 10, - pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar", - interactable: true, + tour: "secondtour", + steps: [ + { + icon: <>👋👋, + title: "Second tour, Step 1", + content: <>Second tour, first step!, + //selector: "#onborda-step1", + customQuerySelector: () => document.getElementById("onborda-step1").closest("div"), // get the parent div + side: "top", + showControls: true, + pointerPadding: 10, + pointerRadius: 10, + route: "/foo", + interactable: true, + } + ] } - ] -} +] ``` +### OnbordaProvider Props +| Property | Type | Description | +|--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | -### Onborda Props +```tsx + + {children} + +``` -| Property | Type | Description | -|------------------|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `children` | `React.ReactNode` | Your website or application content. | -| `steps` | `Array[]` | An array of `Step` objects defining each step of the onboarding process. | -| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | -| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | -| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | -| `cardTransition` | `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | -| `cardComponent` | `React.ReactNode` | The React component to use as the card for each step. | -| `tourComponent` | `React.ReactNode` | The React component to use as a list of steps for the current tour. | -| `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | -| `observerTimeout`| `number` | Optional. The timeout in milliseconds for the observer to wait for the target element to be available. Defaults to `5000`. Observer is used to wait for the target element to be available before proceeding to the next step e.g. when the target element is on a different route. | +### Onborda Props +| Property | Type | Description | +|--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `steps` | `Array[]` | **Deprecated** An array of `Step` objects defining each step of the onboarding process. | +| `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | +| `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | +| `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | +| `cardTransition` | `Transition` | Transitions between steps are of the type Transition from [framer-motion](https://www.framer.com/motion/transition/), see the [transition docs](https://www.framer.com/motion/transition/) for more info. Example: `{{ type: "spring" }}`. | +| `cardComponent` | `React.ReactNode` | The React component to use as the card for each step. | +| `tourComponent` | `React.ReactNode` | The React component to use as a list of steps for the current tour. | +| `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | +| `observerTimeout` | `number` | Optional. The timeout in milliseconds for the observer to wait for the target element to be available. Defaults to `5000`. Observer is used to wait for the target element to be available before proceeding to the next step e.g. when the target element is on a different route. | + +> **Notice**: The `steps` property has been deprecated. Please use the `OnbordaProvider.tours` property instead. ```tsx ``` + +### UseOnborda Context +The `useOnborda` hook provides a set of functions to control the onboarding process from any child within the `OnboardaProvider`. The hook returns an object with the following properties: + +| Property | Type | Description | +|--------------------|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `startOnborda` | `() => void` | A function to start the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | +| `curreentTour` | `string` | The name of the current tour. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `currentTourSteps` | `Step[]` | The steps array for the current tour. | +| `setCurrentStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `completedSteps` | `number[]` | An array of completed step indexes/ids. | +| `setCompletedSteps`| `(steps: number[]) => void` | A function to set the completed steps array. | +| `isOnbordaVisible` | `boolean` | A boolean to determine if the onboarding overlay is visible. | + +```tsx +const { + startOnborda, + closeOnborda, + currentTour, + currentStep, + currentTourSteps, + setCurrentStep, + completedSteps, + setCompletedSteps, + isOnbordaVisible +} = useOnborda(); +``` + ### Contribution To setup the project locally, clone the repository and run the following commands: ```bash @@ -270,7 +328,7 @@ To test the local library in a local Next.js project, run the following command npm link ``` -Then in your Next.js project run the following command to link the package +Then in your Next.js project run the following command to link the package. This will create a symlink in your Next.js project to the local package. ```bash # Link the package npm link onborda @@ -282,9 +340,9 @@ To unlink the package, run the following command in your Next.js project npm unlink onborda ``` -If you already have the published package installed in your project, the symlink will take precedence over the published package. Ensure to push changes to this repo before pushing changes to your project. +If you already have the published package installed in your project, the symlink will take precedence over the published package. Ensure to push changes to this package before pushing changes to your project. -Now you can make changes to the package and see them reflected in your Next.js project. The package must be build after every change to see the changes in your Next.js project. +Now you can make changes to the package and see them reflected in your Next.js project. The package must be built after every change to see the changes in your Next.js project. ```bash # Build the package npm run build From 52886b81b2d0199e482e4fb7e24933ef43c7e0cd Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Thu, 14 Nov 2024 14:11:00 +0000 Subject: [PATCH 19/30] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6517f9d..0aceea9 100644 --- a/README.md +++ b/README.md @@ -158,11 +158,12 @@ export const CustomTourComponent = ({ | `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | | `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | | `nextRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the next step. | -| `prevRoute` | `string`>/del> | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | +| `prevRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | | `route` | `string` | Optional. The route to navigate to using `next/navigation` when this step is set. | | `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | | `initialCompletedState`| `() => Promise` | Optional. A client function that returns a promise that resolves to a boolean. If true, the step is marked as completed. Called via a `Promise.all()` on each step of a tour when started. | +> **Notice**: The `nextRoute` and `prevRoute` properties have been deprecated. Please use the `route` property instead. ```tsx { icon: <>👋, From b79fce33273bc182e5030210a7844202a87cc334 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Thu, 14 Nov 2024 14:18:55 +0000 Subject: [PATCH 20/30] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0aceea9..44c2f1a 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,8 @@ export const CustomTourComponent = ({ | `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | | `initialCompletedState`| `() => Promise` | Optional. A client function that returns a promise that resolves to a boolean. If true, the step is marked as completed. Called via a `Promise.all()` on each step of a tour when started. | -> **Notice**: The `nextRoute` and `prevRoute` properties have been deprecated. Please use the `route` property instead. +> [!IMPORTANT] +> The `nextRoute` and `prevRoute` properties have been deprecated. Please use the `route` property instead. ```tsx { icon: <>👋, @@ -271,7 +272,8 @@ export const CustomTourComponent = ({ | `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | | `observerTimeout` | `number` | Optional. The timeout in milliseconds for the observer to wait for the target element to be available. Defaults to `5000`. Observer is used to wait for the target element to be available before proceeding to the next step e.g. when the target element is on a different route. | -> **Notice**: The `steps` property has been deprecated. Please use the `OnbordaProvider.tours` property instead. +> [!IMPORTANT] +> The `steps` property has been deprecated. Please use the `OnbordaProvider.tours` property instead. ```tsx Date: Thu, 14 Nov 2024 15:32:29 +0000 Subject: [PATCH 21/30] Added support for pre-started tour on mount --- README.md | 9 +++++---- dist/OnbordaContext.js | 10 ++++++++-- dist/types/index.d.ts | 2 ++ src/OnbordaContext.tsx | 8 ++++++++ src/types/index.ts | 2 ++ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 44c2f1a..6867d88 100644 --- a/README.md +++ b/README.md @@ -246,10 +246,11 @@ export const CustomTourComponent = ({ ] ``` ### OnbordaProvider Props -| Property | Type | Description | -|--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `children` | `React.ReactNode` | Your website or application content. | -| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | +| Property | Type | Description | +|--------------------|----------------------|----------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | +| `activeTour` | `string` | Optional. The name of the active tour. If set, Onborda will start automatically. | ```tsx diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index d4395a9..6886905 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -1,6 +1,6 @@ "use client"; import { jsx as _jsx } from "react/jsx-runtime"; -import { createContext, useContext, useState, useCallback } from "react"; +import { createContext, useContext, useState, useCallback, useEffect } from "react"; // Example Hooks Usage: // const { setCurrentStep, closeOnborda, startOnborda } = useOnborda(); // // To trigger a specific step @@ -16,12 +16,18 @@ const useOnborda = () => { } return context; }; -const OnbordaProvider = ({ children, tours = [], }) => { +const OnbordaProvider = ({ children, tours = [], activeTour = null, }) => { const [currentTour, setCurrentTourState] = useState(null); const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); const [currentTourSteps, setCurrentTourStepsState] = useState([]); const [completedSteps, setCompletedSteps] = useState(new Set()); + // Start the active tour on mount + useEffect(() => { + if (activeTour) { + startOnborda(activeTour); + } + }, [activeTour]); const setCurrentStep = useCallback((step, delay) => { // If step is a string, find the index of the step with that id if (typeof step === 'string') { diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index e3e84f3..410d9ca 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -4,6 +4,8 @@ export interface OnbordaProviderProps { children: React.ReactNode; /** An array of tours, each containing multiple steps */ tours: Tour[]; + /** Active Tour */ + activeTour?: string; } export interface OnbordaContextType { /** current step index */ diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index db32875..0fc9137 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -27,6 +27,7 @@ const useOnborda = () => { const OnbordaProvider: React.FC = ({ children, tours = [], + activeTour = null, }) => { const [currentTour, setCurrentTourState] = useState(null); const [currentStep, setCurrentStepState] = useState(0); @@ -34,6 +35,13 @@ const OnbordaProvider: React.FC = ({ const [currentTourSteps, setCurrentTourStepsState] = useState([]); const [completedSteps, setCompletedSteps] = useState>(new Set()); + // Start the active tour on mount + useEffect(() => { + if (activeTour) { + startOnborda(activeTour); + } + }, [activeTour]); + const setCurrentStep = useCallback((step: number | string, delay?: number) => { // If step is a string, find the index of the step with that id if (typeof step === 'string') { diff --git a/src/types/index.ts b/src/types/index.ts index bc2d637..c339e5a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,6 +6,8 @@ export interface OnbordaProviderProps { children: React.ReactNode; /** An array of tours, each containing multiple steps */ tours: Tour[]; + /** Active Tour */ + activeTour?: string; } // Context From c1039ce12827bfd8cf3c84d465f43c888563ca48 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Thu, 14 Nov 2024 15:33:46 +0000 Subject: [PATCH 22/30] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6867d88..69a90a3 100644 --- a/README.md +++ b/README.md @@ -175,8 +175,7 @@ export const CustomTourComponent = ({ showControls: true, pointerPadding: 10, pointerRadius: 10, - nextRoute: "/foo", - prevRoute: "/bar" + route: "/foo" } ``` From 2b32a4c829daa0c96b68b11e05511fcfeb67da93 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Fri, 15 Nov 2024 16:24:36 +0000 Subject: [PATCH 23/30] Added onComplete callbacks to the Step object. Added onComplete callbacks to the Step object. Merged useEffects to improve route change awareness ensuring onComplete is called after the route has updated to prevent potential serverActions revalidation on the previous route blocking router.push. --- dist/Onborda.js | 116 ++++++++++++++++++++++--------------- dist/OnbordaContext.js | 10 +++- dist/types/index.d.ts | 4 ++ src/Onborda.tsx | 126 ++++++++++++++++++++++++++--------------- src/OnbordaContext.tsx | 10 +++- src/types/index.ts | 6 ++ 6 files changed, 176 insertions(+), 96 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index 26e1e53..d1005fd 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run import { useEffect, useRef, useState } from "react"; import { useOnborda } from "./OnbordaContext"; import { motion } from "framer-motion"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { Portal } from "@radix-ui/react-portal"; import { getCardStyle, getArrowStyle } from "./OnbordaStyles"; /** @@ -17,7 +17,6 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); const [currentRoute, setCurrentRoute] = useState(null); - const offset = 20; const hasSelector = (step) => { return !!step?.selector || !!step?.customQuerySelector; }; @@ -27,11 +26,17 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // - - // Route Changes const router = useRouter(); + const path = usePathname(); + // Update the current route on route changes + useEffect(() => { + setCurrentRoute(path); + }, [path]); // - - // Initialisze useEffect(() => { + let cleanup = []; if (isOnbordaVisible && currentTourSteps) { - debug && console.log("Onborda: Current Step Changed"); + debug && console.log("Onborda: Current Step Changed", currentStep); const step = currentTourSteps[currentStep]; if (step) { let elementFound = false; @@ -45,10 +50,46 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT setPointerPosition(getElementPosition(element)); setElementToScroll(element); currentElementRef.current = element; + // Function to mark the step as completed if the conditions are met + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(step?.id ?? currentStep); + return prev; + }); + } + }; + // Initial check + handleInteraction(); // Enable pointer events on the element if (step.interactable) { const htmlElement = element; htmlElement.style.pointerEvents = "auto"; + // Add event listeners if the step is interactable and has conditions + if (step?.isCompleteConditions) { + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + debug && console.log("Onborda: Added event listeners for element", element); + cleanup.push(() => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + debug && console.log("Onborda: Removed event listeners for element", element); + }); + } } elementFound = true; } @@ -56,9 +97,8 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // do we have a route to navigate to? if (step.route) { // Check if the route is set and different from the current route - if (currentRoute == null || currentRoute !== step.route) { + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { debug && console.log("Onborda: Navigating to route", step.route); - setCurrentRoute(step.route); // Trigger the next route router.push(step.route); // Use MutationObserver to detect when the target element is available in the DOM @@ -78,15 +118,16 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT } // Stop observing after the element is found observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after element found", element); } else { debug && console.log("Onborda: Observing for element...", currentTourSteps[currentStep]); } } else { - debug && console.log("Onborda: No selector set for next step while observing", currentTourSteps[currentStep]); setCurrentStep(currentStep); observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after no selector set", currentTourSteps[currentStep]); } }); // Start observing the document body for changes @@ -97,7 +138,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // Set a timeout to disconnect the observer if the element is not found within a certain period const timeoutId = setTimeout(() => { observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); + console.error("Onborda: Observer Timeout", currentTourSteps[currentStep]); }, observerTimeout); // Adjust the timeout period as needed // Clear the timeout if the observer disconnects successfully const originalDisconnect = observer.disconnect.bind(observer); @@ -106,21 +147,31 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT originalDisconnect(); }; } - else if (!elementFound) { - console.error("Onborda: Element not found on same route", currentTourSteps[currentStep]); - } } } else { // no selector, but might still need to navigate to a route if (step.route) { // Check if the route is set and different from the current route - if (currentRoute == null || currentRoute !== step.route) { + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { debug && console.log("Onborda: Navigating to route", step.route); - setCurrentRoute(step.route); // Trigger the next route router.push(step.route); } + else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + } + } + else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); } } // No element set for this step? Place the pointer at the center of the screen @@ -148,40 +199,15 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT const htmlElement = currentElementRef.current; htmlElement.style.pointerEvents = ""; } + // Cleanup any event listeners we may have added + cleanup.forEach(fn => fn()); }; - }, [currentStep, currentTourSteps, offset, isOnbordaVisible]); - // Update the canProceed state based on the nextStepConditions - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - const step = currentTourSteps?.[currentStep]; - const element = step ? getStepSelectorElement(step) : null; - if (element && step?.isCompleteConditions) { - const handleInteraction = () => { - if (step?.isCompleteConditions?.(element) ?? true) { - setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); - }); - } - }; - // Initial check - handleInteraction(); - element.addEventListener("click", handleInteraction); - element.addEventListener("input", handleInteraction); - element.addEventListener("change", handleInteraction); - return () => { - // Cleanup the event listeners - element.removeEventListener("click", handleInteraction); - element.removeEventListener("input", handleInteraction); - element.removeEventListener("change", handleInteraction); - }; - } - else { - setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); - }); - } - } - }, [currentStep, currentTourSteps, isOnbordaVisible]); + }, [ + currentStep, // Re-run the effect when the current step changes + currentTourSteps, // Re-run the effect when the current tour steps change + isOnbordaVisible, // Re-run the effect when the onborda visibility changes + currentRoute, // Re-run the effect when the current route changes + ]); // - - // Helper function to get element position const getElementPosition = (element) => { diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index 6886905..c31a4af 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -49,12 +49,17 @@ const OnbordaProvider = ({ children, tours = [], activeTour = null, }) => { } }, []); const closeOnborda = useCallback(() => { + // If all steps are completed, call the onComplete function + console.log(completedSteps.size, currentTourSteps.length); + if (completedSteps.size === currentTourSteps.length) { + tours.find((tour) => tour.tour === currentTour)?.onComplete?.(); + } setOnbordaVisible(false); setCurrentTourState(null); setCurrentTourStepsState([]); setCurrentStepState(0); setCompletedSteps(new Set()); - }, []); + }, [currentTour, currentTourSteps, completedSteps]); const initializeCompletedSteps = useCallback(async (tourSteps) => { return Promise.all(tourSteps.map(async (step) => { return step.initialCompletedState ? step.initialCompletedState() : false; @@ -67,7 +72,8 @@ const OnbordaProvider = ({ children, tours = [], activeTour = null, }) => { return acc; }, []); setCompletedSteps(new Set(completed)); - return firstIncomplete; + // If all steps are completed, return the last step + return firstIncomplete === -1 ? tourSteps.length - 1 : firstIncomplete; }); }, []); const setCurrentTour = useCallback((tourName) => { diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 410d9ca..2a20189 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -62,12 +62,16 @@ export interface Step { /** The route to navigate to for the previous step */ /** @deprecated Use `route` instead */ prevRoute?: string; + /** Callback function to be called when the step is completed */ + onComplete?: () => Promise; } export interface Tour { /** The name of the tour */ tour: string; /** An array of steps in the tour */ steps: Step[]; + /** Complete Callback */ + onComplete?: () => void; } export interface OnbordaProps { /** The children elements to be rendered inside the Onborda component */ diff --git a/src/Onborda.tsx b/src/Onborda.tsx index ff6d87c..fb88b48 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef, useState} from "react"; import {useOnborda} from "./OnbordaContext"; import {motion} from "framer-motion"; -import {useRouter} from "next/navigation"; +import {usePathname, useRouter} from "next/navigation"; import {Portal} from "@radix-ui/react-portal"; // Types @@ -35,7 +35,6 @@ const Onborda: React.FC = ({ } | null>(null); const currentElementRef = useRef(null); const [currentRoute, setCurrentRoute] = useState(null); - const offset = 20; const hasSelector = (step: Step): boolean => { return !!step?.selector || !!step?.customQuerySelector; @@ -47,12 +46,19 @@ const Onborda: React.FC = ({ // - - // Route Changes const router = useRouter(); + const path = usePathname(); + + // Update the current route on route changes + useEffect(() => { + setCurrentRoute(path); + },[path]) // - - // Initialisze useEffect(() => { + let cleanup : any[] = []; if (isOnbordaVisible && currentTourSteps) { - debug && console.log("Onborda: Current Step Changed"); + debug && console.log("Onborda: Current Step Changed", currentStep); const step = currentTourSteps[currentStep]; if (step) { let elementFound = false; @@ -67,10 +73,53 @@ const Onborda: React.FC = ({ setElementToScroll(element); currentElementRef.current = element; + // Function to mark the step as completed if the conditions are met + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(step?.id ?? currentStep); + return prev; + }); + } + } + + // Initial check + handleInteraction(); + + // Enable pointer events on the element if (step.interactable) { const htmlElement = element as HTMLElement; htmlElement.style.pointerEvents = "auto"; + + // Add event listeners if the step is interactable and has conditions + if (step?.isCompleteConditions) { + element.addEventListener("click", handleInteraction); + element.addEventListener("input", handleInteraction); + element.addEventListener("change", handleInteraction); + debug && console.log("Onborda: Added event listeners for element", element); + + cleanup.push(() => { + // Cleanup the event listeners + element.removeEventListener("click", handleInteraction); + element.removeEventListener("input", handleInteraction); + element.removeEventListener("change", handleInteraction); + debug && console.log("Onborda: Removed event listeners for element", element); + }); + } } elementFound = true; } @@ -78,9 +127,8 @@ const Onborda: React.FC = ({ // do we have a route to navigate to? if (step.route) { // Check if the route is set and different from the current route - if (currentRoute == null || currentRoute !== step.route) { + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { debug && console.log("Onborda: Navigating to route", step.route); - setCurrentRoute(step.route); // Trigger the next route router.push(step.route); @@ -95,6 +143,7 @@ const Onborda: React.FC = ({ setElementToScroll(element); currentElementRef.current = element; + // Enable pointer events on the element if (step.interactable) { const htmlElement = element as HTMLElement; @@ -103,13 +152,14 @@ const Onborda: React.FC = ({ // Stop observing after the element is found observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after element found", element); } else { debug && console.log("Onborda: Observing for element...", currentTourSteps[currentStep]); } } else { - debug && console.log("Onborda: No selector set for next step while observing", currentTourSteps[currentStep]); setCurrentStep(currentStep); observer.disconnect(); + debug && console.log("Onborda: Observer disconnected after no selector set", currentTourSteps[currentStep]); } }); @@ -122,7 +172,7 @@ const Onborda: React.FC = ({ // Set a timeout to disconnect the observer if the element is not found within a certain period const timeoutId = setTimeout(() => { observer.disconnect(); - console.error("Onborda: Element not found within the timeout period"); + console.error("Onborda: Observer Timeout", currentTourSteps[currentStep]); }, observerTimeout); // Adjust the timeout period as needed // Clear the timeout if the observer disconnects successfully @@ -131,21 +181,31 @@ const Onborda: React.FC = ({ clearTimeout(timeoutId); originalDisconnect(); }; - } else if (!elementFound) { - console.error("Onborda: Element not found on same route", currentTourSteps[currentStep]); } } }else { // no selector, but might still need to navigate to a route if (step.route) { // Check if the route is set and different from the current route - if (currentRoute == null || currentRoute !== step.route) { + if (currentRoute == null || !currentRoute?.endsWith(step.route)) { debug && console.log("Onborda: Navigating to route", step.route); - setCurrentRoute(step.route); // Trigger the next route router.push(step.route); + }else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); } + }else { + // Mark the step as completed + step?.onComplete && step.onComplete(); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); } + } // No element set for this step? Place the pointer at the center of the screen @@ -174,43 +234,15 @@ const Onborda: React.FC = ({ const htmlElement = currentElementRef.current as HTMLElement; htmlElement.style.pointerEvents = ""; } + // Cleanup any event listeners we may have added + cleanup.forEach(fn => fn()); } - }, [currentStep, currentTourSteps, offset, isOnbordaVisible]); - - // Update the canProceed state based on the nextStepConditions - useEffect(() => { - if (isOnbordaVisible && currentTourSteps) { - const step = currentTourSteps?.[currentStep]; - const element = step ? getStepSelectorElement(step) : null; - - if (element && step?.isCompleteConditions) { - const handleInteraction = () => { - if (step?.isCompleteConditions?.(element) ?? true) { - setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); - }); - } - }; - // Initial check - handleInteraction(); - - element.addEventListener("click", handleInteraction); - element.addEventListener("input", handleInteraction); - element.addEventListener("change", handleInteraction); - - return () => { - // Cleanup the event listeners - element.removeEventListener("click", handleInteraction); - element.removeEventListener("input", handleInteraction); - element.removeEventListener("change", handleInteraction); - }; - } else { - setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); - }); - } - } - }, [currentStep, currentTourSteps, isOnbordaVisible]); + }, [ + currentStep, // Re-run the effect when the current step changes + currentTourSteps, // Re-run the effect when the current tour steps change + isOnbordaVisible, // Re-run the effect when the onborda visibility changes + currentRoute, // Re-run the effect when the current route changes + ]); // - - // Helper function to get element position diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index 0fc9137..21d5166 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -63,12 +63,17 @@ const OnbordaProvider: React.FC = ({ }, []); const closeOnborda = useCallback(() => { + // If all steps are completed, call the onComplete function + console.log(completedSteps.size, currentTourSteps.length) + if (completedSteps.size === currentTourSteps.length) { + tours.find((tour) => tour.tour === currentTour)?.onComplete?.(); + } setOnbordaVisible(false); setCurrentTourState(null); setCurrentTourStepsState([]); setCurrentStepState(0); setCompletedSteps(new Set()); - }, []); + }, [currentTour, currentTourSteps, completedSteps]); const initializeCompletedSteps = useCallback(async (tourSteps: Step[]) => { @@ -83,7 +88,8 @@ const OnbordaProvider: React.FC = ({ return acc; }, []); setCompletedSteps(new Set(completed)); - return firstIncomplete; + // If all steps are completed, return the last step + return firstIncomplete === -1 ? tourSteps.length - 1 : firstIncomplete; }); },[]); diff --git a/src/types/index.ts b/src/types/index.ts index c339e5a..68cac7e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -74,6 +74,10 @@ export interface Step { /** @deprecated Use `route` instead */ prevRoute?: string; + // Callbacks + /** Callback function to be called when the step is completed */ + onComplete?: () => Promise; + } // Tour @@ -83,6 +87,8 @@ export interface Tour { tour: string; /** An array of steps in the tour */ steps: Step[]; + /** Complete Callback */ + onComplete?: () => void; } // Onborda From 731d695c3b4d5ce359aa4c10dab9de50b53aef36 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 19 Nov 2024 11:38:02 +0000 Subject: [PATCH 24/30] Added tours[] to useOnborda context and pass current tour object to TourCard --- dist/Onborda.js | 33 +++++++++++++++++++++++++++++--- dist/OnbordaContext.js | 4 ++-- dist/types/index.d.ts | 20 +++++++++++++++++--- src/Onborda.tsx | 43 ++++++++++++++++++++++++++++++++++++++---- src/OnbordaContext.tsx | 4 ++-- src/types/index.ts | 21 ++++++++++++++++++--- 6 files changed, 108 insertions(+), 17 deletions(-) diff --git a/dist/Onborda.js b/dist/Onborda.js index d1005fd..53aeb15 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -1,6 +1,6 @@ "use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useOnborda } from "./OnbordaContext"; import { motion } from "framer-motion"; import { usePathname, useRouter } from "next/navigation"; @@ -12,7 +12,7 @@ import { getCardStyle, getArrowStyle } from "./OnbordaStyles"; * @constructor */ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardTransition = { ease: "anticipate", duration: 0.6 }, cardComponent: CardComponent, tourComponent: TourComponent, debug = false, observerTimeout = 5000, }) => { - const { currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps } = useOnborda(); + const { currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps, tours, closeOnborda } = useOnborda(); const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); @@ -23,6 +23,10 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT const getStepSelectorElement = (step) => { return step?.selector ? document.querySelector(step.selector) : step?.customQuerySelector ? step.customQuerySelector() : null; }; + // Get the current tour object + const currentTourObject = useMemo(() => { + return tours.find((tour) => tour.tour === currentTour); + }, [currentTour, isOnbordaVisible]); // - - // Route Changes const router = useRouter(); @@ -53,6 +57,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // Function to mark the step as completed if the conditions are met const handleInteraction = () => { const isComplete = step?.isCompleteConditions?.(element) ?? true; + debug && console.log("Onborda: Step Interaction", step, isComplete); // Check if the step is complete based on the conditions, and not already marked as completed if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { debug && console.log("Onborda: Step Completed", step); @@ -111,6 +116,28 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT setPointerPosition(getElementPosition(element)); setElementToScroll(element); currentElementRef.current = element; + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + debug && console.log("Onborda: Step Interaction", step, isComplete); + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(step?.id ?? currentStep); + return prev; + }); + } + }; + // Initial check + handleInteraction(); // Enable pointer events on the element if (step.interactable) { const htmlElement = element; @@ -318,6 +345,6 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), completedSteps: Array.from(completedSteps) }) }) }) }), TourComponent && currentTourSteps && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { steps: currentTourSteps, currentTour: currentTour, currentStep: currentStep, setStep: setStep, completedSteps: Array.from(completedSteps) }) }) }))] }))] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), completedSteps: Array.from(completedSteps) }) }) }) }), TourComponent && currentTourObject !== undefined && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { tour: currentTourObject, currentTour: currentTour, currentStep: currentStep, setStep: setStep, completedSteps: Array.from(completedSteps), closeOnborda: closeOnborda }) }) }))] }))] })); }; export default Onborda; diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index c31a4af..b97c8c0 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -50,9 +50,8 @@ const OnbordaProvider = ({ children, tours = [], activeTour = null, }) => { }, []); const closeOnborda = useCallback(() => { // If all steps are completed, call the onComplete function - console.log(completedSteps.size, currentTourSteps.length); if (completedSteps.size === currentTourSteps.length) { - tours.find((tour) => tour.tour === currentTour)?.onComplete?.(); + tours.find((tour) => (tour.tour) === currentTour)?.onComplete?.(); } setOnbordaVisible(false); setCurrentTourState(null); @@ -93,6 +92,7 @@ const OnbordaProvider = ({ children, tours = [], activeTour = null, }) => { setCurrentTour(tourName); }, [setCurrentTour]); return (_jsx(OnbordaContext.Provider, { value: { + tours, currentTour, currentStep, currentTourSteps, diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 2a20189..67da477 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -8,6 +8,8 @@ export interface OnbordaProviderProps { activeTour?: string; } export interface OnbordaContextType { + /** array of tours */ + tours: Tour[]; /** current step index */ currentStep: number; /** current tour name */ @@ -64,14 +66,24 @@ export interface Step { prevRoute?: string; /** Callback function to be called when the step is completed */ onComplete?: () => Promise; + /** Any additional data for custom use */ + [key: string]: any; } export interface Tour { - /** The name of the tour */ + /** The tour ID */ tour: string; + /** Tour Title */ + title?: string; + /** Tour Description */ + description?: string; /** An array of steps in the tour */ steps: Step[]; /** Complete Callback */ onComplete?: () => void; + /** Tour can be dismissed. */ + dismissible?: boolean; + /** Any additional data for custom use */ + [key: string]: any; } export interface OnbordaProps { /** The children elements to be rendered inside the Onborda component */ @@ -119,10 +131,12 @@ export interface TourComponentProps { currentTour: string | null; /** The index of the current step */ currentStep: number; - /** An array of steps in the current tour */ - steps: Step[]; + /** The current Tour object containing details of the tour */ + tour: Tour; /** Function to set the current step by step index or step.id */ setStep: (step: number | string) => void; /** Array of completed steps */ completedSteps: (string | number)[]; + /** Function to close the Onborda */ + closeOnborda: () => void; } diff --git a/src/Onborda.tsx b/src/Onborda.tsx index fb88b48..1af0fbb 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -1,5 +1,5 @@ "use client"; -import React, {useEffect, useRef, useState} from "react"; +import React, {useEffect, useMemo, useRef, useState} from "react"; import {useOnborda} from "./OnbordaContext"; import {motion} from "framer-motion"; import {usePathname, useRouter} from "next/navigation"; @@ -24,7 +24,7 @@ const Onborda: React.FC = ({ debug = false, observerTimeout = 5000, }: OnbordaProps) => { - const {currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps} = useOnborda(); + const {currentTour, currentStep, setCurrentStep, isOnbordaVisible, currentTourSteps, completedSteps, setCompletedSteps, tours, closeOnborda} = useOnborda(); const [elementToScroll, setElementToScroll] = useState(null); const [pointerPosition, setPointerPosition] = useState<{ @@ -43,6 +43,11 @@ const Onborda: React.FC = ({ return step?.selector ? document.querySelector(step.selector) : step?.customQuerySelector ? step.customQuerySelector() : null; } + // Get the current tour object + const currentTourObject = useMemo(() => { + return tours.find((tour) => tour.tour === currentTour); + }, [currentTour, isOnbordaVisible]); + // - - // Route Changes const router = useRouter(); @@ -77,6 +82,8 @@ const Onborda: React.FC = ({ const handleInteraction = () => { const isComplete = step?.isCompleteConditions?.(element) ?? true; + debug && console.log("Onborda: Step Interaction", step, isComplete); + // Check if the step is complete based on the conditions, and not already marked as completed if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { debug && console.log("Onborda: Step Completed", step); @@ -143,6 +150,33 @@ const Onborda: React.FC = ({ setElementToScroll(element); currentElementRef.current = element; + const handleInteraction = () => { + const isComplete = step?.isCompleteConditions?.(element) ?? true; + + debug && console.log("Onborda: Step Interaction", step, isComplete); + + // Check if the step is complete based on the conditions, and not already marked as completed + if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Completed", step); + setCompletedSteps((prev) => { + return prev.add(step?.id ?? currentStep); + }); + // If callback is provided, call it + step?.onComplete && step.onComplete(); + + } // Check if the step is incomplete based on the conditions, and already marked as completed + else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + debug && console.log("Onborda: Step Incomplete", step); + setCompletedSteps((prev) => { + prev.delete(step?.id ?? currentStep); + return prev; + }); + } + } + + // Initial check + handleInteraction(); + // Enable pointer events on the element if (step.interactable) { @@ -434,7 +468,7 @@ const Onborda: React.FC = ({ - {TourComponent && currentTourSteps && ( + {TourComponent && currentTourObject !== undefined && ( = ({ className={'pointer-events-auto'} > diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index 21d5166..6bfcae8 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -64,9 +64,8 @@ const OnbordaProvider: React.FC = ({ const closeOnborda = useCallback(() => { // If all steps are completed, call the onComplete function - console.log(completedSteps.size, currentTourSteps.length) if (completedSteps.size === currentTourSteps.length) { - tours.find((tour) => tour.tour === currentTour)?.onComplete?.(); + tours.find((tour) => (tour.tour) === currentTour)?.onComplete?.(); } setOnbordaVisible(false); setCurrentTourState(null); @@ -115,6 +114,7 @@ const OnbordaProvider: React.FC = ({ return ( Promise; + /** Any additional data for custom use */ + [key: string]: any; + } // Tour // export interface Tour { - /** The name of the tour */ + /** The tour ID */ tour: string; + /** Tour Title */ + title?: string; + /** Tour Description */ + description?: string; /** An array of steps in the tour */ steps: Step[]; /** Complete Callback */ onComplete?: () => void; + /** Tour can be dismissed. */ + dismissible?: boolean; + /** Any additional data for custom use */ + [key: string]: any; } // Onborda @@ -159,10 +172,12 @@ export interface TourComponentProps { currentTour: string | null; /** The index of the current step */ currentStep: number; - /** An array of steps in the current tour */ - steps: Step[]; + /** The current Tour object containing details of the tour */ + tour: Tour; /** Function to set the current step by step index or step.id */ setStep: (step: number | string) => void; /** Array of completed steps */ completedSteps: (string|number)[]; + /** Function to close the Onborda */ + closeOnborda: () => void; } From 380f2b0c6d8a9577f4e65c66194a8c0402f5d50f Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 19 Nov 2024 12:11:51 +0000 Subject: [PATCH 25/30] Update README.md --- README.md | 123 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 73 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 69a90a3..74d50e4 100644 --- a/README.md +++ b/README.md @@ -89,31 +89,34 @@ export const CustomCardComponent = ({ ### Tour Component If you require greater control over the tour design or simply wish to create a totally custom component then you can do so easily. -| Prop | Type | Description | -|---------------|----------|----------------------------------------------------------------| -| `currentTour` | `string` | The current tour name. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `steps` | `Step[]` | The steps array for the current tour. | -| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | -| `completedSteps`| `number[]`| An array of completed step indexes/ids. | +| Prop | Type | Description | +|---------------|----------|---------------------------------------------------------------| +| `tour` | `Tour` | The current `Tour` object from your tours array. | +| `currentTour` | `string` | The current tour name. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `completedSteps`| `number[]`| An array of completed step indexes/ids. | +| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | ```tsx "use client" import type { TourComponentProps } from "onborda"; export const CustomTourComponent = ({ + tour, currentTour, currentStep, - steps, setStep, - completedSteps + completedSteps, + closeOnborda }: TourComponentProps) => { return (

{currentTour}

-

{currentStep} of {steps.length}

+

{currentStep} of {tour.steps.length}

+

{completedSteps.length} of {tour.steps.length} completed

    - {steps.map((step, index) => ( + {tour.steps.map((step, index) => (
  • setStep(index)} @@ -122,16 +125,27 @@ export const CustomTourComponent = ({
  • ))}
+
) } ``` ### Tour object -| Prop | Type | Description | -|---------------|----------|----------------------------------------------------------------| -| `tour` | `string` | The name of the tour. | -| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | +| Prop | Type | Description | +|------------------|----------|------------------------------------------------------------------------------------------------------------------------------------| +| `tour` | `string` | The name of the tour. | +| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | +| `title` | `string` | Optional. The title of the tour. | +| `description` | `string` | Optional. The description of the tour. | +| `dissmissable` | `boolean`| Optional. Determines whether the user can dismiss the tour. | +| `onComplete` | `() => void` | Optional. A function that is called when the tour is completed. | +| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the TourCard component. | ```tsx @@ -145,23 +159,26 @@ export const CustomTourComponent = ({ ### Step object -| Prop | Type | Description | -|------------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | `string` | Optional. A unique identifier for the step. If set, step can be acitavted using `setStep(step.id)`. | -| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | -| `title` | `string` | The title of your step | -| `content` | `React.ReactNode` | The main content or body of the step. | -| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | -| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | -| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | -| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | -| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | -| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | -| `nextRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the next step. | -| `prevRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | -| `route` | `string` | Optional. The route to navigate to using `next/navigation` when this step is set. | -| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | -| `initialCompletedState`| `() => Promise` | Optional. A client function that returns a promise that resolves to a boolean. If true, the step is marked as completed. Called via a `Promise.all()` on each step of a tour when started. | +| Prop | Type | Description | +|------------------------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | `string` | Optional. A unique identifier for the step. If set, step can be acitavted using `setStep(step.id)`. Otherwise steps[] index is used. | +| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | +| `title` | `string` | The title of your step | +| `content` | `React.ReactNode` | The main content or body of the step. | +| `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | +| `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | +| `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | +| `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | +| `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | +| `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | +| `nextRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the next step. | +| `prevRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | +| `route` | `string` | Optional. The route to navigate to using `next/navigation` when this step is set. | +| `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | +| `initialCompletedState`| `() => Promise` | Optional. A client function that returns a promise that resolves to a boolean. If true, the step is marked as completed. Called via a `Promise.all()` on each step of a tour when started. | +| `isCompleteConditions` | `(element: Element) => boolean` | Optional. A client function that returns a boolean. If true, the step is marked as completed. Called when the user interacts with the target element. | +| `onComplete` | `() => void` | Optional. A client function that is called when the step is marked as completed. Called if `isCompleteConditions` returns true. | +| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the Card component. | > [!IMPORTANT] > The `nextRoute` and `prevRoute` properties have been deprecated. Please use the `route` property instead. @@ -185,6 +202,10 @@ export const CustomTourComponent = ({ [ { tour: "firsttour", + title: "First Tour", + description: "This is the first tour", + dissmissable: false, + customProperty: "This is a custom property passed to the Tour card", steps: [ { icon: <>👋, @@ -195,7 +216,8 @@ export const CustomTourComponent = ({ showControls: true, pointerPadding: 10, pointerRadius: 10, - route: "/foo" + route: "/foo", + customProperty: "This is a custom property passed to the Step card" }, { icon: <>👋, @@ -245,11 +267,11 @@ export const CustomTourComponent = ({ ] ``` ### OnbordaProvider Props -| Property | Type | Description | -|--------------------|----------------------|----------------------------------------------------------------------------------| -| `children` | `React.ReactNode` | Your website or application content. | -| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | -| `activeTour` | `string` | Optional. The name of the active tour. If set, Onborda will start automatically. | +| Property | Type | Description | +|--------------------|----------------------|-------------------------------------------------------------------------------------| +| `children` | `React.ReactNode` | Your website or application content. | +| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | +| `activeTour` | `string` | Optional. The id of the active tour. If set, Onborda will start tour automatically. | ```tsx @@ -262,7 +284,7 @@ export const CustomTourComponent = ({ | Property | Type | Description | |--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `children` | `React.ReactNode` | Your website or application content. | -| `steps` | `Array[]` | **Deprecated** An array of `Step` objects defining each step of the onboarding process. | +| `steps` | `Array[]` | **Deprecated** An array of `Step` objects defining each step of the onboarding process. | | `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | | `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | | `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | @@ -292,17 +314,18 @@ export const CustomTourComponent = ({ ### UseOnborda Context The `useOnborda` hook provides a set of functions to control the onboarding process from any child within the `OnboardaProvider`. The hook returns an object with the following properties: -| Property | Type | Description | -|--------------------|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `startOnborda` | `() => void` | A function to start the onboarding process. | -| `closeOnborda` | `() => void` | A function to close the onboarding process. | -| `curreentTour` | `string` | The name of the current tour. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `currentTourSteps` | `Step[]` | The steps array for the current tour. | -| `setCurrentStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | -| `completedSteps` | `number[]` | An array of completed step indexes/ids. | -| `setCompletedSteps`| `(steps: number[]) => void` | A function to set the completed steps array. | -| `isOnbordaVisible` | `boolean` | A boolean to determine if the onboarding overlay is visible. | +| Property | Type | Description | +|--------------------|---------------------------------------------------------------|--------------------------------------------------------------------------| +| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | +| `startOnborda` | `() => void` | A function to start the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | +| `curreentTour` | `string` | The name of the current tour. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `currentTourSteps` | `Step[]` | The steps array for the current tour. | +| `setCurrentStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `completedSteps` | `Set` | An array of completed step indexes/ids. | +| `setCompletedSteps`| `React.Dispatch>>` | A function to set the completed steps array. | +| `isOnbordaVisible` | `boolean` | A boolean to determine if the onboarding overlay is visible. | ```tsx const { From c2ff9fec6fb9475941d884d11b080c1bcc1fc8be Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Tue, 19 Nov 2024 12:13:24 +0000 Subject: [PATCH 26/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74d50e4..15698ae 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ export const CustomCardComponent = ({

{currentStep} of {totalSteps}

{step.content}

- + {arrow} From 33c83c9c0895d1e5b0a049d7a4088f9f71994d9c Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Thu, 21 Nov 2024 16:42:47 +0000 Subject: [PATCH 27/30] Added Tour.initialCompletedStepsState prop for fetching boolean[] step states in one call rather than one per step --- README.md | 35 ++++++++++++++++++++++++----------- dist/Onborda.js | 20 ++++++++++---------- dist/OnbordaContext.js | 36 +++++++++++++++++------------------- dist/types/index.d.ts | 8 ++++---- src/Onborda.tsx | 20 ++++++++++---------- src/OnbordaContext.tsx | 39 +++++++++++++++++++-------------------- src/types/index.ts | 8 ++++---- 7 files changed, 88 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 15698ae..a604063 100644 --- a/README.md +++ b/README.md @@ -137,15 +137,16 @@ export const CustomTourComponent = ({ ``` ### Tour object -| Prop | Type | Description | -|------------------|----------|------------------------------------------------------------------------------------------------------------------------------------| -| `tour` | `string` | The name of the tour. | -| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | -| `title` | `string` | Optional. The title of the tour. | -| `description` | `string` | Optional. The description of the tour. | -| `dissmissable` | `boolean`| Optional. Determines whether the user can dismiss the tour. | -| `onComplete` | `() => void` | Optional. A function that is called when the tour is completed. | -| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the TourCard component. | +| Prop | Type | Description | +|------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tour` | `string` | The name of the tour. | +| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | +| `title` | `string` | Optional. The title of the tour. | +| `description` | `string` | Optional. The description of the tour. | +| `dissmissable` | `boolean`| Optional. Determines whether the user can dismiss the tour. | +| `onComplete` | `() => void` | Optional. A function that is called when the tour is completed. | +| `initialCompletedStepsState` | `() => Promise` | Optional. A client or server function that returns a promise that resolves to an array of booleans for each step. If true, the step is marked as completed on tour init. | +| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the TourCard component. | ```tsx @@ -175,7 +176,6 @@ export const CustomTourComponent = ({ | `prevRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | | `route` | `string` | Optional. The route to navigate to using `next/navigation` when this step is set. | | `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | -| `initialCompletedState`| `() => Promise` | Optional. A client function that returns a promise that resolves to a boolean. If true, the step is marked as completed. Called via a `Promise.all()` on each step of a tour when started. | | `isCompleteConditions` | `(element: Element) => boolean` | Optional. A client function that returns a boolean. If true, the step is marked as completed. Called when the user interacts with the target element. | | `onComplete` | `() => void` | Optional. A client function that is called when the step is marked as completed. Called if `isCompleteConditions` returns true. | | `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the Card component. | @@ -199,6 +199,7 @@ export const CustomTourComponent = ({ ### Example `tours` array ```tsx +import { FirstTourInitialState } from "@app/lib/initialTourStates"; // Optional initial stare from server action [ { tour: "firsttour", @@ -206,6 +207,7 @@ export const CustomTourComponent = ({ description: "This is the first tour", dissmissable: false, customProperty: "This is a custom property passed to the Tour card", + initialCompletedStepsState: FirstTourInitialState, //Optional server action called on tour init to get completed steps boolean array steps: [ { icon: <>👋, @@ -225,7 +227,6 @@ export const CustomTourComponent = ({ content: <>First tour, second step. To proceed please provide a value, selector: "#tour1-step2-input", // target the input isCompleteConditions: (element) => ((element as HTMLInputElement)?.value?.trim() !== ''), // check if the step is completed when element is interacted with - initialCompletedState: async () => { await getDatabaseValue('some-required-value') === ''}, // check if the step is already completed when the tour starts interactable: true, // allow user to interact with the input side: "right", showControls: true, @@ -266,6 +267,18 @@ export const CustomTourComponent = ({ } ] ``` +```tsx +'use server' +import API from "@app/lib/api"; +export async function FirstTourInitialState() { + return Promise.all([ + (await API.get("/api/first-tour-step-1")).value === 'true', + (await API.get("/api/first-tour-step-2")).value === 'true', + (await API.get("/api/first-tour-step-3")).value === 'true', + ]) + // return [true, false, false] // Example of an initial state +} +``` ### OnbordaProvider Props | Property | Type | Description | |--------------------|----------------------|-------------------------------------------------------------------------------------| diff --git a/dist/Onborda.js b/dist/Onborda.js index 53aeb15..8a7f9e8 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -59,18 +59,18 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT const isComplete = step?.isCompleteConditions?.(element) ?? true; debug && console.log("Onborda: Step Interaction", step, isComplete); // Check if the step is complete based on the conditions, and not already marked as completed - if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Completed", step); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); // If callback is provided, call it step?.onComplete && step.onComplete(); } // Check if the step is incomplete based on the conditions, and already marked as completed - else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Incomplete", step); setCompletedSteps((prev) => { - prev.delete(step?.id ?? currentStep); + prev.delete(currentStep); return prev; }); } @@ -120,18 +120,18 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT const isComplete = step?.isCompleteConditions?.(element) ?? true; debug && console.log("Onborda: Step Interaction", step, isComplete); // Check if the step is complete based on the conditions, and not already marked as completed - if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Completed", step); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); // If callback is provided, call it step?.onComplete && step.onComplete(); } // Check if the step is incomplete based on the conditions, and already marked as completed - else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Incomplete", step); setCompletedSteps((prev) => { - prev.delete(step?.id ?? currentStep); + prev.delete(currentStep); return prev; }); } @@ -189,7 +189,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // Mark the step as completed step?.onComplete && step.onComplete(); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); } } @@ -197,7 +197,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // Mark the step as completed step?.onComplete && step.onComplete(); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); } } diff --git a/dist/OnbordaContext.js b/dist/OnbordaContext.js index b97c8c0..3e25e72 100644 --- a/dist/OnbordaContext.js +++ b/dist/OnbordaContext.js @@ -59,31 +59,29 @@ const OnbordaProvider = ({ children, tours = [], activeTour = null, }) => { setCurrentStepState(0); setCompletedSteps(new Set()); }, [currentTour, currentTourSteps, completedSteps]); - const initializeCompletedSteps = useCallback(async (tourSteps) => { - return Promise.all(tourSteps.map(async (step) => { - return step.initialCompletedState ? step.initialCompletedState() : false; - })).then((results) => { - const firstIncomplete = results.findIndex((result) => !result); - const completed = results.reduce((acc, result, index) => { - if (result) { - acc.push(tourSteps[index]?.id || index); - } - return acc; - }, []); - setCompletedSteps(new Set(completed)); - // If all steps are completed, return the last step - return firstIncomplete === -1 ? tourSteps.length - 1 : firstIncomplete; - }); - }, []); + const initializeCompletedSteps = useCallback(async (tour) => { + // Get the initial state of the completed steps + const completeSteps = tour?.initialCompletedStepsState && await tour.initialCompletedStepsState() || tour.steps.map(() => false); + const firstIncomplete = completeSteps.findIndex((result) => !result); + const completed = completeSteps.reduce((acc, result, index) => { + if (result) { + acc.push(index); + } + return acc; + }, []); + setCompletedSteps(new Set(completed)); + // If all steps are completed, return the last step + return firstIncomplete === -1 ? tour.steps.length - 1 : firstIncomplete; + }, [currentTour]); const setCurrentTour = useCallback((tourName) => { if (!tourName) { closeOnborda(); return; } setCurrentTourState(tourName); - const tourSteps = tours.find((tour) => tour.tour === tourName)?.steps || []; - setCurrentTourStepsState(tourSteps); - initializeCompletedSteps(tourSteps).then(r => { + const tour = tours.find((tour) => tour.tour === tourName); + setCurrentTourStepsState(tour?.steps || []); + tour && initializeCompletedSteps(tour).then(r => { setCurrentStep(r); setOnbordaVisible(true); }); diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 67da477..172cee3 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -25,9 +25,9 @@ export interface OnbordaContextType { /** flag to check if Onborda is visible */ isOnbordaVisible: boolean; /** default completed steps */ - completedSteps: Set; + completedSteps: Set; /** setstate function to set the completed steps */ - setCompletedSteps: React.Dispatch>>; + setCompletedSteps: React.Dispatch>>; } export interface Step { /** The unique identifier for the step */ @@ -54,8 +54,6 @@ export interface Step { interactable?: boolean; /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ isCompleteConditions?: (element: Element | null) => boolean; - /** Initial completed state of the step. an async function called on tour started.*/ - initialCompletedState?: () => Promise; /** The route for this step */ route?: string; /** The route to navigate to for the next step */ @@ -84,6 +82,8 @@ export interface Tour { dismissible?: boolean; /** Any additional data for custom use */ [key: string]: any; + /** Initial completed steps state of the tour. an async function called on tour started. Can be a Server Action e.g. Promise.all([]) on API calls. */ + initialCompletedStepsState?: () => Promise; } export interface OnbordaProps { /** The children elements to be rendered inside the Onborda component */ diff --git a/src/Onborda.tsx b/src/Onborda.tsx index 1af0fbb..d31af10 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -85,19 +85,19 @@ const Onborda: React.FC = ({ debug && console.log("Onborda: Step Interaction", step, isComplete); // Check if the step is complete based on the conditions, and not already marked as completed - if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Completed", step); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); // If callback is provided, call it step?.onComplete && step.onComplete(); } // Check if the step is incomplete based on the conditions, and already marked as completed - else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Incomplete", step); setCompletedSteps((prev) => { - prev.delete(step?.id ?? currentStep); + prev.delete(currentStep); return prev; }); } @@ -156,19 +156,19 @@ const Onborda: React.FC = ({ debug && console.log("Onborda: Step Interaction", step, isComplete); // Check if the step is complete based on the conditions, and not already marked as completed - if (isComplete && !Array.from(completedSteps).includes(step?.id ?? currentStep)) { + if (isComplete && !Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Completed", step); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); // If callback is provided, call it step?.onComplete && step.onComplete(); } // Check if the step is incomplete based on the conditions, and already marked as completed - else if (!isComplete && Array.from(completedSteps).includes(step?.id ?? currentStep)) { + else if (!isComplete && Array.from(completedSteps).includes(currentStep)) { debug && console.log("Onborda: Step Incomplete", step); setCompletedSteps((prev) => { - prev.delete(step?.id ?? currentStep); + prev.delete(currentStep); return prev; }); } @@ -229,14 +229,14 @@ const Onborda: React.FC = ({ // Mark the step as completed step?.onComplete && step.onComplete(); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); } }else { // Mark the step as completed step?.onComplete && step.onComplete(); setCompletedSteps((prev) => { - return prev.add(step?.id ?? currentStep); + return prev.add(currentStep); }); } diff --git a/src/OnbordaContext.tsx b/src/OnbordaContext.tsx index 6bfcae8..ead1a83 100644 --- a/src/OnbordaContext.tsx +++ b/src/OnbordaContext.tsx @@ -33,7 +33,7 @@ const OnbordaProvider: React.FC = ({ const [currentStep, setCurrentStepState] = useState(0); const [isOnbordaVisible, setOnbordaVisible] = useState(false); const [currentTourSteps, setCurrentTourStepsState] = useState([]); - const [completedSteps, setCompletedSteps] = useState>(new Set()); + const [completedSteps, setCompletedSteps] = useState>(new Set()); // Start the active tour on mount useEffect(() => { @@ -75,22 +75,21 @@ const OnbordaProvider: React.FC = ({ }, [currentTour, currentTourSteps, completedSteps]); - const initializeCompletedSteps = useCallback(async (tourSteps: Step[]) => { - return Promise.all(tourSteps.map(async (step) => { - return step.initialCompletedState ? step.initialCompletedState() : false; - })).then((results) => { - const firstIncomplete = results.findIndex((result) => !result); - const completed = results.reduce<(string | number)[]>((acc, result, index) => { - if (result) { - acc.push(tourSteps[index]?.id || index); - } - return acc; - }, []); - setCompletedSteps(new Set(completed)); - // If all steps are completed, return the last step - return firstIncomplete === -1 ? tourSteps.length - 1 : firstIncomplete; - }); - },[]); + const initializeCompletedSteps = useCallback(async (tour: Tour) => { + // Get the initial state of the completed steps + const completeSteps = tour?.initialCompletedStepsState && await tour.initialCompletedStepsState() || tour.steps.map(() => false); + const firstIncomplete = completeSteps.findIndex((result) => !result); + const completed = completeSteps.reduce<(number)[]>((acc, result, index) => { + if (result) { + acc.push(index); + } + return acc; + }, []); + setCompletedSteps(new Set(completed)); + // If all steps are completed, return the last step + return firstIncomplete === -1 ? tour.steps.length - 1 : firstIncomplete; + + },[currentTour]); const setCurrentTour = useCallback((tourName: string | null) => { if (!tourName) { @@ -98,9 +97,9 @@ const OnbordaProvider: React.FC = ({ return } setCurrentTourState(tourName); - const tourSteps = tours.find((tour) => tour.tour === tourName)?.steps || []; - setCurrentTourStepsState(tourSteps); - initializeCompletedSteps(tourSteps).then(r => { + const tour = tours.find((tour) => tour.tour === tourName) + setCurrentTourStepsState(tour?.steps || []); + tour && initializeCompletedSteps(tour).then(r => { setCurrentStep(r); setOnbordaVisible(true); }); diff --git a/src/types/index.ts b/src/types/index.ts index 0fef8e2..bc0e72e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -29,9 +29,9 @@ export interface OnbordaContextType { /** flag to check if Onborda is visible */ isOnbordaVisible: boolean; /** default completed steps */ - completedSteps: Set; + completedSteps: Set; /** setstate function to set the completed steps */ - setCompletedSteps: React.Dispatch>>; + setCompletedSteps: React.Dispatch>>; } // Step @@ -63,8 +63,6 @@ export interface Step { interactable?: boolean; /** Conditions to be met before the next step can be triggered. Function is bound to event listeners on the target element on 'input', 'change' and 'click' events. */ isCompleteConditions?: (element: Element | null) => boolean; - /** Initial completed state of the step. an async function called on tour started.*/ - initialCompletedState?: () => Promise; // Routing /** The route for this step */ @@ -102,6 +100,8 @@ export interface Tour { dismissible?: boolean; /** Any additional data for custom use */ [key: string]: any; + /** Initial completed steps state of the tour. an async function called on tour started. Can be a Server Action e.g. Promise.all([]) on API calls. */ + initialCompletedStepsState?: () => Promise; } // Onborda From 46d7fd346c324870bd07020be944f88f2fc96b94 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Fri, 22 Nov 2024 10:08:30 +0000 Subject: [PATCH 28/30] Step.icon is now optional. Added TourCard.pendingRouteChange flag and TourCard.closeOnborda() --- README.md | 111 ++++++++++++++++++++++-------------------- dist/Onborda.js | 5 +- dist/types/index.d.ts | 10 ++-- src/Onborda.tsx | 6 +++ src/types/index.ts | 12 +++-- 5 files changed, 85 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index a604063..b9f7396 100644 --- a/README.md +++ b/README.md @@ -46,16 +46,18 @@ const config: Config = { ### Card Component If you require greater control over the card design or simply wish to create a totally custom component then you can do so easily. The following props can be used to customise your card component. -| Prop | Type | Description | -|---------------|--------------------------------------|---------------------------------------------------------------------------------| -| `step` | `Object` | The current `Step` object from your steps array, including content, title, etc. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `totalSteps` | `number` | The total number of steps in the onboarding process. | -| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | -| `nextStep` | `() => void` | A function to advance to the next step in the onboarding process. | -| `prevStep` | `() => void` | A function to go back to the previous step in the onboarding process. | -| `arrow` | `JSX.Element` | An SVG object, the orientation is controlled by the steps side prop | -| `completedSteps`| `number[]` | An array of completed step indexes/ids. | +| Prop | Type | Description | +|----------------------|-------------------------------------|-----------------------------------------------------------------------------------------------| +| `step` | `Object` | The current `Step` object from your steps array, including content, title, etc. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `totalSteps` | `number` | The total number of steps in the onboarding process. | +| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `nextStep` | `() => void` | A function to advance to the next step in the onboarding process. | +| `prevStep` | `() => void` | A function to go back to the previous step in the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | +| `arrow` | `JSX.Element` | An SVG object, the orientation is controlled by the steps side prop | +| `completedSteps` | `number[]` | An array of completed step indexes/ids. | +| `pendingRouteChange` | `boolean` | A boolean to determine if a route change is pending. Only set if the step has a selector set. | ```tsx @@ -69,16 +71,21 @@ export const CustomCardComponent = ({ setStep, nextStep, prevStep, + closeOnborda, arrow, - completedSteps + completedSteps, + pendingRouteChange }: CardComponentProps) => { return (

{step.icon} {step.title} {completedSteps.contains(currentStep) ? <>✅ : <>}

{currentStep} of {totalSteps}

-

{step.content}

- - +
+ {step.content} +
+ + + {arrow}
@@ -89,14 +96,14 @@ export const CustomCardComponent = ({ ### Tour Component If you require greater control over the tour design or simply wish to create a totally custom component then you can do so easily. -| Prop | Type | Description | -|---------------|----------|---------------------------------------------------------------| -| `tour` | `Tour` | The current `Tour` object from your tours array. | -| `currentTour` | `string` | The current tour name. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `completedSteps`| `number[]`| An array of completed step indexes/ids. | -| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | -| `closeOnborda` | `() => void` | A function to close the onboarding process. | +| Prop | Type | Description | +|------------------|-------------------------------------|---------------------------------------------------------------| +| `tour` | `Tour` | The current `Tour` object from your tours array. | +| `currentTour` | `string` | The current tour name. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `completedSteps` | `number[]` | An array of completed step indexes/ids. | +| `setStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | ```tsx "use client" @@ -137,20 +144,20 @@ export const CustomTourComponent = ({ ``` ### Tour object -| Prop | Type | Description | -|------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `tour` | `string` | The name of the tour. | -| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | -| `title` | `string` | Optional. The title of the tour. | -| `description` | `string` | Optional. The description of the tour. | -| `dissmissable` | `boolean`| Optional. Determines whether the user can dismiss the tour. | -| `onComplete` | `() => void` | Optional. A function that is called when the tour is completed. | +| Prop | Type | Description | +|------------------------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `tour` | `string` | The name of the tour. | +| `steps` | `Step[]` | An array of `Step` objects defining each step of the tour. | +| `title` | `string` | Optional. The title of the tour. | +| `description` | `string` | Optional. The description of the tour. | +| `dissmissable` | `boolean` | Optional. Determines whether the user can dismiss the tour. | +| `onComplete` | `() => void` | Optional. A function that is called when the tour is completed. | | `initialCompletedStepsState` | `() => Promise` | Optional. A client or server function that returns a promise that resolves to an array of booleans for each step. If true, the step is marked as completed on tour init. | -| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the TourCard component. | +| `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the TourCard component. | ```tsx -{ +const tour : Tour = { tour: "firstyour", steps: [ Step @@ -162,28 +169,28 @@ export const CustomTourComponent = ({ | Prop | Type | Description | |------------------------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | `string` | Optional. A unique identifier for the step. If set, step can be acitavted using `setStep(step.id)`. Otherwise steps[] index is used. | -| `icon` | `React.ReactNode`, `string`, `null` | An icon or element to display alongside the step title. | -| `title` | `string` | The title of your step | | `content` | `React.ReactNode` | The main content or body of the step. | +| `id` | `string` | Optional. A unique identifier for the step. If set, step can be acitavted using `setStep(step.id)`. Otherwise steps[] index is used. | +| `title` | `string` | Optional. The title of your step | +| `icon` | `React.ReactNode`, `string`, `null` | Optional. An icon or element to display alongside the step title. | | `selector` | `string` | Optional. A string used to target an Element by `id` that this step refers to. Takes precedence over `customQuerySelector`. | | `customQuerySelector` | `()=>Element \| null` | Optional. A client function that returns the element to target that this step refers to. Proceeded by `selector`.
Useful for targeting complex elements, like those from UI libraries. | | `side` | `"top"`, `"bottom"`, `"left"`, `"right"` | Optional. Determines where the tooltip should appear relative to the selector. | | `showControls` | `boolean` | Optional. Determines whether control buttons (next, prev) should be shown if using the default card. | | `pointerPadding` | `number` | Optional. The padding around the pointer (keyhole) highlighting the target element. | | `pointerRadius` | `number` | Optional. The border-radius of the pointer (keyhole) highlighting the target element. | -| `nextRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the next step. | -| `prevRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | | `route` | `string` | Optional. The route to navigate to using `next/navigation` when this step is set. | | `interactable` | `boolean` | Optional. Determines whether the user can interact with the target element. | | `isCompleteConditions` | `(element: Element) => boolean` | Optional. A client function that returns a boolean. If true, the step is marked as completed. Called when the user interacts with the target element. | | `onComplete` | `() => void` | Optional. A client function that is called when the step is marked as completed. Called if `isCompleteConditions` returns true. | | `[key: string]` | `any` | Optional. Any additional properties you wish to add. Will be available in the `useOnborda` hook as well as the Card component. | +| `nextRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the next step. | +| `prevRoute` | `string` | **Deprecated**. Optional. The route to navigate to using `next/navigation` when moving to the previous step. | > [!IMPORTANT] > The `nextRoute` and `prevRoute` properties have been deprecated. Please use the `route` property instead. ```tsx -{ +const step: Step = { icon: <>👋, title: "Tour 1, Step 1", content: <>First tour, first step, @@ -200,7 +207,7 @@ export const CustomTourComponent = ({ ```tsx import { FirstTourInitialState } from "@app/lib/initialTourStates"; // Optional initial stare from server action -[ +const tours : Tour[] = [ { tour: "firsttour", title: "First Tour", @@ -297,7 +304,6 @@ export async function FirstTourInitialState() { | Property | Type | Description | |--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `children` | `React.ReactNode` | Your website or application content. | -| `steps` | `Array[]` | **Deprecated** An array of `Step` objects defining each step of the onboarding process. | | `showOnborda` | `boolean` | Optional. Controls the visibility of the onboarding overlay, eg. if the user is a first time visitor. Defaults to `false`. | | `shadowRgb` | `string` | Optional. The RGB values for the shadow color surrounding the target area. Defaults to black `"0,0,0"`. | | `shadowOpacity` | `string` | Optional. The opacity value for the shadow surrounding the target area. Defaults to `"0.2"` | @@ -306,6 +312,7 @@ export async function FirstTourInitialState() { | `tourComponent` | `React.ReactNode` | The React component to use as a list of steps for the current tour. | | `debug` | `boolean` | Optional. Console logs the current step and the target element. Defaults to `false`. | | `observerTimeout` | `number` | Optional. The timeout in milliseconds for the observer to wait for the target element to be available. Defaults to `5000`. Observer is used to wait for the target element to be available before proceeding to the next step e.g. when the target element is on a different route. | +| `steps` | `Array[]` | **Deprecated** An array of `Step` objects defining each step of the onboarding process. | > [!IMPORTANT] > The `steps` property has been deprecated. Please use the `OnbordaProvider.tours` property instead. @@ -327,18 +334,18 @@ export async function FirstTourInitialState() { ### UseOnborda Context The `useOnborda` hook provides a set of functions to control the onboarding process from any child within the `OnboardaProvider`. The hook returns an object with the following properties: -| Property | Type | Description | -|--------------------|---------------------------------------------------------------|--------------------------------------------------------------------------| -| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | -| `startOnborda` | `() => void` | A function to start the onboarding process. | -| `closeOnborda` | `() => void` | A function to close the onboarding process. | -| `curreentTour` | `string` | The name of the current tour. | -| `currentStep` | `number` | The index of the current step in the steps array. | -| `currentTourSteps` | `Step[]` | The steps array for the current tour. | -| `setCurrentStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | -| `completedSteps` | `Set` | An array of completed step indexes/ids. | -| `setCompletedSteps`| `React.Dispatch>>` | A function to set the completed steps array. | -| `isOnbordaVisible` | `boolean` | A boolean to determine if the onboarding overlay is visible. | +| Property | Type | Description | +|--------------------|-------------------------------------------------------|--------------------------------------------------------------------------| +| `tours` | `Tour[]` | An array of `Tour` objects defining each tour of the onboarding process. | +| `startOnborda` | `() => void` | A function to start the onboarding process. | +| `closeOnborda` | `() => void` | A function to close the onboarding process. | +| `curreentTour` | `string` | The name of the current tour. | +| `currentStep` | `number` | The index of the current step in the steps array. | +| `currentTourSteps` | `Step[]` | The steps array for the current tour. | +| `setCurrentStep` | `(step: number \| string) => void;` | A function to set the current step in the onboarding process. | +| `completedSteps` | `Set` | An array of completed step indexes/ids. | +| `setCompletedSteps`| `React.Dispatch>>` | A function to set the completed steps array. | +| `isOnbordaVisible` | `boolean` | A boolean to determine if the onboarding overlay is visible. | ```tsx const { diff --git a/dist/Onborda.js b/dist/Onborda.js index 8a7f9e8..822fecf 100644 --- a/dist/Onborda.js +++ b/dist/Onborda.js @@ -17,6 +17,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT const [pointerPosition, setPointerPosition] = useState(null); const currentElementRef = useRef(null); const [currentRoute, setCurrentRoute] = useState(null); + const [pendingRouteChange, setPendingRouteChange] = useState(false); const hasSelector = (step) => { return !!step?.selector || !!step?.customQuerySelector; }; @@ -162,6 +163,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT childList: true, subtree: true, }); + setPendingRouteChange(true); // Set a timeout to disconnect the observer if the element is not found within a certain period const timeoutId = setTimeout(() => { observer.disconnect(); @@ -170,6 +172,7 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT // Clear the timeout if the observer disconnects successfully const originalDisconnect = observer.disconnect.bind(observer); observer.disconnect = () => { + setPendingRouteChange(false); clearTimeout(timeoutId); originalDisconnect(); }; @@ -345,6 +348,6 @@ const Onborda = ({ children, shadowRgb = "0, 0, 0", shadowOpacity = "0.2", cardT width: pointerPosition.width + pointerPadding, height: pointerPosition.height + pointerPadding, } - : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), completedSteps: Array.from(completedSteps) }) }) }) }), TourComponent && currentTourObject !== undefined && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { tour: currentTourObject, currentTour: currentTour, currentStep: currentStep, setStep: setStep, completedSteps: Array.from(completedSteps), closeOnborda: closeOnborda }) }) }))] }))] })); + : {}, transition: cardTransition, children: _jsx("div", { className: "absolute flex flex-col max-w-[100%] transition-all min-w-min pointer-events-auto z-[999]", "data-name": "onborda-card", style: getCardStyle(currentTourSteps?.[currentStep]?.side), children: _jsx(CardComponent, { step: currentTourSteps?.[currentStep], currentStep: currentStep, totalSteps: currentTourSteps?.length ?? 0, nextStep: nextStep, prevStep: prevStep, setStep: setStep, closeOnborda: closeOnborda, arrow: _jsx(CardArrow, { isVisible: currentTourSteps?.[currentStep] ? hasSelector(currentTourSteps?.[currentStep]) : false }), completedSteps: Array.from(completedSteps), pendingRouteChange: pendingRouteChange }) }) }) }), TourComponent && currentTourObject !== undefined && (_jsx(motion.div, { "data-name": 'onborda-tour-wrapper', className: 'fixed top-0 left-0 z-[998] w-screen h-screen pointer-events-none', children: _jsx(motion.div, { "data-name": 'onborda-tour', className: 'pointer-events-auto', children: _jsx(TourComponent, { tour: currentTourObject, currentTour: currentTour, currentStep: currentStep, setStep: setStep, completedSteps: Array.from(completedSteps), closeOnborda: closeOnborda }) }) }))] }))] })); }; export default Onborda; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 172cee3..60f5bf5 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -32,12 +32,12 @@ export interface OnbordaContextType { export interface Step { /** The unique identifier for the step */ id?: string; - /** The icon to be displayed in the step */ - icon: React.ReactNode | string | null; /** The title of the step */ - title: string; + title?: string; /** The content to be displayed in the step */ content: React.ReactNode; + /** The icon to be displayed in the step */ + icon?: React.ReactNode | string | null; /** The CSS selector for the element to highlight. Takes precedence over customQuerySelector if both are provided. */ selector?: string; /** A custom function to query the target element. Ignored if selector is provided. */ @@ -121,10 +121,14 @@ export interface CardComponentProps { nextStep: () => void; /** Function to navigate to the previous step */ prevStep: () => void; + /** Function to close the Onborda */ + closeOnborda: () => void; /** The arrow element to be displayed in the card */ arrow: JSX.Element; /** Array of completed steps */ completedSteps: (string | number)[]; + /** Is waiting for Route change */ + pendingRouteChange: boolean; } export interface TourComponentProps { /** The current tour name */ diff --git a/src/Onborda.tsx b/src/Onborda.tsx index d31af10..61b9f62 100644 --- a/src/Onborda.tsx +++ b/src/Onborda.tsx @@ -35,6 +35,7 @@ const Onborda: React.FC = ({ } | null>(null); const currentElementRef = useRef(null); const [currentRoute, setCurrentRoute] = useState(null); + const [pendingRouteChange, setPendingRouteChange] = useState(false); const hasSelector = (step: Step): boolean => { return !!step?.selector || !!step?.customQuerySelector; @@ -203,6 +204,8 @@ const Onborda: React.FC = ({ subtree: true, }); + setPendingRouteChange(true); + // Set a timeout to disconnect the observer if the element is not found within a certain period const timeoutId = setTimeout(() => { observer.disconnect(); @@ -212,6 +215,7 @@ const Onborda: React.FC = ({ // Clear the timeout if the observer disconnects successfully const originalDisconnect = observer.disconnect.bind(observer); observer.disconnect = () => { + setPendingRouteChange(false); clearTimeout(timeoutId); originalDisconnect(); }; @@ -462,8 +466,10 @@ const Onborda: React.FC = ({ nextStep={nextStep} prevStep={prevStep} setStep={setStep} + closeOnborda={closeOnborda} arrow={} completedSteps={Array.from(completedSteps)} + pendingRouteChange={pendingRouteChange} /> diff --git a/src/types/index.ts b/src/types/index.ts index bc0e72e..2cf0489 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -39,12 +39,12 @@ export interface Step { // Step Content /** The unique identifier for the step */ id?: string; - /** The icon to be displayed in the step */ - icon: React.ReactNode | string | null; /** The title of the step */ - title: string; + title?: string; /** The content to be displayed in the step */ content: React.ReactNode; + /** The icon to be displayed in the step */ + icon?: React.ReactNode | string | null; /** The CSS selector for the element to highlight. Takes precedence over customQuerySelector if both are provided. */ selector?: string; /** A custom function to query the target element. Ignored if selector is provided. */ @@ -159,11 +159,17 @@ export interface CardComponentProps { /** Function to navigate to the previous step */ prevStep: () => void; + /** Function to close the Onborda */ + closeOnborda: () => void; + /** The arrow element to be displayed in the card */ arrow: JSX.Element; /** Array of completed steps */ completedSteps: (string|number)[]; + + /** Is waiting for Route change */ + pendingRouteChange: boolean; } // Tour Component From 5dededf46832a788015863cbcfa5533183a0a7b0 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Fri, 22 Nov 2024 11:30:40 +0000 Subject: [PATCH 29/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9f7396..4ce5d73 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ export const CustomTourComponent = ({ closeOnborda }: TourComponentProps) => { return ( -
+
{/* Tailwind CSS classes to position TourCard in bottom left of screen */}

{currentTour}

{currentStep} of {tour.steps.length}

{completedSteps.length} of {tour.steps.length} completed

From 151f202ee46963212f746617c389d62f88ff84d9 Mon Sep 17 00:00:00 2001 From: Alex Carter Date: Fri, 22 Nov 2024 13:34:20 +0000 Subject: [PATCH 30/30] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4ce5d73..4df51b0 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Onborda is a lightweight onboarding flow that utilises [framer-motion](https://w ## Getting started ```bash # npm -npm i Libresoft-UK/onborda +npm i onborda # pnpm -pnpm add Libresoft-UK/onborda +pnpm add onborda # yarn -yarn add Libresoft-UK/onborda +yarn add onborda ``` ### Global `layout.tsx`