├── .npmignore ├── .gitignore ├── .babelrc ├── demo ├── index.html └── demo.jsx ├── src ├── typings.d.ts ├── index.js └── reactScrollJacker.tsx ├── tsconfig.json ├── webpack.config.js ├── package.json └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /src/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib/ 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | title 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'stickybits'; 2 | 3 | declare interface ScrollJackerProps { 4 | children?: any; 5 | scrollSensitivity?: number; 6 | injectChildren? : {}; 7 | stickyOffset? : number; 8 | style?: any; 9 | className?: string; 10 | } 11 | 12 | declare interface ScrollJackerState { 13 | childrenCount: number; 14 | currentPage: number; 15 | currentProgress: number; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "target": "es5", 5 | "moduleResolution": "node", 6 | "noImplicitReturns": true, 7 | "noImplicitThis": true, 8 | "noImplicitAny": true, 9 | "suppressImplicitAnyIndexErrors": true, 10 | "noUnusedLocals": true, 11 | "declaration": true, 12 | "lib": [ 13 | "es2016", 14 | "dom", 15 | "es6" 16 | ], 17 | "jsx": "react", 18 | "typeRoots": [ 19 | "node_modules/@types" 20 | ], 21 | "experimentalDecorators": true, 22 | "emitDecoratorMetadata": true 23 | }, 24 | "exclude": [ 25 | "node_modules", 26 | "lib" 27 | ] 28 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | module.exports = { 3 | entry: './src/index.js', 4 | output: { 5 | path: path.resolve(__dirname, 'lib'), 6 | filename: 'reactScrollJacker.js', 7 | libraryTarget: 'commonjs2' // THIS IS THE MOST IMPORTANT LINE! :mindblow: I wasted more than 2 days until realize this was the line most important in all this guide. 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | include: path.resolve(__dirname, 'src'), 14 | exclude: /(node_modules|bower_components|build)/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['stage-2','react'] 19 | } 20 | } 21 | } 22 | ] 23 | }, 24 | externals: { 25 | 'react': 'commonjs react' // this line is just to use the React dependency of our parent-testing-project instead of using our own React. 26 | } 27 | }; -------------------------------------------------------------------------------- /demo/demo.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import ReactScrollJacker from 'react-scroll-jacker'; 3 | 4 | const ScrollerJackerTest = props => { 5 | return
6 |
7 |

Normal Scrollin'

8 |
9 | 10 | 11 | Help! 12 | Our scroll has been hijacked! 13 | Won't somebody please think of the UX ? 14 | UX?? Where we are going, we don't need UX. 15 | 16 | {/* you can add any number of ReactElements in here !! */} 17 | 18 |
19 |

Oh Thank god

20 |
21 |
22 | }; 23 | 24 | const ReactElement = (props ) => { 25 | return
26 |
27 |

28 | {props.children ? props.children : null} 29 |

30 |
31 | 32 |
33 | } 34 | 35 | export { ScrollerJackerTest }; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scroll-jacker", 3 | "version": "1.8.4", 4 | "description": "ruthless scroll jacking component that takes no ransom. The component makes scrolling certain distance have the effect of changing a page (instead of.. scrolling the page). Under the hood, it takes React Children and shows them one by one, depending on the scrolled distance. ", 5 | "main": "./lib/reactScrollJacker.js", 6 | "types": "./lib/reactScrollJacker.d.ts", 7 | "scripts": { 8 | "test": "test", 9 | "build": "babel src -d lib" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "javascript", 14 | "typescript", 15 | "scroll-jacking" 16 | ], 17 | "author": "James Kim", 18 | "license": "MIT", 19 | "peerDependencies": { 20 | "react": "16.x" 21 | }, 22 | "dependencies": { 23 | "stickybits": "^2.1.1" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "^16.0.34", 27 | "awesome-typescript-loader": "^3.5.0", 28 | "babel-cli": "^6.26.0", 29 | "babel-loader": "^7.1.3", 30 | "babel-preset-env": "^1.6.1", 31 | "babel-preset-es2016": "^6.24.1", 32 | "babel-preset-react": "^6.24.1", 33 | "babel-preset-stage-2": "^6.24.1", 34 | "jest": "^22.1.1", 35 | "source-map-loader": "^0.2.3", 36 | "typescript": "^2.7.2", 37 | "webpack": "^4.0.1", 38 | "webpack-cli": "^2.0.9" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/horizon0708/react-scroll-jacker.git" 43 | }, 44 | "homepage": "https://github.com/horizon0708/react-scroll-jacker" 45 | } 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | This is meant to be a tongue-in-cheek react component. I am not maintaining this currently and I do not recommend usage except for fun :p 2 | 3 | # React Scroll Jacker 4 | React Scroll Jacker is a component that makes your next scroll-hijacking easy! Instead of using scroll to scroll the page like a sane person, this component lets you use scroll to transition between React elements. Users` hijacked scrolls will return after they spin their mousewheels as many times as you have arbitrarily dictated. 5 | 6 | Now sit back and enjoy sweet tears of UX designers and users. 7 | 8 | [Demo](https://scroller-jacker-demo.herokuapp.com/) 9 | 10 | ## Installation 11 | 12 | ``` 13 | $ npm install --save react-scroll-jacker 14 | ``` 15 | 16 | ## Usage 17 | 18 | 1. Add Elements to `````` as its children. 19 | 2. (optional) set the scroll sensitivity 20 | 3. Sit back and enjoy scroll-jacking. 21 | 22 | ```javascript 23 | import ReactScrollJacker from "react-scroll-jacker"; 24 | 25 | const ScrollerJackerTest = props => { 26 | return 27 | Help! 28 | Our scroll has been hijacked! 29 | Wont somebody please think of the UX ? 30 | UX?? Where we are going, we dont need UX. 31 | 32 | {/* you can add any number of ReactElements in here !! */} 33 | 34 | }; 35 | ``` 36 | Elements show in the order they are added in. 37 | 38 | ## Props 39 | 40 | ### scrollSensitivity _(number[1-9]: optional)_ 41 | Decides how sensitive the scrolls are. Lower the number, more you have to scroll to transition to the next element. The default value is 7. 42 | 43 | ### stickyOffset *(number: optional)* 44 | this sets the offset of the stickied element from the top of the viewbox. 45 | 46 | ### injectChildren *(optional)* 47 | This will inject the _React.Children_ with props ```currentPage``` which returns the index of the currently rendered child and ```progress``` which returns a float between 0 and 1 representing the current progress to the next transition, 1 being 100% (Enabling this calls ```setState``` every time you scroll, adding the icing that is bad performance to the frustration cake) 48 | 49 | ```javascript 50 | 51 | Help! 52 | I did not ask for this! 53 | 54 | 55 | // Both ReactElementOnes will have get a prop currentPage that has the index of the current rendered child. 56 | ``` 57 | 58 | This component is a regular div. _style_ and _className_ props are exposed. Style it however you wish. 59 | 60 | ## Under the hood 61 | This component uses [stickybits](https://github.com/dollarshaveclub/stickybits) to make child elements sticky. Stickybits uses css property sticky as a default and provides fallback via JS for browsers that are not supported. 62 | 63 | ## Todo 64 | - [ ] Add Tests & webpack. 65 | - [x] Add Demo. 66 | - [ ] Add an option to pass down inject and not hide the children automatically. 67 | - [ ] apologise to the UX designers and users. 68 | 69 | ## Patchnotes 70 | 71 | ### v0.1.5 72 | - Now able to pass the progress to the next transition as a prop ```progress``` it returns a float between 0 and 1, 1 being 100%. 73 | - This can be used to give some kind of visual feedback of progress to users and make scroll-jacking a bit more bearable (but at the cost of performance :)) 74 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import stickybits from "stickybits"; 3 | 4 | export default class ScrollJacker extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | childrenCount: 0, 9 | currentPage: 0, 10 | currentProgress: 0 11 | }; 12 | } 13 | // increment = 200; 14 | // height = 500; 15 | // container; 16 | 17 | // static defaultProps = { 18 | // scrollSensitivity: 7 19 | // } 20 | 21 | componentDidMount() { 22 | let { children, stickyOffset, scrollSensitivity } = this.props; 23 | scrollSensitivity = scrollSensitivity < 1 ? 1 : scrollSensitivity; 24 | scrollSensitivity = scrollSensitivity > 9 ? 9 : scrollSensitivity; 25 | if (children) { 26 | this.setState({ 27 | childrenCount: React.Children.count(children) 28 | }); 29 | this.increment = this.increment * (10-this.props.scrollSensitivity); 30 | this.height = this.increment * React.Children.count(children); 31 | } 32 | if (window) { 33 | window.addEventListener("scroll", this.updateCurrentPage); 34 | stickybits("#STC-sticky-child", { 35 | stickyBitStickyOffset: stickyOffset || 0 36 | }); 37 | } 38 | } 39 | 40 | componentWillUnmount() { 41 | if (window) { 42 | window.removeEventListener("scroll", this.updateCurrentPage); 43 | } 44 | } 45 | 46 | updateCurrentPage = () => { 47 | if (this.state.currentPage !== this.getCurrentPage()) { 48 | this.setState({ currentPage: this.getCurrentPage() }); 49 | } 50 | this.setState({currentProgress: this.getProgress()}) 51 | 52 | }; 53 | 54 | getCurrentPage() { 55 | const { childrenCount } = this.state; 56 | if (childrenCount < 2 || this.container.getBoundingClientRect().top > 0) { 57 | return 0; 58 | } 59 | 60 | const progress = Math.abs(this.container.getBoundingClientRect().top); 61 | const output = Math.floor(progress / this.increment); 62 | if (output > childrenCount - 1) { 63 | return childrenCount - 1; 64 | } 65 | return output; 66 | } 67 | 68 | getProgress() { 69 | const { childrenCount } = this.state; 70 | if (childrenCount < 2 || this.container.getBoundingClientRect().top > 0) { 71 | return 0; 72 | } 73 | 74 | const progress = Math.abs(this.container.getBoundingClientRect().top); 75 | if (progress / this.increment > childrenCount) { 76 | return 1; 77 | } 78 | const output = progress / this.increment - Math.floor(progress / this.increment); 79 | return output; 80 | } 81 | 82 | //https://stackoverflow.com/questions/42261783/how-to-assign-the-correct-typing-to-react-cloneelement-when-giving-properties-to 83 | injectedChildren() { 84 | const { children } = this.props; 85 | return React.Children.map(children, x => { 86 | if (React.isValidElement(x)) { 87 | return React.cloneElement(x, { 88 | currentPage: this.getCurrentPage(), 89 | progress: this.getProgress() 90 | }); 91 | } 92 | return x; 93 | }); 94 | } 95 | 96 | renderChild() { 97 | const { injectChildren, children } = this.props; 98 | const { currentPage } = this.state; 99 | return injectChildren 100 | ? this.injectedChildren()[currentPage] 101 | : children[this.getCurrentPage()]; 102 | } 103 | 104 | render() { 105 | return ( 106 |
{ 109 | this.container = container; 110 | }} 111 | style={{ ...this.props.style ,height: `${this.height + this.increment}px` }} 112 | > 113 |
114 | {this.renderChild()} 115 |
116 |
117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/reactScrollJacker.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as stickybits from "stickybits"; 3 | export default class ScrollJacker extends React.Component< 4 | ScrollJackerProps, 5 | ScrollJackerState 6 | > { 7 | constructor(props: ScrollJackerProps) { 8 | super(props); 9 | this.state = { 10 | childrenCount: 0, 11 | currentPage: 0, 12 | currentProgress: 0 13 | }; 14 | } 15 | private increment: number = 200; 16 | private height: number = 500; 17 | private container: HTMLElement; 18 | 19 | static defaultProps = { 20 | scrollSensitivity: 7 21 | } 22 | 23 | componentDidMount() { 24 | let { children, stickyOffset, scrollSensitivity } = this.props; 25 | scrollSensitivity = scrollSensitivity < 1 ? 1 : scrollSensitivity; 26 | scrollSensitivity = scrollSensitivity > 9 ? 9 : scrollSensitivity; 27 | if (children) { 28 | this.setState({ 29 | childrenCount: React.Children.count(children) 30 | }); 31 | this.increment = this.increment * (10-this.props.scrollSensitivity); 32 | this.height = this.increment * React.Children.count(children); 33 | } 34 | if (window) { 35 | window.addEventListener("scroll", this.updateCurrentPage); 36 | stickybits("#STC-sticky-child", { 37 | stickyBitStickyOffset: stickyOffset || 0 38 | }); 39 | } 40 | } 41 | 42 | componentWillUnmount() { 43 | if (window) { 44 | window.removeEventListener("scroll", this.updateCurrentPage); 45 | } 46 | } 47 | 48 | updateCurrentPage = () => { 49 | if (this.state.currentPage !== this.getCurrentPage()) { 50 | this.setState({ currentPage: this.getCurrentPage() }); 51 | } 52 | this.setState({currentProgress: this.getProgress()}) 53 | 54 | }; 55 | 56 | getCurrentPage(): number { 57 | const { childrenCount } = this.state; 58 | if (childrenCount < 2 || this.container.getBoundingClientRect().top > 0) { 59 | return 0; 60 | } 61 | 62 | const progress = Math.abs(this.container.getBoundingClientRect().top); 63 | const output = Math.floor(progress / this.increment); 64 | if (output > childrenCount - 1) { 65 | return childrenCount - 1; 66 | } 67 | return output; 68 | } 69 | 70 | getProgress(): number { 71 | const { childrenCount } = this.state; 72 | if (childrenCount < 2 || this.container.getBoundingClientRect().top > 0) { 73 | return 0; 74 | } 75 | 76 | const progress = Math.abs(this.container.getBoundingClientRect().top); 77 | if (progress / this.increment > childrenCount) { 78 | return 1; 79 | } 80 | const output = progress / this.increment - Math.floor(progress / this.increment); 81 | return output; 82 | } 83 | 84 | //https://stackoverflow.com/questions/42261783/how-to-assign-the-correct-typing-to-react-cloneelement-when-giving-properties-to 85 | injectedChildren(): React.ReactChild[] { 86 | const { children } = this.props; 87 | return React.Children.map(children, x => { 88 | if (React.isValidElement(x as React.ReactElement)) { 89 | return React.cloneElement(x as React.ReactElement, { 90 | currentPage: this.getCurrentPage(), 91 | progress: this.getProgress() 92 | }); 93 | } 94 | return x; 95 | }); 96 | } 97 | 98 | renderChild(): React.ReactChild { 99 | const { injectChildren, children } = this.props; 100 | const { currentPage } = this.state; 101 | return injectChildren 102 | ? this.injectedChildren()[currentPage] 103 | : children[this.getCurrentPage()]; 104 | } 105 | 106 | render() { 107 | return ( 108 |
{ 111 | this.container = container; 112 | }} 113 | style={{ ...this.props.style ,height: `${this.height + this.increment}px` }} 114 | > 115 |
116 | {this.renderChild()} 117 |
118 |
119 | ); 120 | } 121 | } 122 | --------------------------------------------------------------------------------