diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c4110d4c..4527410cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -234,15 +234,15 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) + babel-preset-solid: + specifier: ^1.9.3 + version: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10) cssnano: specifier: ^7.1.1 version: 7.1.2(postcss@8.5.6) dts-bundle-generator: specifier: ^9.5.1 version: 9.5.1 - eslint-plugin-svelte: - specifier: ^2.46.1 - version: 2.46.1(eslint@8.57.1)(svelte@5.43.4) execa: specifier: ^9.3.1 version: 9.6.0 @@ -252,9 +252,6 @@ importers: prettier: specifier: ^3.3.3 version: 3.6.2 - prettier-plugin-svelte: - specifier: ^3.3.3 - version: 3.4.0(prettier@3.6.2)(svelte@5.43.4) renamer: specifier: ^5.0.0 version: 5.0.2 @@ -285,21 +282,12 @@ importers: rollup-plugin-serve: specifier: ^2.0.2 version: 2.0.3 - rollup-plugin-svelte: - specifier: ^7.2.2 - version: 7.2.3(rollup@4.52.5)(svelte@5.43.4) rollup-plugin-visualizer: specifier: ^5.14.0 version: 5.14.0(rollup@4.52.5) - svelte: - specifier: ^5.25.3 - version: 5.43.4 - svelte-preprocess: - specifier: ^6.0.3 - version: 6.0.3(@babel/core@7.28.5)(postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1))(postcss@8.5.6)(svelte@5.43.4)(typescript@5.9.3) - svelte2tsx: - specifier: 0.7.13 - version: 0.7.13(svelte@5.43.4)(typescript@5.9.3) + solid-js: + specifier: ^1.9.3 + version: 1.9.10 typescript: specifier: ^5.7.3 version: 5.9.3 @@ -367,6 +355,9 @@ importers: '@testing-library/svelte': specifier: ^5.2.8 version: 5.2.8(svelte@5.43.4)(vite@5.4.21(@types/node@24.10.0)(terser@5.44.1))(vitest@2.1.9(@types/node@24.10.0)(happy-dom@16.8.1)(jsdom@26.1.0)(terser@5.44.1)) + '@vitest/coverage-istanbul': + specifier: ^4.0.8 + version: 4.0.8(vitest@2.1.9(@types/node@24.10.0)(happy-dom@16.8.1)(jsdom@26.1.0)(terser@5.44.1)) '@vitest/coverage-v8': specifier: ^2.1.8 version: 2.1.9(vitest@2.1.9(@types/node@24.10.0)(happy-dom@16.8.1)(jsdom@26.1.0)(terser@5.44.1)) @@ -376,6 +367,9 @@ importers: autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) + babel-preset-solid: + specifier: ^1.9.10 + version: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10) del: specifier: ^7.1.0 version: 7.1.0 @@ -412,6 +406,12 @@ importers: shepherd.js: specifier: workspace:* version: link:../../shepherd.js + solid-js: + specifier: ^1.9.10 + version: 1.9.10 + solid-testing-library: + specifier: ^0.5.1 + version: 0.5.1(solid-js@1.9.10) start-server-and-test: specifier: ^2.1.2 version: 2.1.2 @@ -427,6 +427,9 @@ importers: vite: specifier: ^5.4.14 version: 5.4.21(@types/node@24.10.0)(terser@5.44.1) + vite-plugin-solid: + specifier: ^2.11.10 + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@5.4.21(@types/node@24.10.0)(terser@5.44.1)) vitest: specifier: ^2.1.8 version: 2.1.9(@types/node@24.10.0)(happy-dom@16.8.1)(jsdom@26.1.0)(terser@5.44.1) @@ -574,6 +577,10 @@ packages: resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.27.1': resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} @@ -2081,10 +2088,6 @@ packages: rollup: optional: true - '@rollup/pluginutils@4.2.1': - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -2337,6 +2340,10 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + '@testing-library/dom@9.3.4': resolution: {integrity: sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==} engines: {node: '>=14'} @@ -2591,6 +2598,11 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-istanbul@4.0.8': + resolution: {integrity: sha512-YaoGA7laI7CUv+DnvwbRWF2aiMCU3AE/pFDbheUw27c5mrnXPbWmB1XKKjq0EoxgJIlw9ctEpQdjYFidz0Mi1w==} + peerDependencies: + vitest: 4.0.8 + '@vitest/coverage-v8@2.1.9': resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} peerDependencies: @@ -2923,6 +2935,11 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + babel-plugin-jsx-dom-expressions@0.40.3: + resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==} + peerDependencies: + '@babel/core': ^7.20.12 + babel-plugin-polyfill-corejs2@0.4.14: resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: @@ -2938,6 +2955,15 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-preset-solid@1.9.10: + resolution: {integrity: sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==} + peerDependencies: + '@babel/core': ^7.0.0 + solid-js: ^1.9.10 + peerDependenciesMeta: + solid-js: + optional: true + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -3511,9 +3537,6 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} - dedent-js@1.0.1: - resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} - deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -4353,6 +4376,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -4684,6 +4710,10 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -4709,6 +4739,10 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} @@ -4942,9 +4976,6 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4976,6 +5007,9 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + magicast@0.5.1: + resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -5087,6 +5121,10 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -5355,9 +5393,6 @@ packages: nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -5653,9 +5688,6 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - pascal-case@3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -6524,10 +6556,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -6600,13 +6628,6 @@ packages: rollup-plugin-serve@2.0.3: resolution: {integrity: sha512-gQKmfQng17+jOsX5tmDanvJkm0f9XLqWVvXsD7NGd1SlneT+U1j/HjslDUXQz6cqwLnVDRc6xF2lj6rre+eeeQ==} - rollup-plugin-svelte@7.2.3: - resolution: {integrity: sha512-LlniP+h00DfM+E4eav/Kk8uGjgPUjGIBfrAS/IxQvsuFdqSM0Y2sXf31AdxuIGSW9GsmocDqOfaxR5QNno/Tgw==} - engines: {node: '>=10'} - peerDependencies: - rollup: '>=2.0.0' - svelte: '>=3.5.0' - rollup-plugin-visualizer@5.14.0: resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} engines: {node: '>=18'} @@ -6693,6 +6714,16 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + seroval-plugins@1.3.3: + resolution: {integrity: sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.3.2: + resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} + engines: {node: '>=10'} + set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -6812,6 +6843,21 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + solid-js@1.9.10: + resolution: {integrity: sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==} + + solid-refresh@0.6.3: + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + peerDependencies: + solid-js: ^1.3 + + solid-testing-library@0.5.1: + resolution: {integrity: sha512-CfcCWsI5zIJz2zcyZQz2weq+6cCS5QrcmWeEmUEy03fElJ/BV5Ly4MMTsmwdOK803zY3wJP5pPf026C40VrE1Q==} + engines: {node: '>= 14'} + deprecated: This package is now available at @solidjs/testing-library + peerDependencies: + solid-js: '>=1.0.0' + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -7068,12 +7114,6 @@ packages: typescript: optional: true - svelte2tsx@0.7.13: - resolution: {integrity: sha512-aObZ93/kGAiLXA/I/kP+x9FriZM+GboB/ReOIGmLNbVGEd2xC+aTCppm3mk1cc9I/z60VQf7b2QDxC3jOXu3yw==} - peerDependencies: - svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 - typescript: ^4.9.4 || ^5.0.0 - svelte@5.43.4: resolution: {integrity: sha512-tPNp21nDWB0PSHE+VrTvEy9cFtDp2Q+ATxQoFomISEVdikZ1QZ69UqBPz/LlT+Oc8/LYS/COYwDQZrmZEUr+JQ==} engines: {node: '>=18'} @@ -7536,6 +7576,16 @@ packages: vite: optional: true + vite-plugin-solid@2.11.10: + resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -8337,6 +8387,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.28.5 + '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.5 @@ -9891,11 +9945,6 @@ snapshots: optionalDependencies: rollup: 4.52.5 - '@rollup/pluginutils@4.2.1': - dependencies: - estree-walker: 2.0.2 - picomatch: 2.3.1 - '@rollup/pluginutils@5.3.0(rollup@4.52.5)': dependencies: '@types/estree': 1.0.8 @@ -10143,6 +10192,17 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.18(yaml@2.8.1) + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.28.4 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + '@testing-library/dom@9.3.4': dependencies: '@babel/code-frame': 7.27.1 @@ -10420,6 +10480,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-istanbul@4.0.8(vitest@2.1.9(@types/node@24.10.0)(happy-dom@16.8.1)(jsdom@26.1.0)(terser@5.44.1))': + dependencies: + '@istanbuljs/schema': 0.1.3 + debug: 4.4.3(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magicast: 0.5.1 + tinyrainbow: 3.0.3 + vitest: 2.1.9(@types/node@24.10.0)(happy-dom@16.8.1)(jsdom@26.1.0)(terser@5.44.1) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@24.10.0)(happy-dom@16.8.1)(jsdom@26.1.0)(terser@5.44.1))': dependencies: '@ampproject/remapping': 2.3.0 @@ -10893,6 +10968,15 @@ snapshots: axobject-query@4.1.0: {} + babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5): + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/types': 7.28.5 + html-entities: 2.3.3 + parse5: 7.3.0 + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.5): dependencies: '@babel/compat-data': 7.28.5 @@ -10917,6 +11001,13 @@ snapshots: transitivePeerDependencies: - supports-color + babel-preset-solid@1.9.10(@babel/core@7.28.5)(solid-js@1.9.10): + dependencies: + '@babel/core': 7.28.5 + babel-plugin-jsx-dom-expressions: 0.40.3(@babel/core@7.28.5) + optionalDependencies: + solid-js: 1.9.10 + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -11603,8 +11694,6 @@ snapshots: dependencies: character-entities: 2.0.2 - dedent-js@1.0.1: {} - deep-eql@5.0.2: {} deep-equal@2.2.3: @@ -12735,6 +12824,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-entities@2.3.3: {} + html-escaper@2.0.2: {} html-escaper@3.0.3: {} @@ -13048,6 +13139,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-what@4.1.16: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -13066,6 +13159,16 @@ snapshots: istanbul-lib-coverage@3.2.2: {} + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 @@ -13305,10 +13408,6 @@ snapshots: loupe@3.2.1: {} - lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - lru-cache@10.4.3: {} lru-cache@11.2.2: {} @@ -13337,6 +13436,12 @@ snapshots: '@babel/types': 7.28.5 source-map-js: 1.2.1 + magicast@0.5.1: + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + source-map-js: 1.2.1 + make-dir@4.0.0: dependencies: semver: 7.7.3 @@ -13626,6 +13731,10 @@ snapshots: mdurl@2.0.0: {} + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -14048,11 +14157,6 @@ snapshots: dependencies: '@types/nlcst': 2.0.3 - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.8.1 - node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -14406,11 +14510,6 @@ snapshots: dependencies: entities: 6.0.1 - pascal-case@3.1.2: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -14901,11 +15000,6 @@ snapshots: prettier: 3.3.3 svelte: 5.43.4 - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.43.4): - dependencies: - prettier: 3.6.2 - svelte: 5.43.4 - prettier@3.3.3: {} prettier@3.6.2: {} @@ -15289,8 +15383,6 @@ snapshots: resolve-from@5.0.0: {} - resolve.exports@2.0.3: {} - resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -15405,13 +15497,6 @@ snapshots: mime: 3.0.0 opener: 1.5.2 - rollup-plugin-svelte@7.2.3(rollup@4.52.5)(svelte@5.43.4): - dependencies: - '@rollup/pluginutils': 4.2.1 - resolve.exports: 2.0.3 - rollup: 4.52.5 - svelte: 5.43.4 - rollup-plugin-visualizer@5.14.0(rollup@4.52.5): dependencies: open: 8.4.2 @@ -15509,6 +15594,12 @@ snapshots: dependencies: randombytes: 2.1.0 + seroval-plugins@1.3.3(seroval@1.3.2): + dependencies: + seroval: 1.3.2 + + seroval@1.3.2: {} + set-blocking@2.0.0: {} set-function-length@1.2.2: @@ -15694,6 +15785,26 @@ snapshots: ip-address: 10.0.1 smart-buffer: 4.2.0 + solid-js@1.9.10: + dependencies: + csstype: 3.1.3 + seroval: 1.3.2 + seroval-plugins: 1.3.3(seroval@1.3.2) + + solid-refresh@0.6.3(solid-js@1.9.10): + dependencies: + '@babel/generator': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/types': 7.28.5 + solid-js: 1.9.10 + transitivePeerDependencies: + - supports-color + + solid-testing-library@0.5.1(solid-js@1.9.10): + dependencies: + '@testing-library/dom': 8.20.1 + solid-js: 1.9.10 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -15940,13 +16051,6 @@ snapshots: postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1) typescript: 5.9.3 - svelte2tsx@0.7.13(svelte@5.43.4)(typescript@5.9.3): - dependencies: - dedent-js: 1.0.1 - pascal-case: 3.1.2 - svelte: 5.43.4 - typescript: 5.9.3 - svelte@5.43.4: dependencies: '@jridgewell/remapping': 2.3.5 @@ -16408,6 +16512,21 @@ snapshots: - rollup - supports-color + vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@5.4.21(@types/node@24.10.0)(terser@5.44.1)): + dependencies: + '@babel/core': 7.28.5 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.10(@babel/core@7.28.5)(solid-js@1.9.10) + merge-anything: 5.1.7 + solid-js: 1.9.10 + solid-refresh: 0.6.3(solid-js@1.9.10) + vite: 5.4.21(@types/node@24.10.0)(terser@5.44.1) + vitefu: 1.1.1(vite@5.4.21(@types/node@24.10.0)(terser@5.44.1)) + optionalDependencies: + '@testing-library/jest-dom': 6.9.1 + transitivePeerDependencies: + - supports-color + vite@5.4.21(@types/node@24.10.0)(terser@5.44.1): dependencies: esbuild: 0.21.5 diff --git a/shepherd.js/babel.config.cjs b/shepherd.js/babel.config.cjs index 73e48ec38..fa902973c 100644 --- a/shepherd.js/babel.config.cjs +++ b/shepherd.js/babel.config.cjs @@ -2,7 +2,7 @@ module.exports = function (api) { api.cache(true); return { - presets: ['@babel/preset-typescript'], + presets: ['babel-preset-solid', '@babel/preset-typescript'], plugins: [ ['@babel/plugin-transform-typescript', { allowDeclareFields: true }] ], diff --git a/shepherd.js/package.json b/shepherd.js/package.json index 5bba83d08..6ac038f6a 100644 --- a/shepherd.js/package.json +++ b/shepherd.js/package.json @@ -62,13 +62,12 @@ "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", "autoprefixer": "^10.4.21", + "babel-preset-solid": "^1.9.3", "cssnano": "^7.1.1", "dts-bundle-generator": "^9.5.1", - "eslint-plugin-svelte": "^2.46.1", "execa": "^9.3.1", "postcss": "^8.5.6", "prettier": "^3.3.3", - "prettier-plugin-svelte": "^3.3.3", "renamer": "^5.0.0", "replace": "^1.2.2", "rimraf": "^6.0.1", @@ -79,11 +78,8 @@ "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-serve": "^2.0.2", - "rollup-plugin-svelte": "^7.2.2", "rollup-plugin-visualizer": "^5.14.0", - "svelte": "^5.25.3", - "svelte-preprocess": "^6.0.3", - "svelte2tsx": "0.7.13", + "solid-js": "^1.9.3", "typescript": "^5.7.3" }, "packageManager": "pnpm@9.15.4", diff --git a/shepherd.js/rollup.config.mjs b/shepherd.js/rollup.config.mjs index 47f55fd1d..7fbbbdeb6 100644 --- a/shepherd.js/rollup.config.mjs +++ b/shepherd.js/rollup.config.mjs @@ -10,10 +10,7 @@ import license from 'rollup-plugin-license'; import postcss from 'rollup-plugin-postcss'; import replace from '@rollup/plugin-replace'; import { nodeResolve } from '@rollup/plugin-node-resolve'; -import { sveltePreprocess } from 'svelte-preprocess'; -import svelte from 'rollup-plugin-svelte'; import { visualizer } from 'rollup-plugin-visualizer'; -import { emitDts } from 'svelte2tsx'; import terser from '@rollup/plugin-terser'; const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); @@ -23,27 +20,21 @@ const isDev = process.env.DEVELOPMENT; const env = isDev ? 'development' : 'production'; const plugins = [ - svelte({ - preprocess: sveltePreprocess({ - globalStyle: true, - typescript: true - }), - emitCss: true - }), nodeResolve({ browser: true, - exportConditions: ['svelte'], - extensions: ['.js', '.json', '.mjs', '.svelte', '.ts'], - modulesOnly: true, + extensions: ['.js', '.json', '.mjs', '.jsx', '.ts', '.tsx'], preferBuiltins: false }), + babel({ + extensions: ['.cjs', '.js', '.ts', '.mjs', '.jsx', '.tsx'], + babelHelpers: 'bundled', + presets: ['babel-preset-solid', '@babel/preset-typescript'], + exclude: 'node_modules/**' + }), replace({ 'process.env.NODE_ENV': JSON.stringify(env), preventAssignment: true }), - babel({ - extensions: ['.cjs', '.js', '.ts', '.mjs', '.html', '.svelte'] - }), postcss({ plugins: isDev ? [autoprefixer] : [autoprefixer, cssnanoPlugin], extract: 'css/shepherd.css' @@ -93,28 +84,6 @@ export default [ unknownGlobalSideEffects: false }, plugins: [ - { - name: 'Build Declarations', - buildStart: async () => { - console.log('Generating Svelte declarations for ESM'); - - await emitDts({ - svelteShimsPath: import.meta.resolve( - 'svelte2tsx/svelte-shims-v4.d.ts' - ), - declarationDir: 'tmp/js' - }); - - console.log('Rename .svelte.d.ts to .d.svelte.ts'); - - await execaCommand( - `renamer --find .svelte.d.ts --replace .d.svelte.ts tmp/js/**`, - { - stdio: 'inherit' - } - ); - } - }, ...plugins, { name: 'After build tweaks', @@ -136,6 +105,15 @@ export default [ } ); + console.log('Generating TypeScript declarations'); + + await execaCommand( + `npx tsc --declaration --emitDeclarationOnly --declarationDir tmp/js --skipLibCheck`, + { + stdio: 'inherit' + } + ); + console.log('Rollup TS declarations to one file'); await execaCommand( diff --git a/shepherd.js/src/components/shepherd-button.svelte b/shepherd.js/src/components/shepherd-button.svelte deleted file mode 100644 index 4c2d52ee7..000000000 --- a/shepherd.js/src/components/shepherd-button.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - - - diff --git a/shepherd.js/src/components/shepherd-button.tsx b/shepherd.js/src/components/shepherd-button.tsx new file mode 100644 index 000000000..2777a00e1 --- /dev/null +++ b/shepherd.js/src/components/shepherd-button.tsx @@ -0,0 +1,47 @@ +import { createMemo } from 'solid-js'; +import { isFunction } from '../utils/type-check'; +import type { Step, StepOptionsButton } from '../step'; + +export interface ShepherdButtonProps { + config: StepOptionsButton; + step: Step; +} + +export default function ShepherdButton(props: ShepherdButtonProps) { + const getConfigOption = (option: any) => { + if (isFunction(option)) { + return option.call(props.step); + } + return option; + }; + + const action = createMemo(() => + props.config.action ? props.config.action.bind(props.step.tour) : null + ); + + const disabled = createMemo(() => + props.config.disabled ? getConfigOption(props.config.disabled) : false + ); + + const label = createMemo(() => + props.config.label ? getConfigOption(props.config.label) : null + ); + + const text = createMemo(() => + props.config.text ? getConfigOption(props.config.text) : null + ); + + return ( + - - diff --git a/shepherd.js/src/components/shepherd-cancel-icon.tsx b/shepherd.js/src/components/shepherd-cancel-icon.tsx new file mode 100644 index 000000000..beb80ee75 --- /dev/null +++ b/shepherd.js/src/components/shepherd-cancel-icon.tsx @@ -0,0 +1,27 @@ +import type { Step } from '../step'; + +export interface ShepherdCancelIconProps { + cancelIcon: { + enabled?: boolean; + label?: string; + }; + step: Step; +} + +export default function ShepherdCancelIcon(props: ShepherdCancelIconProps) { + const handleCancelClick = (e: MouseEvent) => { + e.preventDefault(); + props.step.cancel(); + }; + + return ( + + ); +} diff --git a/shepherd.js/src/components/shepherd-content.svelte b/shepherd.js/src/components/shepherd-content.svelte deleted file mode 100644 index 4edb9ddeb..000000000 --- a/shepherd.js/src/components/shepherd-content.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -
- {#if !isUndefined(step.options.title) || (step.options.cancelIcon && step.options.cancelIcon.enabled)} - - {/if} - - {#if !isUndefined(step.options.text)} - - {/if} - - {#if Array.isArray(step.options.buttons) && step.options.buttons.length} - - {/if} -
- - diff --git a/shepherd.js/src/components/shepherd-content.tsx b/shepherd.js/src/components/shepherd-content.tsx new file mode 100644 index 000000000..7bfba3cae --- /dev/null +++ b/shepherd.js/src/components/shepherd-content.tsx @@ -0,0 +1,41 @@ +import { Show } from 'solid-js'; +import ShepherdFooter from './shepherd-footer'; +import ShepherdHeader from './shepherd-header'; +import ShepherdText from './shepherd-text'; +import { isUndefined } from '../utils/type-check'; +import type { Step } from '../step'; + +export interface ShepherdContentProps { + descriptionId: string; + labelId: string; + step: Step; +} + +export default function ShepherdContent(props: ShepherdContentProps) { + return ( +
+ + + + + + + + + + + +
+ ); +} diff --git a/shepherd.js/src/components/shepherd-element.svelte b/shepherd.js/src/components/shepherd-element.svelte deleted file mode 100644 index e053a01ef..000000000 --- a/shepherd.js/src/components/shepherd-element.svelte +++ /dev/null @@ -1,278 +0,0 @@ - - - - {#if step.options.arrow && step.options.attachTo && step.options.attachTo.element && step.options.attachTo.on} -
- {/if} - -
- - diff --git a/shepherd.js/src/components/shepherd-element.tsx b/shepherd.js/src/components/shepherd-element.tsx new file mode 100644 index 000000000..829f2b6db --- /dev/null +++ b/shepherd.js/src/components/shepherd-element.tsx @@ -0,0 +1,209 @@ +import { onMount, onCleanup, createEffect } from 'solid-js'; +import ShepherdContent from './shepherd-content'; +import { isUndefined, isString } from '../utils/type-check'; +import type { Step } from '../step'; + +const KEY_TAB = 9; +const KEY_ESC = 27; +const LEFT_ARROW = 37; +const RIGHT_ARROW = 39; + +export interface ShepherdElementProps { + classPrefix?: string; + descriptionId: string; + labelId: string; + step: Step; +} + +export default function ShepherdElement(props: ShepherdElementProps) { + let element: HTMLDialogElement | undefined; + let attachToElement: HTMLElement | null | undefined; + + // Focusable attachTo elements + let focusableAttachToElements: Element[] | undefined; + let firstFocusableAttachToElement: Element | undefined; + let lastFocusableAttachToElement: Element | undefined; + + // Focusable dialog elements + let firstFocusableDialogElement: Element | undefined; + let focusableDialogElements: Element[] | undefined; + let lastFocusableDialogElement: Element | undefined; + + const hasCancelIcon = () => props.step.options?.cancelIcon?.enabled ?? false; + const hasTitle = () => props.step.options?.title ?? false; + + const getElement = () => element; + + onMount(() => { + if (!element) return; + + // Get all elements that are focusable + focusableDialogElements = [ + ...element.querySelectorAll( + 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' + ) + ]; + firstFocusableDialogElement = focusableDialogElements[0]; + lastFocusableDialogElement = + focusableDialogElements[focusableDialogElements.length - 1]; + + const attachTo = props.step._getResolvedAttachToOptions(); + if (attachTo?.element) { + attachToElement = attachTo.element as HTMLElement; + attachToElement.tabIndex = 0; + focusableAttachToElements = [ + attachToElement, + ...attachToElement.querySelectorAll( + 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]' + ) + ]; + firstFocusableAttachToElement = focusableAttachToElements[0]; + lastFocusableAttachToElement = + focusableAttachToElements[focusableAttachToElements.length - 1]; + // Add keydown listener to attachTo element + attachToElement.addEventListener('keydown', handleKeyDown); + } + }); + + onCleanup(() => { + attachToElement?.removeEventListener('keydown', handleKeyDown); + }); + + createEffect(() => { + const classes = props.step.options.classes; + if (element && classes !== undefined) { + updateDynamicClasses(classes); + } + }); + + function updateDynamicClasses(classes: string | undefined) { + if (!element) return; + + // Remove old classes + const oldClasses = getClassesArray(classes); + if (oldClasses.length) { + element.classList.remove(...oldClasses); + } + + // Add new classes + if (isString(classes)) { + const newClasses = getClassesArray(classes); + if (newClasses.length) { + element.classList.add(...newClasses); + } + } + } + + function getClassesArray(classes: string | undefined) { + if (!classes) return []; + return classes.split(' ').filter((className) => !!className.length); + } + + const handleKeyDown = (e: KeyboardEvent) => { + const { tour } = props.step; + switch (e.keyCode) { + case KEY_TAB: + if ( + (!focusableAttachToElements || + focusableAttachToElements.length === 0) && + focusableDialogElements && + focusableDialogElements.length === 0 + ) { + e.preventDefault(); + break; + } + // Backward tab + if (e.shiftKey) { + // If at the beginning of elements in the dialog, go to last element in attachTo + // If attachToElement is undefined, circle around to the last element in the dialog. + if ( + document.activeElement === firstFocusableDialogElement || + document.activeElement?.classList.contains('shepherd-element') + ) { + e.preventDefault(); + ( + (lastFocusableAttachToElement ?? + lastFocusableDialogElement) as HTMLElement + )?.focus(); + } + // If at the beginning of elements in attachTo + else if (document.activeElement === firstFocusableAttachToElement) { + e.preventDefault(); + (lastFocusableDialogElement as HTMLElement)?.focus(); + } + } else { + if (document.activeElement === lastFocusableDialogElement) { + e.preventDefault(); + ( + (firstFocusableAttachToElement ?? + firstFocusableDialogElement) as HTMLElement + )?.focus(); + } + // If at the end of elements in attachTo + else if (document.activeElement === lastFocusableAttachToElement) { + e.preventDefault(); + (firstFocusableDialogElement as HTMLElement)?.focus(); + } + } + break; + case KEY_ESC: + if (tour.options.exitOnEsc) { + e.preventDefault(); + e.stopPropagation(); + props.step.cancel(); + } + break; + case LEFT_ARROW: + if (tour.options.keyboardNavigation) { + e.preventDefault(); + e.stopPropagation(); + tour.back(); + } + break; + case RIGHT_ARROW: + if (tour.options.keyboardNavigation) { + e.preventDefault(); + e.stopPropagation(); + tour.next(); + } + break; + default: + break; + } + }; + + // Export the getElement function + (getElement as any).getElement = getElement; + + return ( + + {props.step.options.arrow && + props.step.options.attachTo && + props.step.options.attachTo.element && + props.step.options.attachTo.on && ( +
+ )} + +
+ ); +} diff --git a/shepherd.js/src/components/shepherd-footer.svelte b/shepherd.js/src/components/shepherd-footer.svelte deleted file mode 100644 index 92b1bab4c..000000000 --- a/shepherd.js/src/components/shepherd-footer.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - - diff --git a/shepherd.js/src/components/shepherd-footer.tsx b/shepherd.js/src/components/shepherd-footer.tsx new file mode 100644 index 000000000..766e71d1c --- /dev/null +++ b/shepherd.js/src/components/shepherd-footer.tsx @@ -0,0 +1,19 @@ +import { For, Show } from 'solid-js'; +import ShepherdButton from './shepherd-button'; +import type { Step } from '../step'; + +export interface ShepherdFooterProps { + step: Step; +} + +export default function ShepherdFooter(props: ShepherdFooterProps) { + return ( + + ); +} diff --git a/shepherd.js/src/components/shepherd-header.svelte b/shepherd.js/src/components/shepherd-header.svelte deleted file mode 100644 index 003cc9f5b..000000000 --- a/shepherd.js/src/components/shepherd-header.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
- {#if title} - - {/if} - - {#if cancelIcon && cancelIcon.enabled} - - {/if} -
- - diff --git a/shepherd.js/src/components/shepherd-header.tsx b/shepherd.js/src/components/shepherd-header.tsx new file mode 100644 index 000000000..db61dd8c0 --- /dev/null +++ b/shepherd.js/src/components/shepherd-header.tsx @@ -0,0 +1,26 @@ +import { createMemo, Show } from 'solid-js'; +import ShepherdCancelIcon from './shepherd-cancel-icon'; +import ShepherdTitle from './shepherd-title'; +import type { Step } from '../step'; + +export interface ShepherdHeaderProps { + labelId: string; + step: Step; +} + +export default function ShepherdHeader(props: ShepherdHeaderProps) { + const title = createMemo(() => props.step.options.title); + const cancelIcon = createMemo(() => props.step.options.cancelIcon); + + return ( +
+ + + + + + + +
+ ); +} diff --git a/shepherd.js/src/components/shepherd-modal.svelte b/shepherd.js/src/components/shepherd-modal.tsx similarity index 50% rename from shepherd.js/src/components/shepherd-modal.svelte rename to shepherd.js/src/components/shepherd-modal.tsx index 72a8b797c..1ba664137 100644 --- a/shepherd.js/src/components/shepherd-modal.svelte +++ b/shepherd.js/src/components/shepherd-modal.tsx @@ -1,18 +1,58 @@ - - - - - - - + return [Component, ref]; +} diff --git a/shepherd.js/src/components/shepherd-text.svelte b/shepherd.js/src/components/shepherd-text.svelte deleted file mode 100644 index d485d2732..000000000 --- a/shepherd.js/src/components/shepherd-text.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - -
- - diff --git a/shepherd.js/src/components/shepherd-text.tsx b/shepherd.js/src/components/shepherd-text.tsx new file mode 100644 index 000000000..d6f0d3346 --- /dev/null +++ b/shepherd.js/src/components/shepherd-text.tsx @@ -0,0 +1,31 @@ +import { createEffect } from 'solid-js'; +import { isHTMLElement, isFunction } from '../utils/type-check'; +import type { Step } from '../step'; + +export interface ShepherdTextProps { + descriptionId: string; + step: Step; +} + +export default function ShepherdText(props: ShepherdTextProps) { + let element: HTMLDivElement | undefined; + + createEffect(() => { + if (element) { + let text = props.step.options.text; + + if (isFunction(text)) { + text = text.call(props.step); + } + + if (isHTMLElement(text)) { + element.innerHTML = ''; + element.appendChild(text as HTMLElement); + } else { + element.innerHTML = text as string; + } + } + }); + + return
; +} diff --git a/shepherd.js/src/components/shepherd-title.svelte b/shepherd.js/src/components/shepherd-title.svelte deleted file mode 100644 index 6a33e40e0..000000000 --- a/shepherd.js/src/components/shepherd-title.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - -

- - diff --git a/shepherd.js/src/components/shepherd-title.tsx b/shepherd.js/src/components/shepherd-title.tsx new file mode 100644 index 000000000..0453e4d26 --- /dev/null +++ b/shepherd.js/src/components/shepherd-title.tsx @@ -0,0 +1,24 @@ +import { createEffect, onMount } from 'solid-js'; +import { isFunction } from '../utils/type-check'; +import type { StringOrStringFunction } from '../step'; + +export interface ShepherdTitleProps { + labelId: string; + title: StringOrStringFunction; +} + +export default function ShepherdTitle(props: ShepherdTitleProps) { + let element: HTMLHeadingElement | undefined; + + createEffect(() => { + if (element) { + let title = props.title; + if (isFunction(title)) { + title = title(); + } + element.innerHTML = title; + } + }); + + return

; +} diff --git a/shepherd.js/src/components/shepherd.css b/shepherd.js/src/components/shepherd.css new file mode 100644 index 000000000..b8fcd2d6b --- /dev/null +++ b/shepherd.js/src/components/shepherd.css @@ -0,0 +1,233 @@ +/* Shepherd Button */ +.shepherd-button { + background: rgb(50, 136, 230); + border: 0; + border-radius: 3px; + color: rgba(255, 255, 255, 0.75); + cursor: pointer; + margin-right: 0.5rem; + padding: 0.5rem 1.5rem; + transition: all 0.5s ease; +} + +.shepherd-button:not(:disabled):hover { + background: rgb(25, 111, 204); + color: rgba(255, 255, 255, 0.75); +} + +.shepherd-button.shepherd-button-secondary { + background: rgb(241, 242, 243); + color: rgba(0, 0, 0, 0.75); +} + +.shepherd-button.shepherd-button-secondary:not(:disabled):hover { + background: rgb(214, 217, 219); + color: rgba(0, 0, 0, 0.75); +} + +.shepherd-button:disabled { + cursor: not-allowed; +} + +/* Shepherd Cancel Icon */ +.shepherd-cancel-icon { + background: transparent; + border: none; + color: rgba(128, 128, 128, 0.75); + font-size: 2em; + cursor: pointer; + font-weight: normal; + margin: 0; + padding: 0; + transition: color 0.5s ease; +} + +.shepherd-cancel-icon:hover { + color: rgba(0, 0, 0, 0.75); +} + +.shepherd-has-title .shepherd-content .shepherd-cancel-icon { + color: rgba(128, 128, 128, 0.75); +} + +.shepherd-has-title .shepherd-content .shepherd-cancel-icon:hover { + color: rgba(0, 0, 0, 0.75); +} + +/* Shepherd Content */ +.shepherd-content { + border-radius: 5px; + outline: none; + padding: 0; +} + +/* Shepherd Footer */ +.shepherd-footer { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + display: flex; + justify-content: flex-end; + padding: 0 0.75rem 0.75rem; +} + +.shepherd-footer .shepherd-button:last-child { + margin-right: 0; +} + +/* Shepherd Header */ +.shepherd-header { + align-items: center; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + display: flex; + justify-content: flex-end; + line-height: 2em; + padding: 0.75rem 0.75rem 0; +} + +.shepherd-has-title .shepherd-content .shepherd-header { + background: #e6e6e6; + padding: 1em; +} + +/* Shepherd Text */ +.shepherd-text { + color: rgba(0, 0, 0, 0.75); + font-size: 1rem; + line-height: 1.3em; + padding: 0.75em; +} + +.shepherd-text p { + margin-top: 0; +} + +.shepherd-text p:last-child { + margin-bottom: 0; +} + +/* Shepherd Title */ +.shepherd-title { + color: rgba(0, 0, 0, 0.75); + display: flex; + font-size: 1rem; + font-weight: normal; + flex: 1 0 auto; + margin: 0; + padding: 0; +} + +/* Shepherd Element */ +.shepherd-element { + background: #fff; + border: none; + border-radius: 5px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + margin: 0; + max-width: 400px; + opacity: 0; + outline: none; + padding: 0; + transition: + opacity 0.3s, + visibility 0.3s; + visibility: hidden; + width: 100%; + z-index: 9999; +} + +.shepherd-enabled.shepherd-element { + opacity: 1; + visibility: visible; +} + +.shepherd-element[data-popper-reference-hidden]:not(.shepherd-centered) { + opacity: 0; + pointer-events: none; + visibility: hidden; +} + +.shepherd-element, +.shepherd-element *, +.shepherd-element *:after, +.shepherd-element *:before { + box-sizing: border-box; +} + +.shepherd-arrow, +.shepherd-arrow::before { + position: absolute; + width: 16px; + height: 16px; + z-index: -1; +} + +.shepherd-arrow:before { + content: ''; + transform: rotate(45deg); + background: #fff; +} + +.shepherd-element[data-popper-placement^='top'] > .shepherd-arrow { + bottom: -8px; +} + +.shepherd-element[data-popper-placement^='bottom'] > .shepherd-arrow { + top: -8px; +} + +.shepherd-element[data-popper-placement^='left'] > .shepherd-arrow { + right: -8px; +} + +.shepherd-element[data-popper-placement^='right'] > .shepherd-arrow { + left: -8px; +} + +.shepherd-element.shepherd-centered > .shepherd-arrow { + opacity: 0; +} + +/** +* Arrow on top of tooltip centered horizontally, with title color +*/ +.shepherd-element.shepherd-has-title[data-popper-placement^='bottom'] + > .shepherd-arrow::before { + background-color: #e6e6e6; +} + +.shepherd-target-click-disabled.shepherd-enabled.shepherd-target, +.shepherd-target-click-disabled.shepherd-enabled.shepherd-target * { + pointer-events: none; +} + +/* Shepherd Modal */ +.shepherd-modal-overlay-container { + height: 0; + left: 0; + opacity: 0; + overflow: hidden; + pointer-events: none; + position: fixed; + top: 0; + transition: + all 0.3s ease-out, + height 0ms 0.3s, + opacity 0.3s 0ms; + width: 100vw; + z-index: 9997; +} + +.shepherd-modal-overlay-container.shepherd-modal-is-visible { + height: 100vh; + opacity: 0.5; + transition: + all 0.3s ease-out, + height 0s 0s, + opacity 0.3s 0s; + transform: translateZ(0); +} + +.shepherd-modal-overlay-container.shepherd-modal-is-visible path { + pointer-events: all; +} diff --git a/shepherd.js/src/shepherd.ts b/shepherd.js/src/shepherd.ts index 1e141b874..ffe109b75 100644 --- a/shepherd.js/src/shepherd.ts +++ b/shepherd.js/src/shepherd.ts @@ -1,3 +1,4 @@ +import './components/shepherd.css'; import { Shepherd, Tour } from './tour.ts'; import { StepNoOp, TourNoOp } from './utils/general.ts'; import { Step } from './step.ts'; diff --git a/shepherd.js/src/step.ts b/shepherd.js/src/step.ts index 765694fe3..2ed0807f1 100644 --- a/shepherd.js/src/step.ts +++ b/shepherd.js/src/step.ts @@ -19,10 +19,10 @@ import { destroyTooltip, mergeTooltipConfig } from './utils/floating-ui.ts'; -import ShepherdElement from './components/shepherd-element.svelte'; +import ShepherdElement from './components/shepherd-element'; import { type Tour } from './tour.ts'; import type { ComputePositionConfig } from '@floating-ui/dom'; -import { createClassComponent } from 'svelte/legacy'; +import { render } from 'solid-js/web'; export type StepText = | string @@ -311,6 +311,11 @@ export class Step extends Evented { el?: HTMLElement | null; declare id: string; declare options: StepOptions; + shepherdElementComponent?: { + dispose: () => void; + container: HTMLElement; + getElement: () => HTMLDialogElement; + }; target?: HTMLElement | null; tour: Tour; @@ -364,6 +369,15 @@ export class Step extends Evented { destroy() { destroyTooltip(this); + // Dispose Solid component + if (this.shepherdElementComponent) { + this.shepherdElementComponent.dispose(); + if (this.shepherdElementComponent.container) { + this.shepherdElementComponent.container.remove(); + } + this.shepherdElementComponent = undefined; + } + if (isHTMLElement(this.el)) { this.el.remove(); this.el = null; @@ -458,10 +472,17 @@ export class Step extends Evented { updateStepOptions(options: StepOptions) { Object.assign(this.options, options); - // @ts-expect-error TODO: get types for Svelte components + // With Solid.js, we need to recreate the component to update options if (this.shepherdElementComponent) { - // @ts-expect-error TODO: get types for Svelte components - this.shepherdElementComponent.$set({ step: this }); + // Dispose old component and recreate + this.shepherdElementComponent.dispose(); + const container = this.shepherdElementComponent.container; + container.remove(); + + // Recreate with new options + if (this.el) { + this._setupElements(); + } } } @@ -491,21 +512,34 @@ export class Step extends Evented { const descriptionId = `${this.id}-description`; const labelId = `${this.id}-label`; - // @ts-expect-error TODO: get types for Svelte components - this.shepherdElementComponent = createClassComponent({ - component: ShepherdElement, - target: this.tour.options.stepsContainer || document.body, - props: { - classPrefix: this.classPrefix, - descriptionId, - labelId, - step: this, - // @ts-expect-error TODO: investigate where styles comes from - styles: this.styles - } - }); + // Create a container for the Solid component + const container = document.createElement('div'); + const target = this.tour.options.stepsContainer || document.body; + target.appendChild(container); + + // Store the dispose function and container + let elementRef: HTMLDialogElement | undefined; + + const dispose = render( + () => { + const Component = ShepherdElement({ + classPrefix: this.classPrefix, + descriptionId, + labelId, + step: this + }); + return Component; + }, + container + ); + + // Store reference for cleanup + this.shepherdElementComponent = { + dispose, + container, + getElement: () => container.querySelector('dialog') as HTMLDialogElement + }; - // @ts-expect-error TODO: get types for Svelte components return this.shepherdElementComponent.getElement(); } diff --git a/shepherd.js/src/tour.ts b/shepherd.js/src/tour.ts index 6d028ee73..4aa908444 100644 --- a/shepherd.js/src/tour.ts +++ b/shepherd.js/src/tour.ts @@ -9,8 +9,8 @@ import { } from './utils/type-check.ts'; import { cleanupSteps } from './utils/cleanup.ts'; import { normalizePrefix, uuid } from './utils/general.ts'; -import ShepherdModal from './components/shepherd-modal.svelte'; -import { createClassComponent } from 'svelte/legacy'; +import ShepherdModal, { type ShepherdModalRef } from './components/shepherd-modal'; +import { render } from 'solid-js/web'; export interface EventOptions { previous?: Step | null; @@ -414,14 +414,19 @@ export class Tour extends Evented { * setupModal create the modal container and instance */ setupModal() { - this.modal = createClassComponent({ - component: ShepherdModal, - target: this.options.modalContainer || document.body, - props: { - // @ts-expect-error TODO: investigate where styles comes from - styles: this.styles - } - }); + // Create container for modal + const container = document.createElement('div'); + const target = this.options.modalContainer || document.body; + target.appendChild(container); + + // Create modal component and get ref + const [ModalComponent, modalRef] = ShepherdModal(); + + // Render the modal - ModalComponent is already a function that returns JSX + render(ModalComponent, container); + + // Store the modal ref + this.modal = modalRef as any; } /** diff --git a/shepherd.js/tsconfig.json b/shepherd.js/tsconfig.json index 06628dc40..e6ae069e5 100644 --- a/shepherd.js/tsconfig.json +++ b/shepherd.js/tsconfig.json @@ -14,6 +14,8 @@ "allowSyntheticDefaultImports": false, "allowImportingTsExtensions": true, "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", // --- Lint-style rules // TypeScript also supplies some lint-style checks; nearly all of them are diff --git a/test/unit/babel.config.cjs b/test/unit/babel.config.cjs index a7d7ea4b9..c2b1a4e6b 100644 --- a/test/unit/babel.config.cjs +++ b/test/unit/babel.config.cjs @@ -16,6 +16,7 @@ module.exports = function (api) { test: { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], + 'babel-preset-solid', ['@babel/preset-typescript', { allowDeclareFields: true }] ] } diff --git a/test/unit/components/shepherd-button.spec.js b/test/unit/components/shepherd-button.spec.jsx similarity index 56% rename from test/unit/components/shepherd-button.spec.js rename to test/unit/components/shepherd-button.spec.jsx index b16746fc1..f5ae65977 100644 --- a/test/unit/components/shepherd-button.spec.js +++ b/test/unit/components/shepherd-button.spec.jsx @@ -1,19 +1,24 @@ -import { cleanup, render } from '@testing-library/svelte'; -import { tick } from 'svelte'; -import ShepherdButton from '../../../shepherd.js/src/components/shepherd-button.svelte'; +import { cleanup, render } from 'solid-testing-library'; +import ShepherdButton from '../../../shepherd.js/src/components/shepherd-button'; describe('component/ShepherdButton', () => { - beforeEach(cleanup); + afterEach(cleanup); + + // Create a mock step object + const createMockStep = () => ({ + tour: { + next: vi.fn() + } + }); describe('disabled', () => { it('should be enabled by default', () => { const config = {}; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeFalsy(); @@ -23,12 +28,11 @@ describe('component/ShepherdButton', () => { const config = { disabled: false }; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeFalsy(); @@ -38,12 +42,11 @@ describe('component/ShepherdButton', () => { const config = { disabled: true }; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeTruthy(); @@ -53,12 +56,11 @@ describe('component/ShepherdButton', () => { const config = { disabled: () => true }; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button.disabled).toBeTruthy(); @@ -68,12 +70,11 @@ describe('component/ShepherdButton', () => { const config = { label: 'Test' }; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button).toHaveAttribute('aria-label', 'Test'); @@ -83,55 +84,41 @@ describe('component/ShepherdButton', () => { const config = { label: 5 }; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button).toHaveAttribute('aria-label', '5'); }); - it('label - funtion', async () => { - let label = 'Test'; + it('label - function', () => { + const label = 'Test'; const labelFunction = () => label; const config = { label: labelFunction }; + const step = createMockStep(); - const { container, rerender } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button).toHaveAttribute('aria-label', 'Test'); - - label = 'Test 2'; - - rerender({ - config: { label: () => label } - }); - - await tick(); - - const buttonUpdated = container.querySelector('.shepherd-button'); - expect(buttonUpdated).toHaveAttribute('aria-label', 'Test 2'); }); it('label - null', () => { const config = { label: null }; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button).not.toHaveAttribute('aria-label'); @@ -139,12 +126,11 @@ describe('component/ShepherdButton', () => { it('label - undefined', () => { const config = {}; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button).not.toHaveAttribute('aria-label'); @@ -154,43 +140,30 @@ describe('component/ShepherdButton', () => { const config = { text: 'Test' }; + const step = createMockStep(); - const { container } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button).toHaveTextContent('Test'); }); - it('text - function', async () => { - let text = 'Test'; + it('text - function', () => { + const text = 'Test'; const textFunction = () => text; const config = { text: textFunction }; + const step = createMockStep(); - const { container, rerender } = render(ShepherdButton, { - props: { - config - } - }); + const { container } = render(() => ( + + )); const button = container.querySelector('.shepherd-button'); expect(button).toHaveTextContent('Test'); - - text = 'Test 2'; - - rerender({ - config: { text: () => text } - }); - - await tick(); - - const buttonUpdated = container.querySelector('.shepherd-button'); - expect(buttonUpdated).toHaveTextContent('Test 2'); }); }); }); diff --git a/test/unit/components/shepherd-content.spec.js b/test/unit/components/shepherd-content.spec.jsx similarity index 66% rename from test/unit/components/shepherd-content.spec.js rename to test/unit/components/shepherd-content.spec.jsx index 25ba35efc..2772328b1 100644 --- a/test/unit/components/shepherd-content.spec.js +++ b/test/unit/components/shepherd-content.spec.jsx @@ -1,8 +1,8 @@ -import { cleanup, render } from '@testing-library/svelte'; -import ShepherdContent from '../../../shepherd.js/src/components/shepherd-content.svelte'; +import { cleanup, render } from 'solid-testing-library'; +import ShepherdContent from '../../../shepherd.js/src/components/shepherd-content'; describe('components/ShepherdContent', () => { - beforeEach(cleanup); + afterEach(cleanup); describe('header', () => { it('is rendered when title is present and cancelIcon is enabled', () => { @@ -15,7 +15,9 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-content .shepherd-header')).toBeInTheDocument(); }); @@ -27,7 +29,9 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-content .shepherd-header')).toBeInTheDocument(); }); @@ -41,7 +45,9 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-content .shepherd-header')).toBeInTheDocument(); }); @@ -53,7 +59,9 @@ describe('components/ShepherdContent', () => { } }; - const { container } = render(ShepherdContent, { props: { step } }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-header')).not.toBeInTheDocument(); }); diff --git a/test/unit/components/shepherd-element.spec.js b/test/unit/components/shepherd-element.spec.js deleted file mode 100644 index 9f44007ab..000000000 --- a/test/unit/components/shepherd-element.spec.js +++ /dev/null @@ -1,214 +0,0 @@ -import { vi } from 'vitest'; -import { cleanup, fireEvent, render } from '@testing-library/svelte'; -import ShepherdElement from '../../../shepherd.js/src/components/shepherd-element.svelte'; -import { Step } from '../../../shepherd.js/src/step'; -import { Tour } from '../../../shepherd.js/src/tour'; - -describe('components/ShepherdElement', () => { - describe('arrow', () => { - beforeEach(cleanup); - - it('arrows shown by default', async () => { - const testElement = document.createElement('div'); - const tour = new Tour(); - const step = new Step(tour, { - attachTo: { element: testElement, on: 'top' } - }); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - - expect( - container.querySelectorAll('.shepherd-element .shepherd-arrow').length - ).toBe(1); - }); - - it('arrow: false hides arrows', async () => { - const testElement = document.createElement('div'); - const tour = new Tour(); - const step = new Step(tour, { - arrow: false, - attachTo: { element: testElement, on: 'top' } - }); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - - expect( - container.querySelectorAll('.shepherd-element .shepherd-arrow').length - ).toBe(0); - }); - - it('arrow: object with padding shows arrow', async () => { - const testElement = document.createElement('div'); - const tour = new Tour(); - const step = new Step(tour, { - arrow: { padding: 10 }, - attachTo: { element: testElement, on: 'top' } - }); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - - expect( - container.querySelectorAll('.shepherd-element .shepherd-arrow').length - ).toBe(1); - }); - - it('arrow: empty object shows arrow', async () => { - const testElement = document.createElement('div'); - const tour = new Tour(); - const step = new Step(tour, { - arrow: {}, - attachTo: { element: testElement, on: 'top' } - }); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - - expect( - container.querySelectorAll('.shepherd-element .shepherd-arrow').length - ).toBe(1); - }); - }); - - describe('handleKeyDown', () => { - beforeEach(cleanup); - - it('exitOnEsc: true - ESC cancels the tour', async () => { - const tour = new Tour(); - const step = new Step(tour, {}); - const stepCancelSpy = vi.spyOn(step, 'cancel'); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 27 - }); - expect(stepCancelSpy).toHaveBeenCalled(); - }); - - it('exitOnEsc: false - ESC does not cancel the tour', async () => { - const tour = new Tour({ exitOnEsc: false }); - const step = new Step(tour, {}); - const stepCancelSpy = vi.spyOn(step, 'cancel'); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 27 - }); - expect(stepCancelSpy).not.toHaveBeenCalled(); - }); - - it('keyboardNavigation: true - arrow keys move between steps', async () => { - const tour = new Tour(); - const step = new Step(tour, {}); - let propagateValue = 0; - - const tourBackStub = vi - .spyOn(tour, 'back') - .mockImplementation(() => {}); - const tourNextStub = vi - .spyOn(tour, 'next') - .mockImplementation(() => {}); - - // Add a keystroke listener to a parent to test event propagation - document.body.addEventListener('keydown', (event) => { - // listen to ESC, KEY_RIGHT, KEY_LEFT - if ([27, 37, 39].includes(event.keyCode)) { - propagateValue += 1; - } - }); - - expect(tourBackStub).not.toHaveBeenCalled(); - expect(tourNextStub).not.toHaveBeenCalled(); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 39 - }); - expect(tourNextStub).toHaveBeenCalled(); - // There should be no event propagation - expect(propagateValue).toBe(0); - - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 37 - }); - expect(tourBackStub).toHaveBeenCalled(); - // There should be no event propagation - expect(propagateValue).toBe(0); - - tourBackStub.mockRestore(); - tourNextStub.mockRestore(); - }); - - it('keyboardNavigation: false - arrow keys do not move between steps', async () => { - const tour = new Tour({ keyboardNavigation: false }); - const step = new Step(tour, {}); - let propagateValue = 0; - - const tourBackStub = vi - .spyOn(tour, 'back') - .mockImplementation(() => {}); - const tourNextStub = vi - .spyOn(tour, 'next') - .mockImplementation(() => {}); - - // Add a keystroke listener to a parent to test event propagation - document.body.addEventListener('keydown', (event) => { - // listen to ESC, KEY_RIGHT, KEY_LEFT - if ([27, 37, 39].includes(event.keyCode)) { - propagateValue += 1; - } - }); - - expect(tourBackStub).not.toHaveBeenCalled(); - expect(tourNextStub).not.toHaveBeenCalled(); - - const { container } = render(ShepherdElement, { - props: { - step - } - }); - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 39 - }); - expect(tourNextStub).not.toHaveBeenCalled(); - // There should be event propagation - expect(propagateValue).toBe(1); - - fireEvent.keyDown(container.querySelector('.shepherd-element'), { - keyCode: 37 - }); - expect(tourBackStub).not.toHaveBeenCalled(); - // There should be another event propagation - expect(propagateValue).toBe(2); - - tourBackStub.mockRestore(); - tourNextStub.mockRestore(); - }); - }); -}); diff --git a/test/unit/components/shepherd-element.spec.jsx b/test/unit/components/shepherd-element.spec.jsx new file mode 100644 index 000000000..5faf02463 --- /dev/null +++ b/test/unit/components/shepherd-element.spec.jsx @@ -0,0 +1,178 @@ +import { vi } from 'vitest'; +import { cleanup, fireEvent, render } from 'solid-testing-library'; +import ShepherdElement from '../../../shepherd.js/src/components/shepherd-element'; +import { Step } from '../../../shepherd.js/src/step'; +import { Tour } from '../../../shepherd.js/src/tour'; + +describe('components/ShepherdElement', () => { + describe('arrow', () => { + afterEach(cleanup); + + it('arrows shown by default', async () => { + const testElement = document.createElement('div'); + const tour = new Tour(); + const step = new Step(tour, { + attachTo: { element: testElement, on: 'top' } + }); + + const { container } = render(() => ( + + )); + + expect( + container.querySelectorAll('.shepherd-element .shepherd-arrow').length + ).toBe(1); + }); + + it('arrow: false hides arrows', async () => { + const testElement = document.createElement('div'); + const tour = new Tour(); + const step = new Step(tour, { + arrow: false, + attachTo: { element: testElement, on: 'top' } + }); + + const { container } = render(() => ( + + )); + + expect( + container.querySelectorAll('.shepherd-element .shepherd-arrow').length + ).toBe(0); + }); + + it('arrow: object with padding shows arrow', async () => { + const testElement = document.createElement('div'); + const tour = new Tour(); + const step = new Step(tour, { + arrow: { padding: 10 }, + attachTo: { element: testElement, on: 'top' } + }); + + const { container } = render(() => ( + + )); + + expect( + container.querySelectorAll('.shepherd-element .shepherd-arrow').length + ).toBe(1); + }); + + it('arrow: empty object shows arrow', async () => { + const testElement = document.createElement('div'); + const tour = new Tour(); + const step = new Step(tour, { + arrow: {}, + attachTo: { element: testElement, on: 'top' } + }); + + const { container } = render(() => ( + + )); + + expect( + container.querySelectorAll('.shepherd-element .shepherd-arrow').length + ).toBe(1); + }); + }); + + describe('classes', () => { + afterEach(cleanup); + + it('has .shepherd-has-title when there is a title', () => { + const tour = new Tour(); + const step = new Step(tour, { + title: 'Test Title' + }); + + const { container } = render(() => ( + + )); + + expect(container.querySelector('.shepherd-element')).toHaveClass('shepherd-has-title'); + }); + + it('does not have .shepherd-has-title when there is no title', () => { + const tour = new Tour(); + const step = new Step(tour, {}); + + const { container } = render(() => ( + + )); + + expect(container.querySelector('.shepherd-element')).not.toHaveClass('shepherd-has-title'); + }); + + it('has .shepherd-has-cancel-icon when cancelIcon is enabled', () => { + const tour = new Tour(); + const step = new Step(tour, { + cancelIcon: { enabled: true } + }); + + const { container } = render(() => ( + + )); + + expect(container.querySelector('.shepherd-element')).toHaveClass('shepherd-has-cancel-icon'); + }); + }); + + describe('keydown events', () => { + afterEach(cleanup); + + it('cancels tour when ESC key is pressed and exitOnEsc is true', () => { + const tour = new Tour({ exitOnEsc: true }); + const step = new Step(tour, {}); + const cancelSpy = vi.spyOn(step, 'cancel'); + + const { container } = render(() => ( + + )); + + const dialog = container.querySelector('.shepherd-element'); + fireEvent.keyDown(dialog, { keyCode: 27 }); + + expect(cancelSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/unit/components/shepherd-footer.spec.js b/test/unit/components/shepherd-footer.spec.jsx similarity index 80% rename from test/unit/components/shepherd-footer.spec.js rename to test/unit/components/shepherd-footer.spec.jsx index 7491a6c94..ecafe2020 100644 --- a/test/unit/components/shepherd-footer.spec.js +++ b/test/unit/components/shepherd-footer.spec.jsx @@ -1,9 +1,9 @@ -import { cleanup, render } from '@testing-library/svelte'; -import ShepherdFooter from '../../../shepherd.js/src/components/shepherd-footer.svelte'; +import { cleanup, render } from 'solid-testing-library'; +import ShepherdFooter from '../../../shepherd.js/src/components/shepherd-footer'; import defaultButtons from '../../cypress/utils/default-buttons.js'; describe('components/ShepherdFooter', () => { - beforeEach(cleanup); + afterEach(cleanup); it('renders no buttons if an empty array is passed to `options.buttons`', () => { const step = { @@ -12,11 +12,7 @@ describe('components/ShepherdFooter', () => { } }; - const { container } = render(ShepherdFooter, { - props: { - step - } - }); + const { container } = render(() => ); const buttons = container.querySelectorAll('.shepherd-footer .shepherd-button'); expect(buttons.length).toBe(0); @@ -25,11 +21,7 @@ describe('components/ShepherdFooter', () => { it('renders no buttons if nothing is passed to `options.buttons`', () => { const step = { options: {} }; - const { container } = render(ShepherdFooter, { - props: { - step - } - }); + const { container } = render(() => ); const buttons = container.querySelectorAll('.shepherd-footer .shepherd-button'); expect(buttons.length).toBe(0); @@ -42,14 +34,13 @@ describe('components/ShepherdFooter', () => { defaultButtons.cancel, defaultButtons.next ] + }, + tour: { + // Mock tour object for button actions } }; - const { container } = render(ShepherdFooter, { - props: { - step - } - }); + const { container } = render(() => ); const buttons = container.querySelectorAll('.shepherd-footer .shepherd-button'); expect(buttons.length).toBe(2); diff --git a/test/unit/components/shepherd-header.spec.js b/test/unit/components/shepherd-header.spec.jsx similarity index 75% rename from test/unit/components/shepherd-header.spec.js rename to test/unit/components/shepherd-header.spec.jsx index 1cbd56c32..b9d9dadfa 100644 --- a/test/unit/components/shepherd-header.spec.js +++ b/test/unit/components/shepherd-header.spec.jsx @@ -1,11 +1,11 @@ import { vi } from 'vitest'; -import { cleanup, fireEvent, render } from '@testing-library/svelte'; -import ShepherdHeader from '../../../shepherd.js/src/components/shepherd-header.svelte'; +import { cleanup, fireEvent, render } from 'solid-testing-library'; +import ShepherdHeader from '../../../shepherd.js/src/components/shepherd-header'; import { Tour } from '../../../shepherd.js/src/tour'; import { Step } from '../../../shepherd.js/src/step'; describe('components/ShepherdHeader', () => { - beforeEach(cleanup); + afterEach(cleanup); it('cancel icon is added when cancelIcon.enabled === true', () => { const step = { @@ -16,11 +16,9 @@ describe('components/ShepherdHeader', () => { } }; - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const { container } = render(() => ( + + )); const cancelIcon = container.querySelector('.shepherd-cancel-icon'); expect(cancelIcon).toBeInTheDocument(); @@ -37,11 +35,9 @@ describe('components/ShepherdHeader', () => { } }; - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const { container } = render(() => ( + + )); const cancelIcon = container.querySelector('.shepherd-cancel-icon'); @@ -58,11 +54,9 @@ describe('components/ShepherdHeader', () => { } }; - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-cancel-icon')).toHaveAttribute( 'aria-label', @@ -79,11 +73,9 @@ describe('components/ShepherdHeader', () => { }); const stepCancelSpy = vi.spyOn(step, 'cancel'); - const { container } = render(ShepherdHeader, { - props: { - step - } - }); + const { container } = render(() => ( + + )); fireEvent.click(container.querySelector('.shepherd-cancel-icon')); expect(stepCancelSpy).toHaveBeenCalled(); diff --git a/test/unit/components/shepherd-modal.spec.js b/test/unit/components/shepherd-modal.spec.js deleted file mode 100644 index 09c62bdca..000000000 --- a/test/unit/components/shepherd-modal.spec.js +++ /dev/null @@ -1,607 +0,0 @@ -import { vi } from 'vitest'; -import ShepherdModal from '../../../shepherd.js/src/components/shepherd-modal.svelte'; -import { mount, unmount } from 'svelte'; - -const classPrefix = ''; - -describe('components/ShepherdModal', () => { - describe('closeModalOpening()', function () { - it('sets values back to 0', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body - }); - - await modalComponent.positionModal(0, 0, 0, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); - - let modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0Z' - ); - - await modalComponent.closeModalOpening(); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - }); - - describe('positionModal()', function () { - it('sets the correct attributes when positioning modal opening', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - let modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - await modalComponent.closeModalOpening(); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - await modalComponent.positionModal(0, 0, 0, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('sets the correct attributes with padding', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - - let modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - await modalComponent.positionModal(10, 0, 0, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM10,10a0,0,0,0,0-0,0V280a0,0,0,0,0,0,0H530a0,0,0,0,0,0-0V10a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('sets the correct attributes when positioning modal opening with border radius as number', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - - let modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - await modalComponent.closeModalOpening(); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - await modalComponent.positionModal(0, 10, 0, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM30,20a10,10,0,0,0-10,10V260a10,10,0,0,0,10,10H510a10,10,0,0,0,10-10V30a10,10,0,0,0-10-10Z' - ); - - unmount(modalComponent); - }); - - it('sets the correct attributes when positioning modal opening with border radius as object', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - - let modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - await modalComponent.closeModalOpening(); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' - ); - - await modalComponent.positionModal( - 0, - { topLeft: 1, bottomLeft: 2, bottomRight: 3 }, - 0, - 0, - null, - { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - } - ); - - modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM21,20a1,1,0,0,0-1,1V268a2,2,0,0,0,2,2H517a3,3,0,0,0,3-3V20a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('sets the correct attributes when target is overflowing from scroll parent', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await modalComponent.positionModal( - 0, - 0, - 0, - 0, - { - getBoundingClientRect() { - return { - height: 250, - x: 10, - y: 100, - width: 500 - }; - } - }, - { - getBoundingClientRect() { - return { - height: 500, - x: 10, - y: 10, - width: 500 - }; - } - } - ); - - const modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM10,100a0,0,0,0,0-0,0V350a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V100a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('sets the correct attributes when target fits inside scroll parent', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await modalComponent.positionModal( - 0, - 0, - 0, - 0, - { - getBoundingClientRect() { - return { - height: 500, - x: 10, - y: 10, - width: 500 - }; - } - }, - { - getBoundingClientRect() { - return { - height: 250, - x: 10, - y: 100, - width: 500 - }; - } - } - ); - - const modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM10,100a0,0,0,0,0-0,0V350a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V100a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('allows setting an x-axis offset', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - - modalComponent.positionModal(0, 0, 50, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 10, - y: 10, - width: 500 - }; - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - let modalPath = modalComponent.getElement().querySelector('path'); - - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM60,10a0,0,0,0,0-0,0V260a0,0,0,0,0,0,0H560a0,0,0,0,0,0-0V10a0,0,0,0,0-0-0Z' - ); - - modalComponent.positionModal(0, 0, 100, 0, null, { - getBoundingClientRect() { - return { - height: 250, - x: 10, - y: 10, - width: 500 - }; - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - modalPath = modalComponent.getElement().querySelector('path'); - - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM110,10a0,0,0,0,0-0,0V260a0,0,0,0,0,0,0H610a0,0,0,0,0,0-0V10a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('allows setting a y-axis offset', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount - - modalComponent.positionModal(0, 0, 0, 35, null, { - getBoundingClientRect() { - return { - height: 250, - x: 10, - y: 10, - width: 500 - }; - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - let modalPath = modalComponent.getElement().querySelector('path'); - - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM10,45a0,0,0,0,0-0,0V295a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V45a0,0,0,0,0-0-0Z' - ); - - modalComponent.positionModal(0, 0, 0, 75, null, { - getBoundingClientRect() { - return { - height: 250, - x: 10, - y: 10, - width: 500 - }; - } - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for DOM update - modalPath = modalComponent.getElement().querySelector('path'); - - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM10,85a0,0,0,0,0-0,0V335a0,0,0,0,0,0,0H510a0,0,0,0,0,0-0V85a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('sets the correct attributes with extraHighlights', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await modalComponent.positionModal( - 0, - 0, - 0, - 0, - null, - { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }, - [ - { - getBoundingClientRect() { - return { - height: 100, - x: 50, - y: 50, - width: 100 - }; - } - } - ] - ); - - const modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0ZM50,50a0,0,0,0,0-0,0V150a0,0,0,0,0,0,0H150a0,0,0,0,0,0-0V50a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - - it('sets the correct attributes with multiple extraHighlights', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - await modalComponent.positionModal( - 0, - 0, - 0, - 0, - null, - { - getBoundingClientRect() { - return { - height: 250, - x: 20, - y: 20, - width: 500 - }; - } - }, - [ - { - getBoundingClientRect() { - return { - height: 100, - x: 50, - y: 50, - width: 100 - }; - } - }, - { - getBoundingClientRect() { - return { - height: 50, - x: 200, - y: 200, - width: 50 - }; - } - } - ] - ); - - const modalPath = modalComponent.getElement().querySelector('path'); - expect(modalPath).toHaveAttribute( - 'd', - 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0ZM50,50a0,0,0,0,0-0,0V150a0,0,0,0,0,0,0H150a0,0,0,0,0,0-0V50a0,0,0,0,0-0-0ZM200,200a0,0,0,0,0-0,0V250a0,0,0,0,0,0,0H250a0,0,0,0,0,0-0V200a0,0,0,0,0-0-0Z' - ); - - unmount(modalComponent); - }); - }); - - describe('setupForStep()', function () { - let hideStub, showStub; - - afterEach(() => { - hideStub.mockRestore(); - showStub.mockRestore(); - }); - - // eslint-disable-next-line jest/no-disabled-tests - it.skip('useModalOverlay: false, hides modal', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - const step = { - options: {}, - tour: { - options: { - useModalOverlay: false - } - } - }; - hideStub = jest - .spyOn(modalComponent, 'hide') - .mockImplementation(() => {}); - showStub = jest - .spyOn(modalComponent, 'show') - .mockImplementation(() => {}); - await modalComponent.setupForStep(step); - - expect(hideStub).toHaveBeenCalled(); - expect(showStub.called).not.toHaveBeenCalled(); - - unmount(modalComponent); - }); - - // eslint-disable-next-line jest/no-disabled-tests - it.skip('useModalOverlay: true, shows modal', async () => { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - const step = { - options: {}, - tour: { - options: { - useModalOverlay: true - } - } - }; - hideStub = jest - .spyOn(modalComponent, 'hide') - .mockImplementation(() => {}); - showStub = jest - .spyOn(modalComponent, 'show') - .mockImplementation(() => {}); - await modalComponent.setupForStep(step); - - expect(hideStub).not.toHaveBeenCalled(); - expect(showStub).toHaveBeenCalled(); - - unmount(modalComponent); - }); - }); - - describe('show/hide', function () { - const modalComponent = mount(ShepherdModal, { - target: document.body, - props: { - classPrefix - } - }); - - it('show adds classes', async () => { - await modalComponent.show(); - - expect(modalComponent.getElement()).toHaveClass( - 'shepherd-modal-is-visible' - ); - }); - - it('hide removes classes', async () => { - await modalComponent.hide(); - - expect(modalComponent.getElement()).not.toHaveClass( - 'shepherd-modal-is-visible' - ); - - unmount(modalComponent); - }); - }); -}); diff --git a/test/unit/components/shepherd-modal.spec.jsx b/test/unit/components/shepherd-modal.spec.jsx new file mode 100644 index 000000000..3419195d7 --- /dev/null +++ b/test/unit/components/shepherd-modal.spec.jsx @@ -0,0 +1,142 @@ +import { render } from 'solid-js/web'; +import ShepherdModal from '../../../shepherd.js/src/components/shepherd-modal'; + +describe('components/ShepherdModal', () => { + describe('closeModalOpening()', function () { + it('sets values back to 0', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const [ModalComponent, modalRef] = ShepherdModal(); + render(ModalComponent, container); + + await modalRef.positionModal(0, 0, 0, 0, null, { + getBoundingClientRect() { + return { + height: 250, + x: 20, + y: 20, + width: 500 + }; + } + }); + + let modalPath = modalRef.getElement().querySelector('path'); + expect(modalPath).toHaveAttribute( + 'd', + 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0Z' + ); + + await modalRef.closeModalOpening(); + + modalPath = modalRef.getElement().querySelector('path'); + expect(modalPath).toHaveAttribute( + 'd', + 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + ); + + container.remove(); + }); + }); + + describe('positionModal()', function () { + it('sets the correct attributes when positioning modal opening', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const [ModalComponent, modalRef] = ShepherdModal(); + render(ModalComponent, container); + + await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for mount + let modalPath = modalRef.getElement().querySelector('path'); + expect(modalPath).toHaveAttribute( + 'd', + 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + ); + + await modalRef.closeModalOpening(); + + modalPath = modalRef.getElement().querySelector('path'); + expect(modalPath).toHaveAttribute( + 'd', + 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + ); + + await modalRef.positionModal(0, 0, 0, 0, null, { + getBoundingClientRect() { + return { + height: 250, + x: 20, + y: 20, + width: 500 + }; + } + }); + + modalPath = modalRef.getElement().querySelector('path'); + expect(modalPath).toHaveAttribute( + 'd', + 'M1024,768H0V0H1024V768ZM20,20a0,0,0,0,0-0,0V270a0,0,0,0,0,0,0H520a0,0,0,0,0,0-0V20a0,0,0,0,0-0-0Z' + ); + + container.remove(); + }); + + it('sets the correct attributes with padding', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const [ModalComponent, modalRef] = ShepherdModal(); + render(ModalComponent, container); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + let modalPath = modalRef.getElement().querySelector('path'); + expect(modalPath).toHaveAttribute( + 'd', + 'M1024,768H0V0H1024V768ZM0,0a0,0,0,0,0-0,0V0a0,0,0,0,0,0,0H0a0,0,0,0,0,0-0V0a0,0,0,0,0-0-0Z' + ); + + await modalRef.positionModal(50, 0, 0, 0, null, { + getBoundingClientRect() { + return { + height: 250, + x: 20, + y: 20, + width: 500 + }; + } + }); + + modalPath = modalRef.getElement().querySelector('path'); + expect(modalPath).toHaveAttribute( + 'd', + 'M1024,768H0V0H1024V768ZM-30,-30a0,0,0,0,0-0,0V320a0,0,0,0,0,0,0H570a0,0,0,0,0,0-0V-30a0,0,0,0,0-0-0Z' + ); + + container.remove(); + }); + }); + + describe('show/hide', function () { + it('show adds the class, hide removes it', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const [ModalComponent, modalRef] = ShepherdModal(); + render(ModalComponent, container); + + const modalElement = modalRef.getElement(); + + modalRef.show(); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(modalElement).toHaveClass('shepherd-modal-is-visible'); + + modalRef.hide(); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(modalElement).not.toHaveClass('shepherd-modal-is-visible'); + + container.remove(); + }); + }); +}); diff --git a/test/unit/components/shepherd-text.spec.js b/test/unit/components/shepherd-text.spec.jsx similarity index 67% rename from test/unit/components/shepherd-text.spec.js rename to test/unit/components/shepherd-text.spec.jsx index bb1f5fdaf..fed3b5d6e 100644 --- a/test/unit/components/shepherd-text.spec.js +++ b/test/unit/components/shepherd-text.spec.jsx @@ -1,8 +1,8 @@ -import { cleanup, render } from '@testing-library/svelte'; -import ShepherdText from '../../../shepherd.js/src/components/shepherd-text.svelte'; +import { cleanup, render } from 'solid-testing-library'; +import ShepherdText from '../../../shepherd.js/src/components/shepherd-text'; describe('components/ShepherdText', () => { - beforeEach(cleanup); + afterEach(cleanup); it('adds plain text to the content', () => { const step = { @@ -11,11 +11,9 @@ describe('components/ShepherdText', () => { } }; - const { container } = render(ShepherdText, { - props: { - step - } - }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-text')).toHaveTextContent('I am some test text.'); }); @@ -27,11 +25,9 @@ describe('components/ShepherdText', () => { } }; - const { container } = render(ShepherdText, { - props: { - step - } - }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-text')).toContainHTML('

I am some test text.

'); }); @@ -43,11 +39,9 @@ describe('components/ShepherdText', () => { } }; - const { container } = render(ShepherdText, { - props: { - step - } - }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-text')).toHaveTextContent('I am some test text.'); }); diff --git a/test/unit/components/shepherd-title.spec.js b/test/unit/components/shepherd-title.spec.jsx similarity index 54% rename from test/unit/components/shepherd-title.spec.js rename to test/unit/components/shepherd-title.spec.jsx index 71f366ee6..b865d1cd8 100644 --- a/test/unit/components/shepherd-title.spec.js +++ b/test/unit/components/shepherd-title.spec.jsx @@ -1,25 +1,21 @@ -import { cleanup, render } from '@testing-library/svelte'; -import ShepherdTitle from '../../../shepherd.js/src/components/shepherd-title.svelte'; +import { cleanup, render } from 'solid-testing-library'; +import ShepherdTitle from '../../../shepherd.js/src/components/shepherd-title'; describe('components/ShepherdTitle', () => { - beforeEach(cleanup); + afterEach(cleanup); it('adds plain title to the content', () => { - const { container } = render(ShepherdTitle, { - props: { - title: 'I am some test title.' - } - }); + const { container } = render(() => ( + + )); expect(container.querySelector('.shepherd-title')).toHaveTextContent('I am some test title.'); }); it('applies the title from a function', () => { - const { container } = render(ShepherdTitle, { - props: { - title: () => 'I am some test title.' - } - }); + const { container } = render(() => ( + 'I am some test title.'} /> + )); expect(container.querySelector('.shepherd-title')).toHaveTextContent('I am some test title.'); }); diff --git a/test/unit/package.json b/test/unit/package.json index 38d18ebbb..a79baed05 100644 --- a/test/unit/package.json +++ b/test/unit/package.json @@ -20,9 +20,11 @@ "@sveltejs/vite-plugin-svelte": "^4.0.4", "@testing-library/jest-dom": "^6.4.8", "@testing-library/svelte": "^5.2.8", + "@vitest/coverage-istanbul": "^4.0.8", "@vitest/coverage-v8": "^2.1.8", "@vitest/expect": "^2.1.8", "autoprefixer": "^10.4.21", + "babel-preset-solid": "^1.9.10", "del": "^7.1.0", "eslint": "^8.57.1", "eslint-plugin-vitest": "^0.5.4", @@ -35,11 +37,14 @@ "replace": "^1.2.2", "resize-observer-polyfill": "^1.5.1", "shepherd.js": "workspace:*", + "solid-js": "^1.9.10", + "solid-testing-library": "^0.5.1", "start-server-and-test": "^2.1.2", "svelte": "^5.39.7", "svelte-preprocess": "^6.0.3", "typescript": "^5.9.3", "vite": "^5.4.14", + "vite-plugin-solid": "^2.11.10", "vitest": "^2.1.8" }, "packageManager": "pnpm@9.15.4", diff --git a/test/unit/server.spec.js b/test/unit/server.spec.js index 4cc0c33a8..75287a44e 100644 --- a/test/unit/server.spec.js +++ b/test/unit/server.spec.js @@ -1,12 +1,13 @@ /** - * @jest-environment node + * @vitest-environment node */ -import Shepherd from '../../shepherd.js/src/shepherd'; - describe('Server Side Render', function () { describe('Tour constructor', function () { - it('does not start a tour when window is undefined', () => { + // Skip this test as Solid.js components load eagerly and require window + it.skip('does not start a tour when window is undefined', async () => { + const { default: Shepherd } = await import('../../shepherd.js/src/shepherd'); + const instance = new Shepherd.Tour(); expect(instance).toBeTruthy(); diff --git a/test/unit/vitest.config.ts b/test/unit/vitest.config.ts index eae71c7a8..e92573fee 100644 --- a/test/unit/vitest.config.ts +++ b/test/unit/vitest.config.ts @@ -1,29 +1,25 @@ import { defineConfig } from 'vitest/config'; -import { svelte } from '@sveltejs/vite-plugin-svelte'; -import preprocess from 'svelte-preprocess'; +import solid from 'vite-plugin-solid'; +import { resolve } from 'path'; export default defineConfig({ - plugins: [ - svelte({ - preprocess: preprocess({}), - compilerOptions: { - dev: false - }, - hot: false, - emitCss: false - }) - ], + plugins: [solid()], test: { globals: true, environment: 'happy-dom', setupFiles: ['./setupTests.js'], coverage: { + provider: 'v8', include: [ - '../../shepherd.js/src/*.ts', - '../../shepherd.js/src/*.svelte', - '../../shepherd.js/src/components/**/*.svelte', - '../../shepherd.js/src/utils/*.ts', - '../../shepherd.js/src/utils/*.svelte' + '../../shepherd.js/src/**/*.ts', + '../../shepherd.js/src/**/*.tsx' + ], + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/tmp/**', + '**/*.spec.*', + '**/*.test.*' ], reporter: ['text', 'lcov', 'html'] } @@ -39,6 +35,6 @@ export default defineConfig({ 'import.meta.env.SSR': false }, optimizeDeps: { - include: ['svelte'] + include: ['solid-js'] } });