diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b44006..3d0b33f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +### Changed + +- ♻️(frontend) replace custom reactions toolbar with react aria popover #985 + ## [1.8.0] - 2026-02-20 ### Changed diff --git a/src/frontend/src/features/rooms/livekit/components/controls/ReactionsToggle.tsx b/src/frontend/src/features/rooms/livekit/components/controls/ReactionsToggle.tsx index 57e47fc3..8b31f124 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/ReactionsToggle.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/ReactionsToggle.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { RiEmotionLine } from '@remixicon/react' -import { useState, useRef, useEffect } from 'react' +import { useState, useRef } from 'react' import { css } from '@/styled-system/css' import { useRoomContext } from '@livekit/components-react' import { ToggleButton, Button } from '@/primitives' @@ -12,7 +12,12 @@ import { } from '@/features/rooms/livekit/components/ReactionPortal' import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils' import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut' -import { Toolbar as RACToolbar } from 'react-aria-components' +import { + Popover as RACPopover, + Dialog, + DialogTrigger, +} from 'react-aria-components' +import { FocusScope } from '@react-aria/focus' import { Participant } from 'livekit-client' import useRateLimiter from '@/hooks/useRateLimiter' @@ -40,11 +45,11 @@ export const ReactionsToggle = () => { const instanceIdRef = useRef(0) const room = useRoomContext() - const [isVisible, setIsVisible] = useState(false) + const [isOpen, setIsOpen] = useState(false) useRegisterKeyboardShortcut({ id: 'reaction', - handler: () => setIsVisible((prev) => !prev), + handler: () => setIsOpen((prev) => !prev), }) const sendReaction = async (emoji: string) => { @@ -79,100 +84,76 @@ export const ReactionsToggle = () => { windowMs: 1000, }) - // Custom animation implementation for the emoji toolbar - // Could not use a menu and its animation, because a menu would make the toolbar inaccessible by keyboard - // animation isn't perfect - const [isRendered, setIsRendered] = useState(isVisible) - const [opacity, setOpacity] = useState(isVisible ? 1 : 0) - - useEffect(() => { - if (isVisible) { - // Show: first render, then animate in - setIsRendered(true) - // Need to delay setting opacity to ensure CSS transition works - // (using requestAnimationFrame to ensure DOM has updated) - requestAnimationFrame(() => { - requestAnimationFrame(() => { - setOpacity(1) - }) - }) - } else if (isRendered) { - // Hide: first animate out, then unrender - setOpacity(0) - - // Wait for animation to complete before removing from DOM - const timer = setTimeout(() => { - setIsRendered(false) - }, 200) // Match this to your animation duration - return () => clearTimeout(timer) - } - }, [isVisible, isRendered]) - return ( <> -
- setIsVisible(!isVisible)} - > - - - {isRendered && ( -
+ + + + + false} className={css({ - position: 'absolute', - top: -63, - left: -162, borderRadius: '8px', padding: '0.35rem', backgroundColor: 'primaryDark.50', - opacity: opacity, - transition: 'opacity 0.2s ease', + '&[data-entering]': { + animation: 'fade 200ms ease', + }, + '&[data-exiting]': { + animation: 'fade 200ms ease-in reverse', + }, })} - onTransitionEnd={() => { - if (!isVisible) { - setIsRendered(false) - } - }} > - - {Object.values(Emoji).map((emoji, index) => ( - - ))} - -
- )} + {Object.values(Emoji).map((emoji, index) => ( + + ))} +
+ + + +