@nectary/components 1.2.2 → 1.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
package/pop/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCustomElement, getBooleanAttribute, getLiteralAttribute, getRect, isAttrTrue, updateLiteralAttribute, getReactEventHandler, updateBooleanAttribute, NectaryElement, throttleAnimationFrame, isElementFocused, updateIntegerAttribute, getIntegerAttribute, getFirstFocusableElement, getFirstSlotElement, Context, subscribeContext, isTargetEqual } from '../utils';
2
- 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" tabindex="-1"></div><dialog id="dialog"><div id="content"><slot name="content"></slot></div><div id="target-open"><slot name="target-open"></slot></div></dialog>';
2
+ 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>';
3
3
  import { assertOrientation, disableScroll, enableScroll, orientationValues } from './utils';
4
4
  const template = document.createElement('template');
5
5
  template.innerHTML = templateHTML;
@@ -153,13 +153,13 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
153
153
  if (!this.isConnected || this.#isOpen()) {
154
154
  return;
155
155
  }
156
- this.#$targetSlot.addEventListener('blur', this.#stopEventPropagation, true);
157
- this.#$focus.style.setProperty('display', 'block');
158
- this.#$focus.addEventListener('focus', this.#captureRelatedActiveElement);
156
+ this.#$targetSlot.addEventListener('blur', this.#captureActiveElement, true);
157
+ this.#$focus.setAttribute('tabindex', '-1');
158
+ this.#$focus.style.display = 'block';
159
159
  this.#$focus.focus();
160
- this.#$focus.removeEventListener('focus', this.#captureRelatedActiveElement);
161
- this.#$focus.style.removeProperty('display');
162
- this.#$targetSlot.removeEventListener('blur', this.#stopEventPropagation, true);
160
+ this.#$targetSlot.removeEventListener('blur', this.#captureActiveElement, true);
161
+ this.#$focus.removeAttribute('tabindex');
162
+ this.#$focus.removeAttribute('style');
163
163
  this.#$dialog.showModal();
164
164
  this.#$target.setAttribute('aria-expanded', 'true');
165
165
  this.#updateOrientation();
@@ -336,10 +336,6 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
336
336
  #dispatchCloseEvent() {
337
337
  this.dispatchEvent(new CustomEvent('-close'));
338
338
  }
339
- #captureRelatedActiveElement = e => {
340
- e.stopPropagation();
341
- this.#targetActiveElement = e.relatedTarget;
342
- };
343
339
  #captureActiveElement = e => {
344
340
  e.stopPropagation();
345
341
  this.#targetActiveElement = e.target;
