@antify/ui 4.2.0 → 4.2.2

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.
@@ -65,6 +65,7 @@ const props =
65
65
  });
66
66
  const _modelValue = useVModel(props, 'modelValue', emit);
67
67
  const delayedValue = ref(_modelValue.value);
68
+ const inputRef = ref<HTMLInputElement | null>(null);
68
69
  const hasInputState = computed(() => props.skeleton || props.readonly || props.disabled);
69
70
  const inputClasses = computed(() => {
70
71
  const focusColorVariant: Record<InputState, string> = {
@@ -106,7 +107,7 @@ const inputClasses = computed(() => {
106
107
  };
107
108
  });
108
109
  const contentClasses = computed(() => ({
109
- 'text-for-white-bg-font': true,
110
+ 'text-for-white-bg-font w-fit': true,
110
111
  'cursor-pointer': !hasInputState.value,
111
112
  'cursor-not-allowed opacity-50': props.disabled,
112
113
  'text-sm': props.size === Size.lg || props.size === Size.md || props.size === Size.sm,
@@ -119,6 +120,20 @@ const itemSize = computed(() => {
119
120
  return IconSize.sm;
120
121
  }
121
122
  });
123
+ const gapSize = computed(() => {
124
+ switch (props.size) {
125
+ case Size.lg:
126
+ return 'gap-2.5';
127
+ case Size.md:
128
+ return 'gap-2';
129
+ case Size.sm:
130
+ return 'gap-1.5';
131
+ case Size.xs:
132
+ return 'gap-1.5';
133
+ default:
134
+ return 'gap-1';
135
+ }
136
+ });
122
137
  const iconColor = computed(() => {
123
138
  switch (props.state) {
124
139
  case InputState.base:
@@ -167,6 +182,12 @@ function onBlur(e: FocusEvent) {
167
182
  emit('blur', e);
168
183
  }
169
184
 
185
+ const onClickContent = () => {
186
+ if (inputRef.value && !props.disabled && !props.readonly) {
187
+ inputRef.value.focus();
188
+ }
189
+ };
190
+
170
191
  onMounted(() => {
171
192
  handleEnumValidation(props.size, Size, 'size');
172
193
  handleEnumValidation(props.state, InputState, 'state');
@@ -187,8 +208,12 @@ onMounted(() => {
187
208
  :size="size"
188
209
  :expanded="false"
189
210
  :messages="messages"
211
+ @mousedown.prevent="onClickContent"
190
212
  >
191
- <div class="flex gap-1.5">
213
+ <div
214
+ class="flex"
215
+ :class="gapSize"
216
+ >
192
217
  <div
193
218
  class="relative flex"
194
219
  :class="{
@@ -199,8 +224,10 @@ onMounted(() => {
199
224
  <AntSkeleton
200
225
  :visible="skeleton"
201
226
  rounded
227
+ @click.prevent
202
228
  >
203
229
  <input
230
+ ref="inputRef"
204
231
  v-model="_modelValue"
205
232
  :class="inputClasses"
206
233
  type="checkbox"
@@ -223,7 +250,10 @@ onMounted(() => {
223
250
  </AntSkeleton>
224
251
  </div>
225
252
 
226
- <span :class="contentClasses">
253
+ <span
254
+ :class="contentClasses"
255
+ @mousedown.prevent="onClickContent"
256
+ >
227
257
  <AntSkeleton
228
258
  :visible="skeleton"
229
259
  rounded
@@ -13,8 +13,9 @@ import {
13
13
  Direction,
14
14
  } from '../../enums/Direction.enum';
15
15
  import {
16
- InputState, Size,
16
+ InputState, LayoutVariant, Size,
17
17
  } from '../../enums';
18
+ import AntSkeleton from '../AntSkeleton.vue';
18
19
 
19
20
  const emit = defineEmits([
20
21
  'update:modelValue',
@@ -30,6 +31,7 @@ const props = withDefaults(
30
31
  direction?: Direction;
31
32
  state?: InputState;
32
33
  size?: Size;
34
+ layoutVariant?: LayoutVariant;
33
35
  skeleton?: boolean;
34
36
  readonly?: boolean;
35
37
  disabled?: boolean;
@@ -48,8 +50,10 @@ const props = withDefaults(
48
50
  activeColorClass?: string;
49
51
  }>(),
50
52
  {
53
+ checkboxes: () => [],
51
54
  direction: Direction.column,
52
55
  state: InputState.base,
56
+ layoutVariant: LayoutVariant.default,
53
57
  size: Size.md,
54
58
  skeleton: false,
55
59
  readonly: false,
@@ -60,12 +64,67 @@ const props = withDefaults(
60
64
  activeColorClass: 'text-primary-500',
61
65
  },
62
66
  );
67
+
63
68
  const containerClasses = computed(() => ({
64
69
  'flex gap-2.5': true,
65
70
  'flex-row': props.direction === Direction.row,
66
71
  'flex-col': props.direction === Direction.column,
67
72
  }));
73
+ const blockContainerClasses = computed(() => ({
74
+ 'flex flex-col gap-px rounded-md border overflow-hidden': true,
75
+ 'bg-base-300 border-base-300': props.state === InputState.base,
76
+ 'bg-info-500 border-info-500': props.state === InputState.info,
77
+ 'bg-success-500 border-success-500': props.state === InputState.success,
78
+ 'bg-warning-500 border-warning-500': props.state === InputState.warning,
79
+ 'bg-danger-500 border-danger-500': props.state === InputState.danger,
80
+ 'cursor-not-allowed': props.disabled,
81
+ }));
82
+ const blockItemClasses = computed(() => ({
83
+ 'p-2.5': props.size === Size.lg,
84
+ 'p-2': props.size === Size.md,
85
+ 'p-1.5': props.size === Size.sm || props.size === Size.xs,
86
+ 'p-1': props.size === Size.xs2,
87
+ }));
88
+ const tabContainerClasses = computed(() => ({
89
+ 'flex rounded-md overflow-hidden': true,
90
+ 'cursor-not-allowed': props.disabled,
91
+ }));
92
+ const tabItemClasses = computed(() => ({
93
+ 'flex justify-center grow transition-colors': true,
94
+ 'text-sm': props.size === Size.lg || props.size === Size.md || props.size === Size.sm,
95
+ 'text-xs': props.size === Size.xs || props.size === Size.xs2,
96
+ 'p-2.5': props.size === Size.lg,
97
+ 'p-2': props.size === Size.md,
98
+ 'p-1.5': props.size === Size.sm || props.size === Size.xs,
99
+ 'p-1': props.size === Size.xs2,
100
+ }));
101
+ const getTabItemColorClasses = (checkbox: AntCheckboxType, index: number) => {
102
+ const borderClasses = {
103
+ 'border-base-300': props.modelValue?.includes(checkbox.value) || (!props.modelValue?.includes(checkbox.value) && props.state === InputState.base),
104
+ 'border-info-500': !props.modelValue?.includes(checkbox.value) && props.state === InputState.info,
105
+ 'border-success-500': !props.modelValue?.includes(checkbox.value) && props.state === InputState.success,
106
+ 'border-warning-500': !props.modelValue?.includes(checkbox.value) && props.state === InputState.warning,
107
+ 'border-danger-500': !props.modelValue?.includes(checkbox.value) && props.state === InputState.danger,
108
+ };
109
+
110
+ return {
111
+ ...borderClasses,
112
+ 'border-y': true,
113
+ 'border-l': index !== props.checkboxes.length - 1,
114
+ 'border-x': index === props.checkboxes.length - 1,
115
+ 'rounded-l-md': index === 0,
116
+ 'rounded-r-md': index === props.checkboxes.length - 1,
117
+ 'opacity-50': props.disabled || checkbox.disabled,
118
+ 'bg-white text-for-white-bg-font': !props.modelValue?.includes(checkbox.value),
119
+ '!bg-primary-500 text-primary-500-font': !props.skeleton && props.modelValue?.includes(checkbox.value) && props.state === InputState.base,
120
+ '!bg-info-500 text-info-500-font': !props.skeleton && props.modelValue?.includes(checkbox.value) && props.state === InputState.info,
121
+ '!bg-success-500 text-success-500-font': !props.skeleton && props.modelValue?.includes(checkbox.value) && props.state === InputState.success,
122
+ '!bg-warning-500 text-warning-500-font': !props.skeleton && props.modelValue?.includes(checkbox.value) && props.state === InputState.warning,
123
+ '!bg-danger-500 text-danger-500-font': !props.skeleton && props.modelValue?.includes(checkbox.value) && props.state === InputState.danger,
124
+ };
125
+ };
68
126
  const containerRef = ref<null | HTMLElement>(null);
127
+ const checkboxRef = ref<Array<InstanceType<typeof AntCheckbox>>>([]);
69
128
 
70
129
  watch(() => props.modelValue, (val) => {
71
130
  if ([
@@ -115,6 +174,34 @@ function onBlurCheckbox() {
115
174
  }, 100);
116
175
  }
117
176
 
177
+ const onClickBlockItem = (checkbox: AntCheckboxType, index: number) => {
178
+ if (props.skeleton || props.disabled || props.readonly || checkbox.disabled || checkbox.readonly) {
179
+ return;
180
+ }
181
+
182
+ updateValue(checkbox.value);
183
+
184
+ setTimeout(() => {
185
+ const checkboxComponent = checkboxRef.value[index];
186
+
187
+ if (checkboxComponent && checkboxComponent.$el) {
188
+ const input = checkboxComponent.$el.querySelector('input[type="checkbox"]');
189
+
190
+ if (input) {
191
+ input.focus();
192
+ }
193
+ }
194
+ });
195
+ };
196
+
197
+ const onClickTabItem = (checkbox: AntCheckboxType) => {
198
+ if (props.skeleton || props.disabled || props.readonly || checkbox.disabled || checkbox.readonly) {
199
+ return;
200
+ }
201
+
202
+ updateValue(checkbox.value);
203
+ };
204
+
118
205
  onMounted(() => {
119
206
  if (!props.skeleton && props.modelValue !== null) {
120
207
  emit('validate', props.modelValue);
@@ -134,6 +221,7 @@ onMounted(() => {
134
221
  label-for="noop"
135
222
  >
136
223
  <div
224
+ v-if="layoutVariant === LayoutVariant.default"
137
225
  ref="containerRef"
138
226
  :class="containerClasses"
139
227
  >
@@ -155,5 +243,65 @@ onMounted(() => {
155
243
  {{ checkbox.label }}
156
244
  </AntCheckbox>
157
245
  </div>
246
+
247
+ <div
248
+ v-if="layoutVariant === LayoutVariant.block"
249
+ ref="containerRef"
250
+ :class="blockContainerClasses"
251
+ >
252
+ <div
253
+ v-for="(checkbox, index) in checkboxes"
254
+ :key="`checkbox-widget_${checkbox.value}-${index}`"
255
+ :class="{
256
+ ...blockItemClasses,
257
+ 'cursor-not-allowed': props.disabled || checkbox.disabled,
258
+ 'cursor-pointer hover:bg-base-50': !props.skeleton && !props.disabled && !props.readonly && !checkbox.disabled && !checkbox.readonly,
259
+ 'bg-base-100': !skeleton && modelValue?.includes(checkbox.value),
260
+ 'bg-white': skeleton || !modelValue?.includes(checkbox.value),
261
+ }"
262
+ @click="onClickBlockItem(checkbox, index)"
263
+ >
264
+ <AntCheckbox
265
+ :ref="el => checkboxRef[index] = el"
266
+ :model-value="modelValue !== null ? modelValue?.indexOf(checkbox.value) !== -1 : null"
267
+ :skeleton="skeleton"
268
+ :disabled="disabled || checkbox.disabled"
269
+ :readonly="readonly || checkbox.readonly"
270
+ :state="state"
271
+ :size="size"
272
+ @update:model-value="updateValue(checkbox.value)"
273
+ @blur="() => onBlurCheckbox()"
274
+ @click.stop
275
+ >
276
+ {{ checkbox.label }}
277
+ </AntCheckbox>
278
+ </div>
279
+ </div>
280
+
281
+ <div
282
+ v-if="layoutVariant === LayoutVariant.tab"
283
+ :class="tabContainerClasses"
284
+ >
285
+ <div
286
+ v-for="(checkbox, index) in checkboxes"
287
+ :key="`checkbox-widget_${checkbox.value}-${index}`"
288
+ :class="{
289
+ ...tabItemClasses,
290
+ ...getTabItemColorClasses(checkbox, index),
291
+ 'cursor-not-allowed': props.disabled || checkbox.disabled,
292
+ 'cursor-pointer hover:bg-base-50': !props.skeleton && !props.disabled && !props.readonly && !checkbox.disabled && !checkbox.readonly,
293
+ 'bg-base-100': !skeleton && modelValue?.includes(checkbox.value),
294
+ 'bg-white': skeleton || !modelValue?.includes(checkbox.value),
295
+ }"
296
+ @click="onClickTabItem(checkbox)"
297
+ >
298
+ <AntSkeleton
299
+ :visible="skeleton"
300
+ rounded
301
+ >
302
+ {{ checkbox.label }}
303
+ </AntSkeleton>
304
+ </div>
305
+ </div>
158
306
  </AntField>
159
307
  </template>
@@ -24,16 +24,15 @@ import {
24
24
  ref, nextTick,
25
25
  } from 'vue';
26
26
 
27
- const phoneInputNativeRef = ref<HTMLInputElement | null>(null);
28
-
29
27
  defineOptions({
30
28
  inheritAttrs: false,
31
29
  });
32
30
 
33
31
  const props = withDefaults(defineProps<{
34
32
  modelValue: string | null;
35
- countryValue: string | number | null;
33
+ countryValue?: string | number | null;
36
34
  countries?: Country[];
35
+ inputRef?: null | HTMLInputElement;
37
36
 
38
37
  //Common Props
39
38
  size?: Size;
@@ -53,7 +52,6 @@ const props = withDefaults(defineProps<{
53
52
  searchable?: boolean;
54
53
  countryMaxHeight?: string;
55
54
  countryValueKey?: CountryValueKey;
56
- countryErrorMessage?: string;
57
55
  countrySortable?: boolean;
58
56
 
59
57
  //AntBaseInput Props
@@ -61,6 +59,7 @@ const props = withDefaults(defineProps<{
61
59
  nullable?: boolean;
62
60
  locale?: Locale;
63
61
  }>(), {
62
+ inputRef: null,
64
63
  size: Size.md,
65
64
  state: InputState.base,
66
65
  searchable: true,
@@ -68,7 +67,6 @@ const props = withDefaults(defineProps<{
68
67
  countryPlaceholder: 'Select country',
69
68
  placeholder: 'Enter phone number',
70
69
  countryValueKey: CountryValueKey.dialCode,
71
- countryErrorMessage: 'Please select a country code or start with "+"',
72
70
  countrySortable: true,
73
71
  messages: () => [],
74
72
  nullable: true,
@@ -79,13 +77,27 @@ const props = withDefaults(defineProps<{
79
77
  const emit = defineEmits([
80
78
  'update:modelValue',
81
79
  'update:countryValue',
80
+ 'update:inputRef',
82
81
  'select-country',
83
82
  'validate',
84
83
  'blur',
85
84
  ]);
86
85
 
87
- const _countryValue = useVModel(props, 'countryValue', emit);
88
86
  const _phoneNumber = useVModel(props, 'modelValue', emit);
87
+ const internalInputRef = ref<HTMLInputElement | null>(null);
88
+ const _inputRef = useVModel(props, 'inputRef', emit);
89
+ const internalCountryValue = ref<string | number | null>(null);
90
+
91
+ const _countryValue = computed({
92
+ get: () => {
93
+ return props.countryValue !== undefined ? props.countryValue : internalCountryValue.value;
94
+ },
95
+ set: (val) => {
96
+ emit('update:countryValue', val);
97
+
98
+ internalCountryValue.value = val;
99
+ },
100
+ });
89
101
 
90
102
  const updateFullValue = (countryId: string | number | null, rawPhone: string | null) => {
91
103
  if (!rawPhone) {
@@ -104,26 +116,8 @@ const updateFullValue = (countryId: string | number | null, rawPhone: string | n
104
116
  }
105
117
  };
106
118
 
107
- const showCountryError = computed(() => {
108
- const val = props.modelValue || '';
109
-
110
- return props.countryValue == null && val.length > 0 && !val.startsWith('+');
111
- });
112
-
113
- const allMessages = computed(() => {
114
- const msgs = [
115
- ...(props.messages || []),
116
- ];
117
-
118
- if (showCountryError.value) {
119
- msgs.push(props.countryErrorMessage);
120
- }
121
-
122
- return msgs;
123
- });
124
-
125
119
  const currentCountry = computed(() => {
126
- return props.countries.find(c => String(c[props.countryValueKey]) === String(props.countryValue));
120
+ return props.countries.find(c => String(c[props.countryValueKey]) === String(_countryValue.value));
127
121
  });
128
122
 
129
123
  const sortedCountriesByDialCode = computed(() => {
@@ -213,7 +207,7 @@ function onCountrySelect(country: Country) {
213
207
  emit('select-country', country);
214
208
 
215
209
  nextTick(() => {
216
- phoneInputNativeRef.value?.focus();
210
+ internalInputRef.value?.focus();
217
211
  });
218
212
  }
219
213
 
@@ -232,13 +226,13 @@ function onKeyPress(event: KeyboardEvent) {
232
226
  return;
233
227
  }
234
228
 
235
- if (props.countryValue && charStr === '+') {
229
+ if (_countryValue.value && charStr === '+') {
236
230
  event.preventDefault();
237
231
 
238
232
  return;
239
233
  }
240
234
 
241
- if (!props.countryValue && charStr === '+' && currentRawValue.length > 0) {
235
+ if (!_countryValue.value && charStr === '+' && currentRawValue.length > 0) {
242
236
  event.preventDefault();
243
237
  }
244
238
 
@@ -269,6 +263,11 @@ function onPaste(event: ClipboardEvent) {
269
263
  }
270
264
  }
271
265
 
266
+ function onBlur(e: FocusEvent) {
267
+ emit('blur', e);
268
+ emit('validate', _phoneNumber.value);
269
+ }
270
+
272
271
  watch(_countryValue, (newCountryId, oldCountryId) => {
273
272
  if (newCountryId === oldCountryId) {
274
273
  return;
@@ -284,13 +283,29 @@ watch(_countryValue, (newCountryId, oldCountryId) => {
284
283
 
285
284
  updateFullValue(newCountryId, body);
286
285
  });
286
+
287
+ watch(() => props.modelValue, (newVal) => {
288
+ if (newVal && newVal.startsWith('+') && !_countryValue.value) {
289
+ const country = findCountryByPhone(newVal);
290
+
291
+ if (country) {
292
+ _countryValue.value = country[props.countryValueKey] as string | number;
293
+ }
294
+ }
295
+ }, {
296
+ immediate: true,
297
+ });
298
+
299
+ watch(internalInputRef, (el) => {
300
+ _inputRef.value = el;
301
+ });
287
302
  </script>
288
303
 
289
304
  <template>
290
305
  <AntField
291
306
  :label="label"
292
- :messages="allMessages"
293
- :state="showCountryError ? InputState.danger : state"
307
+ :messages="messages"
308
+ :state="state"
294
309
  :size="size"
295
310
  :skeleton="skeleton"
296
311
  :description="description"
@@ -305,7 +320,7 @@ watch(_countryValue, (newCountryId, oldCountryId) => {
305
320
  :countries="countries"
306
321
  :size="size"
307
322
  :locale="locale"
308
- :state="showCountryError ? InputState.danger : state"
323
+ :state="state"
309
324
  :disabled="disabled"
310
325
  :readonly="readonly"
311
326
  :skeleton="skeleton"
@@ -324,10 +339,10 @@ watch(_countryValue, (newCountryId, oldCountryId) => {
324
339
 
325
340
  <AntBaseInput
326
341
  v-model="formattedNumber"
327
- v-model:input-ref="phoneInputNativeRef"
342
+ v-model:input-ref="internalInputRef"
328
343
  :nullable="nullable"
329
344
  :type="BaseInputType.text"
330
- :state="showCountryError ? InputState.danger : state"
345
+ :state="state"
331
346
  :size="size"
332
347
  :skeleton="skeleton"
333
348
  v-bind="$attrs"
@@ -337,8 +352,7 @@ watch(_countryValue, (newCountryId, oldCountryId) => {
337
352
  :grouped="Grouped.right"
338
353
  wrapper-class="flex-grow"
339
354
  class="-ml-px"
340
- @validate="val => $emit('validate', val)"
341
- @blur="e => $emit('blur', e)"
355
+ @blur="onBlur"
342
356
  @keydown="onKeyPress"
343
357
  @paste="onPaste"
344
358
  />
@@ -107,13 +107,6 @@ const gapSize = computed(() => {
107
107
  return 'gap-1';
108
108
  }
109
109
  });
110
- const valueSize = computed(() => {
111
- if (props.size === Size.xs || props.size === Size.xs2) {
112
- return 'h-4';
113
- } else {
114
- return 'h-5';
115
- }
116
- });
117
110
  const innerRadioClass = computed(() => (
118
111
  {
119
112
  'bg-primary-500': props.state === InputState.base,
@@ -177,50 +170,51 @@ onMounted(() => {
177
170
  data-e2e="radio"
178
171
  >
179
172
  <div
180
- class="flex items-center"
173
+ class="flex"
181
174
  :class="gapSize"
182
175
  >
183
- <div class="relative full-height flex items-center">
184
- <div class="absolute flex items-center justify-center w-full h-full">
185
- <Transition name="fade-radio">
186
- <div
187
- v-if="isActive"
188
- class="rounded-full transition-all"
189
- :class="innerRadioClass"
190
- />
191
- </Transition>
192
- </div>
193
-
194
- <input
195
- v-model="_modelValue"
196
- :value="value.value"
197
- :class="inputClasses"
198
- type="radio"
199
- :aria-checked="isActive"
200
- :disabled="disabled || readonly"
201
- @blur="onBlur"
202
- >
203
-
176
+ <div
177
+ class="relative flex"
178
+ :class="{
179
+ 'h-5 w-5': size === Size.lg || size === Size.md || size === Size.sm,
180
+ 'h-4 w-4': size === Size.xs || size === Size.xs2,
181
+ }"
182
+ >
204
183
  <AntSkeleton
205
184
  :visible="skeleton"
206
- absolute
207
185
  rounded-full
208
- />
186
+ @click.prevent
187
+ >
188
+ <div class="absolute flex items-center justify-center w-full h-full">
189
+ <Transition name="fade-radio">
190
+ <div
191
+ v-if="isActive"
192
+ class="rounded-full transition-all"
193
+ :class="innerRadioClass"
194
+ />
195
+ </Transition>
196
+ </div>
197
+
198
+ <input
199
+ v-model="_modelValue"
200
+ :value="value.value"
201
+ :class="inputClasses"
202
+ type="radio"
203
+ :aria-checked="isActive"
204
+ :disabled="disabled || readonly"
205
+ @blur="onBlur"
206
+ >
207
+ </AntSkeleton>
209
208
  </div>
210
209
 
211
- <div
212
- class="flex items-center"
213
- :class="valueSize"
210
+ <AntSkeleton
211
+ :visible="skeleton"
212
+ rounded
214
213
  >
215
214
  <span :class="valueClass">
216
- <AntSkeleton
217
- :visible="skeleton"
218
- rounded
219
- >
220
- {{ value.label }}
221
- </AntSkeleton>
215
+ {{ value.label }}
222
216
  </span>
223
- </div>
217
+ </AntSkeleton>
224
218
  </div>
225
219
  </AntField>
226
220
  </template>