@citizenplane/pimp 9.4.1 → 9.4.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.
@@ -1,12 +1,13 @@
1
1
  <template>
2
2
  <div class="cpInput" :class="dynamicClasses" :aria-disabled="isDisabled" @click="focusOnInput">
3
- <base-input-label v-if="label" v-bind-once="{ for: inputIdentifier }">
4
- {{ capitalizedLabel }} <span v-if="isRequired" class="u-asterisk">*</span>
5
- <cp-tooltip v-if="tooltip" :content="tooltip">
6
- <button type="button" class="cpInput__tooltip">
7
- <cp-icon class="cpInput__tooltipIcon" type="tooltip" />
8
- </button>
9
- </cp-tooltip>
3
+ <base-input-label
4
+ v-if="label"
5
+ v-bind-once="{ for: inputIdentifier }"
6
+ :is-invalid="isInputInvalid"
7
+ :tooltip="tooltip"
8
+ :required="isRequired"
9
+ >
10
+ {{ capitalizedLabel }}
10
11
  </base-input-label>
11
12
  <div
12
13
  ref="cpInputContainer"
@@ -17,21 +18,13 @@
17
18
  <cp-icon v-if="isSearch" type="search" />
18
19
  <slot v-else name="leading-icon" />
19
20
  </div>
20
- <transition name="fade-in">
21
- <div
22
- v-if="displayInvalidityIcon"
23
- class="cpInput__icon cpInput__icon--isInvalidity"
24
- :class="iconInvalidityClasses"
25
- >
26
- <cp-icon type="alert-circle" />
27
- </div>
28
- </transition>
29
21
  <input
30
22
  v-model="inputModel"
31
23
  v-bind-once="{ id: inputIdentifier }"
32
24
  v-maska
33
25
  :data-maska="mask"
34
26
  v-bind="restAttributes"
27
+ :disabled="isDisabled"
35
28
  :aria-invalid="isInputInvalid"
36
29
  :aria-describedby="inputDescribedByAttribute"
37
30
  class="cpInput__inner"
@@ -61,6 +54,7 @@ import { ref, useAttrs, useSlots, computed, nextTick, onMounted, useId } from 'v
61
54
 
62
55
  import BaseInputLabel from '@/components/BaseInputLabel.vue'
63
56
 
57
+ import { Sizes } from '@/constants'
64
58
  import { randomString, capitalizeFirstLetter } from '@/helpers'
65
59
 
