@nectary/components 5.2.1 → 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/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";
@@ -15,6 +15,7 @@ class Pop extends NectaryElement {
15
15
  #$focus;
16
16
  #$dialog;
17
17
  #resizeThrottle;
18
+ #resizeObserver;
18
19
  #$targetSlot;
19
20
  #$targetOpenSlot;
20
21
  #$contentSlot;
@@ -26,6 +27,7 @@ class Pop extends NectaryElement {
26
27
  #targetStyleValue = null;
27
28
  #modalWidth = 0;
28
29
  #modalHeight = 0;
30
+ #scrollableParents = [];
29
31
  constructor() {
30
32
  super();
31
33
  const shadowRoot = this.attachShadow();
@@ -38,6 +40,9 @@ class Pop extends NectaryElement {
38
40
  this.#$contentSlot = shadowRoot.querySelector('slot[name="content"]');
39
41
  this.#$targetOpenWrapper = shadowRoot.querySelector("#target-open");
40
42
  this.#resizeThrottle = throttleAnimationFrame(this.#updateOrientation);
43
+ this.#resizeObserver = new ResizeObserver(() => {
44
+ this.#resizeThrottle.fn();
45
+ });
41
46
  this.#keydownContext = new Context(this.#$contentSlot, "keydown");
42
47
  this.#visibilityContext = new Context(this.#$contentSlot, "visibility");
43
48
  this.#controller = new AbortController();
@@ -64,6 +69,7 @@ class Pop extends NectaryElement {
64
69
  this.#controller.abort();
65
70
  this.#controller = null;
66
71
  this.#resizeThrottle.cancel();
72
+ this.#resizeObserver.disconnect();
67
73
  this.#onCollapse();
68
74
  }
69
75
  static get observedAttributes() {
@@ -72,6 +78,12 @@ class Pop extends NectaryElement {
72
78
  "open"
73
79
  ];
74
80
  }
81
+ get allowScroll() {
82
+ return getBooleanAttribute(this, "allow-scroll");
83
+ }
84
+ get hideOutsideViewport() {
85
+ return getBooleanAttribute(this, "hide-outside-viewport");
86
+ }
75
87
  set modal(isModal) {
76
88
  updateBooleanAttribute(this, "modal", isModal);
77
89
  }
@@ -102,6 +114,9 @@ class Pop extends NectaryElement {
102
114
  get popoverRect() {
103
115
  return getRect(this.#$dialog);
104
116
  }
117
+ get shouldCloseOnBackdropClick() {
118
+ return !getBooleanAttribute(this, "disable-backdrop-close");
119
+ }
105
120
  attributeChangedCallback(name, oldVal, newVal) {
106
121
  if (isAttrEqual(oldVal, newVal)) {
107
122
  return;
@@ -162,74 +177,91 @@ class Pop extends NectaryElement {
162
177
  this.#$targetSlot.removeEventListener("blur", this.#stopEventPropagation, true);
163
178
  this.#$focus.removeAttribute("tabindex");
164
179
  this.#$focus.removeAttribute("style");
165
- this.#$dialog.showModal();
180
+ if (this.modal || !this.allowScroll) {
181
+ this.#$dialog.showModal();
182
+ } else {
183
+ this.#$dialog.show();
184
+ }
166
185
  this.#$targetWrapper.setAttribute("aria-expanded", "true");
167
186
  this.#updateOrientation();
187
+ this.#resizeObserver.observe(this.#$dialog);
168
188
  if (this.modal) {
169
189
  getFirstFocusableElement(this.#$contentSlot)?.focus();
170
190
  } else {
171
- const $targetEl = this.#getFirstTargetElement(this.#$targetSlot);
172
- const targetElComputedStyle = getComputedStyle($targetEl);
173
- const marginLeft = parseInt(targetElComputedStyle.marginLeft);
174
- const marginRight = parseInt(targetElComputedStyle.marginRight);
175
- const marginTop = parseInt(targetElComputedStyle.marginTop);
176
- const marginBottom = parseInt(targetElComputedStyle.marginBottom);
177
- const targetRect = this.#getTargetRect();
178
- this.#$targetWrapper.style.setProperty("display", "block");
179
- this.#$targetWrapper.style.setProperty("width", `${targetRect.width + marginLeft + marginRight}px`);
180
- this.#$targetWrapper.style.setProperty("height", `${targetRect.height + marginTop + marginBottom}px`);
181
- this.#$targetOpenWrapper.style.setProperty("width", `${targetRect.width}px`);
182
- this.#$targetOpenWrapper.style.setProperty("height", `${targetRect.height}px`);
183
- this.#targetStyleValue = $targetEl.getAttribute("style");
184
- $targetEl.style.setProperty("margin", "0");
185
- $targetEl.style.setProperty("position", "static");
186
- if (targetElComputedStyle.transform !== "none") {
187
- const matrix = new DOMMatrixReadOnly(targetElComputedStyle.transform);
188
- $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");
189
212
  }
190
- getFirstSlotElement(this.#$targetSlot)?.setAttribute("slot", "target-open");
191
- this.#$targetOpenSlot.addEventListener("keydown", this.#onTargetKeydown);
213
+ const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
214
+ activeSlot.addEventListener("keydown", this.#onTargetKeydown);
192
215
  if (this.#targetActiveElement !== null) {
193
- this.#$targetOpenSlot.addEventListener("focus", this.#stopEventPropagation, true);
216
+ activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
194
217
  this.#targetActiveElement.focus();
195
- this.#$targetOpenSlot.removeEventListener("focus", this.#stopEventPropagation, true);
218
+ activeSlot.removeEventListener("focus", this.#stopEventPropagation, true);
196
219
  if (!isElementFocused(this.#targetActiveElement)) {
197
220
  requestAnimationFrame(() => {
198
221
  if (this.isDomConnected && this.#$dialog.open) {
199
- this.#$targetOpenSlot.addEventListener("focus", this.#stopEventPropagation, true);
222
+ activeSlot.addEventListener("focus", this.#stopEventPropagation, true);
200
223
  this.#targetActiveElement.focus();
201
- this.#$targetOpenSlot.removeEventListener("focus", this.#stopEventPropagation, true);
224
+ activeSlot.removeEventListener("focus", this.#stopEventPropagation, true);
202
225
  }
203
226
  });
204
227
  }
205
228
  }
206
229
  }
207
- disableOverscroll();
208
- 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
+ }
209
238
  window.addEventListener("resize", this.#onResize);
210
239
  requestAnimationFrame(() => {
211
240
  if (this.isDomConnected && this.#$dialog.open) {
212
241
  this.#$contentSlot.addEventListener("slotchange", this.#onContentSlotChange);
213
242
  }
214
243
  });
244
+ requestAnimationFrame(() => this.#updatePosition());
215
245
  this.#dispatchContentVisibility(true);
216
246
  }
217
247
  #onCollapse() {
218
248
  if (!this.#$dialog.open) {
219
249
  return;
220
250
  }
251
+ this.#resizeObserver.disconnect();
221
252
  const isNonModal = !this.modal;
253
+ const activeSlot = this.allowScroll ? this.#$targetSlot : this.#$targetOpenSlot;
222
254
  this.#dispatchContentVisibility(false);
223
- this.#$targetOpenSlot.removeEventListener("keydown", this.#onTargetKeydown);
255
+ activeSlot.removeEventListener("keydown", this.#onTargetKeydown);
224
256
  if (isNonModal) {
225
- this.#$targetOpenSlot.addEventListener("blur", this.#captureActiveElement, true);
257
+ activeSlot.addEventListener("blur", this.#captureActiveElement, true);
226
258
  }
227
259
  this.#$dialog.close();
228
260
  this.#$targetWrapper.setAttribute("aria-expanded", "false");
229
261
  if (isNonModal) {
230
- this.#$targetOpenSlot.removeEventListener("blur", this.#captureActiveElement, true);
262
+ activeSlot.removeEventListener("blur", this.#captureActiveElement, true);
231
263
  }
232
- if (isNonModal) {
264
+ if (isNonModal && !this.allowScroll) {
233
265
  const targetEl = this.#getFirstTargetElement(this.#$targetOpenSlot);
234
266
  targetEl.style.removeProperty("margin");
235
267
  targetEl.style.removeProperty("position");
@@ -261,17 +293,23 @@ class Pop extends NectaryElement {
261
293
  this.#targetActiveElement = null;
262
294
  }
263
295
  }
264
- 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
+ }
265
303
  this.#resizeThrottle.cancel();
266
304
  window.removeEventListener("resize", this.#onResize);
267
- window.removeEventListener("scroll", this.#updatePosition);
305
+ this.#scrollableParents = [];
268
306
  this.#$contentSlot.removeEventListener("slotchange", this.#onContentSlotChange);
269
307
  }
270
308
  #onResize = () => {
271
309
  this.#resizeThrottle.fn();
272
310
  };
273
- #updatePosition = () => {
274
- const targetRect = this.modal ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
311
+ #updatePosition = (updateWidth) => {
312
+ const targetRect = this.modal || this.allowScroll ? this.#getTargetRect() : this.#$targetWrapper.getBoundingClientRect();
275
313
  const orient = this.orientation;
276
314
  const modalWidth = this.#modalWidth;
277
315
  const modalHeight = this.#modalHeight;
@@ -304,9 +342,17 @@ class Pop extends NectaryElement {
304
342
  }
305
343
  const clampedXPos = Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset));
306
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
+ }
307
350
  this.#$dialog.style.setProperty("left", `${clampedXPos}px`);
308
351
  this.#$dialog.style.setProperty("top", `${clampedYPos}px`);
309
- if (!this.modal) {
352
+ if (updateWidth === true) {
353
+ this.#$dialog.style.setProperty("width", `${modalWidth}px`);
354
+ }
355
+ if (!this.modal && !this.allowScroll) {
310
356
  const targetLeftPos = targetRect.x - clampedXPos;
311
357
  const targetTopPos = targetRect.y - clampedYPos;
312
358
  this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
@@ -321,49 +367,12 @@ class Pop extends NectaryElement {
321
367
  const shouldSetWidthToTarget = orient === "top-stretch" || orient === "bottom-stretch";
322
368
  const modalHeight = modalRect.height;
323
369
  const modalWidth = shouldSetWidthToTarget ? targetRect.width : modalRect.width;
324
- const inset = this.inset;
325
- let xPos = 0;
326
- let yPos = 0;
327
370
  this.#modalHeight = modalHeight;
328
371
  this.#modalWidth = modalWidth;
329
- if (orient === "bottom-right" || orient === "top-right" || orient === "top-stretch" || orient === "bottom-stretch") {
330
- xPos = targetRect.x;
331
- }
332
- if (orient === "bottom-left" || orient === "top-left") {
333
- xPos = targetRect.x + targetRect.width - modalWidth;
334
- }
335
- if (orient === "bottom-center" || orient === "top-center") {
336
- xPos = targetRect.x + targetRect.width / 2 - modalWidth / 2;
337
- }
338
- if (orient === "center-right") {
339
- xPos = targetRect.x + targetRect.width;
340
- }
341
- if (orient === "center-left") {
342
- xPos = targetRect.x - modalWidth;
343
- }
344
- if (orient === "bottom-left" || orient === "bottom-right" || orient === "bottom-stretch" || orient === "bottom-center") {
345
- yPos = targetRect.y + targetRect.height;
346
- }
347
- if (orient === "top-left" || orient === "top-right" || orient === "top-stretch" || orient === "top-center") {
348
- yPos = targetRect.y - modalHeight;
349
- }
350
- if (orient === "center-left" || orient === "center-right") {
351
- yPos = targetRect.y + targetRect.height / 2 - modalHeight / 2;
352
- }
353
- xPos = Math.round(Math.max(inset, Math.min(xPos, window.innerWidth - modalWidth - inset)));
354
- yPos = Math.round(Math.max(inset, Math.min(yPos, window.innerHeight - modalHeight - inset)));
355
- this.#$dialog.style.setProperty("left", `${xPos}px`);
356
- this.#$dialog.style.setProperty("top", `${yPos}px`);
357
- this.#$dialog.style.setProperty("width", `${modalWidth}px`);
358
- if (!this.modal) {
359
- const targetLeftPos = targetRect.x - xPos;
360
- const targetTopPos = targetRect.y - yPos;
361
- this.#$targetOpenWrapper.style.setProperty("left", `${targetLeftPos}px`);
362
- this.#$targetOpenWrapper.style.setProperty("top", `${targetTopPos}px`);
363
- }
372
+ this.#updatePosition(true);
364
373
  };
365
374
  #onBackdropMouseDown = (e) => {
366
- if (isTargetEqual(e, this.#$dialog)) {
375
+ if (this.shouldCloseOnBackdropClick && isTargetEqual(e, this.#$dialog)) {
367
376
  const rect = this.popoverRect;
368
377
  const isInside = e.x >= rect.x && e.x < rect.x + rect.width && e.y >= rect.y && e.y < rect.y + rect.height;
369
378
  if (!isInside) {
@@ -419,6 +428,14 @@ class Pop extends NectaryElement {
419
428
  this.#updateOrientation();
420
429
  }
421
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
+ }
422
439
  }
423
440
  defineCustomElement("sinch-pop", Pop);
424
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,