Issue
What I want to achieve is smoothly scaled div
container while scrolling (using mouse wheel to be strict) so user can zoom in and out.
However, my styles are "applied" by the browser only either when I scroll really slow or scroll normally and then wait about 0.2 seconds (after that time the changes are "bunched up"). I would like for the changes to be visible even during "fast" scrolling, not at the end.
The element with listener:
<div onWheel={(event) => {
console.log("wheeling"); // this console log fires frequently,
// and I want to update styles at the same rate
changeZoom(event);
}}
>
<div ref={scaledItem}> // content div that will be scaled according to event.deltaY
... // contents
</div>
</div>
My React code:
const changeZoom = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
if (!scaledItem.current) return;
const newZoom = parseFloat(scaledItem.current.style.scale) + event.deltaY * 0.001;
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.scale = currentZoom.toString();
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.scale = "1";
}, [])
What I have tried first was to omit all the React states, and edit scaledItem.current.style.scale
directly from useCallback, but the changes took place in a bunch, after the wheeling events stopped coming. Then I moved zoom amount to currentZoom
useState hook, but rerenders don't help either.
Edit: I have also tried adding EventListener inside useEffect directly to the DOM Node:
useEffect(() => {
if (!scaledItemWrapper.current) return; // ref for wrapper of my scaled content
const container = scaledItemWrapper.current;
container.addEventListener("wheel", changeZoom);
return () => {
container.removeEventListener("wheel", changeZoom);
};
}, [changeZoom]);
Solution
I got your code working pretty much without changes. I only had to change scale
because it has poor browser support. Perhaps some other code not posted here is causing the behavior you observe?
I see that if you just set scale
directly, it always gives choppy behavior, making the scale jump instantly according to how much distance was traveled before the onWheel
event.
I'm not sure if this is precisely what you're asking, but it might still help. You can add a CSS transition on the transform property to smooth out the scaling movement. It seems to work well with a value of .2 seconds for this case.
transition: transform .2s ease-out;
For best layout, it's best to first run the snippet before opening it full page. Otherwise it works but it will still scroll the whole page.
Without transition (choppy)
const {useCallback, useEffect, useState, useRef} = React;
const minZoom = .01;
function App() {
const [currentZoom, setCurrentZoom] = useState("1");
const scaledItem = useRef();
const changeZoom = useCallback((event) => {
if (!scaledItem.current) return;
const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
const newZoom = Math.max(minZoom, parseFloat(scaleNumber) + event.deltaY * 0.001);
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.transform = `scale(${currentZoom.toString()})`;
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.transform = "scale(1)";
}, [])
return <div onWheel={(event) => {
console.log("wheeling");
changeZoom(event);
}}
>
<div class="scaled" ref={scaledItem}>
<p>Scale me up and down!</p>
</div>
</div>
}
ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
border: 2px solid lightgreen;
transform: scale(1);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<div id="root2"></div>
With transition (smooth)
const {useCallback, useEffect, useState, useRef} = React;
const minZoom = .01;
function App() {
const [currentZoom, setCurrentZoom] = useState("1");
const scaledItem = useRef();
const changeZoom = useCallback((event) => {
if (!scaledItem.current) return;
const scaleNumber = scaledItem.current.style.transform.replace('scale(','').replace(')','');
const newZoom = Math.max(minZoom, parseFloat(scaleNumber) + event.deltaY * 0.001);
console.log(newZoom); // logs as frequently as "wheeling" above
setCurrentZoom(newZoom);
}, []);
useEffect(() => {
if (!scaledItem.current) return;
scaledItem.current.style.transform = `scale(${currentZoom.toString()})`;
}, [currentZoom]);
useEffect(() => { // this is just for reproduction, needs to set initial scale to 1
if (!scaledItem.current) return;
scaledItem.current.style.transform = "scale(1)";
}, [])
return <div onWheel={(event) => {
console.log("wheeling");
changeZoom(event);
}}
>
<div class="scaled" ref={scaledItem}>
<p>Scale me up and down!</p>
</div>
</div>
}
ReactDOM.render(<App/>, document.getElementById('root'));
.scaled {
border: 2px solid lightgreen;
transform: scale(1);
transition: transform .2s ease-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Answered By - inwerpsel
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.