@citizenplane/pimp 9.6.6 → 9.7.1

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.
@@ -0,0 +1,317 @@
1
+ <template>
2
+ <div class="cpTelInput" :class="componentDynamicClass">
3
+ <base-input-label v-if="label" v-bind-once="{ for: inputIdentifier }" :is-invalid :required :tooltip>
4
+ {{ capitalizedLabel }}
5
+ </base-input-label>
6
+ <div class="cpTelInput__container">
7
+ <vue-tel-input
8
+ ref="telInputRef"
9
+ v-model="telModel"
10
+ class="cpTelInput__wrapper"
11
+ :class="wrapperDynamicClasses"
12
+ :disabled
13
+ :input-options
14
+ mode="international"
15
+ placeholder="Enter your phone number"
16
+ valid-characters-only
17
+ @country-changed="focusOnInput"
18
+ >
19
+ <template #arrow-icon>
20
+ <cp-icon class="cpTelInput__arrow" size="20" type="chevron-down" />
21
+ </template>
22
+ </vue-tel-input>
23
+ </div>
24
+ <transition-expand mode="out-in">
25
+ <p v-if="displayErrorMessage" :id="errorMessageId" class="cpTelInput__error">
26
+ {{ errorMessage }}
27
+ </p>
28
+ <p v-else-if="displayHelp" :id="helpMessageId" class="cpTelInput__help">
29
+ {{ help }}
30
+ </p>
31
+ </transition-expand>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { useAttrs, ref, useId, computed, useTemplateRef } from 'vue'
37
+
38
+ import BaseInputLabel from '@/components/BaseInputLabel.vue'
39
+
40
+ import { Sizes } from '@/constants'
41
+ import { capitalizeFirstLetter } from '@/helpers'
42
+
43
+ interface Props {
44
+ disabled?: boolean
45
+ errorMessage?: string
46
+ help?: string
47
+ isInvalid?: boolean
48
+ label?: string
49
+ placeholder?: string
50
+ required?: boolean
51
+ size?: Sizes
52
+ tooltip?: string
53
+ }
54
+
55
+ const props = withDefaults(defineProps<Props>(), {
56
+ label: '',
57
+ placeholder: 'Enter phone number',
58
+ errorMessage: '',
59
+ help: '',
60
+ disabled: false,
61
+ isInvalid: false,
62
+ required: false,
63
+ tooltip: '',
64
+ size: Sizes.MD,
65
+ })
66
+
67
+ const telInputRef = useTemplateRef<HTMLInputElement>('telInputRef')
68
+
69
+ const helpMessageId = useId()
70
+ const errorMessageId = useId()
71
+
72
+ const attrs = useAttrs()
73
+
74
+ const componentDynamicClass = computed(() => [`cpTelInput--${props.size}`])
75
+
76
+ const wrapperDynamicClasses = computed(() => ({
77
+ 'cpTelInput__wrapper--isInvalid': props.isInvalid,
78
+ 'cpTelInput__wrapper--isDisabled': props.disabled,
79
+ }))
80
+
81
+ const inputOptions = computed(() => ({
82
+ autofocus: true,
83
+ placeholder: props.placeholder,
84
+ styleClasses: {
85
+ cpTelInput__input: true,
86
+ 'cpTelInput__input--isInvalid': props.isInvalid,
87
+ 'cpTelInput__input--isDisabled': props.disabled,
88
+ },
89
+ }))
90
+
91
+ const telModel = defineModel<string>('telModel')
92
+
93
+ const inputIdentifier = ref<string>((attrs.id as string | undefined) || useId())
94
+
95
+ const capitalizedLabel = computed(() => capitalizeFirstLetter(props.label))
96
+
97
+ const displayErrorMessage = computed(() => props.isInvalid && props.errorMessage.length)
98
+
99
+ const displayHelp = computed(() => props.help?.length && !displayErrorMessage.value)
100
+
101
+ const focusOnInput = () => {
102
+ if (!telInputRef.value) return
103
+ telInputRef.value.focus()
104
+ }
105
+ </script>
106
+
107
+ <style lang="scss">
108
+ .cpTelInput {
109
+ position: relative;
110
+ display: flex;
111
+ flex-direction: column;
112
+
113
+ &:has(.cpTelInput__help, .cpTelInput__error) .cpTelInput__container {
114
+ margin-bottom: sp.$space;
115
+ }
116
+
117
+ &__container {
118
+ z-index: 1;
119
+ position: relative;
120
+ display: flex;
121
+ }
122
+
123
+ &__wrapper {
124
+ border: none;
125
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$border-color;
126
+ appearance: none;
127
+ border-radius: fn.px-to-rem(10);
128
+ width: 100%;
129
+ color: inherit;
130
+
131
+ &:focus-within {
132
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$border-color;
133
+ }
134
+
135
+ &:not(.cpTelInput__wrapper--isInvalid, .cpTelInput__wrapper--isDisabled):hover {
136
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$primary-color;
137
+ }
138
+
139
+ &--isInvalid {
140
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$error-color;
141
+ }
142
+
143
+ &.disabled {
144
+ overflow: hidden;
145
+ }
146
+ }
147
+
148
+ &__input {
149
+ padding: sp.$space sp.$space-lg;
150
+ color: inherit;
151
+ line-height: fn.px-to-rem(24);
152
+ font-size: fn.px-to-rem(14);
153
+ border-top-right-radius: fn.px-to-rem(10);
154
+ border-bottom-right-radius: fn.px-to-rem(10);
155
+
156
+ &:is(:hover, :focus) {
157
+ z-index: 1;
158
+ }
159
+
160
+ &:not(.cpTelInput__input--isInvalid, .cpTelInput__input--isDisabled):focus {
161
+ outline: fn.px-to-rem(2) solid colors.$primary-color;
162
+ background-color: colors.$neutral-light;
163
+ }
164
+
165
+ &::placeholder {
166
+ color: colors.$neutral-dark-1;
167
+ }
168
+
169
+ &--isDisabled {
170
+ pointer-events: none;
171
+ background-color: colors.$neutral-light-1;
172
+ }
173
+ }
174
+
175
+ &__input--isInvalid {
176
+ &:focus {
177
+ outline: fn.px-to-rem(2) solid colors.$error-color;
178
+ }
179
+
180
+ .cpTelInput__input:focus {
181
+ outline: fn.px-to-rem(2) solid colors.$error-color;
182
+ box-shadow: 0 0 0 fn.px-to-em(1) color.scale(colors.$error-color, $lightness: 60%);
183
+ }
184
+ }
185
+
186
+ &__arrow {
187
+ color: colors.$neutral-dark-1;
188
+ transition: transform 150ms ease;
189
+ }
190
+
191
+ .vti__dropdown.open .cpTelInput__arrow {
192
+ transform: rotate(180deg);
193
+ }
194
+
195
+ .vti__dropdown {
196
+ z-index: 1;
197
+ position: unset;
198
+ border-right: fn.px-to-rem(1) solid colors.$neutral-dark-5;
199
+
200
+ &:focus,
201
+ &:focus-within {
202
+ outline: fn.px-to-rem(2) solid colors.$primary-color;
203
+ }
204
+ }
205
+
206
+ &__wrapper--isInvalid {
207
+ .vti__dropdown:focus,
208
+ .vti__dropdown:focus-within {
209
+ outline: fn.px-to-rem(2) solid colors.$error-color;
210
+ }
211
+ }
212
+
213
+ &__wrapper--isDisabled {
214
+ background-color: colors.$neutral-light-1;
215
+ color: colors.$neutral-dark-1;
216
+ cursor: not-allowed;
217
+
218
+ .vti__dropdown:focus {
219
+ outline: none;
220
+ }
221
+ }
222
+
223
+ .vti__selection {
224
+ gap: sp.$space;
225
+
226
+ .vti__flag {
227
+ margin: 0;
228
+ }
229
+ }
230
+
231
+ .vti__flag {
232
+ max-width: fn.px-to-rem(20);
233
+ border-radius: fn.px-to-rem(2);
234
+ }
235
+
236
+ .vti__dropdown {
237
+ padding: sp.$space sp.$space sp.$space sp.$space-md;
238
+ border-bottom-left-radius: fn.px-to-rem(10);
239
+ border-top-left-radius: fn.px-to-rem(10);
240
+ }
241
+
242
+ .vti__dropdown-list {
243
+ width: 100%;
244
+ padding: sp.$space-sm;
245
+ border-radius: fn.px-to-rem(10);
246
+
247
+ &.below {
248
+ top: fn.px-to-rem(40) + sp.$space;
249
+ }
250
+ }
251
+
252
+ .vti__dropdown-item {
253
+ padding: sp.$space-md sp.$space-lg sp.$space-md sp.$space-md;
254
+ display: flex;
255
+ align-items: flex-start;
256
+ border-radius: fn.px-to-rem(4);
257
+ font-size: fn.px-to-rem(14);
258
+ line-height: fn.px-to-rem(24);
259
+
260
+ .vti__flag {
261
+ margin: auto sp.$space-lg auto 0;
262
+ }
263
+
264
+ strong,
265
+ span:not(.vti__flag) {
266
+ font-weight: 400;
267
+ }
268
+
269
+ strong {
270
+ @extend %u-text-ellipsis;
271
+
272
+ white-space: nowrap;
273
+ }
274
+
275
+ span:not(.vti__flag) {
276
+ margin-left: 4px;
277
+ }
278
+ }
279
+
280
+ &--lg {
281
+ .cpTelInput__input {
282
+ font-size: fn.px-to-rem(16);
283
+ padding: sp.$space-md sp.$space-lg;
284
+ }
285
+
286
+ .vti__dropdown {
287
+ padding: sp.$space-md sp.$space sp.$space-md sp.$space-md;
288
+ }
289
+
290
+ .vti__dropdown-list.below {
291
+ top: fn.px-to-rem(48) + sp.$space;
292
+ }
293
+
294
+ .vti__dropdown-item {
295
+ font-size: fn.px-to-rem(16);
296
+ }
297
+ }
298
+
299
+ &__help,
300
+ &__error {
301
+ font-size: fn.px-to-em(14);
302
+ line-height: fn.px-to-rem(24);
303
+ }
304
+
305
+ &__help {
306
+ color: colors.$neutral-dark-1;
307
+ }
308
+
309
+ &__error {
310
+ color: colors.$error-color;
311
+
312
+ &::first-letter {
313
+ text-transform: capitalize;
314
+ }
315
+ }
316
+ }
317
+ </style>
@@ -31,6 +31,7 @@ import CpSelect from './CpSelect.vue'
31
31
  import CpSelectMenu from './CpSelectMenu.vue'