@@ -0,0 +1,11 @@
1
+ import type { TSinchSkeletonElement, TSinchSkeletonReact } from './types';
2
+ declare global {
3
+ namespace JSX {
4
+ interface IntrinsicElements {
5
+ 'sinch-skeleton': TSinchSkeletonReact;
6
+ }
7
+ }
8
+ interface HTMLElementTagNameMap {
9
+ 'sinch-skeleton': TSinchSkeletonElement;
10
+ }
11
+ }
@@ -0,0 +1,75 @@
1
+ import { defineCustomElement, getUid, isAttrTrue, NectaryElement, shouldReduceMotion } from '../utils';
2
+ const templateHTML = '<style>:host{display:block}#wrapper{position:relative;display:flex;flex-direction:column;gap:16px;height:100%;box-sizing:border-box;overflow:hidden}:host([card]:not([card=false]))>#wrapper{padding:16px;background-color:var(--sinch-color-snow-100);border-radius:var(--sinch-shape-radius-l);border:1px solid var(--sinch-color-snow-500)}#shimmer{position:absolute;inset:0;background-image:linear-gradient(90deg,transparent 0,var(--sinch-color-snow-400) 100px,transparent 200px);clip-path:url("#clip")}#svg{display:block;width:0;height:0}</style><svg id="svg"><defs><clipPath id="clip"></clipPath></defs></svg><div id="wrapper"><slot></slot><div id="shimmer"></div></div>';
3
+ const template = document.createElement('template');
4
+ template.innerHTML = templateHTML;
5
+ const ANIMATION_DURATION = 2000;
6
+ const BORDER_WIDTH = 1;
7
+ defineCustomElement('sinch-skeleton', class extends NectaryElement {
8
+ #animation = null;
9
+ #shimmer;
10
+ #controller = null;
11
+ #clip;
12
+ #borderWidth = 0;
13
+ constructor() {
14
+ super();
15
+ const shadowRoot = this.attachShadow();
16
+ shadowRoot.appendChild(template.content.cloneNode(true));
17
+ this.#shimmer = shadowRoot.querySelector('#shimmer');
18
+ this.#clip = shadowRoot.querySelector('#clip');
19
+ }
20
+ connectedCallback() {
21
+ const id = getUid();
22
+ this.#shimmer.style.setProperty('clip-path', `url(#${id})`);
23
+ this.#clip.setAttribute('id', id);
24
+ this.#controller = new AbortController();
25
+ if (!shouldReduceMotion()) {
26
+ this.addEventListener('skeleton-item-data', this.#onSkeletonItemData, {
27
+ signal: this.#controller.signal
28
+ });
29
+ this.#updateAnimation();
30
+ }
31
+ }
32
+ disconnectedCallback() {
33
+ this.#animation.cancel();
34
+ this.#animation = null;
35
+ this.#controller.abort();
36
+ this.#controller = null;
37
+ }
38
+ static get observedAttributes() {
39
+ return ['card'];
40
+ }
41
+ attributeChangedCallback(name, oldVal, newVal) {
42
+ if (oldVal === newVal) {
43
+ return;
44
+ }
45
+ switch (name) {
46
+ case 'card':
47
+ {
48
+ this.#borderWidth = isAttrTrue(newVal) ? BORDER_WIDTH : 0;
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ #updateAnimation() {
54
+ const bb = this.getBoundingClientRect();
55
+ const bgWidth = bb.width * 4;
56
+ this.#shimmer.style.setProperty('background-size', `${bgWidth}px`);
57
+ this.#animation = this.#shimmer.animate({
58
+ backgroundPosition: [`0px`, `${bgWidth}px`]
59
+ }, {
60
+ duration: ANIMATION_DURATION,
61
+ iterations: Infinity
62
+ });
63
+ }
64
+ #onSkeletonItemData = e => {
65
+ const bb = this.getBoundingClientRect();
66
+ const data = e.detail;
67
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
68
+ rect.setAttribute('x', (data.x - bb.x - this.#borderWidth).toString());
69
+ rect.setAttribute('y', (data.y - bb.y - this.#borderWidth).toString());
70
+ rect.setAttribute('width', data.width.toString());
71
+ rect.setAttribute('height', data.height.toString());
72
+ rect.setAttribute('rx', data.radius.toString());
73
+ this.#clip.appendChild(rect);
74
+ };
75
+ });
@@ -0,0 +1,9 @@
1
+ import type { TSinchElementReact } from '../types';
2
+ export type TSinchSkeletonElement = HTMLElement & {
3
+ /** Card like container */
4
+ setAttribute(name: 'card', value: ''): void;
5
+ };
6
+ export type TSinchSkeletonReact = TSinchElementReact<TSinchSkeletonElement> & {
7
+ /** Card like container */
8
+ card?: boolean;
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { TSinchSkeletonItemElement, TSinchSkeletonItemReact } from './types';
2
+ declare global {
3
+ namespace JSX {
4
+ interface IntrinsicElements {
5
+ 'sinch-skeleton-item': TSinchSkeletonItemReact;
6
+ }
7
+ }
8
+ interface HTMLElementTagNameMap {
9
+ 'sinch-skeleton-item': TSinchSkeletonItemElement;
10
+ }
11
+ }
@@ -0,0 +1,38 @@
1
+ import { defineCustomElement, getCssVar, NectaryElement, attrValueToInteger, shouldReduceMotion } from '../utils';
2
+ const templateHTML = '<style>:host{display:block;height:var(--sinch-size-m);--sinch-shape-radius:var(--sinch-shape-radius-m)}:host([size=xs]){height:var(--sinch-size-xs);--sinch-shape-radius:var(--sinch-shape-radius-xs)}:host([size="s"]){height:var(--sinch-size-s);--sinch-shape-radius:var(--sinch-shape-radius-s)}:host([size="m"]){height:var(--sinch-size-m);--sinch-shape-radius:var(--sinch-shape-radius-m)}:host([size="l"]){height:var(--sinch-size-l);--sinch-shape-radius:var(--sinch-shape-radius-l)}#content{height:100%;background-color:var(--sinch-color-snow-500);border-radius:var(--sinch-shape-radius)}</style><div id="content"></div>';
3
+ const template = document.createElement('template');
4
+ template.innerHTML = templateHTML;
5
+ defineCustomElement('sinch-skeleton-item', class extends NectaryElement {
6
+ constructor() {
7
+ super();
8
+ const shadowRoot = this.attachShadow();
9
+ shadowRoot.appendChild(template.content.cloneNode(true));
10
+ }
11
+ connectedCallback() {
12
+ if (!shouldReduceMotion()) {
13
+ requestAnimationFrame(() => {
14
+ const {
15
+ x,
16
+ y,
17
+ width,
18
+ height
19
+ } = this.getBoundingClientRect();
20
+ const radiusStr = getCssVar(this, '--sinch-shape-radius') ?? '0';
21
+ const radius = attrValueToInteger(radiusStr, {
22
+ min: 0,
23
+ defaultValue: 0
24
+ });
25
+ this.dispatchEvent(new CustomEvent('skeleton-item-data', {
26
+ bubbles: true,
27
+ detail: {
28
+ x,
29
+ y,
30
+ width,
31
+ height,
32
+ radius
33
+ }
34
+ }));
35
+ });
36
+ }
37
+ }
38
+ });
@@ -0,0 +1,13 @@
1
+ import type { TRect, TSinchElementReact } from '../types';
2
+ import type { TSinchSizeEx } from '../utils/size';
3
+ export type TSinchSkeletonItemBoundingBox = TRect & {
4
+ radius: number;
5
+ };
6
+ export type TSinchSkeletonItemElement = HTMLElement & {
7
+ /** Size */
8
+ setAttribute(name: 'size', value: TSinchSizeEx): void;
9
+ };
10
+ export type TSinchSkeletonItemReact = TSinchElementReact<TSinchSkeletonItemElement> & {
11
+ /** Size */
12
+ size?: TSinchSizeEx;
13
+ };
@@ -0,0 +1 @@
1
+ export {};
package/theme/chip.css CHANGED
@@ -18,6 +18,8 @@
18
18
  --sinch-chip-color-light-violet-fg: var(--sinch-color-violet-700);
19
19
  --sinch-chip-color-light-yellow-bg: var(--sinch-color-bolt-200);
20
20
  --sinch-chip-color-light-yellow-fg: var(--sinch-color-bolt-700);
21
+ --sinch-chip-color-light-red-bg: var(--sinch-color-jasper-200);
22
+ --sinch-chip-color-light-red-fg: var(--sinch-color-jasper-700);
21
23
  --sinch-chip-color-dark-blue-bg: var(--sinch-color-night-700);
22
24
  --sinch-chip-color-dark-blue-fg: var(--sinch-color-night-200);
23
25
  --sinch-chip-color-dark-brown-bg: var(--sinch-color-mud-700);
@@ -34,6 +36,8 @@
34
36
  --sinch-chip-color-dark-violet-fg: var(--sinch-color-violet-200);
35
37
  --sinch-chip-color-dark-yellow-bg: var(--sinch-color-bolt-700);
36
38
  --sinch-chip-color-dark-yellow-fg: var(--sinch-color-bolt-200);
39
+ --sinch-chip-color-dark-red-bg: var(--sinch-color-jasper-700);
40
+ --sinch-chip-color-dark-red-fg: var(--sinch-color-jasper-200);
37
41
  --sinch-chip-color-blue-bg: var(--sinch-color-night-400);
38
42
  --sinch-chip-color-blue-fg: var(--sinch-color-snow-100);
39
43
  --sinch-chip-color-brown-bg: var(--sinch-color-mud-400);
@@ -50,6 +54,8 @@
50
54
  --sinch-chip-color-violet-fg: var(--sinch-color-stormy-500);
51
55
  --sinch-chip-color-yellow-bg: var(--sinch-color-bolt-400);
52
56
  --sinch-chip-color-yellow-fg: var(--sinch-color-stormy-500);
57
+ --sinch-chip-color-red-bg: var(--sinch-color-jasper-400);
58
+ --sinch-chip-color-red-fg: var(--sinch-color-stormy-500);
53
59
  --sinch-chip-color-celtic-bg: var(--sinch-color-feedback-info-bg);
54
60
  --sinch-chip-color-celtic-fg: var(--sinch-color-feedback-info-contrast);
55
61
  --sinch-chip-color-olive-bg: var(--sinch-color-feedback-success-bg);
package/theme/tag.css CHANGED
@@ -18,6 +18,8 @@
18
18
  --sinch-tag-color-light-violet-fg: var(--sinch-color-violet-700);
19
19
  --sinch-tag-color-light-yellow-bg: var(--sinch-color-bolt-200);
20
20
  --sinch-tag-color-light-yellow-fg: var(--sinch-color-bolt-700);
21
+ --sinch-tag-color-light-red-bg: var(--sinch-color-jasper-200);
22
+ --sinch-tag-color-light-red-fg: var(--sinch-color-jasper-700);
21
23
  --sinch-tag-color-dark-blue-bg: var(--sinch-color-night-700);
22
24
  --sinch-tag-color-dark-blue-fg: var(--sinch-color-night-200);
23
25
  --sinch-tag-color-dark-brown-bg: var(--sinch-color-mud-700);
@@ -34,6 +36,8 @@
34
36
  --sinch-tag-color-dark-violet-fg: var(--sinch-color-violet-200);
35
37
  --sinch-tag-color-dark-yellow-bg: var(--sinch-color-bolt-700);
36
38
  --sinch-tag-color-dark-yellow-fg: var(--sinch-color-bolt-200);
39
+ --sinch-tag-color-dark-red-bg: var(--sinch-color-jasper-700);
40
+ --sinch-tag-color-dark-red-fg: var(--sinch-color-jasper-200);
37
41
  --sinch-tag-color-blue-bg: var(--sinch-color-night-400);
38
42
  --sinch-tag-color-blue-fg: var(--sinch-color-snow-100);
39
43
  --sinch-tag-color-brown-bg: var(--sinch-color-mud-400);
@@ -50,6 +54,8 @@
50
54
  --sinch-tag-color-violet-fg: var(--sinch-color-stormy-500);
51
55
  --sinch-tag-color-yellow-bg: var(--sinch-color-bolt-400);
52
56
  --sinch-tag-color-yellow-fg: var(--sinch-color-stormy-500);
57
+ --sinch-tag-color-red-bg: var(--sinch-color-jasper-400);
58
+ --sinch-tag-color-red-fg: var(--sinch-color-stormy-500);
53
59
  --sinch-tag-color-celtic-bg: var(--sinch-color-feedback-info-bg);
54
60
  --sinch-tag-color-celtic-fg: var(--sinch-color-feedback-info-contrast);
55
61
  --sinch-tag-color-olive-bg: var(--sinch-color-feedback-success-bg);
@@ -1,6 +1,6 @@
1
1
  import '../title';
2
2
  import '../text';
3
- import { cloneNode, defineCustomElement, getRect, NectaryElement } from '../utils';
3
+ import { cloneNode, defineCustomElement, getRect, NectaryElement, shouldReduceMotion } from '../utils';
4
4
  const templateHTML = '<style>:host{display:block}#items{display:block;width:0;height:0;visibility:hidden;overflow:hidden}#list{display:flex;flex-direction:column;position:fixed;z-index:1;bottom:0;right:16px}.item-wrapper{height:0;opacity:0;position:relative}.item-wrapper[data-deleting]::after{content:"";position:absolute;top:0;left:0;bottom:0;right:0}</style><slot id="items"></slot><div id="list"></div>';
5
5
  const DURATION_ADD = 250;
6
6
  const DURATION_REMOVE = 250;
@@ -11,7 +11,7 @@ defineCustomElement('sinch-toast-manager', class extends NectaryElement {
11
11
  #$slot;
12
12
  #$list;
13
13
  #map = new WeakMap();
14
- #shouldReduceMotion = false;
14
+ #shouldReduceMotion;
15
15
  #animations = new Set();
16
16
  constructor() {
17
17
  super();
@@ -19,10 +19,10 @@ defineCustomElement('sinch-toast-manager', class extends NectaryElement {
19
19
  shadowRoot.appendChild(template.content.cloneNode(true));
20
20
  this.#$slot = shadowRoot.querySelector('slot');
21
21
  this.#$list = shadowRoot.querySelector('#list');
22
+ this.#shouldReduceMotion = shouldReduceMotion();
22
23
  }
23
24
  connectedCallback() {
24
25
  this.#$slot.addEventListener('slotchange', this.#onSlotChange);
25
- this.#shouldReduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
26
26
  }
27
27
  disconnectedCallback() {
28
28
  this.#$slot.removeEventListener('slotchange', this.#onSlotChange);
package/tooltip/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import '../text';
2
2
  import '../pop';
3
- import { defineCustomElement, getBooleanAttribute, getAttribute, getLiteralAttribute, updateBooleanAttribute, updateAttribute, updateLiteralAttribute, NectaryElement, setClass, rectOverlap, getReactEventHandler } from '../utils';
3
+ import { defineCustomElement, getBooleanAttribute, getAttribute, getLiteralAttribute, updateBooleanAttribute, updateAttribute, updateLiteralAttribute, NectaryElement, setClass, rectOverlap, getReactEventHandler, shouldReduceMotion } from '../utils';
4
4
  const templateHTML = '<style>:host{display:contents}#content-wrapper{padding-bottom:8px}#content{position:relative;display:block;max-width:300px;padding:2px 6px;box-sizing:border-box;color:var(--sinch-color-text-default);background-color:var(--sinch-color-snow-600);border-radius:var(--sinch-shape-radius-xs);pointer-events:none;opacity:0}: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}#text{word-break:break-word;pointer-events:none}#tip{position:absolute;left:50%;top:100%;transform:translateX(-50%) rotate(0);transform-origin:top center;fill:var(--sinch-color-snow-600);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([inverted]:not([inverted=false])) #content{background-color:var(--sinch-color-stormy-500);color:var(--sinch-color-text-inverted)}:host([inverted]:not([inverted=false])) #tip{fill:var(--sinch-color-stormy-500)}</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>';
5
5
  import { TooltipState } from './tooltip-state';
6
6
  import { assertOrientation, getPopOrientation, orientationValues } from './utils';
7
7
  const TIP_SIZE = 8;
8
8
  const SHOW_DELAY = 1000;
9
9
  const HIDE_DELAY = 0;
10
- const ANIMATION_DURATION = 50;
10
+ const ANIMATION_DURATION = 100;
11
11
  const template = document.createElement('template');
12
12
  template.innerHTML = templateHTML;
13
13
  defineCustomElement('sinch-tooltip', class extends NectaryElement {
@@ -32,12 +32,13 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
32
32
  this.#$contentWrapper = shadowRoot.querySelector('#content-wrapper');
33
33
  this.#$tip = shadowRoot.querySelector('#tip');
34
34
  this.#$target = shadowRoot.querySelector('#target');
35
- this.#shouldReduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
35
+ this.#shouldReduceMotion = shouldReduceMotion();
36
36
  this.#tooltipState = new TooltipState({
37
37
  showDelay: SHOW_DELAY,
38
38
  hideDelay: this.#shouldReduceMotion ? HIDE_DELAY + ANIMATION_DURATION : HIDE_DELAY,
39
39
  hideAnimationDuration: this.#shouldReduceMotion ? 0 : ANIMATION_DURATION,
40
- onShow: this.#onStateShow,
40
+ onShowStart: this.#onStateShowStart,
41
+ onShowEnd: this.#onStateShowEnd,
41
42
  onHideStart: this.#onStateHideStart,
42
43
  onHideEnd: this.#onStateHideEnd
43
44
  });
@@ -108,29 +109,27 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
108
109
  }
109
110
  }
110
111
  #onMouseDown = () => {
111
- this.#tooltipState.interrupt();
112
- this.#unsubscribeScroll();
112
+ this.#tooltipState.destroy();
113
113
  };
114
114
  #onPopClose = () => {
115
115
  this.#tooltipState.destroy();
116
- this.#unsubscribeScroll();
117
116
  };
118
117
  #onMouseEnter = () => {
119
118
  this.#tooltipState.show();
120
- this.#subscribeScroll();
121
- this.#subscribeMouseLeaveEvents();
122
119
  };
123
120
  #onMouseLeave = e => {
124
121
  if (!this.#isOpen() || e.relatedTarget !== this.#$contentWrapper && e.relatedTarget !== this.#$target) {
125
122
  this.#tooltipState.hide();
126
- this.#unsubscribeScroll();
127
123
  }
128
124
  };
129
125
  #onScroll = () => {
130
126
  this.#tooltipState.destroy();
131
- this.#unsubscribeScroll();
132
127
  };
133
- #onStateShow = () => {
128
+ #onStateShowStart = () => {
129
+ this.#subscribeScroll();
130
+ this.#subscribeMouseLeaveEvents();
131
+ };
132
+ #onStateShowEnd = () => {
134
133
  this.#dispatchShowEvent();
