@nectary/components 1.1.2 → 1.2.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/card/index.js CHANGED
@@ -1,15 +1,21 @@
1
1
  import '../title';
2
2
  import '../text';
3
- import { defineCustomElement, getBooleanAttribute, getAttribute, updateBooleanAttribute, updateAttribute, setClass, NectaryElement } from '../utils';
4
- const templateHTML = '<style>:host{display:block;outline:0}#wrapper{border-radius:var(--sinch-shape-radius-l);border:1px solid var(--sinch-color-snow-700);background-color:var(--sinch-color-snow-100);box-shadow:var(--sinch-elevation-level-0);overflow:hidden}:host(:hover) #wrapper{box-shadow:var(--sinch-elevation-level-1)}#card-body{padding:24px;box-sizing:border-box;display:flex;flex-direction:column;gap:16px}#illustration-wrapper{display:none;overflow:hidden;height:240px}#illustration-wrapper.active{display:block}#label{color:var(--sinch-color-stormy-300)}#label:empty{display:none}#header{display:flex;flex-direction:row;align-items:center;gap:8px;--sinch-size-icon:48px}#caption{color:var(--sinch-color-stormy-500);flex:1;min-width:0}#description{color:var(--sinch-color-stormy-500);flex:1;min-height:0;max-height:120px;overflow-y:auto}#description:empty{display:none}:host([disabled]:not([disabled=false])) :is(#illustration-wrapper,#description,#header,#label){opacity:.6}slot[name=action]::slotted(*){margin-top:20px;align-self:flex-start;max-width:100%}</style><div id="wrapper"><div id="illustration-wrapper"><slot name="illustration"></slot></div><div id="card-body"><sinch-text id="label" type="s" emphasized ellipsis></sinch-text><div id="header"><slot name="icon"></slot><sinch-title id="caption" type="m" level="3" ellipsis></sinch-title></div><sinch-text id="description" type="m"></sinch-text><slot name="action"></slot></div></div>';
3
+ import { defineCustomElement, getBooleanAttribute, getAttribute, updateBooleanAttribute, updateAttribute, setClass, NectaryElement, isAttrTrue, getRect } from '../utils';
4
+ const templateHTML = '<style>:host{display:block;outline:0}#wrapper{border-radius:var(--sinch-shape-radius-l);border:1px solid var(--sinch-color-snow-700);background-color:var(--sinch-color-snow-100);box-shadow:var(--sinch-elevation-level-0);overflow:hidden}:host(:hover) #wrapper{box-shadow:var(--sinch-elevation-level-1)}#card-body{padding:24px;box-sizing:border-box;display:flex;flex-direction:column;gap:16px}#illustration-wrapper{display:none;overflow:hidden;height:240px}#illustration-wrapper.active{display:block}#label{color:var(--sinch-color-stormy-300)}#label:empty{display:none}#header{display:flex;flex-direction:row;align-items:center;gap:8px;--sinch-size-icon:48px}#caption{color:var(--sinch-color-stormy-500);flex:1;min-width:0}#description{color:var(--sinch-color-stormy-500);flex:1;min-height:0;max-height:120px;overflow-y:auto}#description:empty{display:none}:host([disabled]:not([disabled=false])) :is(#illustration-wrapper,#description,#header,#label){opacity:.6}slot[name=action]::slotted(*){margin-top:20px;align-self:flex-start;max-width:100%}:host([draggable=true]) #header{cursor:grab}</style><div id="wrapper"><div id="illustration-wrapper"><slot name="illustration"></slot></div><div id="card-body"><sinch-text id="label" type="s" emphasized ellipsis></sinch-text><div id="header"><slot name="icon"></slot><sinch-title id="caption" type="m" level="3" ellipsis></sinch-title></div><sinch-text id="description" type="m"></sinch-text><slot name="action"></slot></div></div>';
5
5
  const template = document.createElement('template');
6
6
  template.innerHTML = templateHTML;
