@nectary/components 5.3.0 → 5.4.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
@@ -127,6 +127,22 @@ const shouldReduceMotion = () => window.matchMedia("(prefers-reduced-motion: red
127
127
  const isAttrEqual = (oldVal, newVal) => {
128
128
  return oldVal === newVal || newVal === null && oldVal === "false" || newVal === "" && oldVal === "true";
129
129
  };
130
+ const getScrollableParents = (node) => {
131
+ const scrollableParents = [];
132
+ if (node == null) {
133
+ return scrollableParents;
134
+ }
135
+ let parent = node.parentElement;
136
+ while (parent != null) {
137
+ const computedStyle = getComputedStyle(parent);
138
+ if ((parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) && (computedStyle.overflow === "auto" || computedStyle.overflow === "scroll" || computedStyle.overflowY === "auto" || computedStyle.overflowY === "scroll" || computedStyle.overflowX === "auto" || computedStyle.overflowX === "scroll")) {
139
+ scrollableParents.push(parent);
140
+ }
141
+ parent = parent.parentElement;
142
+ }
143
+ scrollableParents.push(document);
144
+ return scrollableParents;
145
+ };
130
146
  class NectaryElementBase extends HTMLElement {
131
147
  static _isGlobal = false;
132
148
  static get elementName() {
@@ -2846,6 +2862,7 @@ class Pop extends NectaryElement {
2846
2862
  #targetStyleValue = null;
2847
2863
  #modalWidth = 0;
2848
2864
  #modalHeight = 0;
2865
+ #scrollableParents = [];
2849
2866
  constructor() {
2850
2867
  super();
2851
2868
  const shadowRoot = this.attachShadow();
@@ -2896,6 +2913,12 @@ class Pop extends NectaryElement {
2896
2913
  "open"
2897
2914
  ];
2898
2915
  }
2916
+ get allowScroll() {
2917
+ return getBooleanAttribute(this, "allow-scroll");
2918
+ }
2919
+ get hideOutsideViewport() {
2920
+ return getBooleanAttribute(this, "hide-outside-viewport");
2921
+ }
2899
2922
  set modal(isModal) {
2900
2923
  updateBooleanAttribute(this, "modal", isModal);
2901
2924
  }
@@ -2926,6 +2949,9 @@ class Pop extends NectaryElement {
2926
2949
  get popoverRect() {
2927
2950
  return getRect(this.#$dialog);
2928
2951
  }
2952
+ get shouldCloseOnBackdropClick() {
2953
+ return !getBooleanAttribute(this, "disable-backdrop-close");
2954
+ }
2929
2955
  attributeChangedCallback(name, oldVal, newVal) {
2930
2956
  if (isAttrEqual(oldVal, newVal)) {
2931
2957
  return;
@@ -2986,57 +3012,71 @@ class Pop extends NectaryElement {
2986
3012
  this.#$targetSlot.removeEventListener("blur", this.#stopEventPropagation, true);
2987
3013
  this.#$focus.removeAttribute("tabindex");
2988
3014
  this.#$focus.removeAttribute("style");
2989
- this.#$dialog.showModal();
3015
+ if (this.modal || !this.allowScroll) {
3016
+ this.#$dialog.showModal();
3017
+ } else {
3018
+ this.#$dialog.show();
3019
+ }
2990
3020
  this.#$targetWrapper.setAttribute("aria-expanded", "true");
2991
3021
  this.#updateOrientation();
2992
3022
  this.#resizeObserver.observe(this.#$dialog);
2993
3023
  if (this.modal) {
2994
3024
  getFirstFocusableElement(this.#$contentSlot)?.focus();
2995
3025
  } else {
2996
- const $targetEl = this.#getFirstTargetElement(this.#$targetSlot);
2997
- const targetElComputedStyle = getComputedStyle($targetEl);
2998
- const marginLeft = parseInt(targetElComputedStyle.marginLeft);
2999
- const marginRight = parseInt(targetElComputedStyle.marginRight);
3000
- const marginTop = parseInt(targetElComputedStyle.marginTop);
3001
- const marginBottom = parseInt(targetElComputedStyle.marginBottom);
3002
- const targetRect = this.#getTargetRect();
3003
- this.#$targetWrapper.style.setProperty("display", "block");
3004
- this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
3005
- this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
3006
- this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
3007
- this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
3008
- this.#targetStyleValue = $targetEl.getAttribute("style");
3009
- $targetEl.style.setProperty("margin", "0");
3010
- $targetEl.style.setProperty("position", "static");
3011
- if (targetElComputedStyle.transform !== "none") {
3012
- const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
3013
- $targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
3014
- }
3015
- getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
3016
- this.#$targetOpenSlot.addEventListener("keydown", this.#onTargetKeydown);
3026
+ if (!this.allowScroll) {
3027
+ const $targetEl = this.#getFirstTargetElement(this.#$targetSlot);
3028
+ const targetElComputedStyle = getComputedStyle($targetEl);
3029
+ const marginLeft = parseInt(targetElComputedStyle.marginLeft);
3030
+ const marginRight = parseInt(targetElComputedStyle.marginRight);
3031
+ const marginTop = parseInt(targetElComputedStyle.marginTop);
3032
+ const marginBottom = parseInt(targetElComputedStyle.marginBottom);
3033
+ const targetRect = this.#getTargetRect();
3034
+ this.#$targetWrapper.style.setProperty("display", "block");
3035
+ this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
3036
+ this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
3037
+ this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
3038
+ this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
3039
+ this.#targetStyleValue = $targetEl.getAttribute("style");
3040
+ $targetEl.style.setProperty("margin", "0");
3041
+ $targetEl.style.setProperty("position", "static");
3042
+ if (targetElComputedStyle.transform !== "none") {
3043
+ const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
3044
+ $targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
3045
+ }
3046
+ getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
3047
+ }
3048
+ const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3049
+ activeSlot.addEventListener("keydown", this.#onTargetKeydown);
3017
3050
  if (this.#targetActiveElement !== null) {
3018
- this.#$targetOpenSlot.addEventListener("focus", this.#stopEventPropagation, true);
3051
+ activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
3019
3052
  this.#targetActiveElement.focus();
3020
- this.#$targetOpenSlot.removeEventListener("focus", this.#stopEventPropagation, true);
3053
+ activeSlot.removeEventListener("focus", this.#stopEventPropagation, true);
3021
3054
  if (!isElementFocused(this.#targetActiveElement)) {
3022
3055
  requestAnimationFrame(() => {
3023
3056
  if (this.isDomConnected && this.#$dialog.open) {
3024
- this.#$targetOpenSlot.addEventListener("focus", this.#stopEventPropagation, true);
3057
+ activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
3025
3058
  this.#targetActiveElement.focus();
3026
- this.#$targetOpenSlot.removeEventListener("focus", this.#stopEventPropagation, true);
3059
+ activeSlot.removeEventListener("focus", this.#stopEventPropagation, true);
3027
3060
  }
3028
3061
  });
3029
3062
  }
3030
3063
  }
3031
3064
  }
3032
- disableOverscroll();
3033
- window.addEventListener("scroll", this.#updatePosition, { passive: false });
3065
+ if (!this.allowScroll) {
3066
+ disableOverscroll();
3067
+ } else {
3068
+ this.#scrollableParents = getScrollableParents(this.#getFirstTargetElement(this.#$targetSlot));
3069
+ this.#scrollableParents.forEach((el) => {
3070
+ el.addEventListener("scroll", () => this.#updatePosition(false), { passive: true, capture: true });
3071
+ });
3072
+ }
3034
3073
  window.addEventListener("resize", this.#onResize);
3035
3074
  requestAnimationFrame(() => {
3036
3075
  if (this.isDomConnected && this.#$dialog.open) {
3037
3076
  this.#$contentSlot.addEventListener("slotchange", this.#onContentSlotChange);
3038
3077
  }
3039
3078
  });
3079
+ requestAnimationFrame(() => this.#updatePosition());
3040
3080
  this.#dispatchContentVisibility(true);
3041
3081
  }
3042
3082
  #onCollapse() {
@@ -3045,17 +3085,18 @@ class Pop extends NectaryElement {
3045
3085
  }
3046
3086
  this.#resizeObserver.disconnect();
3047
3087
  const isNonModal = !this.modal;
3088
+ const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
3048
3089
  this.#dispatchContentVisibility(false);
3049
- this.#$targetOpenSlot.removeEventListener("keydown", this.#onTargetKeydown);
3090
+ activeSlot.removeEventListener("keydown", this.#onTargetKeydown);
3050
3091
  if (isNonModal) {
3051
- this.#$targetOpenSlot.addEventListener("blur", this.#captureActiveElement, true);
3092
+ activeSlot.addEventListener("blur", this.#captureActiveElement, true);
3052
3093
  }
3053
3094
  this.#$dialog.close();
3054
3095
  this.#$targetWrapper.setAttribute("aria-expanded", "false");
3055
3096
  if (isNonModal) {
3056
- this.#$targetOpenSlot.removeEventListener("blur", this.#captureActiveElement, true);
3097
+ activeSlot.removeEventListener("blur", this.#captureActiveElement, true);
3057
3098
  }
3058
- if (isNonModal) {
3099
+ if (isNonModal && !this.allowScroll) {
3059
3100
  const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
3060
3101
  targetEl.style.removeProperty("margin");
3061
3102
  targetEl.style.removeProperty("position");
@@ -3087,17 +3128,23 @@ class Pop extends NectaryElement {
3087
3128
  this.#targetActiveElement = null;
3088
3129
  }
3089
3130
  }
3090
- enableOverscroll();
3131
+ if (!this.allowScroll) {
3132
+ enableOverscroll();
3133
+ } else {
3134
+ this.#scrollableParents.forEach((el) => {
3135
+ el.removeEventListener("scroll", () => this.#updatePosition(false), { capture: true });
3136
+ });
3137
+ }
3091
3138
  this.#resizeThrottle.cancel();
3092
3139
  window.removeEventListener("resize", this.#onResize);
3093
- window.removeEventListener("scroll", this.#updatePosition);
3140
+ this.#scrollableParents = [];
3094
3141
  this.#$contentSlot.removeEventListener("slotchange", this.#onContentSlotChange);
3095
3142
  }
3096
3143
  #onResize = () => {
3097
3144
  this.#resizeThrottle.fn();
3098
3145
  };
3099
- #updatePosition = () => {
3100
- const targetRect = this.modal ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
3146
+ #updatePosition = (updateWidth) => {
3147
+ const targetRect = this.modal || this.allowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
3101
3148
  const orient = this.orientation;
3102
3149
  const modalWidth = this.#modalWidth;
3103
3150
  const modalHeight = this.#modalHeight;
@@ -3130,9 +3177,17 @@ class Pop extends NectaryElement {
3130
3177
  }
3131
3178
  const clampedXPos = Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset));
3132
3179
  const clampedYPos = Math.max(inset, Math.min(yPos, window.innerHeight - modalHeight - inset));
3180
+ if (this.hideOutsideViewport && this.#isPopPointInViewport(xPos, yPos)) {
3181
+ this.#$dialog.style.setProperty("visibility", "hidden");
3182
+ } else {
3183
+ this.#$dialog.style.removeProperty("visibility");
3184
+ }
3133
3185
  this.#$dialog.style.setProperty("left", `${clampedXPos}px`);
3134
3186
  this.#$dialog.style.setProperty("top", `${clampedYPos}px`);
3135
- if (!this.modal) {
3187
+ if (updateWidth === true) {
3188
+ this.#$dialog.style.setProperty("width", `${modalWidth}px`);
3189
+ }
3190
+ if (!this.modal && !this.allowScroll) {
3136
3191
  const targetLeftPos = targetRect.x - clampedXPos;
3137
3192
  const targetTopPos = targetRect.y - clampedYPos;
3138
3193
  this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
@@ -3147,49 +3202,12 @@ class Pop extends NectaryElement {
3147
3202
  const shouldSetWidthToTarget = orient === "top-stretch" || orient === "bottom-stretch";
3148
3203
  const modalHeight = modalRect.height;
3149
3204
  const modalWidth = shouldSetWidthToTarget ? targetRect.width : modalRect.width;
3150
- const inset = this.inset;
3151
- let xPos = 0;
3152
- let yPos = 0;
3153
3205
  this.#modalHeight = modalHeight;
3154
3206
  this.#modalWidth = modalWidth;
3155
- if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
3156
- xPos = targetRect.x;
3157
- }
3158
- if (orient === "bottom-left" || orient === "top-left") {
3159
- xPos = targetRect.x + targetRect.width - modalWidth;
3160
- }
3161
- if (orient === "bottom-center" || orient === "top-center") {
3162
- xPos = targetRect.x + targetRect.width / 2 - modalWidth / 2;
3163
- }
3164
- if (orient === "center-right") {
3165
- xPos = targetRect.x + targetRect.width;
3166
- }
3167
- if (orient === "center-left") {
3168
- xPos = targetRect.x - modalWidth;
3169
- }
3170
- if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
3171
- yPos = targetRect.y + targetRect.height;
3172
- }
3173
- if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
3174
- yPos = targetRect.y - modalHeight;
3175
- }
3176
- if (orient === "center-left" || orient === "center-right") {
3177
- yPos = targetRect.y + targetRect.height / 2 - modalHeight / 2;
3178
- }
3179
- xPos = Math.round(Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset)));
3180
- yPos = Math.round(Math.max(inset, Math.min(yPos, window.innerHeight - modalHeight - inset)));
3181
- this.#$dialog.style.setProperty("left", `${xPos}px`);
3182
- this.#$dialog.style.setProperty("top", `${yPos}px`);
3183
- this.#$dialog.style.setProperty("width", `${modalWidth}px`);
3184
- if (!this.modal) {
3185
- const targetLeftPos = targetRect.x - xPos;
3186
- const targetTopPos = targetRect.y - yPos;
3187
- this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
3188
- this.#$targetOpenWrapper.style.setProperty("top", `${targetTopPos}px`);
3189
- }
3207
+ this.#updatePosition(true);
3190
3208
  };
3191
3209
  #onBackdropMouseDown = (e) => {
3192
- if (isTargetEqual(e, this.#$dialog)) {
3210
+ if (this.shouldCloseOnBackdropClick && isTargetEqual(e, this.#$dialog)) {
3193
3211
  const rect = this.popoverRect;
3194
3212
  const isInside = e.x >= rect.x && e.x < rect.x + rect.width && e.y >= rect.y && e.y < rect.y + rect.height;
3195
3213
  if (!isInside) {
@@ -3245,6 +3263,14 @@ class Pop extends NectaryElement {
3245
3263
  this.#updateOrientation();
3246
3264
  }
3247
3265
  };
3266
+ #isPopPointInViewport(x, y) {
3267
+ const inset = this.inset;
3268
+ const modalWidth = this.#modalWidth;
3269
+ const modalHeight = this.#modalHeight;
3270
+ const clampedX = Math.max(inset, Math.min(x, window.innerWidth - modalWidth - inset));
3271
+ const clampedY = Math.max(inset, Math.min(y, window.innerHeight - modalHeight - inset));
3272
+ return Math.abs(clampedX - x) > 2 || Math.abs(clampedY - y) > 2;
3273
+ }
3248
3274
  }
3249
3275
  defineCustomElement("sinch-pop", Pop);
3250
3276
  class TooltipState {
@@ -3261,6 +3287,9 @@ class TooltipState {
3261
3287
  };
3262
3288
  }
3263
3289
  show() {
3290
+ if (this.#options.isOpened === false) {
3291
+ return;
3292
+ }
3264
3293
  switch (this.#state) {
3265
3294
  case "hide": {
3266
3295
  this.#switchToHideToShow();
@@ -3273,6 +3302,9 @@ class TooltipState {
3273
3302
  }
3274
3303
  }
3275
3304
  hide() {
3305
+ if (this.#options.isOpened === true) {
3306
+ return;
3307
+ }
3276
3308
  switch (this.#state) {
3277
3309
  case "hide-to-show": {
3278
3310
  this.#onHideAnimationEnd();
@@ -3325,13 +3357,15 @@ class TooltipState {
3325
3357
  this.#options.onShowStart();
3326
3358
  if (this.#options.showDelay === 0) {
3327
3359
  this.#onSwitchToShow();
3360
+ } else if (this.#options.isOpened !== void 0) {
3361
+ this.#timerId = window.setTimeout(this.#onSwitchToShow, 100);
3328
3362
  } else {
3329
3363
  this.#timerId = window.setTimeout(this.#onSwitchToShow, this.#options.showDelay);
3330
3364
  }
3331
3365
  }
3332
3366
  #switchToShowToHide(skipDelay, skipHideAnimation) {
3333
3367
  this.#switchToState("show-to-hide");
3334
- if (skipDelay === true || this.#options.hideDelay === 0) {
3368
+ if (skipDelay === true || this.#options.hideDelay === 0 || this.#options.isOpened !== void 0) {
3335
3369
  this.#onShowToHideEnd(skipHideAnimation);
3336
3370
  } else {
3337
3371
  this.#timerId = window.setTimeout(this.#onShowToHideEnd, this.#options.hideDelay);
@@ -3393,7 +3427,7 @@ const getPopOrientation$1 = (orientation) => {
3393
3427
  }
3394
3428
  return orientation;
3395
3429
  };
3396
- const templateHTML$Q = '<style>:host{display:contents}#content-wrapper{padding-bottom:8px;filter:drop-shadow(var(--sinch-comp-tooltip-shadow))}:host([orientation=left]) #content-wrapper{padding-bottom:0;padding-right:8px}:host([orientation=right]) #content-wrapper{padding-bottom:0;padding-left:8px}:host([orientation^=bottom]) #content-wrapper{padding-bottom:0;padding-top:8px}#content{position:relative;display:block;max-width:300px;padding:2px 6px;box-sizing:border-box;background-color:var(--sinch-local-color-background);border-radius:var(--sinch-comp-tooltip-shape-radius);pointer-events:none;opacity:0;--sinch-local-color-background:var(--sinch-comp-tooltip-color-background);--sinch-global-color-text:var(--sinch-comp-tooltip-color-text)}#text{word-break:break-word;pointer-events:none;--sinch-comp-text-font:var(--sinch-comp-tooltip-font-body)}#tip{position:absolute;left:50%;top:100%;transform:translateX(-50%) rotate(0);transform-origin:top center;fill:var(--sinch-local-color-background);pointer-events:none}#tip.hidden{display:none}:host([orientation=left]) #tip{transform:translateX(-50%) rotate(270deg);top:50%;left:100%}:host([orientation=right]) #tip{transform:translateX(-50%) rotate(90deg);top:50%;left:0}:host([orientation^=bottom]) #tip{transform:translateX(-50%) rotate(180deg);top:0}:host([text-align=right]) #text{--sinch-comp-text-align:right}:host([text-align=center]) #text{--sinch-comp-text-align:center}:host([text-align=left]) #text{--sinch-comp-text-align:left}</style><sinch-pop id="pop"><slot id="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><sinch-text id="text" type="s"></sinch-text><svg id="tip" width="8" height="4" aria-hidden="true"><path d="m4 4 4-4h-8l4 4Z"/></svg></div></div></sinch-pop>';
3430
+ const templateHTML$Q = '<style>:host{display:contents}#content-wrapper{padding-bottom:8px;filter:drop-shadow(var(--sinch-comp-tooltip-shadow))}:host([orientation=left]) #content-wrapper{padding-bottom:0;padding-right:8px}:host([orientation=right]) #content-wrapper{padding-bottom:0;padding-left:8px}:host([orientation^=bottom]) #content-wrapper{padding-bottom:0;padding-top:8px}#content{position:relative;display:block;max-width:300px;padding:2px 6px;box-sizing:border-box;background-color:var(--sinch-local-color-background);border-radius:var(--sinch-comp-tooltip-shape-radius);pointer-events:none;opacity:0;--sinch-local-color-background:var(--sinch-comp-tooltip-color-background);--sinch-global-color-text:var(--sinch-comp-tooltip-color-text)}#text{word-break:break-word;pointer-events:none;--sinch-comp-text-font:var(--sinch-comp-tooltip-font-body)}#tip{position:absolute;left:50%;top:100%;transform:translateX(-50%) rotate(0);transform-origin:top center;fill:var(--sinch-local-color-background);pointer-events:none}#tip.hidden{display:none}:host([orientation=left]) #tip{transform:translateX(-50%) rotate(270deg);top:50%;left:100%}:host([orientation=right]) #tip{transform:translateX(-50%) rotate(90deg);top:50%;left:0}:host([orientation^=bottom]) #tip{transform:translateX(-50%) rotate(180deg);top:0}:host([text-align=right]) #text{--sinch-comp-text-align:right}:host([text-align=center]) #text{--sinch-comp-text-align:center}:host([text-align=left]) #text{--sinch-comp-text-align:left}</style><sinch-pop id="pop" allow-scroll hide-outside-viewport><slot id="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><sinch-text id="text" type="s"></sinch-text><svg id="tip" width="8" height="4" aria-hidden="true"><path d="m4 4 4-4h-8l4 4Z"/></svg></div></div></sinch-pop>';
3397
3431
  const TIP_SIZE$1 = 8;
3398
3432
  const SHOW_DELAY_SLOW = 1e3;
3399
3433
  const SHOW_DELAY_FAST = 250;
@@ -3455,6 +3489,7 @@ class Tooltip extends NectaryElement {
3455
3489
  }
3456
3490
  static get observedAttributes() {
3457
3491
  return [
3492
+ "is-opened",
3458
3493
  "text",
3459
3494
  "orientation",
3460
3495
  "text-align",
@@ -3492,8 +3527,24 @@ class Tooltip extends NectaryElement {
3492
3527
  updateAttribute(this.#$pop, name, newVal);
3493
3528
  break;
3494
3529
  }
3530
+ case "is-opened": {
3531
+ this.#tooltipState.updateOptions({
3532
+ isOpened: this.isOpenedControlled
3533
+ });
3534
+ if (this.isOpenedControlled === true) {
3535
+ updateBooleanAttribute(this.#$pop, "disable-backdrop-close", true);
3536
+ this.#tooltipState.show();
3537
+ } else if (this.isOpenedControlled === false) {
3538
+ updateBooleanAttribute(this.#$pop, "disable-backdrop-close", false);
3539
+ this.#tooltipState.hide();
3540
+ }
3541
+ }
3495
3542
  }
3496
3543
  }
3544
+ get isOpenedControlled() {
3545
+ const isOpenedAttr = getAttribute(this, "is-opened");
3546
+ return isOpenedAttr === null ? void 0 : isOpenedAttr !== "false";
3547
+ }
3497
3548
  get text() {
3498
3549
  return getAttribute(this, "text", "");
3499
3550
  }
@@ -3545,8 +3596,10 @@ class Tooltip extends NectaryElement {
3545
3596
  };
3546
3597
  // Tooltip begins to wait for SHOW_DELAY on mouseenter
3547
3598
  #onStateShowStart = () => {
3548
- this.#subscribeScroll();
3549
- this.#subscribeMouseLeaveEvents();
3599
+ if (this.isOpenedControlled === void 0) {
3600
+ this.#subscribeScroll();
3601
+ this.#subscribeMouseLeaveEvents();
3602
+ }
3550
3603
  };
3551
3604
  // SHOW_DELAY ended, tooltip can be shown with animation
3552
3605
  #onStateShowEnd = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
package/pop/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare class Pop extends NectaryElement {
8
8
  connectedCallback(): void;
9
9
  disconnectedCallback(): void;
10
10
  static get observedAttributes(): string[];
11
+ get allowScroll(): boolean;
12
+ get hideOutsideViewport(): boolean;
11
13
  set modal(isModal: boolean);
12
14
  get modal(): boolean;
13
15
  set open(isOpen: boolean);
@@ -18,5 +20,6 @@ export declare class Pop extends NectaryElement {
18
20
  get inset(): number;
19
21
  get footprintRect(): TRect;
20
22
  get popoverRect(): TRect;
23
+ get shouldCloseOnBackdropClick(): boolean;
21
24
  attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null): void;
22
25
  }
package/pop/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Context, subscribeContext } from "../utils/context.js";
2
- import { getBooleanAttribute, updateBooleanAttribute, getLiteralAttribute, updateLiteralAttribute, updateIntegerAttribute, getIntegerAttribute, isAttrEqual, isAttrTrue } from "../utils/dom.js";
2
+ import { getBooleanAttribute, updateBooleanAttribute, getLiteralAttribute, updateLiteralAttribute, updateIntegerAttribute, getIntegerAttribute, isAttrEqual, getScrollableParents, 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";
@@ -27,6 +27,7 @@ class Pop extends NectaryElement {
27
27
  #targetStyleValue = null;
28
28
  #modalWidth = 0;
29
29
  #modalHeight = 0;
30
+ #scrollableParents = [];
30
31
  constructor() {
31
32
  super();
32
33
  const shadowRoot = this.attachShadow();
@@ -77,6 +78,12 @@ class Pop extends NectaryElement {
77
78
  "open"
78
79
  ];
79
80
  }
81
+ get allowScroll() {
82
+ return getBooleanAttribute(this, "allow-scroll");
83
+ }
84
+ get hideOutsideViewport() {
85
+ return getBooleanAttribute(this, "hide-outside-viewport");
86
+ }
80
87
  set modal(isModal) {
81
88
  updateBooleanAttribute(this, "modal", isModal);
82
89
  }
@@ -107,6 +114,9 @@ class Pop extends NectaryElement {
107
114
  get popoverRect() {
108
115
  return getRect(this.#$dialog);
109
116
  }
117
+ get shouldCloseOnBackdropClick() {
118
+ return !getBooleanAttribute(this, "disable-backdrop-close");
119
+ }
110
120
  attributeChangedCallback(name, oldVal, newVal) {
111
121
  if (isAttrEqual(oldVal, newVal)) {
112
122
  return;
@@ -167,57 +177,71 @@ class Pop extends NectaryElement {
167
177
  this.#$targetSlot.removeEventListener("blur", this.#stopEventPropagation, true);
168
178
  this.#$focus.removeAttribute("tabindex");
169
179
  this.#$focus.removeAttribute("style");
170
- this.#$dialog.showModal();
180
+ if (this.modal || !this.allowScroll) {
181
+ this.#$dialog.showModal();
182
+ } else {
183
+ this.#$dialog.show();
184
+ }
171
185
  this.#$targetWrapper.setAttribute("aria-expanded", "true");
172
186
  this.#updateOrientation();
173
187
  this.#resizeObserver.observe(this.#$dialog);
174
188
  if (this.modal) {
175
189
  getFirstFocusableElement(this.#$contentSlot)?.focus();
176
190
  } else {
177
- const $targetEl = this.#getFirstTargetElement(this.#$targetSlot);
178
- const targetElComputedStyle = getComputedStyle($targetEl);
179
- const marginLeft = parseInt(targetElComputedStyle.marginLeft);
180
- const marginRight = parseInt(targetElComputedStyle.marginRight);
181
- const marginTop = parseInt(targetElComputedStyle.marginTop);
182
- const marginBottom = parseInt(targetElComputedStyle.marginBottom);
183
- const targetRect = this.#getTargetRect();
184
- this.#$targetWrapper.style.setProperty("display", "block");
185
- this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
186
- this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
187
- this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
188
- this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
189
- this.#targetStyleValue = $targetEl.getAttribute("style");
190
- $targetEl.style.setProperty("margin", "0");
191
- $targetEl.style.setProperty("position", "static");
192
- if (targetElComputedStyle.transform !== "none") {
193
- const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
194
- $targetEl.style.setProperty("transform", matrix.translate(-matrix.e, -matrix.f).toString());
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");
195
212
  }
196
- getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
197
- this.#$targetOpenSlot.addEventListener("keydown", this.#onTargetKeydown);
213
+ const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
214
+ activeSlot.addEventListener("keydown", this.#onTargetKeydown);
198
215
  if (this.#targetActiveElement !== null) {
199
- this.#$targetOpenSlot.addEventListener("focus", this.#stopEventPropagation, true);
216
+ activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
200
217
  this.#targetActiveElement.focus();
201
- this.#$targetOpenSlot.removeEventListener("focus", this.#stopEventPropagation, true);
218
+ activeSlot.removeEventListener("focus", this.#stopEventPropagation, true);
202
219
  if (!isElementFocused(this.#targetActiveElement)) {
203
220
  requestAnimationFrame(() => {
204
221
  if (this.isDomConnected && this.#$dialog.open) {
205
- this.#$targetOpenSlot.addEventListener("focus", this.#stopEventPropagation, true);
222
+ activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
206
223
  this.#targetActiveElement.focus();
207
- this.#$targetOpenSlot.removeEventListener("focus", this.#stopEventPropagation, true);
224
+ activeSlot.removeEventListener("focus", this.#stopEventPropagation, true);
208
225
  }
209
226
  });
210
227
  }
211
228
  }
212
229
  }
213
- disableOverscroll();
214
- window.addEventListener("scroll", this.#updatePosition, { passive: false });
230
+ if (!this.allowScroll) {
231
+ disableOverscroll();
232
+ } 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
+ });
237
+ }
215
238
  window.addEventListener("resize", this.#onResize);
216
239
  requestAnimationFrame(() => {
217
240
  if (this.isDomConnected && this.#$dialog.open) {
218
241
  this.#$contentSlot.addEventListener("slotchange", this.#onContentSlotChange);
219
242
  }
220
243
  });
244
+ requestAnimationFrame(() => this.#updatePosition());
221
245
  this.#dispatchContentVisibility(true);
222
246
  }
223
247
  #onCollapse() {
@@ -226,17 +250,18 @@ class Pop extends NectaryElement {
226
250
  }
227
251
  this.#resizeObserver.disconnect();
228
252
  const isNonModal = !this.modal;
253
+ const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
229
254
  this.#dispatchContentVisibility(false);
230
- this.#$targetOpenSlot.removeEventListener("keydown", this.#onTargetKeydown);
255
+ activeSlot.removeEventListener("keydown", this.#onTargetKeydown);
231
256
  if (isNonModal) {
232
- this.#$targetOpenSlot.addEventListener("blur", this.#captureActiveElement, true);
257
+ activeSlot.addEventListener("blur", this.#captureActiveElement, true);
233
258
  }
234
259
  this.#$dialog.close();
235
260
  this.#$targetWrapper.setAttribute("aria-expanded", "false");
236
261
  if (isNonModal) {
237
- this.#$targetOpenSlot.removeEventListener("blur", this.#captureActiveElement, true);
262
+ activeSlot.removeEventListener("blur", this.#captureActiveElement, true);
238
263
  }
239
- if (isNonModal) {
264
+ if (isNonModal && !this.allowScroll) {
240
265
  const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
241
266
  targetEl.style.removeProperty("margin");
242
267
  targetEl.style.removeProperty("position");
@@ -268,17 +293,23 @@ class Pop extends NectaryElement {
268
293
  this.#targetActiveElement = null;
269
294
  }
270
295
  }
271
- enableOverscroll();
296
+ if (!this.allowScroll) {
297
+ enableOverscroll();
298
+ } else {
299
+ this.#scrollableParents.forEach((el) => {
300
+ el.removeEventListener("scroll", () => this.#updatePosition(false), { capture: true });
301
+ });
302
+ }
272
303
  this.#resizeThrottle.cancel();
273
304
  window.removeEventListener("resize", this.#onResize);
274
- window.removeEventListener("scroll", this.#updatePosition);
305
+ this.#scrollableParents = [];
275
306
  this.#$contentSlot.removeEventListener("slotchange", this.#onContentSlotChange);
276
307
  }
277
308
  #onResize = () => {
278
309
  this.#resizeThrottle.fn();
279
310
  };
280
- #updatePosition = () => {
281
- const targetRect = this.modal ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
311
+ #updatePosition = (updateWidth) => {
312
+ const targetRect = this.modal || this.allowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
282
313
  const orient = this.orientation;
283
314
  const modalWidth = this.#modalWidth;
284
315
  const modalHeight = this.#modalHeight;
@@ -311,9 +342,17 @@ class Pop extends NectaryElement {
311
342
  }
312
343
  const clampedXPos = Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset));
313
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");
347
+ } else {
348
+ this.#$dialog.style.removeProperty("visibility");
349
+ }
314
350
  this.#$dialog.style.setProperty("left", `${clampedXPos}px`);
315
351
  this.#$dialog.style.setProperty("top", `${clampedYPos}px`);
316
- if (!this.modal) {
352
+ if (updateWidth === true) {
353
+ this.#$dialog.style.setProperty("width", `${modalWidth}px`);
354
+ }
355
+ if (!this.modal && !this.allowScroll) {
317
356
  const targetLeftPos = targetRect.x - clampedXPos;
318
357
  const targetTopPos = targetRect.y - clampedYPos;
319
358
  this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
@@ -328,49 +367,12 @@ class Pop extends NectaryElement {
328
367
  const shouldSetWidthToTarget = orient === "top-stretch" || orient === "bottom-stretch";
329
368
  const modalHeight = modalRect.height;
330
369
  const modalWidth = shouldSetWidthToTarget ? targetRect.width : modalRect.width;
331
- const inset = this.inset;
332
- let xPos = 0;
333
- let yPos = 0;
334
370
  this.#modalHeight = modalHeight;
335
371
  this.#modalWidth = modalWidth;
336
- if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
337
- xPos = targetRect.x;
338
- }
339
- if (orient === "bottom-left" || orient === "top-left") {
340
- xPos = targetRect.x + targetRect.width - modalWidth;
341
- }
342
- if (orient === "bottom-center" || orient === "top-center") {
343
- xPos = targetRect.x + targetRect.width / 2 - modalWidth / 2;
344
- }
345
- if (orient === "center-right") {
346
- xPos = targetRect.x + targetRect.width;
347
- }
348
- if (orient === "center-left") {
349
- xPos = targetRect.x - modalWidth;
350
- }
351
- if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
352
- yPos = targetRect.y + targetRect.height;
353
- }
354
- if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
355
- yPos = targetRect.y - modalHeight;
356
- }
357
- if (orient === "center-left" || orient === "center-right") {
358
- yPos = targetRect.y + targetRect.height / 2 - modalHeight / 2;
359
- }
360
- xPos = Math.round(Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset)));
361
- yPos = Math.round(Math.max(inset, Math.min(yPos, window.innerHeight - modalHeight - inset)));
362
- this.#$dialog.style.setProperty("left", `${xPos}px`);
363
- this.#$dialog.style.setProperty("top", `${yPos}px`);
364
- this.#$dialog.style.setProperty("width", `${modalWidth}px`);
365
- if (!this.modal) {
366
- const targetLeftPos = targetRect.x - xPos;
367
- const targetTopPos = targetRect.y - yPos;
368
- this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
369
- this.#$targetOpenWrapper.style.setProperty("top", `${targetTopPos}px`);
370
- }
372
+ this.#updatePosition(true);
371
373
  };
372
374
  #onBackdropMouseDown = (e) => {
373
- if (isTargetEqual(e, this.#$dialog)) {
375
+ if (this.shouldCloseOnBackdropClick && isTargetEqual(e, this.#$dialog)) {
374
376
  const rect = this.popoverRect;
375
377
  const isInside = e.x >= rect.x && e.x < rect.x + rect.width && e.y >= rect.y && e.y < rect.y + rect.height;
376
378
  if (!isInside) {
@@ -426,6 +428,14 @@ class Pop extends NectaryElement {
426
428
  this.#updateOrientation();
427
429
  }
428
430
  };
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;
438
+ }
429
439
  }
430
440
  defineCustomElement("sinch-pop", Pop);
431
441
  export {
package/pop/types.d.ts CHANGED
@@ -1,10 +1,13 @@
1
1
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType, TRect, NectaryComponentReact, NectaryComponentVanilla } from '../types';
2
2
  export type TSinchPopOrientation = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'bottom-center' | 'bottom-stretch' | 'top-center' | 'top-stretch' | 'center-right' | 'center-left';
3
3
  export type TSinchPopProps = {
4
+ /** Allow scrolling of the page when pop is open */
5
+ 'allow-scroll'?: boolean;
4
6
  /** Open/close state */
5
7
  open: boolean;
6
8
  /** Orientation, where it *points to* from origin */
7
9
  orientation: TSinchPopOrientation;
10
+ 'hide-outside-viewport'?: boolean;
8
11
  /** Modal/non-modal mode */
9
12
  modal?: boolean;
10
13
  inset?: number;
@@ -11,6 +11,7 @@ export declare class Tooltip extends NectaryElement {
11
11
  disconnectedCallback(): void;
12
12
  static get observedAttributes(): string[];
13
13
  attributeChangedCallback(name: string, _: string | null, newVal: string | null): void;
14
+ get isOpenedControlled(): boolean | undefined;
14
15
  get text(): string;
15
16
  set text(value: string);
16
17
  get orientation(): TSinchTooltipOrientation;
package/tooltip/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import "../text/index.js";
2
2
  import "../pop/index.js";
3
- import { shouldReduceMotion, updateAttribute, getAttribute, getLiteralAttribute, updateLiteralAttribute, updateBooleanAttribute, setClass } from "../utils/dom.js";
3
+ import { shouldReduceMotion, updateAttribute, updateBooleanAttribute, getAttribute, getLiteralAttribute, updateLiteralAttribute, setClass } from "../utils/dom.js";
4
4
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
5
5
  import { rectOverlap } from "../utils/rect.js";
6
6
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
7
7
  import { TooltipState } from "./tooltip-state.js";
8
8
  import { getPopOrientation, orientationValues, textAlignValues, typeValues } from "./utils.js";
9
- const templateHTML = '<style>:host{display:contents}#content-wrapper{padding-bottom:8px;filter:drop-shadow(var(--sinch-comp-tooltip-shadow))}:host([orientation=left]) #content-wrapper{padding-bottom:0;padding-right:8px}:host([orientation=right]) #content-wrapper{padding-bottom:0;padding-left:8px}:host([orientation^=bottom]) #content-wrapper{padding-bottom:0;padding-top:8px}#content{position:relative;display:block;max-width:300px;padding:2px 6px;box-sizing:border-box;background-color:var(--sinch-local-color-background);border-radius:var(--sinch-comp-tooltip-shape-radius);pointer-events:none;opacity:0;--sinch-local-color-background:var(--sinch-comp-tooltip-color-background);--sinch-global-color-text:var(--sinch-comp-tooltip-color-text)}#text{word-break:break-word;pointer-events:none;--sinch-comp-text-font:var(--sinch-comp-tooltip-font-body)}#tip{position:absolute;left:50%;top:100%;transform:translateX(-50%) rotate(0);transform-origin:top center;fill:var(--sinch-local-color-background);pointer-events:none}#tip.hidden{display:none}:host([orientation=left]) #tip{transform:translateX(-50%) rotate(270deg);top:50%;left:100%}:host([orientation=right]) #tip{transform:translateX(-50%) rotate(90deg);top:50%;left:0}:host([orientation^=bottom]) #tip{transform:translateX(-50%) rotate(180deg);top:0}:host([text-align=right]) #text{--sinch-comp-text-align:right}:host([text-align=center]) #text{--sinch-comp-text-align:center}:host([text-align=left]) #text{--sinch-comp-text-align:left}</style><sinch-pop id="pop"><slot id="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><sinch-text id="text" type="s"></sinch-text><svg id="tip" width="8" height="4" aria-hidden="true"><path d="m4 4 4-4h-8l4 4Z"/></svg></div></div></sinch-pop>';
9
+ const templateHTML = '<style>:host{display:contents}#content-wrapper{padding-bottom:8px;filter:drop-shadow(var(--sinch-comp-tooltip-shadow))}:host([orientation=left]) #content-wrapper{padding-bottom:0;padding-right:8px}:host([orientation=right]) #content-wrapper{padding-bottom:0;padding-left:8px}:host([orientation^=bottom]) #content-wrapper{padding-bottom:0;padding-top:8px}#content{position:relative;display:block;max-width:300px;padding:2px 6px;box-sizing:border-box;background-color:var(--sinch-local-color-background);border-radius:var(--sinch-comp-tooltip-shape-radius);pointer-events:none;opacity:0;--sinch-local-color-background:var(--sinch-comp-tooltip-color-background);--sinch-global-color-text:var(--sinch-comp-tooltip-color-text)}#text{word-break:break-word;pointer-events:none;--sinch-comp-text-font:var(--sinch-comp-tooltip-font-body)}#tip{position:absolute;left:50%;top:100%;transform:translateX(-50%) rotate(0);transform-origin:top center;fill:var(--sinch-local-color-background);pointer-events:none}#tip.hidden{display:none}:host([orientation=left]) #tip{transform:translateX(-50%) rotate(270deg);top:50%;left:100%}:host([orientation=right]) #tip{transform:translateX(-50%) rotate(90deg);top:50%;left:0}:host([orientation^=bottom]) #tip{transform:translateX(-50%) rotate(180deg);top:0}:host([text-align=right]) #text{--sinch-comp-text-align:right}:host([text-align=center]) #text{--sinch-comp-text-align:center}:host([text-align=left]) #text{--sinch-comp-text-align:left}</style><sinch-pop id="pop" allow-scroll hide-outside-viewport><slot id="target" slot="target"></slot><div id="content-wrapper" slot="content"><div id="content"><sinch-text id="text" type="s"></sinch-text><svg id="tip" width="8" height="4" aria-hidden="true"><path d="m4 4 4-4h-8l4 4Z"/></svg></div></div></sinch-pop>';
10
10
  const TIP_SIZE = 8;
11
11
  const SHOW_DELAY_SLOW = 1e3;
12
12
  const SHOW_DELAY_FAST = 250;
@@ -68,6 +68,7 @@ class Tooltip extends NectaryElement {
68
68
  }
69
69
  static get observedAttributes() {
70
70
  return [
71
+ "is-opened",
71
72
  "text",
72
73
  "orientation",
73
74
  "text-align",
@@ -105,8 +106,24 @@ class Tooltip extends NectaryElement {
105
106
  updateAttribute(this.#$pop, name, newVal);
106
107
  break;
107
108
  }
109
+ case "is-opened": {
110
+ this.#tooltipState.updateOptions({
111
+ isOpened: this.isOpenedControlled
112
+ });
113
+ if (this.isOpenedControlled === true) {
114
+ updateBooleanAttribute(this.#$pop, "disable-backdrop-close", true);
115
+ this.#tooltipState.show();
116
+ } else if (this.isOpenedControlled === false) {
117
+ updateBooleanAttribute(this.#$pop, "disable-backdrop-close", false);
118
+ this.#tooltipState.hide();
119
+ }
120
+ }
108
121
  }
109
122
  }
123
+ get isOpenedControlled() {
124
+ const isOpenedAttr = getAttribute(this, "is-opened");
125
+ return isOpenedAttr === null ? void 0 : isOpenedAttr !== "false";
126
+ }
110
127
  get text() {
111
128
  return getAttribute(this, "text", "");
112
129
  }
@@ -158,8 +175,10 @@ class Tooltip extends NectaryElement {
158
175
  };
159
176
  // Tooltip begins to wait for SHOW_DELAY on mouseenter
160
177
  #onStateShowStart = () => {
161
- this.#subscribeScroll();
162
- this.#subscribeMouseLeaveEvents();
178
+ if (this.isOpenedControlled === void 0) {
179
+ this.#subscribeScroll();
180
+ this.#subscribeMouseLeaveEvents();
181
+ }
163
182
  };
164
183
  // SHOW_DELAY ended, tooltip can be shown with animation
165
184
  #onStateShowEnd = () => {
@@ -1,4 +1,5 @@
1
1
  type TTooltipStateOptions = {
2
+ isOpened?: boolean | undefined;
2
3
  showDelay: number;
3
4
  hideDelay: number;
4
5
  hideAnimationDuration: number;
@@ -12,6 +12,9 @@ class TooltipState {
12
12
  };
13
13
  }
14
14
  show() {
15
+ if (this.#options.isOpened === false) {
16
+ return;
17
+ }
15
18
  switch (this.#state) {
16
19
  case "hide": {
17
20
  this.#switchToHideToShow();
@@ -24,6 +27,9 @@ class TooltipState {
24
27
  }
25
28
  }
26
29
  hide() {
30
+ if (this.#options.isOpened === true) {
31
+ return;
32
+ }
27
33
  switch (this.#state) {
28
34
  case "hide-to-show": {
29
35
  this.#onHideAnimationEnd();
@@ -76,13 +82,15 @@ class TooltipState {
76
82
  this.#options.onShowStart();
77
83
  if (this.#options.showDelay === 0) {
78
84
  this.#onSwitchToShow();
85
+ } else if (this.#options.isOpened !== void 0) {
86
+ this.#timerId = window.setTimeout(this.#onSwitchToShow, 100);
79
87
  } else {
80
88
  this.#timerId = window.setTimeout(this.#onSwitchToShow, this.#options.showDelay);
81
89
  }
82
90
  }
83
91
  #switchToShowToHide(skipDelay, skipHideAnimation) {
84
92
  this.#switchToState("show-to-hide");
85
- if (skipDelay === true || this.#options.hideDelay === 0) {
93
+ if (skipDelay === true || this.#options.hideDelay === 0 || this.#options.isOpened !== void 0) {
86
94
  this.#onShowToHideEnd(skipHideAnimation);
87
95
  } else {
88
96
  this.#timerId = window.setTimeout(this.#onShowToHideEnd, this.#options.hideDelay);
@@ -3,6 +3,7 @@ export type TSinchTooltipOrientation = 'top' | 'bottom' | 'left' | 'right' | 'to
3
3
  export type TSinchTooltipTextAlign = 'center' | 'right' | 'left';
4
4
  export type TSinchTooltipType = 'slow' | 'fast';
5
5
  export type TSinchTooltipProps = {
6
+ 'is-opened'?: string;
6
7
  /** Text */
7
8
  text: string;
8
9
  /** Orientation, where it *points to* from origin */
package/utils/dom.d.ts CHANGED
@@ -32,4 +32,5 @@ export declare const getCssVars: (element: Element, variableNames: string[]) =>
32
32
  export declare const cloneNode: (el: Element, deep: boolean) => Element;
33
33
  export declare const shouldReduceMotion: () => boolean;
34
34
  export declare const isAttrEqual: (oldVal: string | null, newVal: string | null) => boolean;
35
+ export declare const getScrollableParents: (node: HTMLElement | null) => (HTMLElement | Document)[];
35
36
  export {};
package/utils/dom.js CHANGED
@@ -137,6 +137,22 @@ const shouldReduceMotion = () => window.matchMedia("(prefers-reduced-motion: red
137
137
  const isAttrEqual = (oldVal, newVal) => {
138
138
  return oldVal === newVal || newVal === null && oldVal === "false" || newVal === "" && oldVal === "true";
139
139
  };
140
+ const getScrollableParents = (node) => {
141
+ const scrollableParents = [];
142
+ if (node == null) {
143
+ return scrollableParents;
144
+ }
145
+ let parent = node.parentElement;
146
+ while (parent != null) {
147
+ const computedStyle = getComputedStyle(parent);
148
+ if ((parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) && (computedStyle.overflow === "auto" || computedStyle.overflow === "scroll" || computedStyle.overflowY === "auto" || computedStyle.overflowY === "scroll" || computedStyle.overflowX === "auto" || computedStyle.overflowX === "scroll")) {
149
+ scrollableParents.push(parent);
150
+ }
151
+ parent = parent.parentElement;
152
+ }
153
+ scrollableParents.push(document);
154
+ return scrollableParents;
155
+ };
140
156
  export {
141
157
  attrValueToInteger,
142
158
  attrValueToPixels,
@@ -148,6 +164,7 @@ export {
148
164
  getCssVars,
149
165
  getIntegerAttribute,
150
166
  getLiteralAttribute,
167
+ getScrollableParents,
151
168
  hasClass,
152
169
  isAttrEqual,
153
170
  isAttrTrue,
package/utils/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, subscribeContext } from "./context.js";
2
2
  import { CSV_DELIMITER, getFirstCsvValue, packCsv, unpackCsv, updateCsv } from "./csv.js";
3
- import { attrValueToInteger, attrValueToPixels, clampNumber, cloneNode, getAttribute, getBooleanAttribute, getCssVar, getCssVars, getIntegerAttribute, getLiteralAttribute, hasClass, isAttrEqual, isAttrTrue, isLiteralValue, setClass, shouldReduceMotion, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute, updateIntegerAttribute, updateLiteralAttribute } from "./dom.js";
3
+ import { attrValueToInteger, attrValueToPixels, clampNumber, cloneNode, getAttribute, getBooleanAttribute, getCssVar, getCssVars, getIntegerAttribute, getLiteralAttribute, getScrollableParents, hasClass, isAttrEqual, isAttrTrue, isLiteralValue, setClass, shouldReduceMotion, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute, updateIntegerAttribute, updateLiteralAttribute } from "./dom.js";
4
4
  import { NectaryElement, defineCustomElement, pascalToKebabCase, registerComponent, resetNectaryRegistry, setNectaryRegistry } from "./element.js";
5
5
  import { getRect, getTargetRect, rectOverlap } from "./rect.js";
6
6
  import { getFirstFocusableElement, getFirstSlotElement, isElementFocused } from "./slot.js";
@@ -32,6 +32,7 @@ export {
32
32
  getLiteralAttribute,
33
33
  getReactEventHandler,
34
34
  getRect,
35
+ getScrollableParents,
35
36
  getTargetAttribute,
36
37
  getTargetByAttribute,
37
38
  getTargetIndexInParent,