66
60
  interface Emits {
@@ -70,15 +64,14 @@ interface Emits {
70
64
  interface Props {
71
65
  errorMessage?: string
72
66
  help?: string
73
- hideInvalidityIcon?: boolean
74
67
  inputId?: string | null
75
68
  isInvalid?: boolean
76
- isLarge?: boolean
77
69
  isSearch?: boolean
78
70
  label?: string
79
71
  mask?: string | Record<string, unknown> | null
80
72
  modelValue?: string | number | boolean
81
73
  removeBorder?: boolean
74
+ size?: Sizes
82
75
  tooltip?: string
83
76
  }
84
77
 
@@ -90,11 +83,10 @@ const props = withDefaults(defineProps<Props>(), {
90
83
  isInvalid: false,
91
84
  errorMessage: '',
92
85
  mask: null,
93
- hideInvalidityIcon: false,
94
86
  removeBorder: false,
95
- isLarge: false,
96
87
  isSearch: false,
97
88
  help: '',
89
+ size: Sizes.MD,
98
90
  })
99
91
 
100
92
  const emit = defineEmits<Emits>()
@@ -136,18 +128,17 @@ const isRequired = computed(() => checkAttribute('required'))
136
128
  const dynamicClasses = computed(() => {
137
129
  return [
138
130
  attrs.class,
131
+ `cpInput--${props.size}`,
139
132
  {
140
133
  'cpInput--isInvalid': isInputInvalid.value,
141
134
  'cpInput--isDisabled': isDisabled.value,
142
135
  'cpInput--hasNoBorder': props.removeBorder,
143
- 'cpInput--isLarge': props.isLarge,
144
136
  'cpInput--isSearch': props.isSearch,
145
137
  },
146
138
  ]
147
139
  })
148
140
 
149
141
  const isInputInvalid = computed(() => props.isInvalid || !isDOMElementValid.value)
150
- const displayInvalidityIcon = computed(() => !props.hideInvalidityIcon && isInputInvalid.value)
151
142
 
152
143
  const hasBeforeIconSlot = computed(() => !!slots['leading-icon'])
153
144
  const hasBeforeIcon = computed(() => hasBeforeIconSlot.value || props.isSearch)
@@ -155,16 +146,10 @@ const hasBeforeIcon = computed(() => hasBeforeIconSlot.value || props.isSearch)
155
146
  const hasAfterIconSlot = computed(() => !!slots['trailing-icon'])
156
147
  const hasAfterIcon = computed(() => hasAfterIconSlot.value || props.isSearch)
157
148
 
158
- const iconInvalidityClasses = computed(() => {
159
- return {
160
- 'cpInput__icon--hasAfterAndInvalidityIcon': hasAfterIconSlot.value,
161
- }
162
- })
163
-
164
149
  const DOMElement = computed(() => cpInputContainer.value.children.namedItem(inputIdentifier.value))
165
150
 
166
151
  const displayErrorMessage = computed(() => isInputInvalid.value && props.errorMessage.length)
167
- const isClearButtonVisible = computed(() => props.isSearch && inputModel.value.toString().length)
152
+ const isClearButtonVisible = computed(() => props.isSearch && inputModel.value.toString().length && !isDisabled.value)
168
153
 
169
154
  const displayHelp = computed(() => props.help?.length && !displayErrorMessage.value)
170
155
 
@@ -197,11 +182,6 @@ onMounted(async () => {
197
182
  </script>
198
183
 
199
184
  <style lang="scss">
200
- // Mixins
201
- @mixin cp-input-hover-style() {
202
- outline: fn.px-to-rem(1) solid colors.$primary-color;
203
- }
204
-
205
185
  // Main
206
186
  .cpInput {
207
187
  position: relative;
@@ -222,41 +202,21 @@ onMounted(async () => {
222
202
  }
223
203
  }
224
204
 
225
- &__clear {
226
- display: flex;
227
- padding: fn.px-to-em(3);
228
- color: colors.$neutral-light;
229
- border-radius: 50%;
230
- background-color: colors.$neutral-dark-1;
231
-
232
- &:focus-visible {
233
- outline: fn.px-to-em(3) solid color.scale(colors.$primary-color, $lightness: 80%);
234
- }
235
- }
236
-
237
- &__clearIcon {
238
- width: fn.px-to-em(12);
239
- height: fn.px-to-em(12);
240
- pointer-events: none;
241
- }
242
-
243
205
  &__inner {
244
- box-shadow: inset 0 fn.px-to-em(1) fn.px-to-em(2) rgba(0, 0, 0, 0.12);
206
+ box-shadow: 0 0 0 fn.px-to-rem(1) rgba(0, 0, 0, 0.12);
245
207
  appearance: none;
246
- outline: fn.px-to-rem(1) solid colors.$neutral-dark-4;
247
- border-radius: fn.px-to-em(10);
208
+ border-radius: fn.px-to-rem(10);
248
209
  width: 100%;
249
210
  color: inherit;
250
- padding: sp.$space;
211
+ padding: sp.$space sp.$space-lg;
251
212
  line-height: fn.px-to-rem(24);
252
213
  font-size: fn.px-to-em(14);
253
214
 
254
215
  &:hover {
255
- @include cp-input-hover-style;
216
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$primary-color;
256
217
  }
257
218
 
258
219
  &:focus {
259
- box-shadow: 0 0 0 fn.px-to-em(2) color.scale(colors.$primary-color, $lightness: 80%);
260
220
  outline: fn.px-to-rem(2) solid colors.$primary-color;
261
221
  background-color: colors.$neutral-light;
262
222
  }
@@ -271,7 +231,7 @@ onMounted(async () => {
271
231
  .cpInput__inner:hover,
272
232
  .cpInput__inner:focus,
273
233
  .cpInput__icon:hover ~ .cpInput__inner {
274
- outline: fn.px-to-rem(1) solid colors.$error-color;
234
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$error-color;
275
235
  }
276
236
 
277
237
  .cpInput__inner:focus {
@@ -281,7 +241,7 @@ onMounted(async () => {
281
241
  }
282
242
 
283
243
  &--isDisabled {
284
- color: colors.$neutral-dark-2 !important;
244
+ color: colors.$neutral-dark-2;
285
245
 
286
246
  .cpInput__container * {
287
247
  cursor: not-allowed;
@@ -293,11 +253,11 @@ onMounted(async () => {
293
253
 
294
254
  .cpInput__inner,
295
255
  .cpInput__inner:hover {
296
- outline: fn.px-to-rem(1) solid colors.$neutral-dark-4 !important;
256
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$neutral-dark-4;
297
257
  }
298
258
 
299
259
  .cpInput__icon svg {
300
- stroke: colors.$neutral-dark-2 !important;
260
+ stroke: colors.$neutral-dark-2;
301
261
  }
302
262
  }
303
263
 
@@ -309,63 +269,18 @@ onMounted(async () => {
309
269
  box-shadow: none;
310
270
  }
311
271
 
312
- &__tooltip {
313
- display: flex;
314
- padding: sp.$space-sm;
315
- align-items: center;
316
- justify-content: center;
317
- border-radius: 50%;
318
- color: colors.$neutral-dark-3;
319
- outline-offset: fn.px-to-em(-3);
320
-
321
- &:hover,
322
- &:focus {
323
- color: colors.$primary-color;
324
- }
325
-
326
- &:focus-visible {
327
- outline: fn.px-to-em(2) solid colors.$primary-color;
328
- }
329
- }
330
-
331
- &__tooltipIcon {
332
- width: fn.px-to-rem(16);
333
- height: fn.px-to-rem(16);
334
- }
335
-
336
- &__help,
337
- &__error {
338
- font-size: fn.px-to-em(14);
339
- line-height: fn.px-to-rem(24);
340
- }
341
-
342
- &__help {
343
- color: colors.$neutral-dark-1;
344
- }
345
-
346
- &__error {
347
- color: colors.$error-color;
348
- font-weight: 500;
349
-
350
- &::first-letter {
351
- text-transform: capitalize;
352
- }
353
- }
354
-
355
- $cp-input-icon-size: fn.px-to-em(20);
356
-
357
272
  &__icon {
358
- left: fn.px-to-rem(8);
273
+ @include mx.square-sizing(16);
274
+
275
+ left: fn.px-to-rem(14);
359
276
  z-index: 1;
360
277
  position: absolute;
361
278
  top: 50%;
362
279
  transform: translateY(-50%);
363
- width: $cp-input-icon-size;
364
- height: $cp-input-icon-size;
365
280
  overflow: hidden;
366
281
 
367
282
  &:hover ~ .cpInput__inner {
368
- @include cp-input-hover-style;
283
+ box-shadow: 0 0 0 fn.px-to-rem(1) colors.$primary-color;
369
284
  }
370
285
 
371
286
  svg {
@@ -374,102 +289,115 @@ onMounted(async () => {
374
289
  stroke-width: 1.6;
375
290
  }
376
291
 
377
- &--isInvalidity svg {
378
- stroke: colors.$error-color;
379
- }
380
-
381
- &--isAfter,
382
- &--isInvalidity {
292
+ &--isAfter {
383
293
  left: auto;
384
- right: fn.px-to-rem(8);
385
- justify-content: flex-end;
294
+ right: fn.px-to-rem(14);
386
295
  }
387
296
 
388
- &--isAfter {
389
- transition: 50ms right 0.25s cubic-bezier(0.17, 0.84, 0.44, 1);
297
+ &--isBefore ~ input {
298
+ padding-left: calc(#{sp.$space-md} + #{sp.$space-sm} + #{fn.px-to-rem(16) + sp.$space});
390
299
  }
391
300
 
392
301
  &--isAfter ~ input {
393
- padding-right: calc(#{fn.px-to-rem(8)} + #{$cp-input-icon-size} * 2 + #{fn.px-to-em(8)});
302
+ padding-right: calc(#{sp.$space-md} + #{sp.$space-sm} + #{fn.px-to-rem(16) + sp.$space});
394
303
  }
304
+ }
395
305
 
396
- &--isBefore ~ input {
397
- padding-left: calc(#{fn.px-to-rem(8)} + #{$cp-input-icon-size} + #{fn.px-to-em(8)});
306
+ &--sm {
307
+ .cpInput__inner {
308
+ padding: sp.$space-sm sp.$space-md;
309
+ font-size: fn.px-to-rem(14);
398
310
  }
399
311
 
400
- &--isInvalidity ~ input {
401
- padding-right: calc(#{fn.px-to-rem(8)} + #{$cp-input-icon-size} + #{fn.px-to-em(8)});
312
+ .cpInput__icon {
313
+ left: fn.px-to-rem(10);
402
314
  }
403
315
 
404
- &--hasAfterAndInvalidityIcon ~ input {
405
- padding-right: calc(#{fn.px-to-rem(8)} + #{$cp-input-icon-size} * 2 + #{fn.px-to-em(8)} * 2);
316
+ .cpInput__icon--isAfter {
317
+ left: auto;
318
+ right: fn.px-to-rem(10);
406
319
  }
407
320
 
408
- &--isInvalidity {
409
- & ~ .cpInput__icon--isAfter {
410
- right: calc(#{fn.px-to-rem(8)} + #{$cp-input-icon-size} + #{fn.px-to-em(8)});
411
- transition: right 0.25s cubic-bezier(0.17, 0.84, 0.44, 1);
412
- }
321
+ .cpInput__icon--isBefore ~ input {
322
+ padding-left: calc(#{sp.$space-sm * 2} + #{fn.px-to-rem(20) + sp.$space});
413
323
  }
414
- }
415
324
 
416
- &--isLarge {
417
- $cp-input-large-icon-size: fn.px-to-em(24);
418
- $cp-input-large-spacing: fn.px-to-em(12);
419
- $cp-input-large-padding: fn.px-to-em(16);
325
+ .cpInput__icon--isAfter ~ input {
326
+ padding-right: calc(#{sp.$space-sm * 2} + #{fn.px-to-rem(20) + sp.$space});
327
+ }
328
+ }
420
329
 
330
+ &--lg {
421
331
  .cpInput__inner {
422
- height: sizing.$component-size-large;
423
- padding: $cp-input-large-padding;
332
+ padding: sp.$space-md sp.$space-lg;
333
+ font-size: fn.px-to-rem(16);
424
334
  }
425
335
 
426
336
  .cpInput__icon {
427
- left: $cp-input-large-padding;
428
- width: $cp-input-large-icon-size;
429
- height: $cp-input-large-icon-size;
337
+ @include mx.square-sizing(20);
338
+
339
+ left: fn.px-to-rem(12);
430
340
  }
431
341
 
432
- .cpInput__icon--isInvalidity,
433
342
  .cpInput__icon--isAfter {
434
343
  left: auto;
435
- right: $cp-input-large-padding;
344
+ right: fn.px-to-rem(12);
436
345
  }
437
346
 
438
347
  .cpInput__icon--isAfter ~ input {
439
- padding-right: calc(#{$cp-input-large-padding} + #{$cp-input-large-icon-size} * 2 + #{$cp-input-large-spacing});
348
+ padding-right: calc(#{sp.$space-lg} + #{fn.px-to-rem(20)} + #{sp.$space-sm});
440
349
  }
441
350
 
442
351
  .cpInput__icon--isBefore ~ input {
443
- padding-left: calc(#{$cp-input-large-padding} + #{$cp-input-large-icon-size} + #{$cp-input-large-spacing});
352
+ padding-left: calc(#{sp.$space-lg} + #{fn.px-to-rem(20)} + #{sp.$space-sm});
444
353
  }
354
+ }
445
355
 
446
- .cpInput__icon--isInvalidity ~ input {
447
- padding-right: calc(#{$cp-input-large-padding} + #{$cp-input-large-icon-size} + #{$cp-input-large-spacing});
356
+ &--isSearch {
357
+ .cpInput__icon--isAfter {
358
+ width: auto;
359
+ height: auto;
360
+ overflow: visible;
448
361
  }
449
362
 
450
- .cpInput__icon--hasAfterAndInvalidityIcon ~ input {
451
- padding-right: calc(
452
- #{$cp-input-large-padding} + #{$cp-input-large-icon-size} * 2 + #{$cp-input-large-spacing} * 2
453
- );
454
- }
363
+ .cpInput__clear {
364
+ display: flex;
365
+ color: colors.$neutral-light;
366
+ padding: sp.$space-xs;
367
+ border-radius: 50%;
368
+ background-color: colors.$neutral-dark-1;
455
369
 
456
- .cpInput__icon--isInvalidity ~ .cpInput__icon--isAfter {
457
- & ~ .cpInput__icon--isAfter {
458
- right: calc(#{$cp-input-large-padding} + #{$cp-input-large-icon-size} + #{fn.px-to-em(8)});
370
+ &:focus-visible {
371
+ outline: fn.px-to-rem(2) solid color.scale(colors.$primary-color, $lightness: 80%);
459
372
  }
460
373
  }
461
- }
462
374
 
463
- &--isSearch {
464
- .cpInput__icon--isAfter {
465
- display: flex;
466
- align-items: center;
467
- justify-content: center;
468
- overflow: visible;
375
+ .cpInput__clearIcon {
376
+ @include mx.square-sizing(12);
377
+
378
+ pointer-events: none;
469
379
  }
470
380
 
471
- .cpInput__inner {
472
- padding-right: calc(#{$cp-input-icon-size} * 2);
381
+ &.cpInput--lg .cpInput__clearIcon {
382
+ @include mx.square-sizing(16);
383
+ }
384
+ }
385
+
386
+ &__help,
387
+ &__error {
388
+ font-size: fn.px-to-em(14);
389
+ line-height: fn.px-to-rem(24);
390
+ }
391
+
392
+ &__help {
393
+ color: colors.$neutral-dark-1;
394
+ }
395
+
396
+ &__error {
397
+ color: colors.$error-color;
398
+
399
+ &::first-letter {
400
+ text-transform: capitalize;
473
401
  }
474
402
  }
475
403
  }
@@ -30,6 +30,7 @@ const ariaId = useId()
30
30
  <style lang="scss">
31
31
  .cpTooltip.v-popper--theme-tooltip {
32
32
  display: inline-flex;
33
+ white-space: pre-wrap;
33
34
 
34
35
  .v-popper__inner {
35
36
  padding: sp.$space sp.$space-md;
@@ -4,6 +4,8 @@ import type { Meta, StoryObj } from '@storybook/vue3'
4
4
 
5
5
  import CpInput from '@/components/CpInput.vue'
6
6
 
7
+ import { Sizes } from '@/constants'
8
+
7
9
  const meta = {
8
10
  title: 'CpInput',
9
11
  component: CpInput,
@@ -12,6 +14,11 @@ const meta = {
12
14
  control: 'text',
13
15
  description: 'The input value',
14
16
  },
17
+ size: {
18
+ control: 'select',
19
+ options: ['sm', 'md', 'lg'],
20
+ description: 'The size of the input',
21
+ },
15
22
  type: {
16
23
  control: 'select',
17
24
  options: ['text', 'email', 'password', 'number', 'tel', 'url'],
@@ -55,15 +62,14 @@ export const Default: Story = {
55
62
  args: {
56
63
  label: 'Input Label',
57
64
  placeholder: 'Enter text here',
65
+ size: Sizes.MD,
58
66
  type: 'text',
59
67
  required: false,
60
68
  disabled: false,
61
69
  autocomplete: 'off',
62
70
  isInvalid: false,
63
71
  errorMessage: '',
64
- hideInvalidityIcon: false,
65
72
  removeBorder: false,
66
- isLarge: false,
67
73
  isSearch: false,
68
74
  help: '',
69
75
  },
@@ -155,3 +161,32 @@ export const Mask: Story = {
155
161
  mask: '## ## ## ## ##',
156
162
  },
157
163
  }
164
+ export const WithIcon: Story = {
165
+ args: {
166
+ ...Default.args,
167
+ required: false,
168
+ disabled: false,
169
+ autocomplete: 'off',
170
+ isInvalid: false,
171
+ errorMessage: '',
172
+ removeBorder: false,
173
+ isSearch: true,
174
+ help: '',
175
+ },
176
+ render: (args) => ({
177
+ components: { CpInput },
178
+ setup() {
179
+ const value = ref('')
180
+ return { args, value }
181
+ },
182
+ template: `
183
+ <div style="max-width: 400px; padding: 20px;">
184
+ <CpInput
185
+ v-model="value"
186
+ v-bind="args"
187
+ >
188
+ </CpInput>
189
+ </div>
190
+ `,
191
+ }),
192
+ }