| 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 |
|