@bethinkpl/design-system 42.0.0 → 44.0.0

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 (85) hide show
  1. package/dist/design-system.css +1 -1
  2. package/dist/design-system.js +17470 -13135
  3. package/dist/design-system.js.map +1 -1
  4. package/dist/lib/js/components/Cards/Card/Card.consts.d.ts +14 -0
  5. package/dist/lib/js/components/Cards/Card/Card.vue.d.ts +10 -5
  6. package/dist/lib/js/components/Cards/CardExpandable/CardExpandable.vue.d.ts +33 -15
  7. package/dist/lib/js/components/Form/PinInputField/PinInputField.consts.d.ts +14 -0
  8. package/dist/lib/js/components/Form/PinInputField/PinInputField.types.d.ts +12 -0
  9. package/dist/lib/js/components/Form/PinInputField/PinInputField.utils.d.ts +2 -0
  10. package/dist/lib/js/components/Form/PinInputField/PinInputField.vue.d.ts +91 -0
  11. package/dist/lib/js/components/Form/PinInputField/index.d.ts +5 -0
  12. package/dist/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.d.ts +8 -0
  13. package/dist/lib/js/components/Form/SelectField/SelectField.types.d.ts +26 -0
  14. package/dist/lib/js/components/Form/SelectField/SelectField.utils.d.ts +7 -0
  15. package/dist/lib/js/components/Form/SelectField/SelectField.vue.d.ts +107 -0
  16. package/dist/lib/js/components/Form/SelectField/SelectFieldOption.vue.d.ts +19 -0
  17. package/dist/lib/js/components/Form/SelectField/index.d.ts +4 -0
  18. package/dist/lib/js/components/Form/SelectField/useSelectFieldWithinForm.d.ts +8 -0
  19. package/dist/lib/js/components/Form/TextAreaField/TextAreaField.types.d.ts +9 -0
  20. package/dist/lib/js/components/Form/TextAreaField/TextAreaField.vue.d.ts +76 -0
  21. package/dist/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue.d.ts +6 -0
  22. package/dist/lib/js/components/Form/TextAreaField/index.d.ts +4 -0
  23. package/dist/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue.d.ts +12 -1
  24. package/dist/lib/js/components/Pagination/Pagination.vue.d.ts +2 -0
  25. package/dist/lib/js/components/SelectList/SelectListItem/SelectListItem.vue.d.ts +2 -0
  26. package/dist/lib/js/components/SelectList/SelectListItemToggle/SelectListItemToggle.vue.d.ts +2 -0
  27. package/dist/lib/js/components/SurveyQuestions/SurveyQuestionOpenEnded/SurveyQuestionOpenEnded.vue.d.ts +33 -15
  28. package/dist/lib/js/components/SurveyQuestions/SurveyQuestionScale/SurveyQuestionScale.vue.d.ts +33 -15
  29. package/dist/lib/js/components/Toast/Toast.consts.d.ts +11 -9
  30. package/dist/lib/js/components/Toast/Toast.vue.d.ts +51 -415
  31. package/dist/lib/js/composables/useFormFieldWithinForm.d.ts +3 -3
  32. package/dist/lib/js/composables/useTextFieldWithinForm.d.ts +8 -0
  33. package/dist/lib/js/index.d.ts +6 -0
  34. package/lib/js/components/Cards/Card/Card.consts.ts +18 -0
  35. package/lib/js/components/Cards/Card/Card.spec.ts +159 -33
  36. package/lib/js/components/Cards/Card/Card.stories.ts +21 -2
  37. package/lib/js/components/Cards/Card/Card.vue +90 -23
  38. package/lib/js/components/Dropdown/Dropdown.vue +6 -10
  39. package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.spec.ts +15 -0
  40. package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.vue +5 -2
  41. package/lib/js/components/Form/Form.stories.ts +37 -2
  42. package/lib/js/components/Form/InputField/InputField.spec.ts +11 -1
  43. package/lib/js/components/Form/InputField/InputField.vue +7 -4
  44. package/lib/js/components/Form/PinInputField/PinInputField.consts.ts +18 -0
  45. package/lib/js/components/Form/PinInputField/PinInputField.spec.ts +394 -0
  46. package/lib/js/components/Form/PinInputField/PinInputField.stories.ts +124 -0
  47. package/lib/js/components/Form/PinInputField/PinInputField.types.ts +42 -0
  48. package/lib/js/components/Form/PinInputField/PinInputField.utils.ts +5 -0
  49. package/lib/js/components/Form/PinInputField/PinInputField.vue +211 -0
  50. package/lib/js/components/Form/PinInputField/index.ts +6 -0
  51. package/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.ts +29 -0
  52. package/lib/js/components/Form/SelectField/SelectField.spec.ts +495 -0
  53. package/lib/js/components/Form/SelectField/SelectField.stories.ts +282 -0
  54. package/lib/js/components/Form/SelectField/SelectField.types.ts +65 -0
  55. package/lib/js/components/Form/SelectField/SelectField.utils.ts +83 -0
  56. package/lib/js/components/Form/SelectField/SelectField.vue +342 -0
  57. package/lib/js/components/Form/SelectField/SelectFieldOption.vue +51 -0
  58. package/lib/js/components/Form/SelectField/index.ts +5 -0
  59. package/lib/js/components/Form/SelectField/useSelectFieldWithinForm.ts +27 -0
  60. package/lib/js/components/Form/TextAreaField/TextAreaField.spec.ts +341 -0
  61. package/lib/js/components/Form/TextAreaField/TextAreaField.stories.ts +99 -0
  62. package/lib/js/components/Form/TextAreaField/TextAreaField.types.ts +23 -0
  63. package/lib/js/components/Form/TextAreaField/TextAreaField.vue +190 -0
  64. package/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue +19 -0
  65. package/lib/js/components/Form/TextAreaField/index.ts +5 -0
  66. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.spec.ts +26 -0
  67. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.stories.ts +16 -3
  68. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue +5 -0
  69. package/lib/js/components/LoadingBar/LoadingBar.spec.ts +84 -0
  70. package/lib/js/components/LoadingBar/LoadingBar.vue +30 -19
  71. package/lib/js/components/SelectList/SelectListItem/SelectListItem.spec.ts +77 -0
  72. package/lib/js/components/SelectList/SelectListItem/SelectListItem.vue +4 -1
  73. package/lib/js/components/Toast/Toast.consts.ts +4 -2
  74. package/lib/js/components/Toast/Toast.spec.ts +177 -0
  75. package/lib/js/components/Toast/Toast.stories.ts +65 -31
  76. package/lib/js/components/Toast/Toast.vue +176 -195
  77. package/lib/js/composables/useFormFieldWithinForm.ts +4 -4
  78. package/lib/js/{components/Form/InputField/useInputFieldWithinForm.ts → composables/useTextFieldWithinForm.ts} +7 -2
  79. package/lib/js/index.ts +6 -0
  80. package/lib/js/styles/Shadows/Shadows.stories.scss +16 -0
  81. package/lib/js/styles/Shadows/Shadows.stories.ts +4 -0
  82. package/lib/styles/mixins/_dropdown-surface.scss +24 -0
  83. package/lib/styles/settings/_shadows.scss +18 -0
  84. package/package.json +1 -1
  85. package/dist/lib/js/components/Form/InputField/useInputFieldWithinForm.d.ts +0 -8
