@nuvia-ui/components 4.0.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.
Files changed (230) hide show
  1. package/package.json +27 -0
  2. package/src/ds-accordion/ds-accordion-item.js +288 -0
  3. package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
  4. package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
  5. package/src/ds-accordion/ds-accordion.js +68 -0
  6. package/src/ds-accordion/ds-accordion.stories.js +118 -0
  7. package/src/ds-accordion/ds-accordion.test.js +146 -0
  8. package/src/ds-accordion/index.js +2 -0
  9. package/src/ds-action-bar/ds-action-bar.js +116 -0
  10. package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
  11. package/src/ds-action-bar/ds-action-bar.test.js +64 -0
  12. package/src/ds-action-bar/index.js +1 -0
  13. package/src/ds-alert/ds-alert.a11y.test.js +151 -0
  14. package/src/ds-alert/ds-alert.js +223 -0
  15. package/src/ds-alert/ds-alert.mdx +142 -0
  16. package/src/ds-alert/ds-alert.stories.js +166 -0
  17. package/src/ds-alert/ds-alert.test.js +256 -0
  18. package/src/ds-alert/index.js +1 -0
  19. package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
  20. package/src/ds-avatar/ds-avatar.js +216 -0
  21. package/src/ds-avatar/ds-avatar.stories.js +120 -0
  22. package/src/ds-avatar/ds-avatar.test.js +83 -0
  23. package/src/ds-avatar/index.js +1 -0
  24. package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
  25. package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
  26. package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
  27. package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
  28. package/src/ds-avatar-extended/index.js +1 -0
  29. package/src/ds-banner/ds-banner.a11y.test.js +51 -0
  30. package/src/ds-banner/ds-banner.js +233 -0
  31. package/src/ds-banner/ds-banner.stories.js +185 -0
  32. package/src/ds-banner/ds-banner.test.js +116 -0
  33. package/src/ds-banner/index.js +1 -0
  34. package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
  35. package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
  36. package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
  37. package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
  38. package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
  39. package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
  40. package/src/ds-button/ds-button.a11y.test.js +49 -0
  41. package/src/ds-button/ds-button.js +205 -0
  42. package/src/ds-button/ds-button.mdx +141 -0
  43. package/src/ds-button/ds-button.stories.js +152 -0
  44. package/src/ds-button/ds-button.test.js +62 -0
  45. package/src/ds-button/index.js +1 -0
  46. package/src/ds-button-group/ds-button-group.js +82 -0
  47. package/src/ds-button-group/ds-button-group.mdx +39 -0
  48. package/src/ds-button-group/ds-button-group.stories.js +47 -0
  49. package/src/ds-button-group/ds-button-group.test.js +47 -0
  50. package/src/ds-button-group/index.js +1 -0
  51. package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
  52. package/src/ds-checkbox/ds-checkbox.js +271 -0
  53. package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
  54. package/src/ds-checkbox/ds-checkbox.test.js +191 -0
  55. package/src/ds-checkbox/index.js +1 -0
  56. package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
  57. package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
  58. package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
  59. package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
  60. package/src/ds-checkbox-group/index.js +1 -0
  61. package/src/ds-dialog/ds-dialog.js +466 -0
  62. package/src/ds-dialog/ds-dialog.stories.js +274 -0
  63. package/src/ds-dialog/ds-dialog.test.js +441 -0
  64. package/src/ds-dialog/index.js +1 -0
  65. package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
  66. package/src/ds-dropdown/ds-dropdown.js +891 -0
  67. package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
  68. package/src/ds-dropdown/ds-dropdown.test.js +268 -0
  69. package/src/ds-dropdown/index.js +1 -0
  70. package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
  71. package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
  72. package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
  73. package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
  74. package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
  75. package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
  76. package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
  77. package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
  78. package/src/ds-file-uploader/ds-file-uploader.js +442 -0
  79. package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
  80. package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
  81. package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
  82. package/src/ds-header/ds-header.a11y.test.js +38 -0
  83. package/src/ds-header/ds-header.js +149 -0
  84. package/src/ds-header/ds-header.stories.js +63 -0
  85. package/src/ds-header/ds-header.test.js +52 -0
  86. package/src/ds-header/index.js +1 -0
  87. package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
  88. package/src/ds-header-nav/ds-header-nav.js +114 -0
  89. package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
  90. package/src/ds-header-nav/ds-header-nav.test.js +93 -0
  91. package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
  92. package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
  93. package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
  94. package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
  95. package/src/ds-icon/ds-icon.a11y.test.js +49 -0
  96. package/src/ds-icon/ds-icon.js +75 -0
  97. package/src/ds-icon/ds-icon.mdx +36 -0
  98. package/src/ds-icon/ds-icon.stories.js +88 -0
  99. package/src/ds-icon/ds-icon.test.js +97 -0
  100. package/src/ds-icon/index.js +1 -0
  101. package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
  102. package/src/ds-icon-button/ds-icon-button.js +224 -0
  103. package/src/ds-icon-button/ds-icon-button.mdx +131 -0
  104. package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
  105. package/src/ds-icon-button/ds-icon-button.test.js +90 -0
  106. package/src/ds-icon-button/index.js +1 -0
  107. package/src/ds-input/ds-input.a11y.test.js +145 -0
  108. package/src/ds-input/ds-input.js +645 -0
  109. package/src/ds-input/ds-input.mdx +251 -0
  110. package/src/ds-input/ds-input.stories.js +298 -0
  111. package/src/ds-input/ds-input.test.js +792 -0
  112. package/src/ds-input/index.js +1 -0
  113. package/src/ds-link/ds-link.js +111 -0
  114. package/src/ds-link/ds-link.stories.js +56 -0
  115. package/src/ds-link/ds-link.test.js +74 -0
  116. package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
  117. package/src/ds-list-item/ds-list-item.js +292 -0
  118. package/src/ds-list-item/ds-list-item.stories.js +101 -0
  119. package/src/ds-list-item/ds-list-item.test.js +63 -0
  120. package/src/ds-menu/ds-menu.js +30 -0
  121. package/src/ds-menu/ds-menu.stories.js +120 -0
  122. package/src/ds-menu/ds-menu.test.js +123 -0
  123. package/src/ds-menu-group/ds-menu-group.js +101 -0
  124. package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
  125. package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
  126. package/src/ds-nav-item/ds-nav-item.js +307 -0
  127. package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
  128. package/src/ds-nav-item/ds-nav-item.test.js +169 -0
  129. package/src/ds-nav-item/index.js +1 -0
  130. package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
  131. package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
  132. package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
  133. package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
  134. package/src/ds-nav-vertical/index.js +1 -0
  135. package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
  136. package/src/ds-pagination/ds-pagination.js +232 -0
  137. package/src/ds-pagination/ds-pagination.stories.js +63 -0
  138. package/src/ds-pagination/ds-pagination.test.js +141 -0
  139. package/src/ds-pagination/index.js +1 -0
  140. package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
  141. package/src/ds-progress-bar/ds-progress-bar.js +81 -0
  142. package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
  143. package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
  144. package/src/ds-radio/ds-radio.a11y.test.js +69 -0
  145. package/src/ds-radio/ds-radio.js +240 -0
  146. package/src/ds-radio/ds-radio.stories.js +102 -0
  147. package/src/ds-radio/ds-radio.test.js +114 -0
  148. package/src/ds-radio/index.js +1 -0
  149. package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
  150. package/src/ds-radio-group/ds-radio-group.js +257 -0
  151. package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
  152. package/src/ds-radio-group/ds-radio-group.test.js +194 -0
  153. package/src/ds-radio-group/index.js +1 -0
  154. package/src/ds-rich-list/ds-rich-list.js +246 -0
  155. package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
  156. package/src/ds-rich-list/ds-rich-list.test.js +293 -0
  157. package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
  158. package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
  159. package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
  160. package/src/ds-slider/ds-slider.js +399 -0
  161. package/src/ds-slider/ds-slider.stories.js +107 -0
  162. package/src/ds-slider/ds-slider.test.js +308 -0
  163. package/src/ds-spinner/ds-spinner.js +173 -0
  164. package/src/ds-spinner/ds-spinner.stories.js +52 -0
  165. package/src/ds-spinner/ds-spinner.test.js +50 -0
  166. package/src/ds-status-border/ds-status-border.js +88 -0
  167. package/src/ds-status-border/ds-status-border.stories.js +242 -0
  168. package/src/ds-status-border/ds-status-border.test.js +168 -0
  169. package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
  170. package/src/ds-stepper/ds-stepper.js +207 -0
  171. package/src/ds-stepper/ds-stepper.stories.js +530 -0
  172. package/src/ds-stepper/ds-stepper.test.js +311 -0
  173. package/src/ds-stepper-item/ds-stepper-item.js +485 -0
  174. package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
  175. package/src/ds-switch/ds-switch.js +348 -0
  176. package/src/ds-switch/ds-switch.stories.js +145 -0
  177. package/src/ds-switch/ds-switch.test.js +226 -0
  178. package/src/ds-switch/index.js +1 -0
  179. package/src/ds-tab-item/ds-tab-item.js +341 -0
  180. package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
  181. package/src/ds-tabs/ds-tab-panel.js +48 -0
  182. package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
  183. package/src/ds-tabs/ds-tabs.js +180 -0
  184. package/src/ds-tabs/ds-tabs.stories.js +152 -0
  185. package/src/ds-tabs/ds-tabs.test.js +306 -0
  186. package/src/ds-tabs/index.js +3 -0
  187. package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
  188. package/src/ds-tag-action/ds-tag-action.js +185 -0
  189. package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
  190. package/src/ds-tag-action/ds-tag-action.test.js +44 -0
  191. package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
  192. package/src/ds-tag-removable/ds-tag-removable.js +146 -0
  193. package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
  194. package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
  195. package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
  196. package/src/ds-tag-status/ds-tag-status.js +164 -0
  197. package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
  198. package/src/ds-tag-status/ds-tag-status.test.js +140 -0
  199. package/src/ds-tag-status/index.js +1 -0
  200. package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
  201. package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
  202. package/src/ds-textarea/ds-textarea.js +505 -0
  203. package/src/ds-textarea/ds-textarea.stories.js +335 -0
  204. package/src/ds-textarea/ds-textarea.test.js +218 -0
  205. package/src/ds-textarea/index.js +1 -0
  206. package/src/ds-thumbnail/ds-thumbnail.js +207 -0
  207. package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
  208. package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
  209. package/src/ds-toast/ds-toast-provider.js +110 -0
  210. package/src/ds-toast/ds-toast.a11y.test.js +34 -0
  211. package/src/ds-toast/ds-toast.js +243 -0
  212. package/src/ds-toast/ds-toast.stories.js +143 -0
  213. package/src/ds-toast/ds-toast.test.js +93 -0
  214. package/src/ds-toast/index.js +2 -0
  215. package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
  216. package/src/ds-tooltip/ds-tooltip.js +217 -0
  217. package/src/ds-tooltip/ds-tooltip.mdx +75 -0
  218. package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
  219. package/src/ds-tooltip/ds-tooltip.test.js +191 -0
  220. package/src/ds-tooltip/index.js +1 -0
  221. package/src/ds-tooltip/positioner.js +117 -0
  222. package/src/index.js +50 -0
  223. package/src/mixins/field-label.mixin.js +113 -0
  224. package/src/mixins/field-message.mixin.js +66 -0
  225. package/src/token-provider/index.js +1 -0
  226. package/src/token-provider/token-provider.a11y.test.js +44 -0
  227. package/src/token-provider/token-provider.js +85 -0
  228. package/src/token-provider/token-provider.stories.js +105 -0
  229. package/src/token-provider/token-provider.test.js +134 -0
  230. package/src/utils/number-input.utils.js +42 -0
