@volverjs/ui-vue 0.0.9-beta.17 → 0.0.9-beta.18

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.
@@ -12,6 +12,7 @@
12
12
 
13
13
  <script setup lang="ts">
14
14
  import type { Ref } from 'vue'
15
+ import { toRefs } from 'vue'
15
16
  import { VvComboboxProps, VvComboboxEvents } from '.'
16
17
  import VvIcon from '../VvIcon/VvIcon.vue'
17
18
  import VvDropdown from '../VvDropdown/VvDropdown.vue'
@@ -84,7 +85,7 @@
84
85
  const searchText = ref('')
85
86
  const debouncedSearchText = refDebounced(
86
87
  searchText,
87
- Number(props.debounceSearch),
88
+ computed(() => Number(props.debounceSearch)),
88
89
  )
89
90
  watch(debouncedSearchText, () =>
90
91
  emit('change:search', debouncedSearchText.value),
@@ -235,12 +236,15 @@
235
236
  } else if (props.modelValue) {
236
237
  selectedValues = [props.modelValue]
237
238
  }
238
- const options = props.options.reduce((acc, value) => {
239
- if (isGroup(value)) {
240
- return [...acc, ...getOptionGrouped(value)]
241
- }
242
- return [...acc, value]
243
- }, [] as Array<Option | string>)
239
+ const options = props.options.reduce(
240
+ (acc, value) => {
241
+ if (isGroup(value)) {
242
+ return [...acc, ...getOptionGrouped(value)]
243
+ }
244
+ return [...acc, value]
245
+ },
246
+ [] as Array<Option | string>,
247
+ )
244
248
 
