├── .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 |
--------------------------------------------------------------------------------