ResizeObserver not being triggered when content height changes (React) - javascript

It works when I manually resize the window, but not when the content height changes which is what I need.
Am I doing something wrong?
class MainContainer extends React.Component {
constructor(props) {
super(props);
this.containerRef = React.createRef();
this.containerObserver = null;
this.state = {
top: false,
};
}
componentDidMount() {
this.containerObserver = new ResizeObserver((e) => this.handleResize(e));
this.containerObserver.observe(this.containerRef.current);
}
componentWillUnmount() {
if (this.containerObserver) {
this.containerObserver.disconnect();
}
}
handleResize = (e) => {
const { target } = e[0];
const top = target.scrollTop;
const scrollHeight = target.scrollHeight;
const position = scrollHeight - top;
const clientHeight = target.clientHeight;
console.log({ top }, { scrollHeight }, { position }, { clientHeight });
if (top < 10) {
if (this.state.top) {
this.setState({ top: false });
}
} else {
if (!this.state.top) {
this.setState({ top: true });
}
}
if (position >= clientHeight - 40 && position <= clientHeight) {
if (!this.state.top) {
this.setState({ top: true });
}
}
};
render() {
return (
<React.Fragment>
<Container ref={this.containerRef} onScroll={this.handleScroll}>
<Body />
</Container>
<ShadowTop show={this.state.top} />
</React.Fragment>
);
}
}
--
export const Container = styled.div`
#media (max-width: 760px) {
position: absolute;
}
margin-top: ${({ theme }) => theme.header.height.percent}%;
margin-top: -webkit-calc(${({ theme }) => theme.header.height.pixel}px);
margin-top: -moz-calc(${({ theme }) => theme.header.height.pixel}px);
margin-top: calc(${({ theme }) => theme.header.height.pixel}px);
height: ${({ theme }) => Math.abs(100 - theme.header.height.percent)}%;
height: -webkit-calc(100% - ${({ theme }) => theme.header.height.pixel}px);
height: -moz-calc(100% - ${({ theme }) => theme.header.height.pixel}px);
height: calc(100% - ${({ theme }) => theme.header.height.pixel}px);
position: fixed;
float: none;
clear: both;
top: 0;
right: 0;
width: ${({ theme }) => 100 - theme.sidebar.width.percent}%;
width: -webkit-calc(100% - ${({ theme }) => theme.sidebar.width.pixel}px);
width: -moz-calc(100% - ${({ theme }) => theme.sidebar.width.pixel}px);
width: calc(100% - ${({ theme }) => theme.sidebar.width.pixel}px);
z-index: 2;
pointer-events: auto;
overflow: auto;
`;

You are observing Container for size change, but it has fixed dimensions:
height: ${({ theme }) => Math.abs(100 - theme.header.height.percent)}%;
// ...
width: ${({ theme }) => 100 - theme.sidebar.width.percent}%;
so it never actually resizes, and resize observer never triggers.
Try wrapping its content inside an element that shrinks to the size of its content, and use the resize observer on this element instead:
this.contentObserver.observe(this.contentRef.current);
// ...
<Container onScroll={this.handleScroll}>
<ShrinkWrapper ref={this.contentRef}>
<Body />
</ShrinkWrapper>
</Container>
const ShrinkWrapper = styled.div`
width: fit-content;
height: fit-content;
`

Solved it by using a MutationObserver with subtree enabled.
this.containerObserver = new MutationObserver(this.handleResize);
this.containerObserver.observe(this.containerRef.current, {
childList: true,
subtree: true,
});

Related

How to make read progressBar smoother react

