@kiva/kv-components 2.0.0 → 3.0.0

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.
Files changed (50) hide show
  1. package/{.eslintrc.js → .eslintrc.cjs} +1 -1
  2. package/CHANGELOG.md +45 -0
  3. package/__mocks__/ResizeObserver.js +13 -0
  4. package/package.json +23 -10
  5. package/{postcss.config.js → postcss.config.cjs} +2 -2
  6. package/{tailwind.config.js → tailwind.config.cjs} +3 -3
  7. package/tests/unit/jest-setup.js +5 -0
  8. package/tests/unit/specs/components/KvButton.spec.js +14 -25
  9. package/tests/unit/specs/components/KvCarousel.spec.js +11 -0
  10. package/tests/unit/specs/components/KvCheckbox.spec.js +73 -14
  11. package/tests/unit/specs/components/KvLightbox.spec.js +14 -0
  12. package/tests/unit/specs/components/KvProgressBar.spec.js +11 -0
  13. package/tests/unit/specs/components/KvRadio.spec.js +94 -5
  14. package/tests/unit/specs/components/KvSelect.spec.js +113 -0
  15. package/tests/unit/specs/components/KvSwitch.spec.js +92 -33
  16. package/tests/unit/specs/components/KvTabPanel.spec.js +11 -0
  17. package/tests/unit/specs/components/KvTabs.spec.js +99 -0
  18. package/tests/unit/specs/components/KvTextInput.spec.js +86 -9
  19. package/tests/unit/specs/components/KvTextLink.spec.js +16 -24
  20. package/tests/unit/specs/components/KvToast.spec.js +11 -0
  21. package/tests/unit/utils/addVueRouter.js +24 -0
  22. package/utils/attrs.js +62 -0
  23. package/utils/{themeUtils.js → themeUtils.cjs} +0 -0
  24. package/vue/.storybook/{main.js → main.cjs} +13 -5
  25. package/vue/.storybook/preview.js +6 -1
  26. package/vue/KvButton.vue +80 -53
  27. package/vue/KvCarousel.vue +142 -106
  28. package/vue/KvCheckbox.vue +86 -60
  29. package/vue/KvContentfulImg.vue +45 -34
  30. package/vue/KvLightbox.vue +108 -69
  31. package/vue/KvProgressBar.vue +33 -19
  32. package/vue/KvRadio.vue +72 -41
  33. package/vue/KvSelect.vue +46 -20
  34. package/vue/KvSwitch.vue +55 -33
  35. package/vue/KvTab.vue +49 -21
  36. package/vue/KvTabPanel.vue +26 -6
  37. package/vue/KvTabs.vue +70 -53
  38. package/vue/KvTextInput.vue +71 -48
  39. package/vue/KvTextLink.vue +42 -20
  40. package/vue/KvThemeProvider.vue +1 -1
  41. package/vue/KvToast.vue +53 -37
  42. package/vue/stories/KvCheckbox.stories.js +5 -5
  43. package/vue/stories/KvSwitch.stories.js +2 -2
  44. package/vue/stories/KvTabs.stories.js +8 -8
  45. package/vue/stories/KvTextInput.stories.js +1 -1
  46. package/vue/stories/KvThemeProvider.stories.js +1 -1
  47. package/vue/stories/KvToast.stories.js +3 -2
  48. package/vue/stories/StyleguidePrimitives.stories.js +9 -9
  49. package/.babelrc +0 -16
  50. package/jest.config.js +0 -36
@@ -1,12 +1,15 @@
1
1
  <template>
2
- <div>
2
+ <div
3
+ :class="classes"
4
+ :style="styles"
5
+ >
3
6
  <label
4
7
  class="tw-inline-flex tw-items-center"
5
8
  :class="{ 'tw-opacity-low': disabled }"
6
9
  :for="uuid"
7
10
  >
8
11
  <input
9
- v-bind="$attrs"
12
+ v-bind="inputAttrs"
10
13
  :id="uuid"
