Inconsistent useRef().clientHeight - javascript

I have a parent div card which can display one of three child divs. I want to adjust the height of the parent div to fit the exact contents of the currently-visible child div.
To do this, I create a updateDonationCardSize function which reads the refs of all three views, and depending on which one should be currently visible, sets the parent div height to match the appropriate child div height.
function updateDonationCardSize() { // Sets the size of the card to fit contents
const height = stepOneContainerRef.current.clientHeight
setDonationCardSize(prevState => ({
...prevState,
['width']: paymentFooterRef.current.clientWidth,
['height']: height
}))
}
I call this function whenever any of the refs pointing to one of the subviews updates.
useEffect(() => {
updateDonationCardSize()
setTimeout(() => updateDonationCardSize(), 700)
}, [stepOneContainerRef, stepTwoContainerRef, stepThreeContainerRef])
Notice the silly approach above where the function is called twice, once immediately and once after a brief delay. For some reason, the first time it runs, it returns the wrong height of 830 whereas the second time it runs, it returns the correct height of 700.
What's even stranger is that if I print out height variable as well as the stepOneContainerRef object into the console next to each other, the height variable is returned as 830 while stepOneContainerRef object actually displays its current.clientWidth property as 700...
I am completely clueless and have spent hours over this already.
Edit - Providing additional information
The parent div contains the following structure:
<div id='parent div' className='overflow-hidden'>
<div id='parent of dynamic content'>
<div id='containerOne' className='relative' style={stepOneContainerStyle} />
<div id='containerTwo' className='relative' style={stepTwoContainerStyle} />
<div id='containerThree' className='relative' style={stepThreeContainerStyle} />
</div>
<div id='footer' />
</div>
and the stepXContainerStyle is a dictionary which sets position: top and x to 0 if it the particular div should be currently visible, otherwise it moves it outside of the overflow region to hide it.

The different heights are result of unfinished DOM mutations during the rendering of the components. To fix that change useEffect hook to useLayoutEffect which fires synchronously after all DOM mutations. It will allow you to call updateDonationCardSize only once.
In addition, you should wrap updateDonationCardSize with useCallback for correct hook dependency specification.
const updateDonationCardSize = useCallback(() => { // Sets the size of the card to fit contents
setDonationCardSize(prevState => ({
...prevState,
['width']: paymentFooterRef.current.clientWidth,
['height']: stepOneContainerRef.current.clientHeight
}))
}, [paymentFooterRef, stepOneContainerRef]);
useLayoutEffect(() => {
updateDonationCardSize()
}, [updateDonationCardSize])
I'd also recommand to try solve this problem with CSS rules - it looks like a CSS problem more than component rendering problem.

Related

Can't measure width and height in an img

I have a component that renders Posts into list items. One of the list items is
<li className="content media">
{content.isMedia === "image" && (
<img src={content.img} alt={content.title} ref={measuredRef}></img>
)}
</li>
I want to apply a specific class to the images that have a specific ratio (height / width > 3.5) of .streched (images that are way too tall for their width). The problem is I can't seem to get the measuredRef correctly, currently I'm just logging the values returned by the Ref with useCallback
const measuredRef = useCallback((node) => {
if (node !== null) {
console.log(node.clientHeight);
}
}, []);
but the clientHeights and clientWidths are always 0. This is so strange, because if I console.log(node) and inspect the object the clientHeight and clientWidth is the actual image sizes and correct.
It's like the ref is not returning the updated values, when I thought (according to the Hooks FAQ documentation) that the callback would run once the node is created. It seems to be called before the element is actually assigned a width and a size, but logging the node actually gives the correct height and width.
I tried getBoundingClientRect() but the object also has 0 values for width and height, but actually populates X and Y properties, but these don't accomplish what I need.
EDIT: For clarification: the image is not stored locally, it's fetched by a url. This probably is relevant.
EDIT2: I found that using the img onLoad listener gives me the correct information and is viable. Please let me know if y'all can think of a better solution, for now I'm sticking with
<img
src={content.img}
alt={content.title}
onLoad={(e) => console.log(e.target.clientHeight)}></img>

Scroll to top of a bubble in botframework webchat