@@ -0,0 +1,505 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { FieldMessageMixin, fieldMessageStyles } from '../mixins/field-message.mixin.js';
3
+ import { fieldLabelStyles } from '../mixins/field-label.mixin.js';
4
+ import '../ds-icon-button/ds-icon-button.js';
5
+ import '../ds-tooltip/ds-tooltip.js';
6
+
7
+ /**
8
+ * Textarea component for multi-line text input with label, counter, and validation
9
+ *
10
+ * @element ds-textarea
11
+ *
12
+ * @prop {string} label - Label text (optional)
13
+ * @prop {string} info - Info tooltip text for info button
14
+ * @prop {string} placeholder - Placeholder text
15
+ * @prop {string} value - Textarea value
16
+ * @prop {string} helper - Help text below field
17
+ * @prop {string} validationStatus - Validation state: 'error' | 'success' | undefined
18
+ * @prop {string} validationMessage - Validation message to display
19
+ * @prop {boolean} disabled - Disabled state
20
+ * @prop {boolean} readonly - Read-only state
21
+ * @prop {boolean} required - Required field
22
+ * @prop {number} rows - Number of visible text lines (default: 3)
23
+ * @prop {number} minlength - Minimum character length
24
+ * @prop {number} maxlength - Maximum character length
25
+ * @prop {boolean} showCounter - Show character counter (requires maxlength)
26
+ * @prop {string} resize - Resize behavior: 'none' | 'vertical' (default: 'vertical')
27
+ * @prop {boolean} autoResize - Auto-grow with content
28
+ * @prop {boolean} clearable - Show clear button when textarea is focused and has value
29
+ * @prop {string} labelPosition - Label position: 'top' | 'inline-start' (default: 'top')
30
+ * @prop {string} labelWidth - Fixed label width for inline layout (e.g., '100px')
31
+ *
32
+ * @fires input - Fired when textarea value changes
33
+ * @fires change - Fired when textarea loses focus
34
+ * @fires info-click - Fired when info button is clicked
35
+ */
36
+ export class DsTextarea extends FieldMessageMixin(LitElement) {
37
+ static properties = {
38
+ label: { type: String },
39
+ info: { type: String },
40
+ labelPosition: { type: String, attribute: 'label-position' },
41
+ labelWidth: { type: String, attribute: 'label-width' },
42
+ placeholder: { type: String },
43
+ value: { type: String },
44
+ helper: { type: String },
45
+ validationStatus: { type: String, reflect: true, attribute: 'validation-status' },
46
+ validationMessage: { type: String, attribute: 'validation-message' },
47
+ disabled: { type: Boolean, reflect: true },
48
+ readonly: { type: Boolean, reflect: true },
49
+ required: { type: Boolean, reflect: true },
50
+ rows: { type: Number },
51
+ minlength: { type: Number },
52
+ maxlength: { type: Number },
53
+ showCounter: { type: Boolean, attribute: 'show-counter' },
54
+ resize: { type: String, reflect: true },
55
+ autoResize: { type: Boolean, attribute: 'auto-resize' },
56
+ clearable: { type: Boolean },
57
+ _isFocused: { type: Boolean, state: true }
58
+ };
59
+
60
+ static styles = css`
61
+ :host {
62
+ display: block;
63
+ box-sizing: border-box;
64
+ }
65
+
66
+ .field-wrapper {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: var(--ds-space-xs); /* 4px gap between sections */
70
+ }
71
+
72
+ /* Label styles from mixin with counter support */
73
+ ${fieldLabelStyles}
74
+
75
+ .label-row {
76
+ display: flex;
77
+ align-items: flex-start;
78
+ gap: var(--ds-space-xs);
79
+ justify-content: space-between; /* Push counter to the right */
80
+ }
81
+
82
+ .label-content {
83
+ display: flex;
84
+ align-items: flex-start;
85
+ gap: var(--ds-space-xs);
86
+ }
87
+
88
+ .label-row label {
89
+ font: var(--ds-typo-content-body-bold);
90
+ color: var(--ds-color-text-default);
91
+ line-height: 24px;
92
+ cursor: pointer;
93
+ }
94
+
95
+ .label-row ds-icon-button {
96
+ flex-shrink: 0;
97
+ }
98
+
99
+ .counter {
100
+ font: var(--ds-typo-content-body-regular);
101
+ color: var(--ds-color-text-secondary);
102
+ line-height: 24px;
103
+ white-space: nowrap;
104
+ flex-shrink: 0;
105
+ }
106
+
107
+ .counter.over-limit {
108
+ color: var(--ds-color-text-error);
109
+ }
110
+
111
+ /* Inline label layout */
112
+ .field-wrapper.inline-label {
113
+ display: flex;
114
+ flex-direction: row;
115
+ align-items: flex-start;
116
+ gap: var(--ds-space-sm, 8px);
117
+ }
118
+
119
+ .field-wrapper.inline-label .label-row {
120
+ flex-shrink: 0;
121
+ padding-top: 4px; /* Align label text with first line of textarea */
122
+ justify-content: flex-end;
123
+ }
124
+
125
+ .field-wrapper.inline-label .field-content {
126
+ flex: 1;
127
+ min-width: 0; /* Allow shrinking */
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: var(--ds-space-xs);
131
+ }
132
+
133
+ /* Textarea field wrapper */
134
+ .textarea-field {
135
+ display: flex;
136
+ flex-direction: column;
137
+ padding: var(--ds-space-xs) var(--ds-space-sm); /* 4px vertical, 8px horizontal */
138
+ border: 1px solid var(--ds-color-border-strong);
139
+ border-radius: var(--ds-radius-input);
140
+ background: var(--ds-color-bg-default);
141
+ box-sizing: border-box;
142
+ transition: background-color 0.2s, border-color 0.2s;
143
+ position: relative; /* For clear button positioning */
144
+ }
145
+
146
+ /* Action container for clear button */
147
+ .action-container {
148
+ position: absolute;
149
+ top: var(--ds-space-xs);
150
+ right: var(--ds-space-xs);
151
+ display: flex;
152
+ gap: var(--ds-space-2xs);
153
+ pointer-events: none; /* Let clicks pass through to textarea */
154
+ }
155
+
156
+ .action-container ds-icon-button {
157
+ pointer-events: auto; /* But buttons should be clickable */
158
+ }
159
+
160
+ /* Textarea element */
161
+ textarea {
162
+ border: none;
163
+ background: transparent;
164
+ outline: none;
165
+ resize: vertical; /* Default */
166
+
167
+ font: var(--ds-typo-content-body-regular);
168
+ color: var(--ds-color-text-default);
169
+ line-height: 20px; /* Match body line-height */
170
+
171
+ box-sizing: border-box;
172
+ padding: 0;
173
+ margin: 0;
174
+ width: 100%;
175
+
176
+ /* Minimum height to ensure field total is at least 32px (24px + 8px padding) */
177
+ min-height: 24px;
178
+ }
179
+
180
+ textarea::placeholder {
181
+ color: var(--ds-color-text-secondary);
182
+ }
183
+
184
+ /* Resize variants */
185
+ :host([resize="none"]) textarea {
186
+ resize: none;
187
+ }
188
+
189
+ :host([resize="vertical"]) textarea {
190
+ resize: vertical;
191
+ }
192
+
193
+ /* Auto-resize: disable manual resize and scroll */
194
+ :host([auto-resize]) textarea {
195
+ resize: none;
196
+ overflow-y: hidden;
197
+ }
198
+
199
+ /* States */
200
+
201
+ /* Hover */
202
+ :host(:not([disabled]):not([readonly])) .textarea-field:hover {
203
+ background: var(--ds-color-bg-hover);
204
+ }
205
+
206
+ /* Focus */
207
+ :host(:not([disabled]):not([readonly])) .textarea-field:has(textarea:focus) {
208
+ border-color: var(--ds-color-border-focus);
209
+ outline: 2px solid var(--ds-color-border-focus);
210
+ outline-offset: -1px;
211
+ }
212
+
213
+ /* Disabled */
214
+ :host([disabled]) .textarea-field {
215
+ background: var(--ds-color-bg-disabled);
216
+ border: 1px solid transparent;
217
+ cursor: not-allowed;
218
+ }
219
+
220
+ :host([disabled]) textarea {
221
+ color: var(--ds-color-text-disabled);
222
+ cursor: not-allowed;
223
+ }
224
+
225
+ /* Read-only */
226
+ :host([readonly]) .textarea-field {
227
+ background: transparent;
228
+ border: 1px solid transparent;
229
+ }
230
+
231
+ :host([readonly]) textarea {
232
+ cursor: default;
233
+ }
234
+
235
+ /* Validation states */
236
+ :host([validation-status="error"]) .textarea-field {
237
+ border-color: var(--ds-color-border-error);
238
+ }
239
+
240
+ :host([validation-status="success"]) .textarea-field {
241
+ border-color: var(--ds-color-border-success, var(--ds-color-border-strong));
242
+ }
243
+
244
+ /* Message styles from mixin */
245
+ ${fieldMessageStyles}
246
+ `;
247
+
248
+ constructor() {
249
+ super();
250
+ this.label = '';
251
+ this.info = '';
252
+ this.labelPosition = 'top';
253
+ this.labelWidth = '';
254
+ this.placeholder = '';
255
+ this.value = '';
256
+ this.helper = '';
257
+ this.validationStatus = undefined;
258
+ this.validationMessage = '';
259
+ this.disabled = false;
260
+ this.readonly = false;
261
+ this.required = false;
262
+ this.rows = 3;
263
+ this.minlength = undefined;
264
+ this.maxlength = undefined;
265
+ this.showCounter = false;
266
+ this.resize = 'vertical';
267
+ this.autoResize = false;
268
+ this.clearable = false;
269
+ this._isFocused = false;
270
+ }
271
+
272
+ firstUpdated() {
273
+ // Adjust height on initial render if auto-resize is enabled
274
+ if (this.autoResize) {
275
+ this._adjustHeight();
276
+ }
277
+ }
278
+
279
+ updated(changedProperties) {
280
+ super.updated(changedProperties);
281
+
282
+ // Handle auto-resize on value or autoResize property change
283
+ if (this.autoResize && (changedProperties.has('value') || changedProperties.has('autoResize'))) {
284
+ this._adjustHeight();
285
+ }
286
+ }
287
+
288
+ _adjustHeight() {
289
+ if (!this.autoResize) return;
290
+
291
+ const textarea = this.shadowRoot.querySelector('textarea');
292
+ if (!textarea) return;
293
+
294
+ // Store the current scroll position
295
+ const scrollTop = textarea.scrollTop;
296
+
297
+ // Reset height to get the true scrollHeight
298
+ const previousHeight = textarea.style.height;
299
+ textarea.style.height = '0px';
300
+
301
+ // Calculate the minimum height based on rows
302
+ const lineHeight = 20; // px - from CSS
303
+ const minHeight = this.rows * lineHeight;
304
+
305
+ // Get the scroll height (content height)
306
+ const scrollHeight = textarea.scrollHeight;
307
+
308
+ // Set height to the larger of minHeight or scrollHeight
309
+ const newHeight = Math.max(minHeight, scrollHeight);
310
+ textarea.style.height = `${newHeight}px`;
311
+
312
+ // Restore scroll position if needed
313
+ textarea.scrollTop = scrollTop;
314
+ }
315
+
316
+ _handleInput(e) {
317
+ this.value = e.target.value;
318
+
319
+ if (this.autoResize) {
320
+ this._adjustHeight();
321
+ }
322
+
323
+ this._dispatchInputEvent();
324
+ }
325
+
326
+ _dispatchInputEvent() {
327
+ this.dispatchEvent(new CustomEvent('input', {
328
+ detail: { value: this.value },
329
+ bubbles: true,
330
+ composed: true
331
+ }));
332
+ }
333
+
334
+ _handleChange(e) {
335
+ this.dispatchEvent(new CustomEvent('change', {
336
+ detail: { value: this.value },
337
+ bubbles: true,
338
+ composed: true
339
+ }));
340
+ }
341
+
342
+ _handleFocus() {
343
+ this._isFocused = true;
344
+ }
345
+
346
+ _handleBlur() {
347
+ this._isFocused = false;
348
+ }
349
+
350
+ _handleKeyDown(e) {
351
+ // Clear on Escape key
352
+ if (e.key === 'Escape' && this.value && this.clearable) {
353
+ e.preventDefault();
354
+ this._handleClear();
355
+ }
356
+ }
357
+
358
+ _handleClear() {
359
+ this.value = '';
360
+ const textarea = this.shadowRoot.querySelector('textarea');
361
+ if (textarea) {
362
+ textarea.value = '';
363
+ textarea.focus();
364
+ }
365
+ this._dispatchInputEvent();
366
+ }
367
+
368
+ _handleInfoClick(e) {
369
+ e.preventDefault();
370
+ e.stopPropagation();
371
+ this.dispatchEvent(new CustomEvent('info-click', {
372
+ bubbles: true,
373
+ composed: true,
374
+ detail: { info: this.info }
375
+ }));
376
+ }
377
+
378
+ _getCharacterCount() {
379
+ return this.value ? this.value.length : 0;
380
+ }
381
+
382
+ _isOverLimit() {
383
+ if (!this.maxlength) return false;
384
+ return this._getCharacterCount() > this.maxlength;
385
+ }
386
+
387
+ render() {
388
+ const hasError = this.validationStatus === 'error';
389
+ const errorMessage = hasError ? this.validationMessage : '';
390
+ const textareaId = 'textarea-field';
391
+ const messageId = 'field-message';
392
+ const showCounter = this.showCounter && this.maxlength;
393
+ const charCount = this._getCharacterCount();
394
+ const overLimit = this._isOverLimit();
395
+ const isInline = this.labelPosition === 'inline-start';
396
+ const wrapperClass = isInline ? 'field-wrapper inline-label' : 'field-wrapper';
397
+ const labelStyle = isInline && this.labelWidth ? `width: ${this.labelWidth}` : '';
398
+
399
+ const textareaFieldContent = html`
400
+ <div class="textarea-field" part="field">
401
+ <textarea
402
+ id="${textareaId}"
403
+ .value=${this.value}
404
+ placeholder=${this.placeholder}
405
+ ?disabled=${this.disabled}
406
+ ?readonly=${this.readonly}
407
+ ?required=${this.required}
408
+ rows=${this.rows}
409
+ minlength=${this.minlength || undefined}
410
+ maxlength=${this.maxlength || undefined}
411
+ aria-invalid=${hasError ? 'true' : 'false'}
412
+ aria-required=${this.required ? 'true' : 'false'}
413
+ aria-describedby=${this.helper || errorMessage ? messageId : ''}
414
+ aria-label=${!this.label ? this.placeholder || 'Textarea' : ''}
415
+ @input=${this._handleInput}
416
+ @change=${this._handleChange}
417
+ @focus=${this._handleFocus}
418
+ @blur=${this._handleBlur}
419
+ @keydown=${this._handleKeyDown}
420
+ part="textarea"
421
+ ></textarea>
422
+
423
+ ${this.clearable && this.value && this._isFocused ? html`
424
+ <div class="action-container">
425
+ <ds-icon-button
426
+ icon="close"
427
+ variant="action"
428
+ size="s"
429
+ aria-label="Clear textarea"
430
+ tabindex="-1"
431
+ @mousedown=${(e) => e.preventDefault()}
432
+ @click=${this._handleClear}
433
+ ></ds-icon-button>
434
+ </div>
435
+ ` : ''}
436
+ </div>
437
+
438
+ ${this.helper || errorMessage ? html`
439
+ <div id="${messageId}">
440
+ ${this.renderFieldMessage(this.helper, errorMessage)}
441
+ </div>
442
+ ` : ''}
443
+ `;
444
+
445
+ if (isInline) {
446
+ return html`
447
+ <div class="${wrapperClass}">
448
+ <div class="label-row" part="label-row" style="${labelStyle}">
449
+ <div class="label-content">
450
+ ${this.label ? html`<label for="${textareaId}">${this.label}</label>` : ''}
451
+ ${this.info ? html`
452
+ <ds-tooltip content="${this.info}" placement="top">
453
+ <ds-icon-button
454
+ icon="info"
455
+ variant="action"
456
+ size="s"
457
+ aria-label="More information"
458
+ @click=${this._handleInfoClick}
459
+ ></ds-icon-button>
460
+ </ds-tooltip>
461
+ ` : ''}
462
+ </div>
463
+ </div>
464
+ <div class="field-content">
465
+ ${showCounter ? html`
466
+ <div style="text-align: right;">
467
+ <span class="counter ${overLimit ? 'over-limit' : ''}">${charCount}/${this.maxlength}</span>
468
+ </div>
469
+ ` : ''}
470
+ ${textareaFieldContent}
471
+ </div>
472
+ </div>
473
+ `;
474
+ }
475
+
476
+ return html`
477
+ <div class="field-wrapper">
478
+ ${this.label || showCounter ? html`
479
+ <div class="label-row" part="label-row">
480
+ <div class="label-content">
481
+ ${this.label ? html`<label for="${textareaId}">${this.label}</label>` : ''}
482
+ ${this.info ? html`
483
+ <ds-tooltip content="${this.info}" placement="top">
484
+ <ds-icon-button
485
+ icon="info"
486
+ variant="action"
487
+ size="s"
488
+ aria-label="More information"
489
+ @click=${this._handleInfoClick}
490
+ ></ds-icon-button>
491
+ </ds-tooltip>
492
+ ` : ''}
493
+ </div>
494
+ ${showCounter ? html`
495
+ <span class="counter ${overLimit ? 'over-limit' : ''}">${charCount}/${this.maxlength}</span>
496
+ ` : ''}
497
+ </div>
498
+ ` : ''}
499
+ ${textareaFieldContent}
500
+ </div>
501
+ `;
502
+ }
503
+ }
504
+
505
+ customElements.define('ds-textarea', DsTextarea);