@nectary/components 5.29.1 → 5.29.2

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 CHANGED
@@ -143,6 +143,38 @@ const getScrollableParents = (node) => {
143
143
  scrollableParents.push(document);
144
144
  return scrollableParents;
145
145
  };
146
+ const isTransformedElement = (element) => {
147
+ if (element == null) {
148
+ return false;
149
+ }
150
+ const style = getComputedStyle(element);
151
+ const backdropFilter = style.getPropertyValue("backdrop-filter");
152
+ const hasTransform = style.transform !== "none" || style.perspective !== "none" || style.filter !== "none" || backdropFilter !== "" && backdropFilter !== "none";
153
+ const hasWillChange = style.willChange.split(",").map((value) => value.trim().toLowerCase()).some((value) => value === "transform" || value === "perspective" || value === "filter" || value === "backdrop-filter");
154
+ return hasTransform || hasWillChange;
155
+ };
156
+ const getTransformedAncestor = (node) => {
157
+ let current = node;
158
+ while (current != null) {
159
+ if (current !== node) {
160
+ if (isTransformedElement(current)) {
161
+ return current;
162
+ }
163
+ }
164
+ const parent = current.parentElement;
165
+ if (parent != null) {
166
+ current = parent;
167
+ continue;
168
+ }
169
+ const root = current.getRootNode();
170
+ if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
171
+ current = root.host;
172
+ continue;
173
+ }
174
+ break;
175
+ }
176
+ return null;
177
+ };
146
178
  class NectaryElementBase extends HTMLElement {
147
179
  static _isGlobal = false;
148
180
  static get elementName() {
@@ -3138,6 +3170,79 @@ const createThrottle = (delayFn, cancelFn) => (cb) => {
3138
3170
  };
3139
3171
  };
3140
3172
  const throttleAnimationFrame = createThrottle(globalThis.requestAnimationFrame, globalThis.cancelAnimationFrame);
3173
+ const resolveTransformedAncestor = (node, ancestorHint) => {
3174
+ if (node != null && ancestorHint != null && ancestorHint.isConnected && isTransformedElement(ancestorHint) && (ancestorHint === node || ancestorHint.contains(node))) {
3175
+ return ancestorHint;
3176
+ }
3177
+ return getTransformedAncestor(node);
3178
+ };
3179
+ const getTransformedAncestorScale = (ancestor) => {
3180
+ if (ancestor == null) {
3181
+ return { scaleX: 1, scaleY: 1 };
3182
+ }
3183
+ const transform = getComputedStyle(ancestor).transform;
3184
+ let matrixScaleX = null;
3185
+ let matrixScaleY = null;
3186
+ if (transform !== "none") {
3187
+ try {
3188
+ const matrix = new DOMMatrixReadOnly(transform);
3189
+ matrixScaleX = Math.hypot(matrix.a, matrix.b);
3190
+ matrixScaleY = Math.hypot(matrix.c, matrix.d);
3191
+ } catch {
3192
+ matrixScaleX = null;
3193
+ matrixScaleY = null;
3194
+ }
3195
+ }
3196
+ if (matrixScaleX !== null && matrixScaleY !== null) {
3197
+ return {
3198
+ scaleX: Number.isFinite(matrixScaleX) && matrixScaleX > 0 ? matrixScaleX : 1,
3199
+ scaleY: Number.isFinite(matrixScaleY) && matrixScaleY > 0 ? matrixScaleY : 1
3200
+ };
3201
+ }
3202
+ const rect = ancestor.getBoundingClientRect();
3203
+ const baseWidth = ancestor.offsetWidth;
3204
+ const baseHeight = ancestor.offsetHeight;
3205
+ const scaleX = baseWidth > 0 ? rect.width / baseWidth : 1;
3206
+ const scaleY = baseHeight > 0 ? rect.height / baseHeight : 1;
3207
+ return {
3208
+ scaleX: Number.isFinite(scaleX) && scaleX > 0 ? scaleX : 1,
3209
+ scaleY: Number.isFinite(scaleY) && scaleY > 0 ? scaleY : 1
3210
+ };
3211
+ };
3212
+ const getPlacementContext = (node, ancestorHint) => {
3213
+ const transformedAncestor = resolveTransformedAncestor(node, ancestorHint);
3214
+ const ancestorRect = transformedAncestor?.getBoundingClientRect() ?? null;
3215
+ const ancestorClientLeft = transformedAncestor?.clientLeft ?? 0;
3216
+ const ancestorClientTop = transformedAncestor?.clientTop ?? 0;
3217
+ const ancestorClientWidth = transformedAncestor?.clientWidth ?? 0;
3218
+ const ancestorClientHeight = transformedAncestor?.clientHeight ?? 0;
3219
+ const { scaleX, scaleY } = getTransformedAncestorScale(transformedAncestor);
3220
+ const boundsLeft = ancestorRect != null ? ancestorRect.x + ancestorClientLeft * scaleX : 0;
3221
+ const boundsTop = ancestorRect != null ? ancestorRect.y + ancestorClientTop * scaleY : 0;
3222
+ const boundsWidth = ancestorRect != null && ancestorClientWidth > 0 ? ancestorClientWidth : ancestorRect != null ? ancestorRect.width / scaleX : window.innerWidth;
3223
+ const boundsHeight = ancestorRect != null && ancestorClientHeight > 0 ? ancestorClientHeight : ancestorRect != null ? ancestorRect.height / scaleY : window.innerHeight;
3224
+ return {
3225
+ transformedAncestor,
3226
+ ancestorRect,
3227
+ scaleX,
3228
+ scaleY,
3229
+ boundsLeft,
3230
+ boundsTop,
3231
+ boundsWidth,
3232
+ boundsHeight
3233
+ };
3234
+ };
3235
+ const toLocalRect = (rect, placementContext) => {
3236
+ if (placementContext.transformedAncestor == null) {
3237
+ return rect;
3238
+ }
3239
+ return {
3240
+ x: (rect.x - placementContext.boundsLeft) / placementContext.scaleX,
3241
+ y: (rect.y - placementContext.boundsTop) / placementContext.scaleY,
3242
+ width: rect.width / placementContext.scaleX,
3243
+ height: rect.height / placementContext.scaleY
3244
+ };
3245
+ };
3141
3246
  const orientationValues$2 = [
3142
3247
  "top-left",
3143
3248
  "top-right",
@@ -3150,6 +3255,50 @@ const orientationValues$2 = [
3150
3255
  "center-left",
3151
3256
  "center-right"
3152
3257
  ];
3258
+ const getAnchorPosition = (rect, width, height, orient) => {
3259
+ let x = 0;
3260
+ let y = 0;
3261
+ if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
3262
+ x = rect.x;
3263
+ }
3264
+ if (orient === "bottom-left" || orient === "top-left") {
3265
+ x = rect.x + rect.width - width;
3266
+ }
3267
+ if (orient === "bottom-center" || orient === "top-center") {
3268
+ x = rect.x + rect.width / 2 - width / 2;
3269
+ }
3270
+ if (orient === "center-right") {
3271
+ x = rect.x + rect.width;
3272
+ }
3273
+ if (orient === "center-left") {
3274
+ x = rect.x - width;
3275
+ }
3276
+ if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
3277
+ y = rect.y + rect.height;
3278
+ }
3279
+ if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
3280
+ y = rect.y - height;
3281
+ }
3282
+ if (orient === "center-left" || orient === "center-right") {
3283
+ y = rect.y + rect.height / 2 - height / 2;
3284
+ }
3285
+ return { x, y };
3286
+ };
3287
+ const clampPosition = ({
3288
+ x,
3289
+ y,
3290
+ boundsWidth,
3291
+ boundsHeight,
3292
+ insetX,
3293
+ insetY,
3294
+ modalWidth,
3295
+ modalHeight
3296
+ }) => {
3297
+ return {
3298
+ x: Math.max(insetX, Math.min(x, boundsWidth - modalWidth - insetX)),
3299
+ y: Math.max(insetY, Math.min(y, boundsHeight - modalHeight - insetY))
3300
+ };
3301
+ };
3153
3302
  const bodyEl$1 = document.body;
