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