It pops up on command, and closes easily with a click outside or on a close button.
1'use client';
2import useClickOutside from '@/hooks/useClickOutside';
3import { AnimatePresence, MotionConfig, motion } from 'motion/react';
4import { ArrowLeftIcon } from 'lucide-react';
5import { useRef, useState, useEffect, useId } from 'react';
6
7const TRANSITION = {
8 type: 'spring',
9 bounce: 0.05,
10 duration: 0.3,
11};
12
13export default function Popover() {
14 const uniqueId = useId();
15 const formContainerRef = useRef<HTMLDivElement>(null);
16 const [isOpen, setIsOpen] = useState(false);
17 const [note, setNote] = useState<null | string>(null);
18
19 const openMenu = () => {
20 setIsOpen(true);
21 };
22
23 const closeMenu = () => {
24 setIsOpen(false);
25 setNote(null);
26 };
27
28 useClickOutside(formContainerRef, () => {
29 closeMenu();
30 });
31
32 useEffect(() => {
33 const handleKeyDown = (event: KeyboardEvent) => {
34 if (event.key === 'Escape') {
35 closeMenu();
36 }
37 };
38
39 document.addEventListener('keydown', handleKeyDown);
40
41 return () => {
42 document.removeEventListener('keydown', handleKeyDown);
43 };
44 }, []);
45
46 return (
47 <MotionConfig transition={TRANSITION}>
48 <div className='relative flex items-center justify-center'>
49 <motion.button
50 key='button'
51 layoutId={`popover-${uniqueId}`}
52 className='flex h-9 items-center border border-zinc-950/10 bg-white px-3 text-zinc-950 dark:border-zinc-50/10 dark:bg-zinc-700 dark:text-zinc-50'
53 style={{
54 borderRadius: 8,
55 }}
56 onClick={openMenu}
57 >
58 <motion.span
59 layoutId={`popover-label-${uniqueId}`}
60 className='text-sm'
61 >
62 Add Note
63 </motion.span>
64 </motion.button>
65
66 <AnimatePresence>
67 {isOpen && (
68 <motion.div
69 ref={formContainerRef}
70 layoutId={`popover-${uniqueId}`}
71 className='absolute h-[200px] w-[364px] overflow-hidden border border-zinc-950/10 bg-white outline-hidden dark:bg-zinc-700'
72 style={{
73 borderRadius: 12,
74 }}
75 >
76 <form
77 className='flex h-full flex-col'
78 onSubmit={(e) => {
79 e.preventDefault();
80 }}
81 >
82 <motion.span
83 layoutId={`popover-label-${uniqueId}`}
84 aria-hidden='true'
85 style={{
86 opacity: note ? 0 : 1,
87 }}
88 className='absolute left-4 top-3 select-none text-sm text-zinc-500 dark:text-zinc-400'
89 >
90 Add Note
91 </motion.span>
92 <textarea
93 className='h-full w-full resize-none rounded-md bg-transparent px-4 py-3 text-sm outline-hidden'
94 autoFocus
95 onChange={(e) => setNote(e.target.value)}
96 />
97 <div key='close' className='flex justify-between px-4 py-3'>
98 <button
99 type='button'
100 className='flex items-center'
101 onClick={closeMenu}
102 aria-label='Close popover'
103 >
104 <ArrowLeftIcon
105 size={16}
106 className='text-zinc-900 dark:text-zinc-100'
107 />
108 </button>
109 <button
110 className='relative ml-1 flex h-8 shrink-0 scale-100 select-none appearance-none items-center justify-center rounded-lg border border-zinc-950/10 bg-transparent px-2 text-sm text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-800 focus-visible:ring-2 active:scale-[0.98] dark:border-zinc-50/10 dark:text-zinc-50 dark:hover:bg-zinc-800'
111 type='submit'
112 aria-label='Submit note'
113 onClick={() => {
114 closeMenu();
115 }}
116 >
117 Submit Note
118 </button>
119 </div>
120 </form>
121 </motion.div>
122 )}
123 </AnimatePresence>
124 </div>
125 </MotionConfig>
126 );
127}
128