32
32
  import CpSwitch from './CpSwitch.vue'
33
33
  import CpTable from './CpTable.vue'
34
+ import CpTelInput from './CpTelInput.vue'
34
35
  import CpTextarea from './CpTextarea.vue'
35
36
  import CpToaster from './CpToaster.vue'
36
37
  import CpTooltip from './CpTooltip.vue'
@@ -73,6 +74,7 @@ const Components = {
73
74
  CpSwitch,
74
75
  CpTable,
75
76
  CpIcon,
77
+ CpTelInput,
76
78
  CpTooltip,
77
79
  CpPartnerBadge,
78
80
  CpAirlineLogo,
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+
3
+ import CpTelInput from '@/components/CpTelInput.vue'
4
+
5
+ const meta = {
6
+ title: 'CpTelInput',
7
+ component: CpTelInput,
8
+ argTypes: {
9
+ telModel: {
10
+ control: 'text',
11
+ description: 'The phone number value',
12
+ },
13
+ size: {
14
+ control: 'select',
15
+ options: ['md', 'lg'],
16
+ description: 'The size of the input',
17
+ },
18
+ },
19
+ } satisfies Meta<typeof CpTelInput>
20
+
21
+ export default meta
22
+ type Story = StoryObj<typeof meta>
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ telModel: '',
27
+ },
28
+ render: (args) => ({
29
+ components: { CpTelInput },
30
+ setup() {
31
+ return { args }
32
+ },
33
+ template: `<CpTelInput v-bind="args" />`,
34
+ }),
35
+ }