framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
22.56k stars 748 forks source link

[BUG] dragConstraints are gone when resizing the window and updating them #1659

Closed zamorai closed 6 months ago

zamorai commented 1 year ago

1. Read the FAQs 👇

2. Describe the bug

Im creating a carousel that overflows the main window, and using drag with drag constraints to interact with the carousel. This all works well, but when I try to resize the window, the dragConstraints disappear and i can drag the carousel with no bounds.

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

https://codesandbox.io/s/distracted-bas-x7zirt?file=/src/App.js

This code sand box represents the issue perfectly, dont resize the window and see how the carousel works fine, as soon as you resize, it breaks and the bounds arent updated, but rather disappear.

4. Steps to reproduce

Steps to reproduce the behavior:

  1. resize window
  2. observe dragConstraints disappear

5. Expected behavior

dragConstraints are updated to fit the new window size.

6. Video or screenshots

If applicable, add a video or screenshots to help explain the bug.

almeshekah commented 1 year ago

I'm facing the same issue do you have any solution

kvpendergast commented 1 year ago

I'm not sure if this is related, but if dragging and ending the drag with momentum the draggableConstraints are also ignored. If you stop moving finger after stopping motion, draggable constraints are respected.

awesomechoi11 commented 1 year ago

You can get around this bug for the time being by having a resize listener and having the component forcefully re-render by defining a key.

const size = useWindowSize();
...
<motion.div
    drag="x"
    dragConstraints={{
        left: 0,
        right: 0,
    }}
    key={JSON.stringify(size)}
>
    ...
</motion.div>
sarahkirby commented 1 year ago

Experiencing the same issue in version 7.6.1.

samclk commented 1 year ago

also got some issue, dragConstraints are disabled when resizing

version 7.6.1

brendanmoore commented 1 year ago

I've been experiencing this same issue, and so far I can see that after a window.onresize event this.contraints becomes false in the VisualElementDragControls

https://github.com/framer/motion/blob/f736e4251a4d59f5c93c401cfbf59628734c5b36/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts#L366

Screenshot 2022-11-02 at 11 11 09

So far I've not found an elegant workaround other than manually manipulating the axis motion value in onDragTransitionEnd

[EDIT] - Looking back through the call stack it appears that the visualElement.projection.layout is lost after screen resize... 👀

dehcastro commented 1 year ago

I'm facing the same issue! Whenever I resize the window, all boundaries disappear 🫠

DarioCorbinelli commented 1 year ago

Exactly same issue here too

ClayCooperLA commented 1 year ago

To get around this I used the ref option, dragConstraints={constraintsRef}. In my case I needed to create a dummy div nested inside that mimics the proper drag area.

kentare commented 1 year ago

We experience the same issues. Neither the ref or key workaround fixes the problem for mobile (both iOS and Android), so we are not able to use drag with constraints at all.

laneme commented 1 year ago

To get around this I used the ref option, dragConstraints={constraintsRef}. In my case I needed to create a dummy div nested inside that mimics the proper drag area.

I'm not sure how i should exactly recommend people to structure their divs but my issue solve with this too.

I used a wrapper div, made sure it is flex so it gets the full width of the draggable div.

And so far works.

But the original issue remains, not everyone's case will work with ref constraints. I hope someone gets time to look into it from framer team

batuhanbilginn commented 1 year ago

I thinks this fix the issue. At least it fixed the mine...

<div ref={constraintsRef}>
     <motion.div drag dragConstraints={constraintsRef} dragElastic={0} ... />
</div>
CecoSvidovski commented 1 year ago

I need to dynamically set the left constraint for what I need and it works fine but at resize same happens to me... I can't just use a ref of the outside div since the outside div's width is less than the inside div.

  const viewportSize = useViewportSize();
  const [dragConstraints, setDragConstraints] = useState({
    right: 0,
    left: 0
  });
  const slider = useRef();

  useEffect(() => {
    setDragConstraints(d => ({ ...d, left: -(slider.current.scrollWidth - viewportSize.width) }))
    console.log(slider.current.scrollWidth - viewportSize.width);
  }, [slider, viewportSize])

  return (
    <Content>
      <Slider ref={slider} whileTap={{ cursor: 'grabbing' }}>
        <InnerSlider drag='x' dragConstraints={dragConstraints}>
          {data.map((d) => (
            <Item key={d.id}>
              <Image src={d.imgSrc} width={683} height={1024} alt='' />
            </Item>
          ))}
        </InnerSlider>
      </Slider>
    </Content>
  );

EDIT: I'm on version 10.12.4. In the console it reports the correct dimensions on every resize.

My useVIewportSize hook looks like this:

import { useState, useEffect } from 'react';

