funda-ui 4.7.625 → 4.7.701

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CascadingSelect/index.js +2 -2
  2. package/CascadingSelectE2E/index.js +2 -2
  3. package/Chatbox/index.js +3 -17
  4. package/Checkbox/index.js +3 -3
  5. package/ColorPicker/index.js +3 -18
  6. package/Date/index.js +3 -18
  7. package/EventCalendarTimeline/index.d.ts +1 -1
  8. package/EventCalendarTimeline/index.js +11 -1
  9. package/File/index.d.ts +9 -0
  10. package/File/index.js +245 -93
  11. package/Input/index.js +3 -18
  12. package/LiveSearch/index.js +3 -18
  13. package/NativeSelect/index.js +3 -3
  14. package/NumberInput/index.js +3 -18
  15. package/Popover/index.css +198 -0
  16. package/Popover/index.d.ts +4 -0
  17. package/Popover/index.js +1808 -0
  18. package/README.md +1 -0
  19. package/Radio/index.js +3 -3
  20. package/RangeSlider/index.js +3 -18
  21. package/SearchBar/index.js +3 -18
  22. package/Select/index.js +3 -2
  23. package/Switch/index.js +3 -3
  24. package/TagInput/index.css +31 -31
  25. package/TagInput/index.js +12 -23
  26. package/Textarea/index.js +3 -17
  27. package/Utils/useSSE.d.ts +9 -0
  28. package/Utils/useSSE.js +211 -0
  29. package/all.d.ts +1 -0
  30. package/all.js +1 -0
  31. package/lib/cjs/CascadingSelect/index.js +2 -2
  32. package/lib/cjs/CascadingSelectE2E/index.js +2 -2
  33. package/lib/cjs/Chatbox/index.js +3 -17
  34. package/lib/cjs/Checkbox/index.js +3 -3
  35. package/lib/cjs/ColorPicker/index.js +3 -18
  36. package/lib/cjs/Date/index.js +3 -18
  37. package/lib/cjs/EventCalendarTimeline/index.d.ts +1 -1
  38. package/lib/cjs/EventCalendarTimeline/index.js +11 -1
  39. package/lib/cjs/File/index.d.ts +9 -0
  40. package/lib/cjs/File/index.js +245 -93
  41. package/lib/cjs/Input/index.js +3 -18
  42. package/lib/cjs/LiveSearch/index.js +3 -18
  43. package/lib/cjs/NativeSelect/index.js +3 -3
  44. package/lib/cjs/NumberInput/index.js +3 -18
  45. package/lib/cjs/Popover/index.d.ts +4 -0
  46. package/lib/cjs/Popover/index.js +1808 -0
  47. package/lib/cjs/Radio/index.js +3 -3
  48. package/lib/cjs/RangeSlider/index.js +3 -18
  49. package/lib/cjs/SearchBar/index.js +3 -18
  50. package/lib/cjs/Select/index.js +3 -2
  51. package/lib/cjs/Switch/index.js +3 -3
  52. package/lib/cjs/TagInput/index.js +12 -23
  53. package/lib/cjs/Textarea/index.js +3 -17
  54. package/lib/cjs/Utils/useSSE.d.ts +9 -0
  55. package/lib/cjs/Utils/useSSE.js +211 -0
  56. package/lib/cjs/index.d.ts +1 -0
  57. package/lib/cjs/index.js +1 -0
  58. package/lib/css/Popover/index.css +198 -0
  59. package/lib/css/TagInput/index.css +31 -31
  60. package/lib/esm/CascadingSelect/index.tsx +2 -2
  61. package/lib/esm/CascadingSelectE2E/index.tsx +2 -2
  62. package/lib/esm/Checkbox/index.tsx +3 -3
  63. package/lib/esm/ColorPicker/index.tsx +4 -15
  64. package/lib/esm/EventCalendarTimeline/index.tsx +11 -2
  65. package/lib/esm/File/index.tsx +148 -23
  66. package/lib/esm/Input/index.tsx +6 -17
  67. package/lib/esm/NativeSelect/index.tsx +3 -3
  68. package/lib/esm/NumberInput/index.tsx +7 -15
  69. package/lib/esm/Popover/Popover.tsx +251 -0
  70. package/lib/esm/Popover/PopoverClose.tsx +51 -0
  71. package/lib/esm/Popover/PopoverContent.tsx +72 -0
  72. package/lib/esm/Popover/PopoverTrigger.tsx +62 -0
  73. package/lib/esm/Popover/index.scss +272 -0
  74. package/lib/esm/Popover/index.tsx +4 -0
  75. package/lib/esm/Radio/index.tsx +3 -3
  76. package/lib/esm/SearchBar/index.tsx +8 -12
  77. package/lib/esm/Select/index.tsx +2 -2
  78. package/lib/esm/Switch/index.tsx +3 -3
  79. package/lib/esm/TagInput/index.scss +24 -24
  80. package/lib/esm/TagInput/index.tsx +13 -20
  81. package/lib/esm/Textarea/index.tsx +6 -14
  82. package/lib/esm/Utils/hooks/useSSE.tsx +109 -0
  83. package/lib/esm/index.js +1 -0
  84. package/package.json +1 -1
