@bethinkpl/design-system 43.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 (51) hide show
  1. package/dist/design-system.css +1 -1
  2. package/dist/design-system.js +11664 -11121
  3. package/dist/design-system.js.map +1 -1
  4. package/dist/lib/js/components/Form/PinInputField/PinInputField.consts.d.ts +14 -0
  5. package/dist/lib/js/components/Form/PinInputField/PinInputField.types.d.ts +12 -0
  6. package/dist/lib/js/components/Form/PinInputField/PinInputField.utils.d.ts +2 -0
  7. package/dist/lib/js/components/Form/PinInputField/PinInputField.vue.d.ts +91 -0
  8. package/dist/lib/js/components/Form/PinInputField/index.d.ts +5 -0
  9. package/dist/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.d.ts +8 -0
  10. package/dist/lib/js/components/Form/TextAreaField/TextAreaField.types.d.ts +9 -0
  11. package/dist/lib/js/components/Form/TextAreaField/TextAreaField.vue.d.ts +76 -0
  12. package/dist/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue.d.ts +6 -0
  13. package/dist/lib/js/components/Form/TextAreaField/index.d.ts +4 -0
  14. package/dist/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue.d.ts +12 -1
  15. package/dist/lib/js/components/Toast/Toast.consts.d.ts +11 -9
  16. package/dist/lib/js/components/Toast/Toast.vue.d.ts +51 -433
  17. package/dist/lib/js/{components/Form/InputField/useInputFieldWithinForm.d.ts → composables/useTextFieldWithinForm.d.ts} +1 -1
  18. package/dist/lib/js/index.d.ts +4 -0
  19. package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.spec.ts +15 -0
  20. package/lib/js/components/Form/CheckboxGroupField/CheckboxGroupField.vue +5 -2
  21. package/lib/js/components/Form/Form.stories.ts +23 -2
  22. package/lib/js/components/Form/InputField/InputField.spec.ts +11 -1
  23. package/lib/js/components/Form/InputField/InputField.vue +7 -4
  24. package/lib/js/components/Form/PinInputField/PinInputField.consts.ts +18 -0
  25. package/lib/js/components/Form/PinInputField/PinInputField.spec.ts +394 -0
  26. package/lib/js/components/Form/PinInputField/PinInputField.stories.ts +124 -0
  27. package/lib/js/components/Form/PinInputField/PinInputField.types.ts +42 -0
  28. package/lib/js/components/Form/PinInputField/PinInputField.utils.ts +5 -0
  29. package/lib/js/components/Form/PinInputField/PinInputField.vue +211 -0
  30. package/lib/js/components/Form/PinInputField/index.ts +6 -0
  31. package/lib/js/components/Form/PinInputField/usePinInputFieldWithinForm.ts +29 -0
  32. package/lib/js/components/Form/SelectField/SelectField.stories.ts +0 -5
  33. package/lib/js/components/Form/SelectField/useSelectFieldWithinForm.ts +1 -1
  34. package/lib/js/components/Form/TextAreaField/TextAreaField.spec.ts +341 -0
  35. package/lib/js/components/Form/TextAreaField/TextAreaField.stories.ts +99 -0
  36. package/lib/js/components/Form/TextAreaField/TextAreaField.types.ts +23 -0
  37. package/lib/js/components/Form/TextAreaField/TextAreaField.vue +190 -0
  38. package/lib/js/components/Form/TextAreaField/TextAreaFieldAutosized.vue +19 -0
  39. package/lib/js/components/Form/TextAreaField/index.ts +5 -0
  40. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.spec.ts +26 -0
  41. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.stories.ts +16 -3
  42. package/lib/js/components/LabelValue/LabelValueItem/LabelValueItem.vue +5 -0
  43. package/lib/js/components/LoadingBar/LoadingBar.spec.ts +84 -0
  44. package/lib/js/components/LoadingBar/LoadingBar.vue +30 -19
  45. package/lib/js/components/Toast/Toast.consts.ts +4 -2
  46. package/lib/js/components/Toast/Toast.spec.ts +177 -0
  47. package/lib/js/components/Toast/Toast.stories.ts +65 -31
  48. package/lib/js/components/Toast/Toast.vue +176 -195
  49. package/lib/js/{components/Form/InputField/useInputFieldWithinForm.ts → composables/useTextFieldWithinForm.ts} +7 -2
  50. package/lib/js/index.ts +4 -0
  51. package/package.json +1 -1
