@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/bundle.js +497 -107
- package/button/index.js +37 -5
- package/button/types.d.ts +4 -0
- package/package.json +1 -1
- package/pop/index.d.ts +6 -0
- package/pop/index.js +140 -93
- package/pop/utils.d.ts +20 -0
- package/pop/utils.js +46 -0
- package/tooltip/index.js +176 -12
- package/utils/dom.d.ts +2 -0
- package/utils/dom.js +34 -0
- package/utils/index.js +3 -1
- package/utils/placement.d.ts +13 -0
- package/utils/placement.js +78 -0
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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(
|
|
393
|
+
requestAnimationFrame(() => {
|
|
394
|
+
if (!this.isDomConnected || !this.#isOpen()) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.#updateTipOrientation();
|
|
398
|
+
});
|
|
240
399
|
return;
|
|
241
400
|
}
|
|
242
|
-
const
|
|
243
|
-
const
|
|
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 +
|
|
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 +
|
|
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,
|
|
416
|
+
xPos = Math.max(xPos, contentWidth * 0.75);
|
|
253
417
|
}
|
|
254
418
|
if (orient === "bottom-right" || orient === "top-right") {
|
|
255
|
-
xPos = Math.min(xPos,
|
|
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
|
+
};
|