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

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,106 @@
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
+ if (
83
+ el.value instanceof HTMLInputElement &&
84
+ el.value.type === 'date'
85
+ ) {
86
+ localModelValue.value = el.value.value
87
+ return
88
+ }
89
+ let date = typed.value
90
+ if (!(date instanceof Date)) {
91
+ date = new Date(date)
92
+ }
93
+ localModelValue.value = `${date.getFullYear()}-${(
94
+ '0' +
95
+ (date.getMonth() + 1)
96
+ ).slice(-2)}-${('0' + date.getDate()).slice(-2)}`
97
+ return
72
98
  }
73
- if (props.type === INPUT_TYPES.NUMBER) {
74
- return Number(value)
99
+ if (type.value === INPUT_TYPES.DATETIME_LOCAL) {
100
+ if (
101
+ el.value instanceof HTMLInputElement &&
102
+ el.value.type === 'datetime-local'
103
+ ) {
104
+ localModelValue.value = el.value.value
105
+ return
106
+ }
107
+ let date = typed.value
108
+ if (!(typed.value instanceof Date)) {
109
+ date = new Date(date)
110
+ }
111
+ localModelValue.value = `${date.getFullYear()}-${(
112
+ '0' +
113
+ (date.getMonth() + 1)
114
+ ).slice(-2)}-${('0' + date.getDate()).slice(-2)}T${(
115
+ '0' + date.getHours()
116
+ ).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}`
117
+ return
75
118
  }
76
- return value
119
+ localModelValue.value = unmasked.value
77
120
  },
78
121
  },
79
122
  )
123
+ onMounted(() => {
124
+ if (mask.value) {
125
+ maskReady.value = true
126
+ typed.value = localModelValue.value ?? ''
127
+ }
128
+ })
129
+ watch(
130
+ () => props.modelValue,
131
+ (newValue) => {
132
+ if (mask.value) {
133
+ typed.value =
134
+ newValue && iMask?.value?.mask === Date
135
+ ? new Date(newValue)
136
+ : newValue ?? null
137
+ }
138
+ },
139
+ )
140
+ watch(
141
+ () => props.masked,
142
+ (newValue) => {
143
+ masked.value = newValue ?? ''
144
+ },
145
+ )
146
+ const inputEl = el as Ref<HTMLInputElement>
147
+ const innerEl = ref()
148
+
149
+ defineExpose({ $inner: innerEl })
150
+
151
+ // debounce
152
+ const localModelValue = useDebouncedInput(
153
+ modelValue,
154
+ emit,
155
+ debounce?.value ?? 0,
156
+ )
80
157
 
81
158
  // focus
82
159
  const { focused } = useComponentFocus(inputEl, emit)
@@ -118,12 +195,21 @@
118
195
  const isNumber = computed(() => props.type === INPUT_TYPES.NUMBER)
119
196
  const onStepUp = () => {
120
197
  if (isClickable.value) {
198
+ if (iMask?.value) {
199
+ typed.value = typed.value + Number(step?.value ?? 1)
200
+ return
201
+ }
121
202
  inputEl.value.stepUp()
122
203
  localModelValue.value = unref(inputEl).value
123
204
  }
124
205
  }
125
206
  const onStepDown = () => {
126
207
  if (isClickable.value) {
208
+ if (iMask?.value) {
209
+ typed.value = typed.value - Number(step?.value ?? 1)
210
+
211
+ return
212
+ }
127
213
  inputEl.value.stepDown()
128
214
  localModelValue.value = unref(inputEl).value
129
215
  }
@@ -132,7 +218,7 @@
132
218
  // search
133
219
  const isSearch = computed(() => props.type === INPUT_TYPES.SEARCH)
134
220
  const onClear = () => {
135
- localModelValue.value = undefined
221
+ localModelValue.value = ''
136
222
  }
137
223
 
138
224
  // icons
@@ -158,9 +244,9 @@
158
244
 
159
245
  // count
160
246
  const { formatted: countFormatted } = useTextCount(localModelValue, {
161
- mode: props.count,
162
- upperLimit: Number(props.maxlength),
163
- lowerLimit: Number(props.minlength),
247
+ mode: count.value,
248
+ upperLimit: Number(maxlength?.value),
249
+ lowerLimit: Number(minlength?.value),
164
250
  })
165
251
 
166
252
  // tabindex
@@ -212,6 +298,9 @@
212
298
  if (isDateTime.value && !isDirty.value && !focused.value) {
213
299
  return INPUT_TYPES.TEXT
214
300
  }
301
+ if (iMask?.value) {
302
+ return INPUT_TYPES.TEXT
303
+ }
215
304
  return props.type
216
305
  })()
217
306
  const toReturn: InputHTMLAttributes = {
@@ -304,33 +393,6 @@
304
393
  props,
305
394
  )
306
395
 
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
396
  // auto-width
335
397
  const onClickInner = () => {
336
398
  if (isClickable.value) {
@@ -348,6 +410,26 @@
348
410
  : undefined,
349
411
  }
350
412
  })
413
+
414
+ // keydown
415
+ const onKeyDown = (event: KeyboardEvent) => {
416
+ switch (event.code) {
417
+ case 'ArrowUp':
418
+ if (isNumber.value) {
419
+ onStepUp()
420
+ event.preventDefault()
421
+ }
422
+ break
423
+
424
+ case 'ArrowDown':
425
+ if (isNumber.value) {
426
+ onStepDown()
427
+ event.preventDefault()
428
+ }
429
+ break
430
+ }
431
+ emit('keydown', event)
432
+ }
351
433
  </script>
352
434
 
353
435
  <template>
@@ -373,10 +455,11 @@
373
455
  <input
374
456
  :id="hasId"
375
457
  ref="inputEl"
376
- v-model="localModelValue"
377
458
  v-bind="hasAttrs"
378
459
  :style="hasStyle"
379
460
  @keyup="emit('keyup', $event)"
461
+ @keydown="onKeyDown"
462
+ @keypress="emit('keypress', $event)"
380
463
  />
381
464
  <div
382
465
  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
  }