@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 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() {
@@ -2316,9 +2348,12 @@ class Button extends NectaryElement {
2316
2348
  super.connectedCallback();
2317
2349
  this.#controller = new AbortController();
2318
2350
  const { signal } = this.#controller;
2319
- this.setAttribute("role", "button");
2320
- this.#internals.role = "button";
2321
- this.tabIndex = 0;
2351
+ if (!this.hasAttribute("tabindex")) {
2352
+ this.setAttribute("tabindex", "0");
2353
+ }
2354
+ if (!this.hasAttribute("role")) {
2355
+ this.setAttribute("role", "button");
2356
+ }
2322
2357
  this.addEventListener("click", this.#onButtonClick, { signal });
2323
2358
  this.addEventListener("focus", this.#onButtonFocus, { signal });
2324
2359
  this.addEventListener("blur", this.#onButtonBlur, { signal });
@@ -2342,7 +2377,9 @@ class Button extends NectaryElement {
2342
2377
  "toggled",
2343
2378
  "size",
2344
2379
  "data-size",
2345
- "data-managed-aria-disabled"
2380
+ "data-managed-aria-disabled",
2381
+ "role",
2382
+ "tabindex"
2346
2383
  ];
2347
2384
  }
2348
2385
  attributeChangedCallback(name, oldVal, newVal) {
@@ -2380,6 +2417,33 @@ class Button extends NectaryElement {
2380
2417
  this.#onSizeUpdate();
2381
2418
  break;
2382
2419
  }
2420
+ case "role": {
2421
+ if (isAttrEqual(oldVal, newVal)) {
2422
+ break;
2423
+ }
2424
+ const effectiveRole = newVal !== null && newVal !== "" ? newVal : "button";
2425
+ this.#internals.role = effectiveRole;
2426
+ if (newVal === null || newVal === "") {
2427
+ this.setAttribute("role", "button");
2428
+ }
2429
+ break;
2430
+ }
2431
+ case "tabindex": {
2432
+ if (newVal === null) {
2433
+ if (this.isDomConnected) {
2434
+ this.setAttribute("tabindex", "0");
2435
+ }
2436
+ break;
2437
+ }
2438
+ if (isAttrEqual(oldVal, newVal)) {
2439
+ break;
2440
+ }
2441
+ const parsed = Number.parseInt(newVal, 10);
2442
+ if (Number.isNaN(parsed)) {
2443
+ this.setAttribute("tabindex", "0");
2444
+ }
2445
+ break;
2446
+ }
2383
2447
  }
2384
2448
  }
2385
2449
  set type(value) {
@@ -3138,6 +3202,79 @@ const createThrottle = (delayFn, cancelFn) => (cb) => {
3138
3202
  };
3139
3203
  };
3140
3204
  const throttleAnimationFrame = createThrottle(globalThis.requestAnimationFrame, globalThis.cancelAnimationFrame);
3205
+ const resolveTransformedAncestor = (node, ancestorHint) => {
3206
+ if (node != null && ancestorHint != null && ancestorHint.isConnected && isTransformedElement(ancestorHint) && (ancestorHint === node || ancestorHint.contains(node))) {
3207
+ return ancestorHint;
3208
+ }
3209
+ return getTransformedAncestor(node);
3210
+ };
3211
+ const getTransformedAncestorScale = (ancestor) => {
3212
+ if (ancestor == null) {
3213
+ return { scaleX: 1, scaleY: 1 };
3214
+ }
3215
+ const transform = getComputedStyle(ancestor).transform;
3216
+ let matrixScaleX = null;
3217
+ let matrixScaleY = null;
3218
+ if (transform !== "none") {
3219
+ try {
3220
+ const matrix = new DOMMatrixReadOnly(transform);
3221
+ matrixScaleX = Math.hypot(matrix.a, matrix.b);
3222
+ matrixScaleY = Math.hypot(matrix.c, matrix.d);
3223
+ } catch {
3224
+ matrixScaleX = null;
3225
+ matrixScaleY = null;
3226
+ }
3227
+ }
3228
+ if (matrixScaleX !== null && matrixScaleY !== null) {
3229
+ return {
3230
+ scaleX: Number.isFinite(matrixScaleX) && matrixScaleX > 0 ? matrixScaleX : 1,
3231
+ scaleY: Number.isFinite(matrixScaleY) && matrixScaleY > 0 ? matrixScaleY : 1
3232
+ };
3233
+ }
3234
+ const rect = ancestor.getBoundingClientRect();
3235
+ const baseWidth = ancestor.offsetWidth;
3236
+ const baseHeight = ancestor.offsetHeight;
3237
+ const scaleX = baseWidth > 0 ? rect.width / baseWidth : 1;
3238
+ const scaleY = baseHeight > 0 ? rect.height / baseHeight : 1;
3239
+ return {
3240
+ scaleX: Number.isFinite(scaleX) && scaleX > 0 ? scaleX : 1,
3241
+ scaleY: Number.isFinite(scaleY) && scaleY > 0 ? scaleY : 1
3242
+ };
3243
+ };
3244
+ const getPlacementContext = (node, ancestorHint) => {
3245
+ const transformedAncestor = resolveTransformedAncestor(node, ancestorHint);
3246
+ const ancestorRect = transformedAncestor?.getBoundingClientRect() ?? null;
3247
+ const ancestorClientLeft = transformedAncestor?.clientLeft ?? 0;
3248
+ const ancestorClientTop = transformedAncestor?.clientTop ?? 0;
3249
+ const ancestorClientWidth = transformedAncestor?.clientWidth ?? 0;
3250
+ const ancestorClientHeight = transformedAncestor?.clientHeight ?? 0;
3251
+ const { scaleX, scaleY } = getTransformedAncestorScale(transformedAncestor);
3252
+ const boundsLeft = ancestorRect != null ? ancestorRect.x + ancestorClientLeft * scaleX : 0;
3253
+ const boundsTop = ancestorRect != null ? ancestorRect.y + ancestorClientTop * scaleY : 0;
3254
+ const boundsWidth = ancestorRect != null && ancestorClientWidth > 0 ? ancestorClientWidth : ancestorRect != null ? ancestorRect.width / scaleX : window.innerWidth;
3255
+ const boundsHeight = ancestorRect != null && ancestorClientHeight > 0 ? ancestorClientHeight : ancestorRect != null ? ancestorRect.height / scaleY : window.innerHeight;
3256
+ return {
3257
+ transformedAncestor,
3258
+ ancestorRect,
3259
+ scaleX,
3260
+ scaleY,
3261
+ boundsLeft,
3262
+ boundsTop,
3263
+ boundsWidth,
3264
+ boundsHeight
3265
+ };
3266
+ };
3267
+ const toLocalRect = (rect, placementContext) => {
3268
+ if (placementContext.transformedAncestor == null) {
3269
+ return rect;
3270
+ }
3271
+ return {
3272
+ x: (rect.x - placementContext.boundsLeft) / placementContext.scaleX,
3273
+ y: (rect.y - placementContext.boundsTop) / placementContext.scaleY,
3274
+ width: rect.width / placementContext.scaleX,
3275
+ height: rect.height / placementContext.scaleY
3276
+ };
3277
+ };
3141
3278
  const orientationValues$2 = [
3142
3279
  "top-left",
3143
3280
  "top-right",
@@ -3150,6 +3287,50 @@ const orientationValues$2 = [
3150
3287
  "center-left",
3151
3288
  "center-right"
3152
3289
  ];
3290
+ const getAnchorPosition = (rect, width, height, orient) => {
3291
+ let x = 0;
3292
+ let y = 0;
3293
+ if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
3294
+ x = rect.x;
3295
+ }
3296
+ if (orient === "bottom-left" || orient === "top-left") {
3297
+ x = rect.x + rect.width - width;
3298
+ }
3299
+ if (orient === "bottom-center" || orient === "top-center") {
3300
+ x = rect.x + rect.width / 2 - width / 2;
3301
+ }
3302
+ if (orient === "center-right") {
3303
+ x = rect.x + rect.width;
3304
+ }
3305
+ if (orient === "center-left") {
3306
+ x = rect.x - width;
3307
+ }
3308
+ if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
3309
+ y = rect.y + rect.height;
3310
+ }
3311
+ if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
3312
+ y = rect.y - height;
3313
+ }
3314
+ if (orient === "center-left" || orient === "center-right") {
3315
+ y = rect.y + rect.height / 2 - height / 2;
3316
+ }
3317
+ return { x, y };
3318
+ };
3319
+ const clampPosition = ({
3320
+ x,
3321
+ y,
3322
+ boundsWidth,
3323
+ boundsHeight,
3324
+ insetX,
3325
+ insetY,
3326
+ modalWidth,
3327
+ modalHeight
3328
+ }) => {
3329
+ return {
3330
+ x: Math.max(insetX, Math.min(x, boundsWidth - modalWidth - insetX)),
3331
+ y: Math.max(insetY, Math.min(y, boundsHeight - modalHeight - insetY))
3332
+ };
3333
+ };
3153
3334
  const bodyEl$1 = document.body;
3154
3335
  const disableOverscroll = () => {
3155
3336
  bodyEl$1.__pop_counter__ = (bodyEl$1.__pop_counter__ ?? 0) + 1;
@@ -3165,7 +3346,7 @@ const enableOverscroll = () => {
3165
3346
  document.documentElement.style.removeProperty("overscroll-behavior");
3166
3347
  }
3167
3348
  };
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>';
3349
+ 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
3350
  const template$U = document.createElement("template");
3170
3351
  template$U.innerHTML = templateHTML$U;
3171
3352
  class Pop extends NectaryElement {
@@ -3173,6 +3354,7 @@ class Pop extends NectaryElement {
3173
3354
  #$focus;
3174
3355
  #$dialog;
3175
3356
  #resizeThrottle;
3357
+ #scrollPositionThrottle;
3176
3358
  #resizeObserver;
3177
3359
  #$targetSlot;
3178
3360
  #$targetOpenSlot;
@@ -3182,10 +3364,15 @@ class Pop extends NectaryElement {
3182
3364
  #controller;
3183
3365
  #keydownContext;
3184
3366
  #visibilityContext;
3185
- #targetStyleValue = null;
3186
3367
  #modalWidth = 0;
3187
3368
  #modalHeight = 0;
3188
3369
  #scrollableParents = [];
3370
+ #openSession = {
3371
+ effectiveAllowScroll: false,
3372
+ modalSemantics: false,
3373
+ targetStyleValue: null,
3374
+ transformedAncestor: null
3375
+ };
3189
3376
  constructor() {
3190
3377
  super();
3191
3378
  const shadowRoot = this.attachShadow();
@@ -3198,6 +3385,9 @@ class Pop extends NectaryElement {
3198
3385
  this.#$contentSlot = shadowRoot.querySelector('slot[name="content"]');
3199
3386
  this.#$targetOpenWrapper = shadowRoot.querySelector("#target-open");
3200
3387
  this.#resizeThrottle = throttleAnimationFrame(this.#updateOrientation);
3388
+ this.#scrollPositionThrottle = throttleAnimationFrame(() => {
3389
+ this.#updatePosition(false);
3390
+ });
3201
3391
  this.#resizeObserver = new ResizeObserver(() => {
3202
3392
  this.#resizeThrottle.fn();
3203
3393
  });
@@ -3227,6 +3417,7 @@ class Pop extends NectaryElement {
3227
3417
  this.#controller.abort();
3228
3418
  this.#controller = null;
3229
3419
  this.#resizeThrottle.cancel();
3420
+ this.#scrollPositionThrottle.cancel();
3230
3421
  this.#resizeObserver.disconnect();
3231
3422
  this.#onCollapse();
3232
3423
  }
@@ -3322,10 +3513,68 @@ class Pop extends NectaryElement {
3322
3513
  }
3323
3514
  return item;
3324
3515
  }