Some answers of our chatbot are very long. The webchat scrolls automatically to the bottom so users have to scroll up to get to the top of the bubble and start reading.
I've implemented a custom renderer (react) to wrap the answers into a custom component which simply wraps the answer into a div-tag. I also implemented a simple piece of code to scroll to the top of the bubble.
const MyCustomActivityContainer = ({ children }) => {
const triggerScrollTo = () => {
if (scrollRef && scrollRef.current) {
(scrollRef.current as any).scrollIntoView({
behavior: 'smooth',
block: 'start',
})
}
}
const scrollRef: React.RefObject<HTMLDivElement> = React.createRef()
return (
<div ref={ scrollRef } onClick={ triggerScrollTo }>
{ children }
</div>
)
}
export const activityMiddleware = () => next => card => {
if (/* some conditions */) {
return (
<MyCustomActivityContainer>
{ next(card) }
</MyCustomActivityContainer>
);
} else {
return (
{ next(card) }
)
}
};
But this only works if the scrollbar slider is not at its lowest position (there is at least 1 pixel left to scroll down, see here). The problem is the useScrollToBottom hook which always scrolls to bottom automatically if the scrollbar is completely scrolled down.
Is there any way to overwrite the scroll behavior or to temporarily disable the scrollToBottom feature?
As there is no reproducible example I can only guess.
And I'll have to make some guesses on the question too.
Because it's not clear what exactly in not working:
Do you mean that click on the <div> of MyCustomActivityContainer and subsequent call to triggerScrollTo doesn't result into a scroll?
That would be strange, but who knows. In this case I doubt anyone will help you without reproducible example.
Or do you mean that you can scroll the message into view, but if it is already in the view then new messages can result into a scroll while user is still reading a message.
That's so, but it contradicts with you statement that your messages are very long, because that would be the problem with short messages, not with the long ones.
But anyway, you should be able to fix that.
If it works fine with 1 pixel off the lowest position, then just scroll that 1 pixel. You'll need to find the scrollable element. And do scrollable_element.scrollTop -= 1. I tested this approach here. And it worked (there the scrollable element is the grandparent of <p>'s)
Or do you try to scroll automatically at the moment the message arrives? Аnd that is the real issue, but you forgot to mention it, and didn't posted the code that tries to auto-scroll?
In that case you can try to use setTimeout() and defer the scroll by, let's say, 200ms.
This number in based on what I gathered from the source:
BotFramework-WebChat uses react-scroll-to-bottom
In react-scroll-to-bottom there are some timeouts 100ms and 34ms
BotFramework-WebChat doesn't redefine them
There are some heuristics in react-scroll-to-bottom that probably coursing the trouble
https://github.com/compulim/react-scroll-to-bottom/blob/3eb21bc469ee5f5095a431ac584be29a0d2da950/packages/component/src/ScrollToBottom/Composer.js
Currently, there are no reliable way to check if the "scroll" event is trigger due to user gesture, programmatic scrolling, or Chrome-synthesized "scroll" event to compensate size change. Thus, we use our best-effort to guess if it is triggered by user gesture, and disable sticky if it is heading towards the start direction.
And
https://github.com/compulim/react-scroll-to-bottom/blob/f19b14d6db63dcb07ffa45b4433e72284a9d53b6/packages/component/src/ScrollToBottom/Composer.js#L91
For what we observed, #1 is fired about 20ms before #2. There is a chance that this stickyCheckTimeout is being scheduled between 1 and 2. That means, if we just look at #1 to decide if we should scroll, we will always scroll, in oppose to the user's intention.
That's why I think you should use setTimeout()
Since there isn't a reproducible code for me tweak and show you. My suggestion is tweak your code slightly. Chatbot requires constant streaming of data when a new message arrives calculate the height of the div element created for the message. If the div element is greater than the widget height scroll to the top else you can choose to leave it as it is.

Getting a mousemove event from a component that doesn't want me to have it