11
14
  ref="checkboxRef"
12
15
  class="tw-peer tw-appearance-none tw-w-max"
@@ -61,6 +64,18 @@
61
64
 
62
65
  <script>
63
66
  import { nanoid } from 'nanoid';
67
+ import {
68
+ onMounted,
69
+ ref,
70
+ toRefs,
71
+ watch,
72
+ } from 'vue-demi';
73
+ import { useAttrs } from '../utils/attrs';
74
+
75
+ const emits = [
76
+ 'change',
77
+ 'update:modelValue',
78
+ ];
64
79
 
65
80
  /**
66
81
  * Use as you would an <input type="checkbox" />
@@ -69,16 +84,17 @@ import { nanoid } from 'nanoid';
69
84
 
70
85
  export default {
71
86
  inheritAttrs: false,
72
- // v-model will change when checked value changes
87
+
73
88
  model: {
74
- prop: 'checked',
75
- event: 'change',
89
+ prop: 'modelValue',
90
+ event: 'update:modelValue',
76
91
  },
92
+
77
93
  props: {
78
94
  /**
79
95
  * Whether the checkbox is checked or not
80
96
  * */
81
- checked: {
97
+ modelValue: {
82
98
  type: [Boolean, Array],
83
99
  default: false,
84
100
  },
@@ -93,7 +109,7 @@ export default {
93
109
  * Value of the checkbox if v-model is an array
94
110
  * */
95
111
  value: {
96
- type: String,
112
+ type: [String, Boolean],
97
113
  default: '',
98
114
  },
99
115
  /**
@@ -105,73 +121,83 @@ export default {
105
121
  default: true,
106
122
  },
107
123
  },
108
- data() {
109
- return {
110
- uuid: `kvc-${nanoid(10)}`,
111
- isChecked: false,
112
- };
113
- },
114
- computed: {
115
- inputListeners() {
116
- return {
117
- // Pass through any listeners from the parent to the input element, like blur, focus, etc.
118
- // https://vuejs.org/v2/guide/components-custom-events.html#Binding-Native-Events-to-Components
119
- ...this.$listeners,
120
- // ...except for the listener to the 'change' event which is emitted by this component
121
- change: () => {},
122
- };
123
- },
124
- },
125
- watch: {
126
- checked() {
127
- this.setChecked();
128
- },
129
- },
130
- mounted() {
131
- this.uuid = `kvc-${nanoid(10)}`;
132
- },
133
- created() {
134
- this.setChecked();
135
- },
136
- methods: {
137
- onChange(event) {
138
- // get the input[type=checkbox] state
139
- const isChecked = event.target.checked;
124
+ emits,
125
+ setup(props, context) {
126
+ const {
127
+ modelValue,
128
+ value,
129
+ } = toRefs(props);
140
130
 
131
+ const { emit } = context;
132
+ const uuid = ref(`kvc-${nanoid(10)}`);
133
+ const isChecked = ref(false);
134
+ const checkboxRef = ref(null);
135
+
136
+ const {
137
+ classes,
138
+ styles,
139
+ inputAttrs,
140
+ inputListeners,
141
+ } = useAttrs(context, emits);
142
+
143
+ const onChange = (event) => {
144
+ const inputChecked = event.target.checked;
141
145
  let checkboxValue;
142
146
 
143
- if (Array.isArray(this.checked)) {
147
+ if (Array.isArray(modelValue.value)) {
144
148
  // if the model is an array, add or remove our value from it
145
- if (isChecked) {
146
- checkboxValue = [...this.checked, event.target.value];
149
+ if (inputChecked) {
150
+ checkboxValue = [...modelValue.value, event.target.value];
147
151
  } else {
148
- checkboxValue = this.checked.filter((item) => item !== this.value);
152
+ checkboxValue = modelValue.value.filter((item) => item !== value.value);
149
153
  }
150
154
  } else {
151
- checkboxValue = isChecked;
155
+ checkboxValue = inputChecked;
152
156
  }
153
157
 
154
158
  // emit the change event to update the model
155
- this.$emit('change', checkboxValue);
156
- },
157
- /**
158
- * Updates the visual state of the checkbox
159
- * */
160
- setChecked() {
161
- if (Array.isArray(this.checked)) {
159
+ emit('change', checkboxValue);
160
+ emit('update:modelValue', checkboxValue);
161
+ };
162
+
163
+ const setChecked = () => {
164
+ if (Array.isArray(modelValue.value)) {
162
165
  // if the model is array like <kv-checkbox v-model="['item1', 'item2']" value="item1">
163
- this.isChecked = this.checked.includes(this.value);
166
+ isChecked.value = modelValue.value.includes(value.value);
164
167
  } else {
165
168
  // else it's a boolean like <kv-checkbox v-model="true">
166
- this.isChecked = this.checked;
169
+ isChecked.value = modelValue.value;
167
170
  }
168
- },
169
- focus() {
170
- this.$refs.checkboxRef.focus();
171
- },
172
- blur() {
173
- this.$refs.checkboxRef.blur();
174
- },
171
+ };
172
+
173
+ const focus = () => {
174
+ checkboxRef.focus();
175
+ };
176
+
177
+ const blur = () => {
178
+ checkboxRef.blur();
179
+ };
180
+
181
+ setChecked();
182
+ watch(modelValue, () => setChecked());
183
+ onMounted(() => {
184
+ uuid.value = `kvc-${nanoid(10)}`;
185
+ });
186
+
187
+ return {
188
+ uuid,
189
+ isChecked,
190
+ checkboxRef,
191
+ onChange,
192
+ setChecked,
193
+ focus,
194
+ blur,
195
+ classes,
196
+ styles,
197
+ inputAttrs,
198
+ inputListeners,
199
+ };
175
200
  },
176
201
  };