3154
3303
  const disableOverscroll = () => {
3155
3304
  bodyEl$1.__pop_counter__ = (bodyEl$1.__pop_counter__ ?? 0) + 1;
@@ -3165,7 +3314,7 @@ const enableOverscroll = () => {
3165
3314
  document.documentElement.style.removeProperty("overscroll-behavior");
3166
3315
  }
3167
3316
  };
3168
- const templateHTML$U = '<style>:host{display:contents;position:relative}dialog{position:fixed;left:0;top:0;margin:0;outline:0;padding:0;border:none;box-sizing:border-box;max-width:unset;max-height:unset;z-index:1;background:0 0;overflow:visible}dialog:not([open]){display:none}dialog::backdrop{background-color:transparent}#content{position:relative;z-index:1}#target-open{display:flex;flex-direction:column;position:absolute;left:0;top:0}#focus{display:none;position:absolute;width:0;height:0}</style><slot id="target" name="target" aria-haspopup="dialog" aria-expanded="false"></slot><div id="focus"></div><dialog id="dialog"><div id="target-open"><slot name="target-open"></slot></div><div id="content"><slot name="content"></slot></div></dialog>';
3317
+ const templateHTML$U = '<style>:host{display:contents;position:relative}dialog{position:fixed;left:0;top:0;transform:translate(var(--sinch-pop-offset-x),var(--sinch-pop-offset-y));margin:0;outline:0;padding:0;border:none;box-sizing:border-box;max-width:unset;max-height:unset;z-index:1;background:0 0;overflow:visible}dialog:not([open]){display:none}dialog::backdrop{background-color:transparent}#content{position:relative;z-index:1}#target-open{display:flex;flex-direction:column;position:absolute;left:0;top:0}#focus{display:none;position:absolute;width:0;height:0}</style><slot id="target" name="target" aria-haspopup="dialog" aria-expanded="false"></slot><div id="focus"></div><dialog id="dialog"><div id="target-open"><slot name="target-open"></slot></div><div id="content"><slot name="content"></slot></div></dialog>';
3169
3318
  const template$U = document.createElement("template");
3170
3319
  template$U.innerHTML = templateHTML$U;
