PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / components / ui / popover.tsx

popover.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/components/ui/popover.tsx

262 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import * as React from "react";
2
3 export interface PopoverProps {
4 children: React.ReactNode;
5 open?: boolean;
6 onOpenChange?: (open: boolean) => void;
7 }
8
9 const PopoverContext = React.createContext<{
10 open: boolean;
11 setOpen: (open: boolean) => void;
12 triggerRef: React.MutableRefObject<HTMLElement | null>;
13 }>({
14 open: false,
15 setOpen: () => {},
16 triggerRef: { current: null },
17 });
18
19 export const Popover: React.FC<PopoverProps> = ({
20 children,
21 open: controlledOpen,
22 onOpenChange,
23 }) => {
24 const [internalOpen, setInternalOpen] = React.useState(false);
25 const triggerRef = React.useRef<HTMLElement | null>(null);
26 const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
27 const setOpen = React.useCallback(
28 (newOpen: boolean) => {
29 if (controlledOpen === undefined) {
30 setInternalOpen(newOpen);
31 }
32 onOpenChange?.(newOpen);
33 },
34 [controlledOpen, onOpenChange],
35 );
36
37 return (
38 <PopoverContext.Provider value={{ open, setOpen, triggerRef }}>
39 <div className="relative">{children}</div>
40 </PopoverContext.Provider>
41 );
42 };
43
44 export const PopoverTrigger: React.FC<{
45 children: React.ReactNode;
46 asChild?: boolean;
47 }> = ({ children, asChild }) => {
48 const { open, setOpen, triggerRef } = React.useContext(PopoverContext);
49
50 if (asChild && React.isValidElement(children)) {
51 return React.cloneElement(children, {
52 ref: (node: HTMLElement) => {
53 triggerRef.current = node;
54 if (typeof (children as any).ref === "function") {
55 (children as any).ref(node);
56 }
57 },
58 onClick: () => setOpen(!open),
59 } as any);
60 }
61
62 return (
63 <div
64 ref={(node) => {
65 triggerRef.current = node;
66 }}
67 onClick={() => setOpen(!open)}
68 className="cursor-pointer"
69 >
70 {children}
71 </div>
72 );
73 };
74
75 export const PopoverContent: React.FC<{
76 children: React.ReactNode;
77 className?: string;
78 align?: "start" | "center" | "end";
79 side?: "top" | "bottom" | "left" | "right";
80 }> = ({ children, className = "", align = "start", side = "bottom" }) => {
81 const { open, setOpen, triggerRef } = React.useContext(PopoverContext);
82 const contentRef = React.useRef<HTMLDivElement>(null);
83 const [position, setPosition] = React.useState<{
84 top: number;
85 left: number;
86 } | null>(null);
87
88 // Calculate position
89 React.useEffect(() => {
90 if (!open || !triggerRef.current) {
91 setPosition(null);
92 return;
93 }
94
95 const calculatePosition = () => {
96 if (!triggerRef.current) return;
97
98 const triggerRect = triggerRef.current.getBoundingClientRect();
99 const viewportWidth = window.innerWidth;
100 const viewportHeight = window.innerHeight;
101 const scrollX = window.scrollX;
102 const scrollY = window.scrollY;
103
104 // Use estimated dimensions if content not yet rendered
105 const estimatedWidth = 320; // w-80 = 320px
106 const estimatedHeight = 300; // Estimated height
107
108 let top = 0;
109 let left = 0;
110
111 // Calculate position based on side
112 if (side === "bottom") {
113 top = triggerRect.bottom + scrollY + 8;
114 if (top + estimatedHeight > scrollY + viewportHeight) {
115 // Flip to top if not enough space below
116 top = triggerRect.top + scrollY - estimatedHeight - 8;
117 }
118 } else if (side === "top") {
119 top = triggerRect.top + scrollY - estimatedHeight - 8;
120 if (top < scrollY) {
121 // Flip to bottom if not enough space above
122 top = triggerRect.bottom + scrollY + 8;
123 }
124 } else if (side === "right") {
125 top = triggerRect.top + scrollY;
126 left = triggerRect.right + scrollX + 8;
127 if (left + estimatedWidth > scrollX + viewportWidth) {
128 // Flip to left if not enough space on right
129 left = triggerRect.left + scrollX - estimatedWidth - 8;
130 }
131 } else if (side === "left") {
132 top = triggerRect.top + scrollY;
133 left = triggerRect.left + scrollX - estimatedWidth - 8;
134 if (left < scrollX) {
135 // Flip to right if not enough space on left
136 left = triggerRect.right + scrollX + 8;
137 }
138 }
139
140 // Adjust horizontal alignment
141 if (side === "top" || side === "bottom") {
142 if (align === "start") {
143 left = triggerRect.left + scrollX;
144 } else if (align === "center") {
145 left =
146 triggerRect.left +
147 scrollX +
148 triggerRect.width / 2 -
149 estimatedWidth / 2;
150 } else if (align === "end") {
151 left = triggerRect.right + scrollX - estimatedWidth;
152 }
153
154 // Ensure popover doesn't go off-screen horizontally
155 if (left < scrollX) {
156 left = scrollX + 8;
157 } else if (left + estimatedWidth > scrollX + viewportWidth) {
158 left = scrollX + viewportWidth - estimatedWidth - 8;
159 }
160 }
161
162 // Ensure popover doesn't go off-screen vertically
163 if (top < scrollY) {
164 top = scrollY + 8;
165 } else if (top + estimatedHeight > scrollY + viewportHeight) {
166 top = scrollY + viewportHeight - estimatedHeight - 8;
167 }
168
169 setPosition({ top, left });
170
171 // Refine position after content is rendered
172 requestAnimationFrame(() => {
173 if (!contentRef.current || !triggerRef.current) return;
174
175 const contentRect = contentRef.current.getBoundingClientRect();
176 const currentTop = top;
177 const currentLeft = left;
178
179 let refinedTop = currentTop;
180 let refinedLeft = currentLeft;
181
182 // Refine vertical position
183 if (refinedTop + contentRect.height > scrollY + viewportHeight) {
184 refinedTop = scrollY + viewportHeight - contentRect.height - 8;
185 }
186 if (refinedTop < scrollY) {
187 refinedTop = scrollY + 8;
188 }
189
190 // Refine horizontal position
191 if (refinedLeft + contentRect.width > scrollX + viewportWidth) {
192 refinedLeft = scrollX + viewportWidth - contentRect.width - 8;
193 }
194 if (refinedLeft < scrollX) {
195 refinedLeft = scrollX + 8;
196 }
197
198 // Only update if position changed
199 if (refinedTop !== currentTop || refinedLeft !== currentLeft) {
200 setPosition({ top: refinedTop, left: refinedLeft });
201 }
202 });
203 };
204
205 // Calculate position immediately
206 calculatePosition();
207
208 const handleResize = () => calculatePosition();
209 const handleScroll = () => calculatePosition();
210
211 window.addEventListener("resize", handleResize);
212 window.addEventListener("scroll", handleScroll, true);
213
214 return () => {
215 window.removeEventListener("resize", handleResize);
216 window.removeEventListener("scroll", handleScroll, true);
217 };
218 }, [open, align, side]);
219
220 React.useEffect(() => {
221 if (!open) return;
222
223 const handleClickOutside = (event: MouseEvent) => {
224 if (
225 contentRef.current &&
226 !contentRef.current.contains(event.target as Node)
227 ) {
228 // Check if click is on trigger
229 if (
230 triggerRef.current &&
231 triggerRef.current.contains(event.target as Node)
232 ) {
233 return;
234 }
235 setOpen(false);
236 }
237 };
238
239 document.addEventListener("mousedown", handleClickOutside);
240 return () => document.removeEventListener("mousedown", handleClickOutside);
241 }, [open, setOpen]);
242
243 if (!open) return null;
244
245 // Show with initial position, refine after render
246 const initialPosition = position || { top: 0, left: 0 };
247
248 return (
249 <div
250 ref={contentRef}
251 className={`fixed z-[9999] w-72 rounded-md border border-gray-200 bg-white p-4 shadow-lg dark:border-gray-700 dark:bg-gray-800 ${className}`}
252 style={{
253 top: `${initialPosition.top}px`,
254 left: `${initialPosition.left}px`,
255 visibility: position ? "visible" : "hidden",
256 }}
257 >
258 {children}
259 </div>
260 );
261 };
262