@ioca/react 1.4.75 → 1.4.76

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.
@@ -1,151 +1,322 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
- import { useRef, useContext, useMemo, useEffect, useLayoutEffect, Children, isValidElement, cloneElement } from 'react';
3
- import { useReactive, useMouseUp, useCreation, useResizeObserver } from '../../js/hooks.js';
2
+ import { debounce } from 'radash';
3
+ import { useRef, useState, useMemo, useEffect, useLayoutEffect, Children, isValidElement, cloneElement } from 'react';
4
+ import { useResizeObserver, useMouseUp } from '../../js/hooks.js';
4
5
  import { getPosition, getPointPosition } from '../../js/utils.js';
5
- import ModalContext from '../modal/context.js';
6
6
  import Content from './content.js';
7
7
 
8
8
  function Popup(props) {
9
- const { visible = false, content, trigger = "hover", gap = 12, offset = 8, fixed, position = "top", showDelay = 16, hideDelay = 12, touchable, arrow = true, align, fitSize, watchResize, clickOutside = true, disabled, style, className, getContainer, children, onVisibleChange, } = props;
9
+ const { visible = false, content, trigger = "hover", gap = 12, offset = 8, position = "top", showDelay = 16, hideDelay = 12, touchable, arrow = true, align = "center", fitSize, disabled, style, className, children, onVisibleChange, } = props;
10
10
  const triggerRef = useRef(null);
11
11
  const contentRef = useRef(null);
12
12
  const timerRef = useRef(null);
13
- const statusRef = useRef("");
14
- const isInModal = useContext(ModalContext);
15
- const refWindow = isInModal || fixed;
16
- const state = useReactive({
17
- show: false,
18
- style: { position: refWindow ? "fixed" : "absolute" },
19
- arrowProps: {},
20
- });
21
- useMouseUp((e) => {
22
- if (!triggerRef.current || !contentRef.current || !clickOutside)
23
- return;
24
- const tar = e.target;
25
- const isContain = triggerRef.current.contains(tar) ||
26
- contentRef.current.contains(tar);
27
- if (!state.show || isContain)
28
- return;
29
- handleToggle(false);
13
+ const afterHideTimerRef = useRef(null);
14
+ const rafRef = useRef(null);
15
+ const [show, setShow] = useState(false);
16
+ const showRef = useRef(false);
17
+ showRef.current = show;
18
+ const latestRef = useRef({
19
+ disabled,
20
+ trigger,
21
+ touchable,
22
+ showDelay,
23
+ hideDelay,
24
+ position,
25
+ gap,
26
+ offset,
27
+ align,
28
+ fitSize,
29
+ onVisibleChange,
30
30
  });
31
+ latestRef.current = {
32
+ disabled,
33
+ trigger,
34
+ touchable,
35
+ showDelay,
36
+ hideDelay,
37
+ position,
38
+ gap,
39
+ offset,
40
+ align,
41
+ fitSize,
42
+ onVisibleChange,
43
+ };
44
+ const phaseRef = useRef("");
45
+ const lastPosRef = useRef(null);
46
+ const lastArrowRef = useRef(null);
47
+ const arrowElRef = useRef(null);
48
+ const pointRef = useRef(null);
31
49
  const clearTimer = () => {
32
50
  if (!timerRef.current)
33
51
  return;
34
52
  clearTimeout(timerRef.current);
35
53
  timerRef.current = null;
36
- statusRef.current = "";
54
+ phaseRef.current = "";
55
+ };
56
+ const clearAllTimers = () => {
57
+ clearTimer();
58
+ if (afterHideTimerRef.current) {
59
+ clearTimeout(afterHideTimerRef.current);
60
+ afterHideTimerRef.current = null;
61
+ }
62
+ if (rafRef.current !== null) {
63
+ cancelAnimationFrame(rafRef.current);
64
+ rafRef.current = null;
65
+ }
66
+ };
67
+ const setContentVisible = (visible) => {
68
+ const el = contentRef.current;
69
+ if (!el)
70
+ return;
71
+ el.style.opacity = visible ? "1" : "0";
72
+ el.style.transform = visible ? "none" : "translate(0, 2px)";
73
+ };
74
+ const ensureBaseStyle = () => {
75
+ const el = contentRef.current;
76
+ if (!el)
77
+ return;
78
+ const pos = "fixed";
79
+ if (el.style.position !== pos)
80
+ el.style.position = pos;
81
+ };
82
+ const applyFitSize = () => {
83
+ const o = latestRef.current;
84
+ const triggerEl = triggerRef.current;
85
+ const contentEl = contentRef.current;
86
+ if (!triggerEl || !contentEl)
87
+ return;
88
+ const vertical = ["top", "bottom"].includes(o.position);
89
+ const key = vertical ? "width" : "height";
90
+ if (!o.fitSize) {
91
+ contentEl.style[key] = "";
92
+ return;
93
+ }
94
+ const size = triggerEl[vertical ? "offsetWidth" : "offsetHeight"];
95
+ contentEl.style[key] =
96
+ typeof size === "number" ? `${size}px` : "";
97
+ };
98
+ const applyArrow = (arrowX, arrowY, arrowPos) => {
99
+ const contentEl = contentRef.current;
100
+ if (!contentEl)
101
+ return;
102
+ const arrowEl = arrowElRef.current ??
103
+ contentEl.querySelector(".i-popup-arrow");
104
+ arrowElRef.current = arrowEl;
105
+ if (!arrowEl)
106
+ return;
107
+ let left = arrowX ?? 0;
108
+ let top = arrowY ?? 0;
109
+ let transform = "";
110
+ switch (arrowPos) {
111
+ case "left":
112
+ left += 2;
113
+ transform = `translate(-100%, -50%) rotate(180deg)`;
114
+ break;
115
+ case "right":
116
+ left -= 2;
117
+ transform = `translate(0, -50%)`;
118
+ break;
119
+ case "top":
120
+ top -= 2;
121
+ transform = `translate(-50%, -50%) rotate(-90deg)`;
122
+ break;
123
+ case "bottom":
124
+ top += 2;
125
+ transform = `translate(-50%, -50%) rotate(90deg)`;
126
+ break;
127
+ }
128
+ const prev = lastArrowRef.current;
129
+ if (prev &&
130
+ prev.left === left &&
131
+ prev.top === top &&
132
+ prev.transform === transform) {
133
+ return;
134
+ }
135
+ lastArrowRef.current = { left, top, transform };
136
+ arrowEl.style.left = `${left}px`;
137
+ arrowEl.style.top = `${top}px`;
138
+ arrowEl.style.transform = transform;
139
+ };
140
+ const applyLeftTop = (left, top) => {
141
+ const contentEl = contentRef.current;
142
+ if (!contentEl)
143
+ return;
144
+ const prev = lastPosRef.current;
145
+ if (prev && prev.left === left && prev.top === top)
146
+ return;
147
+ lastPosRef.current = { left, top };
148
+ contentEl.style.left = `${left}px`;
149
+ contentEl.style.top = `${top}px`;
150
+ };
151
+ const computeRelativePosition = () => {
152
+ const triggerEl = triggerRef.current;
153
+ const contentEl = contentRef.current;
154
+ if (!triggerEl || !contentEl)
155
+ return;
156
+ const o = latestRef.current;
157
+ applyFitSize();
158
+ const [left, top, { arrowX, arrowY, arrowPos }] = getPosition(triggerEl, contentEl, {
159
+ position: o.position,
160
+ gap: o.gap,
161
+ offset: o.offset,
162
+ align: o.align,
163
+ refWindow: true,
164
+ });
165
+ applyLeftTop(left, top);
166
+ applyArrow(arrowX, arrowY, arrowPos);
167
+ };
168
+ const computePointPosition = () => {
169
+ const contentEl = contentRef.current;
170
+ if (!contentEl)
171
+ return;
172
+ const point = pointRef.current;
173
+ if (!point)
174
+ return;
175
+ const [left, top] = getPointPosition(point, contentEl);
176
+ applyLeftTop(left, top);
177
+ };
178
+ const scheduleComputePosition = () => {
179
+ if (!showRef.current)
180
+ return;
181
+ if (rafRef.current !== null)
182
+ return;
183
+ rafRef.current = requestAnimationFrame(() => {
184
+ rafRef.current = null;
185
+ if (!showRef.current)
186
+ return;
187
+ ensureBaseStyle();
188
+ if (latestRef.current.trigger === "contextmenu") {
189
+ computePointPosition();
190
+ return;
191
+ }
192
+ computeRelativePosition();
193
+ });
37
194
  };
38
195
  const handleShow = () => {
39
- if (disabled)
196
+ const opts = latestRef.current;
197
+ if (opts.disabled)
40
198
  return;
41
- if (state.show &&
42
- (trigger !== "hover" || (trigger === "hover" && !touchable))) {
199
+ clearAllTimers();
200
+ if (showRef.current &&
201
+ (opts.trigger !== "hover" ||
202
+ (opts.trigger === "hover" && !opts.touchable))) {
203
+ ensureBaseStyle();
204
+ computeRelativePosition();
205
+ setContentVisible(true);
43
206
  return;
44
207
  }
45
- statusRef.current = "showing";
46
- state.show = true;
208
+ phaseRef.current = "showing";
209
+ if (!showRef.current) {
210
+ lastPosRef.current = null;
211
+ lastArrowRef.current = null;
212
+ arrowElRef.current = null;
213
+ setShow(true);
214
+ }
47
215
  timerRef.current = setTimeout(() => {
48
- if (statusRef.current !== "showing")
216
+ if (phaseRef.current !== "showing")
49
217
  return;
50
- requestAnimationFrame(() => {
51
- if (statusRef.current !== "showing")
218
+ rafRef.current = requestAnimationFrame(() => {
219
+ rafRef.current = null;
220
+ if (phaseRef.current !== "showing")
52
221
  return;
53
- const [left, top, { arrowX, arrowY, arrowPos }] = getPosition(triggerRef.current, contentRef.current, {
54
- position,
55
- gap,
56
- offset,
57
- align,
58
- refWindow,
59
- });
60
- state.style = {
61
- ...state.style,
62
- opacity: 1,
63
- transform: "none",
64
- left,
65
- top,
66
- };
67
- state.arrowProps = {
68
- left: arrowX,
69
- top: arrowY,
70
- pos: arrowPos,
71
- };
72
- onVisibleChange?.(true);
222
+ if (!contentRef.current)
223
+ return;
224
+ ensureBaseStyle();
225
+ if (opts.trigger === "contextmenu") {
226
+ computePointPosition();
227
+ }
228
+ else {
229
+ computeRelativePosition();
230
+ }
231
+ setContentVisible(true);
232
+ opts.onVisibleChange?.(true);
73
233
  clearTimer();
74
- statusRef.current = "";
234
+ phaseRef.current = "";
75
235
  });
76
- }, showDelay);
236
+ }, opts.showDelay);
77
237
  };
78
238
  const handleHide = () => {
79
- if (!state.show)
239
+ if (!showRef.current)
80
240
  return;
81
- statusRef.current = "hiding";
241
+ clearAllTimers();
242
+ phaseRef.current = "hiding";
82
243
  timerRef.current = setTimeout(() => {
83
- if (statusRef.current !== "hiding") {
244
+ if (phaseRef.current !== "hiding") {
84
245
  clearTimer();
85
246
  return;
86
247
  }
87
- state.style = {
88
- ...state.style,
89
- opacity: 0,
90
- transform: "translate(0, 2px)",
91
- };
92
- setTimeout(() => {
93
- state.show = false;
94
- clearTimer();
95
- onVisibleChange?.(false);
96
- statusRef.current = "";
248
+ setContentVisible(false);
249
+ afterHideTimerRef.current = setTimeout(() => {
250
+ afterHideTimerRef.current = null;
251
+ setShow(false);
252
+ clearAllTimers();
253
+ latestRef.current.onVisibleChange?.(false);
254
+ phaseRef.current = "";
97
255
  }, 160);
98
- }, hideDelay);
256
+ }, latestRef.current.hideDelay);
99
257
  };
100
258
  const handleToggle = (action) => {
101
259
  if (action !== undefined) {
102
260
  action ? handleShow() : handleHide();
103
261
  return;
104
262
  }
105
- state.show ? handleHide() : handleShow();
106
- };
107
- const eventMaps = useCreation(() => ({
108
- click: {
109
- onClick: () => handleToggle(true),
110
- },
111
- hover: {
112
- onMouseEnter: () => handleToggle(true),
113
- onMouseLeave: () => handleToggle(false),
114
- },
115
- focus: {
116
- onFocus: () => handleToggle(true),
117
- onBlur: () => handleToggle(false),
118
- },
119
- contextmenu: {
120
- onContextMenu: (e) => {
121
- e.preventDefault();
122
- e.stopPropagation();
123
- if (state.show) {
124
- const [left, top] = getPointPosition(e, contentRef.current);
125
- state.style = {
126
- ...state.style,
127
- left,
128
- top,
129
- };
130
- return;
131
- }
132
- state.show = true;
133
- timerRef.current = setTimeout(() => {
134
- const [left, top] = getPointPosition(e, contentRef.current);
135
- state.style = {
136
- ...state.style,
137
- opacity: 1,
138
- transform: "none",
139
- left,
140
- top,
263
+ showRef.current ? handleHide() : handleShow();
264
+ };
265
+ const hideRef = useRef(handleHide);
266
+ const toggleRef = useRef(handleToggle);
267
+ hideRef.current = handleHide;
268
+ toggleRef.current = handleToggle;
269
+ const doHide = useMemo(() => () => hideRef.current(), []);
270
+ const doToggle = useMemo(() => (action) => toggleRef.current(action), []);
271
+ const eventMaps = useMemo(() => {
272
+ return {
273
+ click: {
274
+ onClick: () => doToggle(true),
275
+ },
276
+ hover: {
277
+ onMouseEnter: () => doToggle(true),
278
+ onMouseLeave: () => doToggle(false),
279
+ },
280
+ focus: {
281
+ onFocus: () => doToggle(true),
282
+ onBlur: () => doToggle(false),
283
+ },
284
+ contextmenu: {
285
+ onContextMenu: (e) => {
286
+ e.preventDefault();
287
+ e.stopPropagation();
288
+ pointRef.current = {
289
+ pageX: e.pageX,
290
+ pageY: e.pageY,
141
291
  };
142
- clearTimer();
143
- onVisibleChange?.(true);
144
- }, showDelay);
292
+ if (showRef.current) {
293
+ ensureBaseStyle();
294
+ computePointPosition();
295
+ return;
296
+ }
297
+ clearAllTimers();
298
+ phaseRef.current = "showing";
299
+ lastPosRef.current = null;
300
+ lastArrowRef.current = null;
301
+ arrowElRef.current = null;
302
+ setShow(true);
303
+ timerRef.current = setTimeout(() => {
304
+ if (phaseRef.current !== "showing")
305
+ return;
306
+ if (!contentRef.current)
307
+ return;
308
+ ensureBaseStyle();
309
+ computePointPosition();
310
+ setContentVisible(true);
311
+ clearTimer();
312
+ latestRef.current.onVisibleChange?.(true);
313
+ phaseRef.current = "";
314
+ }, latestRef.current.showDelay);
315
+ },
145
316
  },
146
- },
147
- none: {},
148
- }), []);
317
+ none: {},
318
+ };
319
+ }, [doToggle]);
149
320
  const contentTouch = useMemo(() => {
150
321
  if (!touchable)
151
322
  return {};
@@ -158,72 +329,132 @@ function Popup(props) {
158
329
  }
159
330
  return events;
160
331
  }, [touchable, trigger]);
161
- const computePosition = () => {
162
- if (!state.show)
163
- return;
164
- const [left, top, { arrowX, arrowY, arrowPos }] = getPosition(triggerRef.current, contentRef.current, {
165
- position,
166
- gap,
167
- offset,
168
- align,
169
- refWindow,
170
- });
171
- Object.assign(state, {
172
- style: { ...state.style, left, top },
173
- arrowProps: { left: arrowX, top: arrowY, pos: arrowPos },
174
- });
175
- };
176
332
  const { observe, unobserve, disconnect } = useResizeObserver();
177
333
  useEffect(() => {
178
- if (trigger === "contextmenu" || !observe)
179
- return;
180
- triggerRef.current && observe(triggerRef.current, computePosition);
181
- if (!watchResize || !contentRef.current)
334
+ if (!observe)
182
335
  return;
183
- observe(contentRef.current, computePosition);
336
+ const triggerEl = triggerRef.current;
337
+ const contentEl = contentRef.current;
338
+ if (triggerEl)
339
+ observe(triggerEl, scheduleComputePosition);
340
+ if (contentEl)
341
+ observe(contentEl, scheduleComputePosition);
184
342
  return () => {
185
- if (!watchResize || !contentRef.current)
186
- return;
187
- unobserve(contentRef.current);
188
- triggerRef.current && unobserve(triggerRef.current);
343
+ if (contentEl)
344
+ unobserve(contentEl);
345
+ if (triggerEl)
346
+ unobserve(triggerEl);
189
347
  disconnect();
190
348
  };
191
- }, [watchResize, contentRef.current, triggerRef.current]);
349
+ }, [trigger, observe, unobserve, disconnect, show]);
192
350
  useLayoutEffect(() => {
193
- if (!fitSize || !state.show)
351
+ if (!show)
194
352
  return;
195
- const vertical = ["top", "bottom"].includes(position);
196
- const size = triggerRef.current?.[vertical ? "offsetWidth" : "offsetHeight"];
197
- state.style = { ...state.style, [vertical ? "width" : "height"]: size };
198
- }, [state.show, fitSize]);
353
+ ensureBaseStyle();
354
+ if (latestRef.current.trigger === "contextmenu") {
355
+ computePointPosition();
356
+ }
357
+ else {
358
+ computeRelativePosition();
359
+ }
360
+ }, [show]);
199
361
  useLayoutEffect(() => {
200
- handleToggle(visible);
362
+ doToggle(visible);
201
363
  }, [visible]);
202
364
  useEffect(() => {
203
365
  return () => {
204
- clearTimer();
366
+ clearAllTimers();
205
367
  };
206
368
  }, []);
207
- return (jsxs(Fragment, { children: [Children.map(children, (child) => {
208
- if (!isValidElement(child))
209
- return;
210
- const { className, ...restProps } = child.props;
211
- Object.keys(eventMaps[trigger]).map((evt) => {
212
- if (!restProps[evt])
213
- return;
214
- const fn = eventMaps[trigger][evt];
215
- eventMaps[trigger][evt] = (e) => {
216
- fn();
217
- restProps[evt](e);
218
- };
219
- });
220
- return cloneElement(child, {
221
- ref: triggerRef,
222
- className,
223
- ...restProps,
224
- ...eventMaps[trigger],
369
+ const mouseUpHandlerRef = useRef(() => { });
370
+ mouseUpHandlerRef.current = (e) => {
371
+ if (!showRef.current)
372
+ return;
373
+ const triggerEl = triggerRef.current;
374
+ const contentEl = contentRef.current;
375
+ if (!triggerEl || !contentEl)
376
+ return;
377
+ const tar = e.target;
378
+ if (triggerEl.contains(tar) || contentEl.contains(tar))
379
+ return;
380
+ doHide();
381
+ };
382
+ const onGlobalMouseUp = useMemo(() => (e) => mouseUpHandlerRef.current(e), []);
383
+ useMouseUp(onGlobalMouseUp);
384
+ useEffect(() => {
385
+ if (!show)
386
+ return;
387
+ if (typeof window === "undefined")
388
+ return;
389
+ const onScrollOrResize = debounce({ delay: 160 }, () => {
390
+ scheduleComputePosition();
391
+ });
392
+ window.addEventListener("scroll", onScrollOrResize, {
393
+ passive: true,
394
+ capture: true,
395
+ });
396
+ return () => {
397
+ window.removeEventListener("scroll", onScrollOrResize, true);
398
+ };
399
+ }, [show]);
400
+ const mergeRefs = (...refs) => {
401
+ return (node) => {
402
+ for (const ref of refs) {
403
+ if (!ref)
404
+ continue;
405
+ if (typeof ref === "function") {
406
+ ref(node);
407
+ }
408
+ else {
409
+ ref.current = node;
410
+ }
411
+ }
412
+ };
413
+ };
414
+ return (jsxs(Fragment, { children: [(() => {
415
+ const events = eventMaps[trigger];
416
+ const items = Children.toArray(children);
417
+ const canAttachRef = (el) => {
418
+ if (!isValidElement(el))
419
+ return false;
420
+ const t = el.type;
421
+ if (typeof t === "string")
422
+ return true;
423
+ if (t?.prototype?.isReactComponent)
424
+ return true;
425
+ if (t?.$$typeof === Symbol.for("react.forward_ref"))
426
+ return true;
427
+ return false;
428
+ };
429
+ if (items.length !== 1) {
430
+ return (jsx("div", { ref: triggerRef, ...events, className: 'i-popup-trigger', style: { display: "inline-block" }, children: children }));
431
+ }
432
+ const only = items[0];
433
+ if (!isValidElement(only) || !canAttachRef(only)) {
434
+ return (jsx("div", { ref: triggerRef, ...events, className: 'i-popup-trigger', style: { display: "inline-block" }, children: only }));
435
+ }
436
+ const { className: childClassName, ...restProps } = only.props;
437
+ const nextProps = { ...restProps };
438
+ for (const evt of Object.keys(events)) {
439
+ const theirs = restProps[evt];
440
+ const ours = events[evt];
441
+ nextProps[evt] =
442
+ typeof theirs === "function"
443
+ ? (e) => {
444
+ ours(e);
445
+ theirs(e);
446
+ }
447
+ : ours;
448
+ }
449
+ return cloneElement(only, {
450
+ ref: mergeRefs(only.ref, triggerRef),
451
+ className: childClassName,
452
+ ...nextProps,
225
453
  });
226
- }), state.show && (jsx(Content, { ref: contentRef, arrow: arrow && trigger !== "contextmenu", style: { ...style, ...state.style }, arrowProps: state.arrowProps, className: className, ...contentTouch, trigger: triggerRef.current, getContainer: getContainer, children: content }))] }));
454
+ })(), show && (jsx(Content, { ref: contentRef, arrow: arrow && trigger !== "contextmenu", style: {
455
+ ...style,
456
+ position: "fixed",
457
+ }, className: className, ...contentTouch, trigger: triggerRef.current, children: content }))] }));
227
458
  }
228
459
 
229
460
  export { Popup as default };