I have function to show reading article progress,my problem is it doesnt go smooth at all how i can remove this lagging effect.On useEffect hook it set procent of all page hight. After that i set width in styled component to that procent. It all working but i cant rid off this lagging and its very annoying.
export const SingleArticle = () => {
const [readProgress, setReadProgress] = useState(0)
useEffect(() => {
const calcReadProgress = () => {
const { scrollHeight, clientHeight } = document.documentElement
const height = scrollHeight - clientHeight
setReadProgress((document.documentElement.scrollTop / height) * 100)
}
window.addEventListener('scroll', calcReadProgress)
return () => window.removeEventListener('scroll', calcReadProgress)
}, [])
return (
<ReadProgressBar progress={readProgress} />
)
}
type StyledProps = {
progress: number
}
export const ReadProgressBar = styled.div<StyledProps>`
width: 100%;
height: 10px;
background-color: ${({ theme }) => theme.colors.darkBlue};
position: fixed;
top: 0;
left: 0;
z-index: 10;
&::after {
content: '';
width: ${({ progress }) => (progress ? `${progress}%` : '0%')};
height: 10px;
background-color: ${({ theme }) => theme.colors.purple};
position: absolute;
top: 0px;
left: 0px;
}
`

React resize even listener isn't triggered

On resize even listener is not being triggered.
class MainContainer extends React.Component {
constructor(props) {
super(props);
this.containerRef = React.createRef();
this.state = {};
}
componentDidMount() {
this.containerRef.current.addEventListener("resize", this.handleResize);
}
componentWillUnmount() {
this.containerRef.current.removeEventListener("resize", this.handleResize);
}
handleResize() {
console.log("handleResize");
}
render() {
return (
<React.Fragment>
<Container ref={this.containerRef}>
<Body />
</Container>
<ShadowTop show={this.state.top} />
</React.Fragment>
);
}
}
--
export const Container = styled.div`
#media (max-width: 760px) {
position: absolute;
}
margin-top: ${({ theme }) => theme.header.height.percent}%;
margin-top: -webkit-calc(${({ theme }) => theme.header.height.pixel}px);
margin-top: -moz-calc(${({ theme }) => theme.header.height.pixel}px);
margin-top: calc(${({ theme }) => theme.header.height.pixel}px);
height: ${({ theme }) => Math.abs(100 - theme.header.height.percent)}%;
height: -webkit-calc(100% - ${({ theme }) => theme.header.height.pixel}px);
height: -moz-calc(100% - ${({ theme }) => theme.header.height.pixel}px);
height: calc(100% - ${({ theme }) => theme.header.height.pixel}px);
position: fixed;
float: none;
clear: both;
top: 0;
right: 0;
width: ${({ theme }) => 100 - theme.sidebar.width.percent}%;
width: -webkit-calc(100% - ${({ theme }) => theme.sidebar.width.pixel}px);
width: -moz-calc(100% - ${({ theme }) => theme.sidebar.width.pixel}px);
width: calc(100% - ${({ theme }) => theme.sidebar.width.pixel}px);
z-index: 2;
pointer-events: auto;
overflow: auto;
`;
What am I doing wrong here?
Am trying to detect when the div aka Container a styled-components element has changed size.
The resize event is triggered on window, not on individual elements. This is because the resize event is meant to handle viewport resize, not content resize. To detect content size changes you can use ResizeObserver.
There are a lot of ways you can incorporate this into your React project. Here is a example similar to what you have in the question:
class MainContainer extends React.Component {
constructor(props) {
super(props);
this.ulRef = React.createRef();
this.state = { item: "", todoList: [] };
// Binding methods to the current intance is only needed if you pass
// the method as an argument to another function and want acces to the
// `this` keyword in the method.
this.handleResize = this.handleResize.bind(this);
this.addTodoItem = this.addTodoItem.bind(this);
}
componentDidMount() {
this.ulObserver = new ResizeObserver(this.handleResize);
this.ulObserver.observe(this.ulRef.current);
}
componentWillUnmount() {
this.ulObserver.disconnect();
}
handleResize(entries) {
console.log("handleResize");
console.log(entries);
}
addTodoItem(event) {
event.preventDefault();
this.setState((state) => ({
todoList: [...state.todoList, state.item],
item: "",
}));
}
render() {
return (
<div>
<form onSubmit={this.addTodoItem}>
<input
value={this.state.item}
onChange={e => this.setState({ item: e.target.value })}
/>
<button type="submit">add</button>
(or press <kbd>Enter</kbd>)
</form>
<ul ref={this.ulRef}>
{this.state.todoList.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
}
ReactDOM.render(<MainContainer />, document.querySelector("#root"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
There might also be libraries out there that help you combine ResizeObserver and React. But it doesn't hurt to understand what is happening under the hood.
resize events are only fired on the window object
You can read more about resize event
It should be:
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
You can add debounce to handleResize to make it less often.

How to prevent tooltip going out of screen reactjs

Here a small demo. There are a few block; hovering on each block appears a tooltip(orange rect). It doesn't work correctly.
Tooltip should be displayed from left or right side. To get sizes of tooltip need to display it. Coords to display tooltip can be calculated only after tooltip is displayed
Codesandbox https://codesandbox.io/s/react-ref-65jj6?file=/src/index.js:88-231
const { useState, useEffect, useCallback } = React;
function App() {
return (
<div>
<HoveredBlock index={1} />
<HoveredBlock index={2} blockStyle={{ marginLeft: "5%" }} />
<HoveredBlock index={3} blockStyle={{ marginLeft: "50%" }} />
</div>
);
}
function calcCoords(blockRect, hoverRect) {
const docWidth = document.documentElement.clientWidth;
const isLeft = blockRect.right + hoverRect.width > docWidth;
const coords = {};
if (!isLeft) {
coords.x = blockRect.right;
coords.y = blockRect.top;
coords.type = "right";
} else {
coords.x = blockRect.left - 5 - hoverRect.width;
coords.y = blockRect.top;
coords.type = "left";
}
return coords;
}
function HoveredBlock({ index, blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback(
(node) => {
if (showHover && node) {
setHoverRect(node.getBoundingClientRect());
}
},
[showHover]
);
useEffect(() => {
if (showHover && hoverRect) {
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [hoverRect]);
const isHidden = !showHover || !coords ? 'hidden' : '';
return (
<div>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
>
{index}
</div>
<div
ref={hoverRef}
className={'hover-block' + isHidden}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 82%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
display: none;
}
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
I solved it. I changed the way how to hide element: visibility:hidded instead of display:none
export default function HoveredBlock({ blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback((node) => {
if (node) {
setHoverRect(node.getBoundingClientRect());
}
}, []);
useEffect(() => {
if (showHover) {
console.log({ blockRect, hoverRect });
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [showHover, blockRect, hoverRect]);
return (
<>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
/>
<div
ref={hoverRef}
className={cx("hover-block", {
hidden: !showHover || !coords
})}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
></div>
</>
);
}
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 20%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
visibility: hidden;
}

How to use transition on className change in React?

on click button, I add or remove className 'a' to div. It becomes 50px width but without transition
const Navbar = ({ size }) => {
const MobileNavigation = styled.nav`
div {
width: 100px;
height: 100px;
background-color: blue;
transition: width 1s;
&.a {
width: 50px;
}
}
`;
const [active, setActive] = useState(0);
if (size.width < 500 || (size.width > size.height && size.width < 800)) {
return (
<MobileNavigation>
<button
onClick={() => {
if (active) {
setActive(false);
} else {
setActive(true);
}
}}
>
button
</button>
<div className={active ? 'a' : ''}></div>
</MobileNavigation>
}
);
export default withSize()(Navbar);
How do I add class to this element with transition? Thanks!
your styled component needs to be moved outside of NavBar. Every time NavBar rerenders you are creating a brand new MobileNavigation component and that new component has no idea that it was supposed to transition from a previous width
const MobileNavigation = styled.nav`
div {
width: 100px;
height: 100px;
background-color: blue;
transition: width 1s;
&.a {
width: 50px;
}
}
`;
const Navbar = ({ size }) => {
const [active, setActive] = useState(false);
if (size.width < 500 || (size.width > size.height && size.width < 800)) {
return (
<MobileNavigation>
<button
onClick={() => {
if (active) {
setActive(prevState => !prevState);
} else {
setActive(prevState => !prevState);
}
}}
>
button
</button>
<div className={active ? "a" : ""} />
</MobileNavigation>
);
}
return null;
};
export default withSize()(Navbar);

show average rating in read mode and just total or half value in write mode

I am trying to create star rating where the functionality has to be following:
In read mode, the stars are shown as per average (should support 100%
i.e 5 or 96% i.e 4.6) in write mode, the user can only rate 1, 1.5, 2,
2.5 etc not 2.6
The read mode is working as expected but is having problem with write mode.
The problem in write mode is I cannot update the rating with non-decimal value from 1 to 5 and also half value like 1.5, 2.5, 3.5 etc. On hovering how do i decide if my mouse pointer is in the full star or half of star? Can anyone look at this, please?
I have created a sandbox for showing the demo
Here it is
https://codesandbox.io/s/9l6kmnw7vw
The code is as follow
UPDATED CODE
// #flow
import React from "react";
import styled, { css } from "styled-components";
const StyledIcon = styled.i`
display: inline-block;
width: 12px;
overflow: hidden;
direction: ${props => props.direction && props.direction};
${props => props.css && css(...props.css)};
`;
const StyledRating = styled.div`
unicode-bidi: bidi-override;
font-size: 25px;
height: 25px;
width: 125px;
margin: 0 auto;
position: relative;
padding: 0;
text-shadow: 0px 1px 0 #a2a2a2;
color: grey;
`;
const TopStyledRating = styled.div`
padding: 0;
position: absolute;
z-index: 1;
display: block;
top: 0;
left: 0;
overflow: hidden;
${props => props.css && css(...props.css)};
width: ${props => props.width && props.width};
`;
const BottomStyledRating = styled.div`
padding: 0;
display: block;
z-index: 0;
`;
class Rating extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rating: this.props.rating || null,
// eslint-disable-next-line
temp_rating: null
};
}
handleMouseover(rating) {
console.log("rating", rating);
this.setState(prev => ({
rating,
// eslint-disable-next-line
temp_rating: prev.rating
}));
}
handleMouseout() {
this.setState(prev => ({
rating: prev.temp_rating
}));
}
rate(rating) {
this.setState({
rating,
// eslint-disable-next-line
temp_rating: rating
});
}
calculateWidth = value => {
const { total } = this.props;
const { rating } = this.state;
return Math.floor((rating / total) * 100).toFixed(2) + "%";
};
render() {
const { disabled, isReadonly } = this.props;
const { rating } = this.state;
const topStars = [];
const bottomStars = [];
const writableStars = [];
console.log("rating", rating);
// eslint-disable-next-line
if (isReadonly) {
for (let i = 0; i < 5; i++) {
topStars.push(<span>★</span>);
}
for (let i = 0; i < 5; i++) {
bottomStars.push(<span>★</span>);
}
} else {
// eslint-disable-next-line
for (let i = 0; i < 10; i++) {
let klass = "star_border";
if (rating >= i && rating !== null) {
klass = "star";
}
writableStars.push(
<StyledIcon
direction={i % 2 === 0 ? "ltr" : "rtl"}
className="material-icons"
css={this.props.css}
onMouseOver={() => !disabled && this.handleMouseover(i)}
onFocus={() => !disabled && this.handleMouseover(i)}
onClick={() => !disabled && this.rate(i)}
onMouseOut={() => !disabled && this.handleMouseout()}
onBlur={() => !disabled && this.handleMouseout()}
>
{klass}
</StyledIcon>
);
}
}
return (
<React.Fragment>
{isReadonly ? (
<StyledRating>
<TopStyledRating
css={this.props.css}
width={this.calculateWidth(this.props.rating)}
>
{topStars}
</TopStyledRating>
<BottomStyledRating>{bottomStars}</BottomStyledRating>
</StyledRating>
) : (
<React.Fragment>
{rating}
{writableStars}
</React.Fragment>
)}
</React.Fragment>
);
}
}
Rating.defaultProps = {
css: "",
disabled: false
};
export default Rating;
Now the writable stars is separately done to show the stars status when hovering and clicking but when I am supplying rating as 5 it is filling the third stars instead of 5th.
I think your current problem seems to be with where your mouse event is set, as you are handling it on the individual stars, they disappear, and trigger a mouseout event, causing this constant switch in visibility.
I would rather set the detection of the rating on the outer div, and then track where the mouse is in relation to the div, and set the width of the writable stars according to that.
I tried to make a sample from scratch, that shows how you could handle the changes from the outer div. I am sure the formula I used can be simplified still, but okay, this was just to demonstrate how it can work.
const { Component } = React;
const getRating = x => (parseInt(x / 20) * 20 + (x % 20 >= 13 ? 20 : x % 20 >= 7 ? 10 : 0));
class Rating extends Component {
constructor() {
super();
this.state = {
appliedRating: '86%'
};
this.setParentElement = this.setParentElement.bind( this );
this.handleMouseOver = this.handleMouseOver.bind( this );
this.applyRating = this.applyRating.bind( this );
this.reset = this.reset.bind( this );
this.stopReset = this.stopReset.bind( this );
}
stopReset() {
clearTimeout( this.resetTimeout );
}
setParentElement(e) {
this.parentElement = e;
}
handleMouseOver(e) {
this.stopReset();
if (e.currentTarget !== this.parentElement) {
return;
}
const targetRating = getRating(e.clientX - this.parentElement.offsetLeft);
if (this.state.setRating !== targetRating) {
this.setState({
setRating: targetRating
});
}
}
applyRating(e) {
this.setState({
currentRating: this.state.setRating
});
}
reset(e) {
this.resetTimeout = setTimeout(() => this.setState( { setRating: null } ), 50 );
}
renderStars( width, ...classes ) {
return (
<div
onMouseEnter={this.stopReset}
className={ ['flex-rating', ...classes].join(' ')}
style={{width}}>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
</div>
);
}
renderFixed() {
return this.renderStars('100%', 'fixed');
}
renderReadOnlyRating() {
const { appliedRating } = this.state;
return this.renderStars( appliedRating, 'readonly' );
}
renderWriteRating() {
let { setRating, currentRating } = this.state;
if (setRating === 0) {
setRating = '0%';
}
if (currentRating === undefined) {
currentRating = '100%';
}
return this.renderStars( setRating || currentRating, 'writable' );
}
render() {
return (
<div>
<div
ref={ this.setParentElement }
className="rating"
onMouseMove={ this.handleMouseOver }
onMouseOut={ this.reset }
onClick={ this.applyRating }>
{ this.renderFixed() }
{ this.renderReadOnlyRating() }
{ this.renderWriteRating() }
</div>
<div>Current rating: { ( ( this.state.currentRating || 0 ) / 20) }</div>
</div>
);
}
}
ReactDOM.render( <Rating />, document.getElementById('container') );
body { margin: 50px; }
.rating {
font-family: 'Courier new';
font-size: 16px;
position: relative;
display: inline-block;
width: 100px;
height: 25px;
align-items: flex-start;
justify-content: center;
align-content: center;
background-color: white;
}
.flex-rating {
position: absolute;
top: 0;
left: 0;
display: flex;
height: 100%;
overflow: hidden;
cursor: pointer;
}
.fixed {
color: black;
font-size: 1.1em;
font-weight: bold;
}
.readonly {
color: silver;
font-weight: bold;
}
.writable {
color: blue;
background-color: rgba(100, 100, 100, .5);
}
.star {
text-align: center;
width: 20px;
max-width: 20px;
min-width: 20px;
}
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>

Categories