Add framer-motion and motion dependencies; implement animations in ModalContainer and PopupContainer components; enhance PopupWrapper for touch support; update SoundCheckModal and VoiceCheckModal for improved audio testing; refactor Select and ActionsPopover components for animation support.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Button from "./Button";
|
||||
import MoreIcon from "../icons/MoreIcon";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
interface ActionsPopoverProps {
|
||||
options: {
|
||||
@@ -19,7 +20,7 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
|
||||
const [menuHeight, setMenuHeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||
if (
|
||||
popoverRef.current &&
|
||||
!popoverRef.current.contains(event.target as Node)
|
||||
@@ -29,8 +30,10 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("touchstart", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("touchstart", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -39,11 +42,7 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const spaceBelow = window.innerHeight - buttonRect.bottom;
|
||||
const spaceAbove = buttonRect.top;
|
||||
if (spaceBelow < menuHeight && spaceAbove > menuHeight) {
|
||||
setOpenUpwards(true);
|
||||
} else {
|
||||
setOpenUpwards(false);
|
||||
}
|
||||
setOpenUpwards(spaceBelow < menuHeight && spaceAbove > menuHeight);
|
||||
}
|
||||
}, [isOpened, menuHeight, options.length]);
|
||||
|
||||
@@ -54,34 +53,39 @@ export default function ActionsPopover({ options }: ActionsPopoverProps) {
|
||||
className="flex items-center justify-center gap-[0.139vw] size-[1.667vw] rounded-[0.556vw] text-[#CCCCCC] hover:text-[#7D7D7D] hover:bg-[#F3F3F3] active:text-[#141414] "
|
||||
onClick={() => setIsOpened(!isOpened)}
|
||||
>
|
||||
<div className="size-[1.111vw] rounded-[0.556vw]">
|
||||
<div className="2xl:size-[1.111vw] size-4 2xl:rounded-[0.556vw] rounded-2xl">
|
||||
<MoreIcon />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isOpened && (
|
||||
<div
|
||||
ref={(el) => {
|
||||
setMenuHeight(el?.offsetHeight || 0);
|
||||
}}
|
||||
className={`absolute z-10 right-0 w-[13.333vw] bg-white rounded-[1.111vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden ${
|
||||
openUpwards ? "bottom-[100%]" : "top-[100%]"
|
||||
}`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="p-[0.833vw] button-s w-full flex items-center !rounded-none !justify-start gap-[0.556vw]"
|
||||
key={option.label}
|
||||
onClick={option.onClick}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<div className="size-[1.111vw] ">{option.icon}</div>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{isOpened && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
ref={(el) => {
|
||||
setMenuHeight(el?.offsetHeight || 0);
|
||||
}}
|
||||
className={`absolute z-10 right-0 w-[13.333vw] bg-white rounded-[1.111vw] shadow-[0_4px_40px_0_rgba(15,16,17,0.1)] overflow-hidden ${
|
||||
openUpwards ? "bottom-[100%]" : "top-[100%]"
|
||||
}`}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
className="p-[0.833vw] button-s w-full flex items-center !rounded-none !justify-start gap-[0.556vw]"
|
||||
key={option.label}
|
||||
onClick={option.onClick}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
<div className="size-[1.111vw] ">{option.icon}</div>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,20 +20,25 @@ function RangeInput({
|
||||
|
||||
useEffect(() => {
|
||||
addEventListener("mouseup", () => setMouseDown(false));
|
||||
addEventListener("mousemove", handleMouseMove);
|
||||
addEventListener("touchend", () => setMouseDown(false));
|
||||
addEventListener("mousemove", handleMove);
|
||||
addEventListener("touchmove", handleMove);
|
||||
return () => {
|
||||
removeEventListener("mouseup", () => setMouseDown(false));
|
||||
removeEventListener("mousemove", handleMouseMove);
|
||||
removeEventListener("touchend", () => setMouseDown(false));
|
||||
removeEventListener("mousemove", handleMove);
|
||||
removeEventListener("touchmove", handleMove);
|
||||
};
|
||||
}, [handleMouseMove]);
|
||||
}, [handleMove]);
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
function handleMove(e: MouseEvent | TouchEvent) {
|
||||
if (mouseDown && ref.current) {
|
||||
onChange(
|
||||
Math.min(
|
||||
Math.max(
|
||||
min,
|
||||
((e.clientX - ref.current.getBoundingClientRect().left) /
|
||||
((("clientX" in e ? e.clientX : e.touches[0].clientX) -
|
||||
ref.current.getBoundingClientRect().left) /
|
||||
ref.current.clientWidth) *
|
||||
(max - min) +
|
||||
min
|
||||
@@ -44,14 +49,17 @@ function RangeInput({
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(e: React.MouseEvent<HTMLDivElement>) {
|
||||
function handleMouseDownOrTouchStart(
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) {
|
||||
e.preventDefault();
|
||||
if (ref.current) {
|
||||
onChange(
|
||||
Math.min(
|
||||
Math.max(
|
||||
min,
|
||||
((e.clientX - ref.current.getBoundingClientRect().left) /
|
||||
((("clientX" in e ? e.clientX : e.touches[0].clientX) -
|
||||
ref.current.getBoundingClientRect().left) /
|
||||
ref.current.clientWidth) *
|
||||
(max - min) +
|
||||
min
|
||||
@@ -67,7 +75,8 @@ function RangeInput({
|
||||
<div
|
||||
ref={ref}
|
||||
className="2xl:w-[21.111vw] w-[304px] 2xl:h-[0.139vw] h-[2px] relative bg-[#F0F0F0] 2xl:rounded-[0.556vw] rounded-lg cursor-grab active:cursor-grabbing"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseDown={handleMouseDownOrTouchStart}
|
||||
onTouchStart={handleMouseDownOrTouchStart}
|
||||
>
|
||||
<div
|
||||
className="bg-[#7B60F3] 2xl:rounded-[0.556vw] h-full absolute left-0 top-0 rounded-lg"
|
||||
|
||||
@@ -3,6 +3,7 @@ import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ChevronDownIcon from "../icons/ChevronDownIcon";
|
||||
import CheckIcon from "../icons/CheckIcon";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
interface SelectProps {
|
||||
options: string[];
|
||||
@@ -68,35 +69,40 @@ function Select({
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={dropDownRef}
|
||||
className="absolute left-0 2xl:top-[calc(100%+0.556vw)] top-[calc(100%+8px)] z-10 w-full bg-white 2xl:rounded-[1.111vw] rounded-2xl shadow-[0px_4px_40px_0px_rgba(0,0,0,0.05),0px_2px_2px_0px_rgba(0,0,0,0.05)] 2xl:p-[0.833vw] p-3 2xl:space-y-[0.278vw] space-y-1 overflow-auto"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
className={clsx(
|
||||
"w-full flex items-center 2xl:gap-[0.278vw] gap-1 2xl:p-[0.833vw] p-3 2xl:rounded-[0.556vw] rounded-lg text-left transition-colors hover:bg-[#F3F3F3]"
|
||||
)}
|
||||
onClick={() => {
|
||||
setSelectedOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
ref={dropDownRef}
|
||||
className="absolute left-0 2xl:top-[calc(100%+0.556vw)] top-[calc(100%+8px)] z-10 w-full bg-white 2xl:rounded-[1.111vw] rounded-2xl shadow-[0px_4px_40px_0px_rgba(0,0,0,0.05),0px_2px_2px_0px_rgba(0,0,0,0.05)] 2xl:p-[0.833vw] p-3 2xl:space-y-[0.278vw] space-y-1 overflow-auto"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
className={clsx(
|
||||
"2xl:size-[1.111vw] size-4 shrink-0 text-[#7B60F3]",
|
||||
option !== selectedOption && "opacity-0"
|
||||
"w-full flex items-center 2xl:gap-[0.278vw] gap-1 2xl:p-[0.833vw] p-3 2xl:rounded-[0.556vw] rounded-lg text-left transition-colors hover:bg-[#F3F3F3]"
|
||||
)}
|
||||
onClick={() => {
|
||||
setSelectedOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
<span className="text-s">{option}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:size-[1.111vw] size-4 shrink-0 text-[#7B60F3]",
|
||||
option !== selectedOption && "opacity-0"
|
||||
)}
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
<span className="text-s">{option}</span>
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user