3171
3320
  class Pop extends NectaryElement {
@@ -3173,6 +3322,7 @@ class Pop extends NectaryElement {
3173
3322
  #$focus;
3174
3323
  #$dialog;
3175
3324
  #resizeThrottle;
3325
+ #scrollPositionThrottle;
3176
3326
  #resizeObserver;
3177
3327
  #$targetSlot;
3178
3328
  #$targetOpenSlot;
@@ -3182,10 +3332,15 @@ class Pop extends NectaryElement {
3182
3332
  #controller;
3183
3333
  #keydownContext;
3184
3334
  #visibilityContext;
3185
- #targetStyleValue = null;
3186
3335
  #modalWidth = 0;
3187
3336
  #modalHeight = 0;
3188
3337
  #scrollableParents = [];
3338
+ #openSession = {
3339
+ effectiveAllowScroll: false,
3340
+ modalSemantics: false,
3341
+ targetStyleValue: null,
3342
+ transformedAncestor: null
3343
+ };
3189
3344
  constructor() {
3190
3345
  super();
3191
3346
  const shadowRoot = this.attachShadow();
@@ -3198,6 +3353,9 @@ class Pop extends NectaryElement {
3198
3353
  this.#$contentSlot = shadowRoot.querySelector('slot[name="content"]');
3199
3354
  this.#$targetOpenWrapper = shadowRoot.querySelector("#target-open");
3200
3355
  this.#resizeThrottle = throttleAnimationFrame(this.#updateOrientation);
3356
+ this.#scrollPositionThrottle = throttleAnimationFrame(() => {
3357
+ this.#updatePosition(false);
3358
+ });
3201
3359
  this.#resizeObserver = new ResizeObserver(() => {
3202
3360
  this.#resizeThrottle.fn();
3203
3361
  });
@@ -3227,6 +3385,7 @@ class Pop extends NectaryElement {
3227
3385
  this.#controller.abort();
3228
3386
  this.#controller = null;
3229
3387
  this.#resizeThrottle.cancel();
3388
+ this.#scrollPositionThrottle.cancel();
3230
3389
  this.#resizeObserver.disconnect();
3231
3390
  this.#onCollapse();
3232
3391
  }
@@ -3322,10 +3481,68 @@ class Pop extends NectaryElement {
3322
3481
  }
3323
3482
  return item;
3324
3483
  }
3484
+ #prepareTransferredTarget() {
3485
+ const targetEl = this.#getFirstTargetElement(this.#$targetSlot);
3486
+ const targetElComputedStyle = getComputedStyle(targetEl);
3487
+ const marginLeft = parseInt(targetElComputedStyle.marginLeft, 10);
3488
+ const marginRight = parseInt(targetElComputedStyle.marginRight, 10);
3489
+ const marginTop = parseInt(targetElComputedStyle.marginTop, 10);
3490
+ const marginBottom = parseInt(targetElComputedStyle.marginBottom, 10);
3491
+ const targetRect = this.#getTargetRect();
3492
+ this.#$targetWrapper.style.setProperty("display", "block");
3493
+ this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
3494
+ this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
3495
+ this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
3496
+ this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
3497
+ this.#openSession.targetStyleValue = targetEl.getAttribute("style");
3498
+ targetEl.style.setProperty("margin", "0");
3499
+ targetEl.style.setProperty("position", "static");
3500
+ if (targetElComputedStyle.transform !== "none") {
3501
+ const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
3502
+ targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
3503
+ }
3504
+ getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
3505
+ }
3506
+ #restoreTransferredTarget() {
3507
+ const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
3508
+ const { targetStyleValue } = this.#openSession;
3509
+ if (targetStyleValue === null) {
3510
+ targetEl.style.removeProperty("margin");
3511
+ targetEl.style.removeProperty("position");
3512
+ targetEl.style.removeProperty("transform");
3513
+ } else {
3514
+ targetEl.setAttribute("style", targetStyleValue);
3515
+ }
3516
+ getFirstSlotElement(this.#$targetOpenSlot)?.setAttribute("slot", "target");
3517
+ this.#$targetWrapper.style.removeProperty("display");
3518
+ this.#$targetWrapper.style.removeProperty("width");
3519
+ this.#$targetWrapper.style.removeProperty("height");
3520
+ }
3521
+ #subscribeScrollTracking() {
3522
+ this.#scrollableParents = getScrollableParents(this.#getFirstTargetElement(this.#$targetSlot));
3523
+ this.#scrollableParents.forEach((el) => {
3524
+ el.addEventListener("scroll", this.#onScrollableParentScroll, { passive: true, capture: true });
3525
+ });
3526
+ }
3527
+ #unsubscribeScrollTracking() {
3528
+ this.#scrollableParents.forEach((el) => {
3529
+ el.removeEventListener("scroll", this.#onScrollableParentScroll, { capture: true });
3530
+ });
3531
+ }
3325
3532
  #onExpand() {
3326
3533
  if (!this.isDomConnected || this.#$dialog.open) {
3327
3534
  return;
3328
3535
  }
3536
+ const transformedAncestor = getTransformedAncestor(this);
3537
+ const effectiveAllowScroll = this.allowScroll || transformedAncestor != null;
3538
+ const shouldUseModal = this.modal && transformedAncestor == null;
3539
+ const openAsModal = shouldUseModal || !effectiveAllowScroll;
3540
+ this.#openSession = {
3541
+ effectiveAllowScroll,
3542
+ modalSemantics: shouldUseModal,
3543
+ targetStyleValue: null,
3544
+ transformedAncestor
3545
+ };
3329
3546
  this.#$targetSlot.addEventListener("blur", this.#stopEventPropagation, true);
3330
3547
  this.#$focus.setAttribute("tabindex", "-1");
3331
3548
  this.#$focus.style.display = "block";
