@nectary/components 0.45.0 → 0.45.2

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.
@@ -6,6 +6,7 @@ import '../popover';
6
6
  import '../tabs';
7
7
  import '../tabs-icon-option';
8
8
  import '../emoji';
9
+ import '../text';
9
10
  import '../icons/search';
10
11
  import '../icons/sentiment-satisfied';
11
12
  import '../icons/emoji-people';
@@ -6,6 +6,7 @@ import '../popover';
6
6
  import '../tabs';
7
7
  import '../tabs-icon-option';
8
8
  import '../emoji';
9
+ import '../text';
9
10
  import '../icons/search';
10
11
  import '../icons/sentiment-satisfied';
11
12
  import '../icons/emoji-people';
@@ -15,13 +16,15 @@ import '../icons/emoji-objects';
15
16
  import '../icons/emoji-transportation';
16
17
  import '../icons/emoji-events';
17
18
  import '../icons/emoji-symbols';
18
- import { defineCustomElement, getAttribute, getBooleanAttribute, NectaryElement, updateAttribute, updateBooleanAttribute, getReactEventHandler, getRect, subscribeContext } from '../utils';
19
+ import { defineCustomElement, getAttribute, getBooleanAttribute, NectaryElement, updateAttribute, updateBooleanAttribute, getReactEventHandler, getRect, subscribeContext, debounceTimeout, setClass } from '../utils';
19
20
  import dataJson from './data.json';
20
- const templateHTML = '<style>:host{display:block}#wrapper{width:384px;max-height:504px;display:flex;flex-direction:column;gap:8px;padding:12px 0}#toolbar{display:flex;gap:8px;padding:0 12px}#input{flex:1}#list-wrapper{overflow-y:auto;overflow-x:hidden;width:384px;box-sizing:border-box;scrollbar-gutter:stable}#list{display:flex;flex-wrap:wrap;gap:8px;padding:4px 12px 0;width:384px;box-sizing:border-box}</style><div id="wrapper"><div id="toolbar"><sinch-input id="input" size="l" aria-label="Search emojis"><sinch-icon-search slot="icon"></sinch-icon-search></sinch-input><sinch-popover id="skin-popover" orientation="bottom-left" aria-label="Emoji skin tone select"><sinch-icon-button id="skin-button" slot="target" size="l" aria-label="Select emoji skin tones"><sinch-color-swatch id="skin-swatch" slot="icon" name="skin-tone-0"></sinch-color-swatch></sinch-icon-button><sinch-color-menu id="skin-menu" slot="content" cols="1" value="skin-tone-0" colors="skin-tone-0,skin-tone-10,skin-tone-20,skin-tone-30,skin-tone-40,skin-tone-50" aria-label="Emoji skin tone menu"></sinch-color-menu></sinch-popover></div><sinch-tabs id="tabs" aria-label="Emoji groups"></sinch-tabs><div id="list-wrapper"><div id="list"></div></div></div>';
21
+ const templateHTML = '<style>:host{display:block}#wrapper{width:384px;max-height:504px;display:flex;flex-direction:column;gap:8px;padding:12px 0}#toolbar{display:flex;gap:8px;padding:0 12px}#input{flex:1}#list-wrapper{overflow-y:auto;overflow-x:hidden;width:384px;box-sizing:border-box;scrollbar-gutter:stable}#list{display:flex;flex-wrap:wrap;gap:8px;padding:4px 12px 0;width:384px;box-sizing:border-box}#not-found{display:none;width:100%;height:48px;align-items:center;justify-content:center;color:var(--sinch-color-text-muted);pointer-events:none;user-select:none}#not-found.active{display:flex}</style><div id="wrapper"><div id="toolbar"><sinch-input id="input" size="l" aria-label="Search emojis"><sinch-icon-search slot="icon"></sinch-icon-search></sinch-input><sinch-popover id="skin-popover" orientation="bottom-left" aria-label="Emoji skin tone select"><sinch-icon-button id="skin-button" slot="target" size="l" aria-label="Select emoji skin tones"><sinch-color-swatch id="skin-swatch" slot="icon" name="skin-tone-0"></sinch-color-swatch></sinch-icon-button><sinch-color-menu id="skin-menu" slot="content" cols="1" value="skin-tone-0" colors="skin-tone-0,skin-tone-10,skin-tone-20,skin-tone-30,skin-tone-40,skin-tone-50" aria-label="Emoji skin tone menu"></sinch-color-menu></sinch-popover></div><sinch-tabs id="tabs" aria-label="Emoji groups"></sinch-tabs><div id="list-wrapper"><div id="list"></div><div id="not-found"><sinch-text type="m">No results</sinch-text></div></div></div>';
21
22
  const groupIconTagNames = ['sinch-icon-sentiment-satisfied', 'sinch-icon-emoji-people', 'sinch-icon-emoji-nature', 'sinch-icon-emoji-food-beverage', 'sinch-icon-emoji-transportation', 'sinch-icon-emoji-events', 'sinch-icon-emoji-objects', 'sinch-icon-emoji-symbols'];
22
23
  const groupLabels = ['Emotions', 'People', 'Animals and nature', 'Food and drinks', 'Travel and places', 'Sports and activities', 'Objects', 'Symbols and flags'];
23
24
  const data = dataJson;
24
25
  const template = document.createElement('template');
26
+ const MIN_SEARCH_LENGTH = 2;
27
+ const SEARCH_DEBOUNCE_TIMEOUT = 300;
25
28
  template.innerHTML = templateHTML;
26
29
  defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
27
30
  #$tabs;
@@ -31,9 +34,12 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
31
34
  #$skinSwatch;
32
35
  #$skinButton;
33
36
  #$list;
37
+ #$notFound;
34
38
  #controller = null;
35
39
  #$sh;
40
+ #searchDebounce;
36
41
  #currentSkinTone = 0;
42
+ #prevTabsValue = null;
37
43
  constructor() {
38
44
  super();
39
45
  const shadowRoot = this.attachShadow();
@@ -46,6 +52,8 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
46
52
  this.#$skinSwatch = shadowRoot.querySelector('#skin-swatch');
47
53
  this.#$skinButton = shadowRoot.querySelector('#skin-button');
48
54
  this.#$list = shadowRoot.querySelector('#list');
55
+ this.#$notFound = shadowRoot.querySelector('#not-found');
56
+ this.#searchDebounce = debounceTimeout(SEARCH_DEBOUNCE_TIMEOUT)(this.#updateSearch);
49
57
  }
50
58
  connectedCallback() {
51
59
  this.#controller = new AbortController();
@@ -83,6 +91,7 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
83
91
  }
84
92
  disconnectedCallback() {
85
93
  this.#controller.abort();
94
+ this.#searchDebounce.cancel();
86
95
  }