245
249
  return options.filter((option) => {
246
250
  if (isGroup(option)) {
@@ -6,7 +6,7 @@
6
6
 
7
7
  <script setup lang="ts">
8
8
  import type { InputHTMLAttributes } from 'vue'
9
- import { Mask } from 'maska'
9
+ import { useIMask } from 'vue-imask'
10
10
  import HintSlotFactory from '../common/HintSlot'
11
11
  import VvIcon from '../VvIcon/VvIcon.vue'
12
12
  import VvInputTextActionsFactory from '../VvInputText/VvInputTextActions'
@@ -29,12 +29,6 @@
29
29
  props,
30
30
  )
31
31
 
32
- // template refs
33
- const inputEl = ref()
34
- const innerEl = ref()
35
-
36
- defineExpose({ $inner: innerEl })
37
-
38
32
  // data
39
33
  const {
40
34
  id,
@@ -46,6 +40,12 @@
46
40
  valid,
47
41
  invalid,
48
42
  loading,
43
+ debounce,
44
+ maxlength,
45
+ minlength,
46
+ type,
47
+ iMask,
48
+ step,
49
49
  } = toRefs(props)
50
50
  const hasId = useUniqueId(id)
51
51
  const hasHintId = computed(() => `${hasId.value}-hint`)
@@ -54,29 +54,92 @@
54
54
  props.floating && isEmpty(props.placeholder) ? ' ' : props.placeholder,
55
55
  )
56
56
 
57
- // debounce
58
- const localModelValue = useDebouncedInput(
59
- modelValue,
60
- emit,
61
- props.debounce,
57
+ // template refs
58
+ const maskReady = ref(false)
59
+ const { el, mask, typed, masked, unmasked } = useIMask(
60
+ computed(
61
+ () =>
62
+ iMask?.value ?? {
63
+ mask: /./,
64
+ },
65
+ ),
62
66
  {
63
- getter: (value) => {
64
- if (mask.value) {
65
- return mask.value.masked(value ?? '')
67
+ emit,
68
+ onAccept: () => {
69
+ if (!maskReady.value) {
70
+ return
66
71
  }
67
- return value
68
- },
69
- setter: (value) => {
70
- if (mask.value) {
71
- value = mask.value.unmasked(value)
72
+ emit('update:masked', masked.value)
73
+ if (type.value === INPUT_TYPES.NUMBER) {
74
+ if (typeof typed.value !== 'number') {
75
+ localModelValue.value = Number(typed.value)
76
+ return
77
+ }
78
+ localModelValue.value = typed.value
79
+ return
80
+ }
81
+ if (type.value === INPUT_TYPES.DATE) {
82
+ let date = typed.value
83
+ if (!(date instanceof Date)) {
84
+ date = new Date(date)
85
+ }
86
+ localModelValue.value = `${date.getFullYear()}-${(
87
+ '0' +
88
+ (date.getMonth() + 1)
89
+ ).slice(-2)}-${('0' + date.getDate()).slice(-2)}`
90
+ return
72
91
  }
73
- if (props.type === INPUT_TYPES.NUMBER) {
74
- return Number(value)
92
+ if (type.value === INPUT_TYPES.DATETIME_LOCAL) {
93
+ let date = typed.value
94
+ if (!(typed.value instanceof Date)) {
95
+ date = new Date(date)
96
+ }
97
+ localModelValue.value = `${date.getFullYear()}-${(
98
+ '0' +
99
+ (date.getMonth() + 1)
100
+ ).slice(-2)}-${('0' + date.getDate()).slice(-2)}T${(
101
+ '0' + date.getHours()
102
+ ).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}`
103
+ return
75
104
  }
76
- return value
105
+ localModelValue.value = unmasked.value
77
106
  },
78
107
  },
79
108
  )
109
+ onMounted(() => {
110
+ if (mask.value) {
111
+ maskReady.value = true
112
+ typed.value = localModelValue.value ?? ''
113
+ }
114
+ })
115
+ watch(
116
+ () => props.modelValue,
117
+ (newValue) => {
118
+ if (mask.value) {
119
+ typed.value =
120
+ newValue && iMask?.value?.mask === Date
121
+ ? new Date(newValue)
122
+ : newValue ?? null
123
+ }
124
+ },
125
+ )
126
+ watch(
127
+ () => props.masked,
128
+ (newValue) => {
129
+ masked.value = newValue ?? ''
130
+ },
131
+ )
132
+ const inputEl = el as Ref<HTMLInputElement>
133
+ const innerEl = ref()
134
+
135
+ defineExpose({ $inner: innerEl })
136
+
137
+ // debounce
138
+ const localModelValue = useDebouncedInput(
139
+ modelValue,
140
+ emit,
141
+ debounce?.value ?? 0,
142
+ )
80
143
 
81
144
  // focus
82
145
  const { focused } = useComponentFocus(inputEl, emit)
@@ -118,12 +181,21 @@
118
181
  const isNumber = computed(() => props.type === INPUT_TYPES.NUMBER)
119
182
  const onStepUp = () => {
120
183
  if (isClickable.value) {
184
+ if (iMask?.value) {
185
+ typed.value = typed.value + Number(step?.value ?? 1)
186
+ return
187
+ }
121
188
  inputEl.value.stepUp()
122
189
  localModelValue.value = unref(inputEl).value
123
190
  }
124
191
  }
125
192
  const onStepDown = () => {
126
193
  if (isClickable.value) {
194
+ if (iMask?.value) {
195
+ typed.value = typed.value - Number(step?.value ?? 1)
196
+
197
+ return
198
+ }
127
199
  inputEl.value.stepDown()
128
200
  localModelValue.value = unref(inputEl).value
129
201
  }
@@ -132,7 +204,7 @@
132
204
  // search
133
205
  const isSearch = computed(() => props.type === INPUT_TYPES.SEARCH)
134
206
  const onClear = () => {
135
- localModelValue.value = undefined
207
+ localModelValue.value = ''
136
208
  }
137
209
 
138
210
  // icons
@@ -158,9 +230,9 @@
158
230
 
159
231
  // count
160
232
  const { formatted: countFormatted } = useTextCount(localModelValue, {
161
- mode: props.count,
162
- upperLimit: Number(props.maxlength),
163
- lowerLimit: Number(props.minlength),
233
+ mode: count.value,
234
+ upperLimit: Number(maxlength?.value),
235
+ lowerLimit: Number(minlength?.value),
164
236
  })
165
237
 
166
238
  // tabindex
@@ -212,6 +284,9 @@
212
284
  if (isDateTime.value && !isDirty.value && !focused.value) {
213
285
  return INPUT_TYPES.TEXT
214
286
  }
287
+ if (iMask?.value) {
288
+ return INPUT_TYPES.TEXT
289
+ }
215
290
  return props.type
216
291
  })()
217
292
  const toReturn: InputHTMLAttributes = {
@@ -304,33 +379,6 @@
304
379
  props,
305
380
  )
306
381
 
307
- // mask
308
- const mask = ref()
309
- watch(
310
- [
311
- () => props.mask,
312
- () => props.type,
313
- () => props.maskEager,
314
- () => props.maskReversed,
315
- () => props.maskTokens,
316
- () => props.maskTokensReplace,
317
- ],
318
- ([newMask, newType, eager, reversed, tokens, tokensReplace]) => {
319
- if (newMask && newType === INPUT_TYPES.TEXT) {
320
- mask.value = new Mask({
321
- mask: newMask,
322
- eager,
323
- reversed,
324
- tokens,
325
- tokensReplace,
326
- })
327
- return
328
- }
329
- mask.value = undefined
330
- },
331
- { immediate: true },
332
- )
333
-
334
382
  // auto-width
335
383
  const onClickInner = () => {
336
384
  if (isClickable.value) {
@@ -348,6 +396,26 @@
348
396
  : undefined,
349
397
  }
350
398
  })
399
+
400
+ // keydown
401
+ const onKeyDown = (event: KeyboardEvent) => {
402
+ switch (event.code) {
403
+ case 'ArrowUp':
404
+ if (isNumber.value) {
405
+ onStepUp()
406
+ event.preventDefault()
407
+ }
408
+ break
409
+
410
+ case 'ArrowDown':
411
+ if (isNumber.value) {
412
+ onStepDown()
413
+ event.preventDefault()
414
+ }
415
+ break
416
+ }
417
+ emit('keydown', event)
418
+ }
351
419
  </script>
352
420
 
353
421
  <template>
@@ -373,10 +441,11 @@
373
441
  <input
374
442
  :id="hasId"
375
443
  ref="inputEl"
376
- v-model="localModelValue"
377
444
  v-bind="hasAttrs"
378
445
  :style="hasStyle"
379
446
  @keyup="emit('keyup', $event)"
447
+ @keydown="onKeyDown"
448
+ @keypress="emit('keypress', $event)"
380
449
  />
381
450
  <div
382
451
  v-if="(unit || $slots.unit) && isDirty"
@@ -1,5 +1,5 @@
1
1
  import type { ExtractPropTypes, PropType } from 'vue'
2
- import type { MaskTokens } from 'maska'
2
+ import type { FactoryOpts } from 'imask'
3
3
  import { InputTextareaProps } from '../../props'
4
4
 
5
5
  export const INPUT_TYPES = {
@@ -28,7 +28,23 @@ export const TYPES_ICON = {
28
28
  SEARCH: 'close',
29
29
  } as const
30
30
 
31
- export const VvInputTextEvents = ['update:modelValue', 'focus', 'blur', 'keyup']
31
+ export const VvInputTextEvents = [
32
+ 'update:modelValue',
33
+ 'update:masked',
34
+ 'accept',
35
+ 'accept:typed',
36
+ 'accept:masked',
37
+ 'accept:unmasked',
38
+ 'complete',
39
+ 'complete:typed',
40
+ 'complete:masked',
41
+ 'complete:unmasked',
42
+ 'focus',
43
+ 'blur',
44
+ 'keyup',
45
+ 'keydown',
46
+ 'keypress',
47
+ ]
32
48
 
33
49
  export const VvInputTextProps = {
34
50
  ...InputTextareaProps,
@@ -137,45 +153,20 @@ export const VvInputTextProps = {
137
153
  default: 'Clear',
138
154
  },
139
155
  /**
140
- * Input mask, only for text type
141
- * @see https://beholdr.github.io/maska/
156
+ * iMask options
157
+ * @see https://imask.js.org/guide.html
142
158
  */
143
- mask: {
144
- type: String,
159
+ iMask: {
160
+ type: Object as PropType<FactoryOpts>,
145
161
  default: undefined,
146
162
  },
147
163
  /**
148
- * Show mask before typing
149
- * @see https://beholdr.github.io/maska/#/?id=maskinput-options
164
+ * Masked value
150
165
  */
151
- maskEager: {
152
- type: Boolean,
153
- default: false,
154
- },
155
- /**
156
- * Write values reverse (ex. for numbers)
157
- * @see https://beholdr.github.io/maska/#/?id=maskinput-options
158
- */
159
- maskReversed: {
160
- type: Boolean,
161
- default: false,
162
- },
163
- /**
164
- * Add mask custom tokens
165
- * @see https://beholdr.github.io/maska/#/?id=custom-tokens
166
- */
167
- maskTokens: {
168
- type: Object as PropType<MaskTokens>,
166
+ masked: {
167
+ type: String,
169
168
  default: undefined,
170
169
  },
171
- /**
172
- * Replace default tokens
173
- * @see https://beholdr.github.io/maska/#/?id=custom-tokens
174
- */
175
- maskTokensReplace: {
176
- type: Boolean,
177
- default: false,
178
- },
179
170
  /**
180
171
  * Adjust input width to content
181
172
  */
@@ -37,6 +37,9 @@
37
37
  invalid,
38
38
  loading,
39
39
  modifiers,
40
+ debounce,
41
+ minlength,
42
+ maxlength,
40
43
  } = toRefs(props)
41
44
  const hasId = useUniqueId(id)
42
45
  const hasHintId = computed(() => `${hasId.value}-hint`)
@@ -46,7 +49,7 @@
46
49
  )
47
50
 
48
51
  // debounce
49
- const localModelValue = useDebouncedInput(modelValue, emit, props.debounce)
52
+ const localModelValue = useDebouncedInput(modelValue, emit, debounce?.value)
50
53
 
51
54
  // icons
52
55
  const { hasIcon, hasIconBefore, hasIconAfter } = useComponentIcon(
@@ -67,9 +70,9 @@
67
70
 
68
71
  // count
69
72
  const { formatted: countFormatted } = useTextCount(localModelValue, {
70
- mode: props.count,
71
- upperLimit: Number(props.maxlength),
72
- lowerLimit: Number(props.minlength),
73
+ mode: count?.value,
74
+ upperLimit: Number(maxlength?.value),
75
+ lowerLimit: Number(minlength?.value),
73
76
  })
74
77
 
75
78
  // tabindex
@@ -143,7 +146,7 @@
143
146
  'aria-errormessage': hasInvalidLabelOrSlot.value
144
147
  ? hasHintId.value
145
148
  : undefined,
146
- } as TextareaHTMLAttributes),
149
+ }) as TextareaHTMLAttributes,
147
150
  )
148
151
 
149
152
  // slots props
@@ -174,42 +174,16 @@ export const argTypes = {
174
174
  },
175
175
  },
176
176
  },