3516
+ #prepareTransferredTarget() {
3517
+ const targetEl = this.#getFirstTargetElement(this.#$targetSlot);
3518
+ const targetElComputedStyle = getComputedStyle(targetEl);
3519
+ const marginLeft = parseInt(targetElComputedStyle.marginLeft, 10);
3520
+ const marginRight = parseInt(targetElComputedStyle.marginRight, 10);
3521
+ const marginTop = parseInt(targetElComputedStyle.marginTop, 10);
3522
+ const marginBottom = parseInt(targetElComputedStyle.marginBottom, 10);
3523
+ const targetRect = this.#getTargetRect();
3524
+ this.#$targetWrapper.style.setProperty("display", "block");
3525
+ this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
3526
+ this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
3527
+ this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
3528
+ this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
3529
+ this.#openSession.targetStyleValue = targetEl.getAttribute("style");
3530
+ targetEl.style.setProperty("margin", "0");
3531
+ targetEl.style.setProperty("position", "static");
3532
+ if (targetElComputedStyle.transform !== "none") {
3533
+ const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
3534
+ targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
3535
+ }
3536
+ getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
3537
+ }
3538
+ #restoreTransferredTarget() {
3539
+ const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
3540
+ const { targetStyleValue } = this.#openSession;
3541
+ if (targetStyleValue === null) {
3542
+ targetEl.style.removeProperty("margin");
3543
+ targetEl.style.removeProperty("position");
3544
+ targetEl.style.removeProperty("transform");
3545
+ } else {
3546
+ targetEl.setAttribute("style", targetStyleValue);
3547
+ }
3548
+ getFirstSlotElement(this.#$targetOpenSlot)?.setAttribute("slot", "target");
3549
+ this.#$targetWrapper.style.removeProperty("display");
3550
+ this.#$targetWrapper.style.removeProperty("width");
3551
+ this.#$targetWrapper.style.removeProperty("height");
3552
+ }
3553
+ #subscribeScrollTracking() {
3554
+ this.#scrollableParents = getScrollableParents(this.#getFirstTargetElement(this.#$targetSlot));
3555
+ this.#scrollableParents.forEach((el) => {
3556
+ el.addEventListener("scroll", this.#onScrollableParentScroll, { passive: true, capture: true });
3557
+ });
3558
+ }
3559
+ #unsubscribeScrollTracking() {
3560
+ this.#scrollableParents.forEach((el) => {
3561
+ el.removeEventListener("scroll", this.#onScrollableParentScroll, { capture: true });
3562
+ });
3563
+ }
3325
3564
  #onExpand() {