export default function useViewportSize() {
  const [viewportSize, setViewportSize] = useState({
    width: null,
    height: null,
  });

  useEffect(() => {
    function handleResize() {
      setViewportSize({ width: window.innerWidth, height: window.innerHeight });
    }

    if(typeof window !== 'undefined'){
      handleResize();
    }

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return viewportSize;
}
LuLue7775 commented 1 year ago

To get around this I used the ref option, dragConstraints={constraintsRef}. In my case I needed to create a dummy div nested inside that mimics the proper drag area.

I'm not sure how i should exactly recommend people to structure their divs but my issue solve with this too.

I used a wrapper div, made sure it is flex so it gets the full width of the draggable div.

And so far works.

But the original issue remains, not everyone's case will work with ref constraints. I hope someone gets time to look into it from framer team

How exactly does it work around? I did it this way, but it didn't work.

const SliderWrap = ({
  children,
  sliderRef,
  x,
  sliderConstraints,
  bounceStiffness,
  bounceDamping,
}) => {
  return (
    <div
      ref={sliderRef}
      className="bg-red-100 overflow-x-hidden h-slider flex items-center "
    >
      <motion.div
        className="cursor-grab flex justify-between"
        drag="x"
        initial={{x: 0}}
        style={{x}}
        dragConstraints={{
          left: parseInt(`${-sliderConstraints}`),
          right: 0,
        }}
        dragTransition={{bounceStiffness, bounceDamping}}
        key={JSON.stringify(sliderConstraints)}
      >
        {children}
      </motion.div>
    </div>
  );
};

And outside this sliderWrap, I have a state updater. Still didn't work.

export const DragSlider = ({
  children,
}) => {
  const ref = useRef();
  const x = useMotionValue(0);

  const [sliderWidth, setSliderWidth] = useState(0);
  const [sliderChildrenWidth, setSliderChildrenWidth] = useState(0);
  const [sliderConstraints, setSliderConstraints] = useState(0);

  useEffect(() => {
    if (!ref && !ref.current) return;
    const calcSliderChildrenWidth = () => {
    setSliderChildrenWidth(ref?.current?.scrollWidth);
    };
    calcSliderChildrenWidth();
    const calcSliderWidth = () => {
      setSliderWidth(document.body.scrollWidth);
    };
    calcSliderWidth();
    window.addEventListener('resize', calcSliderWidth);
    const calcSliderConstraints = () => {
      setSliderConstraints(sliderChildrenWidth - sliderWidth);
    };
    calcSliderConstraints();
    window.addEventListener('resize', calcSliderConstraints);
  }, [ref, sliderChildrenWidth, sliderWidth]);

  return (
    <SliderWrap
      sliderRef={ref}
      x={x}
      sliderConstraints={sliderConstraints}
      bounceStiffness={bounceStiffness}
      bounceDamping={bounceDamping}
    >
      <>{children}</>
    </SliderWrap>
  );
};
foxalex commented 1 year ago

As @ClayCooperLA had mentioned, I can solve this problem using a generic element to serve as a reference for the constraints.

In this example I made a Horizontal List draggable when the content is bigger than the container and it handles resizing without problems;

export function HorizontalList(props: HorizontalListProps) {
  const [offset, setOffset] = useState(0);
  const [dragField, setDragField] = useState(0);

  const wrapperRef = useRef<HTMLDivElement>(null);
  const dragFieldRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const updateOffset = () => {
      if (wrapperRef.current && contentRef.current) {
        const { width } = wrapperRef.current.getBoundingClientRect();

        const offSetWidth = contentRef.current.scrollWidth;
        const newOffset = offSetWidth - width;

        setOffset(newOffset);
        setDragField(offSetWidth);
      }
    };

    // Set Initial Value
    updateOffset();

    // Check for resizing Events.
    window.addEventListener("resize", updateOffset);
    return () => {
      window.removeEventListener("resize", updateOffset);
    };
  }, []);

  return (
    <div ref={wrapperRef} className="w-full overflow-hidden relative">
      {/* Element for Constraints Reference */}
      <div
        ref={dragFieldRef}
        className="h-full absolute top-0 left-0 pointer-events-none"
        style={{
          left: `-${offset}px`,
          width: `${dragField}px`
        }}
      ></div>

      {/* Content */}
      <motion.div
        ref={contentRef}
        drag={offset > 0 ? "x" : undefined}
        dragConstraints={dragFieldRef}
        className="flex gap-4 relative"
      >
        {props.children}
      </motion.div>
    </div>
  );
}

This example on Codesandbox

dimitrisanastasiadis commented 11 months ago

Debouncing the setter of the constraint seems be working for me. Hope this helps somebody:

import { motion } from "framer-motion"
import React, { useCallback, useEffect, useRef, useState } from "react"
import { debounce } from "lodash"

const ContentSlider = ({ children }: { children: React.ReactNode }) => {
  const [constraintLeft, setConstraintLeft] = useState(0)
  const [node, setNode] = useState<HTMLElement>()

  const containerRef = useCallback((node: HTMLElement | null) => {
    if (!node) return
    setNode(node)
  }, [])

  useEffect(() => {
    if (!node) return

    const handleResize = debounce(() => {
      setConstraintLeft(-(node.scrollWidth - node.clientWidth))
    }, 500)

    handleResize()

    const resizeObserver = new ResizeObserver(handleResize)
    resizeObserver.observe(node)

    return () => resizeObserver.disconnect()
  }, [node])

  return (
    <motion.div className="flex gap-5" ref={containerRef} dragConstraints={{ right: 0, left: constraintLeft }} drag="x">
      {children}
    </motion.div>
  )
}