135
134
  updateBooleanAttribute(this.#$pop, 'open', true);
136
135
  requestAnimationFrame(this.#updateTipOrientation);
@@ -152,11 +151,14 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
152
151
  this.#animation.play();
153
152
  };
154
153
  #onStateHideEnd = () => {
155
- this.#animation.finish();
154
+ if (this.#isOpen()) {
155
+ this.#animation.finish();
156
+ this.#dispatchHideEvent();
157
+ updateBooleanAttribute(this.#$pop, 'open', false);
158
+ }
156
159
  this.#resetTipOrientation();
157
- updateBooleanAttribute(this.#$pop, 'open', false);
158
- this.#dispatchHideEvent();
159
160
  this.#unsubscribeMouseLeaveEvents();
161
+ this.#unsubscribeScroll();
160
162
  };
161
163
  #resetTipOrientation() {
162
164
  this.#$tip.style.top = '';
@@ -187,17 +189,16 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
187
189
  if (!this.isConnected) {
188
190
  return;
189
191
  }
190
- const value = this.text;
191
- this.#$tooltipText.textContent = value;
192
- if (value.length === 0) {
192
+ const text = this.text;
193
+ this.#$tooltipText.textContent = text;
194
+ if (text.length === 0) {
193
195
  if (this.#isSubscribed) {
194
196
  this.#tooltipState.destroy();
195
197
  this.#unsubscribeMouseEnterEvent();
196
- this.#unsubscribeMouseLeaveEvents();
197
198
  }
198
- return;
199
+ } else {
200
+ this.#subscribeMouseEnterEvent();
199
201
  }
200
- this.#subscribeMouseEnterEvent();
201
202
  }
202
203
  #subscribeMouseEnterEvent() {
203
204
  if (!this.isConnected || this.#isSubscribed) {
@@ -2,7 +2,8 @@ type TTooltipStateOptions = {
2
2
  showDelay: number;
3
3
  hideDelay: number;
4
4
  hideAnimationDuration: number;
5
- onShow(): void;
5
+ onShowStart(): void;
6
+ onShowEnd(): void;
6
7
  onHideStart(): void;
7
8
  onHideEnd(): void;
8
9
  };
@@ -1,119 +1,121 @@
1
1
  export class TooltipState {
2
2
  #timerId = null;
3
- #state = 'initial';
3
+ #state = 'hide';
4
4
  #options;
5
5
  constructor(options) {
6
6
  this.#options = options;
7
7
  }
8
8
  show() {
9
+ console.log('-> show');
9
10
  switch (this.#state) {
10
- case 'initial':
11
- {
12
- this.#delayShow();
13
- break;
14
- }
15
- case 'hide-delay':
11
+ case 'hide':
16
12
  {
17
- this.#cancelStateChange('show');
13
+ this.#switchToHideToShow();
18
14
  break;
19
15
  }
20
- case 'hide':
16
+ case 'show-to-hide':
21
17
  {
22
- this.#cancelStateChange();
23
- this.#onShow();
18
+ this.#switchToState('show');
24
19
  break;
25
20
  }
26
21
  }
27
22
  }
28
23
  hide() {
24
+ console.log('-> hide');
29
25
  switch (this.#state) {
30
- case 'show-delay':
26
+ case 'hide-to-show':
31
27
  {
32
- this.#cancelStateChange('initial');
28
+ this.#onHideAnimationEnd();
33
29
  break;
34
30
  }
35
31
  case 'show':
36
32
  {
37
- this.#delayHide();
33
+ this.#switchToShowToHide();
38
34
  break;
39
35
  }
40
36
  }
41
37
  }
42
38
  interrupt() {
39
+ console.log('-> interrupt');
43
40
  switch (this.#state) {
44
- case 'show-delay':
41
+ case 'hide-to-show':
45
42
  {
46
- this.#cancelStateChange('initial');
43
+ this.#onHideAnimationEnd();
47
44
  break;
48
45
  }
49
46
  case 'show':
50
47
  {
51
- this.#onHideStart();
48
+ this.#switchToShowToHide(true, false);
52
49
  break;
53
50
  }
54
- case 'hide-delay':
51
+ case 'show-to-hide':
55
52
  {
56
- this.#cancelStateChange();
57
- this.#onHideStart();
53
+ this.#switchToShowToHide(true, false);
58
54
  break;
59
55
  }
60
56
  }
61
57
  }
62
58
  destroy() {
59
+ console.log('-> destroy');
63
60
  switch (this.#state) {
64
- case 'show-delay':
61
+ case 'hide-to-show':
65
62
  {
66
- this.#cancelStateChange('initial');
63
+ this.#onHideAnimationEnd();
67
64
  break;
68
65
  }
69
66
  case 'show':
70
67
  {
71
- this.#onHideStart(true);
68
+ this.#switchToShowToHide(true, true);
72
69
  break;
73
70
  }
74
- case 'hide-delay':
71
+ case 'show-to-hide':
75
72
  {
76
- this.#cancelStateChange();
77
- this.#onHideStart(true);
73
+ this.#switchToShowToHide(true, true);
78
74
  break;
79
75
  }
80
76
  case 'hide':
81
77
  {
82
- this.#cancelStateChange();
83
- this.#onHideEnd();
78
+ this.#onHideAnimationEnd();
84
79
  break;
85
80
  }
86
81
  }
87
82
  }
88
- #delayShow() {
89
- this.#state = 'show-delay';
90
- this.#timerId = window.setTimeout(this.#onShow, this.#options.showDelay);
83
+ #switchToHideToShow() {
84
+ this.#switchToState('hide-to-show');
85
+ this.#options.onShowStart();
86
+ if (this.#options.showDelay === 0) {
87
+ this.#onSwitchToShow();
88
+ } else {
89
+ this.#timerId = window.setTimeout(this.#onSwitchToShow, this.#options.showDelay);
90
+ }
91
91
  }
92
- #delayHide() {
93
- this.#state = 'hide-delay';
94
- this.#timerId = window.setTimeout(this.#onHideStart, this.#options.hideDelay);
92
+ #switchToShowToHide(skipDelay, skipHideAnimation) {
93
+ this.#switchToState('show-to-hide');
94
+ if (skipDelay === true || this.#options.hideDelay === 0) {
95
+ this.#onShowToHideEnd(skipHideAnimation);
96
+ } else {
97
+ this.#timerId = window.setTimeout(this.#onShowToHideEnd, this.#options.hideDelay);
98
+ }
95
99
  }
96
- #onShow = () => {
97
- this.#state = 'show';
98
- this.#options.onShow();
100
+ #onSwitchToShow = () => {
101
+ this.#switchToState('show');
102
+ this.#options.onShowEnd();
99
103
  };
100
- #onHideStart = isImmediate => {
101
- this.#state = 'hide';
104
+ #onShowToHideEnd = skipHideAnimation => {
105
+ this.#switchToState('hide');
102
106
  this.#options.onHideStart();
103
- if (isImmediate === true || this.#options.hideAnimationDuration === 0) {
104
- this.#onHideEnd();
107
+ if (skipHideAnimation === true || this.#options.hideAnimationDuration === 0) {
108
+ this.#onHideAnimationEnd();
105
109
  } else {
106
- this.#timerId = window.setTimeout(this.#onHideEnd, this.#options.hideAnimationDuration);
110
+ this.#timerId = window.setTimeout(this.#onHideAnimationEnd, this.#options.hideAnimationDuration);
107
111
  }
108
112
  };
109
- #onHideEnd = () => {
110
- this.#state = 'initial';
113
+ #onHideAnimationEnd = () => {
114
+ this.#switchToState('hide');
111
115
  this.#options.onHideEnd();
112
116
  };
