@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/button/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context, subscribeContext } from "../utils/context.js";
2
- import { updateAttribute, isAttrEqual, updateBooleanAttribute, isAttrTrue, updateLiteralAttribute, getLiteralAttribute, getAttribute, getBooleanAttribute } from "../utils/dom.js";
2
+ import { isAttrEqual, updateAttribute, updateBooleanAttribute, isAttrTrue, updateLiteralAttribute, getLiteralAttribute, getAttribute, getBooleanAttribute } from "../utils/dom.js";
3
3
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
4
4
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
5
5
  import { requestSubmitForm } from "../utils/form.js";
@@ -28,9 +28,12 @@ class Button extends NectaryElement {
28
28
  super.connectedCallback();
29
29
  this.#controller = new AbortController();
30
30
  const { signal } = this.#controller;
31
- this.setAttribute("role", "button");
32
- this.#internals.role = "button";
33
- this.tabIndex = 0;
31
+ if (!this.hasAttribute("tabindex")) {
32
+ this.setAttribute("tabindex", "0");
33
+ }
34
+ if (!this.hasAttribute("role")) {
35
+ this.setAttribute("role", "button");
36
+ }
34
37
  this.addEventListener("click", this.#onButtonClick, { signal });
35
38
  this.addEventListener("focus", this.#onButtonFocus, { signal });
36
39
  this.addEventListener("blur", this.#onButtonBlur, { signal });
@@ -54,7 +57,9 @@ class Button extends NectaryElement {
54
57
  "toggled",
55
58
  "size",
56
59
  "data-size",
57
- "data-managed-aria-disabled"
60
+ "data-managed-aria-disabled",
61
+ "role",
62
+ "tabindex"
58
63
  ];
59
64
  }
60
65
  attributeChangedCallback(name, oldVal, newVal) {
@@ -92,6 +97,33 @@ class Button extends NectaryElement {
92
97
  this.#onSizeUpdate();
93
98
  break;
94
99
  }
100
+ case "role": {
101
+ if (isAttrEqual(oldVal, newVal)) {
102
+ break;
103
+ }
104
+ const effectiveRole = newVal !== null && newVal !== "" ? newVal : "button";
105
+ this.#internals.role = effectiveRole;
106
+ if (newVal === null || newVal === "") {
107
+ this.setAttribute("role", "button");
108
+ }
109
+ break;
110
+ }
111
+ case "tabindex": {
112
+ if (newVal === null) {
113
+ if (this.isDomConnected) {
114
+ this.setAttribute("tabindex", "0");
115
+ }
116
+ break;
117
+ }
118
+ if (isAttrEqual(oldVal, newVal)) {
119
+ break;
120
+ }
121
+ const parsed = Number.parseInt(newVal, 10);
122
+ if (Number.isNaN(parsed)) {
123
+ this.setAttribute("tabindex", "0");
124
+ }
125
+ break;
126
+ }
95
127
  }
96
128
  }
97
129
  set type(value) {
package/button/types.d.ts CHANGED
@@ -5,6 +5,10 @@ export type TSinchButtonType = 'primary' | 'secondary'
5
5
  /** @deprecated */
6
6
  | 'tertiary' | 'subtle-primary' | 'subtle-secondary' | 'cta-primary' | 'cta-secondary' | 'destructive';
7
7
  export type TSinchButtonProps = {
8
+ /** ARIA role, `button` by default. Curated union for common use cases; hosts can still set other roles in HTML. */
9
+ role?: 'button' | 'tab' | 'menuitem' | 'switch' | 'link' | 'option';
10
+ /** Keyboard navigation order, `0` by default */
11
+ tabIndex?: number;
8
12
  /** Button Type */
9
13
  type?: TSinchButtonType;
10
14
  /** Size, `m` by default */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.29.1",
3
+ "version": "5.30.0",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
package/pop/index.d.ts CHANGED
@@ -2,6 +2,12 @@ import { NectaryElement } from '../utils';
2
2
  import type { TSinchPopOrientation } from './types';
3
3
  import type { TRect } from '../types';
4
4
  export * from './types';
5
+ /**
6
+ * `<sinch-pop>` is the overlay primitive shared by `<sinch-tooltip>`,
7
+ * `<sinch-dropdown>` and similar components. It anchors a popover to a target
8
+ * element, manages its open lifecycle, and reconciles position across viewport
9
+ * resizes, scrolling, and transformed ancestors.
10
+ */
5
11
  export declare class Pop extends NectaryElement {
6
12
  #private;
7
13
  constructor();
package/pop/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Context, subscribeContext } from "../utils/context.js";
2
- import { getBooleanAttribute, updateBooleanAttribute, getLiteralAttribute, updateLiteralAttribute, updateIntegerAttribute, getIntegerAttribute, isAttrEqual, getScrollableParents, isAttrTrue } from "../utils/dom.js";
2
+ import { getBooleanAttribute, updateBooleanAttribute, getLiteralAttribute, updateLiteralAttribute, updateIntegerAttribute, getIntegerAttribute, isAttrEqual, getScrollableParents, getTransformedAncestor, isAttrTrue } from "../utils/dom.js";
3
3
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
4
4
  import { getRect } from "../utils/rect.js";
5
5
  import { getFirstSlotElement, getFirstFocusableElement, isElementFocused } from "../utils/slot.js";
6
6
  import { throttleAnimationFrame } from "../utils/throttle.js";
7
7
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
8
8
  import { isTargetEqual } from "../utils/event-target.js";
9
- import { orientationValues, disableOverscroll, enableOverscroll } from "./utils.js";
10
- const templateHTML = '<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>';
9
+ import { getPlacementContext, toLocalRect } from "../utils/placement.js";
10
+ import { orientationValues, disableOverscroll, enableOverscroll, getAnchorPosition, clampPosition } from "./utils.js";
11
+ const templateHTML = '<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>';
11
12
  const template = document.createElement("template");
12
13
  template.innerHTML = templateHTML;
13
14
  class Pop extends NectaryElement {
@@ -15,6 +16,7 @@ class Pop extends NectaryElement {
15
16
  #$focus;
16
17
  #$dialog;
17
18
  #resizeThrottle;
19
+ #scrollPositionThrottle;
18
20
  #resizeObserver;
19
21
  #$targetSlot;
20
22
  #$targetOpenSlot;
@@ -24,10 +26,15 @@ class Pop extends NectaryElement {
24
26
  #controller;
25
27
  #keydownContext;
26
28
  #visibilityContext;
27
- #targetStyleValue = null;
28
29
  #modalWidth = 0;
29
30
  #modalHeight = 0;
30
31
  #scrollableParents = [];
32
+ #openSession = {
33
+ effectiveAllowScroll: false,
34
+ modalSemantics: false,
35
+ targetStyleValue: null,
36
+ transformedAncestor: null
37
+ };
31
38
  constructor() {
32
39
  super();
33
40
  const shadowRoot = this.attachShadow();
@@ -40,6 +47,9 @@ class Pop extends NectaryElement {
40
47
  this.#$contentSlot = shadowRoot.querySelector('slot[name="content"]');
41
48
  this.#$targetOpenWrapper = shadowRoot.querySelector("#target-open");
42
49
  this.#resizeThrottle = throttleAnimationFrame(this.#updateOrientation);
50
+ this.#scrollPositionThrottle = throttleAnimationFrame(() => {
51
+ this.#updatePosition(false);
52
+ });
43
53
  this.#resizeObserver = new ResizeObserver(() => {
44
54
  this.#resizeThrottle.fn();
45
55
  });
@@ -69,6 +79,7 @@ class Pop extends NectaryElement {
69
79
  this.#controller.abort();
70
80
  this.#controller = null;
71
81
  this.#resizeThrottle.cancel();
82
+ this.#scrollPositionThrottle.cancel();
72
83
  this.#resizeObserver.disconnect();
73
84
  this.#onCollapse();
74
85
  }
@@ -164,10 +175,68 @@ class Pop extends NectaryElement {
164
175
  }
165
176
  return item;
166
177
  }
178
+ #prepareTransferredTarget() {
179
+ const targetEl = this.#getFirstTargetElement(this.#$targetSlot);
180
+ const targetElComputedStyle = getComputedStyle(targetEl);
181
+ const marginLeft = parseInt(targetElComputedStyle.marginLeft, 10);
182
+ const marginRight = parseInt(targetElComputedStyle.marginRight, 10);
183
+ const marginTop = parseInt(targetElComputedStyle.marginTop, 10);
184
+ const marginBottom = parseInt(targetElComputedStyle.marginBottom, 10);
185
+ const targetRect = this.#getTargetRect();
186
+ this.#$targetWrapper.style.setProperty("display", "block");
187
+ this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
188
+ this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
189
+ this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
190
+ this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
191
+ this.#openSession.targetStyleValue = targetEl.getAttribute("style");
192
+ targetEl.style.setProperty("margin", "0");
193
+ targetEl.style.setProperty("position", "static");
194
+ if (targetElComputedStyle.transform !== "none") {
195
+ const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
196
+ targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
197
+ }
198
+ getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
199
+ }
200
+ #restoreTransferredTarget() {
201
+ const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
202
+ const { targetStyleValue } = this.#openSession;
203
+ if (targetStyleValue === null) {
204
+ targetEl.style.removeProperty("margin");
205
+ targetEl.style.removeProperty("position");
206
+ targetEl.style.removeProperty("transform");
207
+ } else {
208
+ targetEl.setAttribute("style", targetStyleValue);
209
+ }
210
+ getFirstSlotElement(this.#$targetOpenSlot)?.setAttribute("slot", "target");
211
+ this.#$targetWrapper.style.removeProperty("display");
212
+ this.#$targetWrapper.style.removeProperty("width");
213
+ this.#$targetWrapper.style.removeProperty("height");
214
+ }
215
+ #subscribeScrollTracking() {
216
+ this.#scrollableParents = getScrollableParents(this.#getFirstTargetElement(this.#$targetSlot));
217
+ this.#scrollableParents.forEach((el) => {
218
+ el.addEventListener("scroll", this.#onScrollableParentScroll, { passive: true, capture: true });
219
+ });
220
+ }
221
+ #unsubscribeScrollTracking() {
222
+ this.#scrollableParents.forEach((el) => {
223
+ el.removeEventListener("scroll", this.#onScrollableParentScroll, { capture: true });
224
+ });
225
+ }
167
226
  #onExpand() {
