├── .travis.yml
├── .gitignore
├── .npmignore
├── src
├── index.js
├── Container.js
└── Sticky.js
├── .babelrc
├── examples
├── index.html
├── header.js
├── navbar.js
├── index.js
├── relative
│ └── relative.js
├── stacked
│ └── stacked.js
├── basic
│ └── basic.js
└── styles.js
├── test
├── setup.js
└── spec
│ ├── Container.js
│ └── Sticky.js
├── webpack.config.js
├── LICENSE
├── .github
└── ISSUE_TEMPLATE.md
├── package.json
└── README.md
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | lib
4 | package-lock.json
5 |
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | examples
3 | dist
4 | .gitignore
5 | .npmignore
6 | .babelrc
7 | .travis.yml
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Sticky from "./Sticky";
2 | import Container from "./Container";
3 |
4 | export { Sticky };
5 | export { Container as StickyContainer };
6 | export default Sticky;
7 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-class-properties",
8 | "@babel/transform-runtime"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | (this.node = node)}
86 | onScroll={this.notifySubscribers}
87 | onTouchStart={this.notifySubscribers}
88 | onTouchMove={this.notifySubscribers}
89 | onTouchEnd={this.notifySubscribers}
90 | />
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/test/spec/Container.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { expect } from "chai";
3 | import { mount } from "enzyme";
4 | import { StickyContainer } from "../../src";
5 |
6 | const attachTo = document.getElementById("mount");
7 |
8 | describe("StickyContainer", () => {
9 | let container, containerNode;
10 | beforeEach(() => {
11 | container = mount(
, { attachTo });
12 | containerNode = container.node;
13 | });
14 |
15 | describe("getChildContext", () => {
16 | let childContext;
17 | beforeEach(() => {
18 | childContext = containerNode.getChildContext();
19 | });
20 |
21 | it("should expose a subscribe function that adds a callback to the subscriber list", () => {
22 | expect(childContext.subscribe).to.be.a("function");
23 |
24 | const callback = () => ({});
25 | expect(containerNode.subscribers).to.be.empty;
26 | childContext.subscribe(callback);
27 | expect(containerNode.subscribers[0]).to.equal(callback);
28 | });
29 |
30 | it("should expose an unsubscribe function that removes a callback from the subscriber list", () => {
31 | expect(childContext.unsubscribe).to.be.a("function");
32 |
33 | const callback = () => ({});
34 | childContext.subscribe(callback);
35 | expect(containerNode.subscribers[0]).to.equal(callback);
36 | childContext.unsubscribe(callback);
37 | expect(containerNode.subscribers).to.be.empty;
38 | });
39 |
40 | it("should expose a getParent function that returns the container's underlying DOM ref", () => {
41 | expect(childContext.getParent).to.be.a("function");
42 | expect(childContext.getParent()).to.equal(containerNode.node);
43 | });
44 | });
45 |
46 | describe("subscribers", () => {
47 | let subscribe;
48 | beforeEach(() => {
49 | subscribe = containerNode.getChildContext().subscribe;
50 | });
51 |
52 | // container events
53 | ["scroll", "touchstart", "touchmove", "touchend"].forEach(eventName => {
54 | it(`should be notified on container ${eventName} event`, done => {
55 | expect(containerNode.subscribers).to.be.empty;
56 | subscribe(() => done());
57 | container.simulate(eventName);
58 | });
59 | });
60 |
61 | // window events
62 | [
63 | "resize",
64 | "scroll",
65 | "touchstart",
66 | "touchmove",
67 | "touchend",
68 | "pageshow",
69 | "load"
70 | ].forEach(eventName => {
71 | it(`should be notified on window ${eventName} event`, done => {
72 | expect(containerNode.subscribers).to.be.empty;
73 | subscribe(() => done());
74 | window.dispatchEvent(new Event(eventName));
75 | });
76 | });
77 | });
78 |
79 | describe("notifySubscribers", () => {
80 | it("should publish document.body as eventSource to subscribers when window event", done => {
81 | containerNode.subscribers = [
82 | ({ eventSource }) => (
83 | expect(eventSource).to.equal(document.body), done()
84 | )
85 | ];
86 | containerNode.notifySubscribers({ currentTarget: window });
87 | });
88 |
89 | it("should publish node as eventSource to subscribers when div event", done => {
90 | containerNode.subscribers = [
91 | ({ eventSource }) => (
92 | expect(eventSource).to.equal(containerNode.node), done()
93 | )
94 | ];
95 | containerNode.notifySubscribers({ currentTarget: containerNode.node });
96 | });
97 |
98 | it("should publish node top and bottom to subscribers", done => {
99 | containerNode.subscribers = [
100 | ({ distanceFromTop, distanceFromBottom }) => {
101 | expect(distanceFromTop).to.equal(100);
102 | expect(distanceFromBottom).to.equal(200);
103 | done();
104 | }
105 | ];
106 |
107 | containerNode.node.getBoundingClientRect = () => ({
108 | top: 100,
109 | bottom: 200
110 | });
111 | containerNode.notifySubscribers({ currentTarget: window });
112 | });
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/src/Sticky.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import ReactDOM from "react-dom";
3 | import PropTypes from "prop-types";
4 |
5 | export default class Sticky extends Component {
6 | static propTypes = {
7 | topOffset: PropTypes.number,
8 | bottomOffset: PropTypes.number,
9 | relative: PropTypes.bool,
10 | children: PropTypes.func.isRequired
11 | };
12 |
13 | static defaultProps = {
14 | relative: false,
15 | topOffset: 0,
16 | bottomOffset: 0,
17 | disableCompensation: false,
18 | disableHardwareAcceleration: false
19 | };
20 |
21 | static contextTypes = {
22 | subscribe: PropTypes.func,
23 | unsubscribe: PropTypes.func,
24 | getParent: PropTypes.func
25 | };
26 |
27 | state = {
28 | isSticky: false,
29 | wasSticky: false,
30 | style: {}
31 | };
32 |
33 | componentWillMount() {
34 | if (!this.context.subscribe)
35 | throw new TypeError(
36 | "Expected Sticky to be mounted within StickyContainer"
37 | );
38 |
39 | this.context.subscribe(this.handleContainerEvent);
40 | }
41 |
42 | componentWillUnmount() {
43 | this.context.unsubscribe(this.handleContainerEvent);
44 | }
45 |
46 | componentDidUpdate() {
47 | this.placeholder.style.paddingBottom = this.props.disableCompensation
48 | ? 0
49 | : `${this.state.isSticky ? this.state.calculatedHeight : 0}px`;
50 | }
51 |
52 | handleContainerEvent = ({
53 | distanceFromTop,
54 | distanceFromBottom,
55 | eventSource
56 | }) => {
57 | const parent = this.context.getParent();
58 |
59 | let preventingStickyStateChanges = false;
60 | if (this.props.relative) {
61 | preventingStickyStateChanges = eventSource !== parent;
62 | distanceFromTop =
63 | -(eventSource.scrollTop + eventSource.offsetTop) +
64 | this.placeholder.offsetTop;
65 | }
66 |
67 | const placeholderClientRect = this.placeholder.getBoundingClientRect();
68 | const contentClientRect = this.content.getBoundingClientRect();
69 | const calculatedHeight = contentClientRect.height;
70 |
71 | const bottomDifference =
72 | distanceFromBottom - this.props.bottomOffset - calculatedHeight;
73 |
74 | const wasSticky = !!this.state.isSticky;
75 | const isSticky = preventingStickyStateChanges
76 | ? wasSticky
77 | : distanceFromTop <= -this.props.topOffset &&
78 | distanceFromBottom > -this.props.bottomOffset;
79 |
80 | distanceFromBottom =
81 | (this.props.relative
82 | ? parent.scrollHeight - parent.scrollTop
83 | : distanceFromBottom) - calculatedHeight;
84 |
85 | const style = !isSticky
86 | ? {}
87 | : {
88 | position: "fixed",
89 | top:
90 | bottomDifference > 0
91 | ? this.props.relative
92 | ? parent.offsetTop - parent.offsetParent.scrollTop
93 | : 0
94 | : bottomDifference,
95 | left: placeholderClientRect.left,
96 | width: placeholderClientRect.width
97 | };
98 |
99 | if (!this.props.disableHardwareAcceleration) {
100 | style.transform = "translateZ(0)";
101 | }
102 |
103 | this.setState({
104 | isSticky,
105 | wasSticky,
106 | distanceFromTop,
107 | distanceFromBottom,
108 | calculatedHeight,
109 | style
110 | });
111 | };
112 |
113 | render() {
114 | const element = React.cloneElement(
115 | this.props.children({
116 | isSticky: this.state.isSticky,
117 | wasSticky: this.state.wasSticky,
118 | distanceFromTop: this.state.distanceFromTop,
119 | distanceFromBottom: this.state.distanceFromBottom,
120 | calculatedHeight: this.state.calculatedHeight,
121 | style: this.state.style
122 | }),
123 | {
124 | ref: content => {
125 | this.content = ReactDOM.findDOMNode(content);
126 | }
127 | }
128 | );
129 |
130 | return (
131 |
132 |
(this.placeholder = placeholder)} />
133 | {element}
134 |
135 | );
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-sticky [](https://travis-ci.org/captivationsoftware/react-sticky)
2 |
3 | Make your React components sticky!
4 |
5 | #### Update No longer actively maintained:
6 |
7 | The 6.0.3 release is the last release maintained. This means we will not be considering any PR's and/or responding to any issues until a new maintainer is identified. It is *highly* recommended that you begin transitioning to another sticky library to ensure better support and sustainability. This is obviously less than ideal - sorry for any inconvenience!
8 |
9 | #### Demos
10 |
11 | * [Basic](http://react-sticky.netlify.com/#/basic)
12 | * [Relative](http://react-sticky.netlify.com/#/relative)
13 | * [Stacked](http://react-sticky.netlify.com/#/stacked)
14 |
15 | #### Version 6.x Highlights
16 |
17 | * Completely redesigned to support sticky behavior via higher-order component, giving you ultimate control of implementation details
18 | * Features a minimal yet efficient API
19 | * Drops support for versions of React < 15.3. If you are using an earlier version of React, continue to use the 5.x series
20 |
21 | #### CSS
22 | There's a CSS alternative to `react-sticky`: the `position: sticky` feature. However it currently does not have [full browser support](https://caniuse.com/#feat=css-sticky), specifically a lack of IE11 support and some bugs with table elements. Before using `react-sticky`, check to see if the browser support and restrictions prevent you from using `position: sticky`, as CSS will always be faster and more durable than a JS implementation.
23 | ```css
24 | position: -webkit-sticky;
25 | position: sticky;
26 | top: 0;
27 | ```
28 |
29 | ## Installation
30 |
31 | ```sh
32 | npm install react-sticky
33 | ```
34 |
35 | ## Overview & Basic Example
36 |
37 | The goal of `react-sticky` is make it easier for developers to build UIs that have sticky elements. Some examples include a sticky navbar, or a two-column layout where the left side sticks while the right side scrolls.
38 |
39 | `react-sticky` works by calculating the position of a `
` component relative to a `` component. If it would be outside the viewport, the styles required to affix it to the top of the screen are passed as an argument to a render callback, a function passed as a child.
40 |
41 | ```js
42 |
43 | {({ style }) => Sticky element
}
44 |
45 | ```
46 |
47 | The majority of use cases will only need the style to pass to the DOM, but some other properties are passed for advanced use cases:
48 |
49 | * `style` _(object)_ - modifiable style attributes to optionally be passed to the element returned by this function. For many uses, this will be the only attribute needed.
50 | * `isSticky` _(boolean)_ - is the element sticky as a result of the current event?
51 | * `wasSticky` _(boolean)_ - was the element sticky prior to the current event?
52 | * `distanceFromTop` _(number)_ - number of pixels from the top of the `Sticky` to the nearest `StickyContainer`'s top
53 | * `distanceFromBottom` _(number)_ - number of pixels from the bottom of the `Sticky` to the nearest `StickyContainer`'s bottom
54 | * `calculatedHeight` _(number)_ - height of the element returned by this function
55 |
56 | The `Sticky`'s child function will be called when events occur in the parent `StickyContainer`,
57 | and will serve as the callback to apply your own logic and customizations, with sane `style` attributes
58 | to get you up and running quickly.
59 |
60 | ### Full Example
61 |
62 | Here's an example of all of those pieces together:
63 |
64 | app.js
65 |
66 | ```js
67 | import React from 'react';
68 | import { StickyContainer, Sticky } from 'react-sticky';
69 | // ...
70 |
71 | class App extends React.Component {
72 | render() {
73 | return (
74 |
75 | {/* Other elements can be in between `StickyContainer` and `Sticky`,
76 | but certain styles can break the positioning logic used. */}
77 |
78 | {({
79 | style,
80 |
81 | // the following are also available but unused in this example
82 | isSticky,
83 | wasSticky,
84 | distanceFromTop,
85 | distanceFromBottom,
86 | calculatedHeight
87 | }) => (
88 |
91 | )}
92 |
93 | {/* ... */}
94 |
95 | );
96 | },
97 | };
98 | ```
99 |
100 | When the "stickiness" becomes activated, the arguments to the sticky function
101 | are modified. Similarly, when deactivated, the arguments will update accordingly.
102 |
103 | ### `` Props
104 |
105 | `` supports all valid `` props.
106 |
107 | ### `` Props
108 |
109 | #### relative _(default: false)_
110 |
111 | Set `relative` to `true` if the `` element will be rendered within
112 | an overflowing `` (e.g. `style={{ overflowY: 'auto' }}`) and you want
113 | the `` behavior to react to events only within that container.
114 |
115 | When in `relative` mode, `window` events will not trigger sticky state changes. Only scrolling
116 | within the nearest `StickyContainer` can trigger sticky state changes.
117 |
118 | #### topOffset _(default: 0)_
119 |
120 | Sticky state will be triggered when the top of the element is `topOffset` pixels from the top of the closest ``. Positive numbers give the impression of a lazy sticky state, whereas negative numbers are more eager in their attachment.
121 |
122 | app.js
123 |
124 | ```js
125 |
126 | ...
127 |
128 | { props => (...) }
129 |
130 | ...
131 |
132 | ```
133 |
134 | The above would result in an element that becomes sticky once its top is greater than or equal to 80px away from the top of the ``.
135 |
136 | #### bottomOffset _(default: 0)_
137 |
138 | Sticky state will be triggered when the bottom of the element is `bottomOffset` pixels from the bottom of the closest ``.
139 |
140 | app.js
141 |
142 | ```js
143 |
144 | ...
145 |
146 | { props => (...) }
147 |
148 | ...
149 |
150 | ```
151 |
152 | The above would result in an element that ceases to be sticky once its bottom is 80px away from the bottom of the ``.
153 |
154 | #### disableCompensation _(default: false)_
155 |
156 | Set `disableCompensation` to `true` if you do not want your `` to apply padding to
157 | a hidden placeholder `` to correct "jumpiness" as attachment changes from `position:fixed`
158 | and back.
159 |
160 | app.js
161 |
162 | ```js
163 |
164 | ...
165 |
166 | { props => (...) }
167 |
168 | ...
169 |
170 | ```
171 |
172 | #### disableHardwareAcceleration _(default: false)_
173 |
174 | When `disableHardwareAcceleration` is set to `true`, the `` element will not use hardware acceleration (e.g. `transform: translateZ(0)`). This setting is not recommended as it negatively impacts
175 | the mobile experience, and can usually be avoided by improving the structure of your DOM.
176 |
177 | app.js
178 |
179 | ```js
180 |
181 | ...
182 |
183 | { props => (...) }
184 |
185 | ...
186 |
187 | ```
188 |
189 | ## FAQ
190 |
191 | ### I get errors while using React.Fragments
192 | React.Fragments does not correspond to an actual DOM node, so `react-sticky` can not calculate its position. Because of this, React.Fragments is not supported.
193 |
--------------------------------------------------------------------------------
/test/spec/Sticky.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { expect } from "chai";
3 | import { mount } from "enzyme";
4 | import { StickyContainer, Sticky } from "../../src";
5 |
6 | const attachTo = document.getElementById("mount");
7 |
8 | describe("Invalid Sticky", () => {
9 | it("should complain if Sticky child is not a function", () => {
10 | expect(() =>
11 | mount(
12 |
13 |
14 | ,
15 | { attachTo }
16 | )
17 | ).to.throw(TypeError);
18 | });
19 |
20 | it("should complain if StickyContainer is not found", () => {
21 | expect(() =>
22 | mount({() => }, { attachTo })
23 | ).to.throw(TypeError);
24 | });
25 | });
26 |
27 | describe("Valid Sticky", () => {
28 | const componentFactory = props => (
29 |
30 |
31 |
32 | );
33 |
34 | describe("lifecycle", () => {
35 | let container;
36 | beforeEach(() => {
37 | container = mount(componentFactory({ children: () => }), {
38 | attachTo
39 | });
40 | });
41 |
42 | it("should register as subscriber of parent on mount", () => {
43 | expect(container.node.subscribers).to.contain(
44 | container.children().node.handleContainerEvent
45 | );
46 | });
47 |
48 | it("should unregister as subscriber of parent on unmount", () => {
49 | expect(container.node.subscribers).to.contain(
50 | container.children().node.handleContainerEvent
51 | );
52 | mount(, { attachTo });
53 | expect(container.node.subscribers).to.be.empty;
54 | });
55 | });
56 |
57 | describe("with no props", () => {
58 | const expectedStickyStyle = {
59 | left: 10,
60 | top: 0,
61 | width: 100,
62 | position: "fixed",
63 | transform: "translateZ(0)"
64 | };
65 |
66 | let sticky;
67 | beforeEach(() => {
68 | const wrapper = mount(
69 | componentFactory({
70 | children: () =>
71 | }),
72 | { attachTo }
73 | );
74 |
75 | const {
76 | position,
77 | transform,
78 | ...boundingClientRect
79 | } = expectedStickyStyle;
80 |
81 | sticky = wrapper.children().node;
82 | sticky.content.getBoundingClientRect = () => ({
83 | ...boundingClientRect,
84 | height: 100
85 | });
86 | sticky.placeholder.getBoundingClientRect = () => ({
87 | ...boundingClientRect,
88 | height: 100
89 | });
90 | });
91 |
92 | it("should change have an expected start state", () => {
93 | expect(sticky.state).to.eql({
94 | isSticky: false,
95 | wasSticky: false,
96 | style: {}
97 | });
98 | });
99 |
100 | it("should be sticky when distanceFromTop is 0", () => {
101 | sticky.handleContainerEvent({
102 | distanceFromTop: 0,
103 | distanceFromBottom: 1000,
104 | eventSource: document.body
105 | });
106 | expect(sticky.state).to.eql({
107 | isSticky: true,
108 | wasSticky: false,
109 | style: expectedStickyStyle,
110 | distanceFromTop: 0,
111 | distanceFromBottom: 900,
112 | calculatedHeight: 100
113 | });
114 | expect(parseInt(sticky.placeholder.style.paddingBottom)).to.equal(100);
115 | });
116 |
117 | it("should be sticky when distanceFromTop is negative", () => {
118 | sticky.handleContainerEvent({
119 | distanceFromTop: -1,
120 | distanceFromBottom: 999,
121 | eventSource: document.body
122 | });
123 | expect(sticky.state).to.eql({
124 | isSticky: true,
125 | wasSticky: false,
126 | style: expectedStickyStyle,
127 | distanceFromTop: -1,
128 | distanceFromBottom: 899,
129 | calculatedHeight: 100
130 | });
131 | expect(parseInt(sticky.placeholder.style.paddingBottom)).to.equal(100);
132 | });
133 |
134 | it("should continue to be sticky when distanceFromTop becomes increasingly negative", () => {
135 | sticky.handleContainerEvent({
136 | distanceFromTop: -1,
137 | distanceFromBottom: 999,
138 | eventSource: document.body
139 | });
140 | sticky.handleContainerEvent({
141 | distanceFromTop: -2,
142 | distanceFromBottom: 998,
143 | eventSource: document.body
144 | });
145 | expect(sticky.state).to.eql({
146 | isSticky: true,
147 | wasSticky: true,
148 | style: expectedStickyStyle,
149 | distanceFromTop: -2,
150 | distanceFromBottom: 898,
151 | calculatedHeight: 100
152 | });
153 | expect(parseInt(sticky.placeholder.style.paddingBottom)).to.equal(100);
154 | });
155 |
156 | it("should cease to be sticky when distanceFromTop becomes greater than 0", () => {
157 | sticky.handleContainerEvent({
158 | distanceFromTop: -1,
159 | distanceFromBottom: 999,
160 | eventSource: document.body
161 | });
162 | sticky.handleContainerEvent({
163 | distanceFromTop: 1,
164 | distanceFromBottom: 1001,
165 | eventSource: document.body
166 | });
167 | expect(sticky.state).to.eql({
168 | isSticky: false,
169 | wasSticky: true,
170 | style: { transform: "translateZ(0)" },
171 | distanceFromTop: 1,
172 | distanceFromBottom: 901,
173 | calculatedHeight: 100
174 | });
175 | expect(parseInt(sticky.placeholder.style.paddingBottom)).to.equal(0);
176 | });
177 |
178 | it("should compensate sticky style height when distanceFromBottom is < 0", () => {
179 | sticky.handleContainerEvent({
180 | distanceFromTop: -901,
181 | distanceFromBottom: 99,
182 | eventSource: document.body
183 | });
184 | expect(sticky.state).to.eql({
185 | isSticky: true,
186 | wasSticky: false,
187 | style: { ...expectedStickyStyle, top: -1 },
188 | distanceFromTop: -901,
189 | distanceFromBottom: -1,
190 | calculatedHeight: 100
191 | });
192 | expect(parseInt(sticky.placeholder.style.paddingBottom)).to.equal(100);
193 | });
194 | });
195 |
196 | describe("with topOffset not equal to 0", () => {
197 | it("should attach lazily when topOffset is positive", () => {
198 | const wrapper = mount(
199 | componentFactory({
200 | topOffset: 1,
201 | children: () =>
202 | }),
203 | { attachTo }
204 | );
205 |
206 | const sticky = wrapper.children().node;
207 | sticky.handleContainerEvent({
208 | distanceFromTop: 0,
209 | distanceFromBottom: 100,
210 | eventSource: document.body
211 | });
212 | expect(sticky.state.isSticky).to.be.false;
213 | sticky.handleContainerEvent({
214 | distanceFromTop: -1,
215 | distanceFromBottom: 99,
216 | eventSource: document.body
217 | });
218 | expect(sticky.state.isSticky).to.be.true;
219 | });
220 |
221 | it("should attach aggressively when topOffset is negative", () => {
222 | const wrapper = mount(
223 | componentFactory({
224 | topOffset: -1,
225 | children: () =>
226 | }),
227 | { attachTo }
228 | );
229 |
230 | const sticky = wrapper.children().node;
231 | sticky.handleContainerEvent({
232 | distanceFromTop: 2,
233 | distanceFromBottom: 99,
234 | eventSource: document.body
235 | });
236 | expect(sticky.state.isSticky).to.be.false;
237 | sticky.handleContainerEvent({
238 | distanceFromTop: 1,
239 | distanceFromBottom: 98,
240 | eventSource: document.body
241 | });
242 | expect(sticky.state.isSticky).to.be.true;
243 | });
244 | });
245 |
246 | describe("when relative = true", () => {
247 | let eventSource, sticky;
248 | beforeEach(() => {
249 | const wrapper = mount(
250 | componentFactory({
251 | relative: true,
252 | children: () =>
253 | }),
254 | { attachTo }
255 | );
256 |
257 | eventSource = wrapper.node.node;
258 | eventSource.scrollHeight = 1000;
259 | eventSource.offsetTop = 0;
260 | eventSource.offsetParent = { scrollTop: 0 };
261 |
262 | sticky = wrapper.children().node;
263 | });
264 |
265 | it("should not change sticky state when event source is not StickyContainer", () => {
266 | sticky.placeholder.offsetTop = 0;
267 | eventSource.scrollTop = 0;
268 |
269 | sticky.handleContainerEvent({
270 | distanceFromTop: 100,
271 | distanceFromBottom: 500,
272 | eventSource
273 | });
274 | expect(sticky.state.isSticky).to.be.true;
275 |
276 | sticky.handleContainerEvent({
277 | distanceFromTop: 100,
278 | distanceFromBottom: 500,
279 | eventSource: document.body
280 | });
281 | expect(sticky.state.isSticky).to.be.true;
282 | });
283 |
284 | it("should change sticky state when event source is StickyContainer", () => {
285 | sticky.placeholder.offsetTop = 1;
286 | eventSource.scrollTop = 0;
287 |
288 | sticky.handleContainerEvent({
289 | distanceFromTop: 100,
290 | distanceFromBottom: 500,
291 | eventSource
292 | });
293 | expect(sticky.state.isSticky).to.be.false;
294 |
295 | eventSource.scrollTop = 1;
296 | sticky.handleContainerEvent({
297 | distanceFromTop: 100,
298 | distanceFromBottom: 500,
299 | eventSource
300 | });
301 | expect(sticky.state.isSticky).to.be.true;
302 |
303 | eventSource.scrollTop = 2;
304 | sticky.handleContainerEvent({
305 | distanceFromTop: 100,
306 | distanceFromBottom: 500,
307 | eventSource
308 | });
309 | expect(sticky.state.isSticky).to.be.true;
310 | });
311 |
312 | it("should adjust sticky style.top when StickyContainer has a negative distanceFromTop", () => {
313 | sticky.placeholder.offsetTop = 0;
314 | eventSource.scrollTop = 0;
315 |
316 | sticky.handleContainerEvent({
317 | distanceFromTop: 0,
318 | distanceFromBottom: 1000,
319 | eventSource
320 | });
321 | expect(sticky.state.isSticky).to.be.true;
322 | expect(sticky.state.style.top).to.equal(0);
323 |
324 | eventSource.offsetParent.scrollTop = 1;
325 | sticky.handleContainerEvent({
326 | distanceFromTop: -1,
327 | distanceFromBottom: 999,
328 | eventSource: document.body
329 | });
330 | expect(sticky.state.isSticky).to.be.true;
331 | expect(sticky.state.style.top).to.equal(-1);
332 |
333 | eventSource.scrollTop = 1;
334 | sticky.handleContainerEvent({
335 | distanceFromTop: -1,
336 | distanceFromBottom: 1000,
337 | eventSource
338 | });
339 | expect(sticky.state.isSticky).to.be.true;
340 | expect(sticky.state.style.top).to.equal(-1);
341 | });
342 | });
343 |
344 | describe("with disableHardwareAcceleration = true", () => {
345 | it("should not include translateZ style when sticky", () => {
346 | const wrapper = mount(
347 | componentFactory({
348 | disableHardwareAcceleration: true,
349 | children: () =>
350 | }),
351 | { attachTo }
352 | );
353 |
354 | const sticky = wrapper.children().node;
355 | sticky.handleContainerEvent({
356 | distanceFromTop: 1,
357 | distanceFromBottom: 100,
358 | eventSource: document.body
359 | });
360 | expect(sticky.state.isSticky).to.be.false;
361 | expect(sticky.state.style.transform).to.be.undefined;
362 |
363 | sticky.handleContainerEvent({
364 | distanceFromTop: -1,
365 | distanceFromBottom: 99,
366 | eventSource: document.body
367 | });
368 | expect(sticky.state.isSticky).to.be.true;
369 | expect(sticky.state.style.transform).to.be.undefined;
370 | });
371 | });
372 |
373 | describe("with disableCompensation = true", () => {
374 | it("should not include translateZ style when sticky", () => {
375 | const wrapper = mount(
376 | componentFactory({
377 | disableCompensation: true,
378 | children: () =>
379 | }),
380 | { attachTo }
381 | );
382 |
383 | const sticky = wrapper.children().node;
384 | sticky.handleContainerEvent({
385 | distanceFromTop: -1,
386 | distanceFromBottom: 99,
387 | eventSource: document.body
388 | });
389 | expect(sticky.state.isSticky).to.be.true;
390 | expect(parseInt(sticky.placeholder.style.paddingBottom)).to.equal(0);
391 | });
392 | });
393 | });
394 |
--------------------------------------------------------------------------------