PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.6.0
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.6.0
3.6.0 3.5.3 3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / src / style-customizer-v2 / Popover.tsx
everest-forms / src / style-customizer-v2 Last commit date
AiAssistant.tsx 2 days ago App.tsx 2 days ago BuilderSync.ts 2 days ago ControlRenderer.tsx 2 days ago HoverTip.tsx 2 days ago Popover.tsx 2 days ago PreviewBridge.ts 2 days ago PreviewPane.tsx 2 days ago constants.ts 2 days ago cssVars.ts 2 days ago globals.d.ts 2 days ago index.tsx 2 days ago panes.tsx 2 days ago store.ts 2 days ago style.scss 2 days ago types.ts 2 days ago
Popover.tsx
219 lines
1 /**
2 * Style Customizer v2 — floating popover (position:fixed), positioned relative to an anchor
3 * rect and closed on outside click / Escape / scroll.
4 */
5 import React from 'react';
6
7 const __ = ( window as any ).wp?.i18n?.__ || ( ( s: string ) => s );
8
9 export interface PopoverState {
10 anchor: HTMLElement;
11 render: () => React.ReactNode;
12 matchWidth?: boolean;
13 /** Optional identity so the opener can toggle a specific popover (e.g. the palette grid). */
14 kind?: string;
15 /** Header title. When set (or `closable`), the popover shows a titled header with a × button. */
16 title?: string;
17 /** Show a close (×) button in the header. Implied when `title` is set. */
18 closable?: boolean;
19 }
20
21 export function Popover( { state, onClose }: { state: PopoverState; onClose: () => void } ) {
22 const ref = React.useRef< HTMLDivElement >( null );
23 const closeRef = React.useRef< HTMLButtonElement >( null );
24 const showHeader = !! ( state.title || state.closable );
25 const showClose = !! state.closable;
26 const [ pos, setPos ] = React.useState< { left: number; top: number; width?: number; maxHeight?: number } >( {
27 left: -9999,
28 top: -9999,
29 } );
30
31 React.useLayoutEffect( () => {
32 const el = ref.current;
33 if ( ! el ) {
34 return;
35 }
36
37 const reposition = () => {
38 const r = state.anchor.getBoundingClientRect();
39 const w = el.offsetWidth;
40 let left = state.matchWidth ? r.left : Math.min( Math.max( 8, r.left ), window.innerWidth - w - 8 );
41 left = Math.min( Math.max( 8, left ), window.innerWidth - w - 8 );
42
43 // scrollHeight (not offsetHeight) gives the true content height despite the CSS max-height clamp.
44 const naturalH = el.scrollHeight;
45 const desiredH = Math.min( naturalH, 460 );
46 const GAP = 6;
47 const MARGIN = 8;
48 const MIN_H = 150;
49 // The WP admin toolbar is `position:fixed` above everything in wp-admin — a popover that
50 // grows tall enough (e.g. the "Create Custom Palette" editor) must not be pushed up under
51 // it, or its top controls end up visually obstructed and unclickable (the toolbar intercepts
52 // the click even though the popover paints on top of it).
53 const adminBar = document.getElementById( 'wpadminbar' );
54 const topBoundary = adminBar ? Math.max( MARGIN, adminBar.getBoundingClientRect().bottom + GAP ) : MARGIN;
55 const spaceBelow = window.innerHeight - r.bottom - GAP - MARGIN;
56 const spaceAbove = r.top - GAP - topBoundary;
57
58 let top: number;
59 let maxHeight: number;
60 if ( desiredH <= spaceBelow ) {
61 top = r.bottom + GAP;
62 maxHeight = desiredH;
63 } else if ( desiredH <= spaceAbove ) {
64 maxHeight = desiredH;
65 top = r.top - maxHeight - GAP;
66 } else {
67 // Neither side fits in full — use whichever has more room.
68 maxHeight = Math.max( Math.max( spaceBelow, spaceAbove ), MIN_H );
69 top = spaceBelow >= spaceAbove ? r.bottom + GAP : r.top - maxHeight - GAP;
70 }
71 setPos( { left, top: Math.max( topBoundary, top ), width: state.matchWidth ? r.width : undefined, maxHeight } );
72 };
73
74 reposition();
75
76 // Content height can change after the initial layout (e.g. the palette popover growing
77 // when it switches from the browse grid to the "Create Custom Palette" editor) — `state`
78 // itself doesn't change in that case, so re-run the fit calc whenever the popover's own
79 // size changes rather than only when it (re)opens.
80 const ro = new ResizeObserver( reposition );
81 ro.observe( el );
82 return () => ro.disconnect();
83 }, [ state ] );
84
85 React.useEffect( () => {
86 const onDown = ( e: MouseEvent ) => {
87 const el = ref.current;
88 if ( el && ! el.contains( e.target as Node ) && ! state.anchor.contains( e.target as Node ) ) {
89 onClose();
90 }
91 };
92 const onKey = ( e: KeyboardEvent ) => {
93 if ( e.key === 'Escape' ) {
94 onClose();
95 }
96 };
97 document.addEventListener( 'pointerdown', onDown, true );
98 document.addEventListener( 'keydown', onKey );
99 return () => {
100 document.removeEventListener( 'pointerdown', onDown, true );
101 document.removeEventListener( 'keydown', onKey );
102 };
103 }, [ state, onClose ] );
104
105 // Move focus into the popover on open so keyboard users don't have to tab across the page.
106 React.useEffect( () => {
107 if ( showClose && closeRef.current ) {
108 closeRef.current.focus();
109 } else if ( ref.current ) {
110 ref.current.focus();
111 }
112 // eslint-disable-next-line react-hooks/exhaustive-deps
113 }, [ state ] );
114
115 return (
116 <div
117 ref={ ref }
118 className="scv2-pop"
119 role="dialog"
120 aria-modal="false"
121 aria-label={ state.title || undefined }
122 tabIndex={ -1 }
123 style={ {
124 left: pos.left,
125 top: pos.top,
126 width: pos.width,
127 minWidth: pos.width,
128 maxWidth: pos.width || undefined,
129 maxHeight: pos.maxHeight,
130 } }
131 >
132 { showHeader && (
133 <div className="pop-head">
134 { state.title && <span className="pop-head-title">{ state.title }</span> }
135 { showClose && (
136 <button
137 ref={ closeRef }
138 type="button"
139 className="pop-close"
140 aria-label={ __( 'Close', 'everest-forms' ) }
141 onClick={ onClose }
142 >
143 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={ 2 } aria-hidden="true">
144 <path d="M18 6 6 18M6 6l12 12" />
145 </svg>
146 </button>
147 ) }
148 </div>
149 ) }
150 { state.render() }
151 </div>
152 );
153 }
154
155 /* --------------------------------------------------------------------- *
156 * Confirm modal — centered/backdropped dialog for destructive actions.
157 * --------------------------------------------------------------------- */
158
159 export interface ConfirmState {
160 title: string;
161 message: string;
162 confirmLabel?: string;
163 /** Styles the confirm button as a destructive action. */
164 danger?: boolean;
165 onConfirm: () => void;
166 }
167
168 export function ConfirmModal( { state, onClose }: { state: ConfirmState; onClose: () => void } ) {
169 const confirmRef = React.useRef< HTMLButtonElement >( null );
170
171 React.useEffect( () => {
172 confirmRef.current?.focus();
173 }, [] );
174
175 React.useEffect( () => {
176 const onKey = ( e: KeyboardEvent ) => {
177 if ( e.key === 'Escape' ) {
178 onClose();
179 }
180 };
181 document.addEventListener( 'keydown', onKey );
182 return () => document.removeEventListener( 'keydown', onKey );
183 }, [ onClose ] );
184
185 return (
186 <div
187 className="scv2-modal-backdrop"
188 onMouseDown={ ( e ) => {
189 if ( e.target === e.currentTarget ) {
190 onClose();
191 }
192 } }
193 >
194 <div className="scv2-modal" role="alertdialog" aria-modal="true" aria-labelledby="scv2-modal-title">
195 <h3 id="scv2-modal-title" className="scv2-modal-title">
196 { state.title }
197 </h3>
198 <p className="scv2-modal-msg">{ state.message }</p>
199 <div className="scv2-modal-actions">
200 <button type="button" className="scv2-modal-cancel" onClick={ onClose }>
201 { __( 'Cancel', 'everest-forms' ) }
202 </button>
203 <button
204 ref={ confirmRef }
205 type="button"
206 className={ 'scv2-modal-confirm' + ( state.danger ? ' danger' : '' ) }
207 onClick={ () => {
208 state.onConfirm();
209 onClose();
210 } }
211 >
212 { state.confirmLabel || __( 'Confirm', 'everest-forms' ) }
213 </button>
214 </div>
215 </div>
216 </div>
217 );
218 }
219