PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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.2.1, at src/Agent/components/OptionsPopover.jsx

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