@homecode/ui 5.1.3 → 5.1.5

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.
@@ -6,6 +6,105 @@ import 'compareq';
6
6
  import 'lodash.pick';
7
7
  import '../../tools/queryParams.js';
8
8
 
9
+ const ZERO_BOUNDARY_FIT = {
10
+ left: 0,
11
+ top: 0,
12
+ maxWidth: null,
13
+ };
14
+ function viewportClientRectEdges() {
15
+ const vv = typeof window !== 'undefined' && window.visualViewport
16
+ ? window.visualViewport
17
+ : null;
18
+ if (vv) {
19
+ return {
20
+ left: vv.offsetLeft,
21
+ top: vv.offsetTop,
22
+ right: vv.offsetLeft + vv.width,
23
+ bottom: vv.offsetTop + vv.height,
24
+ };
25
+ }
26
+ const d = document.documentElement;
27
+ return {
28
+ left: 0,
29
+ top: 0,
30
+ right: d.clientWidth,
31
+ bottom: d.clientHeight,
32
+ };
33
+ }
34
+ function domRectToEdges(r) {
35
+ return {
36
+ left: r.left,
37
+ top: r.top,
38
+ right: r.right,
39
+ bottom: r.bottom,
40
+ };
41
+ }
42
+ function intersectEdges(a, b) {
43
+ const left = Math.max(a.left, b.left);
44
+ const top = Math.max(a.top, b.top);
45
+ const right = Math.min(a.right, b.right);
46
+ const bottom = Math.min(a.bottom, b.bottom);
47
+ if (left >= right || top >= bottom)
48
+ return undefined;
49
+ return { left, top, right, bottom };
50
+ }
51
+ function shrinkEdgesUniform(outer, top, right, bottom, left) {
52
+ return {
53
+ left: outer.left + left,
54
+ top: outer.top + top,
55
+ right: outer.right - right,
56
+ bottom: outer.bottom - bottom,
57
+ };
58
+ }
59
+ function edgesWidth(edges) {
60
+ return Math.max(0, edges.right - edges.left);
61
+ }
62
+ function popupBoundaryPadding(baseGap, offset) {
63
+ const n = (num) => Number(num) || 0;
64
+ return {
65
+ top: baseGap + n(offset?.top),
66
+ right: baseGap + n(offset?.right),
67
+ bottom: baseGap + n(offset?.bottom),
68
+ left: baseGap + n(offset?.left),
69
+ };
70
+ }
71
+ function popupBoundaryEdges({ boundary, boundaryMountSelector, inline, padding, }) {
72
+ let outer = viewportClientRectEdges();
73
+ const mode = boundary ?? (inline ? 'viewport' : 'clipping');
74
+ if (mode === 'clipping' && !inline) {
75
+ const mountElem = document.querySelector(boundaryMountSelector);
76
+ if (mountElem instanceof Element) {
77
+ outer =
78
+ intersectEdges(outer, domRectToEdges(mountElem.getBoundingClientRect())) ?? outer;
79
+ }
80
+ }
81
+ const inner = shrinkEdgesUniform(outer, padding.top, padding.right, padding.bottom, padding.left);
82
+ return inner.left < inner.right && inner.top < inner.bottom ? inner : outer;
83
+ }
84
+ function constrainAxisShift(pref, lo, hi) {
85
+ if (lo > hi)
86
+ return lo;
87
+ return Math.min(Math.max(pref, lo), hi);
88
+ }
89
+ /** Minimal shift to move `rect` into `inner`, preferring zero. */
90
+ function shiftMarginsIntoBounds(rect, inner) {
91
+ const dxMin = inner.left - rect.left;
92
+ const dxMax = inner.right - rect.right;
93
+ const dyMin = inner.top - rect.top;
94
+ const dyMax = inner.bottom - rect.bottom;
95
+ return {
96
+ marginLeft: constrainAxisShift(0, dxMin, dxMax),
97
+ marginTop: constrainAxisShift(0, dyMin, dyMax),
98
+ };
99
+ }
100
+ function fitRectToBoundary(rect, boundary, maxWidth) {
101
+ const { marginLeft, marginTop } = shiftMarginsIntoBounds(rect, boundary);
102
+ return {
103
+ left: Number.isFinite(marginLeft) ? marginLeft : 0,
104
+ top: Number.isFinite(marginTop) ? marginTop : 0,
105
+ maxWidth,
106
+ };
107
+ }
9
108
  const childs = {};
10
109
  let popupIds = 0;
11
110
  const getId = () => ++popupIds;
@@ -28,4 +127,4 @@ function unsetChild(rootId, id) {
28
127
  delete childs[rootId];
29
128
  }
30
129
 
31
- export { childs, getId, getPopupId, isLastChild, setChild, unsetChild };
130
+ export { ZERO_BOUNDARY_FIT, childs, constrainAxisShift, domRectToEdges, edgesWidth, fitRectToBoundary, getId, getPopupId, intersectEdges, isLastChild, popupBoundaryEdges, popupBoundaryPadding, setChild, shiftMarginsIntoBounds, shrinkEdgesUniform, unsetChild, viewportClientRectEdges };
@@ -1,9 +1,10 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
- import { getId, getPopupId, isLastChild, unsetChild, childs, setChild } from './Popup.helpers.js';
2
+ import { ZERO_BOUNDARY_FIT, getId, getPopupId, popupBoundaryEdges, popupBoundaryPadding, edgesWidth, fitRectToBoundary, isLastChild, unsetChild, childs, setChild } from './Popup.helpers.js';
3
3
  import { observe, unobserve } from '../../tools/resizeObserver.js';
