tyrell-components 1.0.0-TC52 → 1.0.0-TC53

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.
Files changed (105) hide show
  1. package/css/tyrell-theme.css +25 -2
  2. package/css/tyrell.css +4 -0
  3. package/dist/tyrell-colors-static.css +1 -316
  4. package/dist/tyrell-theme.css +1 -1443
  5. package/dist/tyrell.css +1 -1390
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.js +196 -196
  8. package/lib/components/button.d.ts +1 -1
  9. package/lib/components/button.js +260 -259
  10. package/lib/components/calendar-month.d.ts +2 -2
  11. package/lib/components/calendar-month.js +426 -416
  12. package/lib/components/calendar-navigation.js +238 -238
  13. package/lib/components/calendar.js +605 -602
  14. package/lib/components/checkbox.d.ts +1 -1
  15. package/lib/components/checkbox.js +273 -272
  16. package/lib/components/copy.d.ts +1 -1
  17. package/lib/components/copy.js +279 -278
  18. package/lib/components/date-picker.d.ts +1 -1
  19. package/lib/components/date-picker.js +1103 -1108
  20. package/lib/components/file-upload.js +404 -404
  21. package/lib/components/icon.js +141 -141
  22. package/lib/components/input.d.ts +1 -1
  23. package/lib/components/input.js +593 -596
  24. package/lib/components/modal.js +319 -319
  25. package/lib/components/option.js +167 -167
  26. package/lib/components/popup.js +233 -233
  27. package/lib/components/radio.d.ts +2 -2
  28. package/lib/components/radio.js +407 -406
  29. package/lib/components/resize-observer.js +67 -67
  30. package/lib/components/scroll-container.js +294 -294
  31. package/lib/components/select.d.ts +1 -1
  32. package/lib/components/select.js +1524 -1532
  33. package/lib/components/selected-tags.js +184 -184
  34. package/lib/components/step.js +62 -62
  35. package/lib/components/switch.d.ts +1 -1
  36. package/lib/components/switch.js +214 -213
  37. package/lib/components/tab.js +62 -62
  38. package/lib/components/tabs.js +507 -507
  39. package/lib/components/tag.d.ts +1 -1
  40. package/lib/components/tag.js +236 -235
  41. package/lib/components/textarea.d.ts +1 -1
  42. package/lib/components/textarea.js +457 -456
  43. package/lib/components/tooltip.js +283 -283
  44. package/lib/components/wizard.js +384 -384
  45. package/lib/icons/heroicons/micro.js +392 -392
  46. package/lib/icons/heroicons/mini.js +382 -382
  47. package/lib/icons/heroicons/outline.js +337 -337
  48. package/lib/icons/heroicons/solid.js +407 -407
  49. package/lib/icons/lucide.js +22843 -22843
  50. package/lib/icons/material/filled.js +9653 -9653
  51. package/lib/icons/material/outlined.js +10271 -10271
  52. package/lib/icons/material/round.js +10495 -10495
  53. package/lib/icons/material/sharp.js +9905 -9905
  54. package/lib/icons/material/two-tone.js +12792 -12792
  55. package/lib/index.js +21 -21
  56. package/lib/styles/button.js +176 -288
  57. package/lib/styles/calendar-month.js +123 -183
  58. package/lib/styles/calendar-navigation.js +60 -79
  59. package/lib/styles/calendar.js +8 -9
  60. package/lib/styles/checkbox.js +45 -72
  61. package/lib/styles/copy.js +67 -91
  62. package/lib/styles/custom-scrollbar.js +80 -100
  63. package/lib/styles/date-picker.js +261 -331
  64. package/lib/styles/field-size.d.ts +6 -0
  65. package/lib/styles/field-size.js +91 -0
  66. package/lib/styles/file-upload.js +139 -169
  67. package/lib/styles/icon.js +92 -156
  68. package/lib/styles/input.js +125 -241
  69. package/lib/styles/modal.js +67 -102
  70. package/lib/styles/option.d.ts +1 -1
  71. package/lib/styles/option.js +47 -67
  72. package/lib/styles/popup.js +34 -65
  73. package/lib/styles/radio.js +69 -96
  74. package/lib/styles/resize-observer.js +2 -5
  75. package/lib/styles/scroll-container.js +100 -130
  76. package/lib/styles/select-base.js +525 -778
  77. package/lib/styles/select.js +140 -228
  78. package/lib/styles/step.js +22 -29
  79. package/lib/styles/switch.js +34 -55
  80. package/lib/styles/tab.js +22 -29
  81. package/lib/styles/tabs.js +171 -252
  82. package/lib/styles/tag.js +135 -188
  83. package/lib/styles/textarea.d.ts +1 -1
  84. package/lib/styles/textarea.js +205 -271
  85. package/lib/styles/tooltip.js +3 -3
  86. package/lib/styles/wizard.js +170 -226
  87. package/lib/types/common.d.ts +2 -0
  88. package/lib/types/common.js +6 -5
  89. package/lib/utils/calendar-utils.js +100 -100
  90. package/lib/utils/custom-scrollbar.js +272 -272
  91. package/lib/utils/flavor-sheet.js +14 -14
  92. package/lib/utils/icon-registry.js +127 -127
  93. package/lib/utils/loader-registry.js +3 -3
  94. package/lib/utils/locale.js +34 -34
  95. package/lib/utils/mobile.js +1 -1
  96. package/lib/utils/number-format.js +54 -54
  97. package/lib/utils/positioning.js +212 -212
  98. package/lib/utils/property-capture.js +36 -36
  99. package/lib/utils/property-manager.js +125 -125
  100. package/lib/utils/resize-observer.js +23 -23
  101. package/lib/utils/scroll-lock.js +57 -57
  102. package/lib/utils/styles.js +17 -17
  103. package/lib/version.d.ts +1 -1
  104. package/lib/version.js +1 -1
  105. package/package.json +4 -4
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_SIZE } from '../types/common.js';
1
2
  import { TyComponent } from '../base/ty-component.js';
