PluginProbe
Extendify / 3.0.4
Extendify v3.0.4
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 / layouts / SidebarLayout.jsx

SidebarLayout.jsx in Extendify 3.0.4, at src/Agent/components/layouts/SidebarLayout.jsx

269 lines 7.3 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 { useGlobalStore } from '@agent/state/global';
3 import { createPortal, useEffect, useRef } from '@wordpress/element';
4 import { __ } from '@wordpress/i18n';
5 import { close, Icon } from '@wordpress/icons';
6 import { motion } from 'framer-motion';
7 import { OptionsPopover } from '../OptionsPopover';
8
9 const SIDEBAR_WIDTH = 384; // 96 * 4 (w-96)
10 const FRAME_WIDTH = 8; // border-8
11 const ANIMATE_TIME = 300;
12
13 export const SidebarLayout = ({ children }) => {
14 const mountNode = usePortal('extendify-agent-sidebar-mount');
15 const frameNode = usePortal('extendify-agent-border-frame-mount');
16 const { open, setOpen } = useGlobalStore();
17 useLayoutShift(open);
18
19 const closeAgent = () => {
20 setOpen(false);
21 window.dispatchEvent(new CustomEvent('extendify-agent:closed-button'));
22 };
23
24 useEffect(() => {
25 if (open) return;
26 if (!mountNode?.contains(document.activeElement)) return;
27 document.activeElement?.blur();
28 }, [open]);
29
30 if (!mountNode) return null;
31
32 // A border that sits around the entire browser to look like the sidebar is inside it
33 const frameAnim = {
34 open: {
35 top: FRAME_WIDTH,
36 right: FRAME_WIDTH,
37 bottom: FRAME_WIDTH,
38 left: SIDEBAR_WIDTH,
39 boxShadow: '#e0e0e0 0px 0px 0px 9999px',
40 borderRadius: '1rem',
41 },
42 closed: {
43 inset: 0,
44 boxShadow: '0 0 0 0 #fff',
45 borderRadius: 0,
46 },
47 };
48
49 const frameBar = frameNode
50 ? createPortal(
51 <div className="fixed inset-0 pointer-events-none z-high">
52 <motion.div
53 className="absolute overflow-hidden"
54 initial={false}
55 animate={open ? 'open' : 'closed'}
56 variants={frameAnim}
57 transition={{ duration: ANIMATE_TIME / 1000, ease: 'easeInOut' }}
58 />
59 <motion.div
60 className="absolute rounded-2xl shadow-xl"
61 style={{
62 top: FRAME_WIDTH,
63 right: FRAME_WIDTH,
64 bottom: FRAME_WIDTH,
65 left: SIDEBAR_WIDTH,
66 }}
67 initial={false}
68 animate={{ opacity: open ? 1 : 0 }}
69 transition={{
70 duration: 0.15,
71 delay: open ? ANIMATE_TIME / 1000 : 0,
72 }}
73 />
74 </div>,
75 frameNode,
76 )
77 : null;
78
79 const sidebar = mountNode
80 ? createPortal(
81 <motion.div
82 style={{ width: SIDEBAR_WIDTH }}
83 className=" fixed top-0 bottom-0 left-0 w-96 flex-col z-higher border-transparent border-8"
84 id="extendify-agent-sidebar"
85 initial={false}
86 inert={open ? undefined : ''}
87 animate={{ x: open ? 0 : -SIDEBAR_WIDTH }}
88 transition={{ duration: ANIMATE_TIME / 1000, ease: 'easeInOut' }}
89 >
90 <div className="h-full flex flex-col shadow-lg rounded-2xl overflow-hidden bg-white">
91 <div className="group flex shrink-0 items-center justify-between overflow-hidden bg-banner-main text-banner-text">
92 <div className="flex h-full grow items-center justify-between gap-1 p-0 py-2.5">
93 <div className="flex h-5 px-4 max-w-36 overflow-hidden">
94 <img
95 className="max-h-full max-w-full object-contain"
96 src={window.extSharedData.partnerLogo}
97 alt={window.extSharedData.partnerName}
98 />
99 </div>
100 </div>
101 <div className="flex gap-1 h-full items-center p-2">
102 <OptionsPopover />
103 <button
104 type="button"
105 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"
106 onClick={closeAgent}
107 >
108 <Icon
109 className="pointer-events-none fill-current leading-none"
110 icon={close}
111 size={18}
112 />
113 <span className="sr-only">
114 {__('Close window', 'extendify-local')}
115 </span>
116 </button>
117 </div>
118 </div>
119 {open ? children : null}
120 </div>
121 </motion.div>,
122 mountNode,
123 )
124 : null;
125
126 return (
127 <>
128 {frameBar}
129 {sidebar}
130 </>
131 );
132 };
133
134 export const useLayoutShift = (open) => {
135 const ease = 'ease-in-out';
136 const t = (props) =>
137 props.map((p) => `${p} ${ANIMATE_TIME}ms ${ease}`).join(', ');
138 const firstRun = useRef(true);
139
140 useEffect(() => {
141 const siteBlocks = document.querySelector('.wp-site-blocks');
142 const wpadminbar = document.querySelector('#wpadminbar');
143 const stickyHeader = document.querySelector(
144 'header.is-position-sticky, header.wp-block-template-part:has(.ext-header-sticky)',
145 );
146
147 const applyScaling = () => {
148 if (!siteBlocks) return;
149
150 if (open) {
151 const viewportWidth = window.innerWidth;
152 const scale = (viewportWidth - SIDEBAR_WIDTH) / viewportWidth;
153 const scaledHeight = window.innerHeight / scale;
154
155 Object.assign(siteBlocks.style, {
156 transformOrigin: 'top left',
157 transform: `translateX(${SIDEBAR_WIDTH}px) translateY(40px) scale(${scale})`,
158 height: `${scaledHeight}px`,
159 overflowY: 'auto',
160 scrollBehavior: 'smooth',
161 });
162 if (stickyHeader) {
163 stickyHeader.style.setProperty(
164 '--wp-admin--admin-bar--position-offset',
165 '0px',
166 );
167 }
168 document.body.style.overflow = 'hidden';
169 document.body.style.position = 'fixed';
170 document.body.style.top = '0';
171 document.body.style.left = '0';
172 document.body.style.width = '100vw';
173 window.scrollTo(0, 0);
174 } else {
175 Object.assign(siteBlocks.style, {
176 transformOrigin: 'top left',
177 transform: 'translateX(0) translateY(0) scale(1)',
178 height: '',
179 overflowY: '',
180 maxWidth: '100vw',
181 scrollBehavior: '',
182 });
183 if (stickyHeader) {
184 stickyHeader.style.removeProperty(
185 '--wp-admin--admin-bar--position-offset',
186 '32px',
187 );
188 }
189 document.body.style.overflow = '';
190 document.body.style.position = '';
191 document.body.style.top = '';
192 document.body.style.left = '';
193 document.body.style.width = '';
194 }
195 };
196
197 if (!firstRun.current) {
198 if (siteBlocks) {
199 siteBlocks.style.transition = t(['transform']);
200 }
201 if (wpadminbar) {
202 wpadminbar.style.transition = t([
203 'margin-left',
204 'margin-top',
205 'margin-right',
206 'border-radius',
207 'max-width',
208 ]);
209 }
210 } else {
211 firstRun.current = false;
212 }
213
214 const raf = requestAnimationFrame(() => {
215 const fw = open ? `${FRAME_WIDTH}px` : '0px';
216 const ml = open ? `${SIDEBAR_WIDTH}px` : '0px';
217
218 applyScaling();
219
220 if (wpadminbar) {
221 Object.assign(wpadminbar.style, {
222 marginTop: fw,
223 marginRight: fw,
224 marginBottom: '0px',
225 marginLeft: ml,
226 borderRadius: open ? '8px 8px 0 0' : '0',
227 maxWidth: open
228 ? `calc(100% - ${SIDEBAR_WIDTH + FRAME_WIDTH}px)`
229 : '100%',
230 });
231 }
232 });
233
234 window.addEventListener('resize', applyScaling);
235
236 return () => {
237 cancelAnimationFrame(raf);
238 window.removeEventListener('resize', applyScaling);
239 document.body.style.overflowX = '';
240 if (siteBlocks) {
241 Object.assign(siteBlocks.style, {
242 transition: '',
243 transform: '',
244 transformOrigin: '',
245 height: '',
246 overflowY: '',
247 maxWidth: '',
248 });
249 }
250 if (stickyHeader) {
251 stickyHeader.style.removeProperty(
252 '--wp-admin--admin-bar--position-offset',
253 );
254 }
255 if (wpadminbar) {
256 Object.assign(wpadminbar.style, {
257 transition: '',
258 marginTop: '',
259 marginRight: '',
260 marginBottom: '',
261 marginLeft: '',
262 borderRadius: '',
263 maxWidth: '',
264 });
265 }
266 };
267 }, [open]);
268 };
269