@volverjs/ui-vue 0.0.9-beta.16 → 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,35 +54,103 @@
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)
83
146
  const isFocused = computed(
84
147
  () => focused.value && !props.disabled && !props.readonly,
85
148
  )
149
+ watch(isFocused, (newValue) => {
150
+ if (newValue && propsDefaults.value.selectOnFocus && inputEl.value) {
151
+ inputEl.value.select()
152
+ }
153
+ })
86
154
 
87
155
  // visibility
88
156
  const isVisible = useElementVisibility(inputEl)
@@ -113,12 +181,21 @@
113
181
  const isNumber = computed(() => props.type === INPUT_TYPES.NUMBER)
114
182
  const onStepUp = () => {
115
183
  if (isClickable.value) {
184
+ if (iMask?.value) {
185
+ typed.value = typed.value + Number(step?.value ?? 1)
186
+ return
187
+ }
116
188
  inputEl.value.stepUp()
117
189
  localModelValue.value = unref(inputEl).value
118
190
  }
119
191
  }
120
192
  const onStepDown = () => {
121
193
  if (isClickable.value) {
194
+ if (iMask?.value) {
195
+ typed.value = typed.value - Number(step?.value ?? 1)
196
+
197
+ return
198
+ }
122
199
  inputEl.value.stepDown()
123
200
  localModelValue.value = unref(inputEl).value
124
201
  }
@@ -127,7 +204,7 @@
127
204
  // search
128
205
  const isSearch = computed(() => props.type === INPUT_TYPES.SEARCH)
129
206
  const onClear = () => {
130
- localModelValue.value = undefined
207
+ localModelValue.value = ''
131
208
  }
132
209
 
133
210
  // icons
@@ -153,9 +230,9 @@
153
230
 
154
231
  // count
155
232
  const { formatted: countFormatted } = useTextCount(localModelValue, {
156
- mode: props.count,
157
- upperLimit: Number(props.maxlength),
158
- lowerLimit: Number(props.minlength),
233
+ mode: count.value,
234
+ upperLimit: Number(maxlength?.value),
235
+ lowerLimit: Number(minlength?.value),
159
236
  })
160
237
 
161
238
  // tabindex
@@ -207,6 +284,9 @@
207
284
  if (isDateTime.value && !isDirty.value && !focused.value) {
208
285
  return INPUT_TYPES.TEXT
209
286
  }
287
+ if (iMask?.value) {
288
+ return INPUT_TYPES.TEXT
289
+ }
210
290
  return props.type
211
291
  })()
212
292
  const toReturn: InputHTMLAttributes = {
@@ -299,33 +379,6 @@
299
379
  props,
300
380
  )
301
381
 
302
- // mask
303
- const mask = ref()
304
- watch(
305
- [
306
- () => props.mask,
307
- () => props.type,
308
- () => props.maskEager,
309
- () => props.maskReversed,
310
- () => props.maskTokens,
311
- () => props.maskTokensReplace,
312
- ],
313
- ([newMask, newType, eager, reversed, tokens, tokensReplace]) => {
314
- if (newMask && newType === INPUT_TYPES.TEXT) {
315
- mask.value = new Mask({
316
- mask: newMask,
317
- eager,
318
- reversed,
319
- tokens,
320
- tokensReplace,
321
- })
322
- return
323
- }
324
- mask.value = undefined
325
- },
326
- { immediate: true },
327
- )
328
-
329
382
  // auto-width
330
383
  const onClickInner = () => {
331
384
  if (isClickable.value) {
@@ -343,6 +396,26 @@
343
396
  : undefined,
344
397
  }
345
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
+ }
346
419
  </script>
347
420
 
348
421
  <template>
@@ -368,10 +441,11 @@
368
441
  <input
369
442
  :id="hasId"
370
443
  ref="inputEl"
371
- v-model="localModelValue"
372
444
  v-bind="hasAttrs"
373
445
  :style="hasStyle"
374
446
  @keyup="emit('keyup', $event)"
447
+ @keydown="onKeyDown"
448
+ @keypress="emit('keypress', $event)"
375
449
  />
376
450
  <div
377
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
150
- */
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
164
+ * Masked value
166
165
  */
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
  */
@@ -196,6 +187,13 @@ export const VvInputTextProps = {
196
187
  unit: {
197
188
  type: String,
198
189
  },
190
+ /**
191
+ * Select input text on focus
192
+ */
193
+ selectOnFocus: {
194
+ type: Boolean,
195
+ default: false,
196
+ },
199
197
  }
200
198
 
201
199
  export type VvInputTextPropsTypes = ExtractPropTypes<typeof VvInputTextProps>
@@ -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,43 +174,64 @@ export const argTypes = {
174
174
  },
175
175
  },
176
176
  },
177
- mask: {
178
- description: 'Input mask, only for text type',
179
-
177
+ iMask: {
178
+ description: '[iMask](https://imask.js.org/guide.html) options',
179
+ control: {
180
+ type: 'object',
181
+ },
182
+ },
183
+ masked: {
184
+ description: 'Masked input',
180
185
  control: {
181
186
  type: 'text',
182
187
  },
183
188
  },
184
- maskEager: {
185
- description: 'Show mask before typing',
189
+ autoWidth: {
190
+ description: 'Adjust input width to content',
191
+ control: {
192
+ type: 'boolean',
193
+ },
186
194
  table: {
187
195
  defaultValue: {
188
196
  summary: false,
189
197
  },
198
+ type: {
199
+ summary: 'boolean',
200
+ },
190
201
  },
191
202
  },
192
- maskReversed: {
193
- description: 'Write typing reverse (ex. for numbers)',
203
+ hideActions: {
204
+ description: 'Hide type number, password and search actions',
205
+ control: {
206
+ type: 'boolean',
207
+ },
194
208
  table: {
195
209
  defaultValue: {
196
210
  summary: false,
197
211
  },
212
+ type: {
213
+ summary: 'boolean',
214
+ },
198
215
  },
199
216
  },
200
- maskTokens: {
201
- description: 'Add mask custom tokens',
202
- table: {
203
- defaultValue: {
204
- summary: 'undefined',
205
- },
217
+ unit: {
218
+ description: 'Add unit label to input',
219
+ control: {
220
+ type: 'text',
206
221
  },
207
222
  },
208
- maskTokensReplace: {
209
- description: 'Replace default tokens',
223
+ selectOnFocus: {
224
+ description: 'Select input text on focus',
225
+ control: {
226
+ type: 'boolean',
227
+ },
210
228
  table: {
211
229
  defaultValue: {
212
230
  summary: false,
213
231
  },
232
+ type: {
233
+ summary: 'boolean',
234
+ },
214
235
  },
215
236
  },
216
237
  before: {
@@ -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
  }