87
96
  get skinToneButtonRect() {
88
97
  return getRect(this.#$skinButton);
@@ -90,6 +99,9 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
90
99
  get searchInputRect() {
91
100
  return getRect(this.#$input);
92
101
  }
102
+ get searchClearButtonRect() {
103
+ return this.#$input.clearButtonRect;
104
+ }
93
105
  nthSkinToneRect(index) {
94
106
  return this.#$skinMenu.nthItemRect(index);
95
107
  }
@@ -146,13 +158,8 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
146
158
  this.#updateEmojis();
147
159
  };
148
160
  #onSearchChange = e => {
149
- const value = e.detail;
150
- this.#$input.value = value;
151
- if (value.length < 3) {
152
- return;
153
- }
154
- updateAttribute(this.#$tabs, 'value', '');
155
- this.#updateSearchEmojis();
161
+ this.#$input.value = e.detail;
162
+ this.#searchDebounce.fn();
156
163
  };
157
164
  #onChangeReactHandler = e => {
158
165
  getReactEventHandler(this, 'on-change')?.(e);
@@ -163,6 +170,24 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
163
170
  #getDocumentRoot() {
164
171
  return Reflect.has(this.#$sh, 'createElement') ? this.#$sh : document;
165
172
  }
173
+ #updateSearch = () => {
174
+ const value = this.#$input.value;
175
+ if (value.length < MIN_SEARCH_LENGTH) {
176
+ if (this.#isSearchMode()) {
177
+ if (this.#prevTabsValue !== null) {
178
+ this.#$tabs.setAttribute('value', this.#prevTabsValue);
179
+ }
180
+ this.#updateEmojis();
181
+ }
182
+ return;
183
+ }
184
+ const currentActiveTab = this.#$tabs.getAttribute('value');
185
+ if (currentActiveTab !== null) {
186
+ this.#prevTabsValue = currentActiveTab;
187
+ }
188
+ this.#$tabs.removeAttribute('value');
189
+ this.#updateSearchEmojis();
190
+ };
166
191
  #updateTabs() {
167
192
  const doc = this.#getDocumentRoot();
168
193
  const tabsFragment = document.createDocumentFragment();
@@ -183,7 +208,7 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
183
208
  *#iterateSearchEmojis(searchValue, skinTone) {
184
209
  for (const group of data) {
185
210
  for (const entry of group.emojis) {
186
- if (entry.label.includes(searchValue)) {
211
+ if (entry.label.toLowerCase().includes(searchValue)) {
187
212
  const hasSkins = entry.skins != null;
188
213
  if (skinTone === 0 || !hasSkins) {
189
214
  yield entry;
@@ -214,15 +239,18 @@ defineCustomElement('sinch-emoji-picker', class extends NectaryElement {
214
239
  }
215
240
  #updateSearchEmojis() {
216
241
  const searchValue = this.#$input.value;
217
- if (searchValue.length < 3) {
242
+ if (searchValue.length < MIN_SEARCH_LENGTH) {
218
243
  return;
219
244
  }
220
245
  const doc = this.#getDocumentRoot();
221
246
  const fragment = document.createDocumentFragment();
247
+ let someFound = false;
222
248
  for (const entry of this.#iterateSearchEmojis(searchValue, this.#currentSkinTone)) {
223
249
  const el = this.#createEmojiElement(doc, entry);
250
+ someFound = true;
224
251
  fragment.appendChild(el);
225
252
  }
253
+ setClass(this.#$notFound, 'active', !someFound);
226
254
  this.#$list.replaceChildren(fragment);
227
255
  this.#$list.scrollTo(0, 0);
228
256
  }
@@ -13,6 +13,7 @@ export declare type TEmojiGroup = {
13
13
  export declare type TSinchEmojiPickerElement = HTMLElement & {
14
14
  readonly skinToneButtonRect: TRect;
15
15
  readonly searchInputRect: TRect;
16
+ readonly searchClearButtonRect: TRect;
16
17
  nthSkinToneRect(index: number): TRect | null;
17
18
  nthTabRect(index: number): TRect | null;
18
19
  nthEmojiRect(index: number): TRect | null;
@@ -1,20 +1,39 @@
1
1
  import '../tooltip';
2
2
  import '../icons/help-outline';
3
- import { defineCustomElement, getAttribute, getBooleanAttribute, getIntegerAttribute, NectaryElement, updateAttribute, updateBooleanAttribute, updateIntegerAttribute } from '../utils';
3
+ import { defineCustomElement, getAttribute, getBooleanAttribute, getIntegerAttribute, getReactEventHandler, NectaryElement, updateAttribute, updateBooleanAttribute, updateIntegerAttribute } from '../utils';
4
4
  const templateHTML = '<style>:host{display:contents}#icon{--sinch-size-icon:18px}</style><sinch-tooltip><sinch-icon-help-outline id="icon"></sinch-icon-help-outline></sinch-tooltip>';
5
5
  const template = document.createElement('template');
6
6
  template.innerHTML = templateHTML;
7
7
  defineCustomElement('sinch-help-tooltip', class extends NectaryElement {
8
8
  #$tooltip;
9
+ #controller = null;
9
10
  constructor() {
10
11
  super();
11
12
  const shadowRoot = this.attachShadow();
12
13
  shadowRoot.appendChild(template.content.cloneNode(true));
13
14
  this.#$tooltip = shadowRoot.querySelector('sinch-tooltip');
14
15
  }
16
+ connectedCallback() {
17
+ super.connectedCallback();
18
+ this.#controller = new AbortController();
19
+ const options = {
20
+ signal: this.#controller.signal
21
+ };
22
+ this.#$tooltip.addEventListener('-show', this.#onTooltipShow, options);
23
+ this.#$tooltip.addEventListener('-hide', this.#onTooltipHide, options);
24
+ this.addEventListener('-show', this.#onTooltipShowReactHandler, options);
25
+ this.addEventListener('-hide', this.#onTooltipHideReactHandler, options);
26
+ }
27
+ disconnectedCallback() {
28
+ super.disconnectedCallback();
29
+ this.#controller.abort();
30
+ }
15
31
  static get observedAttributes() {
16
32
  return ['text', 'width', 'orientation', 'inverted'];
17
33
  }
34
+ attributeChangedCallback(name, _, newVal) {
35
+ updateAttribute(this.#$tooltip, name, newVal);
36
+ }
18
37
  get text() {
19
38
  return getAttribute(this, 'text', '');
20
39
  }
@@ -45,7 +64,16 @@ defineCustomElement('sinch-help-tooltip', class extends NectaryElement {
45
64
  get tooltipRect() {
46
65
  return this.#$tooltip.tooltipRect;
47
66
  }
48
- attributeChangedCallback(name, _, newVal) {
49
- updateAttribute(this.#$tooltip, name, newVal);
50
- }
67
+ #onTooltipShow = () => {
68
+ this.dispatchEvent(new CustomEvent('-show'));
69
+ };
70
+ #onTooltipHide = () => {
71
+ this.dispatchEvent(new CustomEvent('-hide'));
72
+ };
73
+ #onTooltipShowReactHandler = () => {
74
+ getReactEventHandler(this, 'on-show')?.();
75
+ };
76
+ #onTooltipHideReactHandler = () => {
77
+ getReactEventHandler(this, 'on-hide')?.();
78
+ };
51
79
  });
@@ -33,6 +33,10 @@ defineCustomElement('sinch-icon-button', class extends NectaryElement {
33
33
  this.addEventListener('-click', this.#onClickReactHandler, options);
34
34
  this.addEventListener('-focus', this.#onFocusReactHandler, options);
35
35
  this.addEventListener('-blur', this.#onBlurReactHandler, options);
36
+ this.#$tooltip.addEventListener('-show', this.#onTooltipShow, options);
37
+ this.#$tooltip.addEventListener('-hide', this.#onTooltipHide, options);
38
+ this.addEventListener('-tooltip-show', this.#onTooltipShowReactHandler, options);
39
+ this.addEventListener('-tooltip-hide', this.#onTooltipHideReactHandler, options);
36
40
  subscribeContext(this, 'size', this.#onContextSize, this.#controller.signal);
37
41
  this.#sizeContext.listen(this.#controller.signal);
38
42
  this.#onSizeUpdate();
@@ -150,6 +154,12 @@ defineCustomElement('sinch-icon-button', class extends NectaryElement {
150
154
  #onButtonBlur = () => {
151
155
  this.dispatchEvent(new CustomEvent('-blur'));
152
156
  };
157
+ #onTooltipShow = () => {
158
+ this.dispatchEvent(new CustomEvent('-tooltip-show'));
159
+ };
160
+ #onTooltipHide = () => {
161
+ this.dispatchEvent(new CustomEvent('-tooltip-hide'));
162
+ };
153
163
  #onFocusReactHandler = () => {
154
164
  getReactEventHandler(this, 'on-focus')?.();
155
165
  };
@@ -159,4 +169,10 @@ defineCustomElement('sinch-icon-button', class extends NectaryElement {
159
169
  #onClickReactHandler = e => {
160
170
  getReactEventHandler(this, 'on-click')?.(e);
161
171
  };
172
+ #onTooltipShowReactHandler = () => {
173
+ getReactEventHandler(this, 'on-tooltip-show')?.();
174
+ };
175
+ #onTooltipHideReactHandler = () => {
176
+ getReactEventHandler(this, 'on-tooltip-hide')?.();
177
+ };
162
178
  });
@@ -37,4 +37,8 @@ export declare type TSinchIconButtonReact = TSinchElementReact<TSinchIconButtonE
37
37
  'on-focus'?: (e: CustomEvent<void>) => void;
38
38
  /** Blur event handler */
39
39
  'on-blur'?: (e: CustomEvent<void>) => void;
40
+ /** Tooltip Show Event */
41
+ 'on-tooltip-show'?: (e: CustomEvent<void>) => void;
42
+ /** Tooltip Hide Event */
43
+ 'on-tooltip-hide'?: (e: CustomEvent<void>) => void;
40
44
  };
package/input/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import '../icon-button';
2
2
  import '../icons/close';
3
3
  import '../stop-events';
4
- import { Context, defineCustomElement, getAttribute, getBooleanAttribute, getLiteralAttribute, getReactEventHandler, isAttrTrue, NectaryElement, setClass, subscribeContext, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute, updateLiteralAttribute } from '../utils';
4
+ import { Context, defineCustomElement, getAttribute, getBooleanAttribute, getLiteralAttribute, getReactEventHandler, getRect, isAttrTrue, NectaryElement, setClass, subscribeContext, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute, updateLiteralAttribute } from '../utils';
5
5
  import { assertSize, DEFAULT_SIZE, sizeValues } from '../utils/size';
6
- const templateHTML = '<style>:host{display:inline-block;vertical-align:middle}:host([data-size="l"]){--sinch-size:var(--sinch-size-l);--sinch-size-icon:var(--sinch-input-icon-size-l);--sinch-shape-radius:var(--sinch-shape-radius-l)}:host([data-size="m"]){--sinch-size:var(--sinch-size-m);--sinch-size-icon:var(--sinch-input-icon-size-m);--sinch-shape-radius:var(--sinch-shape-radius-m)}:host([data-size="s"]){--sinch-size:var(--sinch-size-s);--sinch-size-icon:var(--sinch-input-icon-size-s);--sinch-shape-radius:var(--sinch-shape-radius-s)}#wrapper{position:relative;display:flex;flex-direction:row;align-items:center;box-sizing:border-box;border-radius:var(--sinch-shape-radius);width:100%;height:var(--sinch-size);background-color:var(--sinch-color-bg-primary-light)}#input{all:initial;flex:1;min-width:0;height:100%;padding:0 12px 0 40px;font:var(--sinch-font-text-m);color:var(--sinch-color-text-default)}:host([data-size="s"]) #input{padding-left:32px}#input::placeholder{font:var(--sinch-font-text-m);color:var(--sinch-color-text-muted);opacity:1}#border{position:absolute;border:1px solid var(--sinch-color-border-dark);border-radius:var(--sinch-shape-radius);inset:0;pointer-events:none}:host([invalid]:not([invalid=false]):not([disabled])) #border{border-color:var(--sinch-color-text-invalid)}#input:disabled{color:var(--sinch-color-text-disabled)}#input:disabled::placeholder{color:var(--sinch-color-text-disabled)}#input:disabled+#border{border-color:var(--sinch-color-border-disabled)}#input:focus+#border{border-color:var(--sinch-color-border-focus);border-width:2px}#input[type=password]{font-size:1.5em;letter-spacing:.1em}#icon-wrapper{position:relative;height:100%}#icon{position:absolute;display:flex;align-items:center;left:10px;top:0;bottom:0;pointer-events:none;--sinch-color-icon:var(--sinch-color-stormy-500)}:host([disabled]:not([disabled=false])) #icon{--sinch-color-icon:var(--sinch-color-border-disabled)}#icon-wrapper.empty{display:none}#icon-wrapper.empty~#input{padding-left:12px}#clear{display:none;--sinch-icon-button-color-icon:var(--sinch-color-text-muted)}#clear.active{display:block}:host([disabled]:not([disabled=false])) #clear{display:none}#wrapper:focus-within #clear{--sinch-icon-button-color-icon:var(--sinch-color-stormy-500)}#right{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-right:4px}#right.empty{display:none}#left{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-left:4px}#left.empty{display:none}</style><div id="wrapper"><div id="left"><slot name="left"></slot></div><div id="icon-wrapper"><div id="icon"><slot name="icon"></slot></div></div><input id="input" type="text"/><div id="border"></div><div id="right"><sinch-stop-events events="keydown"><sinch-icon-button id="clear" aria-label="Clear input value"><sinch-icon-close slot="icon"></sinch-icon-close></sinch-icon-button></sinch-stop-events><slot name="right"></slot></div></div>';
6
+ const templateHTML = '<style>:host{display:inline-block;vertical-align:middle}:host([data-size="l"]){--sinch-size:var(--sinch-size-l);--sinch-size-icon:var(--sinch-input-icon-size-l);--sinch-shape-radius:var(--sinch-shape-radius-l)}:host([data-size="m"]){--sinch-size:var(--sinch-size-m);--sinch-size-icon:var(--sinch-input-icon-size-m);--sinch-shape-radius:var(--sinch-shape-radius-m)}:host([data-size="s"]){--sinch-size:var(--sinch-size-s);--sinch-size-icon:var(--sinch-input-icon-size-s);--sinch-shape-radius:var(--sinch-shape-radius-s)}#wrapper{position:relative;display:flex;flex-direction:row;align-items:center;box-sizing:border-box;border-radius:var(--sinch-shape-radius);width:100%;height:var(--sinch-size);background-color:var(--sinch-color-bg-primary-light)}#input{all:initial;flex:1;min-width:0;height:100%;padding:0 12px 0 40px;font:var(--sinch-font-text-m);color:var(--sinch-color-text-default)}:host([data-size="s"]) #input{padding-left:32px}#input::placeholder{font:var(--sinch-font-text-m);color:var(--sinch-color-text-muted);opacity:1}#border{position:absolute;border:1px solid var(--sinch-color-border-dark);border-radius:var(--sinch-shape-radius);inset:0;pointer-events:none}:host([invalid]:not([invalid=false]):not([disabled])) #border{border-color:var(--sinch-color-text-invalid)}#input:disabled{color:var(--sinch-color-text-disabled)}#input:disabled::placeholder{color:var(--sinch-color-text-disabled)}#input:disabled+#border{border-color:var(--sinch-color-border-disabled)}#input:focus+#border{border-color:var(--sinch-color-border-focus);border-width:2px}#input[type=password]{font-size:1.5em;letter-spacing:.1em}#icon-wrapper{position:relative;height:100%}#icon{position:absolute;display:flex;align-items:center;left:10px;top:0;bottom:0;pointer-events:none;--sinch-color-icon:var(--sinch-color-stormy-500)}:host([disabled]:not([disabled=false])) #icon{--sinch-color-icon:var(--sinch-color-border-disabled)}#icon-wrapper.empty{display:none}#icon-wrapper.empty~#input{padding-left:12px}#clear-wrapper{margin-left:-10px;margin-right:4px}#clear{display:none;--sinch-icon-button-color-icon:var(--sinch-color-stormy-500)}:host([value]:not([value=""])) #clear[data-focus],:host([value]:not([value=""])) #clear[data-tooltip]{display:block}#right{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-right:4px}#right.empty{display:none}#left{display:flex;flex-direction:row;align-self:stretch;align-items:center;gap:4px;padding-left:4px}#left.empty{display:none}</style><div id="wrapper"><div id="left"><slot name="left"></slot></div><div id="icon-wrapper"><div id="icon"><slot name="icon"></slot></div></div><input id="input" type="text"/><div id="border"></div><div id="clear-wrapper"><sinch-stop-events events="keydown"><sinch-icon-button id="clear" aria-label="Clear input value"><sinch-icon-close slot="icon"></sinch-icon-close></sinch-icon-button></sinch-stop-events></div><div id="right"><slot name="right"></slot></div></div>';
7
7
  import { inputTypes } from './utils';
8
8
  const template = document.createElement('template');
9
9
  template.innerHTML = templateHTML;
@@ -51,7 +51,10 @@ defineCustomElement('sinch-input', class extends NectaryElement {
51
51
  this.#$input.addEventListener('keydown', this.#onSelectionChange, options);
52
52
  this.#$input.addEventListener('focus', this.#onInputFocus, options);
53
53
  this.#$input.addEventListener('blur', this.#onInputBlur, options);
54
- this.#$clear.addEventListener('click', this.#onClear, options);
54
+ this.#$clear.addEventListener('click', this.#onClearButtonClick, options);
55
+ this.#$clear.addEventListener('blur', this.#onClearButtonBlur, options);
56
+ this.#$clear.addEventListener('-tooltip-show', this.#onClearButtonTooltipShow, options);
57
+ this.#$clear.addEventListener('-tooltip-hide', this.#onClearButtonTooltipHide, options);
55
58
  this.#$iconSlot.addEventListener('slotchange', this.#onIconSlotChange, options);
56
59
  this.#$leftSlot.addEventListener('slotchange', this.#onLeftSlotChange, options);
57
60
  this.#$rightSlot.addEventListener('slotchange', this.#onRightSlotChange, options);
@@ -91,7 +94,6 @@ defineCustomElement('sinch-input', class extends NectaryElement {
91
94
  this.#$input.setSelectionRange(this.#cursorPos, this.#cursorPos);
92
95
  }
93
96
  }
94
- setClass(this.#$clear, 'active', nextVal.length > 0);
95
97
  this.#onRightSlotChange();
96
98
  break;
97
99
  }
@@ -185,6 +187,9 @@ defineCustomElement('sinch-input', class extends NectaryElement {
185
187
  set selectionDirection(value) {
186
188
  this.#$input.selectionDirection = value;
187
189
  }
190
+ get clearButtonRect() {
191
+ return getRect(this.#$clear);
192
+ }
188
193
  get focusable() {
189
194
  return true;
190
195
  }
@@ -253,19 +258,18 @@ defineCustomElement('sinch-input', class extends NectaryElement {
253
258
  setClass(this.#$leftWrapper, 'empty', isEmpty);
254
259
  };
255
260
  #onRightSlotChange = () => {
256
- const isEmpty = this.#$rightSlot.assignedElements().length === 0 && this.value.length === 0;
261
+ const isEmpty = this.#$rightSlot.assignedElements().length === 0;
257
262
  setClass(this.#$rightWrapper, 'empty', isEmpty);
258
263
  };
259
264
  #onInputFocus = () => {
265
+ this.#$clear.setAttribute('data-focus', '');
260
266
  this.dispatchEvent(new CustomEvent('-focus'));
261
267
  };
262
- #onInputBlur = () => {
268
+ #onInputBlur = e => {
263
269
  this.dispatchEvent(new CustomEvent('-blur'));
264
- };
265
- #onClear = () => {
266
- this.#$input.value = '';
267
- this.#$input.focus();
268
- this.#handleInput();
270
+ if (e.relatedTarget !== this.#$clear) {
271
+ this.#$clear.removeAttribute('data-focus');
272
+ }
269
273
  };
270
274
  #onSizeUpdate() {
271
275
  if (!this.isConnected) {
@@ -274,6 +278,23 @@ defineCustomElement('sinch-input', class extends NectaryElement {
274
278
  const size = this.getAttribute('data-size') ?? DEFAULT_SIZE;
275
279
  this.#sizeContext.dispatch(size);
276
280
  }
281
+ #onClearButtonClick = () => {
282
+ this.#$input.value = '';
283
+ this.#$input.focus();
284
+ this.#handleInput();
285
+ };
286
+ #onClearButtonBlur = e => {
287
+ if (e.relatedTarget !== this.#$input) {
288
+ this.#$clear.removeAttribute('data-focus');
289
+ }
290
+ };
291
+ #onClearButtonTooltipShow = () => {
292
+ this.#$clear.setAttribute('data-tooltip', '');
293
+ };
294
+ #onClearButtonTooltipHide = () => {
295
+ this.#$input.focus();
296
+ this.#$clear.removeAttribute('data-tooltip');
297
+ };
277
298
  #onChangeReactHandler = e => {
278
299
  getReactEventHandler(this, 'on-change')?.(e);
279
300
  };
package/input/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TSinchElementReact } from '../types';
1
+ import type { TRect, TSinchElementReact } from '../types';
2
2
  import type { TSinchSize } from '../utils/size';
3
3
  import type { SyntheticEvent } from 'react';
4
4
  export declare type TSinchInputType = 'text' | 'password';
@@ -18,6 +18,7 @@ export declare type TSinchInputElement = HTMLElement & {
18
18
  selectionStart: number | null;
19
19
  selectionEnd: number | null;
20
20
  selectionDirection: 'forward' | 'backward' | 'none' | null;
21
+ readonly clearButtonRect: TRect;
21
22
  /** Change value event */
22
23
  addEventListener(type: '-change', listener: (e: CustomEvent<string>) => void): void;
23
24
  /** Focus event */
package/link/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import '../icons/open-in-new';
2
2
  import '../icons/arrow-forward';
3
3
  import { defineCustomElement, getBooleanAttribute, getAttribute, updateBooleanAttribute, updateAttribute, NectaryElement, isAttrTrue, getReactEventHandler } from '../utils';
4
- const templateHTML = '<style>:host{display:inline}a{font:var(--sinch-font-text-m);font-size:inherit;line-height:inherit;color:var(--sinch-color-tropical-500);border-radius:.5em;--sinch-size-icon:1em;--sinch-color-icon:var(--sinch-color-tropical-500)}a:hover{color:var(--sinch-color-tropical-600);--sinch-color-icon:var(--sinch-color-tropical-600)}a:focus-visible{outline:2px solid var(--sinch-color-border-focus);outline-offset:2px}#external-icon{display:none;margin-right:.2em;vertical-align:-.2em}#standalone-icon{display:none}:host([external]:not([external=false])) #external-icon{display:inline-block}:host([standalone]:not([standalone=false])){display:block}:host([standalone]:not([standalone=false])) a{display:block;font:var(--sinch-font-text-m);font-weight:var(--sinch-font-weight-emphasized);text-decoration:none;border-radius:var(--sinch-shape-radius-m);width:fit-content;--sinch-size-icon:24px}:host([standalone]:not([standalone=false])) #external-icon{margin-right:8px;vertical-align:-7px}:host([standalone]:not([standalone=false]):is([external=false],:not([external]))) #standalone-icon{display:inline-block;vertical-align:-7px;margin-left:8px}:host([disabled]:not([disabled=false])) a{color:var(--sinch-color-tropical-200);pointer-events:none;cursor:initial;--sinch-color-icon:var(--sinch-color-tropical-200)}</style><a referrerpolicy="no-referer" aria-hidden="true"><sinch-icon-open-in-new id="external-icon"></sinch-icon-open-in-new><span id="content"></span><sinch-icon-arrow-forward id="standalone-icon"></sinch-icon-arrow-forward></a>';
4
+ const templateHTML = '<style>:host{display:inline}a{font:var(--sinch-font-text-m);font-size:inherit;line-height:inherit;color:var(--sinch-color-tropical-500);border-radius:.5em;white-space:nowrap;--sinch-size-icon:1em;--sinch-color-icon:var(--sinch-color-tropical-500)}a:hover{color:var(--sinch-color-tropical-600);--sinch-color-icon:var(--sinch-color-tropical-600)}a:focus-visible{outline:2px solid var(--sinch-color-border-focus);outline-offset:2px}#external-icon{display:none;margin-right:.2em;vertical-align:-.2em}#standalone-icon,#standalone-icon-prefix{display:none}:host([external]:not([external=false])) #external-icon{display:inline-block}:host([standalone]:not([standalone=false])){display:block}:host([standalone]:not([standalone=false])) a{display:block;font:var(--sinch-font-text-m);font-weight:var(--sinch-font-weight-emphasized);text-decoration:none;border-radius:var(--sinch-shape-radius-m);width:fit-content;--sinch-size-icon:24px}:host([standalone]:not([standalone=false])) #external-icon{margin-right:8px;vertical-align:-7px}:host([standalone]:not([standalone=false])) #standalone-icon-prefix{display:inline}:host([standalone]:not([standalone=false]):is([external=false],:not([external]))) #standalone-icon{display:inline-block;vertical-align:-7px}:host([disabled]:not([disabled=false])) a{color:var(--sinch-color-tropical-200);pointer-events:none;cursor:initial;--sinch-color-icon:var(--sinch-color-tropical-200)}#content{white-space:var(--sinch-text-white-space,normal)}</style><a referrerpolicy="no-referer" aria-hidden="true"><sinch-icon-open-in-new id="external-icon"></sinch-icon-open-in-new><span id="content"></span><span id="standalone-icon-prefix">&nbsp;</span><sinch-icon-arrow-forward id="standalone-icon"></sinch-icon-arrow-forward></a>';
5
5
  const template = document.createElement('template');
6
6
  template.innerHTML = templateHTML;
7
7
  defineCustomElement('sinch-link', class extends NectaryElement {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "0.45.0",
3
+ "version": "0.45.2",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
package/popover/index.js CHANGED
@@ -29,7 +29,7 @@ defineCustomElement('sinch-popover', class extends NectaryElement {
29
29
  this.#$pop.addEventListener('-close', this.#onPopClose, {
30
30
  signal
31
31
  });
32
- subscribeContext(this, 'visibility', this.#onContextVisibility, signal);
32
+ subscribeContext(this.#$content, 'visibility', this.#onContextVisibility, signal);
33
33
  updateAttribute(this.#$pop, 'orientation', getPopOrientation(this.orientation));
34
34
  }
35
35
  disconnectedCallback() {
package/text/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import '../icons/cancel';
2
2
  import { defineCustomElement, getBooleanAttribute, updateBooleanAttribute, NectaryElement, getLiteralAttribute, updateLiteralAttribute, isAttrTrue } from '../utils';
3
- const templateHTML = '<style>:host{display:block;font:var(--sinch-font-text-m)}:host([inline]:not([inline=false])){display:inline}:host([type="s"]){font:var(--sinch-font-text-s)}:host([type=xs]){font:var(--sinch-font-text-xs)}:host([type=xxs]){font:var(--sinch-font-text-xxs)}:host([emphasized]:not([emphasized=false])){font-weight:var(--sinch-font-weight-emphasized)}:host([ellipsis]:not([ellipsis=false])){overflow:hidden;text-overflow:ellipsis;white-space:nowrap}</style><slot></slot>';
3
+ const templateHTML = '<style>:host{display:block;font:var(--sinch-font-text-m)}:host([inline]:not([inline=false])){display:inline}:host([type="s"]){font:var(--sinch-font-text-s)}:host([type=xs]){font:var(--sinch-font-text-xs)}:host([type=xxs]){font:var(--sinch-font-text-xxs)}:host([emphasized]:not([emphasized=false])){font-weight:var(--sinch-font-weight-emphasized)}:host([ellipsis]:not([ellipsis=false])){overflow:hidden;text-overflow:ellipsis;white-space:nowrap;--sinch-text-white-space:nowrap}</style><slot></slot>';
4
4
  import { assertType, typeValues } from './utils';
5
5
  const template = document.createElement('template');
6
6
  template.innerHTML = templateHTML;
package/textarea/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCustomElement, getAttribute, getBooleanAttribute, getIntegerAttribute, getReactEventHandler, isAttrTrue, NectaryElement, updateAttribute, updateBooleanAttribute, updateExplicitBooleanAttribute } from '../utils';
2
- const templateHTML = '<style>:host{display:inline-block;vertical-align:middle}#input{all:initial;display:block;border:1px solid var(--sinch-color-stormy-200);box-sizing:border-box;border-radius:var(--sinch-shape-radius-s);width:100%;padding:8px 12px;font:var(--sinch-font-text-m);color:var(--sinch-color-text-default);background-color:var(--sinch-color-snow-100);resize:none}#input::placeholder{font:var(--sinch-font-text-m);color:var(--sinch-color-text-muted);opacity:1}#input:disabled{border-color:var(--sinch-color-snow-500);color:var(--sinch-color-stormy-100)}#input:disabled::placeholder{color:var(--sinch-color-snow-500)}#input:focus{border-color:var(--sinch-color-stormy-600)}:host([resizable]:not([resizable=false])) #input{resize:vertical}:host([invalid]:not([invalid=false])) #input:enabled{border-color:var(--sinch-color-text-invalid)}</style><textarea id="input"></textarea>';
2
+ const templateHTML = '<style>:host{display:inline-block;vertical-align:middle}#input{all:initial;display:block;border:1px solid var(--sinch-color-stormy-200);box-sizing:border-box;border-radius:var(--sinch-shape-radius-s);width:100%;padding:8px 12px;font:var(--sinch-font-text-m);color:var(--sinch-color-text-default);background-color:var(--sinch-color-snow-100);resize:none;white-space:pre-wrap}#input::placeholder{font:var(--sinch-font-text-m);color:var(--sinch-color-text-muted);opacity:1}#input:disabled{border-color:var(--sinch-color-snow-500);color:var(--sinch-color-stormy-100)}#input:disabled::placeholder{color:var(--sinch-color-snow-500)}#input:focus{border-color:var(--sinch-color-stormy-600)}:host([resizable]:not([resizable=false])) #input{resize:vertical}:host([invalid]:not([invalid=false])) #input:enabled{border-color:var(--sinch-color-text-invalid)}</style><textarea id="input"></textarea>';
3
3
  const template = document.createElement('template');
4
4
  template.innerHTML = templateHTML;
5
5
  defineCustomElement('sinch-textarea', class extends NectaryElement {
@@ -1,7 +1,5 @@
1
1
  import '../icon-button';
2
2
  import '../icons/done';
3
- import '../icons/arrow-drop-up';
4
- import '../icons/arrow-drop-down';
5
3
  import '../segmented-control';
6
4
  import '../segmented-control-option';
7
5
  import type { TSinchTimePickerElement, TSinchTimePickerReact } from './types';
@@ -1,11 +1,9 @@
1
1
  import '../icon-button';
2
2
  import '../icons/done';
3
- import '../icons/arrow-drop-up';
4
- import '../icons/arrow-drop-down';
5
3
  import '../segmented-control';
6
4
  import '../segmented-control-option';
7
5
  import { defineCustomElement, getAttribute, getBooleanAttribute, getReactEventHandler, getRect, isAttrTrue, NectaryElement, setClass, updateAttribute, updateBooleanAttribute } from '../utils';
8
- const templateHTML = '<style>:host{display:block;outline:0}#wrapper{display:flex;flex-direction:column;width:248px;padding:16px;box-sizing:border-box;gap:16px}#header{position:relative;width:100%;height:48px;font:var(--sinch-font-title-xl);line-height:48px}#footer{display:flex;justify-content:center;width:100%;height:32px}#picker{position:relative;width:216px;height:216px;border-radius:50%;box-sizing:border-box;border:1px solid var(--sinch-color-stormy-500)}#picker-hours,#picker-minutes{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:50%;pointer-events:none;user-select:none}.digit-hour-12,.digit-hour-24,.digit-minute{position:absolute;width:28px;height:28px;font:var(--sinch-font-text-s);line-height:28px;text-align:center;color:var(--sinch-color-text-default);top:calc(50% - 14px);left:calc(50% - 14px);z-index:1;cursor:pointer}.digit-hour-24{font:var(--sinch-font-text-xs);line-height:28px;color:var(--sinch-color-text-muted)}.digit-minute{font:var(--sinch-font-text-xs);line-height:28px;color:var(--sinch-color-text-muted)}.digit-hour-12:hover,.digit-hour-24:hover,.digit-minute:hover{color:var(--sinch-color-tropical-500)}.digit-hour-12.selected,.digit-hour-24.selected,.digit-minute.selected{color:var(--sinch-color-tropical-500)}.digit-hour-12.selected{font-size:16px}.digit-hour-24.selected{font-size:16px}.digit-minute.selected{font-size:16px}#picker-touch{position:absolute;left:0;top:0;width:100%;height:100%;cursor:pointer;border-radius:50%}#needle-hour,#needle-minute,#picker-touch::after{background-color:var(--sinch-color-stormy-500)}#needle-hour,#needle-minute{position:absolute;transform-origin:bottom center;transform:rotate(0);bottom:50%;height:50px;transition-duration:.25s;transition-timing-function:ease-in-out;transition-property:transform height;z-index:2}@media (prefers-reduced-motion){#needle-hour,#needle-minute{transition:none}}#needle-hour{width:4px;left:calc(50% - 2px);border-radius:2px}#needle-minute{width:2px;left:calc(50% - 1px);border-radius:1px}#needle-minute:not(.selected)::after{content:"";position:absolute;transform:translateX(-50%);left:0;top:-16px;width:4px;height:4px;border-radius:50%;background-color:var(--sinch-color-tropical-500)}#picker-touch::after{content:"";position:absolute;top:50%;left:50%;width:12px;height:12px;border-radius:50%;transform:translate(-50%,-50%)}#header-hours,#header-minutes{position:absolute;padding:0 4px;width:50px;outline:0;--sinch-size-icon:24px}#header-hours{right:calc(50% + 8px);text-align:right}#header-minutes{left:calc(50% + 8px)}#header-hours::before,#header-minutes::before{content:"";display:none;position:absolute;border-radius:12px;left:50%;top:50%;width:100%;height:100%;transform:translate(-50%,-50%);padding:1px;border:1px solid var(--sinch-color-border-focus)}#header-hours-down,#header-hours-up,#header-minutes-down,#header-minutes-up{display:none;position:absolute;left:50%;transform:translateX(-50%)}#header-hours-up,#header-minutes-up{top:-18px}#header-hours-down,#header-minutes-down{bottom:-18px}#header-hours:focus #header-hours-down,#header-hours:focus #header-hours-up,#header-hours:focus::before{display:block}#header-minutes:focus #header-minutes-down,#header-minutes:focus #header-minutes-up,#header-minutes:focus::before{display:block}#header-colon{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}#submit{position:absolute;right:0;top:50%;transform:translateY(-50%)}:host([ampm]) .digit-hour-24{display:none}:host(:not([ampm])) #footer{display:none}</style><div id="wrapper"><div id="header"><div id="header-hours" tabindex="0" role="meter" aria-valuemin="0" aria-valuemax="23" aria-valuenow="0" aria-valuetext="0"><span>00</span><sinch-icon-arrow-drop-up id="header-hours-up"></sinch-icon-arrow-drop-up><sinch-icon-arrow-drop-down id="header-hours-down"></sinch-icon-arrow-drop-down></div><div id="header-colon">&colon;</div><div id="header-minutes" tabindex="0" role="meter" aria-valuemin="0" aria-valuemax="59" aria-valuenow="0" aria-valuetext="0"><span>00</span><sinch-icon-arrow-drop-up id="header-minutes-up"></sinch-icon-arrow-drop-up><sinch-icon-arrow-drop-down id="header-minutes-down"></sinch-icon-arrow-drop-down></div><sinch-icon-button id="submit" size="s" aria-label="Submit"><sinch-icon-done slot="icon"></sinch-icon-done></sinch-icon-button></div><div id="picker" aria-hidden="true"><div id="picker-hours"></div><div id="picker-minutes"></div><div id="picker-touch"><div id="needle-hour"></div><div id="needle-minute"></div></div></div><div id="footer"><sinch-segmented-control id="ampm"><sinch-segmented-control-option value="am" text="AM" aria-label="AM"></sinch-segmented-control-option><sinch-segmented-control-option value="pm" text="PM" aria-label="PM"></sinch-segmented-control-option></sinch-segmented-control></div></div>';
6
+ const templateHTML = '<style>:host{display:block;outline:0}#wrapper{display:flex;flex-direction:column;width:248px;padding:16px;box-sizing:border-box;gap:16px}#header{position:relative;width:100%;height:48px;font:var(--sinch-font-title-xl);line-height:48px;user-select:none}#footer{display:flex;justify-content:center;width:100%;height:32px}#picker{position:relative;width:216px;height:216px;border-radius:50%;box-sizing:border-box;border:1px solid var(--sinch-color-stormy-500)}#picker-hours,#picker-minutes{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:50%;pointer-events:none;user-select:none}.digit-hour-12,.digit-hour-24,.digit-minute{position:absolute;width:28px;height:28px;font:var(--sinch-font-text-s);line-height:28px;text-align:center;color:var(--sinch-color-text-default);top:calc(50% - 14px);left:calc(50% - 14px);z-index:1;cursor:pointer}.digit-hour-24{font:var(--sinch-font-text-xs);line-height:28px;color:var(--sinch-color-text-muted)}.digit-minute{font:var(--sinch-font-text-xs);line-height:28px;color:var(--sinch-color-text-muted)}.digit-hour-12:hover,.digit-hour-24:hover,.digit-minute:hover{color:var(--sinch-color-tropical-500)}.digit-hour-12.selected,.digit-hour-24.selected,.digit-minute.selected{color:var(--sinch-color-tropical-500)}.digit-hour-12.selected{font-size:16px}.digit-hour-24.selected{font-size:16px}.digit-minute.selected{font-size:16px}#picker-touch{position:absolute;left:0;top:0;width:100%;height:100%;cursor:pointer;border-radius:50%}#needle-hour,#needle-minute,#picker-touch::after{background-color:var(--sinch-color-stormy-500)}#needle-hour,#needle-minute{position:absolute;transform-origin:bottom center;transform:rotate(0);bottom:50%;height:50px;transition-duration:.25s;transition-timing-function:ease-in-out;transition-property:transform height;z-index:2;outline:0}@media (prefers-reduced-motion){#needle-hour,#needle-minute{transition:none}}#needle-hour{width:4px;left:calc(50% - 2px);border-radius:2px}#needle-minute{width:2px;left:calc(50% - 1px);border-radius:1px}#needle-hour:focus-visible,#needle-minute:focus-visible{background-color:var(--sinch-color-tropical-500)}#needle-minute:not(.selected)::after{content:"";position:absolute;transform:translateX(-50%);left:0;top:-16px;width:4px;height:4px;border-radius:50%;background-color:var(--sinch-color-tropical-500)}#picker-touch::after{content:"";position:absolute;top:50%;left:50%;width:12px;height:12px;border-radius:50%;transform:translate(-50%,-50%)}#header-hours,#header-minutes{position:absolute;padding:0 4px;width:50px;outline:0;--sinch-size-icon:24px}#header-hours{right:calc(50% + 8px);text-align:right}#header-minutes{left:calc(50% + 8px)}#header-colon{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}#submit{position:absolute;right:0;top:50%;transform:translateY(-50%)}:host([ampm]) .digit-hour-24{display:none}:host(:not([ampm])) #footer{display:none}</style><div id="wrapper"><div id="header"><div id="header-hours" role="meter" aria-valuemin="0" aria-valuemax="23" aria-valuenow="0" aria-valuetext="0"><span>00</span></div><div id="header-colon">&colon;</div><div id="header-minutes" role="meter" aria-valuemin="0" aria-valuemax="59" aria-valuenow="0" aria-valuetext="0"><span>00</span></div><sinch-icon-button id="submit" size="s" aria-label="Submit"><sinch-icon-done slot="icon"></sinch-icon-done></sinch-icon-button></div><div id="picker" aria-hidden="true"><div id="picker-hours"></div><div id="picker-minutes"></div><div id="picker-touch"><div id="needle-hour" tabindex="0"></div><div id="needle-minute" tabindex="0"></div></div></div><div id="footer"><sinch-segmented-control id="ampm"><sinch-segmented-control-option value="am" text="AM" aria-label="AM"></sinch-segmented-control-option><sinch-segmented-control-option value="pm" text="PM" aria-label="PM"></sinch-segmented-control-option></sinch-segmented-control></div></div>';
9
7
  import { getNeedleRotationDeg, getShortestCssDeg, hourToIndex, parseTime, stringifyHour, stringifyHourFace, stringifyMinute, stringifyTime } from './utils';
10
8
  const template = document.createElement('template');
11
9
  template.innerHTML = templateHTML;
@@ -33,6 +31,7 @@ defineCustomElement('sinch-time-picker', class extends NectaryElement {
33
31
  #$submitButton;
34
32
  #hour = 0;
35
33
  #minute = 0;
34
+ #controller = null;
36
35
  constructor() {
37
36
  super();
38
37
  const shadowRoot = this.attachShadow();
@@ -96,20 +95,19 @@ defineCustomElement('sinch-time-picker', class extends NectaryElement {
96
95
  this.#$pickerMinutes.appendChild(minutesFrag);
97
96
  }
98
97
  connectedCallback() {
99
- this.#$pickerTouch.addEventListener('click', this.#onPickerClick);
100
- this.#$ampm.addEventListener('change', this.#onAmPmChange);
101
- this.#$submitButton.addEventListener('click', this.#onSubmitButtonClick);
102
- this.#$headerHours.addEventListener('keydown', this.#onHoursKeydown);
103
- this.#$headerMinutes.addEventListener('keydown', this.#onMinutesKeydown);
104
- this.addEventListener('-change', this.#onChangeReactHandler);
98
+ this.#controller = new AbortController();
99
+ const options = {
100
+ signal: this.#controller.signal
101
+ };
102
+ this.#$pickerTouch.addEventListener('click', this.#onPickerClick, options);
103
+ this.#$ampm.addEventListener('change', this.#onAmPmChange, options);
104
+ this.#$submitButton.addEventListener('click', this.#onSubmitButtonClick, options);
105
+ this.#$needleHour.addEventListener('keydown', this.#onHoursKeydown, options);
106
+ this.#$needleMinute.addEventListener('keydown', this.#onMinutesKeydown, options);
107
+ this.addEventListener('-change', this.#onChangeReactHandler, options);
105
108
  }
106
109
  disconnectedCallback() {
107
- this.#$pickerTouch.removeEventListener('click', this.#onPickerClick);
108
- this.#$ampm.removeEventListener('change', this.#onAmPmChange);
109
- this.#$submitButton.removeEventListener('click', this.#onSubmitButtonClick);
110
- this.#$headerHours.removeEventListener('keydown', this.#onHoursKeydown);
111
- this.#$headerMinutes.removeEventListener('keydown', this.#onMinutesKeydown);
112
- this.removeEventListener('-change', this.#onChangeReactHandler);
110
+ this.#controller.abort();
113
111
  }
114
112
  static get observedAttributes() {
115
113
  return ['value', 'ampm', 'submit-aria-label'];
package/tooltip/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import '../text';
2
2
  import '../pop';
3
- import { defineCustomElement, getBooleanAttribute, getAttribute, getLiteralAttribute, updateBooleanAttribute, updateAttribute, updateLiteralAttribute, NectaryElement, setClass, rectOverlap } from '../utils';
3
+ import { defineCustomElement, getBooleanAttribute, getAttribute, getLiteralAttribute, updateBooleanAttribute, updateAttribute, updateLiteralAttribute, NectaryElement, setClass, rectOverlap, getReactEventHandler } 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';
@@ -49,6 +49,8 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
49
49
  signal: this.#controller.signal
50
50
  };
51
51
  this.#$pop.addEventListener('-close', this.#onPopClose, options);
52
+ this.addEventListener('-show', this.#onShowReactHandler, options);
53
+ this.addEventListener('-hide', this.#onHideReactHandler, options);
52
54
  updateAttribute(this.#$pop, 'orientation', getPopOrientation(this.orientation));
53
55
  this.#updateText();
54
56
  }
@@ -129,6 +131,7 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
129
131
  this.#unsubscribeScroll();
130
132
  };
131
133
  #onStateShow = () => {
134
+ this.#dispatchShowEvent();
132
135
  updateBooleanAttribute(this.#$pop, 'open', true);
133
136
  requestAnimationFrame(this.#updateTipOrientation);
134
137
  if (this.#animation !== null) {
@@ -152,6 +155,7 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
152
155
  this.#animation.finish();
153
156
  this.#resetTipOrientation();
154
157
  updateBooleanAttribute(this.#$pop, 'open', false);
158
+ this.#dispatchHideEvent();
155
159
  this.#unsubscribeMouseLeaveEvents();
156
160
  };
157
161
  #resetTipOrientation() {
@@ -232,4 +236,16 @@ defineCustomElement('sinch-tooltip', class extends NectaryElement {
232
236
  #isOpen() {
233
237
  return this.#$pop.hasAttribute('open');
234
238
  }
239
+ #dispatchShowEvent() {
240
+ this.dispatchEvent(new CustomEvent('-show'));
241
+ }
242
+ #dispatchHideEvent() {
243
+ this.dispatchEvent(new CustomEvent('-hide'));
244
+ }
245
+ #onShowReactHandler = () => {
246
+ getReactEventHandler(this, 'on-show')?.();
247
+ };
248
+ #onHideReactHandler = () => {
249
+ getReactEventHandler(this, 'on-hide')?.();
250
+ };
235
251
  });
@@ -11,6 +11,10 @@ export declare type TSinchTooltipElement = HTMLElement & {
11
11
  orientation: TSinchTooltipOrientation;
12
12
  readonly footprintRect: TRect;
13
13
  readonly tooltipRect: TRect;
14
+ /** Show event */
15
+ addEventListener(type: '-show', listener: (e: CustomEvent<void>) => void): void;
16
+ /** Hide event */
17
+ addEventListener(type: '-hide', listener: (e: CustomEvent<void>) => void): void;
14
18
  /** Text */
15
19
  setAttribute(name: 'text', value: string): void;
16
20
  /** @deprecated */
@@ -29,4 +33,8 @@ export declare type TSinchTooltipReact = TSinchElementReact<TSinchTooltipElement
29
33
  inverted?: boolean;
30
34
  /** Orientation, where it *points to* from origin */
31
35
  orientation?: TSinchTooltipOrientation;
36
+ /** Show event handler */
37
+ 'on-show'?: (e: CustomEvent<void>) => void;
38
+ /** Hide event handler */
39
+ 'on-hide'?: (e: CustomEvent<void>) => void;
32
40
  };