@@ -0,0 +1,211 @@
1
+ <template>
2
+ <form-field v-bind="formFieldProps">
3
+ <template #field="{ fieldId, messageId, labelId }">
4
+ <div v-if="isLoading" class="ds-pinInputField -ds-loading" aria-busy="true">
5
+ <ds-icon
6
+ class="ds-pinInputField__spinner"
7
+ :icon="ICONS.FAD_SPINNER_THIRD"
8
+ :size="ICON_SIZES.LARGE"
9
+ spinning
10
+ />
11
+ </div>
12
+ <!--
13
+ `required` reaches a 1px-clipped, `aria-hidden` input inside reka's root, so in a
14
+ native `<form>` the browser blocks `submit` while the pin is empty and
15
+ `@submit.prevent` never runs. vee-validate forms need `novalidate`. `SelectField`
16
+ forwards it the same way.
17
+ -->
18
+ <pin-input-root
19
+ v-else
20
+ :id="fieldId"
21
+ class="ds-pinInputField"
22
+ role="group"
23
+ :model-value="chars as Array<string>"
24
+ :otp="otp"
25
+ :type="type"
26
+ :required="formFieldProps.hasRequiredIndicator"
27
+ :aria-labelledby="formFieldProps.label ? labelId : undefined"
28
+ :aria-label="formFieldProps.label ? undefined : ariaLabel"
29
+ :aria-describedby="hasMessage ? messageId : undefined"
30
+ @complete="onPinComplete"
31
+ @focusout="onFieldFocusOut"
32
+ @update:model-value="onCharsUpdate"
33
+ >
34
+ <pin-input-input
35
+ v-for="index in length"
36
+ :key="index"
37
+ class="ds-pinInputField__item"
38
+ :index="index - 1"
39
+ />
40
+ </pin-input-root>
41
+ </template>
42
+ <!-- begin: FormField slots -->
43
+ <template v-if="$slots.help" #help>
44
+ <slot name="help" />
45
+ </template>
46
+ <template v-if="$slots.labelAside" #labelAside>
47
+ <slot name="labelAside" />
48
+ </template>
49
+ <template v-if="$slots.message" #message="{ messageId }">
50
+ <slot name="message" :message-id="messageId" />
51
+ </template>
52
+ <template v-if="$slots.fieldStatus" #fieldStatus>
53
+ <slot name="fieldStatus" />
54
+ </template>
55
+ <!-- end: FormField slots -->
56
+ </form-field>
57
+ </template>
58
+
59
+ <style lang="scss" scoped>
60
+ @import '../../../../styles/settings/spacings';
61
+ @import '../../../../styles/settings/radiuses';
62
+ @import '../../../../styles/settings/borders';
63
+ @import '../../../../styles/settings/colors/tokens';
64
+ @import '../../../../styles/settings/typography/tokens';
65
+ @import '../../../../styles/settings/shadows';
66
+
67
+ .ds-pinInputField {
68
+ align-items: center;
69
+ align-self: stretch;
70
+ display: flex;
71
+ gap: $space-4;
72
+
73
+ &.-ds-loading {
74
+ flex-direction: column;
75
+ min-height: 48px;
76
+ }
77
+
78
+ &__item {
79
+ @include formText-m-default-bold;
80
+
81
+ background: $color-default-background;
82
+ border: $border-xs solid $color-neutral-border-strong;
83
+ border-radius: $radius-s;
84
+ box-shadow: $shadow-inset-m;
85
+ color: $color-neutral-text-heavy;
86
+ flex: 0 1 48px;
87
+ height: 48px;
88
+ max-width: 48px;
89
+ min-width: 24px;
90
+ outline: none;
91
+ padding: 0 $space-3;
92
+ text-align: center;
93
+
94
+ &:focus {
95
+ border-color: $color-primary-border;
96
+ }
97
+
98
+ &:hover:not(:focus) {
99
+ border-color: $color-neutral-border-strong-hovered;
100
+ }
101
+ }
102
+
103
+ &__spinner {
104
+ color: $color-neutral-icon;
105
+ }
106
+ }
107
+ </style>
108
+
109
+ <script lang="ts" setup>
110
+ import { computed, ref, watch } from 'vue';
111
+ import { PinInputInput, PinInputRoot } from 'reka-ui';
112
+ import FormField, { FORM_FIELD_STATES, FormFieldProps } from '../FormField';
113
+ import { extractFormFieldProps } from '../FormField/FormField.utils';
114
+ import DsIcon, { ICON_SIZES, ICONS } from '../../Icons/Icon';
115
+ import {
116
+ PIN_INPUT_FIELD_DEFAULT_LENGTH,
117
+ PIN_INPUT_FIELD_STATES,
118
+ PIN_INPUT_FIELD_TYPES,
119
+ } from './PinInputField.consts';
120
+ import { PinInputFieldProps, PinInputFieldSlots } from './PinInputField.types';
121
+ import { PinChars, toPinChars } from './PinInputField.utils';
122
+ import { usePinInputFieldWithinForm } from './usePinInputFieldWithinForm';
123
+
124
+ const {
125
+ length = PIN_INPUT_FIELD_DEFAULT_LENGTH,
126
+ state = PIN_INPUT_FIELD_STATES.DEFAULT,
127
+ otp = true,
128
+ type = PIN_INPUT_FIELD_TYPES.NUMBER,
129
+ ariaLabel,
130
+ name,
131
+ ...rest
132
+ } = defineProps<PinInputFieldProps>();
133
+
134
+ const slots = defineSlots<PinInputFieldSlots>();
135
+
136
+ const emit = defineEmits<{
137
+ /** Fires whenever the pin becomes complete, including programmatic fills. */
138
+ complete: [value: string];
139
+ }>();
140
+
141
+ /** The filled boxes joined, in order. A cleared middle box shortens it (`1 2 _ 4 5 6` → `'12456'`). */
142
+ const modelValue = defineModel<string>();
143
+
144
+ const { value, errors, onComplete, onBlur } = usePinInputFieldWithinForm(() => name, modelValue);
145
+
146
+ // The array, not the joined string, is the source of truth: clearing a middle box leaves a hole in
147
+ // it (`delete` in `type: 'number'` mode, `''` in `type: 'text'` mode), so rebuilding it from the
148
+ // string would shift the later boxes leftwards. In numeric mode reka also stores numbers rather than
149
+ // strings, which its own `modelValue` type does not admit — hence the cast on the binding.
150
+ const chars = ref<PinChars>(toPinChars(value.value, length));
151
+
152
+ // reka rewrites the whole array on every keystroke; `join('')` flattens the numbers it stores in
153
+ // numeric mode and the holes a cleared box leaves behind.
154
+ function onCharsUpdate(next: PinChars) {
155
+ chars.value = next;
156
+ value.value = next.join('');
157
+ }
158
+
159
+ // Inbound only: parent `v-model`, a vee-validate reset, or a shrinking `length`. The write from
160
+ // `onCharsUpdate` lands here too, but is already in sync — rebuilding would collapse a cleared
161
+ // middle box onto its neighbours.
162
+ watch([value, () => length], ([next]) => {
163
+ const nextValue = next ?? '';
164
+
165
+ if (chars.value.length <= length && chars.value.join('') === nextValue) {
166
+ return;
167
+ }
168
+
169
+ chars.value = toPinChars(next, length);
170
+
171
+ // A shrink drops the surplus characters, and the model has to follow: they no longer have a box
172
+ // to edit them in.
173
+ const joined = chars.value.join('');
174
+
175
+ if (joined !== nextValue) {
176
+ value.value = joined;
177
+ }
178
+ });
179
+
180
+ const formFieldProps = computed<FormFieldProps>(() => {
181
+ // this is needed to avoid passing modelValue to FormField as prop
182
+ return extractFormFieldProps(
183
+ {
184
+ ...rest,
185
+ state: state === PIN_INPUT_FIELD_STATES.ERROR ? FORM_FIELD_STATES.ERROR : undefined,
186
+ },
187
+ errors.value,
188
+ );
189
+ });
190
+
191
+ const isLoading = computed(() => state === PIN_INPUT_FIELD_STATES.LOADING);
192
+
193
+ // Avoids pointing `aria-describedby` at a message element that is never rendered.
194
+ const hasMessage = computed(() => !!(formFieldProps.value.messageText || slots.message));
195
+
196
+ // Boxes blur on every hop between boxes; only focus leaving the field is a real blur.
197
+ function onFieldFocusOut(event: FocusEvent) {
198
+ const root = event.currentTarget as HTMLElement | null;
199
+
200
+ if (root?.contains(event.relatedTarget as Node | null)) {
201
+ return;
202
+ }
203
+
204
+ onBlur(event);
205
+ }
206
+
207
+ function onPinComplete() {
208
+ onComplete(new Event('blur'));
209
+ emit('complete', chars.value.join(''));
210
+ }
211
+ </script>
@@ -0,0 +1,6 @@
1
+ import PinInputField from './PinInputField.vue';
2
+
3
+ export * from './PinInputField.consts';
4
+ export * from './PinInputField.types';
5
+
6
+ export default PinInputField;
@@ -0,0 +1,29 @@
1
+ import { computed, MaybeRefOrGetter, Ref } from 'vue';
2
+ import { useFormFieldWithinForm } from '../../../composables/useFormFieldWithinForm';
3
+
4
+ /**
5
+ * Pin counterpart of `useTextFieldWithinForm`. A pin has no single blur — its boxes blur on every
6
+ * hop between boxes — so the field is marked touched when the pin is completed or when focus leaves
7
+ * the field entirely.
8
+ */
9
+ export function usePinInputFieldWithinForm(
10
+ name: MaybeRefOrGetter<string | undefined>,
11
+ modelValue: Ref<string | undefined>,
12
+ ) {
13
+ const { value, errors, field } = useFormFieldWithinForm(name, modelValue);
14
+
15
+ const markTouched = (event: Event) => {
16
+ field?.handleBlur(event, true);
17
+ };
18
+
19
+ // `meta.touched` covers both callers below and `handleSubmit`, so a half-typed pin is never
20
+ // marked invalid but a submitted incomplete one still shows its error.
21
+ const visibleErrors = computed(() => (field?.meta.touched ? errors.value : []));
22
+
23
+ return {
24
+ value,
25
+ errors: visibleErrors,
26
+ onComplete: markTouched,
27
+ onBlur: markTouched,
28
+ };
29
+ }
@@ -174,7 +174,6 @@ export const WithGroups: Story = {
174
174
  ...Interactive.args,
175
175
  options: GROUPED_OPTIONS,
176
176
  },