export default ContentSlider
MauroSerrano-dev commented 8 months ago

I'm having the same problem.

I thought I had managed to solve it using dragConstraints={containerRef} but this caused me a visual bug when I was dragging to the end and there were multiple carousels on the page. This visual bug does not happen when I use dragConstraints={{ right: 0, left: -constraint }}, but again have the problem of dragConstraints being false when the resize happens.

Using dynamic key that changes as the resize happens didn't work for me.

Does anyone have a solution for this problem?

Kitketovsky commented 7 months ago

As @ClayCooperLA had mentioned, I can solve this problem using a generic element to serve as a reference for the constraints.

In this example I made a Horizontal List draggable when the content is bigger than the container and it handles resizing without problems;

export function HorizontalList(props: HorizontalListProps) {
  const [offset, setOffset] = useState(0);
  const [dragField, setDragField] = useState(0);

  const wrapperRef = useRef<HTMLDivElement>(null);
  const dragFieldRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const updateOffset = () => {
      if (wrapperRef.current && contentRef.current) {
        const { width } = wrapperRef.current.getBoundingClientRect();

        const offSetWidth = contentRef.current.scrollWidth;
        const newOffset = offSetWidth - width;

        setOffset(newOffset);
        setDragField(offSetWidth);
      }
    };

    // Set Initial Value
    updateOffset();

    // Check for resizing Events.
    window.addEventListener("resize", updateOffset);
    return () => {
      window.removeEventListener("resize", updateOffset);
    };
  }, []);

  return (
    <div ref={wrapperRef} className="w-full overflow-hidden relative">
      {/* Element for Constraints Reference */}
      <div
        ref={dragFieldRef}
        className="h-full absolute top-0 left-0 pointer-events-none"
        style={{
          left: `-${offset}px`,
          width: `${dragField}px`
        }}
      ></div>

      {/* Content */}
      <motion.div
        ref={contentRef}
        drag={offset > 0 ? "x" : undefined}
        dragConstraints={dragFieldRef}
        className="flex gap-4 relative"
      >
        {props.children}
      </motion.div>
    </div>
  );
}

This example on Codesandbox

this one actually helped me

tarviroos commented 7 months ago

This is a reaaaally nasty bugger! It took me so much time to first of all understand what's going on.. Then I found this thread. Unfortunately none of the solutions really worked for me and what I eventually did was omit dragConstraints and dragElasticity entirely and instead use onDragEnd, onDragTransitionEnd and MotionValue for Y axis to manually create my own constraining, but it's not as elegant nor does it feel as good.

The biggest problem of it all is iOS Safari where on page scroll the address bar height size changes together with viewport size. That always loses the dragConstraints and turns this prop essentially useless.

gandarufu commented 4 months ago

As @ClayCooperLA had mentioned, I can solve this problem using a generic element to serve as a reference for the constraints.

In this example I made a Horizontal List draggable when the content is bigger than the container and it handles resizing without problems;

export function HorizontalList(props: HorizontalListProps) {
  const [offset, setOffset] = useState(0);
  const [dragField, setDragField] = useState(0);

  const wrapperRef = useRef<HTMLDivElement>(null);
  const dragFieldRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const updateOffset = () => {
      if (wrapperRef.current && contentRef.current) {
        const { width } = wrapperRef.current.getBoundingClientRect();

        const offSetWidth = contentRef.current.scrollWidth;
        const newOffset = offSetWidth - width;

        setOffset(newOffset);
        setDragField(offSetWidth);
      }
    };

    // Set Initial Value
    updateOffset();

    // Check for resizing Events.
    window.addEventListener("resize", updateOffset);
    return () => {
      window.removeEventListener("resize", updateOffset);
    };
  }, []);

  return (
    <div ref={wrapperRef} className="w-full overflow-hidden relative">
      {/* Element for Constraints Reference */}
      <div
        ref={dragFieldRef}
        className="h-full absolute top-0 left-0 pointer-events-none"
        style={{
          left: `-${offset}px`,
          width: `${dragField}px`
        }}
      ></div>

      {/* Content */}
      <motion.div
        ref={contentRef}
        drag={offset > 0 ? "x" : undefined}
        dragConstraints={dragFieldRef}
        className="flex gap-4 relative"
      >
        {props.children}
      </motion.div>
    </div>
  );
}

This example on Codesandbox

You, sir, are a hero. Your solution worked for me, and paired with @dimitrisanastasiadis debounce, it even works when maximizing the window.

I'm wondering if this whole over-scroll functionality could be hard-coded into framer-motion from the get-go. I'm sure a lot of people would use it just to create responsive sliders that don't over-shoot.