7
7
  defineCustomElement('sinch-card', class extends NectaryElement {
8
8
  #$text;
9
9
  #$label;
10
10
  #$caption;
11
+ #$header;
12
+ #$cardBody;
13
+ #$iconSlot;
11
14
  #$illustrationSlot;
12
15
  #$illustrationSlotWrapper;
16
+ #isDraggingCorrectHandle = false;
17
+ #isDraggingSubscribed = false;
18
+ #controller = null;
13
19
  constructor() {
14
20
  super();
15
21
  const shadowRoot = this.attachShadow();
@@ -17,20 +23,36 @@ defineCustomElement('sinch-card', class extends NectaryElement {
17
23
  this.#$text = shadowRoot.querySelector('#description');
18
24
  this.#$label = shadowRoot.querySelector('#label');
19
25
  this.#$caption = shadowRoot.querySelector('#caption');
26
+ this.#$header = shadowRoot.querySelector('#header');
27
+ this.#$cardBody = shadowRoot.querySelector('#wrapper');
28
+ this.#$iconSlot = shadowRoot.querySelector('slot[name="icon"]');
20
29
  this.#$illustrationSlot = shadowRoot.querySelector('slot[name="illustration"]');
21
30
  this.#$illustrationSlotWrapper = shadowRoot.querySelector('#illustration-wrapper');
22
31
  }
23
32
  connectedCallback() {
24
- this.#$illustrationSlot.addEventListener('slotchange', this.#onIllustrationSlotChange);
33
+ super.connectedCallback();
34
+ this.#controller = new AbortController();
35
+ const options = {
36
+ signal: this.#controller.signal
37
+ };
38
+ this.#$illustrationSlot.addEventListener('slotchange', this.#onIllustrationSlotChange, options);
25
39
  this.#onIllustrationSlotChange();
40
+ if (getBooleanAttribute(this, 'draggable')) {
41
+ this.#enableDraggable();
42
+ }
26
43
  }
27
44
  disconnectedCallback() {
28
- this.#$illustrationSlot.removeEventListener('slotchange', this.#onIllustrationSlotChange);
45
+ super.disconnectedCallback();
46
+ this.#disableDragging();
47
+ this.#controller.abort();
29
48
  }
30
49
  static get observedAttributes() {
31
- return ['text', 'label', 'caption', 'disabled'];
50
+ return ['text', 'label', 'caption', 'disabled', 'draggable'];
32
51
  }
33
- attributeChangedCallback(name, _, newVal) {
52
+ attributeChangedCallback(name, prevVal, newVal) {
53
+ if (prevVal === newVal) {
54
+ return;
55
+ }
34
56
  switch (name) {
35
57
  case 'text':
36
58
  {
@@ -47,6 +69,22 @@ defineCustomElement('sinch-card', class extends NectaryElement {
47
69
  updateAttribute(this.#$caption, 'text', newVal);
48
70
  break;
49
71
  }
72
+ case 'disabled':
73
+ {
74
+ updateBooleanAttribute(this, 'disabled', isAttrTrue(newVal));
75
+ break;
76
+ }
77
+ case 'draggable':
78
+ {
79
+ const isDraggingEnabled = isAttrTrue(newVal);
80
+ if (isDraggingEnabled) {
81
+ this.#enableDraggable();
82
+ } else {
83
+ this.#disableDragging();
84
+ this.removeAttribute('draggable');
85
+ }
86
+ break;
87
+ }
50
88
  }
51
89
  }
52
90
  set text(value) {
@@ -73,7 +111,33 @@ defineCustomElement('sinch-card', class extends NectaryElement {
73
111
  get disabled() {
74
112
  return getBooleanAttribute(this, 'disabled');
75
113
  }
114
+ get dragRect() {
115
+ return this.#isDraggingSubscribed ? getRect(this.#$header) : null;
116
+ }
76
117
  #onIllustrationSlotChange = () => {
77
118
  setClass(this.#$illustrationSlotWrapper, 'active', this.#$illustrationSlot.assignedElements().length > 0);
78
119
  };
120
+ #enableDraggable() {
121
+ if (this.isConnected && !this.#isDraggingSubscribed) {
122
+ this.addEventListener('dragstart', this.#onDragStart);
123
+ this.#$cardBody.addEventListener('mousedown', this.#onDraggableMouseDown);
124
+ this.#isDraggingSubscribed = true;
125
+ }
126
+ }
127
+ #disableDragging() {
128
+ if (this.#isDraggingSubscribed) {
129
+ this.removeEventListener('dragstart', this.#onDragStart);
130
+ this.#$cardBody.removeEventListener('mousedown', this.#onDraggableMouseDown);
131
+ this.#isDraggingSubscribed = false;
132
+ }
133
+ }
134
+ #onDraggableMouseDown = e => {
135
+ this.#isDraggingCorrectHandle = this.#$header.contains(e.target) || this.#$iconSlot.assignedElements().includes(e.target);
136
+ };
137
+ #onDragStart = e => {
138
+ if (!this.#isDraggingCorrectHandle) {
139
+ e.preventDefault();
140
+ e.stopPropagation();
141
+ }
142
+ };
79
143
  });
package/card/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TSinchElementReact } from '../types';
1
+ import type { TRect, TSinchElementReact } from '../types';
2
2
  export type TSinchCardElement = HTMLElement & {
3
3
  /** Text */
4
4
  text: string;
@@ -8,6 +8,7 @@ export type TSinchCardElement = HTMLElement & {
8
8
  label: string | null;
9
9
  /** Disabled */
10
10
  disabled: boolean;
11
+ readonly dragRect: TRect | null;
11
12
  /** Text */
12
13
  setAttribute(name: 'text', value: string): void;
13
14
  /** Caption */
@@ -16,6 +17,8 @@ export type TSinchCardElement = HTMLElement & {
16
17
  setAttribute(name: 'label', value: string): void;
17
18
  /** Disabled */
18
19
  setAttribute(name: 'disabled', value: ''): void;
20
+ /** Draggable */
21
+ setAttribute(name: 'draggable', value: ''): void;
19
22
  };
20
23
  export type TSinchCardReact = TSinchElementReact<TSinchCardElement> & {
21
24
  /** Text */
@@ -26,4 +29,6 @@ export type TSinchCardReact = TSinchElementReact<TSinchCardElement> & {
26
29
  label?: string;
27
30
  /** Disabled */
28
31
  disabled?: boolean;
32
+ /** Draggable */
33
+ draggable?: boolean;
29
34
  };
@@ -365,7 +365,7 @@ defineCustomElement('sinch-date-picker', class extends NectaryElement {
365
365
  updateBooleanAttribute(this.#$nextMonth, 'disabled', canGoNextMonth(this.#uiDate, this.#maxDate) === false);
366
366
  updateBooleanAttribute(this.#$prevYear, 'disabled', canGoPrevYear(this.#uiDate, this.#minDate) === false);
367
367
  updateBooleanAttribute(this.#$nextYear, 'disabled', canGoNextYear(this.#uiDate, this.#maxDate) === false);
368
- this.#$date.textContent = `${this.#monthNames[this.#uiDate.getMonth()]} ${this.#uiDate.getFullYear()}`;
368
+ this.#$date.textContent = `${this.#monthNames[this.#uiDate.getUTCMonth()]} ${this.#uiDate.getUTCFullYear()}`;
369
369
  for (let wi = 0; wi < this.#$days.length; wi++) {
370
370
  const $week = this.#$days[wi];
371
371
  let isEmptyWeek = true;
@@ -383,7 +383,7 @@ defineCustomElement('sinch-date-picker', class extends NectaryElement {
383
383
  $day.setAttribute('aria-hidden', 'true');
384
384
  } else {
385
385
  const dayIso = dateToIso(day);
386
- $day.textContent = day.getDate().toString();
386
+ $day.textContent = day.getUTCDate().toString();
387
387
  $day.setAttribute('data-date', dayIso);
388
388
  if (isDateBetween(day, this.#minDate, this.#maxDate)) {
389
389
  $day.removeAttribute('disabled');
@@ -9,11 +9,11 @@ export const getCalendarMonth = (date, options) => {
9
9
  firstDayOfWeek: 1,
10
10
  ...options
11
11
  };
12
- const firstDateOfMonth = new Date(date.getFullYear(), date.getMonth(), 1);
13
- const lastDateOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0);
14
- const firstWeekdayOfMonth = firstDateOfMonth.getDay();
15
- const lastWeekdayOfMonth = lastDateOfMonth.getDay();
16
- const daysInMonth = lastDateOfMonth.getDate();
12
+ const firstDateOfMonth = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
13
+ const lastDateOfMonth = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0));
14
+ const firstWeekdayOfMonth = firstDateOfMonth.getUTCDay();
15
+ const lastWeekdayOfMonth = lastDateOfMonth.getUTCDay();
16
+ const daysInMonth = lastDateOfMonth.getUTCDate();
17
17
  const daysToPrepend = (firstWeekdayOfMonth - firstDayOfWeek + DAYS_IN_WEEK) % DAYS_IN_WEEK;
18
18
  const daysToAppend = (DAYS_IN_WEEK - 1 - lastWeekdayOfMonth + firstDayOfWeek) % DAYS_IN_WEEK;
19
19
  const month = [];
@@ -23,7 +23,7 @@ export const getCalendarMonth = (date, options) => {
23
23
  week.push(null);
24
24
  } else {
25
25
  const result = new Date(date);
26
- result.setDate(i);
26
+ result.setUTCDate(i);
27
27
  week.push(result);
28
28
  }
29
29
  if (week.length === 7) {
@@ -34,13 +34,14 @@ export const getCalendarMonth = (date, options) => {
34
34
  return month;
35
35
  };
36
36
  export const dateToIso = date => {
37
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
37
+ return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
38
38
  };
39
39
  export const isoToDate = value => {
40
- return new Date(`${value.substring(0, 10)}T00:00:00`);
40
+ return new Date(`${value.substring(0, 10)}T00:00:00Z`);
41
41
  };
42
42
  export const today = () => {
43
- return isoToDate(dateToIso(new Date()));
43
+ const today = new Date();
44
+ return new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
44
45
  };
45
46
  export const getDayNames = locale => {
46
47
  const formatter = new Intl.DateTimeFormat(locale, {
@@ -99,35 +100,35 @@ export const clampMaxDate = (date, max) => {
99
100
  }
100
101
  };
101
102
  export const incMonth = (date, max) => {
102
- date.setMonth(date.getMonth() + 1);
103
+ date.setUTCMonth(date.getUTCMonth() + 1);
103
104
  clampMaxDate(date, max);
104
105
  };
105
106
  export const decMonth = (date, min) => {
106
- date.setMonth(date.getMonth() - 1);
107
+ date.setUTCMonth(date.getUTCMonth() - 1);
107
108
  clampMinDate(date, min);
108
109
  };
109
110
  export const incYear = (date, max) => {
110
- date.setFullYear(date.getFullYear() + 1);
111
+ date.setUTCFullYear(date.getUTCFullYear() + 1);
111
112
  clampMaxDate(date, max);
112
113
  };
113
114
  export const decYear = (date, min) => {
114
- date.setFullYear(date.getFullYear() - 1);
115
+ date.setUTCFullYear(date.getUTCFullYear() - 1);
115
116
  clampMinDate(date, min);
116
117
  };
117
118
  export const canGoPrevMonth = (date, min) => {
118
- const prevMonth = new Date(Date.UTC(date.getFullYear(), date.getMonth(), 0));
119
+ const prevMonth = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 0));
119
120
  return compareDates(prevMonth, min) >= 0;
120
121
  };
121
122
  export const canGoNextMonth = (date, max) => {
122
- const nextMonth = new Date(Date.UTC(date.getFullYear(), date.getMonth() + 1, 1));
123
+ const nextMonth = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1));
123
124
  return compareDates(max, nextMonth) >= 0;
124
125
  };
125
126
  export const canGoNextYear = (date, max) => {
126
- const nextYear = new Date(Date.UTC(date.getFullYear() + 1, 0, 1));
127
+ const nextYear = new Date(Date.UTC(date.getUTCFullYear() + 1, 0, 1));
127
128
  return compareDates(max, nextYear) >= 0;
128
129
  };
129
130
  export const canGoPrevYear = (date, min) => {
130
- const prevYear = new Date(Date.UTC(date.getFullYear(), 0, 0));
131
+ const prevYear = new Date(Date.UTC(date.getUTCFullYear(), 0, 0));
131
132
  return compareDates(prevYear, min) >= 0;
132
133
  };
133
134
  export const isDateBetween = (date, min, max) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
package/pop/index.js CHANGED
@@ -13,7 +13,7 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
13
13
  #$contentSlot;
14
14
  #$targetOpenWrapper;
15
15
  #targetActiveElement = null;
16
- #controller = null;
16
+ #controller;
17
17
  #keydownContext;
18
18
  #visibilityContext;
19
19
  #targetStyleValue = null;
@@ -31,10 +31,13 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
31
31
  this.#resizeThrottle = throttleAnimationFrame(this.#updateOrientation);
32
32
  this.#keydownContext = new Context(this.#$contentSlot, 'keydown');
33
33
  this.#visibilityContext = new Context(this.#$contentSlot, 'visibility');
34
+ this.#controller = new AbortController();
34
35
  }
35
36
  connectedCallback() {
36
37
  super.connectedCallback();
37
- this.#controller = new AbortController();
38
+ if (this.#controller === null) {
39
+ this.#controller = new AbortController();
40
+ }
38
41
  const {
39
42
  signal
40
43
  } = this.#controller;
@@ -58,6 +61,8 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
58
61
  disconnectedCallback() {
59
62
  super.disconnectedCallback();
60
63
  this.#controller.abort();
64
+ this.#controller = null;
65
+ this.#resizeThrottle.cancel();
61
66
  this.#onCollapse();
62
67
  }
63
68
  static get observedAttributes() {
@@ -102,7 +107,9 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
102
107
  {
103
108
  if (isAttrTrue(newVal)) {
104
109
  requestAnimationFrame(() => {
105
- this.#onExpand();
110
+ if (this.isConnected && getBooleanAttribute(this, 'open')) {
111
+ this.#onExpand();
112
+ }
106
113
  });
107
114
  } else {
108
115
  this.#onCollapse();
@@ -186,9 +193,11 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
186
193
  this.#$targetOpenSlot.removeEventListener('focus', this.#stopEventPropagation, true);
187
194
  if (!isElementFocused(this.#targetActiveElement)) {
188
195
  requestAnimationFrame(() => {
189
- this.#$targetOpenSlot.addEventListener('focus', this.#stopEventPropagation, true);
190
- this.#targetActiveElement.focus();
191
- this.#$targetOpenSlot.removeEventListener('focus', this.#stopEventPropagation, true);
196
+ if (this.isConnected && this.#isOpen()) {
197
+ this.#$targetOpenSlot.addEventListener('focus', this.#stopEventPropagation, true);
198
+ this.#targetActiveElement.focus();
199
+ this.#$targetOpenSlot.removeEventListener('focus', this.#stopEventPropagation, true);
200
+ }
192
201
  });
193
202
  }
194
203
  }
@@ -196,7 +205,7 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
196
205
  disableScroll();
197
206
  window.addEventListener('resize', this.#onResize);
198
207
  requestAnimationFrame(() => {
199
- if (this.#isOpen()) {
208
+ if (this.isConnected && this.#isOpen()) {
200
209
  this.#$contentSlot.addEventListener('slotchange', this.#onContentSlotChange);
201
210
  }
202
211
  });
@@ -238,9 +247,11 @@ defineCustomElement('sinch-pop', class extends NectaryElement {
238
247
  if (!isElementFocused(this.#targetActiveElement)) {
239
248
  const $targetEl = this.#targetActiveElement;
240
249
  requestAnimationFrame(() => {
241
- this.#$targetSlot.addEventListener('focus', this.#stopEventPropagation, true);
242
- $targetEl.focus();
243
- this.#$targetSlot.removeEventListener('focus', this.#stopEventPropagation, true);
250
+ if (this.isConnected && !this.#isOpen()) {
251
+ this.#$targetSlot.addEventListener('focus', this.#stopEventPropagation, true);
252
+ $targetEl.focus();
253
+ this.#$targetSlot.removeEventListener('focus', this.#stopEventPropagation, true);
254
+ }
244
255
  });
245
256
  }
246
257
  this.#targetActiveElement = null;