@nectary/components 5.29.1 → 5.30.0

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.
package/tooltip/index.js CHANGED
@@ -4,6 +4,7 @@ import { shouldReduceMotion, updateAttribute, updateBooleanAttribute, getAttribu
4
4
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
5
5
  import { rectOverlap } from "../utils/rect.js";
6
6
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
7
+ import { getPlacementContext, toLocalRect } from "../utils/placement.js";
7
8
  import { TooltipState } from "./tooltip-state.js";
8
9
  import { getPopOrientation, orientationValues, textAlignValues, typeValues } from "./utils.js";
9
10
  const templateHTML = '<style>:host{display:contents}#content-wrapper{padding-bottom:8px;filter:drop-shadow(var(--sinch-comp-tooltip-shadow))}:host([orientation=left]) #content-wrapper{padding-bottom:0;padding-right:8px}:host([orientation=right]) #content-wrapper{padding-bottom:0;padding-left:8px}:host([orientation^=bottom]) #content-wrapper{padding-bottom:0;padding-top:8px}#content{position:relative;display:block;max-width:300px;padding:2px 6px;box-sizing:border-box;background-color:var(--sinch-local-color-background);border-radius:var(--sinch-comp-tooltip-shape-radius);pointer-events:none;opacity:0;--sinch-local-color-background:var(--sinch-comp-tooltip-color-background);--sinch-global-color-text:var(--sinch-comp-tooltip-color-text)}#text{word-break:break-word;pointer-events:none;--sinch-comp-text-font:var(--sinch-comp-tooltip-font-body)}#tip{position:absolute;left:50%;top:100%;transform:translateX(-50%) rotate(0);transform-origin:top center;fill:var(--sinch-local-color-background);pointer-events:none}#tip.hidden{display:none}:host([orientation=left]) #tip{transform:translateX(-50%) rotate(270deg);top:50%;left:100%}:host([orientation=right]) #tip{transform:translateX(-50%) rotate(90deg);top:50%;left:0}:host([orientation^=bottom]) #tip{transform:translateX(-50%) rotate(180deg);top:0}:host([text-align=right]) #text{--sinch-comp-text-align:right}:host([text-align=center]) #text{--sinch-comp-text-align:center}:host([text-align=left]) #text{--sinch-comp-text-align:left}</style><sinch-pop id="pop"><slot id="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><sinch-text id="text" type="s"></sinch-text><svg id="tip" width="8" height="4" aria-hidden="true"><path d="m4 4 4-4h-8l4 4Z"/></svg></div></div></sinch-pop>';
@@ -12,6 +13,10 @@ const SHOW_DELAY_SLOW = 1e3;
12
13
  const SHOW_DELAY_FAST = 250;
13
14
  const HIDE_DELAY = 0;
14
15
  const ANIMATION_DURATION = 100;
16
+ const OVERLAP_TOLERANCE = 1;
17
+ const MAX_ZERO_DIMENSION_PLACEMENT_RETRIES = 8;
18
+ const MIN_FIRST_REVEAL_STABILITY_FRAMES = 3;
19
+ const MAX_FIRST_REVEAL_STABILITY_FRAMES = 6;
15
20
  const template = document.createElement("template");
16
21
  template.innerHTML = templateHTML;
17
22
  class Tooltip extends NectaryElement {
@@ -21,11 +26,16 @@ class Tooltip extends NectaryElement {
21
26
  #$contentWrapper;
22
27
  #$tip;
23
28
  #$target;
24
- #controller = null;
29
+ #resizeObserver = null;
25
30
  #tooltipState;
26
31
  #animation = null;
27
32
  #shouldReduceMotion = false;
28
33
  #isSubscribed = false;
34
+ #controller;
35
+ #placementScheduled = false;
36
+ #zeroDimensionPlacementRetries = 0;
37
+ #revealRequestId = 0;
38
+ #hasCompletedFirstReveal = false;
29
39
  constructor() {
30
40
  super();
31
41
  const shadowRoot = this.attachShadow();
@@ -37,6 +47,7 @@ class Tooltip extends NectaryElement {
37
47
  this.#$tip = shadowRoot.querySelector("#tip");
38
48
  this.#$target = shadowRoot.querySelector("#target");
39
49
  this.#shouldReduceMotion = shouldReduceMotion();
50
+ this.#controller = null;
40
51
  this.#tooltipState = new TooltipState({
41
52
  showDelay: SHOW_DELAY_SLOW,
42
53
  hideDelay: this.#shouldReduceMotion ? HIDE_DELAY + ANIMATION_DURATION : HIDE_DELAY,
@@ -57,6 +68,10 @@ class Tooltip extends NectaryElement {
57
68
  this.#$pop.addEventListener("-close", this.#onPopClose, options);
58
69
  this.addEventListener("-show", this.#onShowReactHandler, options);
59
70
  this.addEventListener("-hide", this.#onHideReactHandler, options);
71
+ this.#resizeObserver = new ResizeObserver(() => {
72
+ this.#schedulePlacement();
73
+ });
74
+ this.#resizeObserver.observe(this.#$content);
60
75
  updateAttribute(this.#$pop, "orientation", getPopOrientation(this.orientation));
61
76
  updateBooleanAttribute(this.#$pop, "hide-outside-viewport", !this.showOutsideViewport);
62
77
  this.#updateText();
@@ -66,6 +81,8 @@ class Tooltip extends NectaryElement {
66
81
  this.#tooltipState.destroy();
67
82
  this.#controller.abort();
68
83
  this.#controller = null;
84
+ this.#resizeObserver?.disconnect();
85
+ this.#resizeObserver = null;
69
86
  }
70
87
  static get observedAttributes() {
71
88
  return [
@@ -197,9 +214,13 @@ class Tooltip extends NectaryElement {
197
214
  };
198
215
  // SHOW_DELAY ended, tooltip can be shown with animation
199
216
  #onStateShowEnd = () => {
217
+ const revealRequestId = ++this.#revealRequestId;
200
218
  this.#dispatchShowEvent();
201
219
  updateBooleanAttribute(this.#$pop, "open", true);
202
- requestAnimationFrame(this.#updateTipOrientation);
220
+ this.#schedulePlacement();
221
+ this.#scheduleReveal(revealRequestId);
222
+ };
223
+ #playShowAnimation() {
203
224
  if (this.#animation !== null) {
204
225
  this.#animation.updatePlaybackRate(1);
205
226
  this.#animation.play();
@@ -212,20 +233,64 @@ class Tooltip extends NectaryElement {
212
233
  fill: "forwards"
213
234
  });
214
235
  }
215
- };
236
+ }
237
+ #isRectStable(previousRect, nextRect) {
238
+ return Math.abs(previousRect.x - nextRect.x) < 0.5 && Math.abs(previousRect.y - nextRect.y) < 0.5 && Math.abs(previousRect.width - nextRect.width) < 0.5 && Math.abs(previousRect.height - nextRect.height) < 0.5;
239
+ }
240
+ #scheduleReveal(revealRequestId) {
241
+ const reveal = () => {
242
+ if (!this.isDomConnected || !this.#isOpen() || this.#revealRequestId !== revealRequestId) {
243
+ return;
244
+ }
245
+ this.#playShowAnimation();
246
+ this.#hasCompletedFirstReveal = true;
247
+ };
248
+ if (this.#hasCompletedFirstReveal) {
249
+ reveal();
250
+ return;
251
+ }
252
+ let previousRect = null;
253
+ let observedFrames = 0;
254
+ let remainingFrames = MAX_FIRST_REVEAL_STABILITY_FRAMES;
255
+ const waitForStableRect = () => {
256
+ if (!this.isDomConnected || !this.#isOpen() || this.#revealRequestId !== revealRequestId) {
257
+ return;
258
+ }
259
+ const nextRect = this.#$pop.popoverRect;
260
+ if (observedFrames >= MIN_FIRST_REVEAL_STABILITY_FRAMES && previousRect !== null && this.#isRectStable(previousRect, nextRect)) {
261
+ reveal();
262
+ return;
263
+ }
264
+ if (remainingFrames === 0) {
265
+ reveal();
266
+ return;
267
+ }
268
+ previousRect = nextRect;
269
+ observedFrames += 1;
270
+ remainingFrames -= 1;
271
+ requestAnimationFrame(waitForStableRect);
272
+ };
273
+ requestAnimationFrame(waitForStableRect);
274
+ }
216
275
  // HIDE_DELAY ended, begin tooltip hide animation
217
276
  #onStateHideStart = () => {
277
+ this.#revealRequestId += 1;
278
+ if (this.#animation === null) {
279
+ return;
280
+ }
218
281
  this.#animation.updatePlaybackRate(-1);
219
282
  this.#animation.play();
220
283
  };
221
284
  // Hide animation ended, tooltip can be hidden
222
285
  #onStateHideEnd = () => {
223
286
  if (this.#isOpen()) {
224
- this.#animation.finish();
287
+ this.#animation?.finish();
225
288
  this.#dispatchHideEvent();
226
289
  updateBooleanAttribute(this.#$pop, "open", false);
227
290
  }
228
291
  this.#resetTipOrientation();
292
+ this.#resetContentOffset();
293
+ this.#zeroDimensionPlacementRetries = 0;
229
294
  this.#unsubscribeMouseLeaveEvents();
230
295
  this.#unsubscribeScroll();
231
296
  };
@@ -233,26 +298,125 @@ class Tooltip extends NectaryElement {
233
298
  this.#$tip.style.top = "";
234
299
  this.#$tip.style.left = "";
235
300
  }
236
- #updateTipOrientation = () => {
301
+ #resetContentOffset() {
302
+ this.#$pop.style.removeProperty("--sinch-pop-offset-x");
303
+ this.#$pop.style.removeProperty("--sinch-pop-offset-y");
304
+ }
305
+ #schedulePlacement(resetZeroDimensionRetries = true) {
306
+ if (!this.#isOpen() || this.#placementScheduled) {
307
+ return;
308
+ }
309
+ if (resetZeroDimensionRetries) {
310
+ this.#zeroDimensionPlacementRetries = 0;
311
+ }
312
+ this.#placementScheduled = true;
313
+ requestAnimationFrame(() => {
314
+ this.#placementScheduled = false;
315
+ if (!this.isDomConnected || !this.#isOpen()) {
316
+ return;
317
+ }
318
+ this.#updatePlacement();
319
+ });
320
+ }
321
+ #applyContentOffset(offsetX, offsetY) {
322
+ if (offsetX === 0 && offsetY === 0) {
323
+ this.#$pop.style.removeProperty("--sinch-pop-offset-x");
324
+ this.#$pop.style.removeProperty("--sinch-pop-offset-y");
325
+ return;
326
+ }
327
+ this.#$pop.style.setProperty("--sinch-pop-offset-x", `${offsetX}px`);
328
+ this.#$pop.style.setProperty("--sinch-pop-offset-y", `${offsetY}px`);
329
+ }
330
+ #updatePlacement = () => {
331
+ if (!this.isDomConnected || !this.#isOpen()) {
332
+ return;
333
+ }
334
+ const popRect = this.#$pop.popoverRect;
335
+ if (popRect.width === 0 || popRect.height === 0) {
336
+ if (this.#zeroDimensionPlacementRetries >= MAX_ZERO_DIMENSION_PLACEMENT_RETRIES) {
337
+ return;
338
+ }
339
+ this.#zeroDimensionPlacementRetries += 1;
340
+ this.#schedulePlacement(false);
341
+ return;
342
+ }
343
+ this.#zeroDimensionPlacementRetries = 0;
344
+ const placementContext = getPlacementContext(this.#$pop);
345
+ this.#resetContentOffset();
346
+ this.#updateTipOrientation(placementContext);
347
+ const didOffset = this.#resolveOverlap(placementContext);
348
+ if (didOffset) {
349
+ requestAnimationFrame(() => {
350
+ if (!this.isDomConnected || !this.#isOpen()) {
351
+ return;
352
+ }
353
+ this.#updateTipOrientation(placementContext);
354
+ });
355
+ }
356
+ };
357
+ #resolveOverlap(placementContext) {
358
+ const orientation = this.orientation;
359
+ const targetRect = toLocalRect(this.#$pop.footprintRect, placementContext);
360
+ const contentRect = toLocalRect(this.#$content.getBoundingClientRect(), placementContext);
361
+ const tipRect = toLocalRect(this.#$tip.getBoundingClientRect(), placementContext);
362
+ const targetBottom = targetRect.y + targetRect.height;
363
+ const targetRight = targetRect.x + targetRect.width;
364
+ const bottomEdge = Math.max(contentRect.y + contentRect.height, tipRect.y + tipRect.height);
365
+ const topEdge = Math.min(contentRect.y, tipRect.y);
366
+ const rightEdge = Math.max(contentRect.x + contentRect.width, tipRect.x + tipRect.width);
367
+ const leftEdge = Math.min(contentRect.x, tipRect.x);
368
+ let offsetX = 0;
369
+ let offsetY = 0;
370
+ if (orientation.startsWith("top")) {
371
+ if (bottomEdge > targetRect.y + OVERLAP_TOLERANCE) {
372
+ offsetY = targetRect.y - bottomEdge;
373
+ }
374
+ } else if (orientation.startsWith("bottom")) {
375
+ if (topEdge < targetBottom - OVERLAP_TOLERANCE) {
376
+ offsetY = targetBottom - topEdge;
377
+ }
378
+ } else if (orientation === "left") {
379
+ if (rightEdge > targetRect.x + OVERLAP_TOLERANCE) {
380
+ offsetX = targetRect.x - rightEdge;
381
+ }
382
+ } else if (orientation === "right") {
383
+ if (leftEdge < targetRight - OVERLAP_TOLERANCE) {
384
+ offsetX = targetRight - leftEdge;
385
+ }
386
+ }
387
+ this.#applyContentOffset(offsetX, offsetY);
388
+ return offsetX !== 0 || offsetY !== 0;
389
+ }
390
+ #updateTipOrientation = (placementContext) => {
237
391
  const orient = this.orientation;
238
392
  if (!("footprintRect" in this.#$pop)) {
239
- requestAnimationFrame(this.#updateTipOrientation);
393
+ requestAnimationFrame(() => {
394
+ if (!this.isDomConnected || !this.#isOpen()) {
395
+ return;
396
+ }
397
+ this.#updateTipOrientation();
398
+ });
240
399
  return;
241
400
  }
242
- const targetRect = this.#$pop.footprintRect;
243
- const contentRect = this.#$content.getBoundingClientRect();
401
+ const ctx = placementContext ?? getPlacementContext(this.#$pop);
402
+ const targetRect = toLocalRect(this.#$pop.footprintRect, ctx);
403
+ const contentRect = toLocalRect(this.#$content.getBoundingClientRect(), ctx);
244
404
  const diffX = targetRect.x - contentRect.x;
245
405
  const diffY = targetRect.y - contentRect.y;
406
+ const targetWidth = targetRect.width;
407
+ const targetHeight = targetRect.height;
408
+ const contentWidth = contentRect.width;
409
+ const contentHeight = contentRect.height;
246
410
  if (orient === "left" || orient === "right") {
247
- const yPos = Math.max(TIP_SIZE, Math.min(diffY + targetRect.height / 2, contentRect.height - TIP_SIZE));
411
+ const yPos = Math.max(TIP_SIZE, Math.min(diffY + targetHeight / 2, contentHeight - TIP_SIZE));
248
412
  this.#$tip.style.top = `${yPos}px`;
249
413
  } else {
250
- let xPos = Math.max(TIP_SIZE, Math.min(diffX + targetRect.width / 2, contentRect.width - TIP_SIZE));
414
+ let xPos = Math.max(TIP_SIZE, Math.min(diffX + targetWidth / 2, contentWidth - TIP_SIZE));
251
415
  if (orient === "bottom-left" || orient === "top-left") {
252
- xPos = Math.max(xPos, contentRect.width * 0.75);
416
+ xPos = Math.max(xPos, contentWidth * 0.75);
253
417
  }
254
418
  if (orient === "bottom-right" || orient === "top-right") {
255
- xPos = Math.min(xPos, contentRect.width * 0.25);
419
+ xPos = Math.min(xPos, contentWidth * 0.25);
256
420
  }
257
421
  this.#$tip.style.left = `${xPos}px`;
258
422
  }
package/utils/dom.d.ts CHANGED
@@ -33,4 +33,6 @@ export declare const cloneNode: (el: Element, deep: boolean) => Element;
33
33
  export declare const shouldReduceMotion: () => boolean;
34
34
  export declare const isAttrEqual: (oldVal: string | null, newVal: string | null) => boolean;
35
35
  export declare const getScrollableParents: (node: HTMLElement | null) => (HTMLElement | Document)[];
36
+ export declare const isTransformedElement: (element: HTMLElement | null) => element is HTMLElement;
37
+ export declare const getTransformedAncestor: (node: HTMLElement | null) => HTMLElement | null;
36
38
  export {};
package/utils/dom.js CHANGED
@@ -153,6 +153,38 @@ const getScrollableParents = (node) => {
153
153
  scrollableParents.push(document);
154
154
  return scrollableParents;
155
155
  };
156
+ const isTransformedElement = (element) => {
157
+ if (element == null) {
158
+ return false;
159
+ }
160
+ const style = getComputedStyle(element);
161
+ const backdropFilter = style.getPropertyValue("backdrop-filter");
162
+ const hasTransform = style.transform !== "none" || style.perspective !== "none" || style.filter !== "none" || backdropFilter !== "" && backdropFilter !== "none";
163
+ const hasWillChange = style.willChange.split(",").map((value) => value.trim().toLowerCase()).some((value) => value === "transform" || value === "perspective" || value === "filter" || value === "backdrop-filter");
164
+ return hasTransform || hasWillChange;
165
+ };
166
+ const getTransformedAncestor = (node) => {
167
+ let current = node;
168
+ while (current != null) {
169
+ if (current !== node) {
170
+ if (isTransformedElement(current)) {
171
+ return current;
172
+ }
173
+ }
174
+ const parent = current.parentElement;
175
+ if (parent != null) {
176
+ current = parent;
177
+ continue;
178
+ }
179
+ const root = current.getRootNode();
180
+ if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
181
+ current = root.host;
182
+ continue;
183
+ }
184
+ break;
185
+ }
186
+ return null;
187
+ };
156
188
  export {
157
189
  attrValueToInteger,
158
190
  attrValueToPixels,
@@ -165,10 +197,12 @@ export {
165
197
  getIntegerAttribute,
166
198
  getLiteralAttribute,
167
199
  getScrollableParents,
200
+ getTransformedAncestor,
168
201
  hasClass,
169
202
  isAttrEqual,
170
203
  isAttrTrue,
171
204
  isLiteralValue,
205
+ isTransformedElement,
172
206
  setClass,
173
207
  shouldReduceMotion,
174
208
  updateAttribute,
package/utils/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, subscribeContext } from "./context.js";
2
2
  import { CSV_DELIMITER, getFirstCsvValue, packCsv, unpackCsv, updateCsv } from "./csv.js";
3
- import { attrValueToInteger, attrValueToPixels, clampNumber, cloneNode, getAttribute, getBooleanAttribute, getCssVar, getCssVars, getIntegerAttribute, getLiteralAttribute, getScrollableParents, hasClass, isAttrEqual, isAttrTrue, isLiteralValue, setClass, shouldReduceMotion, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute, updateIntegerAttribute, updateLiteralAttribute } from "./dom.js";
3
+ import { attrValueToInteger, attrValueToPixels, clampNumber, cloneNode, getAttribute, getBooleanAttribute, getCssVar, getCssVars, getIntegerAttribute, getLiteralAttribute, getScrollableParents, getTransformedAncestor, hasClass, isAttrEqual, isAttrTrue, isLiteralValue, isTransformedElement, setClass, shouldReduceMotion, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute, updateIntegerAttribute, updateLiteralAttribute } from "./dom.js";
4
4
  import { NectaryElement, defineCustomElement, pascalToKebabCase, registerComponent, resetNectaryRegistry, setNectaryRegistry } from "./element.js";
5
5
  import { getRect, getTargetRect, rectOverlap } from "./rect.js";
6
6
  import { getFirstFocusableElement, getFirstSlotElement, isElementFocused } from "./slot.js";
@@ -37,6 +37,7 @@ export {
37
37
  getTargetByAttribute,
38
38
  getTargetIndexInParent,
39
39
  getTargetRect,
40
+ getTransformedAncestor,
40
41
  getUid,
41
42
  hasClass,
42
43
  isAttrEqual,
@@ -45,6 +46,7 @@ export {
45
46
  isEmojiString,
46
47
  isLiteralValue,
47
48
  isTargetEqual,
49
+ isTransformedElement,
48
50
  packCsv,
49
51
  parseMarkdown,
50
52
  pascalToKebabCase,
@@ -0,0 +1,13 @@
1
+ import type { TRect } from '../types';
2
+ export type TPlacementContext = {
3
+ transformedAncestor: HTMLElement | null;
4
+ ancestorRect: DOMRect | null;
5
+ scaleX: number;
6
+ scaleY: number;
7
+ boundsLeft: number;
8
+ boundsTop: number;
9
+ boundsWidth: number;
10
+ boundsHeight: number;
11
+ };
12
+ export declare const getPlacementContext: (node: HTMLElement | null, ancestorHint?: HTMLElement | null) => TPlacementContext;
13
+ export declare const toLocalRect: (rect: TRect, placementContext: TPlacementContext) => TRect;
@@ -0,0 +1,78 @@
1
+ import { isTransformedElement, getTransformedAncestor } from "./dom.js";
2
+ const resolveTransformedAncestor = (node, ancestorHint) => {
3
+ if (node != null && ancestorHint != null && ancestorHint.isConnected && isTransformedElement(ancestorHint) && (ancestorHint === node || ancestorHint.contains(node))) {
4
+ return ancestorHint;
5
+ }
6
+ return getTransformedAncestor(node);
7
+ };
8
+ const getTransformedAncestorScale = (ancestor) => {
9
+ if (ancestor == null) {
10
+ return { scaleX: 1, scaleY: 1 };
11
+ }
12
+ const transform = getComputedStyle(ancestor).transform;
13
+ let matrixScaleX = null;
14
+ let matrixScaleY = null;
15
+ if (transform !== "none") {
16
+ try {
17
+ const matrix = new DOMMatrixReadOnly(transform);
18
+ matrixScaleX = Math.hypot(matrix.a, matrix.b);
19
+ matrixScaleY = Math.hypot(matrix.c, matrix.d);
20
+ } catch {
21
+ matrixScaleX = null;
22
+ matrixScaleY = null;
23
+ }
24
+ }
25
+ if (matrixScaleX !== null && matrixScaleY !== null) {
26
+ return {
27
+ scaleX: Number.isFinite(matrixScaleX) && matrixScaleX > 0 ? matrixScaleX : 1,
28
+ scaleY: Number.isFinite(matrixScaleY) && matrixScaleY > 0 ? matrixScaleY : 1
29
+ };
30
+ }
31
+ const rect = ancestor.getBoundingClientRect();
32
+ const baseWidth = ancestor.offsetWidth;
33
+ const baseHeight = ancestor.offsetHeight;
34
+ const scaleX = baseWidth > 0 ? rect.width / baseWidth : 1;
35
+ const scaleY = baseHeight > 0 ? rect.height / baseHeight : 1;
36
+ return {
37
+ scaleX: Number.isFinite(scaleX) && scaleX > 0 ? scaleX : 1,
38
+ scaleY: Number.isFinite(scaleY) && scaleY > 0 ? scaleY : 1
39
+ };
40
+ };
41
+ const getPlacementContext = (node, ancestorHint) => {
42
+ const transformedAncestor = resolveTransformedAncestor(node, ancestorHint);
43
+ const ancestorRect = transformedAncestor?.getBoundingClientRect() ?? null;
44
+ const ancestorClientLeft = transformedAncestor?.clientLeft ?? 0;
45
+ const ancestorClientTop = transformedAncestor?.clientTop ?? 0;
46
+ const ancestorClientWidth = transformedAncestor?.clientWidth ?? 0;
47
+ const ancestorClientHeight = transformedAncestor?.clientHeight ?? 0;
48
+ const { scaleX, scaleY } = getTransformedAncestorScale(transformedAncestor);
49
+ const boundsLeft = ancestorRect != null ? ancestorRect.x + ancestorClientLeft * scaleX : 0;
50
+ const boundsTop = ancestorRect != null ? ancestorRect.y + ancestorClientTop * scaleY : 0;
51
+ const boundsWidth = ancestorRect != null && ancestorClientWidth > 0 ? ancestorClientWidth : ancestorRect != null ? ancestorRect.width / scaleX : window.innerWidth;
52
+ const boundsHeight = ancestorRect != null && ancestorClientHeight > 0 ? ancestorClientHeight : ancestorRect != null ? ancestorRect.height / scaleY : window.innerHeight;
53
+ return {
54
+ transformedAncestor,
55
+ ancestorRect,
56
+ scaleX,
57
+ scaleY,
58
+ boundsLeft,
59
+ boundsTop,
60
+ boundsWidth,
61
+ boundsHeight
62
+ };
63
+ };
64
+ const toLocalRect = (rect, placementContext) => {
65
+ if (placementContext.transformedAncestor == null) {
66
+ return rect;
67
+ }
68
+ return {
69
+ x: (rect.x - placementContext.boundsLeft) / placementContext.scaleX,
70
+ y: (rect.y - placementContext.boundsTop) / placementContext.scaleY,
71
+ width: rect.width / placementContext.scaleX,
72
+ height: rect.height / placementContext.scaleY
73
+ };
74
+ };
75
+ export {
76
+ getPlacementContext,
77
+ toLocalRect
78
+ };