feat: add ProfileDropdown component with animated icons and avatar fallback#72
feat: add ProfileDropdown component with animated icons and avatar fallback#72vxnsh1 wants to merge 8 commits intoitshover:masterfrom
Conversation
…t, and default avatar
|
@vxnsh1 is attempting to deploy a commit to the itshover's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a new client-side ProfileDropdown component with animated per-item icons and accessibility features, registers it in EXAMPLE_REGISTRY, introduces five forwardRef animated SVG icon components exposing start/stop animation handles, and bumps the motion dependency. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant ProfileDropdown
participant AnimatedIcon
participant FramerMotion
participant Document
User->>ProfileDropdown: Click avatar button
ProfileDropdown->>ProfileDropdown: set isOpen = true
ProfileDropdown->>FramerMotion: play open (enter) animation
FramerMotion-->>ProfileDropdown: visible
User->>ProfileDropdown: Hover dropdown item
ProfileDropdown->>AnimatedIcon: startAnimation (if isAnimated)
AnimatedIcon-->>ProfileDropdown: animation running
User->>Document: Click outside / press Escape
Document->>ProfileDropdown: outside-click / keydown event
ProfileDropdown->>FramerMotion: play close (exit) animation
FramerMotion-->>ProfileDropdown: hidden
ProfileDropdown->>ProfileDropdown: set isOpen = false
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
Greptile OverviewGreptile SummaryThis PR adds a new Key Changes:
Critical Issues Found:
Confidence Score: 2/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant User
participant ProfileDropdown
participant Button
participant AnimatePresence
participant DropdownMenu
participant ProfileDropdownItem
participant AnimatedIcon
User->>Button: Click avatar
Button->>ProfileDropdown: Toggle isOpen state
ProfileDropdown->>AnimatePresence: Render with isOpen=true
AnimatePresence->>DropdownMenu: Animate entry (opacity, y, scale)
loop For each menu item
DropdownMenu->>ProfileDropdownItem: Render item
ProfileDropdownItem->>AnimatedIcon: Pass ref and disableHover prop
end
User->>ProfileDropdownItem: Mouse enter
ProfileDropdownItem->>AnimatedIcon: Call startAnimation()
AnimatedIcon->>AnimatedIcon: Execute animation
User->>ProfileDropdownItem: Mouse leave
ProfileDropdownItem->>AnimatedIcon: Call stopAnimation()
AnimatedIcon->>AnimatedIcon: Reset animation
User->>Document: Click outside
Document->>ProfileDropdown: mousedown event listener
ProfileDropdown->>AnimatePresence: Set isOpen=false
AnimatePresence->>DropdownMenu: Animate exit (opacity, y, scale)
DropdownMenu->>ProfileDropdown: Unmount
|
| import UserPlusIcon from "@/icons/user-plus-icon"; | ||
| import GearIcon from "@/icons/gear-icon"; | ||
| import MessageCircleIcon from "@/icons/message-circle-icon"; | ||
| import CreditCard from "@/icons/credit-card"; | ||
| import LogoutIcon from "@/icons/logout-icon"; |
There was a problem hiding this comment.
icons from @/icons/ don't support the disableHover prop used on line 52. Either import from ../ui/ (which supports disableHover) or remove the prop usage.
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/examples/profile-dropdown.tsx
Line: 7:11
Comment:
icons from `@/icons/` don't support the `disableHover` prop used on line 52. Either import from `../ui/` (which supports `disableHover`) or remove the prop usage.
How can I resolve this? If you propose a fix, please make it concise.| <img | ||
| src={avatarSrc || "/default-avatar.jpg"} | ||
| alt="User avatar" | ||
| className="h-10 w-10 rounded-full object-cover" | ||
| /> |
There was a problem hiding this comment.
using native img tag in Next.js. Consider using next/image for optimized image loading and better performance.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/examples/profile-dropdown.tsx
Line: 107:111
Comment:
using native `img` tag in Next.js. Consider using `next/image` for optimized image loading and better performance.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| <button | ||
| onClick={() => setIsOpen((prev) => !prev)} | ||
| className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2" | ||
| > | ||
| <img | ||
| src={avatarSrc || "/default-avatar.jpg"} | ||
| alt="User avatar" | ||
| className="h-10 w-10 rounded-full object-cover" | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
add aria-label and aria-expanded attributes for better accessibility.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/examples/profile-dropdown.tsx
Line: 103:112
Comment:
add `aria-label` and `aria-expanded` attributes for better accessibility.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @components/examples/profile-dropdown.tsx:
- Around line 103-112: The dropdown toggle currently only supports mouse clicks;
add keyboard handling so Escape closes the menu and ArrowUp/ArrowDown navigate
items: introduce a focusIndex state and an array/ref of item refs (e.g.,
itemRefs), add a handleKeyDown function attached to the menu container or the
toggle button that listens for 'Escape' to call setIsOpen(false) and focus the
toggle button, and listens for 'ArrowDown'/'ArrowUp' to increment/decrement
focusIndex and call itemRefs[focusIndex]?.focus(); ensure focusIndex resets when
the menu opens/closes and that menu items (the interactive elements rendered
inside the dropdown) have tabIndex and ref wired to itemRefs so keyboard
navigation works smoothly.
🧹 Nitpick comments (4)
components/examples/profile-dropdown.tsx (4)
57-59: Consider making badge styling configurable.The badge uses hardcoded purple colors (
bg-purple-200,text-purple-800). Consider accepting badge styling as a prop to make the component more flexible.♻️ Proposed enhancement
interface ProfileDropdownItemProps { icon: React.ComponentType< AnimatedIconProps & React.RefAttributes<AnimatedIconHandle> >; label: string; href: string; badge?: string; + badgeClassName?: string; isAnimated?: boolean; } const ProfileDropdownItem = ({ icon: Icon, label, href, badge, + badgeClassName = "rounded-full bg-purple-200 px-2 py-0.5 text-xs font-semibold text-purple-800", isAnimated = true, }: ProfileDropdownItemProps) => { // ... rest of component {badge && ( - <span className="rounded-full bg-purple-200 px-2 py-0.5 text-xs font-semibold text-purple-800"> + <span className={badgeClassName}> {badge} </span> )}
77-89: Consider making menu items configurable.The menu items are hardcoded within the component. For better reusability, consider accepting items as a prop with sensible defaults.
♻️ Proposed enhancement
interface ProfileDropdownProps { avatarSrc?: string; isAnimated?: boolean; + items?: ProfileDropdownItemProps[]; } const ProfileDropdown = ({ avatarSrc, isAnimated = true, + items: customItems, }: ProfileDropdownProps) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef<HTMLDivElement>(null); - const items: ProfileDropdownItemProps[] = [ + const defaultItems: ProfileDropdownItemProps[] = [ { icon: UserPlusIcon, label: "Profile", href: "#", isAnimated }, { icon: MessageCircleIcon, label: "Community", href: "#", isAnimated }, { icon: CreditCard, label: "Subscription", href: "#", badge: "PRO", isAnimated, }, { icon: GearIcon, label: "Settings", href: "#", isAnimated }, { icon: LogoutIcon, label: "Sign Out", href: "#", isAnimated }, ]; + const items = customItems || defaultItems;
107-111: Add error handling for avatar image loading.Consider adding an
onErrorhandler to gracefully handle cases where the avatar image fails to load.♻️ Proposed enhancement
+ const [avatarError, setAvatarError] = useState(false); + return ( <div ref={dropdownRef} className="relative inline-block"> <button onClick={() => setIsOpen((prev) => !prev)} className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2" > - <img - src={avatarSrc || "/default-avatar.jpg"} - alt="User avatar" - className="h-10 w-10 rounded-full object-cover" - /> + {!avatarError ? ( + <img + src={avatarSrc || "/default-avatar.jpg"} + alt="User avatar" + className="h-10 w-10 rounded-full object-cover" + onError={() => setAvatarError(true)} + /> + ) : ( + <div className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-200"> + <span className="text-sm font-semibold text-gray-600">?</span> + </div> + )} </button>
123-125: Consider closing dropdown on item click.For better UX, the dropdown should close when a user clicks on a menu item. Currently, it remains open after navigation.
♻️ Proposed enhancement
> {items.map((item) => ( - <ProfileDropdownItem key={item.label} {...item} /> + <ProfileDropdownItem + key={item.label} + {...item} + onClick={() => setIsOpen(false)} + /> ))} </motion.div>And update
ProfileDropdownItemto accept and use the onClick:interface ProfileDropdownItemProps { icon: React.ComponentType< AnimatedIconProps & React.RefAttributes<AnimatedIconHandle> >; label: string; href: string; badge?: string; isAnimated?: boolean; + onClick?: () => void; } const ProfileDropdownItem = ({ icon: Icon, label, href, badge, isAnimated = true, + onClick, }: ProfileDropdownItemProps) => { // ... existing code ... return ( <Link href={href} className="group hover:bg-accent hover:text-accent-foreground flex items-center justify-between gap-3 rounded-md px-4 py-2 transition-colors" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} + onClick={onClick} >
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
public/default-avatar.jpgis excluded by!**/*.jpg
📒 Files selected for processing (2)
components/examples/profile-dropdown.tsxlib/examples.ts
🔇 Additional comments (5)
lib/examples.ts (1)
6-6: LGTM! Registry entry is well-structured.The import and registry entry follow the existing pattern correctly. The metadata (description, tags) accurately reflects the component's functionality.
Also applies to: 47-56
components/examples/profile-dropdown.tsx (4)
91-99: LGTM! Click-outside handler is correctly implemented.The event listener properly handles clicks outside the dropdown and cleans up on unmount.
107-111: No action needed — default avatar asset exists.The
/default-avatar.jpgfile is present in thepublicdirectory (8273 bytes). The fallback avatar path is valid and will not result in a broken image.
5-5: The type imports from../ui/typesare correctly exported. BothAnimatedIconHandleandAnimatedIconPropsare defined incomponents/ui/types.tsand the import path is valid.
7-11: All icon imports are correctly implemented. Each icon component properly exports usingforwardRefwith theAnimatedIconPropsinterface andAnimatedIconHandleref support, and they are correctly used in the component.
| <button | ||
| onClick={() => setIsOpen((prev) => !prev)} | ||
| className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2" | ||
| > | ||
| <img | ||
| src={avatarSrc || "/default-avatar.jpg"} | ||
| alt="User avatar" | ||
| className="h-10 w-10 rounded-full object-cover" | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
Add keyboard accessibility support.
The dropdown lacks keyboard navigation support. Users should be able to close the dropdown with the Escape key and navigate items with arrow keys for better accessibility.
♻️ Proposed enhancement
useEffect(() => {
const handler = (e: MouseEvent) => {
if (!dropdownRef.current?.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape" && isOpen) {
+ setIsOpen(false);
+ }
+ };
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [isOpen]);
return (
<div ref={dropdownRef} className="relative inline-block">
<button
onClick={() => setIsOpen((prev) => !prev)}
+ aria-expanded={isOpen}
+ aria-haspopup="true"
+ aria-label="Open profile menu"
className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2"
>🤖 Prompt for AI Agents
In @components/examples/profile-dropdown.tsx around lines 103 - 112, The
dropdown toggle currently only supports mouse clicks; add keyboard handling so
Escape closes the menu and ArrowUp/ArrowDown navigate items: introduce a
focusIndex state and an array/ref of item refs (e.g., itemRefs), add a
handleKeyDown function attached to the menu container or the toggle button that
listens for 'Escape' to call setIsOpen(false) and focus the toggle button, and
listens for 'ArrowDown'/'ArrowUp' to increment/decrement focusIndex and call
itemRefs[focusIndex]?.focus(); ensure focusIndex resets when the menu
opens/closes and that menu items (the interactive elements rendered inside the
dropdown) have tabIndex and ref wired to itemRefs so keyboard navigation works
smoothly.
|
Nice, can you hover over the icons(not text) while keeping the animation toggle off, and share the video as well? |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
components/examples/profile-dropdown.tsx (4)
33-39: Wrap handlers in useCallback for performance.The
handleMouseEnterandhandleMouseLeavefunctions are recreated on every render. While this won't cause functional issues, wrapping them inuseCallbackwould prevent unnecessary re-creations.♻️ Optimize handlers with useCallback
+import React, { useState, useRef, useEffect, useCallback } from "react"; ... const ref = useRef<AnimatedIconHandle>(null); - const handleMouseEnter = () => { + const handleMouseEnter = useCallback(() => { if (isAnimated) ref.current?.startAnimation(); - }; + }, [isAnimated]); - const handleMouseLeave = () => { + const handleMouseLeave = useCallback(() => { if (isAnimated) ref.current?.stopAnimation(); - }; + }, [isAnimated]);
41-43: Consider consolidating animation control logic.The
useEffectcallsstopAnimation()wheneverisAnimatedbecomes false, which is correct. However, consider whether starting the animation whenisAnimatedbecomes true again is desired behavior, as currently it only stops but never auto-starts.
57-61: Consider making badge styling configurable.The badge uses hardcoded purple colors (
bg-purple-200,text-purple-800). Consider accepting badge color variants as a prop if multiple badge types will be needed in the future.
78-90: Memoize the items array to prevent unnecessary re-renders.The
itemsarray is recreated on every render, which can cause unnecessary re-renders of child components. Since it only depends onisAnimated, wrap it inuseMemo.♻️ Memoize items array
+import React, { useState, useRef, useEffect, useMemo } from "react"; ... - const items: ProfileDropdownItemProps[] = [ + const items: ProfileDropdownItemProps[] = useMemo(() => [ { icon: UserPlusIcon, label: "Profile", href: "#", isAnimated }, { icon: MessageCircleIcon, label: "Community", href: "#", isAnimated }, { icon: CreditCard, label: "Subscription", href: "#", badge: "PRO", isAnimated, }, { icon: GearIcon, label: "Settings", href: "#", isAnimated }, { icon: LogoutIcon, label: "Sign Out", href: "#", isAnimated }, - ]; + ], [isAnimated]);
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
components/examples/profile-dropdown.tsx
🔇 Additional comments (5)
components/examples/profile-dropdown.tsx (5)
1-22: LGTM!The imports and type definitions are well-structured. The use of proper TypeScript types for animated icons and the "use client" directive for Next.js 15 client-side interactivity is appropriate.
79-89: Placeholder hrefs should be replaced with actual routes.All menu items use
"#"as the href value. These should be updated with actual route paths or documented as intentional placeholders for the example component.Do these placeholder hrefs need to be updated, or are they intentional for this example component?
92-108: Event listener cleanup is correct.Both
useEffecthooks properly clean up their event listeners, and the empty dependency arrays are appropriate since the handlers only set state and don't depend on external values.
112-125: Good accessibility implementation.The avatar button includes proper ARIA attributes (
aria-labelandaria-expanded) for screen readers, and the Image component is correctly configured with alt text.
127-141: Smooth animation implementation.The Framer Motion integration with
AnimatePresenceprovides clean enter/exit animations for the dropdown. The animation parameters (duration, scale, opacity) create a polished user experience.
Just to clarify when the animation toggle is off the global animation works as expected, but hovering over the icons still triggers the animation. Is this intended behavior or should animations be completely off? |
|
You can turn it off check this PR - 6e1239d |
vxnsh1
left a comment
There was a problem hiding this comment.
Small thing I ran into: ui/ icons have a disableHover prop to turn off hover animations, but the icons from @/icons don’t. Because of that mismatch, I handled stopping hover animations inside the dropdown itself. If you’d rather keep everything using ui/ icons only, I can switch it.
Profile.Dropdown.Fix.Demo.mp4
vxnsh1
left a comment
There was a problem hiding this comment.
Updated ProfileDropdown to use ui icons instead of icons/. Passed disableHover to each icon so that when animation is turned off, hovering over the icon will not trigger the animation.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (6)
components/examples/profile-dropdown.tsx (2)
6-6: Inconsistent motion import path.This file imports from
"framer-motion"while the icon components (e.g.,message-circle-icon.tsx,gear-icon.tsx) import from"motion/react". For consistency and to ensure the same module instance is used, consider aligning the import path.Suggested fix
-import { motion, AnimatePresence } from "framer-motion"; +import { motion, AnimatePresence } from "motion/react";
115-128: Addaria-haspopupfor improved accessibility.The button has good accessibility attributes, but adding
aria-haspopup="menu"would better communicate the dropdown's purpose to screen readers.Suggested fix
<button onClick={() => setIsOpen((prev) => !prev)} aria-label="Toggle profile menu" aria-expanded={isOpen} + aria-haspopup="menu" className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2" >components/ui/message-circle-icon.tsx (1)
42-48: Remove unused functions.
handleHoverStartandhandleHoverEndare defined but never used. TheonHoverStartandonHoverEndprops directly referencestartandstop.Suggested fix
- const handleHoverStart = () => { - start(); - }; - - const handleHoverEnd = () => { - stop(); - }; - return (components/ui/gear-icon.tsx (1)
39-45: Remove unused functions.
handleHoverStartandhandleHoverEndare defined but never used, similar tomessage-circle-icon.tsx. The hover handlers directly referencestartandstop.Suggested fix
- const handleHoverStart = () => { - start(); - }; - - const handleHoverEnd = () => { - stop(); - }; - return (components/ui/credit-card.tsx (2)
12-52: WrapstartandstopinuseCallbackfor consistency and stability.Unlike
LogoutIconwhich correctly usesuseCallback, these functions are recreated on every render. This creates unstable references for the imperative handle and is inconsistent with the other animated icon components in this PR.Suggested fix
+import { forwardRef, useImperativeHandle, useCallback } from "react"; -import { forwardRef, useImperativeHandle } from "react"; import type { AnimatedIconHandle, AnimatedIconProps } from "./types"; import { motion, useAnimate } from "motion/react";Then wrap the functions:
- const start = async () => { + const start = useCallback(async () => { animate( ".card-body", // ... animation config ); // ... rest of animation - }; + }, [animate]); - const stop = () => { + const stop = useCallback(() => { animate(".card-body", { rotate: 0, scale: 1 }, { duration: 0.2 }); // ... rest - }; + }, [animate]);
61-67: Remove unused handler functions.
handleHoverStartandhandleHoverEndare defined but never used—the JSX directly referencesstartandstopon lines 72-73. Remove this dead code.Suggested fix
- const handleHoverStart = () => { - start(); - }; - - const handleHoverEnd = () => { - stop(); - }; - return (
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
components/examples/profile-dropdown.tsxcomponents/ui/credit-card.tsxcomponents/ui/gear-icon.tsxcomponents/ui/logout-icon.tsxcomponents/ui/message-circle-icon.tsxcomponents/ui/user-plus-icon.tsxpackage.json
🔇 Additional comments (8)
components/examples/profile-dropdown.tsx (3)
37-81: Well-implemented animation control.The component correctly implements the animation toggle behavior discussed in the PR:
- Manual
onMouseEnter/onMouseLeavehandlers checkisAnimatedbefore triggering animations- The
useEffectensures running animations stop whenisAnimatedchanges tofalse- The
disableHoverprop on the Icon provides an additional layer of controlThis belt-and-suspenders approach ensures animations are reliably disabled when the toggle is off.
130-147: LGTM!The dropdown animation using
AnimatePresenceandmotion.divis well-structured with appropriate transition values.
121-127: Default avatar exists in public directory.The fallback image
/default-avatar.jpgis present atpublic/default-avatar.jpgand will load correctly without 404 errors.components/ui/message-circle-icon.tsx (1)
5-40: LGTM!The animation logic with path drawing and scale pulse effects is well-implemented. The imperative handle correctly exposes
startAnimationandstopAnimationmethods.components/ui/user-plus-icon.tsx (1)
5-112: LGTM!Well-structured animated icon with proper use of
useCallbackfor memoized animation handlers. The two-part animation (user avatar bounce + plus sign rotation) creates a cohesive effect, and thedisableHoverprop is correctly implemented.components/ui/gear-icon.tsx (1)
47-82: LGTM!The gear animation with rotating parts and the
transformOrigin/transformBoxsetup for proper pivot behavior is well-implemented.package.json (1)
31-31: Motion version bump is stable and safe.The update to
^12.25.0is the official latest release with no known vulnerabilities or breaking changes. This aligns well with the new animated icon components.components/ui/logout-icon.tsx (1)
1-82: LGTM!The component follows the established animated icon pattern correctly:
- Uses
useCallbackfor memoized animation handlers- Properly exposes
startAnimation/stopAnimationvia imperative handle- Correctly disables hover animations when
disableHoveris true- Animation sequencing (arrow → door) provides a nice visual effect
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @components/examples/profile-dropdown.tsx:
- Around line 64-80: The Icon's internal hover handlers can double-trigger
animations because both Link's handlers (handleMouseEnter/handleMouseLeave) and
Icon call start/stop when isAnimated is true; update the Icon usage in
profile-dropdown.tsx to always pass disableHover={true} (i.e., Icon ...
disableHover={true}) and remove reliance on Icon's hover behavior so the parent
Link's onMouseEnter/onMouseLeave exclusively control animations; apply the same
change to the similar occurrence in x-sidebar.tsx to prevent concurrent
animation calls.
🧹 Nitpick comments (4)
components/ui/credit-card.tsx (1)
67-73: Remove unused handler functions.
handleHoverStartandhandleHoverEndare defined but never used—Lines 78-79 passstartandstopdirectly toonHoverStart/onHoverEnd.🧹 Proposed cleanup
- const handleHoverStart = () => { - start(); - }; - - const handleHoverEnd = () => { - stop(); - }; - return (components/ui/message-circle-icon.tsx (1)
48-54: Remove unused handler functions.Same as in
credit-card.tsx:handleHoverStartandhandleHoverEndare defined but never called—Lines 59-60 passstartandstopdirectly.🧹 Proposed cleanup
- const handleHoverStart = () => { - start(); - }; - - const handleHoverEnd = () => { - stop(); - }; - return (components/examples/profile-dropdown.tsx (2)
6-6: Import inconsistency: usemotion/reactfor consistency.Other icon components in this PR import from
motion/react, but this file usesframer-motion. For consistency across the codebase, consider using the same import path.♻️ Suggested change
-import { motion, AnimatePresence } from "framer-motion"; +import { motion, AnimatePresence } from "motion/react";
139-145: Consider using a more stable key.Using
item.labelas a key works if labels are unique, but adding an explicitidfield to items would be more robust against future label changes or duplicates.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
components/examples/profile-dropdown.tsxcomponents/ui/credit-card.tsxcomponents/ui/gear-icon.tsxcomponents/ui/logout-icon.tsxcomponents/ui/message-circle-icon.tsxcomponents/ui/user-plus-icon.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- components/ui/gear-icon.tsx
- components/ui/logout-icon.tsx
🔇 Additional comments (5)
components/ui/credit-card.tsx (1)
75-110: LGTM!The component correctly implements the
AnimatedIconHandleinterface withforwardRefanduseImperativeHandle. The hover behavior respectsdisableHover, and the SVG animation setup with multiple animated paths is well-structured.components/ui/message-circle-icon.tsx (1)
56-82: LGTM!The component correctly implements the animated icon pattern with
pathLengthanimation. The initial state is properly set, and thedisableHoverprop is respected.components/examples/profile-dropdown.tsx (1)
95-111: LGTM on accessibility and event handling.The dropdown correctly handles Escape key and outside clicks. ARIA attributes (
aria-label,aria-expanded) are properly set on the button.components/ui/user-plus-icon.tsx (2)
18-44: Good use ofuseCallbackfor animation handlers.This component correctly wraps
startandstopinuseCallbackwith[animate]as the dependency. This is cleaner than the other icon components that don't memoize these functions—consider applying this pattern consistently across all animated icons.
77-113: LGTM!Clean implementation with no dead code. The SVG structure with separate animated groups for
user-avatarandplus-signis well-organized, andtransformOriginis properly set for each animation target.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
components/examples/profile-dropdown.tsx (3)
115-128: Consider addingaria-haspopupfor better accessibility.The button has good accessibility attributes (
aria-label,aria-expanded), but addingaria-haspopup="menu"would improve screen reader experience by indicating this button opens a menu.♿ Suggested improvement
<button onClick={() => setIsOpen((prev) => !prev)} aria-label="Toggle profile menu" aria-expanded={isOpen} + aria-haspopup="menu" className="hover:ring-accent flex h-10 w-10 items-center justify-center overflow-hidden rounded-full transition-all hover:ring-2" >
103-111: Consider attaching outside click listener only when dropdown is open.The current implementation always listens for
mousedownevents even when the dropdown is closed. For a single dropdown this is negligible, but you could optimize by conditionally attaching the listener.♻️ Optional optimization
useEffect(() => { + if (!isOpen) return; + const handler = (e: MouseEvent) => { if (!dropdownRef.current?.contains(e.target as Node)) { setIsOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); - }, []); + }, [isOpen]);
132-146: Consider addingrole="menu"for accessibility compliance.For screen readers to properly announce this as a menu, the dropdown container should have
role="menu". This pairs with thearia-haspopup="menu"suggestion on the button.♿ Suggested improvement
<motion.div initial={{ opacity: 0, y: -10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -10, scale: 0.95 }} transition={{ duration: 0.2 }} + role="menu" className="border-border bg-background absolute right-0 z-50 mt-2 min-w-[220px] overflow-hidden rounded-xl border shadow-lg" >
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
components/examples/profile-dropdown.tsx
🔇 Additional comments (5)
components/examples/profile-dropdown.tsx (5)
1-12: LGTM!The imports are well-organized with appropriate separation of React hooks, Next.js components, animation utilities, and custom icons.
14-25: LGTM!Static items array correctly defined outside the component to avoid recreation on each render. Labels are unique, making them suitable for use as keys.
27-35: LGTM!The interface properly types the animated icon component with
RefAttributes<AnimatedIconHandle>to support the imperative animation control via refs.
37-81: LGTM!The implementation correctly addresses the double hover animation issue mentioned in the PR:
disableHoverprop on the Icon prevents the icon's built-in hover animation.- Parent Link's mouse events control animation imperatively via ref.
- The
useEffectproperly stops animations whenisAnimatedbecomes false.This aligns with the referenced commit pattern for disabling icon hover when animations are off.
88-151: Well-structured component with proper state management.The ProfileDropdown correctly:
- Manages open/close state with keyboard (Escape) and outside click handling.
- Passes
isAnimatedprop down to all items, respecting the global animation toggle.- Uses Framer Motion's AnimatePresence for smooth enter/exit animations.
This implementation aligns with the PR objective and the commit fix for preventing double hover animation triggers.
|
Accepted the incoming change for examples.ts to avoid modifying the global examples registry. My ProfileDropdown component remains in components/examples, so it can be added to the showcase later if needed. |
|
@vxnsh1 i have left some comments |
I’ve addressed the previous review points (ui icons, disableHover, formatting, etc.).
Just want to confirm before pushing another commit. |
Add an interactive profile dropdown with motion effects and avatar fallback.
Profile.Dropdown.Demo.mp4
Summary by CodeRabbit
New Features
Examples
Chores
✏️ Tip: You can customize this high-level summary in your review settings.