202
+
177
203
  </script>
@@ -5,29 +5,29 @@
5
5
  >
6
6
  <!-- Set of image sources -->
7
7
  <template v-if="sourceSizes.length > 0">
8
- <template v-for="(image, index) in sourceSizes">
9
- <!-- browser supports webp -->
10
- <source
11
- :key="'webp-image'+index"
12
- :media="'('+image.media+')'"
13
- type="image/webp"
14
- :width="image.width ? image.width : null"
15
- :height="image.height ? image.height : null"
16
- :srcset="`
17
- ${buildUrl(image, 2)}&fit=${fit}&f=${focus}&fm=webp&q=65 2x,
18
- ${buildUrl(image)}&fit=${fit}&f=${focus}&fm=webp&q=80 1x`"
19
- >
20
- <!-- browser doesn't support webp -->
21
- <source
22
- :key="'fallback-image'+index"
23
- :media="'('+image.media+')'"
24
- :width="image.width ? image.width : null"
25
- :height="image.height ? image.height : null"
26
- :srcset="`
27
- ${buildUrl(image, 2)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=65 2x,
28
- ${buildUrl(image)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=80 1x`"
29
- >
30
- </template>
8
+ <!-- browser supports webp -->
9
+ <source
10
+ v-for="(image, index) in sourceSizes"
11
+ :key="'webp-image'+index"
12
+ :media="'('+image.media+')'"
13
+ type="image/webp"
14
+ :width="image.width ? image.width : null"
15
+ :height="image.height ? image.height : null"
16
+ :srcset="`
17
+ ${buildUrl(image, 2)}&fit=${fit}&f=${focus}&fm=webp&q=65 2x,
18
+ ${buildUrl(image)}&fit=${fit}&f=${focus}&fm=webp&q=80 1x`"
19
+ >
20
+ <!-- browser doesn't support webp -->
21
+ <source
22
+ v-for="(image, index) in sourceSizes"
23
+ :key="'fallback-image'+index"
24
+ :media="'('+image.media+')'"
25
+ :width="image.width ? image.width : null"
26
+ :height="image.height ? image.height : null"
27
+ :srcset="`
28
+ ${buildUrl(image, 2)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=65 2x,
29
+ ${buildUrl(image)}&fit=${fit}&f=${focus}&fm=${fallbackFormat}&q=80 1x`"
30
+ >
31
31
  <!-- browser doesn't support picture element -->
32
32
  <img
33
33
  class="tw-max-w-full tw-max-h-full"
@@ -65,6 +65,7 @@
65
65
  </template>
66
66
 
67
67
  <script>
68
+ import { toRefs } from 'vue-demi';
68
69
  // Since it's easy for marketing or other to upload massive images to contentful,
69
70
  // in order to be performant respectful of our users data plans, and not damage
70
71
  // our SEO, we shouldn't send the source image directly to our users.
@@ -172,23 +173,33 @@ export default {
172
173
  default: () => [],
173
174
  },
174
175
  },
175
- methods: {
176
- buildUrl(image = null, multiplier = 1) {
177
- let src = image && image.url ? `${image.url}?` : `${this.contentfulSrc}?`;
178
- const width = image ? image.width : this.width;
179
- const height = image ? image.height : this.height;
176
+ setup(props) {
177
+ const {
178
+ contentfulSrc,
179
+ width,
180
+ height,
181
+ } = toRefs(props);
182
+
183
+ const buildUrl = (image = null, multiplier = 1) => {
184
+ let src = image && image.url ? `${image.url}?` : `${contentfulSrc.value}?`;
185
+ const imgWidth = image ? image.width : width.value;
186
+ const imgHeight = image ? image.height : height.value;
180
187
 
181
- if (width) {
182
- src += `w=${width * multiplier}`;
188
+ if (imgWidth) {
189
+ src += `w=${imgWidth * multiplier}`;
183
190
  }
184
- if (width && height) {
191
+ if (imgWidth && imgHeight) {
185
192
  src += '&';
186
193
  }
187
- if (height) {
188
- src += `h=${height * multiplier}`;
194
+ if (imgHeight) {
195
+ src += `h=${imgHeight * multiplier}`;
189
196
  }
190
197
  return src;
191
- },
198
+ };
199
+
200
+ return {
201
+ buildUrl,
202
+ };
192
203
  },
193
204
  };
194
205
  </script>
@@ -19,10 +19,7 @@
19
19
  "
20
20
  @click.stop.prevent="onScreenClick"
21
21
  >
22
- <focus-lock
23
- :disabled="!visible"
24
- :return-focus="true"
25
- >
22
+ <div>
26
23
  <div
27
24
  class="
28
25
  tw-flex
@@ -82,7 +79,7 @@
82
79
  tw-w-6 tw-h-6 tw--m-2
83
80
  hover:tw-text-action-highlight
84
81
  "
85
- @click.stop.prevent="hide"
82
+ @click.stop="hide"
86
83
  >
87
84
  <kv-material-icon
88
85
  class="tw-w-3 tw-h-3"
@@ -121,15 +118,23 @@
121
118
  </div>
122
119
  </div>
123
120
  </div>
124
- </focus-lock>
121
+ </div>
125
122
  </div>
126
123
  </transition>
127
124
  </template>
128
125
 
129
126
  <script>
130
-
127
+ import {
128
+ ref,
129
+ toRefs,
130
+ computed,
131
+ nextTick,
132
+ watch,
133
+ onBeforeUnmount,
134
+ onMounted,
135
+ } from 'vue-demi';
131
136
  import { mdiClose } from '@mdi/js';
132
- import FocusLock from 'vue-focus-lock';
137
+ import { useFocusTrap } from '@vueuse/integrations/useFocusTrap';
133
138
  import { hideOthers as makePageInert } from 'aria-hidden';
134
139
  import { lockScroll, unlockScroll } from '../utils/scrollLock';
135
140
  import { lockPrintSingleEl, unlockPrintSingleEl } from '../utils/printing';
@@ -161,7 +166,6 @@ import KvMaterialIcon from './KvMaterialIcon.vue';
161
166
 
162
167
  export default {
163
168
  components: {
164
- FocusLock,
165
169
  KvMaterialIcon,
166
170
  },
167
171
  props: {
@@ -200,84 +204,119 @@ export default {
200
204
  default: false,
201
205
  },
202
206
  },
203
- data() {
204
- return {
205
- mdiClose,
206
- makePageInert() {}, // reference to aria-hide function
207
- };
208
- },
209
- computed: {
210
- role() {
211
- if (this.variant === 'alert') {
207
+ emits: [
208
+ 'lightbox-closed',
209
+ ],
210
+ setup(props, { emit }) {
211
+ const {
212
+ visible,
213
+ variant,
214
+ preventClose,
215
+ } = toRefs(props);
216
+
217
+ const kvLightbox = ref(null);
218
+ const kvLightboxBody = ref(null);
219
+ const controlsRef = ref(null);
220
+
221
+ const {
222
+ activate: activateFocusTrap,
223
+ deactivate: deactivateFocusTrap,
224
+ } = useFocusTrap(kvLightbox);
225
+
226
+ let makePageInertCallback = null;
227
+ let onKeyUp = null;
228
+
229
+ const role = computed(() => {
230
+ if (variant.value === 'alert') {
212
231
  return 'alertdialog';
213
232
  }
214
233
  return 'dialog';
215
- },
216
- },
217
- watch: {
218
- visible() {
219
- if (this.visible) {
220
- this.show();
221
- } else {
222
- this.hide();
234
+ });
235
+
236
+ const hide = () => {
237
+ // scroll any content inside the lightbox back to top
238
+ if (kvLightbox.value && kvLightboxBody.value) {
239
+ deactivateFocusTrap();
240
+ kvLightboxBody.value.scrollTop = 0;
241
+ unlockPrintSingleEl(kvLightboxBody.value);
223
242
  }
224
- },
225
- },
226
- beforeDestroy() {
227
- this.hide();
228
- },
229
- methods: {
230
- show() {
231
- if (this.visible) {
232
- document.addEventListener('keyup', this.onKeyUp);
243
+ unlockScroll();
244
+ if (makePageInertCallback) {
245
+ makePageInertCallback();
246
+ makePageInertCallback = null;
247
+ }
248
+ document.removeEventListener('keyup', onKeyUp);
249
+
250
+ /**
251
+ * Triggered when the lightbox is closed
252
+ * @event lightbox-closed
253
+ * @type {Event}
254
+ */
255
+ emit('lightbox-closed');
256
+ };
257
+
258
+ onKeyUp = (e) => {
259
+ if (!!e && e.key === 'Escape' && !preventClose.value) {
260
+ hide();
261
+ }
262
+ };
233
263
 
234
- this.$nextTick(() => {
235
- const lightboxBodyRef = this.$refs.kvLightboxBody;
236
- if (lightboxBodyRef) {
237
- this.makePageInert = makePageInert(lightboxBodyRef);
238
- lockPrintSingleEl(lightboxBodyRef);
264
+ const onScreenClick = () => {
265
+ if (!preventClose.value) {
266
+ hide();
267
+ }
268
+ };
269
+
270
+ const show = () => {
271
+ if (visible.value) {
272
+ document.addEventListener('keyup', onKeyUp);
273
+
274
+ nextTick(() => {
275
+ if (kvLightbox.value && kvLightboxBody.value) {
276
+ activateFocusTrap();
277
+ makePageInertCallback = makePageInert(kvLightbox.value);
278
+ lockPrintSingleEl(kvLightboxBody.value);
239
279
  }
240
280
  lockScroll();
241
281
 
242
282
  // alerts should send focus to the first actionable item in the controls
243
- if (this.variant === 'alert') {
244
- const firstControlEl = this.$refs.controlsRef.querySelector('button');
283
+ if (variant.value === 'alert') {
284
+ const firstControlEl = controlsRef.value.querySelector('button');
245
285
  if (firstControlEl) {
246
286
  firstControlEl.focus();
247
287
  }
248
288
  }
249
289
  });
250
290
  }
251
- },
252
- hide() {
253
- // scroll any content inside the lightbox back to top
254
- const lightboxBodyRef = this.$refs.kvLightboxBody;
255
- if (lightboxBodyRef) {
256
- lightboxBodyRef.scrollTop = 0;
257
- unlockPrintSingleEl(lightboxBodyRef);
258
- }
259
- unlockScroll();
260
- if (this.makePageInert) {
261
- this.makePageInert();
262
- }
291
+ };
263
292
 
264
- /**
265
- * Triggered when the lightbox is closed
266
- * @event lightbox-closed
267
- * @type {Event}
268
- */
269
- this.$emit('lightbox-closed');
270
- },
271
- onScreenClick() {
272
- if (!this.preventClose) {
273
- this.hide();
293
+ watch(visible, () => {
294
+ if (visible.value) {
295
+ show();
296
+ } else {
297
+ hide();
274
298
  }
275
- },
276
- onKeyUp(e) {
277
- if (e.key === 'Escape' && !this.preventClose) {
278
- this.hide();
299
+ });
300
+
301
+ onMounted(() => {
302
+ if (visible.value) {
303
+ show();
279
304
  }
280
- },
305
+ });
306
+
307
+ onBeforeUnmount(() => hide());
308
+
309
+ return {
310
+ mdiClose,
311
+ role,
312
+ kvLightbox,
313
+ kvLightboxBody,
314
+ onKeyUp,
315
+ onScreenClick,
316
+ hide,
317
+ show,
318
+ controlsRef,
319
+ };
281
320
  },
282
321
  };
283
322
  </script>
@@ -25,6 +25,13 @@
25
25
  /**
26
26
  * Horizontal progress bar which communicates to the user the progress of a particular process
27
27
  */
28
+ import {
29
+ ref,
30
+ toRefs,
31
+ computed,
32
+ onMounted,
33
+ nextTick,
34
+ } from 'vue-demi';
28
35
 
29
36
  export default {
30
37
  props: {
@@ -59,28 +66,35 @@ export default {
59
66
  required: true,
60
67
  },
61
68
  },
62
- data() {
63
- return {
64
- loaded: false,
65
- };
66
- },
67
- computed: {
68
- percent() {
69
- const percent = ((this.value - this.min) / (this.max - this.min)) * 100;
70
- const rounded = Math.round(percent * 10) / 10; // Keep percents to 1 demical places (12.3%)
69
+ setup(props) {
70
+ const {
71
+ min,
72
+ max,
73
+ value,
74
+ } = toRefs(props);
75
+ const loaded = ref(false);
76
+
77
+ const percent = computed(() => {
78
+ const percentValue = ((value.value - min.value) / (max.value - min.value)) * 100;
79
+ const rounded = Math.round(percentValue * 10) / 10; // Keep percents to 1 decimal places 12.3%
71
80
  const clamped = Math.min(Math.max(rounded, 0), 100); // Always between 0 and 100%
72
81
  return clamped;
73
- },
74
- },
75
- mounted() {
76
- this.$nextTick(() => {
77
- this.animateProgressBar();
78
82
  });
79
- },
80
- methods: {
81
- animateProgressBar() {
82
- this.loaded = true;
83
- },
83
+
84
+ const animateProgressBar = () => {
85
+ loaded.value = true;
86
+ };
87
+
88
+ onMounted(async () => {
89
+ await nextTick();
90
+ animateProgressBar();
91
+ });
92
+
93
+ return {
94
+ loaded,
95
+ percent,
96
+ animateProgressBar,
97
+ };
84
98
  },
85
99
  };
86
100
  </script>