@citizenplane/pimp 9.7.0 → 9.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@citizenplane/pimp",
3
- "version": "9.7.0",
3
+ "version": "9.7.2",
4
4
  "scripts": {
5
5
  "dev": "storybook dev -p 8080",
6
6
  "build-storybook": "storybook build --output-dir ./docs",
@@ -6,19 +6,24 @@
6
6
  <div class="cpTelInput__container">
7
7
  <vue-tel-input
8
8
  ref="telInputRef"
9
- v-model="telModel"
9
+ v-model="inputModel"
10
10
  class="cpTelInput__wrapper"
11
11
  :class="wrapperDynamicClasses"
12
12
  :disabled
13
+ :dropdown-options
13
14
  :input-options
14
15
  mode="international"
15
16
  placeholder="Enter your phone number"
16
17
  valid-characters-only
17
- @country-changed="focusOnInput"
18
+ @close="focusOnInput"
19
+ @open="handleDropdownOpen"
18
20
  >
19
21
  <template #arrow-icon>
20
22
  <cp-icon class="cpTelInput__arrow" size="20" type="chevron-down" />
21
23
  </template>
24
+ <template #search-icon>
25
+ <cp-icon class="cpTelInput__searchIcon" size="20" type="search" />
26
+ </template>
22
27
  </vue-tel-input>
23
28
  </div>
24
29
  <transition-expand mode="out-in">
@@ -33,7 +38,7 @@
33
38
  </template>
34
39
 
35
40
  <script setup lang="ts">
36
- import { useAttrs, ref, useId, computed, useTemplateRef } from 'vue'
41
+ import { useAttrs, ref, useId, computed, nextTick } from 'vue'
37
42
 
38
43
  import BaseInputLabel from '@/components/BaseInputLabel.vue'
39
44
 
@@ -44,8 +49,10 @@ interface Props {
44
49
  disabled?: boolean
45
50
  errorMessage?: string
46
51
  help?: string
52
+ hideSearch?: boolean
47
53
  isInvalid?: boolean
48
54
  label?: string
55
+ name?: string
49
56
  placeholder?: string
50
57
  required?: boolean
51
58
  size?: Sizes
@@ -61,10 +68,13 @@ const props = withDefaults(defineProps<Props>(), {
61
68
  isInvalid: false,
62
69
  required: false,
63
70
  tooltip: '',
71
+ name: '',
72
+ hideSearch: false,
64
73
  size: Sizes.MD,
65
74
  })
66
75
 
67
- const telInputRef = useTemplateRef<HTMLInputElement>('telInputRef')
76
+ const inputModel = defineModel<string>()
77
+ const telInputRef = ref<HTMLInputElement | null>(null)
68
78
 
69
79
  const helpMessageId = useId()
70
80
  const errorMessageId = useId()
@@ -78,9 +88,17 @@ const wrapperDynamicClasses = computed(() => ({
78
88
  'cpTelInput__wrapper--isDisabled': props.disabled,
79
89
  }))
80
90
 
91
+ const dropdownOptions = computed(() => ({
92
+ showSearchBox: !props.hideSearch,
93
+ showFlags: true,
94
+ tabindex: 0,
95
+ searchBoxPlaceholder: 'Search country...',
96
+ }))
97
+
81
98
  const inputOptions = computed(() => ({
82
99
  autofocus: true,
83
100
  placeholder: props.placeholder,
101
+ name: props.name,
84
102
  styleClasses: {
85
103
  cpTelInput__input: true,
86
104
  'cpTelInput__input--isInvalid': props.isInvalid,
@@ -88,8 +106,6 @@ const inputOptions = computed(() => ({
88
106
  },
89
107
  }))
90
108
 
91
- const telModel = defineModel<string>('telModel')
92
-
93
109
  const inputIdentifier = ref<string>((attrs.id as string | undefined) || useId())
94
110
 
95
111
  const capitalizedLabel = computed(() => capitalizeFirstLetter(props.label))
@@ -98,9 +114,19 @@ const displayErrorMessage = computed(() => props.isInvalid && props.errorMessage
98
114
 
99
115
  const displayHelp = computed(() => props.help?.length && !displayErrorMessage.value)
100
116
 
101
- const focusOnInput = () => {
117
+ const focusOnInput = async () => {
118
+ if (!telInputRef.value) return
119
+ setTimeout(() => telInputRef.value?.focus(), 1)
120
+ }
121
+
122
+ const handleDropdownOpen = async () => {
102
123
  if (!telInputRef.value) return
103
- telInputRef.value.focus()
124
+ await nextTick()
125
+
126
+ const searchBox = telInputRef.value?.$el?.querySelector('input.vti__search_box')
127
+ if (!searchBox) return
128
+
129
+ setTimeout(() => searchBox.focus(), 1)
104
130
  }
105
131
  </script>
106
132
 
@@ -115,7 +141,7 @@ const focusOnInput = () => {
115
141
  }
116
142
 
117
143
  &__container {
118
- z-index: 1;
144
+ z-index: 9;
119
145
  position: relative;
120
146
  display: flex;
121
147
  }
@@ -183,7 +209,8 @@ const focusOnInput = () => {
183
209
  }
184
210
  }
185
211
 
186
- &__arrow {
212
+ &__arrow,
213
+ &__searchIcon {
187
214
  color: colors.$neutral-dark-1;
188
215
  transition: transform 150ms ease;
189
216
  }
@@ -199,6 +226,7 @@ const focusOnInput = () => {
199
226
 
200
227
  &:focus,
201
228
  &:focus-within {
229
+ z-index: 2;
202
230
  outline: fn.px-to-rem(2) solid colors.$primary-color;
203
231
  }
204
232
  }
@@ -239,21 +267,74 @@ const focusOnInput = () => {
239
267
  border-top-left-radius: fn.px-to-rem(10);
240
268
  }
241
269
 
242
- .vti__dropdown-list {
270
+ .vti__search_box_container {
271
+ position: sticky;
272
+ top: 0;
273
+ display: flex;
274
+ margin-bottom: sp.$space-sm;
275
+ }
276
+
277
+ .vti__search_box {
278
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$border-color;
279
+ appearance: none;
280
+ border-radius: fn.px-to-rem(6);
243
281
  width: 100%;
282
+ color: inherit;
283
+ padding: sp.$space-sm sp.$space-sm sp.$space-sm sp.$space;
284
+ line-height: fn.px-to-rem(24);
285
+ font-size: fn.px-to-rem(14);
286
+ text-indent: fn.px-to-rem(28);
287
+ border: none;
288
+
289
+ &:hover {
290
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$primary-color;
291
+ }
292
+
293
+ &:focus {
294
+ outline: fn.px-to-rem(2) solid colors.$primary-color;
295
+ background-color: colors.$neutral-light;
296
+ }
297
+
298
+ &::placeholder {
299
+ color: colors.$neutral-dark-1;
300
+ }
301
+ }
302
+
303
+ &__searchIcon {
304
+ @include mx.square-sizing(16);
305
+
306
+ position: absolute;
307
+ top: 50%;
308
+ transform: translateY(-50%);
309
+ left: fn.px-to-rem(12);
310
+ }
311
+
312
+ .vti__dropdown-list {
313
+ clip-path: inset(0 round fn.px-to-rem(10));
314
+ max-height: fn.px-to-rem(130);
315
+ width: calc(100% + fn.px-to-rem(2));
244
316
  padding: sp.$space-sm;
245
317
  border-radius: fn.px-to-rem(10);
318
+ border: fn.px-to-rem(1) solid colors.$border-color;
319
+
320
+ &:has(.vti__search_box) {
321
+ max-height: fn.px-to-rem(170);
322
+ }
246
323
 
247
324
  &.below {
248
325
  top: fn.px-to-rem(40) + sp.$space;
249
326
  }
327
+
328
+ &.above {
329
+ bottom: calc(100% + fn.px-to-rem(8));
330
+ }
250
331
  }
251
332
 
252
333
  .vti__dropdown-item {
253
334
  padding: sp.$space-md sp.$space-lg sp.$space-md sp.$space-md;
254
335
  display: flex;
255
336
  align-items: flex-start;
256
- border-radius: fn.px-to-rem(4);
337
+ border-radius: fn.px-to-rem(6);
257
338
  font-size: fn.px-to-rem(14);
258
339
  line-height: fn.px-to-rem(24);
259
340
 
@@ -287,6 +368,15 @@ const focusOnInput = () => {
287
368
  padding: sp.$space-md sp.$space sp.$space-md sp.$space-md;
288
369
  }
289
370
 
371
+ .vti__search_box {
372
+ padding: sp.$space sp.$space-lg;
373
+ text-indent: fn.px-to-rem(24);
374
+ }
375
+
376
+ .cpTelInput__searchIcon {
377
+ left: fn.px-to-rem(16);
378
+ }
379
+
290
380
  .vti__dropdown-list.below {
291
381
  top: fn.px-to-rem(48) + sp.$space;
292
382
  }
@@ -3,6 +3,8 @@ import { vMaska } from 'maska/vue'
3
3
  import PrimeVue from 'primevue/config'
4
4
  import { App } from 'vue'
5
5
  import { BindOnceDirective } from 'vue-bind-once'
6
+ import VueTelInput from 'vue-tel-input'
7
+ import 'vue-tel-input/vue-tel-input.css'
6
8
 
7
9
  import ClickOutside from '../directives/ClickOutside'
8
10
  import CpCoreDatepicker from '../libs/CoreDatepicker.vue'
@@ -31,6 +33,7 @@ import CpSelect from './CpSelect.vue'
31
33
  import CpSelectMenu from './CpSelectMenu.vue'
32
34
  import CpSwitch from './CpSwitch.vue'
33
35
  import CpTable from './CpTable.vue'
36
+ import CpTelInput from './CpTelInput.vue'
34
37
  import CpTextarea from './CpTextarea.vue'
35
38
  import CpToaster from './CpToaster.vue'
36
39
  import CpTooltip from './CpTooltip.vue'
@@ -73,6 +76,7 @@ const Components = {
73
76
  CpSwitch,
74
77
  CpTable,
75
78
  CpIcon,
79
+ CpTelInput,
76
80
  CpTooltip,
77
81
  CpPartnerBadge,
78
82
  CpAirlineLogo,
@@ -91,6 +95,7 @@ const Components = {
91
95
  const Pimp = {
92
96
  install(app: App, options: Record<string, unknown>) {
93
97
  app.use(PrimeVue, { unstyled: true })
98
+ app.use(VueTelInput)
94
99
 
95
100
  Object.keys(Components).forEach((name) => {
96
101
  app.component(name, Components[name as keyof typeof Components])