PluginProbe
Extendify / 3.0.5
Extendify v3.0.5
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / components / OptionsPopover.jsx

OptionsPopover.jsx in Extendify 3.0.5, at src/Agent/components/OptionsPopover.jsx

188 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { usePortal } from '@agent/hooks/usePortal';
2 import { isChangeSiteDesignWorkflowAvailable } from '@agent/lib/util';
3 import animationWorkflow from '@agent/workflows/theme/change-animation';
4 import changeSiteDesignWorkflow from '@agent/workflows/theme/change-site-design';
5 import vibesWorkflow from '@agent/workflows/theme/change-site-vibes';
6 import fontsWorkflow from '@agent/workflows/theme/change-theme-fonts-variation';
7 import variationWorkflow from '@agent/workflows/theme/change-theme-variation';
8 import { createPortal, useEffect, useRef, useState } from '@wordpress/element';
9 import { __, isRTL } from '@wordpress/i18n';
10 import { Icon, moreVertical } from '@wordpress/icons';
11 import { AnimatePresence, motion } from 'framer-motion';
12
13 export const OptionsPopover = () => {
14 const popoutNode = usePortal('extendify-agent-border-frame-mount');
15 const buttonRef = useRef(null);
16 const firstButtonRef = useRef(null);
17 const [topRight, setTopRight] = useState(null);
18
19 const onOpen = () => {
20 if (topRight) return setTopRight(null);
21 const rect = buttonRef.current.getBoundingClientRect();
22 setTopRight({
23 top: rect.bottom + 4,
24 right: window.innerWidth - rect.right,
25 left: rect.left,
26 });
27 };
28 const onClose = () => {
29 setTopRight(null);
30 buttonRef.current?.focus();
31 };
32
33 useEffect(() => {
34 if (!topRight || !firstButtonRef.current) return;
35 firstButtonRef.current.focus();
36 }, [topRight]);
37
38 useEffect(() => {
39 // click away
40 const handle = (e) => {
41 if (buttonRef.current.contains(e.target)) return;
42 if (popoutNode?.contains(e.target)) return;
43 setTopRight(null);
44 };
45 window.addEventListener('click', handle);
46 return () => window.removeEventListener('click', handle);
47 }, [popoutNode]);
48
49 if (!popoutNode) return null;
50 return (
51 <>
52 {createPortal(
53 topRight ? (
54 <AnimatePresence>
55 <Popover
56 position={topRight}
57 onClose={onClose}
58 firstItemRef={firstButtonRef}
59 />
60 </AnimatePresence>
61 ) : null,
62 popoutNode,
63 )}
64 <button
65 ref={buttonRef}
66 type="button"
67 className="relative z-10 flex justify-center h-6 w-6 items-center border-0 bg-banner-main text-banner-text outline-hidden ring-design-main focus:shadow-none focus:outline-hidden focus-visible:outline-design-main focus:ring-2 hover:opacity-80 rounded-sm"
68 onClick={onOpen}
69 aria-expanded={topRight ? 'true' : 'false'}
70 aria-haspopup="true"
71 >
72 <Icon
73 className="pointer-events-none fill-current leading-none"
74 icon={moreVertical}
75 size={18}
76 />
77 <span className="sr-only">
78 {__('View more options', 'extendify-local')}
79 </span>
80 </button>
81 </>
82 );
83 };
84
85 const buttons = [
86 {
87 name: fontsWorkflow.example.text,
88 available: fontsWorkflow.available,
89 icon: fontsWorkflow.icon,
90 },
91 {
92 name: variationWorkflow.example.text,
93 available: variationWorkflow.available,
94 icon: variationWorkflow.icon,
95 },
96 {
97 name: vibesWorkflow.example.text,
98 available: vibesWorkflow.available,
99 icon: vibesWorkflow.icon,
100 },
101 {
102 name: animationWorkflow.example.text,
103 available: animationWorkflow.available,
104 icon: animationWorkflow.icon,
105 },
106 ...(isChangeSiteDesignWorkflowAvailable()
107 ? [
108 {
109 name: changeSiteDesignWorkflow.example.text,
110 available: changeSiteDesignWorkflow.available,
111 onSelect: changeSiteDesignWorkflow.onSelect,
112 icon: changeSiteDesignWorkflow.icon,
113 },
114 ]
115 : []),
116 ];
117
118 const Popover = ({ position, onClose, firstItemRef }) => {
119 useEffect(() => {
120 const handle = (e) => {
121 if (e.key === 'Escape') {
122 e.preventDefault();
123 onClose();
124 }
125 };
126 window.addEventListener('keydown', handle);
127 return () => window.removeEventListener('keydown', handle);
128 }, [onClose]);
129
130 return (
131 <motion.div
132 className="fixed w-fit whitespace-nowrap rounded-md bg-white shadow-xl py-2 z-max flex flex-col items-start justify-center gap-1 focus:outline-none border border-gray-300"
133 style={{
134 top: position.top,
135 ...(isRTL() ? { left: position.left } : { right: position.right }),
136 }}
137 initial={{ opacity: 0, scale: 0.95 }}
138 animate={{ opacity: 1, scale: 1 }}
139 exit={{ opacity: 0, scale: 0.95 }}
140 transition={{ duration: 0.2 }}
141 onKeyDown={(e) => {
142 e.stopPropagation();
143 e.preventDefault();
144 if (e.key === 'Escape') return onClose();
145 const curr = document.activeElement;
146 if (e.key === 'ArrowDown' || (e.key === 'Tab' && !e.shiftKey)) {
147 if (curr.nextSibling) {
148 return curr.nextSibling.focus();
149 }
150 return firstItemRef.current.focus();
151 }
152 if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {
153 if (curr.previousSibling) {
154 return curr.previousSibling.focus();
155 }
156 return curr.parentNode.lastChild.focus();
157 }
158 if (e.key === 'Enter') {
159 return curr.click();
160 }
161 }}
162 >
163 {buttons
164 .filter(({ available }) => available())
165 .map(({ name, icon }, i) => (
166 <button
167 key={name}
168 type="button"
169 className="w-full inline-flex gap-0.5 items-center px-4 py-2 text-sm text-gray-900 text-left hover:bg-gray-100 focus:ring-2 focus:ring-design-main focus:outline-none capitalize"
170 ref={i === 0 ? firstItemRef : null}
171 role="menuitem"
172 onClick={() => {
173 window.dispatchEvent(
174 new CustomEvent('extendify-agent:chat-submit', {
175 detail: { message: name },
176 }),
177 );
178 onClose();
179 }}
180 >
181 {icon ? <Icon icon={icon} /> : null}
182 {name}
183 </button>
184 ))}
185 </motion.div>
186 );
187 };
188