@@ -8,6 +8,8 @@ import {
8
8
  } from 'funda-utils/dist/cjs/buffer';
9
9
  import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
10
10
 
11
+
12
+
11
13
  export type FileProps = {
12
14
  contentRef?: React.ForwardedRef<any>;
13
15
  wrapperClassName?: string;
@@ -41,6 +43,15 @@ export type FileProps = {
41
43
  fetchFuncAsync?: any;
42
44
  fetchFuncMethod?: string;
43
45
  fetchFuncMethodParams?: any[];
46
+ formDataAppend?: (formData: FormData, files: FileList) => void;
47
+ /** Enable chunked upload for large files */
48
+ enableChunkedUpload?: boolean;
49
+ /** Chunk size in bytes (default: 2MB) */
50
+ chunkSize?: number;
51
+ /** Custom function to append chunk data to FormData. Receives (formData: FormData, chunk: Blob, chunkIndex: number, totalChunks: number, file: File) */
52
+ chunkDataAppend?: (formData: FormData, chunk: Blob, chunkIndex: number, totalChunks: number, file: File) => void;
53
+ /** Callback for chunk upload progress. Receives (uploadedBytes: number, totalBytes: number, file: File, chunkIndex: number, totalChunks: number) */
54
+ onChunkProgress?: (uploadedBytes: number, totalBytes: number, file: File, chunkIndex: number, totalChunks: number) => void;
44
55
  onChange?: (e: any, e2: any, value: any) => void;
45
56
  onComplete?: (e: any, e2: any, callback: any, incomingData: string | null | undefined) => void;
46
57
  onProgress?: (files: any, e: any, e2: any) => void;
@@ -79,9 +90,17 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
79
90
  fetchFuncAsync,
80
91
  fetchFuncMethod,
81
92
  fetchFuncMethodParams,
93
+ formDataAppend,
82
94
  onChange,
83
95
  onComplete,
84
96
  onProgress,
97
+
98
+ // Upload file in chunks
99
+ enableChunkedUpload,
100
+ chunkSize = 2 * 1024 * 1024, // Default 2MB
101
+ chunkDataAppend,
102
+ onChunkProgress,
103
+
85
104
  ...attributes
86
105
  } = props;
87
106
 
@@ -144,10 +163,9 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
144
163
 
145
164
  let res = {};
146
165
  if (typeof window === 'undefined') return res;
147
-
148
-
166
+
149
167
  try {
150
- const response = await fetch(url + '?' + new URLSearchParams(fetchParams as any), {
168
+ const response = await fetch(typeof fetchParams !== 'undefined' ? url + '?' + new URLSearchParams(fetchParams as any) : url, {
151
169
  method: methord,
152
170
  body: data,
153
171
  ...rest
@@ -165,6 +183,75 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
165
183
 
166
184
  }
167
185
 
186
+ /**
187
+ * Upload file in chunks
188
+ */
189
+ async function uploadFileInChunks(file: File, url: string, method: string = 'POST'): Promise<any> {
190
+ const totalChunks = Math.ceil(file.size / chunkSize);
191
+ const uploadId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
192
+ let uploadedBytes = 0;
193
+
194
+ // Upload each chunk sequentially
195
+ for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
196
+ const start = chunkIndex * chunkSize;
197
+ const end = Math.min(start + chunkSize, file.size);
198
+ const chunk = file.slice(start, end);
199
+
200
+ const formData = new FormData();
201
+
202
+ if (chunkDataAppend) {
203
+ // Use custom chunk data append function
204
+ chunkDataAppend(formData, chunk, chunkIndex, totalChunks, file);
205
+ } else {
206
+ // Default chunk upload format
207
+ formData.append('chunk', chunk);
208
+ formData.append('chunkIndex', chunkIndex.toString());
209
+ formData.append('totalChunks', totalChunks.toString());
210
+ formData.append('uploadId', uploadId);
211
+ formData.append('fileName', file.name);
212
+ formData.append('fileSize', file.size.toString());
213
+ formData.append('fileType', file.type);
214
+ }
215
+
216
+ // Update progress
217
+ uploadedBytes += (end - start);
218
+ onChunkProgress?.(uploadedBytes, file.size, file, chunkIndex, totalChunks);
219
+
220
+ // Upload chunk
221
+ const response: any = await fetchDataDefault(
222
+ url,
223
+ formData,
224
+ method,
225
+ {
226
+ headers: { 'Content-Type': 'multipart/form-data' }
227
+ }
228
+ );
229
+
230
+ // Check if chunk upload failed
231
+ if (response.error) {
232
+ throw new Error(`Chunk ${chunkIndex + 1}/${totalChunks} upload failed: ${response.error}`);
233
+ }
234
+
235
+ // If server returns a specific response indicating chunk upload success/failure, handle it
236
+ if (response.code !== undefined && response.code !== 0 && response.code !== 200) {
237
+ throw new Error(`Chunk ${chunkIndex + 1}/${totalChunks} upload failed: ${response.message || 'Unknown error'}`);
238
+ }
239
+ }
240
+
241
+ // After all chunks are uploaded, return the final response
242
+ // The server should merge chunks and return the final result
243
+ return {
244
+ code: 0,
245
+ message: 'Upload completed',
246
+ data: {
247
+ uploadId,
248
+ fileName: file.name,
249
+ fileSize: file.size,
250
+ totalChunks
251
+ }
252
+ };
253
+ }
254
+
168
255
 
169
256
  function handleLabelEnter(event: any) {
170
257
  event.currentTarget.getElementsByTagName('button')[0].className = labelHoverClassName ? labelHoverClassName : 'btn btn-primary';
@@ -192,26 +279,70 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
192
279
  setProgressing(true);
193
280
 
194
281
 
282
+ // Use the default "fetch()" method to handle file references
195
283
  if (fetchUrl) {
196
284
 
197
- const formData = new FormData();
198
- formData.append('action', 'upload_plug_action');
285
+ // Check if chunked upload is enabled
286
+ if (enableChunkedUpload) {
287
+ // Upload files in chunks
288
+ (async () => {
289
+ try {
290
+ const uploadPromises: Promise<any>[] = [];
291
+
292
+ for (let i = 0; i < curFiles.length; i++) {
293
+ const file = curFiles[i];
294
+ uploadPromises.push(uploadFileInChunks(file, fetchUrl, fetchMethod ? fetchMethod : 'POST'));
295
+ }
199
296
 
200
- for (let i = 0; i < curFiles.length; i++) {
201
- formData.append("clientFiles", curFiles[i]);
202
- }
297
+ const results = await Promise.all(uploadPromises);
298
+
299
+ // Combine results from all files
300
+ const jsonData = {
301
+ code: 0,
302
+ message: 'All files uploaded successfully',
303
+ data: results
304
+ };
305
+
306
+ onComplete?.(fileInputRef.current, submitRef.current, jsonData, incomingData);
307
+ setProgressing(false);
308
+ resetDefaultVal();
309
+ } catch (error: any) {
310
+ const errorResponse = {
311
+ code: -1,
312
+ message: error.message || 'Upload failed',
313
+ error: error
314
+ };
315
+ onComplete?.(fileInputRef.current, submitRef.current, errorResponse, incomingData);
316
+ setProgressing(false);
317
+ }
318
+ })();
319
+ } else {
320
+ // Original non-chunked upload
321
+ const formData = new FormData();
322
+
323
+ if (formDataAppend) {
324
+ // Use custom formData.append function
325
+ formDataAppend(formData, curFiles);
326
+ } else {
327
+ // Default behavior
328
+ formData.append('action', 'upload_plug_action');
329
+ for (let i = 0; i < curFiles.length; i++) {
330
+ formData.append("clientFiles", curFiles[i]);
331
+ }
332
+ }
203
333
 
204
- fetchDataDefault(fetchUrl, formData, fetchMethod ? fetchMethod : 'POST', {
205
- headers: { 'Content-Type': 'multipart/form-data' }
206
- }).then(function (response: any) {
207
- const jsonData = response.data;
208
- onComplete?.(fileInputRef.current, submitRef.current, jsonData, incomingData);
209
- setProgressing(false);
334
+ fetchDataDefault(fetchUrl, formData, fetchMethod ? fetchMethod : 'POST', {
335
+ headers: { 'Content-Type': 'multipart/form-data' }
336
+ }).then(function (response: any) {
337
+ const jsonData = response;
338
+ onComplete?.(fileInputRef.current, submitRef.current, jsonData, incomingData);
339
+ setProgressing(false);
210
340
 
211
- // update default value
212
- resetDefaultVal();
341
+ // update default value
342
+ resetDefaultVal();
213
343
 
214
- });
344
+ });
345
+ }
215
346
  } else {
216
347
 
217
348
 
@@ -322,12 +453,6 @@ const File = forwardRef((props: FileProps, externalRef: any) => {
322
453
  setDefaultValue(null);
323
454
 
324
455
 
325
- //----
326
- //remove focus style
327
- if (val === '') {
328
- rootRef.current?.classList.remove('focus');
329
- }
330
-
331
456
  //
332
457
  onChange?.(fileInputRef.current, submitRef.current, fileInputRef.current.files);
333
458
 
@@ -354,8 +354,6 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
354
354
 
355
355
 
356
356
  function handleFocus(event: FocusEvent<HTMLInputElement>) {
357
- rootRef.current?.classList.add('focus');
358
-
359
357
  //
360
358
  if (valRef.current) {
361
359
  onFocus?.(event, onComposition, valRef.current);
@@ -365,13 +363,6 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
365
363
  function handleChange(event: ChangeEvent<HTMLInputElement> | KeyboardEvent<HTMLInputElement> | null, curVal: string) {
366
364
  setChangedVal(curVal);
367
365
 
368
-
369
- //----
370
- //remove focus style
371
- if (curVal === '') {
372
- rootRef.current?.classList.remove('focus');
373
- }
374
-
375
366
  //
376
367
  if (valRef.current) {
377
368
  onChange?.(event, onComposition, valRef.current, curVal);
@@ -390,13 +381,6 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
390
381
  const el = event.target;
391
382
  const val = event.target.value;
392
383
 
393
-
394
- //----
395
- //remove focus style
396
- if (val === '') {
397
- rootRef.current?.classList.remove('focus');
398
- }
399
-
400
384
  //
401
385
  if (valRef.current) {
402
386
  onBlur?.(event, onComposition, valRef.current);
@@ -520,7 +504,12 @@ const Input = forwardRef((props: InputProps, externalRef: any) => {
520
504
  return (
521
505
  <>
522
506
 
523
- <div className={clsWrite(wrapperClassName, 'mb-3 position-relative')} ref={rootRef}>
507
+ <div className={combinedCls(
508
+ clsWrite(wrapperClassName, 'mb-3 position-relative'),
509
+ {
510
+ 'focus-floating': changedVal !== ''
511
+ }
512
+ )} ref={rootRef}>
524
513
  {label ? <>{typeof label === 'string' ? <label htmlFor={idRes} className="form-label" dangerouslySetInnerHTML={{ __html: `${label}` }}></label> : <label htmlFor={idRes} className="form-label">{label}</label>}</> : null}
525
514
 
526
515
  <div className={combinedCls(
@@ -260,7 +260,7 @@ const NativeSelect = forwardRef((props: NativeSelectProps, externalRef: any) =>
260
260
 
261
261
  //
262
262
  function handleFocus(event: any) {
263
- rootRef.current?.classList.add('focus');
263
+ rootRef.current?.classList.add('focus-floating');
264
264
 
265
265
  //
266
266
  onFocus?.(event);
@@ -283,7 +283,7 @@ const NativeSelect = forwardRef((props: NativeSelectProps, externalRef: any) =>
283
283
 
284
284
  //----
285
285
  //remove focus style
286
- rootRef.current?.classList.remove('focus');
286
+ rootRef.current?.classList.remove('focus-floating');
287
287
 
288
288
  //
289
289
  if ( typeof(onChange) === 'function' ) {
@@ -298,7 +298,7 @@ const NativeSelect = forwardRef((props: NativeSelectProps, externalRef: any) =>
298
298
 
299
299
  //----
300
300
  //remove focus style
301
- rootRef.current?.classList.remove('focus');
301
+ rootRef.current?.classList.remove('focus-floating');
302
302
 
303
303
  //
304
304
  onBlur?.(event);
@@ -183,7 +183,6 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
183
183
 
184
184
 
185
185
  function handleFocus(event: FocusEvent<HTMLInputElement>) {
186
- rootRef.current?.classList.add('focus');
187
186
 
188
187
  //
189
188
  onFocus?.(event, valRef.current);
@@ -220,12 +219,7 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
220
219
  setChangedVal(resToInput);
221
220
 
222
221
 
223
- //----
224
- //remove focus style
225
- if (val === '') {
226
- rootRef.current?.classList.remove('focus');
227
- }
228
-
222
+
229
223
  //
230
224
  onChange?.(event, valRef.current, Number(!isNumeric(newVal) ? '0' : resToInput));
231
225
 
@@ -247,13 +241,6 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
247
241
  setChangedVal('0');
248
242
  }
249
243
 
250
-
251
- //----
252
- //remove focus style
253
- if (val === '') {
254
- rootRef.current?.classList.remove('focus');
255
- }
256
-
257
244
  //
258
245
  onBlur?.(event, valRef.current);
259
246
 
@@ -286,7 +273,12 @@ const NumberInput = forwardRef((props: NumberInputProps, externalRef: any) => {
286
273
  return (
287
274
  <>
288
275
 
289
- <div className={clsWrite(wrapperClassName, 'mb-3 position-relative')} ref={rootRef}>
276
+ <div className={combinedCls(
277
+ clsWrite(wrapperClassName, 'mb-3 position-relative'),
278
+ {
279
+ 'focus-floating': changedVal !== ''
280
+ }
281
+ )} ref={rootRef}>
290
282
  {label ? <>{typeof label === 'string' ? <label htmlFor={idRes} className="form-label" dangerouslySetInnerHTML={{__html: `${label}`}}></label> : <label htmlFor={idRes} className="form-label">{label}</label>}</> : null}
291
283
 
292
284
  <div className="position-relative">
@@ -0,0 +1,251 @@
1
+ import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef, useMemo, createContext, useContext } from 'react';
2
+
3
+ import useComId from 'funda-utils/dist/cjs/useComId';
4
+ import useClickOutside from 'funda-utils/dist/cjs/useClickOutside';
5
+ import {
6
+ getAbsolutePositionOfStage
7
+ } from 'funda-utils/dist/cjs/getElementProperty';
8
+ import { getElCSS } from 'funda-utils/dist/cjs/inputsCalculation';
9
+
10
+ // Context for Popover state management
11
+ export interface PopoverContextValue {
12
+ isOpen: boolean;
13
+ setIsOpen: (open: boolean) => void;
14
+ triggerRef: React.RefObject<HTMLElement>;
15
+ contentRef: React.RefObject<HTMLDivElement>;
16
+ id: string;
17
+ direction?: string;
18
+ offset?: number;
19
+ exceededSidePosOffset?: number;
20
+ size?: string;
21
+ popupArrowColor?: number[];
22
+ popupContentStyle?: React.CSSProperties;
23
+ wrapperClassName?: string;
24
+ position: { x: string; y: string };
25
+ setPosition: (pos: { x: string; y: string }) => void;
26
+ popupArrowStyle?: React.CSSProperties;
27
+ }
28
+
29
+ export const PopoverContext = createContext<PopoverContextValue | null>(null);
30
+
31
+ export const usePopoverContext = () => {
32
+ const context = useContext(PopoverContext);
33
+ if (!context) {
34
+ throw new Error('Popover components must be used within a Popover');
35
+ }
36
+ return context;
37
+ };
38
+
39
+ // Main Popover component (container)
40
+ export type PopoverProps = {
41
+ /** The direction of the tip. Defaults to `top`. Possible values are: `top`, `top-right`, `top-left`, `bottom`, `bottom-right`, `bottom-left` */
42
+ direction?: string;
43
+ /** Position offset */
44
+ offset?: number;
45
+ /** Offset px that exceeds the far right or left side of the screen */
46
+ exceededSidePosOffset?: number;
47
+ /** The size of the content area. Defaults to `auto`. Possible values are: `auto`, `large`, `medium`, `small` */
48
+ size?: string;
49
+ /** Custom color for the popup arrow */
50
+ popupArrowColor?: number[];
51
+ /** Custom style for the popup content */
52
+ popupContentStyle?: React.CSSProperties;
53
+ /** The class name of the control wrapper. */
54
+ wrapperClassName?: string;
55
+ /** -- */
56
+ id?: string;
57
+ /** Controls whether the Popover is open (controlled mode) */
58
+ open?: boolean;
59
+ /** Callback fired when the open state changes (controlled mode) */
60
+ onOpenChange?: (open: boolean) => void;
61
+ children: React.ReactNode;
62
+ };
63
+
64
+ const Popover = forwardRef<any, PopoverProps>((props, ref) => {
65
+ const {
66
+ direction,
67
+ offset,
68
+ exceededSidePosOffset,
69
+ size,
70
+ popupArrowColor,
71
+ popupContentStyle,
72
+ wrapperClassName,
73
+ id,
74
+ open,
75
+ onOpenChange,
76
+ children
77
+ } = props;
78
+
79
+ const POS_OFFSET = Number(offset) || 4;
80
+ const EXCEEDED_SIDE_POS_OFFSET = Number(exceededSidePosOffset) || 15;
81
+ const uniqueID = useComId();
82
+ const idRes = id || uniqueID;
83
+ const triggerRef = useRef<HTMLElement>(null);
84
+ const contentRef = useRef<HTMLDivElement>(null);
85
+ const [uncontrolledOpen, setUncontrolledOpen] = useState<boolean>(false);
86
+ const isControlled = typeof open === 'boolean';
87
+ const isOpen = isControlled ? !!open : uncontrolledOpen;
88
+ const [position, setPosition] = useState<{ x: string; y: string }>({
89
+ x: '0',
90
+ y: '0'
91
+ });
92
+
93
+ const setIsOpen = (nextOpen: boolean) => {
94
+ if (!isControlled) {
95
+ setUncontrolledOpen(nextOpen);
96
+ }
97
+ if (typeof onOpenChange === 'function') {
98
+ onOpenChange(nextOpen);
99
+ }
100
+ };
101
+
102
+ const popupArrowStyle = useMemo(() => {
103
+ if (
104
+ typeof popupArrowColor !== 'undefined' &&
105
+ Array.isArray(popupArrowColor) &&
106
+ popupArrowColor.length === 4
107
+ ) {
108
+ return {
109
+ '--cus-popover-arrow-bg-top': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%280%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
110
+ '--cus-popover-arrow-bg-bottom': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2236px%22%20height%3D%2212px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%28180%2018%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
111
+ '--cus-popover-arrow-bg-left': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%28-90%2018%2018%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
112
+ '--cus-popover-arrow-bg-right': `url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2212px%22%20height%3D%2236px%22%3E%3Cpath%20fill%3D%22rgba%28${popupArrowColor[0]},%20${popupArrowColor[1]},%20${popupArrowColor[2]},%20${popupArrowColor[3]}%29%22%20transform%3D%22rotate%2890%206%206%29%22%20d%3D%22M2.658,0.000%20C-13.615,0.000%2050.938,0.000%2034.662,0.000%20C28.662,0.000%2023.035,12.002%2018.660,12.002%20C14.285,12.002%208.594,0.000%202.658,0.000%20Z%22/%3E%3C/svg%3E") no-repeat`,
113
+ } as React.CSSProperties;
114
+ }
115
+ return undefined;
116
+ }, [popupArrowColor]);
117
+
118
+ // Calculate position based on trigger element
119
+ const calculatePosition = () => {
120
+ const triggerEl = triggerRef.current;
121
+ if (!triggerEl) return;
122
+
123
+ const { x, y, width, height } = getAbsolutePositionOfStage(triggerEl);
124
+ const pos = direction || 'top';
125
+
126
+ if (pos.indexOf('top') >= 0) {
127
+ setPosition({
128
+ x: x + (width / 2) + 'px',
129
+ y: y - height - POS_OFFSET + 'px'
130
+ });
131
+ } else if (pos.indexOf('bottom') >= 0) {
132
+ setPosition({
133
+ x: x + (width / 2) + 'px',
134
+ y: y + height + POS_OFFSET + 'px'
135
+ });
136
+ }
137
+ };
138
+
139
+ // Expose show/hide methods to parent via ref
140
+ useImperativeHandle(ref, () => ({
141
+ show: () => {
142
+ calculatePosition();
143
+ setIsOpen(true);
144
+ },
145
+ hide: () => {
146
+ setIsOpen(false);
147
+ }
148
+ }), [direction, POS_OFFSET]);
149
+
150
+ // Click outside to close
151
+ useClickOutside({
152
+ enabled: isOpen && triggerRef.current !== null,
153
+ isOutside: (event: any) => {
154
+ // Prevent closing when clicking inside the popover wrapper
155
+ if (contentRef.current && contentRef.current.contains(event.target)) {
156
+ return false;
157
+ }
158
+ return (
159
+ triggerRef.current !== event.target &&
160
+ !triggerRef.current?.contains(event.target as HTMLElement)
161
+ );
162
+ },
163
+ handle: () => {
164
+ setIsOpen(false);
165
+ }
166
+ }, [isOpen]);
167
+
168
+ const exceededOffsetInit = () => {
169
+ // Determine whether it exceeds the far right or left side of the screen
170
+ //------------------
171
+ const _modalRef: any = contentRef.current;
172
+ if (_modalRef === null) return;
173
+
174
+
175
+ const _modalContent = _modalRef.querySelector('.cus-popover__content');
176
+ if (!_modalContent) return;
177
+
178
+ const _modalBox = _modalContent.getBoundingClientRect();
179
+ if (typeof _modalContent.dataset.offset === 'undefined' && _modalBox.left > 0) {
180
+
181
+ // Adjust the coordinates due to height
182
+ //------------------
183
+ const triggerEl: any = document.querySelector(`[data-overlay-id="${_modalRef.id}"]`);
184
+ if (triggerEl !== null) {
185
+ let pos = triggerEl.dataset.microtipPosition;
186
+ if (typeof pos === 'undefined') pos = 'top';
187
+
188
+ const _offsetY = _modalBox.height - getElCSS(_modalContent, 'font-size', true) - getElCSS(_modalContent, 'padding-top', true) - getElCSS(_modalContent, 'padding-bottom', true);
189
+
190
+ if (pos.indexOf('top') >= 0) {
191
+ _modalRef.style.transform = `translateY(-${_offsetY}px)`;
192
+ }
193
+ }
194
+
195
+ // 10 pixels is used to account for some bias in mobile devices
196
+ if ((_modalBox.right + 10) > window.innerWidth) {
197
+ const _modalOffsetPosition = _modalBox.right - window.innerWidth + EXCEEDED_SIDE_POS_OFFSET;
198
+ _modalContent.dataset.offset = _modalOffsetPosition;
199
+ _modalContent.style.marginLeft = `-${_modalOffsetPosition}px`;
200
+ }
201
+
202
+ if ((_modalBox.left - 10) < 0) {
203
+ const _modalOffsetPosition = Math.abs(_modalBox.left) + EXCEEDED_SIDE_POS_OFFSET;
204
+ _modalContent.dataset.offset = _modalOffsetPosition;
205
+ _modalContent.style.marginLeft = `${_modalOffsetPosition}px`;
206
+ }
207
+ }
208
+ };
209
+
210
+ // Update position when opening
211
+ useEffect(() => {
212
+ if (isOpen) {
213
+ calculatePosition();
214
+ }
215
+ }, [isOpen, direction, POS_OFFSET]);
216
+
217
+ // Prevent element data from being unable to be obtained when ref is null
218
+ useEffect(() => {
219
+ exceededOffsetInit();
220
+ });
221
+
222
+
223
+
224
+ const contextValue: PopoverContextValue = {
225
+ isOpen,
226
+ setIsOpen,
227
+ triggerRef,
228
+ contentRef,
229
+ id: idRes,
230
+ direction,
231
+ offset,
232
+ exceededSidePosOffset,
233
+ size,
234
+ popupArrowColor,
235
+ popupContentStyle,
236
+ wrapperClassName,
237
+ position,
238
+ setPosition,
239
+ popupArrowStyle
240
+ };
241
+
242
+ return (
243
+ <PopoverContext.Provider value={contextValue}>
244
+ {children}
245
+ </PopoverContext.Provider>
246
+ );
247
+ });
248
+
249
+ Popover.displayName = 'Popover';
250
+
251
+ export default Popover;
@@ -0,0 +1,51 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { combinedCls } from 'funda-utils/dist/cjs/cls';
3
+
4
+
5
+ import { usePopoverContext } from './Popover';
6
+
7
+ export type PopoverCloseProps = {
8
+ asChild?: boolean;
9
+ className?: string;
10
+ children?: React.ReactNode;
11
+ } & React.HTMLAttributes<HTMLElement>;
12
+
13
+ const PopoverClose = forwardRef<HTMLElement, PopoverCloseProps>((props, ref) => {
14
+ const { asChild, className, children, onClick, ...rest } = props;
15
+ const { setIsOpen } = usePopoverContext();
16
+
17
+ const handleClick = (event: React.MouseEvent<HTMLElement>) => {
18
+ if (typeof onClick === 'function') {
19
+ onClick(event);
20
+ }
21
+
22
+ if (!event.defaultPrevented) {
23
+ setIsOpen(false);
24
+ }
25
+ };
26
+
27
+ const closeProps: any = {
28
+ ref,
29
+ className: combinedCls('cus-popover__close', className),
30
+ onClick: handleClick,
31
+ ...rest
32
+ };
33
+
34
+ if (asChild && React.isValidElement(children)) {
35
+ return React.cloneElement(children as React.ReactElement<any>, {
36
+ ...closeProps,
37
+ ...(children.props || {})
38
+ });
39
+ }
40
+
41
+ return (
42
+ <button type="button" {...closeProps}>
43
+ {children || 'Close'}
44
+ </button>
45
+ );
46
+ });
47
+
48
+ PopoverClose.displayName = 'PopoverClose';
49
+
50
+ export default PopoverClose;
51
+