177
- parameters: {},
178
177
  };
179
178
 
180
179
  export const WithIconsAndEyebrow: Story = {
@@ -184,7 +183,6 @@ export const WithIconsAndEyebrow: Story = {
184
183
  options: RICH_OPTIONS,
185
184
  placeholder: 'Wybierz status',
186
185
  },
187
- parameters: {},
188
186
  };
189
187
 
190
188
  /**
@@ -230,7 +228,6 @@ export const LongList: Story = {
230
228
  options: LONG_OPTIONS,
231
229
  placeholder: 'Wybierz opcję',
232
230
  },
233
- parameters: {},
234
231
  };
235
232
 
236
233
  /**
@@ -273,7 +270,6 @@ export const Disabled: Story = {
273
270
  ...Interactive.args,
274
271
  state: FORM_FIELD_STATES.DISABLED,
275
272
  },
276
- parameters: {},
277
273
  };
278
274
 
279
275
  export const Error: Story = {
@@ -283,5 +279,4 @@ export const Error: Story = {
283
279
  state: FORM_FIELD_STATES.ERROR,
284
280
  messageText: 'Error message text',
285
281
  },
286
- parameters: {},
287
282
  };
@@ -3,7 +3,7 @@ import { useFormFieldWithinForm } from '../../../composables/useFormFieldWithinF
3
3
  import { SelectFieldValue } from './SelectField.types';
4
4
 
5
5
  /**
6
- * Select counterpart of `useInputFieldWithinForm`. A select has no `input` event, so closing
6
+ * Select counterpart of `useTextFieldWithinForm`. A select has no `input` event, so closing
7
7
  * the listbox stands in for blur — that is when the field is marked touched.
8
8
  *
9
9
  * Revalidation on selection needs no handler here: vee-validate's `useField` defaults to
@@ -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
+ });