I'm working on a React project and I'm using react-player to play a video. I want to get mousemove events while the video is playing, but react-player appears to be capturing the events and not propagating them. What can I do to get the events?
First I tried adding a handler to a div:
<div onMouseMove={() => console.log("mousemove")}>
<ReactPlayer url={props.url} />
</div>
Then I tried using addEventListener:
document.getElementById("my-div").addEventListener("mousemove", () => console.log("mousemove"), true)
<div id="my-div">
<ReactPlayer url={props.url} />
</div>
I was hoping that the addEventListener would work if I set the useCapture code to true, but it didn't help.
I couldn't find any possible way to force component to propagate events if it has internal logic preventing it.
You can try to create invisible <div> with adequate z-index attribute value to make it cover ReactPlayer component. After that you can attach listeners directly to it.
In such a way you will be able to capture all of the mouse events you want. It is not ideal way, but at least working.
Strictly speaking there is no way to provide event propagation the way you want it.
The reason is that ReactPlayer is rendered in a separate <iframe...> element that means that you are dealing with two independent DOM trees: one is in your main window context and another is in iframe context. Yet DOM events are propagated only within single DOM tree and are never being propagated outside of it.
In your specific case that means that mouse events that are happening over the player’s surface are completely processed by the code executed within iframe context and your main window will never even know about those events.
The discussed above solution with placing a kind of transparent div that would overlay the whole player’s surface won’t work either. In this case the “overlay div” would capture the mouse events indeed and these events would be properly propagated but only within the main window DOM tree. Yet DOM tree that is created within iframe would never know about these events. As you said you are aware of this already. This situation is simply opposite to the one described in the previous paragraph.
If you would have full control over the code that is run within iframe you would be able with a bit of efforts arrange events dispatching from main window to iframe using Window.postMessage() and [possibly] achieve desired results but ReactPlayer is a black box for you and this is not a working solution either.
So you have to either reject your idea of mouse event capturing or if you badly need to know that mouse pointer is moving over the players surface you have to invent other solution that is not based on [not existing] mouse event propagation between different DOM trees.
I’ve drafted a little POC component named SmartPlayer that implements one possible solution.
The basic idea is that the player is overlayed by other component named SmartOverlay that is intended to capture mouse events. This SmartOverlay is actually a grid of smaller “tiles” each of which has its own onMouseMove event handler. The trick is that once the tile’s onMouseMove event handler is fired it actually removes the tile from the DOM creating a “hole” in the overlay. Through that “hole” mouse events become “visible” to the player’s iframe. I know it sounds weird but I hope that you can get the whole picture from the code. You can actually see the “tiles” and the moving “hole” if you set “Show overlay” selector on.
Note that mouse move counter isn’t being changed while you move mouse within the “hole”. You can reduce this “granularity” by making tileHeight and tileWidth smaller but the price to be paid is a lower performance.
Strictly speaking it’s a kind of hack and I would think twice before using it in production. Though if you really need to catch the mouse event over the ReactPlayer it’s possibly the simpler solution you can get. There is some performance overhead in this solution but I tried to keep it acceptably low.
Good luck :)
P.S. I’ve had troubles making this code run as a code snippet. Hope to fix it somehow. Meanwhile I included the whole code directly in this answer.
To test the solution you may create a React application with create-react-app and then completely replace the App.js content with the code below.
I also I put the running version here: http://react-player.2358.com.ua/
import React, { Component } from 'react'
import ReactPlayer from 'react-player'
class SmartOverlay extends Component {
constructor(props) {
super(props);
const {height, width, } = props;
const tileHeight = props.tileHeight || 64;
const tileWidth = props.tileWidth || 64;
// below we're creating an array of "tiles"
// these "tiles" together are intended to cover all the players sufrace
this.overlayTiles = [];
for (let top = 0; top < height; top += tileHeight) {
for (let left = 0; left < width; left += tileWidth) {
const elementHeight = Math.min(tileHeight, height - top);
const elementWidth = Math.min(tileWidth, width - left);
const tile = {top, left, elementHeight, elementWidth }
// for each tile its own 'mousmove' event handler is created and assigned as the tile's property
tile.onMouseMove = () => this.onMouseMove(tile);
this.overlayTiles.push(tile);
}
}
// all the overlay tiles are "active" at the beginning
this.state = {activeOverlayTiles: this.overlayTiles}
}
onMouseMove(currentTile) {
// call event handler that is passed to the SmartOvelay via its handleMouseMove property
// using setTimeout here isn't strictly necessary but I prefer that "external" handler was executed only after we exit current method
setTimeout(this.props.handleMouseMove);
// "remove" current tile from the activeOverlayTiles array (that will cause removing the tile's DIV element from DOM eventually)
// actually we are not removing the item from the array but creating a new array that contains all the tiles but the current one
this.setState({activeOverlayTiles: this.overlayTiles.filter(item => item !== currentTile)})
// after current tile is removed from DOM the "hole" is left on the overlay "surface"
// and the player's iframe can "see" the mouse events that are happening within the "hole"
}
render() {
const showOverlayTileStyle = this.props.showOverlay ? {opacity: 0.5, background: '#fff', border: "1px solid #555", } : {}
return (
this.state.activeOverlayTiles.map(item => (
<div onMouseMove = {item.onMouseMove} style = {{...showOverlayTileStyle, position: 'absolute', top: item.top, left: item.left, height: item.elementHeight, width: item.elementWidth,}}></div>
))
);
}
}
const PlayerWithoutOverlay = ({height, width, url}) => (
<div style = {{position: 'absolute'}}>
<ReactPlayer height = {height} width = {width} url = {url} />
</div>
)
const SmartPlayer = ({height, width, url, showOverlay, handleMouseMove}) => (
<>
<PlayerWithoutOverlay height = {height} width = {width} url = {url} />
<SmartOverlay height = {height} width = {width} showOverlay = {showOverlay} handleMouseMove = {handleMouseMove} />
</>
)
class App extends Component {
constructor(props) {
super(props);
this.state = {
showOverlay: false,
mouseMoves: 0
}
}
toggleShowOverlay(e) {
// this simply shows/hide the ovelay depending on checkbox state
this.setState(state => ({showOverlay: !state.showOverlay}))
}
handleMouseMove(){
// adds 1 to state.mouseMoves counter
this.setState(state => ({mouseMoves: state.mouseMoves + 1}));
}
render() {
const height = 420;
const width = 640;
return (
<div style = {{margin: 12, position: 'relative'}}>
<div style = {{height: height }}>
<SmartPlayer
height = {height}
width = {width}
showOverlay = {this.state.showOverlay}
url = "https://www.youtube.com/watch?v=A0Z7fQyTb4M"
handleMouseMove = {this.handleMouseMove.bind(this)} />
</div>
<div style = {{marginTop: 12}}>
<label>Moves detected: </label>
<span>{`${this.state.mouseMoves}`}</span>
<label onClick = {this.toggleShowOverlay.bind(this)}> <input type = "checkbox" value="1" checked = {this.state.showOverlay} />Show overlay</label>
</div>
</div>
)
}
}
export default App;

