tyrell-components 1.0.0-RC14 → 1.0.0-RC15

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 (113) hide show
  1. package/README.md +9 -1
  2. package/css/tyrell-theme.css +25 -2
  3. package/css/tyrell.css +4 -0
  4. package/dist/tyrell-colors-static.css +1 -316
  5. package/dist/tyrell-theme.css +1 -1443
  6. package/dist/tyrell.css +1 -1390
  7. package/dist/tyrell.js +1 -1
  8. package/lib/base/ty-component.js +196 -196
  9. package/lib/components/button.d.ts +1 -1
  10. package/lib/components/button.js +261 -259
  11. package/lib/components/calendar-month.d.ts +2 -2
  12. package/lib/components/calendar-month.js +439 -416
  13. package/lib/components/calendar-navigation.js +238 -238
  14. package/lib/components/calendar.d.ts +5 -3
  15. package/lib/components/calendar.js +611 -602
  16. package/lib/components/checkbox.d.ts +1 -1
  17. package/lib/components/checkbox.js +273 -272
  18. package/lib/components/copy.d.ts +1 -1
  19. package/lib/components/copy.js +280 -278
  20. package/lib/components/date-picker.d.ts +8 -1
  21. package/lib/components/date-picker.js +1128 -1108
  22. package/lib/components/file-upload.js +404 -404
  23. package/lib/components/icon.js +141 -141
  24. package/lib/components/input.d.ts +1 -1
  25. package/lib/components/input.js +593 -596
  26. package/lib/components/modal.js +319 -319
  27. package/lib/components/option.js +167 -167
  28. package/lib/components/popup.js +233 -233
  29. package/lib/components/radio.d.ts +2 -2
  30. package/lib/components/radio.js +407 -406
  31. package/lib/components/resize-observer.js +67 -67
  32. package/lib/components/scroll-container.js +292 -294
  33. package/lib/components/select.d.ts +71 -12
  34. package/lib/components/select.js +1605 -1529
  35. package/lib/components/selected-tags.d.ts +1 -1
  36. package/lib/components/selected-tags.js +188 -182
  37. package/lib/components/step.js +62 -62
  38. package/lib/components/switch.d.ts +1 -1
  39. package/lib/components/switch.js +214 -213
  40. package/lib/components/tab.js +62 -62
  41. package/lib/components/tabs.d.ts +1 -0
  42. package/lib/components/tabs.js +523 -492
  43. package/lib/components/tag.d.ts +1 -1
  44. package/lib/components/tag.js +236 -235
  45. package/lib/components/textarea.d.ts +1 -1
  46. package/lib/components/textarea.js +457 -456
  47. package/lib/components/tooltip.js +283 -283
  48. package/lib/components/wizard.js +384 -384
  49. package/lib/icons/heroicons/micro.js +392 -392
  50. package/lib/icons/heroicons/mini.js +382 -382
  51. package/lib/icons/heroicons/outline.js +337 -337
  52. package/lib/icons/heroicons/solid.js +407 -407
  53. package/lib/icons/lucide.js +22843 -22843
  54. package/lib/icons/material/filled.js +9653 -9653
  55. package/lib/icons/material/outlined.js +10271 -10271
  56. package/lib/icons/material/round.js +10495 -10495
  57. package/lib/icons/material/sharp.js +9905 -9905
  58. package/lib/icons/material/two-tone.js +12792 -12792
  59. package/lib/index.js +21 -21
  60. package/lib/styles/button.js +200 -302
  61. package/lib/styles/calendar-month.js +123 -183
  62. package/lib/styles/calendar-navigation.d.ts +1 -1
  63. package/lib/styles/calendar-navigation.js +64 -79
  64. package/lib/styles/calendar.d.ts +1 -1
  65. package/lib/styles/calendar.js +9 -9
  66. package/lib/styles/checkbox.js +45 -72
  67. package/lib/styles/copy.js +67 -91
  68. package/lib/styles/custom-scrollbar.js +80 -100
  69. package/lib/styles/date-picker.js +261 -331
  70. package/lib/styles/field-size.d.ts +6 -0
  71. package/lib/styles/field-size.js +91 -0
  72. package/lib/styles/file-upload.js +139 -169
  73. package/lib/styles/icon.js +92 -156
  74. package/lib/styles/input.js +125 -241
  75. package/lib/styles/modal.js +67 -102
  76. package/lib/styles/option.d.ts +1 -1
  77. package/lib/styles/option.js +47 -67
  78. package/lib/styles/popup.js +34 -65
  79. package/lib/styles/radio.js +69 -96
  80. package/lib/styles/resize-observer.js +2 -5
  81. package/lib/styles/scroll-container.js +100 -130
  82. package/lib/styles/select-base.js +596 -773
  83. package/lib/styles/select.js +140 -228
  84. package/lib/styles/step.js +22 -29
  85. package/lib/styles/switch.js +34 -55
  86. package/lib/styles/tab.d.ts +1 -1
  87. package/lib/styles/tab.js +22 -29
  88. package/lib/styles/tabs.d.ts +1 -1
  89. package/lib/styles/tabs.js +186 -180
  90. package/lib/styles/tag.js +135 -188
  91. package/lib/styles/textarea.d.ts +1 -1
  92. package/lib/styles/textarea.js +205 -271
  93. package/lib/styles/tooltip.js +3 -3
  94. package/lib/styles/wizard.js +170 -226
  95. package/lib/types/common.d.ts +2 -0
  96. package/lib/types/common.js +6 -5
  97. package/lib/utils/calendar-utils.js +100 -100
  98. package/lib/utils/custom-scrollbar.js +272 -272
  99. package/lib/utils/flavor-sheet.js +14 -14
  100. package/lib/utils/icon-registry.js +127 -127
  101. package/lib/utils/loader-registry.js +3 -3
  102. package/lib/utils/locale.js +34 -34
  103. package/lib/utils/mobile.js +1 -1
  104. package/lib/utils/number-format.js +54 -54
  105. package/lib/utils/positioning.js +212 -212
  106. package/lib/utils/property-capture.js +36 -36
  107. package/lib/utils/property-manager.js +125 -125
  108. package/lib/utils/resize-observer.js +23 -23
  109. package/lib/utils/scroll-lock.js +57 -57
  110. package/lib/utils/styles.js +17 -17
  111. package/lib/version.d.ts +1 -1
  112. package/lib/version.js +1 -1
  113. 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
  }