Согласно документации Slot, если ваш компонент имеет один дочерний элемент, можно создать соответственно полиморфную кнопку:
// your-button.jsx
import React from 'react';
import { Slot } from '@radix-ui/react-slot';
function Button({ asChild, ...props }) {
const Comp = asChild ? Slot : 'button';
return <Comp {...props} />;
}
Что, если вы хотите предварительно настроить компонент Button для обеспечения базовой анимации с помощью framer-motion ?
Компонент полиморфной кнопки должен возвращать компонент движения, который масштабируется до 0,9 при нажатии на него.
С помощью Framer-motion этого можно добиться с помощью Motion Component соответственно:
function Button({ ..props }) {
return <motion.button whileTap = {{ scale: 0.9 }} {...props} />;
}
Как добиться такого же поведения с полиморфной кнопкой, использующей Slot?
Я попытался обернуть Slot компонентом высшего порядка motion
, но это вызвало конфликт типов.
Мой компонент кнопки:
export interface ButtonProps extends HTMLMotionProps<'div'> {
asChild?: boolean;
}
const MotionSlot = motion(Slot);
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, ...props }, ref) => {
const Comp = asChild ? MotionSlot : motion.button;
return <Comp whileTap = {{ scale: 0.9 }} ref = {ref} {...props} />;
}
);
Button.displayName = 'Button';
Ошибка машинописного текста:
Type '{ className?: string | undefined; title?: string | undefined; defaultChecked?: boolean | undefined; defaultValue?: string | number | readonly string[] | undefined; suppressContentEditableWarning?: boolean | undefined; ... 314 more ...; ref: ForwardedRef<...>; }' is not assignable to type 'Omit<SlotProps & RefAttributes<HTMLElement> & MotionProps, "ref">'.
Types of property 'style' are incompatible.
Type 'MotionStyle | undefined' is not assignable to type '(CSSProperties & MakeCustomValueType<{ outline?: MotionValue<number> | MotionValue<string> | MotionValue<any> | Outline<string | number> | undefined; ... 820 more ...; vectorEffect?: MotionValue<...> | ... 3 more ... | undefined; }> & MakeCustomValueType<...> & MakeCustomValueType<...> & MakeCustomValueType<...>) | ...'.
Type 'MotionStyle' is not assignable to type '(CSSProperties & MakeCustomValueType<{ outline?: MotionValue<number> | MotionValue<string> | MotionValue<any> | Outline<string | number> | undefined; ... 820 more ...; vectorEffect?: MotionValue<...> | ... 3 more ... | undefined; }> & MakeCustomValueType<...> & MakeCustomValueType<...> & MakeCustomValueType<...>) | ...'.
Type 'MotionStyle' is not assignable to type 'CSSProperties & MakeCustomValueType<{ outline?: MotionValue<number> | MotionValue<string> | MotionValue<any> | Outline<string | number> | undefined; ... 820 more ...; vectorEffect?: MotionValue<...> | ... 3 more ... | undefined; }> & MakeCustomValueType<...> & MakeCustomValueType<...> & MakeCustomValueType<...>'.
Type 'MotionStyle' is not assignable to type 'CSSProperties'.
Types of property 'accentColor' are incompatible.
Type 'MotionValue<number> | MotionValue<string> | CustomValueType | MotionValue<any> | AccentColor | undefined' is not assignable to type 'AccentColor | undefined'.
Type 'MotionValue<number>' is not assignable to type 'AccentColor | undefined'.ts(2322)
Хорошо, у меня было немного свободного времени, чтобы изучить эту проблему, и мне удалось создать что-то, что работает.
Идея:
PolymorphicButton
является базовым компонентом, который отвечает только за предоставление элемента на основе реквизита asChild
.MotionPolymorphicButton
является результатом обертывания PolymorphicButton
с помощью Framer-Motion motion
Компонента высшего порядка (HoC). Это позволяет нам определять анимацию в PolymorphicButton
.Button
действует как HoC, который внедряет свойства анимации в MotionPolymorphicButton
Код:
interface PolymorphicButtonProps extends ComponentPropsWithoutRef<'button'> {
asChild?: boolean;
}
// Forwarding the ref is mandatory for using the `motion` function,
// ensuring proper animation handling.
const PolymorphicButton = forwardRef<HTMLButtonElement, PolymorphicButtonProps>(
({ asChild = false, ...rest }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref = {ref}
{...rest}
/>
);
}
);
PolymorphicButton.displayName = 'PolymorphicButton';
/* -----------------------------------------------------------------------------------------------*/
// Wrapping PolymorphicButton with `motion` avoids complex type handling,
// keeping the button polymorphic and animated.
const MotionPolymorphicButton = motion(PolymorphicButton);
/* -----------------------------------------------------------------------------------------------*/
// Define pre-configured motion props for the Button component
const buttonMotionProps = {
whileTap: { scale: 0.9 },
} as const satisfies HTMLMotionProps<'button'>;
const isTargetAndTransition = (
field: VariantLabels | TargetAndTransition
): field is TargetAndTransition =>
typeof field !== 'string' && !Array.isArray(field);
interface ButtonProps
extends ComponentPropsWithoutRef<typeof MotionPolymorphicButton> {
disableDefaultAnimations?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
// Merges pre-configured whileTap behavior with user props
const whileTap = useMemo<
VariantLabels | TargetAndTransition | undefined
>(() => {
if (props.disableDefaultAnimations) {
return props.whileTap;
}
if (props.whileTap === undefined) {
return buttonMotionProps.whileTap;
}
return isTargetAndTransition(props.whileTap)
? { ...buttonMotionProps.whileTap, ...props.whileTap }
: props.whileTap;
}, [props.disableDefaultAnimations, props.whileTap]);
return (
<MotionPolymorphicButton
{...buttonMotionProps}
{...props}
ref = {ref}
whileTap = {whileTap}
/>
);
});
Button.displayName = 'Button';