3326
3565
  if (!this.isDomConnected || this.#$dialog.open) {
3327
3566
  return;
3328
3567
  }
3568
+ const transformedAncestor = getTransformedAncestor(this);
3569
+ const effectiveAllowScroll = this.allowScroll || transformedAncestor != null;
3570
+ const shouldUseModal = this.modal && transformedAncestor == null;
3571
+ const openAsModal = shouldUseModal || !effectiveAllowScroll;
3572
+ this.#openSession = {
3573
+ effectiveAllowScroll,
3574
+ modalSemantics: shouldUseModal,
3575
+ targetStyleValue: null,
3576
+ transformedAncestor
3577
+ };
3329
3578
  this.#$targetSlot.addEventListener("blur", this.#stopEventPropagation, true);
3330
3579
  this.#$focus.setAttribute("tabindex", "-1");
3331
3580
  this.#$focus.style.display = "block";
@@ -3335,7 +3584,7 @@ class Pop extends NectaryElement {
3335
3584
  this.#$targetSlot.removeEventListener("blur", this.#stopEventPropagation, true);
3336
3585
  this.#$focus.removeAttribute("tabindex");
3337
3586
  this.#$focus.removeAttribute("style");
3338
- if (this.modal || !this.allowScroll) {
3587
+ if (openAsModal) {
3339
3588
  this.#$dialog.showModal();
3340
3589
  } else {
3341
3590
  this.#$dialog.show();
@@ -3343,32 +3592,13 @@ class Pop extends NectaryElement {
3343
3592
  this.#$targetWrapper.setAttribute("aria-expanded", "true");
3344
3593
  this.#updateOrientation();
3345
3594
  this.#resizeObserver.observe(this.#$dialog);
3346
- if (this.modal) {
3595
+ if (shouldUseModal) {
3347
3596
  getFirstFocusableElement(this.#$contentSlot)?.focus();
3348
3597
  } 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");
3598
+ if (!effectiveAllowScroll) {
3599
+ this.#prepareTransferredTarget();
3370
3600
  }
3371
- const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3601
+ const activeSlot = effectiveAllowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3372
3602
  activeSlot.addEventListener("keydown", this.#onTargetKeydown);
3373
3603
  if (this.#targetActiveElement !== null) {
3374
3604
  activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
@@ -3385,13 +3615,10 @@ class Pop extends NectaryElement {
3385
3615
  }
3386
3616
  }
3387
3617
  }
3388
- if (!this.allowScroll) {
3618
+ if (!effectiveAllowScroll) {
3389
3619
  disableOverscroll();
3390
3620
  } 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
- });
3621
+ this.#subscribeScrollTracking();
3395
3622
  }
3396
3623
  window.addEventListener("resize", this.#onResize);
3397
3624
  requestAnimationFrame(() => {
@@ -3406,9 +3633,11 @@ class Pop extends NectaryElement {
3406
3633
  if (!this.#$dialog.open) {
3407
3634
  return;
3408
3635
  }
3636
+ const openSession = this.#openSession;
3637
+ const effectiveAllowScroll = openSession.effectiveAllowScroll;
3409
3638
  this.#resizeObserver.disconnect();
3410
- const isNonModal = !this.modal;
3411
- const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3639
+ const isNonModal = !openSession.modalSemantics;
3640
+ const activeSlot = effectiveAllowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3412
3641
  this.#dispatchContentVisibility(false);
3413
3642
  activeSlot.removeEventListener("keydown", this.#onTargetKeydown);
3414
3643
  if (isNonModal) {
@@ -3419,19 +3648,8 @@ class Pop extends NectaryElement {
3419
3648
  if (isNonModal) {
3420
3649
  activeSlot.removeEventListener("blur", this.#captureActiveElement, true);
3421
3650
  }
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");
3651
+ if (isNonModal && !effectiveAllowScroll) {
3652
+ this.#restoreTransferredTarget();
3435
3653
  }
3436
3654
  if (this.#targetActiveElement !== null) {
3437
3655
  if (!isElementFocused(this.#targetActiveElement)) {
@@ -3451,57 +3669,69 @@ class Pop extends NectaryElement {
3451
3669
  this.#targetActiveElement = null;
3452
3670
  }
3453
3671
  }
3454
- if (!this.allowScroll) {
3672
+ if (!effectiveAllowScroll) {
3455
3673
  enableOverscroll();
3456
3674
  } else {
3457
- this.#scrollableParents.forEach((el) => {
3458
- el.removeEventListener("scroll", () => this.#updatePosition(false), { capture: true });
3459
- });
3675
+ this.#unsubscribeScrollTracking();
3460
3676
  }
3461
3677
  this.#resizeThrottle.cancel();
3678
+ this.#scrollPositionThrottle.cancel();
3462
3679
  window.removeEventListener("resize", this.#onResize);
3463
3680
  this.#scrollableParents = [];
3464
3681
  this.#$contentSlot.removeEventListener("slotchange", this.#onContentSlotChange);
3682
+ this.#openSession = {
3683
+ effectiveAllowScroll: false,
3684
+ modalSemantics: false,
3685
+ targetStyleValue: null,
3686
+ transformedAncestor: null
3687
+ };
3465
3688
  }
3466
3689
  #onResize = () => {
3467
3690
  this.#resizeThrottle.fn();
3468
3691
  };
3692
+ #onScrollableParentScroll = () => {
3693
+ this.#scrollPositionThrottle.fn();
3694
+ };
3469
3695
  #updatePosition = (updateWidth) => {
3470
- const targetRect = this.modal || this.allowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
3696
+ const placementContext = getPlacementContext(this, this.#openSession.transformedAncestor);
3697
+ const { scaleX, scaleY, boundsWidth, boundsHeight } = placementContext;
3698
+ const { modalSemantics, effectiveAllowScroll } = this.#openSession;
3699
+ const shouldClamp = !effectiveAllowScroll;
3700
+ const targetRectViewport = modalSemantics || effectiveAllowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
3471
3701
  const orient = this.orientation;
3472
- const modalWidth = this.#modalWidth;
3473
- const modalHeight = this.#modalHeight;
3474
3702
  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");
3703
+ const insetX = inset / scaleX;
3704
+ const insetY = inset / scaleY;
3705
+ const targetRect = toLocalRect(targetRectViewport, placementContext);
3706
+ const liveRect = this.#$dialog.open ? this.#$dialog.getBoundingClientRect() : null;
3707
+ const isStretch = orient === "top-stretch" || orient === "bottom-stretch";
3708
+ const modalWidthViewport = isStretch ? this.#modalWidth : liveRect?.width ?? this.#modalWidth;
3709
+ const modalHeightViewport = liveRect?.height ?? this.#modalHeight;
3710
+ const modalWidth = modalWidthViewport / scaleX;
3711
+ const modalHeight = modalHeightViewport / scaleY;
3712
+ const localPos = getAnchorPosition(targetRect, modalWidth, modalHeight, orient);
3713
+ const xPos = localPos.x;
3714
+ const yPos = localPos.y;
3715
+ const localViewportInfos = { boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight };
3716
+ const clampedPosition = shouldClamp ? clampPosition({ x: xPos, y: yPos, ...localViewportInfos }) : { x: xPos, y: yPos };
3717
+ const clampedXPos = clampedPosition.x;
3718
+ const clampedYPos = clampedPosition.y;
3719
+ if (this.hideOutsideViewport) {
3720
+ const viewportPosition = getAnchorPosition(targetRectViewport, modalWidthViewport, modalHeightViewport, orient);
3721
+ const visibilityViewportInfos = {
3722
+ boundsWidth: window.innerWidth,
3723
+ boundsHeight: window.innerHeight,
3724
+ insetX: inset,
3725
+ insetY: inset,
3726
+ modalWidth: modalWidthViewport,
3727
+ modalHeight: modalHeightViewport
3728
+ };
3729
+ const isOutOfViewport = this.#isOutsideViewport(viewportPosition.x, viewportPosition.y, visibilityViewportInfos);
3730
+ if (isOutOfViewport) {
3731
+ this.#$dialog.style.setProperty("visibility", "hidden");
3732
+ } else {
3733
+ this.#$dialog.style.removeProperty("visibility");
3734
+ }
3505
3735
  } else {
3506
3736
  this.#$dialog.style.removeProperty("visibility");
3507
3737
  }
@@ -3510,7 +3740,7 @@ class Pop extends NectaryElement {
3510
3740
  if (updateWidth === true) {
3511
3741
  this.#$dialog.style.setProperty("width", `${modalWidth}px`);
3512
3742
  }
3513
- if (!this.modal && !this.allowScroll) {
3743
+ if (!modalSemantics && !effectiveAllowScroll) {
3514
3744
  const targetLeftPos = targetRect.x - clampedXPos;
3515
3745
  const targetTopPos = targetRect.y - clampedYPos;
3516
3746
  this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
@@ -3586,13 +3816,10 @@ class Pop extends NectaryElement {
3586
3816
  this.#updateOrientation();
3587
3817
  }
3588
3818
  };
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;
3819
+ #isOutsideViewport(x, y, viewportInfos) {
3820
+ const { boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight } = viewportInfos;
3821
+ const clampedPosition = clampPosition({ x, y, boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight });
3822
+ return Math.abs(clampedPosition.x - x) > 2 || Math.abs(clampedPosition.y - y) > 2;
3596
3823
  }
3597
3824
  }
3598
3825
  defineCustomElement("sinch-pop", Pop);
@@ -3756,6 +3983,10 @@ const SHOW_DELAY_SLOW = 1e3;
3756
3983
  const SHOW_DELAY_FAST = 250;
3757
3984
  const HIDE_DELAY = 0;
3758
3985
  const ANIMATION_DURATION = 100;
3986
+ const OVERLAP_TOLERANCE = 1;
3987
+ const MAX_ZERO_DIMENSION_PLACEMENT_RETRIES = 8;
3988
+ const MIN_FIRST_REVEAL_STABILITY_FRAMES = 3;
3989
+ const MAX_FIRST_REVEAL_STABILITY_FRAMES = 6;
3759
3990
  const template$T = document.createElement("template");
3760
3991
  template$T.innerHTML = templateHTML$T;
3761
3992
  class Tooltip extends NectaryElement {
@@ -3765,11 +3996,16 @@ class Tooltip extends NectaryElement {
3765
3996
  #$contentWrapper;
3766
3997
  #$tip;
3767
3998
  #$target;
3768
- #controller = null;
3999
+ #resizeObserver = null;
3769
4000
  #tooltipState;
3770
4001
  #animation = null;
3771
4002
  #shouldReduceMotion = false;
3772
4003
  #isSubscribed = false;
4004
+ #controller;
4005
+ #placementScheduled = false;
4006
+ #zeroDimensionPlacementRetries = 0;
4007
+ #revealRequestId = 0;
4008
+ #hasCompletedFirstReveal = false;
3773
4009
  constructor() {
3774
4010
  super();
3775
4011
  const shadowRoot = this.attachShadow();
@@ -3781,6 +4017,7 @@ class Tooltip extends NectaryElement {
3781
4017
  this.#$tip = shadowRoot.querySelector("#tip");
3782
4018
  this.#$target = shadowRoot.querySelector("#target");
3783
4019
  this.#shouldReduceMotion = shouldReduceMotion();
4020
+ this.#controller = null;
3784
4021
  this.#tooltipState = new TooltipState({
3785
4022
  showDelay: SHOW_DELAY_SLOW,
3786
4023
  hideDelay: this.#shouldReduceMotion ? HIDE_DELAY + ANIMATION_DURATION : HIDE_DELAY,
@@ -3801,6 +4038,10 @@ class Tooltip extends NectaryElement {
3801
4038
  this.#$pop.addEventListener("-close", this.#onPopClose, options);
3802
4039
  this.addEventListener("-show", this.#onShowReactHandler, options);
3803
4040
  this.addEventListener("-hide", this.#onHideReactHandler, options);
4041
+ this.#resizeObserver = new ResizeObserver(() => {
4042
+ this.#schedulePlacement();
4043
+ });
4044
+ this.#resizeObserver.observe(this.#$content);
3804
4045
  updateAttribute(this.#$pop, "orientation", getPopOrientation$1(this.orientation));
3805
4046
  updateBooleanAttribute(this.#$pop, "hide-outside-viewport", !this.showOutsideViewport);
3806
4047
  this.#updateText();
@@ -3810,6 +4051,8 @@ class Tooltip extends NectaryElement {
3810
4051
  this.#tooltipState.destroy();
3811
4052
  this.#controller.abort();
3812
4053
  this.#controller = null;
4054
+ this.#resizeObserver?.disconnect();
4055
+ this.#resizeObserver = null;
3813
4056
  }
3814
4057
  static get observedAttributes() {
3815
4058
  return [
@@ -3941,9 +4184,13 @@ class Tooltip extends NectaryElement {
3941
4184
  };
3942
4185
  // SHOW_DELAY ended, tooltip can be shown with animation
3943
4186
  #onStateShowEnd = () => {
4187
+ const revealRequestId = ++this.#revealRequestId;
3944
4188
  this.#dispatchShowEvent();
3945
4189
  updateBooleanAttribute(this.#$pop, "open", true);
3946
- requestAnimationFrame(this.#updateTipOrientation);
4190
+ this.#schedulePlacement();
4191
+ this.#scheduleReveal(revealRequestId);
4192
+ };
4193
+ #playShowAnimation() {
3947
4194
  if (this.#animation !== null) {
3948
4195
  this.#animation.updatePlaybackRate(1);
3949
4196
  this.#animation.play();
@@ -3956,20 +4203,64 @@ class Tooltip extends NectaryElement {
3956
4203
  fill: "forwards"
3957
4204
  });
3958
4205
  }
3959
- };
4206
+ }
4207
+ #isRectStable(previousRect, nextRect) {
4208
+ 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;
4209
+ }
4210
+ #scheduleReveal(revealRequestId) {
4211
+ const reveal = () => {
4212
+ if (!this.isDomConnected || !this.#isOpen() || this.#revealRequestId !== revealRequestId) {
4213
+ return;
4214
+ }
4215
+ this.#playShowAnimation();
4216
+ this.#hasCompletedFirstReveal = true;
4217
+ };
4218
+ if (this.#hasCompletedFirstReveal) {
4219
+ reveal();
4220
+ return;
4221
+ }
4222
+ let previousRect = null;
4223
+ let observedFrames = 0;
4224
+ let remainingFrames = MAX_FIRST_REVEAL_STABILITY_FRAMES;
4225
+ const waitForStableRect = () => {
4226
+ if (!this.isDomConnected || !this.#isOpen() || this.#revealRequestId !== revealRequestId) {
4227
+ return;
4228
+ }
4229
+ const nextRect = this.#$pop.popoverRect;
4230
+ if (observedFrames >= MIN_FIRST_REVEAL_STABILITY_FRAMES && previousRect !== null && this.#isRectStable(previousRect, nextRect)) {
4231
+ reveal();
4232
+ return;
4233
+ }
4234
+ if (remainingFrames === 0) {
4235
+ reveal();
4236
+ return;
4237
+ }
4238
+ previousRect = nextRect;
4239
+ observedFrames += 1;
4240
+ remainingFrames -= 1;
4241
+ requestAnimationFrame(waitForStableRect);
4242
+ };
4243
+ requestAnimationFrame(waitForStableRect);
4244
+ }
3960
4245
  // HIDE_DELAY ended, begin tooltip hide animation
3961
4246
  #onStateHideStart = () => {
4247
+ this.#revealRequestId += 1;
4248
+ if (this.#animation === null) {
4249
+ return;
4250
+ }
3962
4251
  this.#animation.updatePlaybackRate(-1);
3963
4252
  this.#animation.play();
3964
4253
  };
3965
4254
  // Hide animation ended, tooltip can be hidden
3966
4255
  #onStateHideEnd = () => {
3967
4256
  if (this.#isOpen()) {
3968
- this.#animation.finish();
4257
+ this.#animation?.finish();
3969
4258
  this.#dispatchHideEvent();
3970
4259
  updateBooleanAttribute(this.#$pop, "open", false);
3971
4260
  }
3972
4261
  this.#resetTipOrientation();
4262
+ this.#resetContentOffset();
4263
+ this.#zeroDimensionPlacementRetries = 0;
3973
4264
  this.#unsubscribeMouseLeaveEvents();
3974
4265
  this.#unsubscribeScroll();
3975
4266
  };
@@ -3977,26 +4268,125 @@ class Tooltip extends NectaryElement {
3977
4268
  this.#$tip.style.top = "";
3978
4269
  this.#$tip.style.left = "";
3979
4270
  }
3980
- #updateTipOrientation = () => {
4271
+ #resetContentOffset() {
4272
+ this.#$pop.style.removeProperty("--sinch-pop-offset-x");
4273
+ this.#$pop.style.removeProperty("--sinch-pop-offset-y");
4274
+ }
4275
+ #schedulePlacement(resetZeroDimensionRetries = true) {
4276
+ if (!this.#isOpen() || this.#placementScheduled) {
4277
+ return;
4278
+ }
4279
+ if (resetZeroDimensionRetries) {
4280
+ this.#zeroDimensionPlacementRetries = 0;
4281
+ }
4282
+ this.#placementScheduled = true;
4283
+ requestAnimationFrame(() => {
4284
+ this.#placementScheduled = false;
4285
+ if (!this.isDomConnected || !this.#isOpen()) {
4286
+ return;
4287
+ }
4288
+ this.#updatePlacement();
4289
+ });
4290
+ }
4291
+ #applyContentOffset(offsetX, offsetY) {
4292
+ if (offsetX === 0 && offsetY === 0) {
4293
+ this.#$pop.style.removeProperty("--sinch-pop-offset-x");
4294
+ this.#$pop.style.removeProperty("--sinch-pop-offset-y");
4295
+ return;
4296
+ }
4297
+ this.#$pop.style.setProperty("--sinch-pop-offset-x", `${offsetX}px`);
4298
+ this.#$pop.style.setProperty("--sinch-pop-offset-y", `${offsetY}px`);
4299
+ }
4300
+ #updatePlacement = () => {
4301
+ if (!this.isDomConnected || !this.#isOpen()) {
4302
+ return;
4303
+ }
4304
+ const popRect = this.#$pop.popoverRect;
4305
+ if (popRect.width === 0 || popRect.height === 0) {
4306
+ if (this.#zeroDimensionPlacementRetries >= MAX_ZERO_DIMENSION_PLACEMENT_RETRIES) {
4307
+ return;
4308
+ }
4309
+ this.#zeroDimensionPlacementRetries += 1;
4310
+ this.#schedulePlacement(false);
4311
+ return;
4312
+ }
4313
+ this.#zeroDimensionPlacementRetries = 0;
4314
+ const placementContext = getPlacementContext(this.#$pop);
4315
+ this.#resetContentOffset();
4316
+ this.#updateTipOrientation(placementContext);
4317
+ const didOffset = this.#resolveOverlap(placementContext);
4318
+ if (didOffset) {
4319
+ requestAnimationFrame(() => {
4320
+ if (!this.isDomConnected || !this.#isOpen()) {
4321
+ return;
4322
+ }
4323
+ this.#updateTipOrientation(placementContext);
4324
+ });
4325
+ }
4326
+ };
4327
+ #resolveOverlap(placementContext) {
4328
+ const orientation = this.orientation;
4329
+ const targetRect = toLocalRect(this.#$pop.footprintRect, placementContext);
4330
+ const contentRect = toLocalRect(this.#$content.getBoundingClientRect(), placementContext);
4331
+ const tipRect = toLocalRect(this.#$tip.getBoundingClientRect(), placementContext);
4332
+ const targetBottom = targetRect.y + targetRect.height;
4333
+ const targetRight = targetRect.x + targetRect.width;
4334
+ const bottomEdge = Math.max(contentRect.y + contentRect.height, tipRect.y + tipRect.height);
4335
+ const topEdge = Math.min(contentRect.y, tipRect.y);
4336
+ const rightEdge = Math.max(contentRect.x + contentRect.width, tipRect.x + tipRect.width);
4337
+ const leftEdge = Math.min(contentRect.x, tipRect.x);
4338
+ let offsetX = 0;
4339
+ let offsetY = 0;
4340
+ if (orientation.startsWith("top")) {
4341
+ if (bottomEdge > targetRect.y + OVERLAP_TOLERANCE) {
4342
+ offsetY = targetRect.y - bottomEdge;
4343
+ }
4344
+ } else if (orientation.startsWith("bottom")) {
4345
+ if (topEdge < targetBottom - OVERLAP_TOLERANCE) {
4346
+ offsetY = targetBottom - topEdge;
4347
+ }
4348
+ } else if (orientation === "left") {
4349
+ if (rightEdge > targetRect.x + OVERLAP_TOLERANCE) {
4350
+ offsetX = targetRect.x - rightEdge;
4351
+ }
4352
+ } else if (orientation === "right") {
4353
+ if (leftEdge < targetRight - OVERLAP_TOLERANCE) {
4354
+ offsetX = targetRight - leftEdge;
4355
+ }
4356
+ }
4357
+ this.#applyContentOffset(offsetX, offsetY);
4358
+ return offsetX !== 0 || offsetY !== 0;
4359
+ }
4360
+ #updateTipOrientation = (placementContext) => {
3981
4361
  const orient = this.orientation;
3982
4362
  if (!("footprintRect" in this.#$pop)) {
3983
- requestAnimationFrame(this.#updateTipOrientation);
4363
+ requestAnimationFrame(() => {
4364
+ if (!this.isDomConnected || !this.#isOpen()) {
4365
+ return;
4366
+ }
4367
+ this.#updateTipOrientation();
4368
+ });
3984
4369
  return;
3985
4370
  }
3986
- const targetRect = this.#$pop.footprintRect;
3987
- const contentRect = this.#$content.getBoundingClientRect();
4371
+ const ctx = placementContext ?? getPlacementContext(this.#$pop);
4372
+ const targetRect = toLocalRect(this.#$pop.footprintRect, ctx);
4373
+ const contentRect = toLocalRect(this.#$content.getBoundingClientRect(), ctx);
3988
4374
  const diffX = targetRect.x - contentRect.x;
3989
4375
  const diffY = targetRect.y - contentRect.y;
4376
+ const targetWidth = targetRect.width;
4377
+ const targetHeight = targetRect.height;
4378
+ const contentWidth = contentRect.width;
4379
+ const contentHeight = contentRect.height;
3990
4380
  if (orient === "left" || orient === "right") {
3991
- const yPos = Math.max(TIP_SIZE$1, Math.min(diffY + targetRect.height / 2, contentRect.height - TIP_SIZE$1));
4381
+ const yPos = Math.max(TIP_SIZE$1, Math.min(diffY + targetHeight / 2, contentHeight - TIP_SIZE$1));
3992
4382
  this.#$tip.style.top = `${yPos}px`;
3993
4383
  } else {
3994
- let xPos = Math.max(TIP_SIZE$1, Math.min(diffX + targetRect.width / 2, contentRect.width - TIP_SIZE$1));
4384
+ let xPos = Math.max(TIP_SIZE$1, Math.min(diffX + targetWidth / 2, contentWidth - TIP_SIZE$1));
3995
4385
  if (orient === "bottom-left" || orient === "top-left") {
3996
- xPos = Math.max(xPos, contentRect.width * 0.75);
4386
+ xPos = Math.max(xPos, contentWidth * 0.75);
3997
4387
  }
3998
4388
  if (orient === "bottom-right" || orient === "top-right") {
3999
- xPos = Math.min(xPos, contentRect.width * 0.25);
4389
+ xPos = Math.min(xPos, contentWidth * 0.25);
4000
4390
  }
4001
4391
  this.#$tip.style.left = `${xPos}px`;
4002
4392
  }