motiondivision / motion

A modern animation library for React and JavaScript
https://motion.dev
MIT License
24.6k stars 824 forks source link

[BUG] React HTMLElement typings conflict with motion components (usage with react-aria) #1723

Open binaryartifex opened 2 years ago

binaryartifex commented 2 years ago

Describe the bug

Attempting to combine the efforts of framer-motion and react-aria into a custom design system while leveraging typescript. React aria returns a set of component props that are then spread onto the base component (a button in this instance) however theres substantial difference between several of the spread properties that I have been unable to resolve. At first i thought this might be a react-aria issue however the returned button props from the aria hook are of the type React.ButtonHTMLAttributes<HTMLButtonElement> which are straight from the react typings ecosystem.

https://codesandbox.io/s/blissful-banach-sdgczr?file=/src/button/button.tsx

Steps to reproduce

Steps to reproduce the behavior:

  1. create a forwardRef Button component that accepts props of the type AriaButtonProps that returns a motion.button
  2. use useObjectRef helper hook from @react-aria/utils to derive new ref object
  3. provide useButton hook from react-aria library with props and derived ref -> destructure to expose buttonProps object
  4. Take note of the type of buttonProps which is of type React.ButtonHTMLAttributes<HTMLButtonElement>
  5. Attempt to spread buttonProps on the motion.button component (will be presented with typescript errors)
  6. Separate the properties onAnimationStart, onDragStart, onDragEnd, onDrag from the buttonProps via a separate destructure expression that includes a spread of the remaining props:
    const {
      onAnimationStart,
      onDragStart,
      onDragEnd,
      onDrag,
      ...restOfPropsThatWork
    } = buttonProps;
  1. Spread the "restOfPropsThatWork" onto the motion.button component instead -> no typescript arguments are presented.

Expected behavior Expected to be able to seamlessly spread React typings onto motion components. The actual functionality seems to work just fine as demonstrated in this youtube video that does exactly what i am attempting to do, however javascript is used https://www.youtube.com/watch?v=ydZSNUbHl_8

Environment details

OS Windows 11

VS CODE Version: 1.71.2 (user setup) Electron: 19.0.12 Chromium: 102.0.5005.167 Node.js: 16.14.2 V8: 10.2.154.15-electron.0 Sandboxed: No

DEPENDENCIES @react-aria/utils: "^3.13.3" framer-motion: "^7.5.1" react: "18.2.0", react-aria: "^3.19.0", react-dom: "18.2.0", typescript: "4.8.4"

yerffejytnac commented 1 year ago

@binaryartifex find any workarounds for this? I find myself in a similar scenario

deltasierra96 commented 1 year ago

Any updates on this? Have tried multiple workarounds to no avail.

VugarAhmadov commented 1 year ago

I also have this issue. this is the codesandbox to reproduce the bug https://codesandbox.io/s/unruffled-lehmann-k51cnc?file=/pages/index.tsx for now, the workaround is using {...(buttonProps as any)}

binaryartifex commented 1 year ago

sorry folks, i couldn't figure it out for the life of me either. I end up resigning myself to having a child motion.div in the insert element here. particularly with react-aria the only way i know there's no unintended properties propogating somewhere they shouldn't is thinking ahead about what needs to be animated, and wrapping those in a for-purpose motion.div. sorry i can't be more help. be good to get some feedback from framer themselves. in the absence of react-aria you can't compose react and motion typings together. I havn't tested this yet, was just playin around in the IDE then but this seems to do the trick....

import { HTMLMotionProps, motion } from "framer-motion";
import { forwardRef } from "react";

type ButtonProps = HTMLMotionProps<"button">;

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  function Button(props, ref) {
    return (
      <motion.button {...props} ref={ref}>
        {props.children}
      </motion.button>
    );
  },
);
avendiart commented 1 year ago

This is an issue for us as well, we are heavily relying on polymorphism and this is a deal breaker. Maybe there is some potential here to align type definitions with react handler definitions? Any additional information which framer-motion provides could go into a separate argument if necessary (for instance drag handlers provide additional information through info argument.

tianenpang commented 1 year ago

Hey guys. I got the same type issue, I found that using mergeProps with MotionProps or HTMLMotionProps might help 🤔

Example and CodeSandbox preview.

Handling animations within component

import type { ForwardedRef } from "react";
import type { AriaButtonProps } from "react-aria";
import type { MotionProps } from "framer-motion";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { useButton, mergeProps } from "react-aria";

// safe-motion
const useSafeMotion = (motion: MotionProps): MotionProps => {
  const reduced = useReducedMotion();

  return reduced ? {} : motion;
};

// button
const ButtonComponent = (
  props: ButtonProps,
  ref: ForwardedRef<HTMLButtonElement>
) => {
  const { children, className, ...rest } = props;

  const innerRef = useRef<HTMLButtonElement>(null);

  useImperativeHandle(ref, () => innerRef.current!);

  const { buttonProps } = useButton({ ...rest, children }, innerRef);

  const safeMotion = useSafeMotion({
    initial: { scale: 1 },
    whileTap: { scale: 0.95 },
    whileHover: { scale: 1.05 }
  });

  return (
    <motion.button
      ref={innerRef}
      className={className}
      {...mergeProps(buttonProps, safeMotion)}
    >
      {children}
    </motion.button>
  );
};

export interface ButtonProps extends AriaButtonProps {
  className?: string;
}

export const Button = forwardRef(ButtonComponent);

Passing animations into component via props

import type { ForwardedRef } from "react";
import type { AriaButtonProps } from "react-aria";
import type { HTMLMotionProps } from "framer-motion";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { motion } from "framer-motion";
import { useButton, mergeProps } from "react-aria";

const ButtonComponent = (
  props: ButtonProps,
  ref: ForwardedRef<HTMLButtonElement>
) => {
  const { children, className, ...rest } = props;

  const innerRef = useRef<HTMLButtonElement>(null);

  useImperativeHandle(ref, () => innerRef.current!);

  const { buttonProps } = useButton({ ...rest, children }, innerRef);

  return (
    <motion.button
      ref={innerRef}
      className={className}
      // ⚠ note non-dom attributes that cannot be handled by framer-motion
      {...mergeProps(rest, buttonProps)}
    >
      {children}
    </motion.button>
  );
};

export interface ButtonProps
  extends AriaButtonProps,
    Omit<HTMLMotionProps<"button">, keyof AriaButtonProps> {
  className?: string;
}

export const Button2 = forwardRef(ButtonComponent);
miguelocarvajal commented 1 year ago

I was able to resolve this with: {...(buttonProps as ButtonHTMLAttributes<HTMLButtonElement> & MotionProps)}

Probably could be better but at least TS stops complaining.

nftchance commented 1 year ago

Here to +1 experiencing this edge case and needing a solid solution. Polymorphism w/ framer becomes untenable nearly instantly, given the current configuration.