113
- #cancelStateChange(nextState) {
114
- if (nextState != null) {
115
- this.#state = nextState;
116
- }
117
+ #switchToState(nextState) {
118
+ this.#state = nextState;
117
119
  if (this.#timerId !== null) {
118
120
  clearTimeout(this.#timerId);
119
121
  this.#timerId = null;
package/utils/dom.d.ts CHANGED
@@ -29,4 +29,5 @@ export declare const hasClass: (elem: Element, name: string) => boolean;
29
29
  export declare const getCssVar: (element: Element, variableName: string) => string | null;
30
30
  export declare const getCssVars: (element: Element, variableNames: string[]) => (string | null)[];
31
31
  export declare const cloneNode: (el: Element, deep: boolean) => Element;
32
+ export declare const shouldReduceMotion: () => boolean;
32
33
  export {};
package/utils/dom.js CHANGED
@@ -152,4 +152,5 @@ export const cloneNode = (el, deep) => {
152
152
  return cloned;
153
153
  }
154
154
  return el.cloneNode(deep);
155
- };
155
+ };
156
+ export const shouldReduceMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
package/utils/index.d.ts CHANGED
@@ -9,3 +9,4 @@ export * from './debounce';
9
9
  export * from './get-react-event-handler';
10
10
  export * from './markdown';
11
11
  export * from './event-target';
12
+ export * from './uid';
package/utils/index.js CHANGED
@@ -8,4 +8,5 @@ export * from './throttle';
8
8
  export * from './debounce';
9
9
  export * from './get-react-event-handler';
10
10
  export * from './markdown';
11
- export * from './event-target';
11
+ export * from './event-target';
12
+ export * from './uid';
package/utils/uid.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const getUid: () => string;
package/utils/uid.js ADDED
@@ -0,0 +1,13 @@
1
+ export const getUid = () => crypto.getRandomValues(new Uint8Array(21)).reduce((id, byte) => {
2
+ const nextByte = byte & 63;
3
+ if (nextByte < 36) {
4
+ return id + nextByte.toString(36);
5
+ }
6
+ if (nextByte < 62) {
7
+ return id + (nextByte - 26).toString(36).toUpperCase();
8
+ }
9
+ if (nextByte > 62) {
10
+ return `${id}-`;
11
+ }
12
+ return `${id}_`;
13
+ }, '');