2
3
  import { ensureStyles } from '../utils/styles.js';
3
4
  import { textareaStyles } from '../styles/textarea.js';
@@ -5,465 +6,465 @@ import { CustomScrollbar, isCustomScrollbarEnabled } from '../utils/custom-scrol
5
6
  import { customScrollbarStyles } from '../styles/custom-scrollbar.js';
6
7
  const REQUIRED_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`;
7
8
  export class TyTextarea extends TyComponent {
8
- constructor() {
9
- super();
10
- this._textareaEl = null;
11
- this._dummyEl = null;
12
- this._scrollbar = null;
13
- this._isFocused = false;
14
- this.handleInputEvent = (e) => {
15
- const target = e.target;
16
- const newValue = target.value;
17
- this.value = newValue;
18
- if (this._textareaEl && this._dummyEl) {
19
- this.resizeTextarea(this._textareaEl, this._dummyEl);
20
- }
21
- this.emitValueEvents(e);
22
- };
23
- this.handleFocusEvent = () => {
24
- this._isFocused = true;
25
- };
26
- this.handleBlurEvent = () => {
27
- this._isFocused = false;
28
- };
29
- const shadow = this.shadowRoot;
30
- ensureStyles(shadow, { css: textareaStyles, id: 'ty-textarea' });
31
- if (isCustomScrollbarEnabled()) {
32
- ensureStyles(shadow, { css: customScrollbarStyles, id: 'ty-custom-scrollbar' });
33
- }
34
- }
35
- onConnect() {
36
- }
37
- onDisconnect() {
38
- this._destroyScrollbar();
39
- if (this._textareaEl) {
40
- const newTextarea = this._textareaEl.cloneNode(true);
41
- this._textareaEl.parentNode?.replaceChild(newTextarea, this._textareaEl);
42
- this._textareaEl = null;
43
- }
44
- this._dummyEl = null;
45
- }
46
- onPropertiesChanged(changes) {
47
- }
48
- getFormValue() {
49
- return this.getProperty('value') || null;
50
- }
51
- get value() { return this.getProperty('value'); }
52
- set value(v) { this.setProperty('value', v); }
53
- get name() { return this.getProperty('name'); }
54
- set name(v) { this.setProperty('name', v); }
55
- get placeholder() { return this.getProperty('placeholder'); }
56
- set placeholder(v) { this.setProperty('placeholder', v); }
57
- get label() { return this.getProperty('label'); }
58
- set label(v) { this.setProperty('label', v); }
59
- get disabled() { return this.getProperty('disabled'); }
60
- set disabled(v) { this.setProperty('disabled', v); }
61
- get required() { return this.getProperty('required'); }
62
- set required(v) { this.setProperty('required', v); }
63
- get error() { return this.getProperty('error'); }
64
- set error(v) { this.setProperty('error', v); }
65
- get size() {
66
- return this.getProperty('size');
67
- }
68
- set size(v) {
69
- this.setProperty('size', v);
70
- }
71
- get rows() { return this.getProperty('rows'); }
72
- set rows(v) { this.setProperty('rows', v); }
73
- get cols() { return this.getProperty('cols'); }
74
- set cols(v) { this.setProperty('cols', v); }
75
- get resize() {
76
- return this.getProperty('resize');
77
- }
78
- set resize(v) {
79
- this.setProperty('resize', v);
80
- }
81
- get minHeight() { return this.getProperty('minHeight'); }
82
- set minHeight(v) { this.setProperty('minHeight', v); }
83
- get maxHeight() { return this.getProperty('maxHeight'); }
84
- set maxHeight(v) { this.setProperty('maxHeight', v); }
85
- getFontStyle(el) {
86
- const computedStyle = window.getComputedStyle(el);
87
- return {
88
- fontFamily: computedStyle.fontFamily,
89
- fontSize: computedStyle.fontSize,
90
- fontWeight: computedStyle.fontWeight,
91
- lineHeight: computedStyle.lineHeight,
92
- letterSpacing: computedStyle.letterSpacing,
93
- fontStyle: computedStyle.fontStyle
94
- };
95
- }
96
- getSpacingStyle(el) {
97
- const computedStyle = window.getComputedStyle(el);
98
- return {
99
- padding: computedStyle.padding,
100
- paddingTop: computedStyle.paddingTop,
101
- paddingRight: computedStyle.paddingRight,
102
- paddingBottom: computedStyle.paddingBottom,
103
- paddingLeft: computedStyle.paddingLeft,
104
- margin: computedStyle.margin,
105
- borderWidth: computedStyle.borderWidth
106
- };
107
- }
108
- setupDummyElement(textareaEl) {
109
- const root = this.shadowRoot;
110
- const existingDummy = root.querySelector('.textarea-dummy');
111
- if (existingDummy) {
112
- existingDummy.remove();
113
- }
114
- const dummyEl = document.createElement('pre');
115
- const fontStyle = this.getFontStyle(textareaEl);
116
- const spacingStyle = this.getSpacingStyle(textareaEl);
117
- dummyEl.className = 'textarea-dummy';
118
- const style = dummyEl.style;
119
- style.position = 'absolute';
120
- style.top = '0';
121
- style.left = '0';
122
- style.visibility = 'hidden';
123
- style.whiteSpace = 'pre-wrap';
124
- style.wordBreak = 'break-word';
125
- style.boxSizing = 'border-box';
126
- style.overflow = 'hidden';
127
- style.pointerEvents = 'none';
128
- Object.entries(fontStyle).forEach(([prop, value]) => {
129
- ;
130
- style[prop] = value;
131
- });
132
- Object.entries(spacingStyle).forEach(([prop, value]) => {
133
- ;
134
- style[prop] = value;
135
- });
136
- root.appendChild(dummyEl);
137
- this._dummyEl = dummyEl;
138
- return dummyEl;
139
- }
140
- resizeTextarea(textareaEl, dummyEl) {
141
- if (!textareaEl || !dummyEl)
142
- return;
143
- const value = this.value;
144
- const placeholder = this.placeholder;
145
- const content = (value === '' && placeholder) ? placeholder : value + ' ';
146
- const currentHeight = textareaEl.clientHeight;
147
- dummyEl.textContent = content;
148
- dummyEl.style.width = `${textareaEl.clientWidth}px`;
149
- const measuredHeight = dummyEl.scrollHeight;
150
- const minHeight = this.minHeight ? parseInt(this.minHeight.replace('px', ''), 10) : null;
151
- const maxHeight = this.maxHeight ? parseInt(this.maxHeight.replace('px', ''), 10) : null;
152
- let constrainedHeight = measuredHeight;
153
- if (minHeight !== null)
154
- constrainedHeight = Math.max(constrainedHeight, minHeight);
155
- if (maxHeight !== null)
156
- constrainedHeight = Math.min(constrainedHeight, maxHeight);
157
- if (Math.abs(constrainedHeight - currentHeight) > 2) {
158
- textareaEl.style.height = `${constrainedHeight}px`;
159
- const needsScroll = maxHeight !== null && measuredHeight >= maxHeight;
160
- if (needsScroll) {
161
- textareaEl.style.overflowY = 'auto';
162
- this._setupScrollbar(textareaEl);
163
- }
164
- else {
165
- textareaEl.style.overflowY = 'hidden';
166
- this._destroyScrollbar();
167
- }
168
- }
169
- this._scrollbar?.update();
170
- }
171
- _setupScrollbar(textareaEl) {
172
- if (this._scrollbar || !isCustomScrollbarEnabled())
173
- return;
174
- textareaEl.style.scrollbarWidth = 'none';
175
- this.setAttribute('data-custom-scroll', '');
176
- this._scrollbar = new CustomScrollbar(textareaEl, {
177
- vertical: true,
178
- horizontal: false
179
- });
180
- const scroll = this.shadowRoot.querySelector('.textarea-scroll');
181
- if (scroll && this._scrollbar.trackY) {
182
- scroll.appendChild(this._scrollbar.trackY);
183
- }
184
- }
185
- _destroyScrollbar() {
186
- if (!this._scrollbar)
187
- return;
188
- this._scrollbar.trackY?.remove();
189
- this._scrollbar.destroy();
190
- this._scrollbar = null;
191
- this.removeAttribute('data-custom-scroll');
192
- if (this._textareaEl) {
193
- this._textareaEl.style.scrollbarWidth = '';
194
- }
195
- }
196
- emitValueEvents(originalEvent) {
197
- const value = this.value;
198
- this._internals.setFormValue(value || '');
199
- this.updateValidity();
200
- const data = {
201
- bubbles: true,
202
- composed: true,
203
- detail: {
204
- value,
205
- originalEvent
206
- }
207
- };
208
- setTimeout(() => {
209
- this.dispatchEvent(new CustomEvent('input', data));
210
- this.dispatchEvent(new CustomEvent('change', data));
211
- }, 0);
212
- }
213
- buildClassList() {
214
- const classes = [this.size];
215
- if (this.disabled)
216
- classes.push('disabled');
217
- if (this.required)
218
- classes.push('required');
219
- if (this.error)
220
- classes.push('error');
221
- if (this.resize !== 'none')
222
- classes.push(`resize-${this.resize}`);
223
- const customClass = this.getAttribute('class');
224
- if (customClass)
225
- classes.push(customClass);
226
- return classes.join(' ');
227
- }
228
- applyHeightConstraints(textareaEl) {
229
- if (this.minHeight) {
230
- textareaEl.style.minHeight = this.minHeight;
231
- }
232
- if (this.maxHeight) {
233
- textareaEl.style.maxHeight = this.maxHeight;
234
- }
235
- }
236
- setupTextareaEvents(textareaEl) {
237
- textareaEl.addEventListener('input', this.handleInputEvent);
238
- textareaEl.addEventListener('change', this.handleInputEvent);
239
- textareaEl.addEventListener('focus', this.handleFocusEvent);
240
- textareaEl.addEventListener('blur', this.handleBlurEvent);
241
- }
242
- render() {
243
- const root = this.shadowRoot;
244
- const existingContainer = root.querySelector('.textarea-container');
245
- const existingLabel = root.querySelector('label');
246
- const existingTextarea = root.querySelector('textarea');
247
- const existingError = root.querySelector('.error-message');
248
- if (existingContainer && existingTextarea) {
249
- if (this.label) {
250
- if (existingLabel) {
251
- existingLabel.innerHTML = this.label +
252
- (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
253
- existingLabel.style.display = 'block';
254
- }
255
- }
256
- else if (existingLabel) {
257
- existingLabel.style.display = 'none';
258
- }
259
- existingTextarea.value = this.value || '';
260
- existingTextarea.placeholder = this.placeholder || '';
261
- existingTextarea.disabled = this.disabled;
262
- existingTextarea.required = this.required;
263
- existingTextarea.className = this.buildClassList();
264
- if (this.label)
265
- existingTextarea.setAttribute('aria-label', this.label);
266
- else
267
- existingTextarea.removeAttribute('aria-label');
268
- if (this.name) {
269
- existingTextarea.setAttribute('name', this.name);
270
- }
271
- else {
272
- existingTextarea.removeAttribute('name');
273
- }
274
- if (this.rows)
275
- existingTextarea.setAttribute('rows', this.rows);
276
- if (this.cols)
277
- existingTextarea.setAttribute('cols', this.cols);
278
- this.applyHeightConstraints(existingTextarea);
279
- if (this.error) {
280
- if (!existingError) {
281
- const errorEl = document.createElement('div');
282
- errorEl.className = 'error-message';
283
- existingContainer.appendChild(errorEl);
284
- }
285
- const errorEl = root.querySelector('.error-message');
286
- if (errorEl)
287
- errorEl.textContent = this.error;
288
- }
289
- else if (existingError) {
290
- existingError.remove();
291
- }
292
- const dummyEl = this.setupDummyElement(existingTextarea);
293
- setTimeout(() => this.resizeTextarea(existingTextarea, dummyEl), 0);
294
- this._textareaEl = existingTextarea;
295
- }
296
- else {
297
- const container = document.createElement('div');
298
- container.className = 'textarea-container';
299
- const labelEl = document.createElement('label');
300
- labelEl.className = 'ty-field-label';
301
- if (this.label) {
302
- labelEl.innerHTML = this.label +
303
- (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
304
- labelEl.style.display = 'block';
305
- }
306
- else {
307
- labelEl.style.display = 'none';
308
- }
309
- const textareaEl = document.createElement('textarea');
310
- textareaEl.value = this.value || '';
311
- textareaEl.placeholder = this.placeholder || '';
312
- textareaEl.disabled = this.disabled;
313
- textareaEl.required = this.required;
314
- textareaEl.className = this.buildClassList();
315
- if (this.label)
316
- textareaEl.setAttribute('aria-label', this.label);
317
- if (this.name) {
318
- textareaEl.setAttribute('name', this.name);
319
- }
320
- textareaEl.setAttribute('rows', this.rows || '3');
321
- if (this.cols)
322
- textareaEl.setAttribute('cols', this.cols);
323
- this.applyHeightConstraints(textareaEl);
324
- this.setupTextareaEvents(textareaEl);
325
- const wrapperEl = document.createElement('div');
326
- wrapperEl.className = 'textarea-wrapper';
327
- const headerEl = document.createElement('div');
328
- headerEl.className = 'textarea-header';
329
- const headerSlot = document.createElement('slot');
330
- headerSlot.setAttribute('name', 'header');
331
- headerEl.appendChild(headerSlot);
332
- const scrollEl = document.createElement('div');
333
- scrollEl.className = 'textarea-scroll';
334
- scrollEl.appendChild(textareaEl);
335
- const footerEl = document.createElement('div');
336
- footerEl.className = 'textarea-footer';
337
- const footerSlot = document.createElement('slot');
338
- footerSlot.setAttribute('name', 'footer');
339
- footerEl.appendChild(footerSlot);
340
- wrapperEl.appendChild(headerEl);
341
- wrapperEl.appendChild(scrollEl);
342
- wrapperEl.appendChild(footerEl);
343
- const syncSlot = (slot, region) => {
344
- const update = () => region.classList.toggle('has-content', slot.assignedNodes({ flatten: true }).length > 0);
345
- slot.addEventListener('slotchange', update);
346
- update();
347
- };
348
- syncSlot(headerSlot, headerEl);
349
- syncSlot(footerSlot, footerEl);
350
- container.appendChild(labelEl);
351
- container.appendChild(wrapperEl);
352
- if (this.error) {
353
- const errorEl = document.createElement('div');
354
- errorEl.className = 'error-message';
355
- errorEl.textContent = this.error;
356
- container.appendChild(errorEl);
357
- }
358
- root.appendChild(container);
359
- const dummyEl = this.setupDummyElement(textareaEl);
360
- setTimeout(() => this.resizeTextarea(textareaEl, dummyEl), 0);
361
- this._textareaEl = textareaEl;
362
- }
363
- this.updateValidity();
364
- }
365
- updateValidity() {
366
- const ta = this.shadowRoot?.querySelector('textarea');
367
- if (this.disabled) {
368
- this._internals.setValidity({});
369
- return;
370
- }
371
- const current = ta ? ta.value : String(this.value ?? '');
372
- if (this.required && current.trim() === '') {
373
- this._internals.setValidity({ valueMissing: true }, 'Please fill out this field', ta ?? undefined);
374
- }
375
- else {
376
- this._internals.setValidity({});
377
- }
378
- }
9
+ constructor() {
10
+ super();
11
+ this._textareaEl = null;
12
+ this._dummyEl = null;
13
+ this._scrollbar = null;
14
+ this._isFocused = false;
15
+ this.handleInputEvent = (e) => {
16
+ const target = e.target;
17
+ const newValue = target.value;
18
+ this.value = newValue;
19
+ if (this._textareaEl && this._dummyEl) {
20
+ this.resizeTextarea(this._textareaEl, this._dummyEl);
21
+ }
22
+ this.emitValueEvents(e);
23
+ };
24
+ this.handleFocusEvent = () => {
25
+ this._isFocused = true;
26
+ };
27
+ this.handleBlurEvent = () => {
28
+ this._isFocused = false;
29
+ };
30
+ const shadow = this.shadowRoot;
31
+ ensureStyles(shadow, { css: textareaStyles, id: 'ty-textarea' });
32
+ if (isCustomScrollbarEnabled()) {
33
+ ensureStyles(shadow, { css: customScrollbarStyles, id: 'ty-custom-scrollbar' });
34
+ }
35
+ }
36
+ onConnect() {
37
+ }
38
+ onDisconnect() {
39
+ this._destroyScrollbar();
40
+ if (this._textareaEl) {
41
+ const newTextarea = this._textareaEl.cloneNode(true);
42
+ this._textareaEl.parentNode?.replaceChild(newTextarea, this._textareaEl);
43
+ this._textareaEl = null;
44
+ }
45
+ this._dummyEl = null;
46
+ }
47
+ onPropertiesChanged(changes) {
48
+ }
49
+ getFormValue() {
50
+ return this.getProperty('value') || null;
51
+ }
52
+ get value() { return this.getProperty('value'); }
53
+ set value(v) { this.setProperty('value', v); }
54
+ get name() { return this.getProperty('name'); }
55
+ set name(v) { this.setProperty('name', v); }
56
+ get placeholder() { return this.getProperty('placeholder'); }
57
+ set placeholder(v) { this.setProperty('placeholder', v); }
58
+ get label() { return this.getProperty('label'); }
59
+ set label(v) { this.setProperty('label', v); }
60
+ get disabled() { return this.getProperty('disabled'); }
61
+ set disabled(v) { this.setProperty('disabled', v); }
62
+ get required() { return this.getProperty('required'); }
63
+ set required(v) { this.setProperty('required', v); }
64
+ get error() { return this.getProperty('error'); }
65
+ set error(v) { this.setProperty('error', v); }
66
+ get size() {
67
+ return this.getProperty('size');
68
+ }
69
+ set size(v) {
70
+ this.setProperty('size', v);
71
+ }
72
+ get rows() { return this.getProperty('rows'); }
73
+ set rows(v) { this.setProperty('rows', v); }
74
+ get cols() { return this.getProperty('cols'); }
75
+ set cols(v) { this.setProperty('cols', v); }
76
+ get resize() {
77
+ return this.getProperty('resize');
78
+ }
79
+ set resize(v) {
80
+ this.setProperty('resize', v);
81
+ }
82
+ get minHeight() { return this.getProperty('minHeight'); }
83
+ set minHeight(v) { this.setProperty('minHeight', v); }
84
+ get maxHeight() { return this.getProperty('maxHeight'); }
85
+ set maxHeight(v) { this.setProperty('maxHeight', v); }
86
+ getFontStyle(el) {
87
+ const computedStyle = window.getComputedStyle(el);
88
+ return {
89
+ fontFamily: computedStyle.fontFamily,
90
+ fontSize: computedStyle.fontSize,
91
+ fontWeight: computedStyle.fontWeight,
92
+ lineHeight: computedStyle.lineHeight,
93
+ letterSpacing: computedStyle.letterSpacing,
94
+ fontStyle: computedStyle.fontStyle
95
+ };
96
+ }
97
+ getSpacingStyle(el) {
98
+ const computedStyle = window.getComputedStyle(el);
99
+ return {
100
+ padding: computedStyle.padding,
101
+ paddingTop: computedStyle.paddingTop,
102
+ paddingRight: computedStyle.paddingRight,
103
+ paddingBottom: computedStyle.paddingBottom,
104
+ paddingLeft: computedStyle.paddingLeft,
105
+ margin: computedStyle.margin,
106
+ borderWidth: computedStyle.borderWidth
107
+ };
108
+ }
109
+ setupDummyElement(textareaEl) {
110
+ const root = this.shadowRoot;
111
+ const existingDummy = root.querySelector('.textarea-dummy');
112
+ if (existingDummy) {
113
+ existingDummy.remove();
114
+ }
115
+ const dummyEl = document.createElement('pre');
116
+ const fontStyle = this.getFontStyle(textareaEl);
117
+ const spacingStyle = this.getSpacingStyle(textareaEl);
118
+ dummyEl.className = 'textarea-dummy';
119
+ const style = dummyEl.style;
120
+ style.position = 'absolute';
121
+ style.top = '0';
122
+ style.left = '0';
123
+ style.visibility = 'hidden';
124
+ style.whiteSpace = 'pre-wrap';
125
+ style.wordBreak = 'break-word';
126
+ style.boxSizing = 'border-box';
127
+ style.overflow = 'hidden';
128
+ style.pointerEvents = 'none';
129
+ Object.entries(fontStyle).forEach(([prop, value]) => {
130
+ ;
131
+ style[prop] = value;
132
+ });
133
+ Object.entries(spacingStyle).forEach(([prop, value]) => {
134
+ ;
135
+ style[prop] = value;
136
+ });
137
+ root.appendChild(dummyEl);
138
+ this._dummyEl = dummyEl;
139
+ return dummyEl;
140
+ }
141
+ resizeTextarea(textareaEl, dummyEl) {
142
+ if (!textareaEl || !dummyEl)
143
+ return;
144
+ const value = this.value;
145
+ const placeholder = this.placeholder;
146
+ const content = (value === '' && placeholder) ? placeholder : value + ' ';
147
+ const currentHeight = textareaEl.clientHeight;
148
+ dummyEl.textContent = content;
149
+ dummyEl.style.width = `${textareaEl.clientWidth}px`;
150
+ const measuredHeight = dummyEl.scrollHeight;
151
+ const minHeight = this.minHeight ? parseInt(this.minHeight.replace('px', ''), 10) : null;
152
+ const maxHeight = this.maxHeight ? parseInt(this.maxHeight.replace('px', ''), 10) : null;
153
+ let constrainedHeight = measuredHeight;
154
+ if (minHeight !== null)
155
+ constrainedHeight = Math.max(constrainedHeight, minHeight);
156
+ if (maxHeight !== null)
157
+ constrainedHeight = Math.min(constrainedHeight, maxHeight);
158
+ if (Math.abs(constrainedHeight - currentHeight) > 2) {
159
+ textareaEl.style.height = `${constrainedHeight}px`;
160
+ const needsScroll = maxHeight !== null && measuredHeight >= maxHeight;
161
+ if (needsScroll) {
162
+ textareaEl.style.overflowY = 'auto';
163
+ this._setupScrollbar(textareaEl);
164
+ }
165
+ else {
166
+ textareaEl.style.overflowY = 'hidden';
167
+ this._destroyScrollbar();
168
+ }
169
+ }
170
+ this._scrollbar?.update();
171
+ }
172
+ _setupScrollbar(textareaEl) {
173
+ if (this._scrollbar || !isCustomScrollbarEnabled())
174
+ return;
175
+ textareaEl.style.scrollbarWidth = 'none';
176
+ this.setAttribute('data-custom-scroll', '');
177
+ this._scrollbar = new CustomScrollbar(textareaEl, {
178
+ vertical: true,
179
+ horizontal: false
180
+ });
181
+ const scroll = this.shadowRoot.querySelector('.textarea-scroll');
182
+ if (scroll && this._scrollbar.trackY) {
183
+ scroll.appendChild(this._scrollbar.trackY);
184
+ }
185
+ }
186
+ _destroyScrollbar() {
187
+ if (!this._scrollbar)
188
+ return;
189
+ this._scrollbar.trackY?.remove();
190
+ this._scrollbar.destroy();
191
+ this._scrollbar = null;
192
+ this.removeAttribute('data-custom-scroll');
193
+ if (this._textareaEl) {
194
+ this._textareaEl.style.scrollbarWidth = '';
195
+ }
196
+ }
197
+ emitValueEvents(originalEvent) {
198
+ const value = this.value;
199
+ this._internals.setFormValue(value || '');
200
+ this.updateValidity();
201
+ const data = {
202
+ bubbles: true,
203
+ composed: true,
204
+ detail: {
205
+ value,
206
+ originalEvent
207
+ }
208
+ };
209
+ setTimeout(() => {
210
+ this.dispatchEvent(new CustomEvent('input', data));
211
+ this.dispatchEvent(new CustomEvent('change', data));
212
+ }, 0);
213
+ }
214
+ buildClassList() {
215
+ const classes = [this.size];
216
+ if (this.disabled)
217
+ classes.push('disabled');
218
+ if (this.required)
219
+ classes.push('required');
220
+ if (this.error)
221
+ classes.push('error');
222
+ if (this.resize !== 'none')
223
+ classes.push(`resize-${this.resize}`);
224
+ const customClass = this.getAttribute('class');
225
+ if (customClass)
226
+ classes.push(customClass);
227
+ return classes.join(' ');
228
+ }
229
+ applyHeightConstraints(textareaEl) {
230
+ if (this.minHeight) {
231
+ textareaEl.style.minHeight = this.minHeight;
232
+ }
233
+ if (this.maxHeight) {
234
+ textareaEl.style.maxHeight = this.maxHeight;
235
+ }
236
+ }
237
+ setupTextareaEvents(textareaEl) {
238
+ textareaEl.addEventListener('input', this.handleInputEvent);
239
+ textareaEl.addEventListener('change', this.handleInputEvent);
240
+ textareaEl.addEventListener('focus', this.handleFocusEvent);
241
+ textareaEl.addEventListener('blur', this.handleBlurEvent);
242
+ }
243
+ render() {
244
+ const root = this.shadowRoot;
245
+ const existingContainer = root.querySelector('.textarea-container');
246
+ const existingLabel = root.querySelector('label');
247
+ const existingTextarea = root.querySelector('textarea');
248
+ const existingError = root.querySelector('.error-message');
249
+ if (existingContainer && existingTextarea) {
250
+ if (this.label) {
251
+ if (existingLabel) {
252
+ existingLabel.innerHTML = this.label +
253
+ (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
254
+ existingLabel.style.display = 'block';
255
+ }
256
+ }
257
+ else if (existingLabel) {
258
+ existingLabel.style.display = 'none';
259
+ }
260
+ existingTextarea.value = this.value || '';
261
+ existingTextarea.placeholder = this.placeholder || '';
262
+ existingTextarea.disabled = this.disabled;
263
+ existingTextarea.required = this.required;
264
+ existingTextarea.className = this.buildClassList();
265
+ if (this.label)
266
+ existingTextarea.setAttribute('aria-label', this.label);
267
+ else
268
+ existingTextarea.removeAttribute('aria-label');
269
+ if (this.name) {
270
+ existingTextarea.setAttribute('name', this.name);
271
+ }
272
+ else {
273
+ existingTextarea.removeAttribute('name');
274
+ }
275
+ if (this.rows)
276
+ existingTextarea.setAttribute('rows', this.rows);
277
+ if (this.cols)
278
+ existingTextarea.setAttribute('cols', this.cols);
279
+ this.applyHeightConstraints(existingTextarea);
280
+ if (this.error) {
281
+ if (!existingError) {
282
+ const errorEl = document.createElement('div');
283
+ errorEl.className = 'error-message';
284
+ existingContainer.appendChild(errorEl);
285
+ }
286
+ const errorEl = root.querySelector('.error-message');
287
+ if (errorEl)
288
+ errorEl.textContent = this.error;
289
+ }
290
+ else if (existingError) {
291
+ existingError.remove();
292
+ }
293
+ const dummyEl = this.setupDummyElement(existingTextarea);
294
+ setTimeout(() => this.resizeTextarea(existingTextarea, dummyEl), 0);
295
+ this._textareaEl = existingTextarea;
296
+ }
297
+ else {
298
+ const container = document.createElement('div');
299
+ container.className = 'textarea-container';
300
+ const labelEl = document.createElement('label');
301
+ labelEl.className = 'ty-field-label';
302
+ if (this.label) {
303
+ labelEl.innerHTML = this.label +
304
+ (this.required ? ` <span class="required-icon">${REQUIRED_ICON}</span>` : '');
305
+ labelEl.style.display = 'block';
306
+ }
307
+ else {
308
+ labelEl.style.display = 'none';
309
+ }
310
+ const textareaEl = document.createElement('textarea');
311
+ textareaEl.value = this.value || '';
312
+ textareaEl.placeholder = this.placeholder || '';
313
+ textareaEl.disabled = this.disabled;
314
+ textareaEl.required = this.required;
315
+ textareaEl.className = this.buildClassList();
316
+ if (this.label)
317
+ textareaEl.setAttribute('aria-label', this.label);
318
+ if (this.name) {
319
+ textareaEl.setAttribute('name', this.name);
320
+ }
321
+ textareaEl.setAttribute('rows', this.rows || '3');
322
+ if (this.cols)
323
+ textareaEl.setAttribute('cols', this.cols);
324
+ this.applyHeightConstraints(textareaEl);
325
+ this.setupTextareaEvents(textareaEl);
326
+ const wrapperEl = document.createElement('div');
327
+ wrapperEl.className = 'textarea-wrapper';
328
+ const headerEl = document.createElement('div');
329
+ headerEl.className = 'textarea-header';
330
+ const headerSlot = document.createElement('slot');
331
+ headerSlot.setAttribute('name', 'header');
332
+ headerEl.appendChild(headerSlot);
333
+ const scrollEl = document.createElement('div');
334
+ scrollEl.className = 'textarea-scroll';
335
+ scrollEl.appendChild(textareaEl);
336
+ const footerEl = document.createElement('div');
337
+ footerEl.className = 'textarea-footer';
338
+ const footerSlot = document.createElement('slot');
339
+ footerSlot.setAttribute('name', 'footer');
340
+ footerEl.appendChild(footerSlot);
341
+ wrapperEl.appendChild(headerEl);
342
+ wrapperEl.appendChild(scrollEl);
343
+ wrapperEl.appendChild(footerEl);
344
+ const syncSlot = (slot, region) => {
345
+ const update = () => region.classList.toggle('has-content', slot.assignedNodes({ flatten: true }).length > 0);
346
+ slot.addEventListener('slotchange', update);
347
+ update();
348
+ };
349
+ syncSlot(headerSlot, headerEl);
350
+ syncSlot(footerSlot, footerEl);
351
+ container.appendChild(labelEl);
352
+ container.appendChild(wrapperEl);
353
+ if (this.error) {
354
+ const errorEl = document.createElement('div');
355
+ errorEl.className = 'error-message';
356
+ errorEl.textContent = this.error;
357
+ container.appendChild(errorEl);
358
+ }
359
+ root.appendChild(container);
360
+ const dummyEl = this.setupDummyElement(textareaEl);
361
+ setTimeout(() => this.resizeTextarea(textareaEl, dummyEl), 0);
362
+ this._textareaEl = textareaEl;
363
+ }
364
+ this.updateValidity();
365
+ }
366
+ updateValidity() {
367
+ const ta = this.shadowRoot?.querySelector('textarea');
368
+ if (this.disabled) {
369
+ this._internals.setValidity({});
370
+ return;
371
+ }
372
+ const current = ta ? ta.value : String(this.value ?? '');
373
+ if (this.required && current.trim() === '') {
374
+ this._internals.setValidity({ valueMissing: true }, 'Please fill out this field', ta ?? undefined);
375
+ }
376
+ else {
377
+ this._internals.setValidity({});
378
+ }
379
+ }
379
380
  }
380
381
  TyTextarea.formAssociated = true;
381
382
  TyTextarea.properties = {
382
- value: {
383
- type: 'string',
384
- visual: true,
385
- formValue: true,
386
- emitChange: true,
387
- default: ''
388
- },
389
- name: {
390
- type: 'string',
391
- default: ''
392
- },
393
- placeholder: {
394
- type: 'string',
395
- visual: true,
396
- default: ''
397
- },
398
- label: {
399
- type: 'string',
400
- visual: true,
401
- default: ''
402
- },
403
- disabled: {
404
- type: 'boolean',
405
- visual: true,
406
- default: false
407
- },
408
- required: {
409
- type: 'boolean',
410
- visual: true,
411
- default: false
412
- },
413
- error: {
414
- type: 'string',
415
- visual: true,
416
- default: ''
417
- },
418
- size: {
419
- type: 'string',
420
- visual: true,
421
- default: 'md',
422
- validate: (v) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(v),
423
- coerce: (v) => {
424
- if (!['xs', 'sm', 'md', 'lg', 'xl'].includes(v)) {
425
- console.warn(`[ty-textarea] Invalid size '${v}'. Using 'md'.`);
426
- return 'md';
427
- }
428
- return v;
429
- }
430
- },
431
- rows: {
432
- type: 'string',
433
- visual: true,
434
- default: '3',
435
- coerce: (v) => String(v)
436
- },
437
- cols: {
438
- type: 'string',
439
- visual: true,
440
- default: '',
441
- coerce: (v) => v ? String(v) : ''
442
- },
443
- resize: {
444
- type: 'string',
445
- visual: true,
446
- default: 'none',
447
- validate: (v) => ['none', 'both', 'horizontal', 'vertical'].includes(v),
448
- coerce: (v) => {
449
- if (!['none', 'both', 'horizontal', 'vertical'].includes(v)) {
450
- console.warn(`[ty-textarea] Invalid resize '${v}'. Using 'none'.`);
451
- return 'none';
452
- }
453
- return v;
454
- }
455
- },
456
- minHeight: {
457
- type: 'string',
458
- visual: true,
459
- default: ''
460
- },
461
- maxHeight: {
462
- type: 'string',
463
- visual: true,
464
- default: ''
465
- }
383
+ value: {
384
+ type: 'string',
385
+ visual: true,
386
+ formValue: true,
387
+ emitChange: true,
388
+ default: ''
389
+ },
390
+ name: {
391
+ type: 'string',
392
+ default: ''
393
+ },
394
+ placeholder: {
395
+ type: 'string',
396
+ visual: true,
397
+ default: ''
398
+ },
399
+ label: {
400
+ type: 'string',
401
+ visual: true,
402
+ default: ''
403
+ },
404
+ disabled: {
405
+ type: 'boolean',
406
+ visual: true,
407
+ default: false
408
+ },
409
+ required: {
410
+ type: 'boolean',
411
+ visual: true,
412
+ default: false
413
+ },
414
+ error: {
415
+ type: 'string',
416
+ visual: true,
417
+ default: ''
418
+ },
419
+ size: {
420
+ type: 'string',
421
+ visual: true,
422
+ default: DEFAULT_SIZE,
423
+ validate: (v) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(v),
424
+ coerce: (v) => {
425
+ if (!['xs', 'sm', 'md', 'lg', 'xl'].includes(v)) {
426
+ console.warn(`[ty-textarea] Invalid size '${v}'. Using '${DEFAULT_SIZE}'.`);
427
+ return DEFAULT_SIZE;
428
+ }
429
+ return v;
430
+ }
431
+ },
432
+ rows: {
433
+ type: 'string',
434
+ visual: true,
435
+ default: '3',
436
+ coerce: (v) => String(v)
437
+ },
438
+ cols: {
439
+ type: 'string',
440
+ visual: true,
441
+ default: '',
442
+ coerce: (v) => v ? String(v) : ''
443
+ },
444
+ resize: {
445
+ type: 'string',
446
+ visual: true,
447
+ default: 'none',
448
+ validate: (v) => ['none', 'both', 'horizontal', 'vertical'].includes(v),
449
+ coerce: (v) => {
450
+ if (!['none', 'both', 'horizontal', 'vertical'].includes(v)) {
451
+ console.warn(`[ty-textarea] Invalid resize '${v}'. Using 'none'.`);
452
+ return 'none';
453
+ }
454
+ return v;
455
+ }
456
+ },
457
+ minHeight: {
458
+ type: 'string',
459
+ visual: true,
460
+ default: ''
461
+ },
462
+ maxHeight: {
463
+ type: 'string',
464
+ visual: true,
465
+ default: ''
466
+ }
466
467
  };
467
468
  if (!customElements.get('ty-textarea')) {
468
- customElements.define('ty-textarea', TyTextarea);
469
+ customElements.define('ty-textarea', TyTextarea);
469
470
  }