4
4
  import { Component, createRef } from 'react';
5
5
  import { Paranja } from '../Paranja/Paranja.js';
6
6
  import { Portal } from '../Portal/Portal.js';
7
+ import { config } from '../../tools/config.js';
7
8
  import S from './Popup.styl.js';
8
9
  import Time from 'timen';
9
10
  import cn from 'classnames';
@@ -14,7 +15,7 @@ import throttle from '../../tools/throttle.js';
14
15
 
15
16
  const ANIMATION_DURATION = 100;
16
17
  const OFFSET_GAP = 10;
17
- const INITIAL_OFFSET = { top: 0, left: 0 };
18
+ const BOUNDARY_FIT_EPSILON = 1;
18
19
  class Popup extends Component {
19
20
  rootElem = createRef();
20
21
  triggerElem = createRef();
@@ -25,6 +26,9 @@ class Popup extends Component {
25
26
  if (elem) {
26
27
  this.unsubscribeSizeChange();
27
28
  this.subscribeSizeChange();
29
+ if (this.state.isOpen) {
30
+ this.updateBounds();
31
+ }
28
32
  }
29
33
  };
30
34
  focused = false;
@@ -33,12 +37,12 @@ class Popup extends Component {
33
37
  subscribedSizeChange = false;
34
38
  pointerDownTarget = null;
35
39
  isPointerPressedInside = false;
36
- needDropOffset = false;
37
40
  id;
38
41
  parentPopupContent;
39
42
  timers = Time.create();
40
43
  scrollParent;
41
- offset = { ...INITIAL_OFFSET };
44
+ shiftOuterRafId = 0;
45
+ shiftInnerRafId = 0;
42
46
  static defaultProps = {
43
47
  size: 'm',
44
48
  direction: '',
@@ -51,6 +55,7 @@ class Popup extends Component {
51
55
  animating: false,
52
56
  direction: this.props.direction,
53
57
  triggerBounds: null,
58
+ boundaryFit: ZERO_BOUNDARY_FIT,
54
59
  };
55
60
  constructor(props) {
56
61
  super(props);
@@ -58,6 +63,11 @@ class Popup extends Component {
58
63
  }
59
64
  componentDidMount() {
60
65
  const { hoverControl, focusControl } = this.props;
66
+ const vv = isBrowser ? window.visualViewport : null;
67
+ if (vv) {
68
+ vv.addEventListener('resize', this.onBoundaryGeometryChange);
69
+ vv.addEventListener('scroll', this.onBoundaryGeometryChange);
70
+ }
61
71
  const parentPopupContent = this.triggerElem.current.closest(`.${S.content}`);
62
72
  document.addEventListener('pointerdown', this.onDocPointerDown, true);
63
73
  document.addEventListener('pointerup', this.onDocPointerUp, true);
@@ -73,8 +83,17 @@ class Popup extends Component {
73
83
  if (hoverControl)
74
84
  this.subscribeHoverControl();
75
85
  this.subscribeScroll();
86
+ if (this.state.isOpen &&
87
+ this.state.isContentVisible &&
88
+ this.containerElem) {
89
+ this.scheduleComputeShift();
90
+ }
76
91
  }
77
- componentDidUpdate(prevProps) {
92
+ onBoundaryGeometryChange = throttle(() => {
93
+ if (this.state.isOpen)
94
+ this.scheduleComputeShift();
95
+ }, 100);
96
+ componentDidUpdate(prevProps, prevState) {
78
97
  const { isOpen, disabled, hoverControl } = this.props;
79
98
  if (disabled !== prevProps.disabled) {
80
99
  this.setState({ isOpen: false }); // close when receive disabled=true
@@ -87,10 +106,29 @@ class Popup extends Component {
87
106
  if (typeof isOpen === 'boolean' && isOpen !== prevProps.isOpen) {
88
107
  isOpen ? this.open() : this.close();
89
108
  }
109
+ const justOpened = !prevState.isOpen &&
110
+ this.state.isOpen &&
111
+ this.state.isContentVisible &&
112
+ Boolean(this.containerElem);
113
+ const triggerBoundsCommitted = this.state.isOpen &&
114
+ this.state.isContentVisible &&
115
+ Boolean(this.containerElem) &&
116
+ prevState.triggerBounds === null &&
117
+ this.state.triggerBounds !== null;
118
+ if (justOpened || triggerBoundsCommitted) {
119
+ this.scheduleComputeShift();
120
+ }
90
121
  }
91
122
  componentWillUnmount() {
92
123
  this.timers.clear();
93
- document.removeEventListener('keyup', this.onDocKeyUp, true);
124
+ cancelAnimationFrame(this.shiftOuterRafId);
125
+ cancelAnimationFrame(this.shiftInnerRafId);
126
+ const vv = isBrowser ? window.visualViewport : null;
127
+ if (vv) {
128
+ vv.removeEventListener('resize', this.onBoundaryGeometryChange);
129
+ vv.removeEventListener('scroll', this.onBoundaryGeometryChange);
130
+ }
131
+ document.removeEventListener('keyup', this.onDocKeyUp);
94
132
  if (this.scrollParent) {
95
133
  this.scrollParent.removeEventListener('scroll', this.close);
96
134
  }
@@ -133,7 +171,7 @@ class Popup extends Component {
133
171
  document.removeEventListener('pointerup', this.checkHover);
134
172
  }
135
173
  updateBounds() {
136
- if (this.state.animating || !this.containerElem)
174
+ if (!this.containerElem)
137
175
  return;
138
176
  if (!this.triggerElem.current)
139
177
  return;
@@ -151,42 +189,78 @@ class Popup extends Component {
151
189
  Object.entries(bounds).forEach(([key, value]) => {
152
190
  this.triggerElem.current.style[key] = value;
153
191
  });
154
- this.updateOffset();
155
192
  this.setState({ triggerBounds: bounds });
193
+ this.scheduleComputeShift();
156
194
  }, 200, { trailing: true });
157
- prevContentBounds = { width: 0, height: 0 };
158
- updateOffset = () => {
159
- const content = this.containerElem.getBoundingClientRect();
160
- if (!content.height ||
161
- !content.width ||
162
- this.prevContentBounds.width !== content.width ||
163
- this.prevContentBounds.height !== content.height) {
164
- this.prevContentBounds = content;
165
- Time.after(100, this.updateOffset);
195
+ scheduleComputeShift() {
196
+ if (!isBrowser)
197
+ return;
198
+ cancelAnimationFrame(this.shiftOuterRafId);
199
+ cancelAnimationFrame(this.shiftInnerRafId);
200
+ this.shiftOuterRafId = window.requestAnimationFrame(() => {
201
+ this.shiftOuterRafId = 0;
202
+ this.shiftInnerRafId = window.requestAnimationFrame(() => {
203
+ this.shiftInnerRafId = 0;
204
+ this.computeBoundaryShift();
205
+ });
206
+ });
207
+ }
208
+ computeBoundaryShift = () => {
209
+ if (!this.containerElem ||
210
+ !this.state.isOpen ||
211
+ !this.state.isContentVisible) {
166
212
  return;
167
213
  }
168
- const { offset } = this;
169
- const { innerHeight, innerWidth } = window;
170
- const bottom = content.top + content.height + OFFSET_GAP - offset.top;
171
- const right = content.left + content.width + OFFSET_GAP - offset.left;
172
- if (content.top < 0) {
173
- offset.top = -content.top + OFFSET_GAP;
174
- }
175
- else if (bottom > innerHeight) {
176
- offset.top = innerHeight - bottom;
214
+ const el = this.containerElem;
215
+ const wrapper = el.parentElement;
216
+ if (!(wrapper instanceof HTMLElement)) {
217
+ return;
177
218
  }
178
- if (content.left < 0) {
179
- offset.left = -content.left + OFFSET_GAP;
219
+ const { boundary, boundaryMountSelector, inline, shiftPadding, offset } = this.props;
220
+ const bounds = popupBoundaryEdges({
221
+ boundary,
222
+ boundaryMountSelector: boundaryMountSelector ?? `#${config.appOverlayId}`,
223
+ inline,
224
+ padding: popupBoundaryPadding(OFFSET_GAP + (shiftPadding ?? 0), offset),
225
+ });
226
+ const availWidth = edgesWidth(bounds);
227
+ const fitWidth = availWidth >= BOUNDARY_FIT_EPSILON ? Math.floor(availWidth) : null;
228
+ const measured = this.measureBoundaryRect(el, wrapper, fitWidth, bounds);
229
+ this.setBoundaryFit(fitRectToBoundary(measured.rect, bounds, measured.maxWidth));
230
+ };
231
+ measureBoundaryRect(el, wrapper, fitWidth, bounds) {
232
+ const transform = wrapper.style.transform;
233
+ const maxWidth = el.style.maxWidth;
234
+ const overflowX = el.style.overflowX;
235
+ const overflowY = el.style.overflowY;
236
+ wrapper.style.transform = '';
237
+ el.style.maxWidth = '';
238
+ el.style.overflowX = '';
239
+ el.style.overflowY = '';
240
+ void wrapper.offsetHeight;
241
+ let rect = el.getBoundingClientRect();
242
+ const shouldClamp = fitWidth !== null &&
243
+ rect.width > edgesWidth(bounds) + BOUNDARY_FIT_EPSILON;
244
+ if (shouldClamp) {
245
+ el.style.maxWidth = `${fitWidth}px`;
246
+ el.style.overflowX = 'auto';
247
+ el.style.overflowY = 'hidden';
248
+ void el.offsetHeight;
249
+ rect = el.getBoundingClientRect();
180
250
  }
181
- else if (right > innerWidth) {
182
- offset.left = innerWidth - right;
251
+ wrapper.style.transform = transform;
252
+ el.style.maxWidth = maxWidth;
253
+ el.style.overflowX = overflowX;
254
+ el.style.overflowY = overflowY;
255
+ return { rect, maxWidth: shouldClamp ? fitWidth : null };
256
+ }
257
+ setBoundaryFit(boundaryFit) {
258
+ const prev = this.state.boundaryFit;
259
+ if (prev.left !== boundaryFit.left ||
260
+ prev.top !== boundaryFit.top ||
261
+ prev.maxWidth !== boundaryFit.maxWidth) {
262
+ this.setState({ boundaryFit });
183
263
  }
184
- this.applyOffset();
185
- };
186
- applyOffset() {
187
- const { left, top } = this.offset;
188
- this.containerElem.style.marginTop = `${top}px`;
189
- this.containerElem.style.marginLeft = `${left}px`;
190
264
  }
191
265
  checkHover = debounce((e) => {
192
266
  if (this.isPointerPressedInside)
@@ -234,17 +308,15 @@ class Popup extends Component {
234
308
  }
235
309
  onScroll = throttle(e => {
236
310
  if (!this.state.isOpen) {
237
- const { top, left } = this.offset;
238
- if (left || top) {
239
- this.offset = { ...INITIAL_OFFSET };
240
- this.applyOffset();
311
+ const { top, left, maxWidth } = this.state.boundaryFit;
312
+ if (left || top || maxWidth !== null) {
313
+ this.setState({ boundaryFit: ZERO_BOUNDARY_FIT });
241
314
  }
242
315
  return;
243
316
  }
244
317
  // if scrolling outside this popup - close it
245
318
  if (!this.isPointerOver(e.target, S.content) &&
246
319
  !childs[this.id]?.length) {
247
- this.needDropOffset = true;
248
320
  this.close();
249
321
  }
250
322
  }, 200);
@@ -301,7 +373,10 @@ class Popup extends Component {
301
373
  this.updateBounds();
302
374
  this.subscribeSizeChange();
303
375
  this.subscribeScroll();
304
- this.setState({ isContentVisible: true });
376
+ this.setState({
377
+ isContentVisible: true,
378
+ boundaryFit: ZERO_BOUNDARY_FIT,
379
+ });
305
380
  this.changeState(true, this.afterOpen);
306
381
  if (rootPopupId)
307
382
  setChild(rootPopupId, this.id);
@@ -332,16 +407,12 @@ class Popup extends Component {
332
407
  this.props.onAfterOpen?.();
333
408
  };
334
409
  afterClose = () => {
335
- this.setState({ isContentVisible: false });
336
- this.dropOffset();
410
+ this.setState({
411
+ isContentVisible: false,
412
+ boundaryFit: ZERO_BOUNDARY_FIT,
413
+ });
337
414
  this.props.onAfterClose?.();
338
415
  };
339
- dropOffset() {
340
- if (!this.needDropOffset)
341
- return;
342
- this.offset = { ...INITIAL_OFFSET };
343
- this.applyOffset();
344
- }
345
416
  toggle = throttle(() => {
346
417
  this.state.isOpen ? this.close() : this.open();
347
418
  }, 100);
@@ -372,7 +443,7 @@ class Popup extends Component {
372
443
  }
373
444
  renderContent() {
374
445
  const { content, contentProps = {}, wrapperProps = {}, size, disabled, inline, outlined, animated, paranja, blur, round, elevation, } = this.props;
375
- const { isOpen, isContentVisible, animating, direction, triggerBounds, rootPopupId, } = this.state;
446
+ const { isOpen, isContentVisible, animating, direction, triggerBounds, rootPopupId, boundaryFit, } = this.state;
376
447
  if (disabled)
377
448
  return null;
378
449
  const wrapperClasses = cn(S.contentWrapper, inline && S.inline, isOpen && S.isOpen, animating && S.animating, wrapperProps.className);
@@ -380,11 +451,24 @@ class Popup extends Component {
380
451
  const [axis, float] = direction.split('-');
381
452
  const classes = cn(S.content, outlined && S.outlined, animated && animating && S.animating, elevation && S[`elevation-${elevation}`], S[`size-${size}`], S[`axis-${axis}`], S[`float-${float || 'middle'}`], blur && S.blur, round && S.round, contentProps.className);
382
453
  if (trigger && !inline && triggerBounds) {
383
- wrapperProps.style = { ...triggerBounds };
454
+ const shiftTf = boundaryFit.left !== 0 || boundaryFit.top !== 0
455
+ ? `translate3d(${boundaryFit.left}px, ${boundaryFit.top}px, 0)`
456
+ : undefined;
457
+ wrapperProps.style = {
458
+ ...triggerBounds,
459
+ ...wrapperProps.style,
460
+ ...(shiftTf ? { transform: shiftTf } : {}),
461
+ };
384
462
  }
385
463
  const contentNode = (jsx("div", { ...wrapperProps, className: wrapperClasses, children: jsxs("div", { ...contentProps, ref: this.onContainerElemRef, className: classes, suppressHydrationWarning: true, "data-popup-id": this.id, "data-root-popup-id": rootPopupId, style: {
386
- marginTop: 100,
387
- marginLeft: this.offset.left,
464
+ ...(contentProps.style ?? {}),
465
+ ...(boundaryFit.maxWidth !== null && boundaryFit.maxWidth > 0
466
+ ? {
467
+ maxWidth: boundaryFit.maxWidth,
468
+ overflowX: 'auto',
469
+ overflowY: 'hidden',
470
+ }
471
+ : {}),
388
472
  }, children: [paranja && !rootPopupId && (jsx(Paranja, { visible: isContentVisible, blur: blur })), isContentVisible && jsx(Fragment, { children: content })] }) }));
389
473
  if (inline)
390
474
  return contentNode;
@@ -1,6 +1,6 @@
1
1
  import styleInject from '../../../node_modules/style-inject/dist/style-inject.es.js';
2
2
 
3
- var css_248z = ".Popup_root__uQ-fP{display:inline-block;position:relative}.Popup_contentWrapper__2yi-2{opacity:0;pointer-events:none;position:absolute}.Popup_contentWrapper__2yi-2.Popup_animating__kR0qF{transition:opacity .1s ease-out}.Popup_contentWrapper__2yi-2.Popup_isOpen__BRIdP{opacity:1;pointer-events:all}.Popup_contentWrapper__2yi-2.Popup_inline__1-l1S.Popup_isOpen__BRIdP{position:relative}.Popup_contentWrapper__2yi-2:not(.Popup_inline__1-l1S),.Popup_contentWrapper__2yi-2:not(.Popup_inline__1-l1S)>.Popup_content__e8Qyu{position:absolute}.Popup_trigger__jQNaQ{cursor:pointer}.Popup_trigger__jQNaQ.Popup_isOpen__BRIdP{position:relative;z-index:11}.Popup_trigger__jQNaQ.Popup_disabled__DlE9y{opacity:.4;pointer-events:none}.Popup_content__e8Qyu{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--decent-color);box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50);max-width:70vw;min-width:100%;overflow:hidden;position:relative;transform-origin:top center;z-index:11}.Popup_content__e8Qyu:before{background-color:var(--accent-color-alpha-50);bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.Popup_content__e8Qyu.Popup_blur__1hfU8{-webkit-backdrop-filter:blur(50px);backdrop-filter:blur(50px);background-color:var(--decent-color-alpha-500)}.Popup_content__e8Qyu.Popup_size-xs__7QR-d{border-radius:4px}.Popup_content__e8Qyu.Popup_size-xs__7QR-d.Popup_round__7rD1m{border-radius:12px}.Popup_content__e8Qyu.Popup_size-s__UmixP{border-radius:4px}.Popup_content__e8Qyu.Popup_size-s__UmixP.Popup_round__7rD1m{border-radius:16px}.Popup_content__e8Qyu.Popup_size-m__FYpTL{border-radius:6px}.Popup_content__e8Qyu.Popup_size-m__FYpTL.Popup_round__7rD1m{border-radius:20px}.Popup_content__e8Qyu.Popup_size-l__BTS57{border-radius:8px}.Popup_content__e8Qyu.Popup_size-l__BTS57.Popup_round__7rD1m{border-radius:24px}.Popup_content__e8Qyu.Popup_size-xl__fSfCc{border-radius:10px}.Popup_content__e8Qyu.Popup_size-xl__fSfCc.Popup_round__7rD1m{border-radius:28px}.Popup_content__e8Qyu.Popup_elevation-1__vmP3e{box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 0 var(--p-3) 2px var(--decent-color-alpha-500)}.Popup_content__e8Qyu.Popup_elevation-2__Ci4sI{box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 0 var(--p-5) 2px var(--decent-color-alpha-500)}.Popup_content__e8Qyu.Popup_outlined__g3cJV:after{border-radius:inherit;bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.Popup_isOpen__BRIdP .Popup_content__e8Qyu{opacity:1;pointer-events:all;transform:scaleX(1)}.Popup_animating__kR0qF{transition:70ms ease-out;transition-property:transform,opacity,margin}.Popup_axis-top__BaLgG{bottom:100%}.Popup_axis-bottom__hZwwr{top:100%}.Popup_axis-right__LMYVy{left:100%}.Popup_axis-left__SFKm-{right:100%}.Popup_float-top__8SQAu{bottom:0}.Popup_float-right__mdm-3{left:0}.Popup_float-bottom__7flve{top:0}.Popup_float-left__tz7fX{right:0}.Popup_axis-bottom__hZwwr,.Popup_axis-top__BaLgG{transform:scaleY(.5)}.Popup_axis-bottom__hZwwr.Popup_float-middle__Dmnn1,.Popup_axis-top__BaLgG.Popup_float-middle__Dmnn1{left:50%;transform:translateX(-50%) scaleY(.5)}.Popup_isOpen__BRIdP .Popup_axis-bottom__hZwwr.Popup_float-middle__Dmnn1,.Popup_isOpen__BRIdP .Popup_axis-top__BaLgG.Popup_float-middle__Dmnn1{transform:translateX(-50%) scaleX(1)}.Popup_axis-left__SFKm-,.Popup_axis-right__LMYVy{transform:scaleX(.5)}.Popup_axis-left__SFKm-.Popup_float-middle__Dmnn1,.Popup_axis-right__LMYVy.Popup_float-middle__Dmnn1{top:50%;transform:translateY(-50%) scaleX(.5)}.Popup_isOpen__BRIdP .Popup_axis-left__SFKm-.Popup_float-middle__Dmnn1,.Popup_isOpen__BRIdP .Popup_axis-right__LMYVy.Popup_float-middle__Dmnn1{transform:translateY(-50%) scaleX(1)}.Popup_axis-top__BaLgG.Popup_float-middle__Dmnn1{transform-origin:bottom center}.Popup_axis-top__BaLgG.Popup_float-right__mdm-3{transform-origin:bottom left}.Popup_axis-top__BaLgG.Popup_float-left__tz7fX{transform-origin:bottom right}.Popup_axis-bottom__hZwwr.Popup_float-middle__Dmnn1{transform-origin:top center}.Popup_axis-bottom__hZwwr.Popup_float-right__mdm-3{transform-origin:top left}.Popup_axis-bottom__hZwwr.Popup_float-left__tz7fX{transform-origin:top right}.Popup_axis-right__LMYVy.Popup_float-middle__Dmnn1{transform-origin:center left}.Popup_axis-right__LMYVy.Popup_float-top__8SQAu{transform-origin:bottom left}.Popup_axis-right__LMYVy.Popup_float-bottom__7flve{transform-origin:top left}.Popup_axis-left__SFKm-.Popup_float-middle__Dmnn1{transform-origin:center right}.Popup_axis-left__SFKm-.Popup_float-top__8SQAu{transform-origin:bottom right}.Popup_axis-left__SFKm-.Popup_float-bottom__7flve{transform-origin:top right}";
3
+ var css_248z = ".Popup_root__uQ-fP{display:inline-block;position:relative}.Popup_contentWrapper__2yi-2{opacity:0;pointer-events:none;position:absolute}.Popup_contentWrapper__2yi-2.Popup_animating__kR0qF{transition:opacity .1s ease-out}.Popup_contentWrapper__2yi-2.Popup_isOpen__BRIdP{opacity:1;pointer-events:all}.Popup_contentWrapper__2yi-2.Popup_inline__1-l1S.Popup_isOpen__BRIdP{position:relative}.Popup_contentWrapper__2yi-2:not(.Popup_inline__1-l1S),.Popup_contentWrapper__2yi-2:not(.Popup_inline__1-l1S)>.Popup_content__e8Qyu{position:absolute}.Popup_trigger__jQNaQ{cursor:pointer}.Popup_trigger__jQNaQ.Popup_isOpen__BRIdP{position:relative;z-index:11}.Popup_trigger__jQNaQ.Popup_disabled__DlE9y{opacity:.4;pointer-events:none}.Popup_content__e8Qyu{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--decent-color);box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50);box-sizing:border-box;max-width:min(min(560px,70vw),calc(100svw - 16px));min-width:100%;overflow:hidden;position:relative;transform-origin:top center;z-index:11}.Popup_content__e8Qyu:before{background-color:var(--accent-color-alpha-50);bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.Popup_content__e8Qyu.Popup_blur__1hfU8{-webkit-backdrop-filter:blur(50px);backdrop-filter:blur(50px);background-color:var(--decent-color-alpha-500)}.Popup_content__e8Qyu.Popup_size-xs__7QR-d{border-radius:4px}.Popup_content__e8Qyu.Popup_size-xs__7QR-d.Popup_round__7rD1m{border-radius:12px}.Popup_content__e8Qyu.Popup_size-s__UmixP{border-radius:4px}.Popup_content__e8Qyu.Popup_size-s__UmixP.Popup_round__7rD1m{border-radius:16px}.Popup_content__e8Qyu.Popup_size-m__FYpTL{border-radius:6px}.Popup_content__e8Qyu.Popup_size-m__FYpTL.Popup_round__7rD1m{border-radius:20px}.Popup_content__e8Qyu.Popup_size-l__BTS57{border-radius:8px}.Popup_content__e8Qyu.Popup_size-l__BTS57.Popup_round__7rD1m{border-radius:24px}.Popup_content__e8Qyu.Popup_size-xl__fSfCc{border-radius:10px}.Popup_content__e8Qyu.Popup_size-xl__fSfCc.Popup_round__7rD1m{border-radius:28px}.Popup_content__e8Qyu.Popup_elevation-1__vmP3e{box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 0 var(--p-3) 2px var(--decent-color-alpha-500)}.Popup_content__e8Qyu.Popup_elevation-2__Ci4sI{box-shadow:inset 0 0 0 1px var(--accent-color-alpha-50),0 0 var(--p-5) 2px var(--decent-color-alpha-500)}.Popup_content__e8Qyu.Popup_outlined__g3cJV:after{border-radius:inherit;bottom:0;content:\"\";left:0;pointer-events:none;position:absolute;right:0;top:0}.Popup_isOpen__BRIdP .Popup_content__e8Qyu{opacity:1;pointer-events:all;transform:scaleX(1)}.Popup_animating__kR0qF{transition:70ms ease-out;transition-property:transform,opacity,margin}.Popup_axis-top__BaLgG{bottom:100%}.Popup_axis-bottom__hZwwr{top:100%}.Popup_axis-right__LMYVy{left:100%}.Popup_axis-left__SFKm-{right:100%}.Popup_float-top__8SQAu{bottom:0}.Popup_float-right__mdm-3{left:0}.Popup_float-bottom__7flve{top:0}.Popup_float-left__tz7fX{right:0}.Popup_axis-bottom__hZwwr,.Popup_axis-top__BaLgG{transform:scaleY(.5)}.Popup_axis-bottom__hZwwr.Popup_float-middle__Dmnn1,.Popup_axis-top__BaLgG.Popup_float-middle__Dmnn1{left:50%;transform:translateX(-50%) scaleY(.5)}.Popup_isOpen__BRIdP .Popup_axis-bottom__hZwwr.Popup_float-middle__Dmnn1,.Popup_isOpen__BRIdP .Popup_axis-top__BaLgG.Popup_float-middle__Dmnn1{transform:translateX(-50%) scaleX(1)}.Popup_axis-left__SFKm-,.Popup_axis-right__LMYVy{transform:scaleX(.5)}.Popup_axis-left__SFKm-.Popup_float-middle__Dmnn1,.Popup_axis-right__LMYVy.Popup_float-middle__Dmnn1{top:50%;transform:translateY(-50%) scaleX(.5)}.Popup_isOpen__BRIdP .Popup_axis-left__SFKm-.Popup_float-middle__Dmnn1,.Popup_isOpen__BRIdP .Popup_axis-right__LMYVy.Popup_float-middle__Dmnn1{transform:translateY(-50%) scaleX(1)}.Popup_axis-top__BaLgG.Popup_float-middle__Dmnn1{transform-origin:bottom center}.Popup_axis-top__BaLgG.Popup_float-right__mdm-3{transform-origin:bottom left}.Popup_axis-top__BaLgG.Popup_float-left__tz7fX{transform-origin:bottom right}.Popup_axis-bottom__hZwwr.Popup_float-middle__Dmnn1{transform-origin:top center}.Popup_axis-bottom__hZwwr.Popup_float-right__mdm-3{transform-origin:top left}.Popup_axis-bottom__hZwwr.Popup_float-left__tz7fX{transform-origin:top right}.Popup_axis-right__LMYVy.Popup_float-middle__Dmnn1{transform-origin:center left}.Popup_axis-right__LMYVy.Popup_float-top__8SQAu{transform-origin:bottom left}.Popup_axis-right__LMYVy.Popup_float-bottom__7flve{transform-origin:top left}.Popup_axis-left__SFKm-.Popup_float-middle__Dmnn1{transform-origin:center right}.Popup_axis-left__SFKm-.Popup_float-top__8SQAu{transform-origin:bottom right}.Popup_axis-left__SFKm-.Popup_float-bottom__7flve{transform-origin:top right}";
4
4
  var S = {"root":"Popup_root__uQ-fP","contentWrapper":"Popup_contentWrapper__2yi-2","animating":"Popup_animating__kR0qF","isOpen":"Popup_isOpen__BRIdP","inline":"Popup_inline__1-l1S","content":"Popup_content__e8Qyu","trigger":"Popup_trigger__jQNaQ","disabled":"Popup_disabled__DlE9y","blur":"Popup_blur__1hfU8","size-xs":"Popup_size-xs__7QR-d","round":"Popup_round__7rD1m","size-s":"Popup_size-s__UmixP","size-m":"Popup_size-m__FYpTL","size-l":"Popup_size-l__BTS57","size-xl":"Popup_size-xl__fSfCc","elevation-1":"Popup_elevation-1__vmP3e","elevation-2":"Popup_elevation-2__Ci4sI","outlined":"Popup_outlined__g3cJV","axis-top":"Popup_axis-top__BaLgG","axis-bottom":"Popup_axis-bottom__hZwwr","axis-right":"Popup_axis-right__LMYVy","axis-left":"Popup_axis-left__SFKm-","float-top":"Popup_float-top__8SQAu","float-right":"Popup_float-right__mdm-3","float-bottom":"Popup_float-bottom__7flve","float-left":"Popup_float-left__tz7fX","float-middle":"Popup_float-middle__Dmnn1"};
5
5
  styleInject(css_248z);
6
6
 
@@ -61,6 +61,16 @@ class Virtualized extends Component {
61
61
  return true;
62
62
  if (nextProps.wrapElem !== this.props.wrapElem)
63
63
  return true;
64
+ if (nextProps.renderItem !== this.props.renderItem)
65
+ return true;
66
+ if (nextProps.totalCount !== this.props.totalCount)
67
+ return true;
68
+ if (nextProps.itemHeight !== this.props.itemHeight)
69
+ return true;
70
+ if (nextProps.overlapCount !== this.props.overlapCount)
71
+ return true;
72
+ if (nextProps.getItemProps !== this.props.getItemProps)
73
+ return true;
64
74
  if (!compare(nextState, this.state))
65
75
  return true;
66
76
  return false;
@@ -1,8 +1,9 @@
1
+ import * as H from './Popup.helpers';
1
2
  import * as T from './Popup.types';
2
3
  import { Component } from 'react';
3
4
  export declare const ANIMATION_DURATION = 100;
4
5
  export type PopupProps = T.Props;
5
- export declare class Popup extends Component<T.Props> {
6
+ export declare class Popup extends Component<T.Props, T.State> {
6
7
  rootElem: import("react").RefObject<HTMLDivElement>;
7
8
  triggerElem: import("react").RefObject<HTMLDivElement>;
8
9
  containerElem: HTMLDivElement;
@@ -13,31 +14,22 @@ export declare class Popup extends Component<T.Props> {
13
14
  subscribedSizeChange: boolean;
14
15
  pointerDownTarget: any;
15
16
  isPointerPressedInside: boolean;
16
- needDropOffset: boolean;
17
17
  id: any;
18
18
  parentPopupContent: any;
19
19
  timers: any;
20
20
  scrollParent: any;
21
- offset: {
22
- top: number;
23
- left: number;
24
- };
21
+ shiftOuterRafId: number;
22
+ shiftInnerRafId: number;
25
23
  static defaultProps: {
26
24
  size: string;
27
25
  direction: string;
28
26
  animated: boolean;
29
27
  };
30
- state: {
31
- rootPopupId: any;
32
- isOpen: boolean;
33
- isContentVisible: boolean;
34
- animating: boolean;
35
- direction: T.Direction;
36
- triggerBounds: any;
37
- };
28
+ state: T.State;
38
29
  constructor(props: any);
39
30
  componentDidMount(): void;
40
- componentDidUpdate(prevProps: T.Props): void;
31
+ onBoundaryGeometryChange: any;
32
+ componentDidUpdate(prevProps: T.Props, prevState: T.State): void;
41
33
  componentWillUnmount(): void;
42
34
  subscribeSizeChange(): void;
43
35
  subscribeScroll(): void;
@@ -47,12 +39,13 @@ export declare class Popup extends Component<T.Props> {
47
39
  unsubscribeHoverControl(): void;
48
40
  updateBounds(): void;
49
41
  updateBoundsThrottled: any;
50
- prevContentBounds: {
51
- width: number;
52
- height: number;
42
+ scheduleComputeShift(): void;
43
+ computeBoundaryShift: () => void;
44
+ measureBoundaryRect(el: HTMLDivElement, wrapper: HTMLElement, fitWidth: number | null, bounds: H.ClientRectEdges): {
45
+ rect: DOMRect;
46
+ maxWidth: number | null;
53
47
  };
54
- updateOffset: () => void;
55
- applyOffset(): void;
48
+ setBoundaryFit(boundaryFit: H.BoundaryFit): void;
56
49
  checkHover: any;
57
50
  isControllable: () => boolean;
58
51
  isLastClickInside: () => any;
@@ -73,7 +66,6 @@ export declare class Popup extends Component<T.Props> {
73
66
  changeState(isOpen: boolean, callback: any): void;
74
67
  afterOpen: () => void;
75
68
  afterClose: () => void;
76
- dropOffset(): void;
77
69
  toggle: any;
78
70
  renderTrigger(): JSX.Element;
79
71
  renderContent(): JSX.Element;
@@ -1,4 +1,47 @@
1
1
  type Id = number;
2
+ export type ClientRectEdges = {
3
+ readonly left: number;
4
+ readonly top: number;
5
+ readonly right: number;
6
+ readonly bottom: number;
7
+ };
8
+ type Offset = {
9
+ top?: number;
10
+ right?: number;
11
+ bottom?: number;
12
+ left?: number;
13
+ };
14
+ type BoundaryPadding = {
15
+ readonly top: number;
16
+ readonly right: number;
17
+ readonly bottom: number;
18
+ readonly left: number;
19
+ };
20
+ export type BoundaryFit = {
21
+ readonly left: number;
22
+ readonly top: number;
23
+ readonly maxWidth: number | null;
24
+ };
25
+ export declare const ZERO_BOUNDARY_FIT: BoundaryFit;
26
+ export declare function viewportClientRectEdges(): ClientRectEdges;
27
+ export declare function domRectToEdges(r: DOMRectReadOnly): ClientRectEdges;
28
+ export declare function intersectEdges(a: ClientRectEdges, b: ClientRectEdges): ClientRectEdges | undefined;
29
+ export declare function shrinkEdgesUniform(outer: ClientRectEdges, top: number, right: number, bottom: number, left: number): ClientRectEdges;
30
+ export declare function edgesWidth(edges: ClientRectEdges): number;
31
+ export declare function popupBoundaryPadding(baseGap: number, offset?: Offset): BoundaryPadding;
32
+ export declare function popupBoundaryEdges({ boundary, boundaryMountSelector, inline, padding, }: {
33
+ boundary?: 'viewport' | 'clipping';
34
+ boundaryMountSelector: string;
35
+ inline?: boolean;
36
+ padding: BoundaryPadding;
37
+ }): ClientRectEdges;
38
+ export declare function constrainAxisShift(pref: number, lo: number, hi: number): number;
39
+ /** Minimal shift to move `rect` into `inner`, preferring zero. */
40
+ export declare function shiftMarginsIntoBounds(rect: DOMRectReadOnly, inner: ClientRectEdges): {
41
+ marginLeft: number;
42
+ marginTop: number;
43
+ };
44
+ export declare function fitRectToBoundary(rect: DOMRectReadOnly, boundary: ClientRectEdges, maxWidth: number | null): BoundaryFit;
2
45
  export declare const childs: Record<Id, Id[]>;
3
46
  export declare const getId: () => number;
4
47
  export declare const getPopupId: (elem: any, attr?: string) => number;
@@ -1,5 +1,6 @@
1
1
  import { ComponentType, Size } from '../../types';
2
2
  import { DetailedHTMLProps, HTMLAttributes, ReactNode } from 'react';
3
+ import type { BoundaryFit } from './Popup.helpers';
3
4
  export type TriggerPropsType = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
4
5
  className?: string;
5
6
  };
@@ -26,6 +27,9 @@ export type Props = ComponentType & {
26
27
  bottom?: number;
27
28
  left?: number;
28
29
  };
30
+ shiftPadding?: number;
31
+ boundary?: 'viewport' | 'clipping';
32
+ boundaryMountSelector?: string;
29
33
  direction?: Direction;
30
34
  trigger?: ReactNode;
31
35
  triggerProps?: TriggerPropsType;
@@ -38,6 +42,11 @@ export type Props = ComponentType & {
38
42
  onAfterClose?: () => void;
39
43
  };
40
44
  export type State = {
45
+ rootPopupId: number | null;
41
46
  isOpen: boolean;
42
47
  isContentVisible: boolean;
48
+ animating: boolean;
49
+ direction: Direction | string;
50
+ triggerBounds: Record<string, number> | null;
51
+ boundaryFit: BoundaryFit;
43
52
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homecode/ui",
3
- "version": "5.1.3",
3
+ "version": "5.1.5",
4
4
  "description": "React UI components library",
5
5
  "scripts": {
6
6
  "tests": "jest",