@@ -3335,7 +3552,7 @@ class Pop extends NectaryElement {
3335
3552
  this.#$targetSlot.removeEventListener("blur", this.#stopEventPropagation, true);
3336
3553
  this.#$focus.removeAttribute("tabindex");
3337
3554
  this.#$focus.removeAttribute("style");
3338
- if (this.modal || !this.allowScroll) {
3555
+ if (openAsModal) {
3339
3556
  this.#$dialog.showModal();
3340
3557
  } else {
3341
3558
  this.#$dialog.show();
@@ -3343,32 +3560,13 @@ class Pop extends NectaryElement {
3343
3560
  this.#$targetWrapper.setAttribute("aria-expanded", "true");
3344
3561
  this.#updateOrientation();
3345
3562
  this.#resizeObserver.observe(this.#$dialog);
3346
- if (this.modal) {
3563
+ if (shouldUseModal) {
3347
3564
  getFirstFocusableElement(this.#$contentSlot)?.focus();
3348
3565
  } else {
3349
- if (!this.allowScroll) {
3350
- const $targetEl = this.#getFirstTargetElement(this.#$targetSlot);
3351
- const targetElComputedStyle = getComputedStyle($targetEl);
3352
- const marginLeft = parseInt(targetElComputedStyle.marginLeft);
3353
- const marginRight = parseInt(targetElComputedStyle.marginRight);
3354
- const marginTop = parseInt(targetElComputedStyle.marginTop);
3355
- const marginBottom = parseInt(targetElComputedStyle.marginBottom);
3356
- const targetRect = this.#getTargetRect();
3357
- this.#$targetWrapper.style.setProperty("display", "block");
3358
- this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
3359
- this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
3360
- this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
3361
- this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
3362
- this.#targetStyleValue = $targetEl.getAttribute("style");
3363
- $targetEl.style.setProperty("margin", "0");
3364
- $targetEl.style.setProperty("position", "static");
3365
- if (targetElComputedStyle.transform !== "none") {
3366
- const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
3367
- $targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
3368
- }
3369
- getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
3566
+ if (!effectiveAllowScroll) {
3567
+ this.#prepareTransferredTarget();
3370
3568
  }
3371
- const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3569
+ const activeSlot = effectiveAllowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3372
3570
  activeSlot.addEventListener("keydown", this.#onTargetKeydown);
3373
3571
  if (this.#targetActiveElement !== null) {
3374
3572
  activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
@@ -3385,13 +3583,10 @@ class Pop extends NectaryElement {
3385
3583
  }
3386
3584
  }
3387
3585
  }
3388
- if (!this.allowScroll) {
3586
+ if (!effectiveAllowScroll) {
3389
3587
  disableOverscroll();
3390
3588
  } else {
3391
- this.#scrollableParents = getScrollableParents(this.#getFirstTargetElement(this.#$targetSlot));
3392
- this.#scrollableParents.forEach((el) => {
3393
- el.addEventListener("scroll", () => this.#updatePosition(false), { passive: true, capture: true });
3394
- });
3589
+ this.#subscribeScrollTracking();
3395
3590
  }
3396
3591
  window.addEventListener("resize", this.#onResize);
3397
3592
  requestAnimationFrame(() => {
@@ -3406,9 +3601,11 @@ class Pop extends NectaryElement {
3406
3601
  if (!this.#$dialog.open) {
3407
3602
  return;
3408
3603
  }
3604
+ const openSession = this.#openSession;
3605
+ const effectiveAllowScroll = openSession.effectiveAllowScroll;
3409
3606
  this.#resizeObserver.disconnect();
3410
- const isNonModal = !this.modal;
3411
- const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3607
+ const isNonModal = !openSession.modalSemantics;
3608
+ const activeSlot = effectiveAllowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3412
3609
  this.#dispatchContentVisibility(false);
3413
3610
  activeSlot.removeEventListener("keydown", this.#onTargetKeydown);
3414
3611
  if (isNonModal) {
@@ -3419,19 +3616,8 @@ class Pop extends NectaryElement {
3419
3616
  if (isNonModal) {
3420
3617
  activeSlot.removeEventListener("blur", this.#captureActiveElement, true);
3421
3618
  }
3422
- if (isNonModal && !this.allowScroll) {
3423
- const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
3424
- targetEl.style.removeProperty("margin");
3425
- targetEl.style.removeProperty("position");
3426
- targetEl.style.removeProperty("transform");
3427
- if (this.#targetStyleValue !== null) {
3428
- targetEl.setAttribute("style", this.#targetStyleValue);
3429
- this.#targetStyleValue = null;
3430
- }
3431
- getFirstSlotElement(this.#$targetOpenSlot)?.setAttribute("slot", "target");
3432
- this.#$targetWrapper.style.removeProperty("display");
3433
- this.#$targetWrapper.style.removeProperty("width");
3434
- this.#$targetWrapper.style.removeProperty("height");
3619
+ if (isNonModal && !effectiveAllowScroll) {
3620
+ this.#restoreTransferredTarget();
3435
3621
  }
3436
3622
  if (this.#targetActiveElement !== null) {
3437
3623
  if (!isElementFocused(this.#targetActiveElement)) {
@@ -3451,57 +3637,69 @@ class Pop extends NectaryElement {
3451
3637
  this.#targetActiveElement = null;
3452
3638
  }
3453
3639
  }
3454
- if (!this.allowScroll) {
3640
+ if (!effectiveAllowScroll) {
3455
3641
  enableOverscroll();
3456
3642
  } else {
3457
- this.#scrollableParents.forEach((el) => {
3458
- el.removeEventListener("scroll", () => this.#updatePosition(false), { capture: true });
3459
- });
3643
+ this.#unsubscribeScrollTracking();
3460
3644
  }
3461
3645
  this.#resizeThrottle.cancel();
3646
+ this.#scrollPositionThrottle.cancel();
3462
3647
  window.removeEventListener("resize", this.#onResize);
3463
3648
  this.#scrollableParents = [];
3464
3649
  this.#$contentSlot.removeEventListener("slotchange", this.#onContentSlotChange);
3650
+ this.#openSession = {
3651
+ effectiveAllowScroll: false,
3652
+ modalSemantics: false,
3653
+ targetStyleValue: null,
3654
+ transformedAncestor: null
3655
+ };
3465
3656
  }
3466
3657
  #onResize = () => {
3467
3658
  this.#resizeThrottle.fn();
3468
3659
  };
3660
+ #onScrollableParentScroll = () => {
3661
+ this.#scrollPositionThrottle.fn();
3662
+ };
3469
3663
  #updatePosition = (updateWidth) => {
3470
- const targetRect = this.modal || this.allowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
3664
+ const placementContext = getPlacementContext(this, this.#openSession.transformedAncestor);
3665
+ const { scaleX, scaleY, boundsWidth, boundsHeight } = placementContext;
3666
+ const { modalSemantics, effectiveAllowScroll } = this.#openSession;
3667
+ const shouldClamp = !effectiveAllowScroll;
3668
+ const targetRectViewport = modalSemantics || effectiveAllowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
3471
3669
  const orient = this.orientation;
3472
- const modalWidth = this.#modalWidth;
3473
- const modalHeight = this.#modalHeight;
3474
3670
  const inset = this.inset;
3475
- let xPos = 0;
3476
- let yPos = 0;
3477
- if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
3478
- xPos = targetRect.x;
3479
- }
3480
- if (orient === "bottom-left" || orient === "top-left") {
3481
- xPos = targetRect.x + targetRect.width - modalWidth;
3482
- }
3483
- if (orient === "bottom-center" || orient === "top-center") {
3484
- xPos = targetRect.x + targetRect.width / 2 - modalWidth / 2;
3485
- }
3486
- if (orient === "center-right") {
3487
- xPos = targetRect.x + targetRect.width;
3488
- }
3489
- if (orient === "center-left") {
3490
- xPos = targetRect.x - modalWidth;
3491
- }
3492
- if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
3493
- yPos = targetRect.y + targetRect.height;
3494
- }
3495
- if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
3496
- yPos = targetRect.y - modalHeight;
3497
- }
3498
- if (orient === "center-left" || orient === "center-right") {
3499
- yPos = targetRect.y + targetRect.height / 2 - modalHeight / 2;
3500
- }
3501
- const clampedXPos = Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset));
3502
- const clampedYPos = Math.max(inset, Math.min(yPos, window.innerHeight - modalHeight - inset));
3503
- if (this.hideOutsideViewport && this.#isPopPointInViewport(xPos, yPos)) {
3504
- this.#$dialog.style.setProperty("visibility", "hidden");
3671
+ const insetX = inset / scaleX;
3672
+ const insetY = inset / scaleY;
3673
+ const targetRect = toLocalRect(targetRectViewport, placementContext);
3674
+ const liveRect = this.#$dialog.open ? this.#$dialog.getBoundingClientRect() : null;
3675
+ const isStretch = orient === "top-stretch" || orient === "bottom-stretch";
3676
+ const modalWidthViewport = isStretch ? this.#modalWidth : liveRect?.width ?? this.#modalWidth;
3677
+ const modalHeightViewport = liveRect?.height ?? this.#modalHeight;
3678
+ const modalWidth = modalWidthViewport / scaleX;
3679
+ const modalHeight = modalHeightViewport / scaleY;
3680
+ const localPos = getAnchorPosition(targetRect, modalWidth, modalHeight, orient);
3681
+ const xPos = localPos.x;
3682
+ const yPos = localPos.y;
3683
+ const localViewportInfos = { boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight };
3684
+ const clampedPosition = shouldClamp ? clampPosition({ x: xPos, y: yPos, ...localViewportInfos }) : { x: xPos, y: yPos };
3685
+ const clampedXPos = clampedPosition.x;
3686
+ const clampedYPos = clampedPosition.y;
3687
+ if (this.hideOutsideViewport) {
3688
+ const viewportPosition = getAnchorPosition(targetRectViewport, modalWidthViewport, modalHeightViewport, orient);
3689
+ const visibilityViewportInfos = {
3690
+ boundsWidth: window.innerWidth,
3691
+ boundsHeight: window.innerHeight,
3692
+ insetX: inset,
3693
+ insetY: inset,
3694
+ modalWidth: modalWidthViewport,
3695
+ modalHeight: modalHeightViewport
3696
+ };
3697
+ const isOutOfViewport = this.#isOutsideViewport(viewportPosition.x, viewportPosition.y, visibilityViewportInfos);
3698
+ if (isOutOfViewport) {
3699
+ this.#$dialog.style.setProperty("visibility", "hidden");
3700
+ } else {
3701
+ this.#$dialog.style.removeProperty("visibility");
3702
+ }
3505
3703
  } else {
3506
3704
  this.#$dialog.style.removeProperty("visibility");
3507
3705
  }
@@ -3510,7 +3708,7 @@ class Pop extends NectaryElement {
3510
3708
  if (updateWidth === true) {
3511
3709
  this.#$dialog.style.setProperty("width", `${modalWidth}px`);
3512
3710
  }
3513
- if (!this.modal && !this.allowScroll) {
3711
+ if (!modalSemantics && !effectiveAllowScroll) {
3514
3712
  const targetLeftPos = targetRect.x - clampedXPos;
3515
3713
  const targetTopPos = targetRect.y - clampedYPos;
3516
3714
  this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
@@ -3586,13 +3784,10 @@ class Pop extends NectaryElement {
3586
3784
  this.#updateOrientation();
3587
3785
  }
3588
3786
  };
3589
- #isPopPointInViewport(x, y) {
3590
- const inset = this.inset;
3591
- const modalWidth = this.#modalWidth;
3592
- const modalHeight = this.#modalHeight;
3593
- const clampedX = Math.max(inset, Math.min(x, window.innerWidth - modalWidth - inset));
3594
- const clampedY = Math.max(inset, Math.min(y, window.innerHeight - modalHeight - inset));
3595
- return Math.abs(clampedX - x) > 2 || Math.abs(clampedY - y) > 2;
3787
+ #isOutsideViewport(x, y, viewportInfos) {
3788
+ const { boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight } = viewportInfos;
3789
+ const clampedPosition = clampPosition({ x, y, boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight });
3790
+ return Math.abs(clampedPosition.x - x) > 2 || Math.abs(clampedPosition.y - y) > 2;
3596
3791
  }
3597
3792
  }
3598
3793
  defineCustomElement("sinch-pop", Pop);
@@ -3756,6 +3951,10 @@ const SHOW_DELAY_SLOW = 1e3;
3756
3951
  const SHOW_DELAY_FAST = 250;
3757
3952
  const HIDE_DELAY = 0;
3758
3953
  const ANIMATION_DURATION = 100;
3954
+ const OVERLAP_TOLERANCE = 1;
3955
+ const MAX_ZERO_DIMENSION_PLACEMENT_RETRIES = 8;
3956
+ const MIN_FIRST_REVEAL_STABILITY_FRAMES = 3;
3957
+ const MAX_FIRST_REVEAL_STABILITY_FRAMES = 6;
3759
3958
  const template$T = document.createElement("template");
3760
3959
  template$T.innerHTML = templateHTML$T;
3761
3960
  class Tooltip extends NectaryElement {
@@ -3765,11 +3964,16 @@ class Tooltip extends NectaryElement {
3765
3964
  #$contentWrapper;
3766
3965
  #$tip;
3767
3966
  #$target;
3768
- #controller = null;
3967
+ #resizeObserver = null;
3769
3968
  #tooltipState;
3770
3969
  #animation = null;
3771
3970
  #shouldReduceMotion = false;
3772
3971
  #isSubscribed = false;
3972
+ #controller;
3973
+ #placementScheduled = false;
3974
+ #zeroDimensionPlacementRetries = 0;
3975
+ #revealRequestId = 0;
3976
+ #hasCompletedFirstReveal = false;
3773
3977
  constructor() {
3774
3978
  super();
3775
3979
  const shadowRoot = this.attachShadow();
@@ -3781,6 +3985,7 @@ class Tooltip extends NectaryElement {
3781
3985
  this.#$tip = shadowRoot.querySelector("#tip");
3782
3986
  this.#$target = shadowRoot.querySelector("#target");
3783
3987
  this.#shouldReduceMotion = shouldReduceMotion();
3988
+ this.#controller = null;
3784
3989
  this.#tooltipState = new TooltipState({
3785
3990
  showDelay: SHOW_DELAY_SLOW,
3786
3991
  hideDelay: this.#shouldReduceMotion ? HIDE_DELAY + ANIMATION_DURATION : HIDE_DELAY,
@@ -3801,6 +4006,10 @@ class Tooltip extends NectaryElement {
3801
4006
  this.#$pop.addEventListener("-close", this.#onPopClose, options);
3802
4007
  this.addEventListener("-show", this.#onShowReactHandler, options);
3803
4008
  this.addEventListener("-hide", this.#onHideReactHandler, options);
4009
+ this.#resizeObserver = new ResizeObserver(() => {
4010
+ this.#schedulePlacement();
4011
+ });
4012
+ this.#resizeObserver.observe(this.#$content);
3804
4013
  updateAttribute(this.#$pop, "orientation", getPopOrientation$1(this.orientation));
3805
4014
  updateBooleanAttribute(this.#$pop, "hide-outside-viewport", !this.showOutsideViewport);
3806
4015
  this.#updateText();
@@ -3810,6 +4019,8 @@ class Tooltip extends NectaryElement {
3810
4019
  this.#tooltipState.destroy();
3811
4020
  this.#controller.abort();
3812
4021
  this.#controller = null;
4022
+ this.#resizeObserver?.disconnect();
4023
+ this.#resizeObserver = null;
3813
4024
  }
3814
4025
  static get observedAttributes() {
3815
4026
  return [
@@ -3941,9 +4152,13 @@ class Tooltip extends NectaryElement {
3941
4152
  };
3942
4153
  // SHOW_DELAY ended, tooltip can be shown with animation
3943
4154
  #onStateShowEnd = () => {
4155
+ const revealRequestId = ++this.#revealRequestId;
3944
4156
  this.#dispatchShowEvent();
3945
4157
  updateBooleanAttribute(this.#$pop, "open", true);
3946
- requestAnimationFrame(this.#updateTipOrientation);
4158
+ this.#schedulePlacement();
4159
+ this.#scheduleReveal(revealRequestId);
4160
+ };
4161
+ #playShowAnimation() {
3947
4162
  if (this.#animation !== null) {
3948
4163
  this.#animation.updatePlaybackRate(1);
3949
4164
  this.#animation.play();
@@ -3956,20 +4171,64 @@ class Tooltip extends NectaryElement {
3956
4171
  fill: "forwards"
3957
4172
  });
3958
4173
  }
3959
- };
4174
+ }
4175
+ #isRectStable(previousRect, nextRect) {
4176
+ 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;
4177
+ }
4178
+ #scheduleReveal(revealRequestId) {
4179
+ const reveal = () => {
4180
+ if (!this.isDomConnected || !this.#isOpen() || this.#revealRequestId !== revealRequestId) {
4181
+ return;
4182
+ }
4183
+ this.#playShowAnimation();
4184
+ this.#hasCompletedFirstReveal = true;
4185
+ };
4186
+ if (this.#hasCompletedFirstReveal) {
4187
+ reveal();
4188
+ return;
4189
+ }
4190
+ let previousRect = null;
4191
+ let observedFrames = 0;
4192
+ let remainingFrames = MAX_FIRST_REVEAL_STABILITY_FRAMES;
4193
+ const waitForStableRect = () => {
4194
+ if (!this.isDomConnected || !this.#isOpen() || this.#revealRequestId !== revealRequestId) {
4195
+ return;
4196
+ }
4197
+ const nextRect = this.#$pop.popoverRect;
4198
+ if (observedFrames >= MIN_FIRST_REVEAL_STABILITY_FRAMES && previousRect !== null && this.#isRectStable(previousRect, nextRect)) {
4199
+ reveal();
4200
+ return;
4201
+ }
4202
+ if (remainingFrames === 0) {
4203
+ reveal();
4204
+ return;
4205
+ }
4206
+ previousRect = nextRect;
4207
+ observedFrames += 1;
4208
+ remainingFrames -= 1;
4209
+ requestAnimationFrame(waitForStableRect);
4210
+ };
4211
+ requestAnimationFrame(waitForStableRect);
4212
+ }
3960
4213
  // HIDE_DELAY ended, begin tooltip hide animation
3961
4214
  #onStateHideStart = () => {
4215
+ this.#revealRequestId += 1;
4216
+ if (this.#animation === null) {
4217
+ return;
4218
+ }
3962
4219
  this.#animation.updatePlaybackRate(-1);
3963
4220
  this.#animation.play();
3964
4221
  };
3965
4222
  // Hide animation ended, tooltip can be hidden
3966
4223
  #onStateHideEnd = () => {
3967
4224
  if (this.#isOpen()) {
3968
- this.#animation.finish();
4225
+ this.#animation?.finish();
3969
4226
  this.#dispatchHideEvent();
3970
4227
  updateBooleanAttribute(this.#$pop, "open", false);
3971
4228
  }
3972
4229
  this.#resetTipOrientation();
4230
+ this.#resetContentOffset();
4231
+ this.#zeroDimensionPlacementRetries = 0;
3973
4232
  this.#unsubscribeMouseLeaveEvents();
3974
4233
  this.#unsubscribeScroll();
3975
4234
  };
@@ -3977,26 +4236,125 @@ class Tooltip extends NectaryElement {
3977
4236
  this.#$tip.style.top = "";
3978
4237
  this.#$tip.style.left = "";
3979
4238
  }
3980
- #updateTipOrientation = () => {
4239
+ #resetContentOffset() {
4240
+ this.#$pop.style.removeProperty("--sinch-pop-offset-x");
4241
+ this.#$pop.style.removeProperty("--sinch-pop-offset-y");
4242
+ }
4243
+ #schedulePlacement(resetZeroDimensionRetries = true) {
4244
+ if (!this.#isOpen() || this.#placementScheduled) {
4245
+ return;
4246
+ }
4247
+ if (resetZeroDimensionRetries) {
4248
+ this.#zeroDimensionPlacementRetries = 0;
4249
+ }
4250
+ this.#placementScheduled = true;
4251
+ requestAnimationFrame(() => {
4252
+ this.#placementScheduled = false;
4253
+ if (!this.isDomConnected || !this.#isOpen()) {
4254
+ return;
4255
+ }
4256
+ this.#updatePlacement();
4257
+ });
4258
+ }
4259
+ #applyContentOffset(offsetX, offsetY) {
4260
+ if (offsetX === 0 && offsetY === 0) {
4261
+ this.#$pop.style.removeProperty("--sinch-pop-offset-x");
4262
+ this.#$pop.style.removeProperty("--sinch-pop-offset-y");
4263
+ return;
4264
+ }
4265
+ this.#$pop.style.setProperty("--sinch-pop-offset-x", `${offsetX}px`);
4266
+ this.#$pop.style.setProperty("--sinch-pop-offset-y", `${offsetY}px`);
4267
+ }
4268
+ #updatePlacement = () => {
4269
+ if (!this.isDomConnected || !this.#isOpen()) {
4270
+ return;
4271
+ }
4272
+ const popRect = this.#$pop.popoverRect;
4273
+ if (popRect.width === 0 || popRect.height === 0) {
4274
+ if (this.#zeroDimensionPlacementRetries >= MAX_ZERO_DIMENSION_PLACEMENT_RETRIES) {
4275
+ return;
4276
+ }
4277
+ this.#zeroDimensionPlacementRetries += 1;
4278
+ this.#schedulePlacement(false);
4279
+ return;
4280
+ }
4281
+ this.#zeroDimensionPlacementRetries = 0;
4282
+ const placementContext = getPlacementContext(this.#$pop);
4283
+ this.#resetContentOffset();
4284
+ this.#updateTipOrientation(placementContext);
4285
+ const didOffset = this.#resolveOverlap(placementContext);
4286
+ if (didOffset) {
4287
+ requestAnimationFrame(() => {
4288
+ if (!this.isDomConnected || !this.#isOpen()) {
4289
+ return;
4290
+ }
4291
+ this.#updateTipOrientation(placementContext);
4292
+ });
4293
+ }
4294
+ };
4295
+ #resolveOverlap(placementContext) {
4296
+ const orientation = this.orientation;
4297
+ const targetRect = toLocalRect(this.#$pop.footprintRect, placementContext);
4298
+ const contentRect = toLocalRect(this.#$content.getBoundingClientRect(), placementContext);
4299
+ const tipRect = toLocalRect(this.#$tip.getBoundingClientRect(), placementContext);
4300
+ const targetBottom = targetRect.y + targetRect.height;
4301
+ const targetRight = targetRect.x + targetRect.width;
4302
+ const bottomEdge = Math.max(contentRect.y + contentRect.height, tipRect.y + tipRect.height);
4303
+ const topEdge = Math.min(contentRect.y, tipRect.y);
4304
+ const rightEdge = Math.max(contentRect.x + contentRect.width, tipRect.x + tipRect.width);
4305
+ const leftEdge = Math.min(contentRect.x, tipRect.x);
4306
+ let offsetX = 0;
4307
+ let offsetY = 0;
4308
+ if (orientation.startsWith("top")) {
4309
+ if (bottomEdge > targetRect.y + OVERLAP_TOLERANCE) {
4310
+ offsetY = targetRect.y - bottomEdge;
4311
+ }
4312
+ } else if (orientation.startsWith("bottom")) {
4313
+ if (topEdge < targetBottom - OVERLAP_TOLERANCE) {
4314
+ offsetY = targetBottom - topEdge;
4315
+ }
4316
+ } else if (orientation === "left") {
4317
+ if (rightEdge > targetRect.x + OVERLAP_TOLERANCE) {
4318
+ offsetX = targetRect.x - rightEdge;
4319
+ }
4320
+ } else if (orientation === "right") {
4321
+ if (leftEdge < targetRight - OVERLAP_TOLERANCE) {
4322
+ offsetX = targetRight - leftEdge;
4323
+ }
4324
+ }
4325
+ this.#applyContentOffset(offsetX, offsetY);
4326
+ return offsetX !== 0 || offsetY !== 0;
4327
+ }
4328
+ #updateTipOrientation = (placementContext) => {
3981
4329
  const orient = this.orientation;
3982
4330
  if (!("footprintRect" in this.#$pop)) {
3983
- requestAnimationFrame(this.#updateTipOrientation);
4331
+ requestAnimationFrame(() => {
4332
+ if (!this.isDomConnected || !this.#isOpen()) {
4333
+ return;
4334
+ }
4335
+ this.#updateTipOrientation();
4336
+ });
3984
4337
  return;
3985
4338
  }
3986
- const targetRect = this.#$pop.footprintRect;
3987
- const contentRect = this.#$content.getBoundingClientRect();
4339
+ const ctx = placementContext ?? getPlacementContext(this.#$pop);
4340
+ const targetRect = toLocalRect(this.#$pop.footprintRect, ctx);
4341
+ const contentRect = toLocalRect(this.#$content.getBoundingClientRect(), ctx);
3988
4342
  const diffX = targetRect.x - contentRect.x;
3989
4343
  const diffY = targetRect.y - contentRect.y;
4344
+ const targetWidth = targetRect.width;
4345
+ const targetHeight = targetRect.height;
4346
+ const contentWidth = contentRect.width;
4347
+ const contentHeight = contentRect.height;
3990
4348
  if (orient === "left" || orient === "right") {
3991
- const yPos = Math.max(TIP_SIZE$1, Math.min(diffY + targetRect.height / 2, contentRect.height - TIP_SIZE$1));
4349
+ const yPos = Math.max(TIP_SIZE$1, Math.min(diffY + targetHeight / 2, contentHeight - TIP_SIZE$1));
3992
4350
  this.#$tip.style.top = `${yPos}px`;
3993
4351
  } else {
3994
- let xPos = Math.max(TIP_SIZE$1, Math.min(diffX + targetRect.width / 2, contentRect.width - TIP_SIZE$1));
4352
+ let xPos = Math.max(TIP_SIZE$1, Math.min(diffX + targetWidth / 2, contentWidth - TIP_SIZE$1));
3995
4353
  if (orient === "bottom-left" || orient === "top-left") {
3996
- xPos = Math.max(xPos, contentRect.width * 0.75);
4354
+ xPos = Math.max(xPos, contentWidth * 0.75);
3997
4355
  }
3998
4356
  if (orient === "bottom-right" || orient === "top-right") {
3999
- xPos = Math.min(xPos, contentRect.width * 0.25);
4357
+ xPos = Math.min(xPos, contentWidth * 0.25);
4000
4358
  }
4001
4359
  this.#$tip.style.left = `${xPos}px`;
4002
4360
  }