177
- mask: {
178
- description: 'Input mask, only for text type',
177
+ iMask: {
178
+ description: '[iMask](https://imask.js.org/guide.html) options',
179
179
  control: {
180
- type: 'text',
181
- },
182
- },
183
- maskEager: {
184
- description: 'Show mask before typing',
185
- table: {
186
- defaultValue: {
187
- summary: false,
188
- },
189
- },
190
- },
191
- maskReversed: {
192
- description: 'Write typing reverse (ex. for numbers)',
193
- table: {
194
- defaultValue: {
195
- summary: false,
196
- },
197
- },
198
- },
199
- maskTokens: {
200
- description: 'Add mask custom tokens',
201
- table: {
202
- defaultValue: {
203
- summary: 'undefined',
204
- },
180
+ type: 'object',
205
181
  },
206
182
  },
207
- maskTokensReplace: {
208
- description: 'Replace default tokens',
209
- table: {
210
- defaultValue: {
211
- summary: false,
212
- },
183
+ masked: {
184
+ description: 'Masked input',
185
+ control: {
186
+ type: 'text',
213
187
  },
214
188
  },
215
189
  autoWidth: {
@@ -25,14 +25,15 @@ export const Default: Story = {
25
25
  setup() {
26
26
  return { args }
27
27
  },
28
- data: () => ({ inputValue: undefined }),
28
+ data: () => ({ inputValue: undefined, maskedInputValue: undefined }),
29
29
  template: /* html */ `
30
- <vv-input-text v-bind="args" v-model="inputValue" :data-testData="inputValue" data-testId="element">
30
+ <vv-input-text v-bind="args" v-model="inputValue" v-model:masked="maskedInputValue" :data-testData="inputValue" data-testId="element">
31
31
  <template #before v-if="args.before"><div class="flex" v-html="args.before"></div></template>
32
32
  <template #after v-if="args.after"><div class="flex" v-html="args.after"></div></template>
33
33
  <template #hint v-if="args.hint"><span v-html="args.hint"></span></template>
34
34
  </vv-input-text>
35
- <div>Value: <span data-testId="value">{{inputValue}}</span></div>
35
+ <div>Value: <span data-testId="value">{{ inputValue }}</span></div>
36
+ <div v-if="args.iMask" class="mt-sm">Masked Value: <span data-testId="masked-value">{{ maskedInputValue }}</span></div>
36
37
  `,
37
38
  }),
38
39
  play: defaultTest,
@@ -82,7 +83,6 @@ export const Hint: Story = {
82
83
  ...defaultArgs,
83
84
  hintLabel: 'Please enter your name.',
84
85
  },
85
-
86
86
  }
87
87
 
88
88
  export const Loading: Story = {
@@ -102,14 +102,6 @@ export const Floating: Story = {
102
102
  },
103
103
  }
104
104
 
105
- export const Mask: Story = {
106
- ...Default,
107
- args: {
108
- ...defaultArgs,
109
- mask: '##-###-##',
110
- },
111
- }
112
-
113
105
  export const Unit: Story = {
114
106
  ...Default,
115
107
  args: {
@@ -4,32 +4,48 @@ import { sleep } from '@/test/sleep'
4
4
  import { within, userEvent } from '@storybook/testing-library'
5
5
  import { INPUT_TYPES, type InputType } from '@/components/VvInputText'
6
6
 
7
- const valueByType = (type: InputType, mask?: string) => {
7
+ const valueByType = (type: InputType, mask?: string, id?: string) => {
8
8
  if (mask) {
9
- return '1234567'
9
+ switch (id) {
10
+ case 'phone-number':
11
+ return { toType: '393923847556' }
12
+ case 'date-placeholder':
13
+ return { toType: '01011970', toCheck: '1970-01-01' }
14
+ case 'phone-or-email':
15
+ return {
16
+ toType:
17
+ Math.random() < 0.5 ? '393923847556' : 'test@test.com',
18
+ }
19
+ case 'intl-number':
20
+ return { toType: '1234567890.22' }
21
+ case 'currency':
22
+ return { toType: '1234567890.22' }
23
+ default:
24
+ return { toType: '1234567890' }
25
+ }
10
26
  }
11
27
  switch (type) {
12
28
  case INPUT_TYPES.TEXT:
13
29
  case INPUT_TYPES.PASSWORD:
14
30
  case INPUT_TYPES.SEARCH:
15
- return 'Lorem ipsum'
31
+ return { toType: 'Lorem ipsum' }
16
32
  case INPUT_TYPES.NUMBER:
17
- return '1'
33
+ return { toType: '1' }
18
34
  case INPUT_TYPES.EMAIL:
19
- return 'test@test.com'
35
+ return { toType: 'test@test.com' }
20
36
  case INPUT_TYPES.TEL:
21
- return '+1234567890'
37
+ return { toType: '+1234567890' }
22
38
  case INPUT_TYPES.URL:
23
- return 'https://www.test.com'
39
+ return { toType: 'https://www.test.com' }
24
40
  case INPUT_TYPES.DATE:
25
- return new Date().toISOString().split('T')[0]
41
+ return { toType: new Date().toISOString().split('T')[0] }
26
42
  case INPUT_TYPES.TIME:
27
- return '12:00'
43
+ return { toType: '12:00' }
28
44
  case INPUT_TYPES.COLOR:
29
45
  case INPUT_TYPES.DATETIME_LOCAL:
30
46
  case INPUT_TYPES.MONTH:
31
47
  case INPUT_TYPES.WEEK:
32
- return undefined
48
+ return { toType: undefined }
33
49
  }
34
50
  }
35
51
 
@@ -45,17 +61,17 @@ export async function defaultTest({ canvasElement, args }: PlayAttributes) {
45
61
 
46
62
  // value
47
63
  if (!args.invalid && !args.disabled && !args.readonly) {
48
- const inputValue = valueByType(args.type, args.mask)
49
- if (inputValue) {
64
+ const { toType, toCheck } = valueByType(args.type, args.iMask, args.id)
65
+ if (toType) {
50
66
  await expect(input).toBeClicked()
51
- await userEvent.keyboard(inputValue)
67
+ await userEvent.keyboard(toType)
52
68
  await sleep()
53
69
  if (args.maxlength) {
54
70
  await expect(value.innerHTML).toEqual(
55
- inputValue.slice(0, args.maxlength),
71
+ toType.slice(0, args.maxlength),
56
72
  )
57
73
  } else {
58
- await expect(value.innerHTML).toEqual(inputValue)
74
+ await expect(value.innerHTML).toEqual(toCheck ?? toType)
59
75
  }
60
76
  }
61
77
  }