168
227
  if (!this.isDomConnected || this.#$dialog.open) {
169
228
  return;
170
229
  }
230
+ const transformedAncestor = getTransformedAncestor(this);
231
+ const effectiveAllowScroll = this.allowScroll || transformedAncestor != null;
232
+ const shouldUseModal = this.modal && transformedAncestor == null;
233
+ const openAsModal = shouldUseModal || !effectiveAllowScroll;
234
+ this.#openSession = {
235
+ effectiveAllowScroll,
236
+ modalSemantics: shouldUseModal,
237
+ targetStyleValue: null,
238
+ transformedAncestor
239
+ };
171
240
  this.#$targetSlot.addEventListener("blur", this.#stopEventPropagation, true);
172
241
  this.#$focus.setAttribute("tabindex", "-1");
173
242
  this.#$focus.style.display = "block";
@@ -177,7 +246,7 @@ class Pop extends NectaryElement {
177
246
  this.#$targetSlot.removeEventListener("blur", this.#stopEventPropagation, true);
178
247
  this.#$focus.removeAttribute("tabindex");
179
248
  this.#$focus.removeAttribute("style");
180
- if (this.modal || !this.allowScroll) {
249
+ if (openAsModal) {
181
250
  this.#$dialog.showModal();
182
251
  } else {
183
252
  this.#$dialog.show();
@@ -185,32 +254,13 @@ class Pop extends NectaryElement {
185
254
  this.#$targetWrapper.setAttribute("aria-expanded", "true");
186
255
  this.#updateOrientation();
187
256
  this.#resizeObserver.observe(this.#$dialog);
188
- if (this.modal) {
257
+ if (shouldUseModal) {
189
258
  getFirstFocusableElement(this.#$contentSlot)?.focus();
190
259
  } else {
191
- if (!this.allowScroll) {
192
- const $targetEl = this.#getFirstTargetElement(this.#$targetSlot);
193
- const targetElComputedStyle = getComputedStyle($targetEl);
194
- const marginLeft = parseInt(targetElComputedStyle.marginLeft);
195
- const marginRight = parseInt(targetElComputedStyle.marginRight);
196
- const marginTop = parseInt(targetElComputedStyle.marginTop);
197
- const marginBottom = parseInt(targetElComputedStyle.marginBottom);
198
- const targetRect = this.#getTargetRect();
199
- this.#$targetWrapper.style.setProperty("display", "block");
200
- this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
201
- this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
202
- this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
203
- this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
204
- this.#targetStyleValue = $targetEl.getAttribute("style");
205
- $targetEl.style.setProperty("margin", "0");
206
- $targetEl.style.setProperty("position", "static");
207
- if (targetElComputedStyle.transform !== "none") {
208
- const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
209
- $targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
210
- }
211
- getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
260
+ if (!effectiveAllowScroll) {
261
+ this.#prepareTransferredTarget();
212
262
  }
213
- const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
263
+ const activeSlot = effectiveAllowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
214
264
  activeSlot.addEventListener("keydown", this.#onTargetKeydown);
215
265
  if (this.#targetActiveElement !== null) {
216
266
  activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
@@ -227,13 +277,10 @@ class Pop extends NectaryElement {
227
277
  }
228
278
  }
229
279
  }
230
- if (!this.allowScroll) {
280
+ if (!effectiveAllowScroll) {
231
281
  disableOverscroll();
232
282
  } else {
233
- this.#scrollableParents = getScrollableParents(this.#getFirstTargetElement(this.#$targetSlot));
234
- this.#scrollableParents.forEach((el) => {
235
- el.addEventListener("scroll", () => this.#updatePosition(false), { passive: true, capture: true });
236
- });
283
+ this.#subscribeScrollTracking();
237
284
  }
238
285
  window.addEventListener("resize", this.#onResize);
239
286
  requestAnimationFrame(() => {
@@ -248,9 +295,11 @@ class Pop extends NectaryElement {
248
295
  if (!this.#$dialog.open) {
249
296
  return;
250
297
  }
298
+ const openSession = this.#openSession;
299
+ const effectiveAllowScroll = openSession.effectiveAllowScroll;
251
300
  this.#resizeObserver.disconnect();
252
- const isNonModal = !this.modal;
253
- const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
301
+ const isNonModal = !openSession.modalSemantics;
302
+ const activeSlot = effectiveAllowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
254
303
  this.#dispatchContentVisibility(false);
255
304
  activeSlot.removeEventListener("keydown", this.#onTargetKeydown);
256
305
  if (isNonModal) {
@@ -261,19 +310,8 @@ class Pop extends NectaryElement {
261
310
  if (isNonModal) {
262
311
  activeSlot.removeEventListener("blur", this.#captureActiveElement, true);
263
312
  }
264
- if (isNonModal && !this.allowScroll) {
265
- const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
266
- targetEl.style.removeProperty("margin");
267
- targetEl.style.removeProperty("position");
268
- targetEl.style.removeProperty("transform");
269
- if (this.#targetStyleValue !== null) {
270
- targetEl.setAttribute("style", this.#targetStyleValue);
271
- this.#targetStyleValue = null;
272
- }
273
- getFirstSlotElement(this.#$targetOpenSlot)?.setAttribute("slot", "target");
274
- this.#$targetWrapper.style.removeProperty("display");
275
- this.#$targetWrapper.style.removeProperty("width");
276
- this.#$targetWrapper.style.removeProperty("height");
313
+ if (isNonModal && !effectiveAllowScroll) {
314
+ this.#restoreTransferredTarget();
277
315
  }
278
316
  if (this.#targetActiveElement !== null) {
279
317
  if (!isElementFocused(this.#targetActiveElement)) {
@@ -293,57 +331,69 @@ class Pop extends NectaryElement {
293
331
  this.#targetActiveElement = null;
294
332
  }
295
333
  }
296
- if (!this.allowScroll) {
334
+ if (!effectiveAllowScroll) {
297
335
  enableOverscroll();
298
336
  } else {
299
- this.#scrollableParents.forEach((el) => {
300
- el.removeEventListener("scroll", () => this.#updatePosition(false), { capture: true });
301
- });
337
+ this.#unsubscribeScrollTracking();
302
338
  }
303
339
  this.#resizeThrottle.cancel();
340
+ this.#scrollPositionThrottle.cancel();
304
341
  window.removeEventListener("resize", this.#onResize);
305
342
  this.#scrollableParents = [];
306
343
  this.#$contentSlot.removeEventListener("slotchange", this.#onContentSlotChange);
344
+ this.#openSession = {
345
+ effectiveAllowScroll: false,
346
+ modalSemantics: false,
347
+ targetStyleValue: null,
348
+ transformedAncestor: null
349
+ };
307
350
  }
308
351
  #onResize = () => {
309
352
  this.#resizeThrottle.fn();
310
353
  };
354
+ #onScrollableParentScroll = () => {
355
+ this.#scrollPositionThrottle.fn();
356
+ };
311
357
  #updatePosition = (updateWidth) => {
312
- const targetRect = this.modal || this.allowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
358
+ const placementContext = getPlacementContext(this, this.#openSession.transformedAncestor);
359
+ const { scaleX, scaleY, boundsWidth, boundsHeight } = placementContext;
360
+ const { modalSemantics, effectiveAllowScroll } = this.#openSession;
361
+ const shouldClamp = !effectiveAllowScroll;
362
+ const targetRectViewport = modalSemantics || effectiveAllowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
313
363
  const orient = this.orientation;
314
- const modalWidth = this.#modalWidth;
315
- const modalHeight = this.#modalHeight;
316
364
  const inset = this.inset;
317
- let xPos = 0;
318
- let yPos = 0;
319
- if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
320
- xPos = targetRect.x;
321
- }
322
- if (orient === "bottom-left" || orient === "top-left") {
323
- xPos = targetRect.x + targetRect.width - modalWidth;
324
- }
325
- if (orient === "bottom-center" || orient === "top-center") {
326
- xPos = targetRect.x + targetRect.width / 2 - modalWidth / 2;
327
- }
328
- if (orient === "center-right") {
329
- xPos = targetRect.x + targetRect.width;
330
- }
331
- if (orient === "center-left") {
332
- xPos = targetRect.x - modalWidth;
333
- }
334
- if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
335
- yPos = targetRect.y + targetRect.height;
336
- }
337
- if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
338
- yPos = targetRect.y - modalHeight;
339
- }
340
- if (orient === "center-left" || orient === "center-right") {
341
- yPos = targetRect.y + targetRect.height / 2 - modalHeight / 2;
342
- }
343
- const clampedXPos = Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset));
344
- const clampedYPos = Math.max(inset, Math.min(yPos, window.innerHeight - modalHeight - inset));
345
- if (this.hideOutsideViewport && this.#isPopPointInViewport(xPos, yPos)) {
346
- this.#$dialog.style.setProperty("visibility", "hidden");
365
+ const insetX = inset / scaleX;
366
+ const insetY = inset / scaleY;
367
+ const targetRect = toLocalRect(targetRectViewport, placementContext);
368
+ const liveRect = this.#$dialog.open ? this.#$dialog.getBoundingClientRect() : null;
369
+ const isStretch = orient === "top-stretch" || orient === "bottom-stretch";
370
+ const modalWidthViewport = isStretch ? this.#modalWidth : liveRect?.width ?? this.#modalWidth;
371
+ const modalHeightViewport = liveRect?.height ?? this.#modalHeight;
372
+ const modalWidth = modalWidthViewport / scaleX;
373
+ const modalHeight = modalHeightViewport / scaleY;
374
+ const localPos = getAnchorPosition(targetRect, modalWidth, modalHeight, orient);
375
+ const xPos = localPos.x;
376
+ const yPos = localPos.y;
377
+ const localViewportInfos = { boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight };
378
+ const clampedPosition = shouldClamp ? clampPosition({ x: xPos, y: yPos, ...localViewportInfos }) : { x: xPos, y: yPos };
379
+ const clampedXPos = clampedPosition.x;
380
+ const clampedYPos = clampedPosition.y;
381
+ if (this.hideOutsideViewport) {
382
+ const viewportPosition = getAnchorPosition(targetRectViewport, modalWidthViewport, modalHeightViewport, orient);
383
+ const visibilityViewportInfos = {
384
+ boundsWidth: window.innerWidth,
385
+ boundsHeight: window.innerHeight,
386
+ insetX: inset,
387
+ insetY: inset,
388
+ modalWidth: modalWidthViewport,
389
+ modalHeight: modalHeightViewport
390
+ };
391
+ const isOutOfViewport = this.#isOutsideViewport(viewportPosition.x, viewportPosition.y, visibilityViewportInfos);
392
+ if (isOutOfViewport) {
393
+ this.#$dialog.style.setProperty("visibility", "hidden");
394
+ } else {
395
+ this.#$dialog.style.removeProperty("visibility");
396
+ }
347
397
  } else {
348
398
  this.#$dialog.style.removeProperty("visibility");
349
399
  }
@@ -352,7 +402,7 @@ class Pop extends NectaryElement {
352
402
  if (updateWidth === true) {
353
403
  this.#$dialog.style.setProperty("width", `${modalWidth}px`);
354
404
  }
355
- if (!this.modal && !this.allowScroll) {
405
+ if (!modalSemantics && !effectiveAllowScroll) {
356
406
  const targetLeftPos = targetRect.x - clampedXPos;
357
407
  const targetTopPos = targetRect.y - clampedYPos;
358
408
  this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
@@ -428,13 +478,10 @@ class Pop extends NectaryElement {
428
478
  this.#updateOrientation();
429
479
  }
430
480
  };
431
- #isPopPointInViewport(x, y) {
432
- const inset = this.inset;
433
- const modalWidth = this.#modalWidth;
434
- const modalHeight = this.#modalHeight;
435
- const clampedX = Math.max(inset, Math.min(x, window.innerWidth - modalWidth - inset));
436
- const clampedY = Math.max(inset, Math.min(y, window.innerHeight - modalHeight - inset));
437
- return Math.abs(clampedX - x) > 2 || Math.abs(clampedY - y) > 2;
481
+ #isOutsideViewport(x, y, viewportInfos) {
482
+ const { boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight } = viewportInfos;
483
+ const clampedPosition = clampPosition({ x, y, boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight });
484
+ return Math.abs(clampedPosition.x - x) > 2 || Math.abs(clampedPosition.y - y) > 2;
438
485
  }
439
486
  }
440
487
  defineCustomElement("sinch-pop", Pop);
package/pop/utils.d.ts CHANGED
@@ -1,4 +1,24 @@
1
1
  import type { TSinchPopOrientation } from './types';
2
+ import type { TRect } from '../types';
2
3
  export declare const orientationValues: readonly TSinchPopOrientation[];
4
+ type TClampPositionArgs = {
5
+ x: number;
6
+ y: number;
7
+ boundsWidth: number;
8
+ boundsHeight: number;
9
+ insetX: number;
10
+ insetY: number;
11
+ modalWidth: number;
12
+ modalHeight: number;
13
+ };
14
+ export declare const getAnchorPosition: (rect: TRect, width: number, height: number, orient: TSinchPopOrientation) => {
15
+ x: number;
16
+ y: number;
17
+ };
18
+ export declare const clampPosition: ({ x, y, boundsWidth, boundsHeight, insetX, insetY, modalWidth, modalHeight, }: TClampPositionArgs) => {
19
+ x: number;
20
+ y: number;
21
+ };
3
22
  export declare const disableOverscroll: () => void;
4
23
  export declare const enableOverscroll: () => void;
24
+ export {};
package/pop/utils.js CHANGED
@@ -10,6 +10,50 @@ const orientationValues = [
10
10
  "center-left",
11
11
  "center-right"
12
12
  ];
13
+ const getAnchorPosition = (rect, width, height, orient) => {
14
+ let x = 0;
15
+ let y = 0;
16
+ if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
17
+ x = rect.x;
18
+ }
19
+ if (orient === "bottom-left" || orient === "top-left") {
20
+ x = rect.x + rect.width - width;
21
+ }
22
+ if (orient === "bottom-center" || orient === "top-center") {
23
+ x = rect.x + rect.width / 2 - width / 2;
24
+ }
25
+ if (orient === "center-right") {
26
+ x = rect.x + rect.width;
27
+ }
28
+ if (orient === "center-left") {
29
+ x = rect.x - width;
30
+ }
31
+ if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
32
+ y = rect.y + rect.height;
33
+ }
34
+ if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
35
+ y = rect.y - height;
36
+ }
37
+ if (orient === "center-left" || orient === "center-right") {
38
+ y = rect.y + rect.height / 2 - height / 2;
39
+ }
40
+ return { x, y };
41
+ };
42
+ const clampPosition = ({
43
+ x,
44
+ y,
45
+ boundsWidth,
46
+ boundsHeight,
47
+ insetX,
48
+ insetY,
49
+ modalWidth,
50
+ modalHeight
51
+ }) => {
52
+ return {
53
+ x: Math.max(insetX, Math.min(x, boundsWidth - modalWidth - insetX)),
54
+ y: Math.max(insetY, Math.min(y, boundsHeight - modalHeight - insetY))
55
+ };
56
+ };
13
57
  const bodyEl = document.body;
14
58
  const disableOverscroll = () => {
15
59
  bodyEl.__pop_counter__ = (bodyEl.__pop_counter__ ?? 0) + 1;
@@ -26,7 +70,9 @@ const enableOverscroll = () => {
26
70
  }
27
71
  };
28
72
  export {
73
+ clampPosition,
29
74
  disableOverscroll,
30
75
  enableOverscroll,
76
+ getAnchorPosition,
31
77
  orientationValues
32
78
  };