@volverjs/ui-vue 0.0.10-beta.54 → 0.0.10-beta.56
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.
- package/auto-imports.d.ts +3 -0
- package/dist/components/VvCombobox/VvCombobox.es.js +357 -357
- package/dist/components/VvCombobox/VvCombobox.umd.js +1 -1
- package/dist/components/VvInputText/VvInputText.es.js +150 -44
- package/dist/components/VvInputText/VvInputText.umd.js +1 -1
- package/dist/components/VvInputText/VvInputText.vue.d.ts +6 -6
- package/dist/components/VvInputText/index.d.ts +1 -1
- package/dist/components/VvTextarea/VvTextarea.es.js +966 -67
- package/dist/components/VvTextarea/VvTextarea.umd.js +1 -1
- package/dist/components/VvTextarea/VvTextarea.vue.d.ts +52 -0
- package/dist/components/VvTextarea/index.d.ts +37 -1
- package/dist/components/index.es.js +542 -284
- package/dist/components/index.umd.js +1 -1
- package/dist/icons.es.js +3 -3
- package/dist/icons.umd.js +1 -1
- package/dist/stories/InputText/InputText.test.d.ts +1 -0
- package/dist/stories/InputText/InputTextIso.stories.d.ts +10 -0
- package/dist/utils/DateUtilities.d.ts +22 -0
- package/package.json +22 -22
- package/src/assets/icons/detailed.json +1 -1
- package/src/assets/icons/normal.json +1 -1
- package/src/assets/icons/simple.json +1 -1
- package/src/components/VvCombobox/VvCombobox.vue +3 -3
- package/src/components/VvInputText/VvInputText.vue +103 -63
- package/src/components/VvInputText/index.ts +1 -1
- package/src/components/VvTextarea/VvTextarea.vue +108 -5
- package/src/components/VvTextarea/index.ts +32 -1
- package/src/stories/InputText/InputText.test.ts +25 -0
- package/src/stories/InputText/InputTextIso.stories.ts +69 -0
- package/src/utils/DateUtilities.ts +98 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T extends string | Option">
|
|
2
2
|
import type { Ref } from 'vue'
|
|
3
|
-
import { toRefs } from 'vue'
|
|
4
3
|
import type { Option } from '../../types/generic'
|
|
4
|
+
import { toRefs } from 'vue'
|
|
5
|
+
import { useVvComboboxProps, type VvComboboxEvents } from '.'
|
|
5
6
|
import { DropdownRole } from '../../constants'
|
|
6
7
|
import HintSlotFactory from '../common/HintSlot'
|
|
7
8
|
import VvBadge from '../VvBadge/VvBadge.vue'
|
|
@@ -11,7 +12,6 @@ import VvDropdownOptgroup from '../VvDropdown/VvDropdownOptgroup.vue'
|
|
|
11
12
|
import VvDropdownOption from '../VvDropdown/VvDropdownOption.vue'
|
|
12
13
|
import VvIcon from '../VvIcon/VvIcon.vue'
|
|
13
14
|
import VvSelect from '../VvSelect/VvSelect.vue'
|
|
14
|
-
import { type VvComboboxEvents, useVvComboboxProps } from '.'
|
|
15
15
|
|
|
16
16
|
// props, emit and slots
|
|
17
17
|
// WARNING: This is a provisiaonal implementation, it may change in the future
|
|
@@ -31,7 +31,7 @@ const propsDefaults = useDefaults<typeof VvComboboxProps>(
|
|
|
31
31
|
const inputEl: Ref<HTMLElement | null> = ref(null)
|
|
32
32
|
const inputSearchEl: Ref<HTMLElement | null> = ref(null)
|
|
33
33
|
const wrapperEl: Ref<HTMLElement | null> = ref(null)
|
|
34
|
-
const dropdownEl = ref<typeof VvDropdown
|
|
34
|
+
const dropdownEl = ref<InstanceType<typeof VvDropdown>>()
|
|
35
35
|
|
|
36
36
|
// hint slot
|
|
37
37
|
const {
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import type { MaskedNumberOptions } from 'imask'
|
|
3
3
|
import type { InputHTMLAttributes } from 'vue'
|
|
4
4
|
import { useIMask } from 'vue-imask'
|
|
5
|
+
import {
|
|
6
|
+
getDateFromInputValue,
|
|
7
|
+
getInputValueFromDate,
|
|
8
|
+
isDateIsoString,
|
|
9
|
+
} from '../../utils/DateUtilities'
|
|
5
10
|
import HintSlotFactory from '../common/HintSlot'
|
|
6
11
|
import VvDropdown from '../VvDropdown/VvDropdown.vue'
|
|
7
12
|
import VvDropdownOption from '../VvDropdown/VvDropdownOption.vue'
|
|
@@ -62,9 +67,20 @@ const localModelValue = useDebouncedInput(
|
|
|
62
67
|
debounce?.value ?? 0,
|
|
63
68
|
)
|
|
64
69
|
|
|
70
|
+
// seconds
|
|
71
|
+
const hasSeconds = computed(() => {
|
|
72
|
+
const stepValue = typeof step.value === 'number' ? step.value : Number.parseInt(step.value)
|
|
73
|
+
if (Number.isNaN(stepValue)) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
return stepValue % 60 !== 0
|
|
77
|
+
})
|
|
78
|
+
|
|
65
79
|
// mask
|
|
66
80
|
const NEGATIVE_ZERO_REGEX = /^-0?[.,]?[0*]?$/
|
|
67
81
|
const maskReady = ref(false)
|
|
82
|
+
const modelValueDate = ref<Date>()
|
|
83
|
+
const modelValueDateIsoString = ref<string>()
|
|
68
84
|
const { el, mask, typed, masked, unmasked } = useIMask(
|
|
69
85
|
computed(
|
|
70
86
|
() => {
|
|
@@ -115,56 +131,62 @@ const { el, mask, typed, masked, unmasked } = useIMask(
|
|
|
115
131
|
localModelValue.value = typed.value
|
|
116
132
|
return
|
|
117
133
|
}
|
|
118
|
-
if (type.value === INPUT_TYPES.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
let date = typed.value
|
|
127
|
-
if (date === null || date === '') {
|
|
134
|
+
if (type.value === INPUT_TYPES.DATETIME_LOCAL
|
|
135
|
+
|| type.value === INPUT_TYPES.DATE
|
|
136
|
+
|| type.value === INPUT_TYPES.TIME
|
|
137
|
+
|| type.value === INPUT_TYPES.MONTH
|
|
138
|
+
) {
|
|
139
|
+
if (!typed.value) {
|
|
128
140
|
if (!localModelValue.value) {
|
|
129
141
|
return
|
|
130
142
|
}
|
|
143
|
+
if (modelValueDate.value) {
|
|
144
|
+
localModelValue.value = undefined
|
|
145
|
+
return
|
|
146
|
+
}
|
|
131
147
|
localModelValue.value = ''
|
|
132
148
|
return
|
|
133
149
|
}
|
|
134
|
-
if (!(
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
localModelValue.value = `${date.getFullYear()}-${(
|
|
138
|
-
`0${
|
|
139
|
-
date.getMonth() + 1}`
|
|
140
|
-
).slice(-2)}-${(`0${date.getDate()}`).slice(-2)}`
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
if (type.value === INPUT_TYPES.DATETIME_LOCAL) {
|
|
144
|
-
if (
|
|
145
|
-
el.value instanceof HTMLInputElement
|
|
146
|
-
&& el.value.type === 'datetime-local'
|
|
147
|
-
) {
|
|
148
|
-
localModelValue.value = el.value.value
|
|
150
|
+
if (!(typed.value instanceof Date) && !modelValueDate.value && !modelValueDateIsoString.value) {
|
|
151
|
+
localModelValue.value = typed.value
|
|
149
152
|
return
|
|
150
153
|
}
|
|
154
|
+
|
|
151
155
|
let date = typed.value
|
|
152
|
-
if (date
|
|
153
|
-
|
|
156
|
+
if (!(date instanceof Date)) {
|
|
157
|
+
date = getDateFromInputValue(typed.value, type.value)
|
|
158
|
+
}
|
|
159
|
+
if (modelValueDate.value || modelValueDateIsoString.value) {
|
|
160
|
+
const toReturn = new Date(modelValueDate.value || modelValueDateIsoString.value as string)
|
|
161
|
+
if (type.value === INPUT_TYPES.DATETIME_LOCAL
|
|
162
|
+
|| type.value === INPUT_TYPES.DATE
|
|
163
|
+
|| type.value === INPUT_TYPES.MONTH
|
|
164
|
+
) {
|
|
165
|
+
toReturn.setFullYear(date.getFullYear())
|
|
166
|
+
toReturn.setMonth(date.getMonth())
|
|
167
|
+
}
|
|
168
|
+
if (type.value === INPUT_TYPES.DATETIME_LOCAL
|
|
169
|
+
|| type.value === INPUT_TYPES.DATE
|
|
170
|
+
) {
|
|
171
|
+
toReturn.setDate(date.getDate())
|
|
172
|
+
}
|
|
173
|
+
if (type.value === INPUT_TYPES.DATETIME_LOCAL
|
|
174
|
+
|| type.value === INPUT_TYPES.TIME) {
|
|
175
|
+
toReturn.setHours(date.getHours())
|
|
176
|
+
toReturn.setMinutes(date.getMinutes())
|
|
177
|
+
toReturn.setSeconds(date.getSeconds())
|
|
178
|
+
}
|
|
179
|
+
if (modelValueDate.value instanceof Date) {
|
|
180
|
+
if (localModelValue.value?.getTime() === toReturn.getTime()) {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
localModelValue.value = toReturn
|
|
154
184
|
return
|
|
155
185
|
}
|
|
156
|
-
localModelValue.value =
|
|
186
|
+
localModelValue.value = toReturn.toISOString()
|
|
157
187
|
return
|
|
158
188
|
}
|
|
159
|
-
|
|
160
|
-
date = new Date(date)
|
|
161
|
-
}
|
|
162
|
-
localModelValue.value = `${date.getFullYear()}-${(
|
|
163
|
-
`0${
|
|
164
|
-
date.getMonth() + 1}`
|
|
165
|
-
).slice(-2)}-${(`0${date.getDate()}`).slice(-2)}T${(
|
|
166
|
-
`0${date.getHours()}`
|
|
167
|
-
).slice(-2)}:${(`0${date.getMinutes()}`).slice(-2)}`
|
|
189
|
+
localModelValue.value = getInputValueFromDate(date, type.value, hasSeconds.value)
|
|
168
190
|
return
|
|
169
191
|
}
|
|
170
192
|
if (!localModelValue.value && !unmasked.value) {
|
|
@@ -174,14 +196,14 @@ const { el, mask, typed, masked, unmasked } = useIMask(
|
|
|
174
196
|
},
|
|
175
197
|
},
|
|
176
198
|
)
|
|
177
|
-
function updateMaskValue(newValue: string | number | undefined | null) {
|
|
199
|
+
function updateMaskValue(newValue: string | number | Date | undefined | null) {
|
|
178
200
|
if (newValue === undefined || newValue === null) {
|
|
179
201
|
typed.value = ''
|
|
180
202
|
unmasked.value = ''
|
|
181
203
|
return
|
|
182
204
|
}
|
|
183
205
|
if (props.iMask?.mask === Date) {
|
|
184
|
-
typed.value = new Date(newValue)
|
|
206
|
+
typed.value = newValue instanceof Date ? newValue : new Date(newValue)
|
|
185
207
|
return
|
|
186
208
|
}
|
|
187
209
|
if (
|
|
@@ -191,6 +213,27 @@ function updateMaskValue(newValue: string | number | undefined | null) {
|
|
|
191
213
|
) {
|
|
192
214
|
return
|
|
193
215
|
}
|
|
216
|
+
if (type.value === INPUT_TYPES.DATE
|
|
217
|
+
|| type.value === INPUT_TYPES.MONTH
|
|
218
|
+
|| type.value === INPUT_TYPES.DATETIME_LOCAL
|
|
219
|
+
|| type.value === INPUT_TYPES.TIME) {
|
|
220
|
+
if (newValue instanceof Date || isDateIsoString(newValue)) {
|
|
221
|
+
if (newValue instanceof Date) {
|
|
222
|
+
modelValueDate.value = newValue
|
|
223
|
+
modelValueDateIsoString.value = undefined
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
modelValueDateIsoString.value = newValue as string
|
|
227
|
+
modelValueDate.value = undefined
|
|
228
|
+
}
|
|
229
|
+
const newDate = new Date(newValue)
|
|
230
|
+
typed.value = getInputValueFromDate(newDate, type.value, hasSeconds.value)
|
|
231
|
+
unmasked.value = typed.value
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
modelValueDate.value = undefined
|
|
235
|
+
modelValueDateIsoString.value = undefined
|
|
236
|
+
}
|
|
194
237
|
typed.value = newValue
|
|
195
238
|
unmasked.value = `${typed.value}`
|
|
196
239
|
}
|
|
@@ -219,7 +262,7 @@ watch(
|
|
|
219
262
|
const inputEl = el as Ref<HTMLInputElement>
|
|
220
263
|
const innerEl = ref<HTMLInputElement>()
|
|
221
264
|
const wrapperEl = ref<HTMLDivElement>()
|
|
222
|
-
const
|
|
265
|
+
const suggestionsDropdownEl = ref<InstanceType<typeof VvDropdown>>()
|
|
223
266
|
|
|
224
267
|
defineExpose({ $inner: innerEl })
|
|
225
268
|
|
|
@@ -232,26 +275,24 @@ watch(isFocused, (newValue) => {
|
|
|
232
275
|
if (newValue && propsDefaults.value.selectOnFocus && inputEl.value) {
|
|
233
276
|
inputEl.value.select()
|
|
234
277
|
}
|
|
235
|
-
if (newValue) {
|
|
236
|
-
|
|
278
|
+
if (newValue && suggestions.value?.size) {
|
|
279
|
+
suggestionsDropdownEl.value?.show()
|
|
237
280
|
return
|
|
238
281
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
suggestions.value
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
}
|
|
252
|
-
suggestions.value.add(localModelValue.value)
|
|
282
|
+
if (isDirty.value && suggestions.value) {
|
|
283
|
+
const suggestionsLimit = props.maxSuggestions
|
|
284
|
+
if (
|
|
285
|
+
suggestions.value.size >= suggestionsLimit
|
|
286
|
+
&& !suggestions.value.has(localModelValue.value)
|
|
287
|
+
) {
|
|
288
|
+
suggestions.value = new Set(
|
|
289
|
+
[...suggestions.value].slice(
|
|
290
|
+
suggestions.value.size - suggestionsLimit + 1,
|
|
291
|
+
),
|
|
292
|
+
)
|
|
253
293
|
}
|
|
254
|
-
|
|
294
|
+
suggestions.value.add(localModelValue.value)
|
|
295
|
+
}
|
|
255
296
|
})
|
|
256
297
|
|
|
257
298
|
// visibility
|
|
@@ -386,7 +427,7 @@ const hasSuggestions = computed(
|
|
|
386
427
|
)
|
|
387
428
|
function onSuggestionSelect(suggestion: string) {
|
|
388
429
|
localModelValue.value = suggestion
|
|
389
|
-
|
|
430
|
+
suggestionsDropdownEl.value?.hide()
|
|
390
431
|
}
|
|
391
432
|
function onSuggestionRemove(suggestion: string) {
|
|
392
433
|
suggestions.value?.delete(suggestion)
|
|
@@ -531,10 +572,9 @@ const hasStyle = computed(() => {
|
|
|
531
572
|
return undefined
|
|
532
573
|
}
|
|
533
574
|
return {
|
|
534
|
-
width:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
: undefined,
|
|
575
|
+
width: localModelValue.value !== undefined
|
|
576
|
+
? `${String(localModelValue.value).length + 1}ch`
|
|
577
|
+
: undefined,
|
|
538
578
|
}
|
|
539
579
|
})
|
|
540
580
|
|
|
@@ -651,7 +691,7 @@ export default {
|
|
|
651
691
|
</HintSlot>
|
|
652
692
|
<VvDropdown
|
|
653
693
|
v-if="hasSuggestions"
|
|
654
|
-
ref="
|
|
694
|
+
ref="suggestionsDropdownEl"
|
|
655
695
|
:reference="wrapperEl"
|
|
656
696
|
:autofocus-first="false"
|
|
657
697
|
:trigger-width="true"
|
|
@@ -45,7 +45,7 @@ export const VvInputTextProps = {
|
|
|
45
45
|
* Input value
|
|
46
46
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#value
|
|
47
47
|
*/
|
|
48
|
-
modelValue: [String, Number],
|
|
48
|
+
modelValue: [String, Number, Date],
|
|
49
49
|
/**
|
|
50
50
|
* Type of form control
|
|
51
51
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#type
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import type { TextareaHTMLAttributes } from 'vue'
|
|
3
3
|
import { VvTextareaEvents, VvTextareaProps } from '.'
|
|
4
4
|
import HintSlotFactory from '../common/HintSlot'
|
|
5
|
+
import VvDropdown from '../VvDropdown/VvDropdown.vue'
|
|
6
|
+
import VvDropdownOption from '../VvDropdown/VvDropdownOption.vue'
|
|
5
7
|
import VvIcon from '../VvIcon/VvIcon.vue'
|
|
6
8
|
|
|
7
9
|
// props, emit and slots
|
|
@@ -19,13 +21,17 @@ const propsDefaults = useDefaults<typeof VvTextareaProps>(
|
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
// template refs
|
|
22
|
-
const
|
|
24
|
+
const textareaEl = ref<HTMLTextAreaElement>()
|
|
25
|
+
const wrapperEl = ref<HTMLDivElement>()
|
|
26
|
+
const suggestionsDropdownEl = ref<InstanceType<typeof VvDropdown>>()
|
|
23
27
|
|
|
24
28
|
// data
|
|
25
29
|
const {
|
|
26
30
|
id,
|
|
27
31
|
icon,
|
|
28
32
|
iconPosition,
|
|
33
|
+
iconRemoveSuggestion,
|
|
34
|
+
labelRemoveSuggestion,
|
|
29
35
|
label,
|
|
30
36
|
modelValue,
|
|
31
37
|
count,
|
|
@@ -36,6 +42,8 @@ const {
|
|
|
36
42
|
debounce,
|
|
37
43
|
minlength,
|
|
38
44
|
maxlength,
|
|
45
|
+
storageKey,
|
|
46
|
+
storageType,
|
|
39
47
|
} = toRefs(props)
|
|
40
48
|
const hasId = useUniqueId(id)
|
|
41
49
|
const hasHintId = computed(() => `${hasId.value}-hint`)
|
|
@@ -49,12 +57,41 @@ const localModelValue = useDebouncedInput(modelValue, emit, debounce?.value)
|
|
|
49
57
|
|
|
50
58
|
// icons
|
|
51
59
|
const { hasIconBefore, hasIconAfter } = useComponentIcon(icon, iconPosition)
|
|
60
|
+
const { hasIcon: hasIconRemoveSuggestion }
|
|
61
|
+
= useComponentIcon(iconRemoveSuggestion)
|
|
52
62
|
|
|
53
63
|
// focus
|
|
54
|
-
const { focused } = useComponentFocus(
|
|
64
|
+
const { focused } = useComponentFocus(textareaEl, emit)
|
|
65
|
+
const isFocused = computed(
|
|
66
|
+
() => focused.value && !props.disabled && !props.readonly,
|
|
67
|
+
)
|
|
68
|
+
watch(isFocused, (newValue) => {
|
|
69
|
+
if (newValue && propsDefaults.value.selectOnFocus && textareaEl.value) {
|
|
70
|
+
textareaEl.value.select()
|
|
71
|
+
}
|
|
72
|
+
if (newValue && suggestions.value?.size) {
|
|
73
|
+
suggestionsDropdownEl.value?.show()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (isDirty.value && suggestions.value) {
|
|
77
|
+
const suggestionsLimit = props.maxSuggestions
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
suggestions.value.size >= suggestionsLimit
|
|
81
|
+
&& !suggestions.value.has(localModelValue.value)
|
|
82
|
+
) {
|
|
83
|
+
suggestions.value = new Set(
|
|
84
|
+
[...suggestions.value].slice(
|
|
85
|
+
suggestions.value.size - suggestionsLimit + 1,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
suggestions.value.add(localModelValue.value)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
55
92
|
|
|
56
93
|
// visibility
|
|
57
|
-
const isVisible = useElementVisibility(
|
|
94
|
+
const isVisible = useElementVisibility(textareaEl)
|
|
58
95
|
watch(isVisible, (newValue) => {
|
|
59
96
|
if (newValue && props.autofocus) {
|
|
60
97
|
focused.value = true
|
|
@@ -88,6 +125,41 @@ const isInvalid = computed(() => {
|
|
|
88
125
|
return undefined
|
|
89
126
|
})
|
|
90
127
|
|
|
128
|
+
// suggestions
|
|
129
|
+
const suggestions = usePersistence<Set<string>>(
|
|
130
|
+
storageKey,
|
|
131
|
+
storageType,
|
|
132
|
+
new Set(),
|
|
133
|
+
)
|
|
134
|
+
const filteredSuggestions = computed(() => {
|
|
135
|
+
if (!suggestions.value) {
|
|
136
|
+
return []
|
|
137
|
+
}
|
|
138
|
+
return [...suggestions.value]
|
|
139
|
+
.filter(
|
|
140
|
+
suggestion =>
|
|
141
|
+
isEmpty(localModelValue.value)
|
|
142
|
+
|| (`${suggestion}`
|
|
143
|
+
.toLowerCase()
|
|
144
|
+
.includes(`${localModelValue.value}`.toLowerCase())
|
|
145
|
+
&& suggestion !== localModelValue.value),
|
|
146
|
+
)
|
|
147
|
+
.reverse()
|
|
148
|
+
})
|
|
149
|
+
const hasSuggestions = computed(
|
|
150
|
+
() =>
|
|
151
|
+
storageKey?.value
|
|
152
|
+
&& suggestions.value
|
|
153
|
+
&& suggestions.value.size > 0,
|
|
154
|
+
)
|
|
155
|
+
function onSuggestionSelect(suggestion: string) {
|
|
156
|
+
localModelValue.value = suggestion
|
|
157
|
+
suggestionsDropdownEl.value?.hide()
|
|
158
|
+
}
|
|
159
|
+
function onSuggestionRemove(suggestion: string) {
|
|
160
|
+
suggestions.value?.delete(suggestion)
|
|
161
|
+
}
|
|
162
|
+
|
|
91
163
|
// hint
|
|
92
164
|
const {
|
|
93
165
|
HintSlot,
|
|
@@ -171,7 +243,7 @@ export default {
|
|
|
171
243
|
<label v-if="label" :for="hasId" class="vv-textarea__label">
|
|
172
244
|
{{ label }}
|
|
173
245
|
</label>
|
|
174
|
-
<div class="vv-textarea__wrapper">
|
|
246
|
+
<div ref="wrapperEl" class="vv-textarea__wrapper">
|
|
175
247
|
<!-- @slot Slot to replace icon before textarea -->
|
|
176
248
|
<div v-if="$slots.before" class="vv-textarea__input-before">
|
|
177
249
|
<!-- @slot Slot before input -->
|
|
@@ -185,7 +257,7 @@ export default {
|
|
|
185
257
|
/>
|
|
186
258
|
<textarea
|
|
187
259
|
:id="hasId"
|
|
188
|
-
ref="
|
|
260
|
+
ref="textareaEl"
|
|
189
261
|
v-model="localModelValue"
|
|
190
262
|
v-bind="hasAttrs"
|
|
191
263
|
@keyup="emit('keyup', $event)"
|
|
@@ -221,5 +293,36 @@ export default {
|
|
|
221
293
|
<slot name="invalid" v-bind="hintSlotScope" />
|
|
222
294
|
</template>
|
|
223
295
|
</HintSlot>
|
|
296
|
+
<VvDropdown
|
|
297
|
+
v-if="hasSuggestions"
|
|
298
|
+
ref="suggestionsDropdownEl"
|
|
299
|
+
:reference="wrapperEl"
|
|
300
|
+
:autofocus-first="false"
|
|
301
|
+
:trigger-width="true"
|
|
302
|
+
>
|
|
303
|
+
<template #items>
|
|
304
|
+
<VvDropdownOption
|
|
305
|
+
v-for="value in filteredSuggestions"
|
|
306
|
+
:key="value"
|
|
307
|
+
@click.stop="onSuggestionSelect(value)"
|
|
308
|
+
>
|
|
309
|
+
<div class="flex-1">
|
|
310
|
+
<slot name="suggestion" v-bind="{ value }">
|
|
311
|
+
{{ value }}
|
|
312
|
+
</slot>
|
|
313
|
+
</div>
|
|
314
|
+
<button
|
|
315
|
+
v-if="suggestions && hasIconRemoveSuggestion"
|
|
316
|
+
type="button"
|
|
317
|
+
tabindex="-1"
|
|
318
|
+
class="cursor-pointer"
|
|
319
|
+
:title="labelRemoveSuggestion"
|
|
320
|
+
@click.stop="onSuggestionRemove(value)"
|
|
321
|
+
>
|
|
322
|
+
<VvIcon v-bind="hasIconRemoveSuggestion" />
|
|
323
|
+
</button>
|
|
324
|
+
</VvDropdownOption>
|
|
325
|
+
</template>
|
|
326
|
+
</VvDropdown>
|
|
224
327
|
</div>
|
|
225
328
|
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtractPropTypes } from 'vue'
|
|
2
|
-
import { InputTextareaProps } from '../../props'
|
|
2
|
+
import { InputTextareaProps, StorageProps } from '../../props'
|
|
3
|
+
import { ACTION_ICONS, type VvIconProps } from '../VvIcon'
|
|
3
4
|
|
|
4
5
|
export const WRAP = {
|
|
5
6
|
hard: 'hard',
|
|
@@ -16,6 +17,7 @@ export const VvTextareaEvents = ['update:modelValue', 'focus', 'blur', 'keyup']
|
|
|
16
17
|
|
|
17
18
|
export const VvTextareaProps = {
|
|
18
19
|
...InputTextareaProps,
|
|
20
|
+
...StorageProps,
|
|
19
21
|
/**
|
|
20
22
|
* Textarea value
|
|
21
23
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#value
|
|
@@ -41,6 +43,35 @@ export const VvTextareaProps = {
|
|
|
41
43
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea#wrap
|
|
42
44
|
*/
|
|
43
45
|
spellcheck: { type: [Boolean, String], default: SPELLCHECK.default },
|
|
46
|
+
/**
|
|
47
|
+
* VvIcon name for remove suggestion button
|
|
48
|
+
* @see VVIcon
|
|
49
|
+
*/
|
|
50
|
+
iconRemoveSuggestion: {
|
|
51
|
+
type: [String, Object] as PropType<string | VvIconProps>,
|
|
52
|
+
default: ACTION_ICONS.remove,
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Label for remove suggestion button
|
|
56
|
+
*/
|
|
57
|
+
labelRemoveSuggestion: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: 'Remove suggestion',
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Maximum number of suggestions
|
|
63
|
+
*/
|
|
64
|
+
maxSuggestions: {
|
|
65
|
+
type: Number,
|
|
66
|
+
default: 5,
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* Select input text on focus
|
|
70
|
+
*/
|
|
71
|
+
selectOnFocus: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
default: false,
|
|
74
|
+
},
|
|
44
75
|
/**
|
|
45
76
|
* If true, the textarea will be resizable
|
|
46
77
|
*/
|
|
@@ -157,3 +157,28 @@ export async function defaultTest({ canvasElement, args }: PlayAttributes) {
|
|
|
157
157
|
// check accessibility
|
|
158
158
|
await expect(element).toHaveNoViolations()
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
export async function isoTest({ canvasElement, args }: PlayAttributes) {
|
|
162
|
+
const element = await within(canvasElement).findByTestId('element')
|
|
163
|
+
const input = element.getElementsByTagName('input')[0] as HTMLInputElement
|
|
164
|
+
const value = await within(canvasElement).findByTestId('value')
|
|
165
|
+
|
|
166
|
+
const inputDate = getDateFromInputValue(input.value, args.type)
|
|
167
|
+
if (!inputDate) {
|
|
168
|
+
await expect(value.innerHTML).toEqual('null')
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
const valueDate = new Date(value.innerHTML)
|
|
172
|
+
|
|
173
|
+
if (args.type === INPUT_TYPES.TIME || args.type === INPUT_TYPES.DATETIME_LOCAL) {
|
|
174
|
+
expect(inputDate.getHours()).toEqual(valueDate.getHours())
|
|
175
|
+
expect(inputDate.getMinutes()).toEqual(valueDate.getMinutes())
|
|
176
|
+
}
|
|
177
|
+
if (args.type === INPUT_TYPES.MONTH || args.type === INPUT_TYPES.DATE || args.type === INPUT_TYPES.DATETIME_LOCAL) {
|
|
178
|
+
expect(inputDate.getMonth()).toEqual(valueDate.getMonth())
|
|
179
|
+
expect(inputDate.getFullYear()).toEqual(valueDate.getFullYear())
|
|
180
|
+
}
|
|
181
|
+
if (args.type === INPUT_TYPES.DATE || args.type === INPUT_TYPES.DATETIME_LOCAL) {
|
|
182
|
+
expect(inputDate.getDate()).toEqual(valueDate.getDate())
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import VvInputText from '@/components/VvInputText/VvInputText.vue'
|
|
3
|
+
import { argTypes, defaultArgs } from './InputText.settings'
|
|
4
|
+
import { isoTest } from './InputText.test'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof VvInputText> = {
|
|
7
|
+
title: 'Components/InputText/Iso',
|
|
8
|
+
component: VvInputText,
|
|
9
|
+
args: defaultArgs,
|
|
10
|
+
argTypes,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default meta
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof VvInputText>
|
|
16
|
+
|
|
17
|
+
export const TypeDate: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
...defaultArgs,
|
|
20
|
+
type: 'date',
|
|
21
|
+
},
|
|
22
|
+
render: args => ({
|
|
23
|
+
components: { VvInputText },
|
|
24
|
+
setup() {
|
|
25
|
+
return { args }
|
|
26
|
+
},
|
|
27
|
+
data: () => ({
|
|
28
|
+
inputValue: `2024-12-31T23:00:00.000Z`,
|
|
29
|
+
}),
|
|
30
|
+
template: /* html */ `
|
|
31
|
+
<vv-input-text v-bind="args" v-model="inputValue" :data-testData="inputValue" data-testId="element" />
|
|
32
|
+
<div>Value: <span data-testId="value">{{ inputValue }}</span></div>
|
|
33
|
+
`,
|
|
34
|
+
}),
|
|
35
|
+
play: isoTest,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const TypeTime: Story = {
|
|
39
|
+
...TypeDate,
|
|
40
|
+
args: {
|
|
41
|
+
...defaultArgs,
|
|
42
|
+
type: 'time',
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const TypeTimeMinute: Story = {
|
|
47
|
+
...TypeDate,
|
|
48
|
+
args: {
|
|
49
|
+
...defaultArgs,
|
|
50
|
+
type: 'time',
|
|
51
|
+
step: 60,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const TypeMonth: Story = {
|
|
56
|
+
...TypeDate,
|
|
57
|
+
args: {
|
|
58
|
+
...defaultArgs,
|
|
59
|
+
type: 'month',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const TypeDateTime: Story = {
|
|
64
|
+
...TypeDate,
|
|
65
|
+
args: {
|
|
66
|
+
...defaultArgs,
|
|
67
|
+
type: 'datetime-local',
|
|
68
|
+
},
|
|
69
|
+
}
|