├── .github └── workflows │ ├── playwright.yml │ └── pushToTestAction.yml ├── .gitignore ├── .npmignore ├── README.md ├── demo └── scroll_3.gif ├── package-lock.json ├── package.json ├── playwright.config.ts ├── public └── index.html ├── rollup.config.dev.js ├── rollup.config.js ├── src └── index.tsx ├── test ├── TimelineAnimation │ ├── TimelineAnimation.jsx │ ├── index.js │ └── styles.css └── index.js ├── tests └── timeline.spec.ts └── tsconfig.json /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main ] 5 | jobs: 6 | test: 7 | timeout-minutes: 60 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 16 14 | - name: Install dependencies 15 | run: npm ci 16 | - name: Install Playwright Browsers 17 | run: npx playwright install --with-deps 18 | - name: Run Playwright tests 19 | run: npx playwright test 20 | - uses: actions/upload-artifact@v3 21 | if: always() 22 | with: 23 | name: playwright-report 24 | path: playwright-report/ 25 | retention-days: 30 26 | -------------------------------------------------------------------------------- /.github/workflows/pushToTestAction.yml: -------------------------------------------------------------------------------- 1 | name: push to test 2 | on: 3 | push: 4 | branches: [master, main] 5 | jobs: 6 | merge-master-to-test: 7 | timeout-minutes: 5 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set Git config 12 | run: | 13 | git config --local user.email "kashuba-aleksandr@mail.ru" 14 | git config --local user.name "akashuba" 15 | - name: Merge master back to test 16 | run: | 17 | git fetch --unshallow 18 | git checkout test 19 | git pull 20 | git merge --no-ff main -m "Auto-merge master to test" 21 | git push -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Directory for instrumented libs generated by jscoverage/JSCover 10 | lib-cov 11 | 12 | # Coverage directory used by tools like istanbul 13 | coverage 14 | *.lcov 15 | 16 | # nyc test coverage 17 | .nyc_output 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (https://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directories 26 | node_modules/ 27 | jspm_packages/ 28 | 29 | # TypeScript v1 declaration files 30 | typings/ 31 | 32 | # TypeScript cache 33 | *.tsbuildinfo 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional eslint cache 39 | .eslintcache 40 | 41 | # Output of 'npm pack' 42 | *.tgz 43 | 44 | # Yarn Integrity file 45 | .yarn-integrity 46 | 47 | # generate output 48 | dist 49 | 50 | # vscode 51 | .vscode 52 | /test-results/ 53 | /playwright-report/ 54 | /playwright/.cache/ 55 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashuba/react-timeline-animation/f279375e51230909267c650ad3b375abbfa3dd1f/.npmignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React scroll animation component. 2 | ![test parameter](https://github.com/akashuba/react-timeline-animation/actions/workflows/playwright.yml/badge.svg) 3 | 4 | #### Could be used for timeline filling or any animations related to scrolling and crossing the middle of the screen. Just wrap the animated component with TimelineObserver. 5 | 6 |
7 | drawing 8 |
9 | 10 | ## Demo [codesandbox](https://codesandbox.io/s/brave-kepler-fdbzv?file=/src/App.js:0-1097) 🚀 11 | 12 | ## How to use it 13 | 14 | ### 1. Installation 15 | 16 | ```bash 17 | npm install --save react-timeline-animation 18 | ``` 19 | 20 | or 21 | 22 | ```bash 23 | yarn add react-timeline-animation 24 | ``` 25 | ### 2. Quick start 26 | Important to add a unique id to the observed element (id="timeline100500"). 27 | ``` javascript 28 |
; 29 | ``` 30 | 31 | Component using react "render prop" pattern. 32 | 33 | ```javascript 34 | ( 38 | 43 | )} 44 | />; 45 | ``` 46 | 47 | ```javascript 48 | const Timeline = ({ setObserver, callback }) => { 49 | const timeline = useRef(null); 50 | 51 | // It Will be fired when the element crossed the middle of the screen. 52 | const someCallback = () => { 53 | callback(); 54 | }; 55 | 56 | useEffect(() => { 57 | if (timeline.current) { 58 | setObserver(timeline.current, someCallback); 59 | } 60 | }, []); 61 | 62 | return
; 63 | }; 64 | ``` 65 | 66 | ## Options (props) 🛠 67 | 68 | #### `initialColor`: not required. Initial color of observable element. 69 | 70 | #### `fillColor`: not required. Color to fill element. 71 | 72 | #### `handleObserve`: required. "render prop" to handle observable element. 73 | #### `hasReverse`: not required. Allow to scroll in both directions. 74 | 75 | ```typescript 76 | interface TimelineObserverProps { 77 | handleObserve?: ( 78 | observer: (target: Element, callbackFn?: () => void) => void 79 | ) => JSX.Element; 80 | initialColor?: string; 81 | fillColor?: string; 82 | hasReverse?: boolean; 83 | } 84 | ``` -------------------------------------------------------------------------------- /demo/scroll_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akashuba/react-timeline-animation/f279375e51230909267c650ad3b375abbfa3dd1f/demo/scroll_3.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-timeline-animation", 3 | "version": "1.2.3", 4 | "description": "Scroll animation component", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/akashuba/react-timeline-animation" 9 | }, 10 | "scripts": { 11 | "build": "rollup -c rollup.config.js", 12 | "start": "rollup -c rollup.config.js -w", 13 | "build:dev": "MODE=build rollup -c rollup.config.dev.js", 14 | "start:dev": "MODE=start rollup -c rollup.config.dev.js -w", 15 | "test": "npx playwright test" 16 | }, 17 | "author": "akashuba", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "@babel/core": "^7.20.2", 21 | "@babel/plugin-external-helpers": "^7.0.0", 22 | "@babel/plugin-proposal-optional-chaining": "^7.18.9", 23 | "@babel/preset-env": "^7.0.0", 24 | "@babel/preset-react": "^7.18.6", 25 | "@babel/preset-typescript": "^7.18.6", 26 | "@babel/runtime-corejs2": "^7.0.0", 27 | "@playwright/test": "^1.28.1", 28 | "@rollup/plugin-babel": "^6.0.3", 29 | "@rollup/plugin-commonjs": "^23.0.2", 30 | "@rollup/plugin-html": "^1.0.1", 31 | "@rollup/plugin-node-resolve": "^15.0.1", 32 | "@rollup/plugin-replace": "^5.0.1", 33 | "@rollup/plugin-typescript": "^9.0.2", 34 | "@types/react": "^17.0.19", 35 | "@types/react-dom": "^17.0.9", 36 | "babel-plugin-external-helpers": "^6.22.0", 37 | "postcss": "^8.4.19", 38 | "react": "^16.8.0", 39 | "react-dom": "^16.8.0", 40 | "rollup": "^2.56.3", 41 | "rollup-plugin-commonjs": "^9.1.3", 42 | "rollup-plugin-json": "^3.0.0", 43 | "rollup-plugin-livereload": "^2.0.5", 44 | "rollup-plugin-postcss": "^4.0.2", 45 | "rollup-plugin-progress": "^1.1.2", 46 | "rollup-plugin-sass": "^1.2.7", 47 | "rollup-plugin-serve": "^2.0.1", 48 | "rollup-plugin-typescript2": "^0.30.0", 49 | "typescript": "^4.4.2" 50 | }, 51 | "peerDependencies": { 52 | "react": ">= 16.8.0", 53 | "react-dom": ">= 16.8.0" 54 | }, 55 | "files": [ 56 | "dist" 57 | ], 58 | "keywords": [ 59 | "react", 60 | "typescript", 61 | "animation", 62 | "timeline" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { devices } = require('@playwright/test'); 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | 11 | /** 12 | * @see https://playwright.dev/docs/test-configuration 13 | * @type {import('@playwright/test').PlaywrightTestConfig} 14 | */ 15 | const config = { 16 | testDir: './tests', 17 | /* Maximum time one test can run for. */ 18 | timeout: 30 * 1000, 19 | expect: { 20 | /** 21 | * Maximum time expect() should wait for the condition to be met. 22 | * For example in `await expect(locator).toHaveText();` 23 | */ 24 | timeout: 5000 25 | }, 26 | /* Run tests in files in parallel */ 27 | fullyParallel: true, 28 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 29 | forbidOnly: !!process.env.CI, 30 | /* Retry on CI only */ 31 | retries: process.env.CI ? 2 : 0, 32 | /* Opt out of parallel tests on CI. */ 33 | workers: process.env.CI ? 1 : undefined, 34 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 35 | reporter: 'html', 36 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 37 | webServer: { 38 | command: 'npm run start:dev', 39 | // url: 'http://localhost:3000/', 40 | port: 3000, 41 | timeout: 50000, 42 | reuseExistingServer: true, 43 | }, 44 | use: { 45 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 46 | actionTimeout: 0, 47 | /* Base URL to use in actions like `await page.goto('/')`. */ 48 | // baseURL: 'http://localhost:3000', 49 | 50 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 51 | trace: 'on-first-retry', 52 | }, 53 | 54 | /* Configure projects for major browsers */ 55 | projects: [ 56 | { 57 | name: 'chromium', 58 | use: { 59 | ...devices['Desktop Chrome'], 60 | }, 61 | }, 62 | 63 | // { 64 | // name: 'firefox', 65 | // use: { 66 | // ...devices['Desktop Firefox'], 67 | // }, 68 | // }, 69 | 70 | // { 71 | // name: 'webkit', 72 | // use: { 73 | // ...devices['Desktop Safari'], 74 | // }, 75 | // }, 76 | 77 | /* Test against mobile viewports. */ 78 | // { 79 | // name: 'Mobile Chrome', 80 | // use: { 81 | // ...devices['Pixel 5'], 82 | // }, 83 | // }, 84 | // { 85 | // name: 'Mobile Safari', 86 | // use: { 87 | // ...devices['iPhone 12'], 88 | // }, 89 | // }, 90 | 91 | /* Test against branded browsers. */ 92 | // { 93 | // name: 'Microsoft Edge', 94 | // use: { 95 | // channel: 'msedge', 96 | // }, 97 | // }, 98 | // { 99 | // name: 'Google Chrome', 100 | // use: { 101 | // channel: 'chrome', 102 | // }, 103 | // }, 104 | ], 105 | 106 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 107 | // outputDir: 'test-results/', 108 | }; 109 | 110 | module.exports = config; 111 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React - Rollup Test 7 | 8 | 9 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | // import babel from 'rollup-plugin-babel'; 2 | import { babel } from '@rollup/plugin-babel'; 3 | // import filesize from 'rollup-plugin-filesize'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | import progress from 'rollup-plugin-progress'; 6 | // import visualizer from 'rollup-plugin-visualizer'; 7 | import commonjs from '@rollup/plugin-commonjs'; 8 | import json from 'rollup-plugin-json'; 9 | import serve from "rollup-plugin-serve"; 10 | import livereload from "rollup-plugin-livereload"; 11 | import typescript from '@rollup/plugin-typescript'; 12 | import replace from '@rollup/plugin-replace'; 13 | import postcss from 'rollup-plugin-postcss' 14 | 15 | const isStartMode = process.env.MODE === 'start' 16 | 17 | export default { 18 | input: 'test/index.js', 19 | output: [ 20 | { 21 | file: 'dist/index.js', 22 | format: 'iife', 23 | sourcemap: 'inline', 24 | }, 25 | ], 26 | plugins: [ 27 | typescript(), 28 | progress(), 29 | nodeResolve({ 30 | browser: true, 31 | extensions: ['.js', '.jsx', '.tsx'] 32 | }), 33 | json(), 34 | commonjs({ 35 | include: [ 36 | 'node_modules/**', 37 | ], 38 | exclude: [ 39 | 'node_modules/process-es6/**', 40 | ], 41 | namedExports: { 42 | 'node_modules/react/index.js': ['Children', 'Component', 'PropTypes', 'createElement'], 43 | 'node_modules/react-dom/index.js': ['render'], 44 | }, 45 | }), 46 | babel( 47 | { 48 | presets: [['@babel/preset-env', { modules: false }], '@babel/preset-react', '@babel/preset-typescript'], 49 | // plugins: ['external-helpers'], 50 | }), 51 | // visualizer(), 52 | // filesize(), 53 | (isStartMode ? serve({ 54 | verbose: true, 55 | contentBase: ["", "public"], 56 | host: "localhost", 57 | port: 3000, 58 | }) : null), 59 | 60 | replace({ 61 | 'process.env.NODE_ENV': JSON.stringify('production') 62 | }), 63 | (isStartMode ? livereload({ watch: "dist" }) : null), 64 | postcss() 65 | ], 66 | }; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import sass from 'rollup-plugin-sass' 2 | import typescript from 'rollup-plugin-typescript2' 3 | 4 | import pkg from './package.json' 5 | 6 | export default { 7 | input: 'src/index.tsx', 8 | output: [ 9 | { 10 | file: pkg.main, 11 | format: 'cjs', 12 | exports: 'named', 13 | sourcemap: true, 14 | strict: false 15 | } 16 | ], 17 | plugins: [ 18 | sass({ insert: true }), 19 | typescript({ objectHashIgnoreUnknownHack: true }) 20 | ], 21 | external: ['react', 'react-dom'] 22 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | 3 | interface TimelineObserverProps { 4 | handleObserve?: ( 5 | observer: (target: Element, callbackFn?: () => void) => void 6 | ) => JSX.Element; 7 | 8 | initialColor?: string; 9 | fillColor?: string; 10 | hasReverse?: boolean; 11 | } 12 | 13 | let halfScreenHeight; 14 | if (typeof window !== "undefined") { 15 | halfScreenHeight = window?.innerHeight / 2; 16 | } 17 | 18 | const options = { 19 | root: null, 20 | rootMargin: "0px", 21 | threshold: 0.2, 22 | }; 23 | 24 | interface ObservablesProps { 25 | observable: IntersectionObserverEntry; 26 | isPassed: boolean; 27 | callbackFn?: () => void; 28 | } 29 | 30 | function setObservable({ obs, observableList, callbacks }) { 31 | const obsId = obs?.target?.id; 32 | 33 | if (!observableList.has(obsId)) { 34 | observableList.set(obsId, { 35 | observable: obs, 36 | isPassed: false, 37 | callbackFn: callbacks[obsId] || null, 38 | }); 39 | } 40 | } 41 | 42 | function removeObservable({ obs, observableList }) { 43 | const obsName = obs?.target?.id; 44 | 45 | if (observableList.has(obsName)) { 46 | observableList.set(obsName, { 47 | ...observableList.get(obsName), 48 | isPassed: true, 49 | }); 50 | } 51 | } 52 | 53 | function colorize({ observableList, initialColor, fillColor, hasReverse }) { 54 | observableList.forEach((observable) => { 55 | if (!observable.isPassed) { 56 | const rect = observable.observable.target.getBoundingClientRect(); 57 | const entry = observable?.observable; 58 | 59 | if (rect.bottom > halfScreenHeight && rect.top < halfScreenHeight) { 60 | if (initialColor && fillColor) { 61 | const depthPx = rect.bottom - halfScreenHeight; 62 | const depthPercent = (depthPx * 100) / rect.height; 63 | entry.target.style.background = `linear-gradient(to top, ${initialColor} ${depthPercent}%, ${fillColor} ${depthPercent}% 100%)`; 64 | entry.target.style.transform = "translateZ(0)"; 65 | } 66 | } 67 | 68 | if (rect.bottom < halfScreenHeight) { 69 | if (initialColor && fillColor) { 70 | entry.target.style.background = fillColor; 71 | entry.target.style.transform = "unset"; 72 | } 73 | 74 | if (observable?.callbackFn) { 75 | if (!observable?.callbackFired) { 76 | observable?.callbackFn(); 77 | 78 | observable.callbackFired = true; 79 | } 80 | } 81 | 82 | if (!hasReverse) { 83 | removeObservable({ 84 | obs: entry, 85 | observableList, 86 | }); 87 | } 88 | } 89 | 90 | if (rect.top > halfScreenHeight && hasReverse) { 91 | entry.target.style.background = initialColor; 92 | } 93 | } 94 | }); 95 | } 96 | 97 | const TimelineObserver = ({ 98 | handleObserve, 99 | initialColor, 100 | fillColor, 101 | hasReverse, 102 | }: TimelineObserverProps) => { 103 | const observablesStore = useRef(new Map()); 104 | const callbacks = useRef<{ [key: string]: () => void }>({}); 105 | 106 | const callback = (entries) => { 107 | entries?.forEach((entry) => { 108 | if (entry.isIntersecting) { 109 | setObservable({ 110 | obs: entry, 111 | observableList: observablesStore.current, 112 | callbacks: callbacks.current, 113 | }); 114 | } 115 | }); 116 | }; 117 | const observer = useRef(new IntersectionObserver(callback, options)); 118 | 119 | const animation = () => { 120 | window.requestAnimationFrame(() => { 121 | colorize({ 122 | observableList: observablesStore.current, 123 | initialColor, 124 | fillColor, 125 | hasReverse, 126 | }); 127 | }); 128 | }; 129 | 130 | useEffect(() => { 131 | document.addEventListener("scroll", animation); 132 | return () => { 133 | document.removeEventListener("scroll", animation); 134 | }; 135 | }, []); 136 | 137 | const setObserver = (elem: HTMLElement, callbackFn?: () => void) => { 138 | const elemId = elem?.id; 139 | 140 | if (initialColor) { 141 | elem.style.background = initialColor; 142 | } 143 | 144 | observer.current.observe(elem); 145 | 146 | if (elemId && callbackFn) { 147 | callbacks.current[elemId] = callbackFn; 148 | } 149 | }; 150 | 151 | return
{handleObserve ? handleObserve(setObserver) : null}
; 152 | }; 153 | 154 | export default TimelineObserver; 155 | -------------------------------------------------------------------------------- /test/TimelineAnimation/TimelineAnimation.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | 3 | import TimelineObserver from "../../src"; 4 | 5 | import "./styles.css"; 6 | 7 | const Timeline = ({ setObserver, callback }) => { 8 | const [message1, setMessage1] = useState(""); 9 | const [message2, setMessage2] = useState(""); 10 | const [message3, setMessage3] = useState(""); 11 | 12 | const timeline1 = useRef(null); 13 | const timeline2 = useRef(null); 14 | const timeline3 = useRef(null); 15 | const circle1 = useRef(null); 16 | const circle2 = useRef(null); 17 | const circle3 = useRef(null); 18 | 19 | const someCallback = () => { 20 | setMessage1("Step one"); 21 | callback(); 22 | }; 23 | 24 | const someCallback2 = () => { 25 | setMessage2("Step two"); 26 | }; 27 | 28 | const someCallback3 = () => { 29 | setMessage3("Finish"); 30 | }; 31 | 32 | useEffect(() => { 33 | setObserver(timeline1.current); 34 | setObserver(timeline2.current); 35 | setObserver(timeline3.current); 36 | setObserver(circle1.current, someCallback); 37 | setObserver(circle2.current, someCallback2); 38 | setObserver(circle3.current, someCallback3); 39 | }, []); 40 | 41 | return ( 42 |
43 |
44 |
45 |
46 | 1 47 |
48 |
{message1}
49 |
50 |
51 |
52 |
53 | 2 54 |
55 |
{message2}
56 |
57 |
58 |
59 |
60 | 3 61 |
62 |
{message3}
63 |
64 |
65 | ); 66 | }; 67 | 68 | export const TimelineAnimation = () => { 69 | const [message, setMessage] = useState(""); 70 | 71 | const onCallback = () => { 72 | console.log("awesome"); 73 | }; 74 | 75 | 76 | return ( 77 |
78 |

react-scroll-animation component

79 |
⬇️ scroll to start ⬇️
80 | ( 85 | 90 | )} 91 | /> 92 |
{message}
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /test/TimelineAnimation/index.js: -------------------------------------------------------------------------------- 1 | export { TimelineAnimation } from './TimelineAnimation' -------------------------------------------------------------------------------- /test/TimelineAnimation/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | 6 | .wrapper { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | } 11 | 12 | .timeline { 13 | height: 300px; 14 | width: 5px; 15 | background-color: #e5e5e5; 16 | } 17 | 18 | .stub1 { 19 | line-height: 300px; 20 | font-size: 24px; 21 | background-color: #eae4e4; 22 | } 23 | 24 | .stub2 { 25 | height: 500px; 26 | } 27 | 28 | .circle { 29 | width: 30px; 30 | height: 30px; 31 | display: inline-flex; 32 | align-items: center; 33 | justify-content: center; 34 | color: white; 35 | border-radius: 50%; 36 | background-color: #e5e5e5; 37 | } 38 | 39 | .circleWrapper { 40 | position: relative; 41 | } 42 | 43 | .message { 44 | position: absolute; 45 | top: 20%; 46 | left: 50%; 47 | min-width: 150px; 48 | font-weight: bold; 49 | } 50 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { TimelineAnimation } from './TimelineAnimation/TimelineAnimation'; 4 | 5 | ReactDOM.render(, document.querySelector('#root')); -------------------------------------------------------------------------------- /tests/timeline.spec.ts: -------------------------------------------------------------------------------- 1 | const { test, expect } = require('@playwright/test'); 2 | 3 | test('Check timeline background color after scroll', async ({ page }) => { 4 | await page.goto('http://localhost:3000/'); 5 | 6 | const circle1Before = await page.locator('id=circle1') 7 | const circle1BeforeColor = await circle1Before.evaluate((e) => { 8 | return window.getComputedStyle(e).getPropertyValue("background-color") 9 | }) 10 | 11 | await expect(circle1Before).toHaveCount(1) 12 | expect(circle1BeforeColor).toBe('rgb(229, 229, 229)') 13 | 14 | // Timeline 15 | const timeline1Before = await page.locator('id=timeline1') 16 | const timeline1BeforeColor = await timeline1Before.evaluate((e) => { 17 | return window.getComputedStyle(e).getPropertyValue("background-color") 18 | }) 19 | 20 | await expect(timeline1Before).toHaveCount(1) 21 | expect(timeline1BeforeColor).toBe('rgb(229, 229, 229)') 22 | 23 | 24 | await expect(page.locator("text='Step one'")).toHaveCount(0) 25 | await expect(page.locator("text='Step two'")).toHaveCount(0) 26 | await expect(page.locator("text='Finish'")).toHaveCount(0) 27 | 28 | // Scroll 29 | await page.mouse.wheel(0, -100) 30 | await page.waitForTimeout(100); 31 | await page.mouse.wheel(0, 1100) 32 | await page.waitForTimeout(100); 33 | 34 | const circle1 = await page.locator('id=circle3') 35 | const circle1Color = await circle1.evaluate((e) => { 36 | return window.getComputedStyle(e).getPropertyValue("background-color") 37 | }) 38 | const circle2 = await page.locator('id=circle3') 39 | const circle2Color = await circle2.evaluate((e) => { 40 | return window.getComputedStyle(e).getPropertyValue("background-color") 41 | }) 42 | const circle3 = await page.locator('id=circle3') 43 | const circle3Color = await circle3.evaluate((e) => { 44 | return window.getComputedStyle(e).getPropertyValue("background-color") 45 | }) 46 | 47 | await expect(circle1).toHaveCount(1) 48 | await expect(circle1Color).toBe('rgb(0, 0, 0)') 49 | 50 | await expect(circle2).toHaveCount(1) 51 | await expect(circle2Color).toBe('rgb(0, 0, 0)') 52 | 53 | await expect(circle3).toHaveCount(1) 54 | await expect(circle3Color).toBe('rgb(0, 0, 0)') 55 | 56 | 57 | // Timeline after scroll 58 | 59 | const timeline1 = await page.locator('id=timeline1') 60 | const timeline1Color = await circle1.evaluate((e) => { 61 | return window.getComputedStyle(e).getPropertyValue("background-color") 62 | }) 63 | const timeline2 = await page.locator('id=timeline2') 64 | const timeline2Color = await timeline2.evaluate((e) => { 65 | return window.getComputedStyle(e).getPropertyValue("background-color") 66 | }) 67 | const timeline3 = await page.locator('id=timeline3') 68 | const timeline3Color = await timeline3.evaluate((e) => { 69 | return window.getComputedStyle(e).getPropertyValue("background-color") 70 | }) 71 | 72 | await expect(timeline1).toHaveCount(1) 73 | expect(timeline1Color).toBe('rgb(0, 0, 0)') 74 | 75 | await expect(timeline2).toHaveCount(1) 76 | expect(timeline2Color).toBe('rgb(0, 0, 0)') 77 | 78 | await expect(timeline3).toHaveCount(1) 79 | expect(timeline3Color).toBe('rgb(0, 0, 0)') 80 | 81 | 82 | await expect(page.locator("text='Step one'")).toHaveCount(1) 83 | await expect(page.locator("text='Step two'")).toHaveCount(1) 84 | await expect(page.locator("text='Finish'")).toHaveCount(1) 85 | 86 | // Expects the URL to contain intro. 87 | // await expect(page).toHaveURL(/.*reactjs/); 88 | }); 89 | 90 | // test('Check timeline callbacks', async ({ page }) => { 91 | // await page.goto('http://localhost:3000/') 92 | 93 | // await expect(page.locator("text='Step one'")).toHaveCount(0) 94 | // await expect(page.locator("text='Step two'")).toHaveCount(0) 95 | // await expect(page.locator("text='Finish'")).toHaveCount(0) 96 | 97 | // // Scroll 98 | // await page.mouse.wheel(0, -100) 99 | // await page.mouse.wheel(0, 1100) 100 | // await page.waitForTimeout(500); 101 | 102 | // await expect(page.locator("text='Step one'")).toHaveCount(1) 103 | // await expect(page.locator("text='Step two'")).toHaveCount(1) 104 | // await expect(page.locator("text='Finish'")).toHaveCount(1) 105 | // }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "noImplicitAny": false, 5 | "outDir": "dist", 6 | "module": "esnext", 7 | "target": "es5", 8 | "lib": ["es6", "dom", "es2016", "es2017"], 9 | "sourceMap": true, 10 | "allowJs": false, 11 | "jsx": "react", 12 | "declaration": true, 13 | "moduleResolution": "node", 14 | "forceConsistentCasingInFileNames": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true 21 | }, 22 | "include": ["src"], 23 | "exclude": ["node_modules", "dist", "example", "rollup.config.js"] 24 | } --------------------------------------------------------------------------------