I'm trying to create a lazy loading article list. I've set an Intersecton Observer that checks when the viewport reaches the end of the observed element (the article container) and makes a new api call.
The thing is when I load the new articles in the observed element the height changes but the observation gets triggered only on the initial height.
Should I use a ResizeObserver on top of the IntersectonObserver?
const sections = document.querySelectorAll('.articles_container');
function observeArticlesContainer() {
const changeNav = (entries, observer) => {
entries.forEach((entry) => {
// verify the element is intersecting
if (entry.isIntersecting) {
console.log(entry.target)
// $(results_wrapper).hide()
// $(loading_animation).fadeIn(200);
fetchData();
}
});
}
// init the observer
const options = {
root: null,
rootMargin: '0px 0px -100% 0px',
threshold: 0
}
const observer = new IntersectionObserver(changeNav, options);
// target the elements to be observed
sections.forEach(section => {
observer.observe(section);
});
}
ok, I've just set the container below as the target.
const sections = document.querySelectorAll('footer');
also set rootMargin back to 0px
Related
Cypress's visible matcher treats an element as visible based on a variety of factors, however it doesn't take the viewport into account, so an element that is scrolled off-screen is still treated as visible.
I need to test that a link to an on-page anchor is functioning correctly. Once the link is clicked, the page scrolls to the element with the id as defined in the href of the link (example/#some-id).
How can verify that the element is within the viewport?
I've cobbled together the following commands which appear to work so far, but amazed there isn't on out-of-box solution:
Cypress.Commands.add('topIsWithinViewport', { prevSubject: true }, subject => {
const windowInnerWidth = Cypress.config(`viewportWidth`);
const bounding = subject[0].getBoundingClientRect();
const rightBoundOfWindow = windowInnerWidth;
expect(bounding.top).to.be.at.least(0);
expect(bounding.left).to.be.at.least(0);
expect(bounding.right).to.be.lessThan(rightBoundOfWindow);
return subject;
})
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, subject => {
const windowInnerWidth = Cypress.config(`viewportWidth`);
const windowInnerHeight = Cypress.config(`viewportHeight`);
const bounding = subject[0].getBoundingClientRect();
const rightBoundOfWindow = windowInnerWidth;
const bottomBoundOfWindow = windowInnerHeight;
expect(bounding.top).to.be.at.least(0);
expect(bounding.left).to.be.at.least(0);
expect(bounding.right).to.be.lessThan(rightBoundOfWindow);
expect(bounding.bottom).to.be.lessThan(bottomBoundOfWindow);
return subject;
})
I did a little refactoring on Undistracted's approach if anyone is interested:
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).to.be.within(0, window.innerHeight);
expect(rect.right).to.be.within(0, window.innerWidth);
expect(rect.bottom).to.be.within(0, window.innerHeight);
expect(rect.left).to.be.within(0, window.innerWidth);
return subject;
});
Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).not.to.be.within(0, window.innerHeight);
expect(rect.right).not.to.be.within(0, window.innerWidth);
expect(rect.bottom).not.to.be.within(0, window.innerHeight);
expect(rect.left).not.to.be.within(0, window.innerWidth);
return subject;
});
Uses window.innerWidth and window.innerHeight in case you have used cy.viewport before calling. Also uses .within to facilitate the outside addition.
I have an interesting bug I can't seem to work out and I hope someone with better React knowledge than me can help me work out.
Basically, I have a component (slider carousel, like a Netflix queue) that is trying to set the visibility of two elements (nav slider buttons for left and right nav) if there is overflow of the underlying dev and/or if the underlying div is at a certain position. My visibility setter method is called when onComponentDidMount, when the position of the underlying div changes, and with an window resize event listener.
It works like expected most of the time, however, I have an edge case where I can resize the window, even after going to a new route, and it will work as expected... BUT if I go a new route again I get an error when resizing the window at that point.
It appear as if the refs are not being set after switching routes the second time because they return null.
I've tried detecting if ref is null, but couldn't get that work properly.
setCaretVis() {
const el = this.tray.current;
console.log(el);
const parent = this.wrapper.current;
console.log(parent);
const posRight = this.offsetRight();
const posLeft = el.scrollLeft;
const left = this.caretLeft.current;
const right = this.caretRight.current;
const parWidth = el.parentElement.offsetWidth;
const width = el.scrollWidth;
if (parWidth >= width) {
if (!left.classList.contains("invis")) {
left.classList.add("invis");
} else if (left.classList.contains("invis")) {
}
if (!right.classList.contains("invis")) {
right.classList.add("invis");
}
} else if (parWidth < width) {
if (left.classList.contains("invis") && posLeft != 0) {
left.classList.remove("invis");
} else if (!left.classList.contains("invis") && posLeft === 0) {
left.classList.add("invis");
}
if (right.classList.contains("invis") && posRight != 0) {
right.classList.remove("invis");
} else if (!right.classList.contains("invis") && posRight === 0) {
right.classList.add("invis");
}
}
if (posLeft > 0) {
left.classList.remove("invis");
} else {
left.classList.add("invis");
}
if (posRight === 0) {
console.log("true");
right.classList.add("invis");
} else {
right.classList.remove("invis");
}
}
offsetRight() {
const el = this.tray.current;
//const element = this.refs.tray;
const parent = this.wrapper.current;
const parWidth = parent.offsetWidth;
const width = el.scrollWidth;
const left = el.scrollLeft;
let sub = width - parWidth;
let calc = Math.abs(left - sub);
return calc;
};
// The componentDidMount method
componentDidMount() {
this.setCaretVis();
window.addEventListener("resize", this.setCaretVis);
this.setCaretVis();
}
I would like to set the visibility (adding/removing a css class) on resize after route change without error.
current error reads: Uncaught TypeError: Cannot read property 'offsetWidth' of null
I suspect that your component is recreated when you go to a new route again, but the old listener is still invoked by the resize handler. Try to remove event listener in componentWillUnmount:
componentDidMount() {
this.setCaretVis();
window.addEventListener("resize", this.setCaretVis);
this.setCaretVis();
}
componentWillUnmount() {
window.removeEventListener("resize", this.setCaretVis);
}
When router recreates the component, it will subscribe to resize event again.
From the docs:
componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this method, such as invalidating timers, canceling network requests, or cleaning up any DOM elements that were created in componentDidMount
I'm upgrading a lazy load scroll mechanism with an intersection observer. My scroll mechanism looks like this:
function setScrollListener(callBack) {
document.addEventListener('scroll', throttle(callBack, 1000));
}
const state = {};
function checkInViewport() {
const offset = 400;
state.unrendered = state.unrendered.filter(el => {
const visible = isInViewport(el, offset)
if (visible) {
display(el);
}
return !visible;
});
}
setScrollListener(checkInViewport)
Without going too much into the weeds, isInViewport uses the offset provided and the getClientBoundRect of the element provided to determine if the element is near the viewport accounting for the offset. Essentially, is this element 400px from the viewport? The method display simply displays the element.
Now using an intersection observer I have this:
const observeInViewport = (el, io) => {
const { offset = 400 } = this.props;
if (isInViewport(el, offset)) {
display(el);
io.unobserve(el);
}
}
const observationOpts = {
root: null,
rootMargin: '0px',
threshold: 0.1,
};
const lazyRef = document.getElementsByClassName("lazy");
const io = new IntersectionObserver(([entry]) => {
observeInViewport(entry.target, io);
}, observationOpts);
[].slice.call(lazyRef).forEach(el => {
io.observe(el);
});
I'm using the same isInViewport and display methods using the offset, but the offset won't trigger until the observer does. The observer is of course trigger by its threshold. I can't provide a negative threshold which would be ideal. I tried using the documentElement as my root option and then providing a rootMargin of ${(el.offsetTop - offset)}px 0px 0px 0px. That is a string template, but the formatting here was messed up with the tick marks.
Can I achieve the same offset logic as I did with my scroll listener, but with an intersection observable? Let me know if you have any questions.
given IntersectionObserver like this:
const observeVisibility = intersectionMargin => {
const observer = new IntersectionObserver(
nodes => {
if (nodes[0].isIntersecting) {
/* is really in viewport? */
this.observer.disconnect();
}
},
{ rootMargin: intersectionMargin }
);
observer.observe(...);
};
How to check whether the node itself is actually in viewport or it's just the intersectionMargin that caused observer to be called?
The IntersectionObserver will fire immediately on load. After that, your callback passed to IntersectionObserver will be called when isIntersecting changes or when the intersectionRatio crosses one of your configured thresholds.
As you can see, the callback gets the list of entries and then it is up to you to do what you want.
//assuming 'threshold' is defined in scope
nodes => {
nodes.forEach(entry => {
const { isIntersecting, intersectionRatio } = entry;
if (Array.isArray(threshold)) {
threshold = threshold[threshold.length - 1];
}
if (isIntersecting || intersectionRatio >= threshold) {
this.observer.disconnect();
}
}
}
I am trying to determine whether a slot element has a scrollbar. If it does I want to add a show all/show fewer link next to the slot which will expand or contract it accordingly.
I am using IntersectionObserver because it seems designed for this very use case. The slot contains a ul which itself has a variable number of li children. The ul is not populated until after the function that adds the show more/show link is fired - hence the use of IntersectionObserver. The function I wrote should return a boolean:
_isOverflowing( rootElem, childElem ) {
// rootElem is the slot; childElem is the ul
let observer;
const options = {
root: rootElem,
threshold: 1.0
};
function handleIntersect(entries, observer) {
entries.forEach(entry => {
return entry.intersectionRatio >= options.threshold
});
}
observer = new IntersectionObserver(handleIntersect, options);
return observer.observe( childElem );
}
What I am seeing is that the final function
observer.observe( childElem )
returns as undefined. What am I doing wrong?