@@ -0,0 +1,341 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import TextAreaField from './TextAreaField.vue';
4
+ import TextAreaFieldAutosized from './TextAreaFieldAutosized.vue';
5
+ import { ComputedRef, Ref } from 'vue';
6
+ import { FORM_FIELD_STATES } from '../FormField';
7
+ import { ComponentProps } from 'vue-component-type-helpers';
8
+ import { FormMeta, useForm } from 'vee-validate';
9
+ import { waitForExpectShort } from '../../../tests/helpers';
10
+
11
+ const fieldId = 'form-field-v-0';
12
+ const messageId = `${fieldId}-message`;
13
+
14
+ function setup({ props }: { props?: ComponentProps<typeof TextAreaField> } = {}) {
15
+ return mount(TextAreaField, { props });
16
+ }
17
+
18
+ describe('TextAreaField', () => {
19
+ it('should render', () => {
20
+ const wrapper = setup({
21
+ props: {
22
+ label: 'Label',
23
+ messageText: 'Message text',
24
+ },
25
+ });
26
+
27
+ expect(wrapper.find('.ds-textAreaField').exists()).toBe(true);
28
+ expect(wrapper.find('textarea').exists()).toBe(true);
29
+ expect(wrapper.find('label').text()).toContain('Label');
30
+ expect(wrapper.find(`#${messageId}`).text()).toBe('Message text');
31
+ });
32
+
33
+ it('should handle value updates', async () => {
34
+ const onUpdate = vi.fn();
35
+ const wrapper = setup({
36
+ props: {
37
+ label: 'Label',
38
+ modelValue: 'initial value',
39
+ 'onUpdate:modelValue': onUpdate,
40
+ },
41
+ });
42
+
43
+ const textarea = wrapper.find<HTMLTextAreaElement>('textarea');
44
+ expect(textarea.element.value).toBe('initial value');
45
+
46
+ await textarea.setValue('updated value');
47
+
48
+ expect(onUpdate).toHaveBeenCalledWith('updated value');
49
+ expect(textarea.element.value).toBe('updated value');
50
+ });
51
+
52
+ it('should render with accessible ids', () => {
53
+ const wrapper = setup({
54
+ props: {
55
+ label: 'Label',
56
+ messageText: 'Message text',
57
+ },
58
+ });
59
+
60
+ expect(wrapper.find('label').attributes('for')).toBe(fieldId);
61
+
62
+ const textarea = wrapper.find('textarea');
63
+
64
+ expect(textarea.attributes('id')).toBe(fieldId);
65
+ expect(textarea.attributes('aria-describedby')).toBe(messageId);
66
+ expect(wrapper.find(`#${messageId}`).exists()).toBe(true);
67
+ });
68
+
69
+ it('should set aria-describedby only when a message is rendered', () => {
70
+ const wrapper = setup({
71
+ props: {
72
+ label: 'Label',
73
+ },
74
+ });
75
+
76
+ expect(wrapper.find('textarea').attributes('aria-describedby')).toBeUndefined();
77
+ });
78
+
79
+ it('should pass inputProps to the textarea element', () => {
80
+ const inputProps = {
81
+ placeholder: 'Enter text',
82
+ disabled: true,
83
+ maxlength: 500,
84
+ 'aria-label': 'Text area field',
85
+ };
86
+
87
+ const wrapper = setup({
88
+ props: {
89
+ label: 'Label',
90
+ inputProps,
91
+ },
92
+ });
93
+
94
+ const textarea = wrapper.find('textarea');
95
+ expect(textarea.attributes('placeholder')).toBe(inputProps.placeholder);
96
+ expect(textarea.attributes('disabled')).toBeDefined();
97
+ expect(textarea.attributes('maxlength')).toBe('500');
98
+ expect(textarea.attributes('aria-label')).toBe(inputProps['aria-label']);
99
+ });
100
+
101
+ it('should render 3 rows by default', () => {
102
+ const wrapper = setup();
103
+
104
+ expect(wrapper.find('textarea').attributes('rows')).toBe('3');
105
+ });
106
+
107
+ it('should let inputProps override the default rows', () => {
108
+ const wrapper = setup({
109
+ props: {
110
+ inputProps: {
111
+ rows: 6,
112
+ },
113
+ },
114
+ });
115
+
116
+ expect(wrapper.find('textarea').attributes('rows')).toBe('6');
117
+ });
118
+
119
+ it.each([
120
+ {
121
+ state: FORM_FIELD_STATES.DISABLED,
122
+ expectedClass: '-ds-disabled',
123
+ },
124
+ {
125
+ state: FORM_FIELD_STATES.ERROR,
126
+ expectedClass: '-ds-error',
127
+ },
128
+ ])('should handle state: $state', ({ state, expectedClass }) => {
129
+ const wrapper = setup({
130
+ props: {
131
+ state,
132
+ },
133
+ });
134
+ expect(wrapper.find('.ds-textAreaField').classes()).toContain(expectedClass);
135
+ });
136
+
137
+ it('should make textarea disabled when state is DISABLED', () => {
138
+ const wrapper = setup({
139
+ props: {
140
+ state: FORM_FIELD_STATES.DISABLED,
141
+ },
142
+ });
143
+ const textarea = wrapper.find('textarea');
144
+ expect(textarea.attributes('disabled')).toBeDefined();
145
+ });
146
+
147
+ describe('autoresizing', () => {
148
+ it('should render a plain textarea by default', () => {
149
+ const wrapper = setup();
150
+
151
+ expect(wrapper.findComponent(TextAreaFieldAutosized).exists()).toBe(false);
152
+ expect(wrapper.find('.ds-textAreaField').classes()).not.toContain('-ds-autoresizing');
153
+ expect(wrapper.find('textarea').exists()).toBe(true);
154
+ });
155
+
156
+ it('should render the autosized textarea when isAutoresizing', () => {
157
+ const wrapper = setup({
158
+ props: {
159
+ isAutoresizing: true,
160
+ },
161
+ });
162
+
163
+ expect(wrapper.findComponent(TextAreaFieldAutosized).exists()).toBe(true);
164
+ expect(wrapper.find('.ds-textAreaField').classes()).toContain('-ds-autoresizing');
165
+ });
166
+
167
+ it('should keep the field plumbing when autoresizing', async () => {
168
+ const onUpdate = vi.fn();
169
+ const wrapper = setup({
170
+ props: {
171
+ label: 'Label',
172
+ messageText: 'Message text',
173
+ isAutoresizing: true,
174
+ inputProps: {
175
+ rows: 4,
176
+ },
177
+ 'onUpdate:modelValue': onUpdate,
178
+ },
179
+ });
180
+
181
+ const textarea = wrapper.find('textarea');
182
+ expect(textarea.attributes('id')).toBe(fieldId);
183
+ expect(textarea.attributes('aria-describedby')).toBe(messageId);
184
+ expect(textarea.attributes('rows')).toBe('4');
185
+ expect(textarea.classes()).toContain('ds-textAreaField__input');
186
+
187
+ await textarea.setValue('updated value');
188
+
189
+ expect(onUpdate).toHaveBeenCalledWith('updated value');
190
+ });
191
+ });
192
+
193
+ describe('with vee-validate', () => {
194
+ const fieldName = 'siema';
195
+ const initialValue = 'siemanko!';
196
+ function setupWithForm(props?: ComponentProps<typeof TextAreaField>) {
197
+ let controlledValuesRef: Ref<{ siema: string }> | undefined;
198
+ let errorsRef: ComputedRef<Partial<Record<'siema', string | undefined>>> | undefined;
199
+ let metaRef: Ref<FormMeta<{ siema: string }>> | undefined;
200
+
201
+ const FormComponent = {
202
+ template: `
203
+ <form>
204
+ <TextAreaField
205
+ v-bind="props"
206
+ :name="name"
207
+ />
208
+ </form>
209
+ `,
210
+ components: { TextAreaField },
211
+ setup() {
212
+ const { controlledValues, errors, meta } = useForm({
213
+ initialValues: {
214
+ [fieldName]: initialValue,
215
+ },
216
+ validationSchema: {
217
+ siema: (val: string) => (val.length < 5 ? 'Too short' : true),
218
+ },
219
+ });
220
+
221
+ controlledValuesRef = controlledValues;
222
+ errorsRef = errors;
223
+ metaRef = meta;
224
+
225
+ return {
226
+ name: fieldName,
227
+ props,
228
+ };
229
+ },
230
+ };
231
+
232
+ return {
233
+ wrapper: mount(FormComponent),
234
+ controlledValuesRef,
235
+ errorsRef,
236
+ metaRef,
237
+ };
238
+ }
239
+
240
+ it('should bind textarea value with vee-validate form values', async () => {
241
+ const { wrapper, controlledValuesRef } = setupWithForm();
242
+
243
+ const textarea = wrapper.find('textarea');
244
+
245
+ expect(textarea.element.value).toBe(initialValue);
246
+ expect(controlledValuesRef?.value[fieldName]).toBe(initialValue);
247
+
248
+ const newValue = 'no cześć!';
249
+ await textarea.setValue(newValue);
250
+
251
+ expect(textarea.element.value).toBe(newValue);
252
+ expect(controlledValuesRef?.value[fieldName]).toBe(newValue);
253
+ });
254
+
255
+ it('should call onBlur and mark form as touched on blur', async () => {
256
+ const onBlur = vi.fn();
257
+ const { wrapper, metaRef } = setupWithForm({
258
+ inputProps: {
259
+ onBlur,
260
+ },
261
+ });
262
+ expect(metaRef?.value.touched).toBe(false);
263
+
264
+ const textarea = wrapper.find('textarea');
265
+ await textarea.trigger('blur');
266
+
267
+ expect(onBlur).toHaveBeenCalled();
268
+ expect(metaRef?.value.touched).toBe(true);
269
+ });
270
+
271
+ it('should call onInput and show error message', async () => {
272
+ const onInput = vi.fn();
273
+ const { wrapper, errorsRef } = setupWithForm({
274
+ inputProps: {
275
+ onInput,
276
+ },
277
+ });
278
+
279
+ const textarea = wrapper.find('textarea');
280
+ await textarea.setValue('test');
281
+
282
+ expect(onInput).toHaveBeenCalled();
283
+
284
+ await textarea.trigger('blur');
285
+
286
+ await waitForExpectShort(() => {
287
+ expect(errorsRef?.value?.siema).toBeDefined();
288
+ });
289
+
290
+ expect(wrapper.find('.ds-textAreaField').classes()).toContain('-ds-error');
291
+ expect(wrapper.find(`#${messageId}`).text()).toBe('Too short');
292
+
293
+ await textarea.setValue('valid value');
294
+
295
+ await waitForExpectShort(() => {
296
+ expect(errorsRef?.value?.siema).toBeUndefined();
297
+ });
298
+ });
299
+
300
+ it('should bind the autosized textarea with vee-validate form values', async () => {
301
+ const { wrapper, controlledValuesRef } = setupWithForm({ isAutoresizing: true });
302
+
303
+ const textarea = wrapper.find('textarea');
304
+ expect(textarea.element.value).toBe(initialValue);
305
+
306
+ const newValue = 'no cześć!';
307
+ await textarea.setValue(newValue);
308
+
309
+ expect(controlledValuesRef?.value[fieldName]).toBe(newValue);
310
+ });
311
+
312
+ it('should throw an error if used outside of a form context', () => {
313
+ expect(() => {
314
+ setup({
315
+ props: {
316
+ name: 'siema',
317
+ },
318
+ });
319
+ }).toThrowError();
320
+ });
321
+
322
+ it('should keep an explicit state and message over a vee-validate error', async () => {
323
+ const { wrapper, errorsRef } = setupWithForm({
324
+ state: FORM_FIELD_STATES.SUCCESS,
325
+ messageText: 'Success message',
326
+ });
327
+
328
+ const textarea = wrapper.find('textarea');
329
+ await textarea.setValue('test');
330
+
331
+ await textarea.trigger('blur');
332
+
333
+ await waitForExpectShort(() => {
334
+ expect(errorsRef?.value?.siema).toBeDefined();
335
+ });
336
+
337
+ expect(wrapper.find(`#${messageId}`).classes()).toContain('-ds-success');
338
+ expect(wrapper.find(`#${messageId}`).text()).toBe('Success message');
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,99 @@
1
+ import { Meta, StoryObj } from '@storybook/vue3';
2
+ import TextAreaField from './TextAreaField.vue';
3
+ import HelpButton from '../../Buttons/HelpButton/HelpButton.vue';
4
+ import Modal from '../../Modals/Modal';
5
+ import { args, argTypes } from '../FormField/FormField.stories.shared';
6
+ import { FORM_FIELD_STATES } from '../FormField/FormField.consts';
7
+ import { reactive, toRefs } from 'vue';
8
+
9
+ const meta: Meta<typeof TextAreaField> = {
10
+ title: 'Components/Form/TextAreaField',
11
+ component: TextAreaField,
12
+ render: (args) => ({
13
+ components: { TextAreaField, HelpButton, Modal },
14
+ setup() {
15
+ const { help, labelAside, message, fieldStatus, ...restRefs } = toRefs(args);
16
+ const props = reactive({ ...restRefs });
17
+
18
+ return {
19
+ props,
20
+ labelAside,
21
+ fieldStatus,
22
+ message,
23
+ help,
24
+ FORM_FIELD_STATES,
25
+ };
26
+ },
27
+ data: () => ({
28
+ value: '',
29
+ }),
30
+ template: `<TextAreaField v-bind="props" v-model="value">
31
+ <template v-if="help" #help>
32
+ <HelpButton :is-disabled="props.state === FORM_FIELD_STATES.DISABLED" modal-title="Help modal title">
33
+ <template #modalContent>
34
+ Modal
35
+ </template>
36
+ </HelpButton>
37
+ </template>
38
+ <template #labelAside v-if="labelAside">
39
+ <div v-html="labelAside" />
40
+ </template>
41
+ <template #fieldStatus v-if="fieldStatus">
42
+ <div v-html="fieldStatus" />
43
+ </template>
44
+ <template #message v-if="message">
45
+ <div v-if="message" v-html="message" />
46
+ </template>
47
+ </TextAreaField>`,
48
+ }),
49
+ argTypes: {
50
+ ...argTypes,
51
+ inputProps: {
52
+ control: 'object',
53
+ },
54
+ isAutoresizing: {
55
+ control: 'boolean',
56
+ },
57
+ },
58
+ };
59
+ export default meta;
60
+
61
+ type Story = StoryObj<typeof TextAreaField>;
62
+
63
+ export const Interactive: Story = {
64
+ args: {
65
+ ...args,
66
+ isAutoresizing: false,
67
+ inputProps: {
68
+ rows: 3,
69
+ placeholder: 'TextArea placeholder',
70
+ },
71
+ },
72
+ parameters: {
73
+ design: {
74
+ type: 'figma',
75
+ url: 'https://www.figma.com/design/izQdYyiBR1GQgFkaOIfIJI/LMS---DS-Components?node-id=13216-6572',
76
+ },
77
+ },
78
+ };
79
+
80
+ /**
81
+ * The field grows with its content instead of showing the resize handle. `rows` stays the starting
82
+ * height, and the field grows up to 500px, after which it scrolls.
83
+ */
84
+ export const Autoresizing: Story = {
85
+ ...Interactive,
86
+ args: {
87
+ ...Interactive.args,
88
+ isAutoresizing: true,
89
+ },
90
+ };
91
+
92
+ export const Error: Story = {
93
+ ...Interactive,
94
+ args: {
95
+ ...Interactive.args,
96
+ state: FORM_FIELD_STATES.ERROR,
97
+ messageText: 'Error message text',
98
+ },
99
+ };
@@ -0,0 +1,23 @@
1
+ import { TextareaHTMLAttributes } from 'vue';
2
+ import { FormFieldProps, FormFieldSlots } from '../FormField';
3
+
4
+ export interface TextAreaFieldProps extends FormFieldProps {
5
+ /**
6
+ * Forwarded to the `<textarea>`, so this is also where `rows` (default 3), `placeholder` and
7
+ * `maxlength` go. Overrides the defaults the component applies.
8
+ */
9
+ inputProps?: TextareaHTMLAttributes;
10
+ /**
11
+ * Grows the field with its content instead of showing the resize handle. `rows` stays the
12
+ * starting height, and the field grows up to 500px, after which it scrolls.
13
+ *
14
+ * @default false
15
+ */
16
+ isAutoresizing?: boolean;
17
+ /**
18
+ * vee-validate field name. Requires a surrounding form context, e.g. from `useForm()`.
19
+ */
20
+ name?: string;
21
+ }
22
+
23
+ export type TextAreaFieldSlots = Omit<FormFieldSlots, 'field'>;
@@ -0,0 +1,190 @@
1
+ <template>
2
+ <form-field v-bind="formFieldProps">
3
+ <template #field="{ fieldId, messageId }">
4
+ <div
5
+ :class="[
6
+ 'ds-textAreaField',
7
+ {
8
+ '-ds-autoresizing': isAutoresizing,
9
+ '-ds-error': formFieldProps.state === FORM_FIELD_STATES.ERROR,
10
+ '-ds-disabled': isDisabled,
11
+ },
12
+ ]"
13
+ >
14
+ <text-area-field-autosized
15
+ v-if="isAutoresizing"
16
+ v-bind="finalInputProps"
17
+ :id="fieldId"
18
+ v-model="value"
19
+ class="ds-textAreaField__input"
20
+ :aria-describedby="hasMessage ? messageId : undefined"
21
+ />
22
+ <textarea
23
+ v-else
24
+ v-bind="finalInputProps"
25
+ :id="fieldId"
26
+ v-model="value"
27
+ class="ds-textAreaField__input"
28
+ :aria-describedby="hasMessage ? messageId : undefined"
29
+ />
30
+ </div>
31
+ </template>
32
+ <!-- begin: FormField slots -->
33
+ <template v-if="$slots.help" #help>
34
+ <slot name="help" />
35
+ </template>
36
+ <template v-if="$slots.labelAside" #labelAside>
37
+ <slot name="labelAside" />
38
+ </template>
39
+ <template v-if="$slots.message" #message="{ messageId }">
40
+ <slot name="message" :message-id="messageId" />
41
+ </template>
42
+ <template v-if="$slots.fieldStatus" #fieldStatus>
43
+ <slot name="fieldStatus" />
44
+ </template>
45
+ <!-- end: FormField slots -->
46
+ </form-field>
47
+ </template>
48
+
49
+ <style lang="scss" scoped>
50
+ @import '../../../../styles/settings/spacings';
51
+ @import '../../../../styles/settings/radiuses';
52
+ @import '../../../../styles/settings/borders';
53
+ @import '../../../../styles/settings/colors/tokens';
54
+ @import '../../../../styles/settings/typography/tokens';
55
+ @import '../../../../styles/settings/shadows';
56
+
57
+ $input-padding: $space-4;
58
+ $autoresizing-max-height: 500px;
59
+
60
+ .ds-textAreaField {
61
+ $root: &;
62
+
63
+ align-self: stretch;
64
+ background: $color-default-background;
65
+ border: $border-xs solid $color-neutral-border-strong;
66
+ border-radius: $radius-s;
67
+ box-shadow: $shadow-inset-m;
68
+ display: flex;
69
+ overflow: hidden;
70
+
71
+ &__input {
72
+ @include formText-s-default-regular;
73
+
74
+ background: transparent;
75
+ border: none;
76
+ color: $color-neutral-text-heavy;
77
+ flex: 1;
78
+ min-height: 32px;
79
+ min-width: 0;
80
+ outline: none;
81
+ padding: $input-padding;
82
+ resize: vertical;
83
+
84
+ &::placeholder {
85
+ color: $color-neutral-text-weak;
86
+ }
87
+ }
88
+
89
+ &:focus-within {
90
+ border-color: $color-primary-border;
91
+ }
92
+
93
+ &.-ds-error:not(:focus-within) {
94
+ border-color: $color-danger-border;
95
+ }
96
+
97
+ &:hover:not(.-ds-disabled):not(:focus-within) {
98
+ border-color: $color-neutral-border-strong-hovered;
99
+ }
100
+
101
+ &.-ds-autoresizing {
102
+ #{$root}__input {
103
+ max-height: $autoresizing-max-height;
104
+ // The autosize writes an explicit `height`, which makes the browser ignore `rows`, so
105
+ // the starting height has to be restated here. Capped as well, since `min-height` would
106
+ // otherwise win over `max-height`.
107
+ min-height: min(
108
+ #{$autoresizing-max-height},
109
+ calc(v-bind(rows) * #{$typography-line-height-2xs} + #{$input-padding * 2})
110
+ );
111
+ overflow-y: auto;
112
+ resize: none;
113
+ }
114
+ }
115
+
116
+ &.-ds-disabled {
117
+ border-color: $color-neutral-border-strong-disabled;
118
+ box-shadow: none;
119
+
120
+ #{$root}__input {
121
+ color: $color-neutral-text-heavy-disabled;
122
+
123
+ &::placeholder {
124
+ color: $color-neutral-text-weak-disabled;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ </style>
130
+
131
+ <script lang="ts" setup>
132
+ import { computed, TextareaHTMLAttributes } from 'vue';
133
+ import FormField, { FORM_FIELD_STATES, FormFieldProps } from '../FormField';
134
+ import { extractFormFieldProps } from '../FormField/FormField.utils';
135
+ import TextAreaFieldAutosized from './TextAreaFieldAutosized.vue';
136
+ import { TextAreaFieldProps, TextAreaFieldSlots } from './TextAreaField.types';
137
+ import { useTextFieldWithinForm } from '../../../composables/useTextFieldWithinForm';
138
+
139
+ const DEFAULT_ROWS = 3;
140
+
141
+ const { inputProps, isAutoresizing = false, name, ...rest } = defineProps<TextAreaFieldProps>();
142
+
143
+ const slots = defineSlots<TextAreaFieldSlots>();
144
+ const modelValue = defineModel<string>();
145
+
146
+ const {
147
+ value,
148
+ errors,
149
+ onInput: onFormFieldInput,
150
+ onBlur: onFormFieldBlur,
151
+ } = useTextFieldWithinForm(() => name, modelValue);
152
+
153
+ const { formFieldProps, isDisabled, hasMessage } = useFormFieldState();
154
+ const { finalInputProps } = useTextArea();
155
+
156
+ // Used by the `min-height` of the autoresizing field, see the style block.
157
+ const rows = computed(() => finalInputProps.value.rows);
158
+
159
+ function useFormFieldState() {
160
+ const formFieldProps = computed<FormFieldProps>(() =>
161
+ // this is needed to avoid passing modelValue to FormField as prop
162
+ extractFormFieldProps(rest, errors.value),
163
+ );
164
+
165
+ const isDisabled = computed(() => formFieldProps.value.state === FORM_FIELD_STATES.DISABLED);
166
+
167
+ // Avoids pointing `aria-describedby` at a message element that is never rendered.
168
+ const hasMessage = computed(() => !!(formFieldProps.value.messageText || slots.message));
169
+
170
+ return { formFieldProps, isDisabled, hasMessage };
171
+ }
172
+
173
+ function useTextArea() {
174
+ const finalInputProps = computed<TextareaHTMLAttributes>(() => ({
175
+ disabled: isDisabled.value,
176
+ rows: DEFAULT_ROWS,
177
+ ...inputProps,
178
+ onInput: (event: Event) => {
179
+ onFormFieldInput();
180
+ inputProps?.onInput?.(event);
181
+ },
182
+ onBlur: (event: FocusEvent) => {
183
+ onFormFieldBlur(event);
184
+ inputProps?.onBlur?.(event);
185
+ },
186
+ }));
187
+
188
+ return { finalInputProps };
189
+ }
190
+ </script>
@@ -0,0 +1,19 @@
1
+ <template>
2
+ <textarea ref="textarea" v-model="model" />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ import { computed, useTemplateRef } from 'vue';
7
+ import { useTextareaAutosize } from '@vueuse/core';
8
+
9
+ const model = defineModel<string>();
10
+
11
+ const textareaEl = useTemplateRef<HTMLTextAreaElement>('textarea');
12
+
13
+ // Writes `height`, so the `min-height` / `max-height` from the CSS stay in charge of the bounds —
14
+ // `min-height` would otherwise win over `max-height` and let the field grow past the cap.
15
+ useTextareaAutosize({
16
+ element: textareaEl,
17
+ input: computed(() => model.value ?? ''),
18
+ });
19
+ </script>
@@ -0,0 +1,5 @@
1
+ import TextAreaField from './TextAreaField.vue';
2
+
3
+ export * from './TextAreaField.types';
4
+
5
+ export default TextAreaField;