The background image style is not applied with setInterval in React.js

Using AJAX request, I am trying to change background image and set it's style properties. In the function which is called every few seconds through setInterval method, I define style and set states of component in the following way:
changeThings() {
let maxVal = this.props.data.length;
let ranNum = Math.floor((Math.random() * maxVal));
let imgVal = (this.props.url[ranNum])? 'url('+ this.props.url[ranNum].url+')':null;
let style = {
background:imgVal,
backgroundSize:'cover',
backgroundPosition:'center',
bacgkrouhndRepeat:'no-repeate'
};
this.setState({content:{style:style,
section:section,
title:title,
by:author,
dateInfo:updatedDate}});
}
render() {
return ({
<div>
//...other components
(this.state.content.style)?
<div id="image" style={this.state.content.style}>:null
//...other components
</div>
})
}
The very first image is displayed with every background image css properties applied. From second images however, it only changes images(background:url value) but not other background image properties such as position, repeat, size etc.
What is the reason of this problems and How can I solve it?
Here is how I fixed the problem. First I only saved URL string data to this.state.content rather than entire css style. It worked when I set the style property directly to the JSX.
Also, basically when DOM define the value for style property of background-size, it calculates the value based on the current 'snapshot' value of element height and width where the image will be display. I am currently making responsive web app so I thought the css style does not work when I changed the view.

Element's style's top and left sometimes doesn't update in ReactJS app

I have this element that looks something like this:
render() {
// figure these values out based on props and state
let left = ...
let top = ...
// the values of these are floating point or integer
let src = '/image.png'
let style={top: top, left: left}
console.log("SETTING STYLE", style)
return <img src={src} style={style}/>
}
So far so good. A stylesheet sets these images to position:absolute and that works. I can control the position of the elements with props and state and business logic.
However, the element can be dragged and moved around (using Draggabilly). I have an event listener that listens to when the element stopped being dragged around. That gives me a position (x, y). I then pass that into a function which, based on some business logic, "corrects" the numbers and updates the props and state so that the element is re-rendered.
However, sometimes, the change doesn't take effect. Why?!
As you can see I console.log what the style should be and I can clearly see that it says something like Object {top: 123.5, left: 234.5} but when I inspect the element in my devtools and look at the Rules set, the numbers are different!
E.g.
Basically, those numbers sometimes don't add up with what I set when I rendered the element in the render() method.
The answer the Draggabilly plugin sets the top and left too after it has fired the event which my React app is relying on. I.e. It does something like this:
Draggabilly.prototype._stopDragging = function(event) {
var x = event.something.x; y = event.something.y;
this.triggerEvent('dragEnd', ...)
this.element.style.left = x
this.element.style.top = y
}
So two distinct systems are trying to set the style attribute.

Categories

Resources